feat: GNU/Linux support and more (#10)
Co-authored-by: Santiago Fisela <[email protected]>
@@ -0,0 +1,60 @@
|
|||||||
|
name: 'publish'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-tauri:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: 'macos-latest'
|
||||||
|
args: '--target aarch64-apple-darwin'
|
||||||
|
- platform: 'macos-latest'
|
||||||
|
args: '--target x86_64-apple-darwin'
|
||||||
|
- platform: 'ubuntu-22.04'
|
||||||
|
args: ''
|
||||||
|
- platform: 'windows-latest'
|
||||||
|
args: ''
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: install dependencies (ubuntu only)
|
||||||
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
|
- name: setup node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: './src-tauri -> target'
|
||||||
|
|
||||||
|
- name: install frontend dependencies
|
||||||
|
run: pnpm install
|
||||||
|
- uses: tauri-apps/tauri-action@v0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tagName: app-v__VERSION__
|
||||||
|
releaseName: 'Emerald v__VERSION__'
|
||||||
|
releaseBody: '*pls edit me*'
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: false
|
||||||
|
args: ${{ matrix.args }}
|
||||||
@@ -24,11 +24,5 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Build output
|
|
||||||
/app-icon.png
|
|
||||||
/src-tauri/icons/*.png
|
|
||||||
/src-tauri/icons/*.ico
|
|
||||||
/src-tauri/icons/*.icns
|
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Building Emerald Launcher
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js (and NPM)
|
||||||
|
- Rust
|
||||||
|
- WebKit2GTK-4.1 (GNU/Linux only)
|
||||||
|
- WebKit (macOS only, preinstalled)
|
||||||
|
- Microsoft Edge WebView2 Runtime (Windows only)
|
||||||
|
- PNPM (optional, but recommended)
|
||||||
|
- Windows, GNU/Linux or a macOS system.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm install # or npm
|
||||||
|
pnpm tauri build # or npm
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
On GNU/Linux, the build might fail due to missing logos (because still Alpha, contributions to fix this are welcome).
|
||||||
|
To fix that, build with this command instead:
|
||||||
|
```sh
|
||||||
|
pnpm tauri build --no-bundle
|
||||||
|
```
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
# Emerald Legacy Launcher
|
<div align="center">
|
||||||
|
<img height="150" src="https://raw.githubusercontent.com/Emerald-Legacy-Launcher/Emerald-Legacy-Launcher/refs/heads/main/public/images/logo.png">
|
||||||
|
<h1>Emerald Legacy Launcher</h1>
|
||||||
|
<p>FOSS cross-platform launcher for Minecraft Legacy Console Edition</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
**A free and open-source Minecraft Legacy Console Edition launcher, built with Rust and Tauri.**
|
<p align="center">
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<p align="left">
|
|
||||||
<a href="https://ko-fi.com/kayjann">
|
<a href="https://ko-fi.com/kayjann">
|
||||||
<img src="https://img.shields.io/badge/ko--fi-Donate%20to%20the%20Project-grey?style=for-the-badge&logo=kofi&logoColor=white&labelColor=FF5E5B" alt="Donate to the Project">
|
<img src="https://img.shields.io/badge/ko--fi-Donate%20to%20the%20Project-grey?style=for-the-badge&logo=kofi&logoColor=white&labelColor=FF5E5B" alt="Donate to the Project">
|
||||||
</a>
|
</a>
|
||||||
@@ -21,10 +21,13 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<img width="1355" height="922" alt="image" src="https://github.com/user-attachments/assets/6798dd00-4c7c-4b57-9116-7648ce90ae93" />
|
||||||
|
|
||||||
|
|
||||||
### PROJECT OVERVIEW
|
### 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.
|
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.
|
**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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -68,8 +71,4 @@ Click the heading above to track progress, view active tasks, and see upcoming l
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
***This project is licensed under the [GNU GPL v3 License](LICENSE)***
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
***This project is licensed under the GNU GPL v3 License - see the LICENSE file for details.***
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 987 B After Width: | Height: | Size: 1014 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 7.5 KiB |
@@ -7,7 +7,23 @@ use futures_util::StreamExt;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tauri_plugin_opener::OpenerExt;
|
use tauri_plugin_opener::OpenerExt;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AppConfig {
|
||||||
|
pub username: String,
|
||||||
|
pub linux_runner: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct Runner {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub path: String,
|
||||||
|
pub r#type: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct DownloadState { pub token: Arc<Mutex<Option<CancellationToken>>> }
|
pub struct DownloadState { pub token: Arc<Mutex<Option<CancellationToken>>> }
|
||||||
|
|
||||||
@@ -17,17 +33,78 @@ fn get_app_dir(app: &AppHandle) -> PathBuf {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
fn get_config_path(app: &AppHandle) -> PathBuf {
|
||||||
fn save_config(app: AppHandle, username: String) {
|
get_app_dir(app).join("emerald_legacy_config.json")
|
||||||
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]
|
#[tauri::command]
|
||||||
fn load_config(app: AppHandle) -> String {
|
fn save_config(app: AppHandle, config: AppConfig) {
|
||||||
let path = get_app_dir(&app).join("emerald_legacy_config.txt");
|
let path = get_config_path(&app);
|
||||||
fs::read_to_string(path).unwrap_or_default()
|
let _ = fs::create_dir_all(path.parent().unwrap());
|
||||||
|
if let Ok(json) = serde_json::to_string(&config) {
|
||||||
|
let _ = fs::write(path, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn load_config(app: AppHandle) -> AppConfig {
|
||||||
|
let path = get_config_path(&app);
|
||||||
|
if let Ok(content) = fs::read_to_string(path) {
|
||||||
|
if let Ok(config) = serde_json::from_str(&content) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let old_path = get_app_dir(&app).join("emerald_legacy_config.txt");
|
||||||
|
let username = fs::read_to_string(old_path).unwrap_or_else(|_| "Player".into());
|
||||||
|
AppConfig { username, linux_runner: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_available_runners() -> Vec<Runner> {
|
||||||
|
let mut runners = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if let Ok(output) = Command::new("which").arg("wine").output() {
|
||||||
|
if output.status.success() {
|
||||||
|
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
runners.push(Runner {
|
||||||
|
id: "wine".to_string(),
|
||||||
|
name: "System Wine".to_string(),
|
||||||
|
path,
|
||||||
|
r#type: "wine".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let home = std::env::var("HOME").unwrap_or_default();
|
||||||
|
let steam_paths = [
|
||||||
|
format!("{}/.steam/steam/steamapps/common", home),
|
||||||
|
format!("{}/.local/share/Steam/steamapps/common", home),
|
||||||
|
];
|
||||||
|
|
||||||
|
for base_path in steam_paths {
|
||||||
|
if let Ok(entries) = fs::read_dir(base_path) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
if name.starts_with("Proton") {
|
||||||
|
runners.push(Runner {
|
||||||
|
id: format!("proton_{}", name),
|
||||||
|
name: name,
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
r#type: "proton".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runners
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -89,6 +166,13 @@ async fn download_and_install(app: AppHandle, state: State<'_, DownloadState>, u
|
|||||||
drop(file);
|
drop(file);
|
||||||
{ *state.token.lock().await = None; }
|
{ *state.token.lock().await = None; }
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let status = Command::new("unzip")
|
||||||
|
.args(["-q", zip_path.to_str().unwrap(), "-d", instance_dir.to_str().unwrap()])
|
||||||
|
.status()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
let status = Command::new("tar")
|
let status = Command::new("tar")
|
||||||
.args(["-xf", zip_path.to_str().unwrap(), "-C", instance_dir.to_str().unwrap()])
|
.args(["-xf", zip_path.to_str().unwrap(), "-C", instance_dir.to_str().unwrap()])
|
||||||
.status()
|
.status()
|
||||||
@@ -125,8 +209,8 @@ async fn download_and_install(app: AppHandle, state: State<'_, DownloadState>, u
|
|||||||
async fn launch_game(app: AppHandle, instanceId: String) -> Result<(), String> {
|
async fn launch_game(app: AppHandle, instanceId: String) -> Result<(), String> {
|
||||||
let root = get_app_dir(&app);
|
let root = get_app_dir(&app);
|
||||||
let instance_dir = root.join("instances").join(&instanceId);
|
let instance_dir = root.join("instances").join(&instanceId);
|
||||||
let config_path = root.join("emerald_legacy_config.txt");
|
let config = load_config(app.clone());
|
||||||
let username = fs::read_to_string(config_path).unwrap_or_else(|_| "Player".into());
|
let username = config.username;
|
||||||
|
|
||||||
let _ = fs::write(instance_dir.join("username.txt"), &username);
|
let _ = fs::write(instance_dir.join("username.txt"), &username);
|
||||||
|
|
||||||
@@ -135,7 +219,38 @@ async fn launch_game(app: AppHandle, instanceId: String) -> Result<(), String> {
|
|||||||
return Err("Game executable not found in instance folder.".into());
|
return Err("Game executable not found in instance folder.".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = Command::new(&game_exe).spawn().map_err(|e| e.to_string())?;
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if let Some(runner_id) = config.linux_runner {
|
||||||
|
let runners = get_available_runners();
|
||||||
|
if let Some(runner) = runners.into_iter().find(|r| r.id == runner_id) {
|
||||||
|
let mut cmd = if runner.r#type == "proton" {
|
||||||
|
let proton_exe = PathBuf::from(&runner.path).join("proton");
|
||||||
|
let mut c = Command::new(proton_exe);
|
||||||
|
let compat_data = instance_dir.join("proton_prefix");
|
||||||
|
fs::create_dir_all(&compat_data).map_err(|e| e.to_string())?;
|
||||||
|
c.env("STEAM_COMPAT_CLIENT_INSTALL_PATH", "");
|
||||||
|
c.env("STEAM_COMPAT_DATA_PATH", compat_data.to_str().unwrap());
|
||||||
|
c.arg("run");
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
Command::new(runner.path)
|
||||||
|
};
|
||||||
|
|
||||||
|
cmd.arg(&game_exe)
|
||||||
|
.current_dir(&instance_dir)
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Err("No Linux runner selected in settings.".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
let _ = Command::new(&game_exe).spawn().map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -144,7 +259,7 @@ pub fn run() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.manage(DownloadState { token: Arc::new(Mutex::new(None)) })
|
.manage(DownloadState { token: Arc::new(Mutex::new(None)) })
|
||||||
.plugin(tauri_plugin_opener::init())
|
.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])
|
.invoke_handler(tauri::generate_handler![launch_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error");
|
.expect("error");
|
||||||
}
|
}
|
||||||
@@ -1,38 +1,210 @@
|
|||||||
@font-face { font-family: 'Minecraft'; src: url('/fonts/Mojangles.ttf') format('truetype'); }
|
@font-face {
|
||||||
@tailwind base; @tailwind components; @tailwind utilities;
|
font-family: 'Minecraft';
|
||||||
|
src: url('/fonts/Mojangles.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
* { border-radius: 0 !important; font-family: 'Minecraft', sans-serif !important; image-rendering: pixelated; font-weight: normal !important; }
|
@tailwind base;
|
||||||
body { background: #000; margin: 0; }
|
@tailwind components;
|
||||||
main { background: url('/images/dirt-background.png') repeat !important; background-size: 2000px !important; position: relative; }
|
@tailwind utilities;
|
||||||
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; }
|
border-radius: 0 !important;
|
||||||
|
font-family: 'Minecraft', sans-serif !important;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
font-weight: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
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 {
|
.legacy-btn {
|
||||||
@apply transition-all duration-75 flex items-center justify-center;
|
@apply transition-all duration-75 flex items-center justify-center;
|
||||||
background: #bebebe; border: 4px solid #000 !important;
|
background: #bebebe;
|
||||||
|
border: 4px solid #000 !important;
|
||||||
box-shadow: inset 4px 4px #ffffff, inset -4px -4px #555555 !important;
|
box-shadow: inset 4px 4px #ffffff, inset -4px -4px #555555 !important;
|
||||||
color: #3e3e3e !important; cursor: pointer;
|
color: #3e3e3e !important;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legacy-btn:hover:not(:disabled) { background: #d0d0d0; outline: 4px solid #fff; outline-offset: -8px; }
|
.legacy-btn:hover:not(:disabled) {
|
||||||
.legacy-btn:active:not(:disabled) { box-shadow: inset -4px -4px #ffffff, inset 4px 4px #555555 !important; }
|
background: #d0d0d0;
|
||||||
.legacy-btn:disabled { opacity: 0.6; cursor: not-allowed; filter: grayscale(1); }
|
outline: 4px solid #fff;
|
||||||
|
outline-offset: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
.active-tab { background: #388e3c !important; box-shadow: inset 4px 4px #55ff55, inset -4px -4px #1a4d0a !important; color: #fff !important; }
|
.legacy-btn:active:not(:disabled) {
|
||||||
.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; }
|
box-shadow: inset -4px -4px #ffffff, inset 4px 4px #555555 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.mc-range { -webkit-appearance: none; background: #000; height: 12px; border: 3px solid #555; width: 100%; }
|
.legacy-btn:disabled {
|
||||||
.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; }
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
.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; }
|
.legacy-select {
|
||||||
.social-btn:hover { transform: scale(1.1); }
|
appearance: none;
|
||||||
.social-btn svg { fill: white; width: 32px; height: 32px; }
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background: #bebebe url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%233e3e3e' stroke-width='3' stroke-linecap='square' stroke-linejoin='miter'%3E%3Cpath d='M7 10l5 5 5-5'/%3E%3C/svg%3E") no-repeat right 12px center;
|
||||||
|
border: 4px solid #000 !important;
|
||||||
|
box-shadow: inset 4px 4px #ffffff, inset -4px -4px #555555 !important;
|
||||||
|
color: #3e3e3e !important;
|
||||||
|
cursor: pointer;
|
||||||
|
padding-right: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-discord { background: #5865F2 !important; box-shadow: inset 4px 4px #8ea1e1, inset -4px -4px #313338 !important; }
|
.legacy-select:focus {
|
||||||
.btn-github { background: #333 !important; box-shadow: inset 4px 4px #666, inset -4px -4px #000 !important; }
|
outline: 4px solid #fff;
|
||||||
.btn-reddit { background: #FF4500 !important; box-shadow: inset 4px 4px #ff8b60, inset -4px -4px #942700 !important; }
|
outline-offset: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
.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; }
|
.sidebar-progress {
|
||||||
@keyframes splash-pulse { from { transform: rotate(-20deg) scale(1); } to { transform: rotate(-20deg) scale(1.05); } }
|
background: #2a2a2a;
|
||||||
|
border-top: 4px solid #000;
|
||||||
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: inset 0 4px #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-progress-container {
|
||||||
|
background: #313131 !important;
|
||||||
|
border: 4px solid #000 !important;
|
||||||
|
height: 32px !important;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-progress-bar {
|
||||||
|
background: #55FF55 !important;
|
||||||
|
height: 16px !important;
|
||||||
|
box-shadow: inset -2px -2px #00AA00, inset 2px 2px #AAFFAA !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mc-progress-text {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 2px 2px #000;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: 10;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,14 +5,27 @@ import { useState, useEffect, useRef } from "react";
|
|||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
const Icons = {
|
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>,
|
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>,
|
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>,
|
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 }) => (
|
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>
|
<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>
|
||||||
)
|
),
|
||||||
|
Linux: () => <svg width="32" height="32" viewBox="0 0 32 32" fill="currentColor"><path d="M16 4c-3.3 0-6 2.7-6 6 0 1.2.4 2.3 1 3.2C8.7 15.1 7 18.3 7 22h2c0-3.9 3.1-7 7-7s7 3.1 7 7h2c0-3.7-1.7-6.9-4-8.8.6-.9 1-2 1-3.2 0-3.3-2.7-6-6-6zm0 2c2.2 0 4 1.8 4 4s-1.8 4-4 4-4-1.8-4-4 1.8-4 4-4z" /></svg>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Runner {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppConfig {
|
||||||
|
username: string;
|
||||||
|
linuxRunner?: string;
|
||||||
|
}
|
||||||
|
|
||||||
let audioCtx: AudioContext | null = null;
|
let audioCtx: AudioContext | null = null;
|
||||||
let sfxGain: GainNode | null = null;
|
let sfxGain: GainNode | null = null;
|
||||||
const buffers: Record<string, AudioBuffer> = {};
|
const buffers: Record<string, AudioBuffer> = {};
|
||||||
@@ -23,16 +36,19 @@ export default function App() {
|
|||||||
const [isFirstRun, setIsFirstRun] = useState(true);
|
const [isFirstRun, setIsFirstRun] = useState(true);
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
const [installingInstance, setInstallingInstance] = useState<string | null>(null);
|
const [installingInstance, setInstallingInstance] = useState<string | null>(null);
|
||||||
const [downloadProgress, setDownloadProgress] = useState<number>(0);
|
const [downloadProgress, setDownloadProgress] = useState<number>(0);
|
||||||
const [installedStatus, setInstalledStatus] = useState<Record<string, boolean>>({ vanilla_tu19: false, vanilla_tu24: false });
|
const [installedStatus, setInstalledStatus] = useState<Record<string, boolean>>({ vanilla_tu19: false, vanilla_tu24: false });
|
||||||
const [selectedInstance, setSelectedInstance] = useState<string>("vanilla_tu19");
|
const [selectedInstance, setSelectedInstance] = useState<string>("vanilla_tu19");
|
||||||
const [reinstallModal, setReinstallModal] = useState<{ id: string, url: string } | null>(null);
|
const [reinstallModal, setReinstallModal] = useState<{ id: string, url: string } | null>(null);
|
||||||
const [mcNotif, setMcNotif] = useState<{t: string, m: string} | null>(null);
|
const [mcNotif, setMcNotif] = useState<{ t: string, m: string } | null>(null);
|
||||||
|
const [availableRunners, setAvailableRunners] = useState<Runner[]>([]);
|
||||||
|
const [selectedRunner, setSelectedRunner] = useState<string>("");
|
||||||
|
const [isLinux, setIsLinux] = useState(false);
|
||||||
|
|
||||||
const [musicVol, setMusicVol] = useState(parseFloat(localStorage.getItem("musicVol") || "0.4"));
|
const [musicVol, setMusicVol] = useState(parseFloat(localStorage.getItem("musicVol") || "0.4"));
|
||||||
const [sfxVol, setSfxVol] = useState(parseFloat(localStorage.getItem("sfxVol") || "0.7"));
|
const [sfxVol, setSfxVol] = useState(parseFloat(localStorage.getItem("sfxVol") || "0.7"));
|
||||||
const [isMuted, setIsMuted] = useState(localStorage.getItem("isMuted") === "true");
|
const [isMuted, setIsMuted] = useState(localStorage.getItem("isMuted") === "true");
|
||||||
|
|
||||||
const musicRef = useRef<HTMLAudioElement | null>(null);
|
const musicRef = useRef<HTMLAudioElement | null>(null);
|
||||||
const lastTrack = useRef<number>(0);
|
const lastTrack = useRef<number>(0);
|
||||||
|
|
||||||
@@ -80,7 +96,7 @@ export default function App() {
|
|||||||
lastTrack.current = track;
|
lastTrack.current = track;
|
||||||
musicRef.current.src = `/music/music${track}.ogg`;
|
musicRef.current.src = `/music/music${track}.ogg`;
|
||||||
musicRef.current.volume = isMuted ? 0 : musicVol;
|
musicRef.current.volume = isMuted ? 0 : musicVol;
|
||||||
musicRef.current.play().catch(() => {});
|
musicRef.current.play().catch(() => { });
|
||||||
};
|
};
|
||||||
|
|
||||||
const fadeAndLaunch = async () => {
|
const fadeAndLaunch = async () => {
|
||||||
@@ -97,17 +113,17 @@ export default function App() {
|
|||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await invoke("launch_game", { instanceId: selectedInstance });
|
await invoke("launch_game", { instanceId: selectedInstance });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(`Failed to launch game: ${e}`);
|
alert(`Failed to launch game: ${e}`);
|
||||||
} finally {
|
} finally {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
if (musicRef.current) {
|
if (musicRef.current) {
|
||||||
musicRef.current.volume = isMuted ? 0 : musicVol;
|
musicRef.current.volume = isMuted ? 0 : musicVol;
|
||||||
playRandomMusic();
|
playRandomMusic();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, 1500);
|
}, 1500);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,7 +134,7 @@ export default function App() {
|
|||||||
setMcNotif({ t: "Success!", m: "Ready to play." }); playSfx('orb.ogg');
|
setMcNotif({ t: "Success!", m: "Ready to play." }); playSfx('orb.ogg');
|
||||||
setTimeout(() => setMcNotif(null), 4000);
|
setTimeout(() => setMcNotif(null), 4000);
|
||||||
updateAllStatus();
|
updateAllStatus();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Error during installation: " + e);
|
alert("Error during installation: " + e);
|
||||||
}
|
}
|
||||||
@@ -132,14 +148,24 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
invoke("load_config").then((n) => {
|
invoke("load_config").then((c) => {
|
||||||
const savedName = n as string;
|
const config = c as AppConfig;
|
||||||
if (savedName && savedName.trim() !== "") {
|
if (config.username && config.username.trim() !== "") {
|
||||||
setUsername(savedName);
|
setUsername(config.username);
|
||||||
setIsFirstRun(false);
|
setIsFirstRun(false);
|
||||||
setTimeout(playRandomMusic, 1000);
|
setTimeout(playRandomMusic, 1000);
|
||||||
}
|
}
|
||||||
|
if (config.linuxRunner) setSelectedRunner(config.linuxRunner);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const platform = window.navigator.platform.toLowerCase();
|
||||||
|
if (platform.includes("linux")) {
|
||||||
|
setIsLinux(true);
|
||||||
|
invoke<Runner[]>("get_available_runners").then(runners => {
|
||||||
|
setAvailableRunners(runners);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
updateAllStatus();
|
updateAllStatus();
|
||||||
const unlisten = listen<number>("download-progress", (e) => setDownloadProgress(Math.round(e.payload)));
|
const unlisten = listen<number>("download-progress", (e) => setDownloadProgress(Math.round(e.payload)));
|
||||||
return () => { unlisten.then(f => f()); };
|
return () => { unlisten.then(f => f()); };
|
||||||
@@ -152,7 +178,7 @@ export default function App() {
|
|||||||
<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]">
|
<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>
|
<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..." />
|
<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>
|
<button onClick={() => { ensureAudio(); playSfx('click.wav'); invoke("save_config", { config: { username, linuxRunner: isLinux ? selectedRunner : undefined } }); setIsFirstRun(false); setTimeout(playRandomMusic, 500); }} disabled={!username.trim() || (isLinux && availableRunners.length === 0)} className="legacy-btn py-4 px-12 text-3xl w-full">Start Setup</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -163,8 +189,7 @@ export default function App() {
|
|||||||
<audio ref={musicRef} onEnded={playRandomMusic} />
|
<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]">
|
<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">
|
<div className="mb-10 px-2">
|
||||||
<h2 className="text-emerald-500 text-3xl italic tracking-tighter">EMERALD LEGACY</h2>
|
<img src="/images/logo.png" />
|
||||||
<p className="text-[10px] text-slate-500 uppercase tracking-widest">Launcher</p>
|
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex flex-col gap-3">
|
<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("home"); updateAllStatus(); }} className={`p-4 legacy-btn justify-start ${activeTab === "home" ? "active-tab" : ""}`}>HOME</button>
|
||||||
@@ -172,7 +197,20 @@ export default function App() {
|
|||||||
<button onClick={() => { playSfx('click.wav'); setActiveTab("settings"); }} className={`p-4 legacy-btn justify-start ${activeTab === "settings" ? "active-tab" : ""}`}>SETTINGS</button>
|
<button onClick={() => { playSfx('click.wav'); setActiveTab("settings"); }} className={`p-4 legacy-btn justify-start ${activeTab === "settings" ? "active-tab" : ""}`}>SETTINGS</button>
|
||||||
</nav>
|
</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">
|
{installingInstance && (
|
||||||
|
<div className="sidebar-progress mt-auto">
|
||||||
|
<div className="flex justify-between mb-3 text-slate-300 font-bold text-[10px] uppercase tracking-widest px-1">
|
||||||
|
<span>Installing</span>
|
||||||
|
<button onClick={() => { playSfx('back.ogg'); invoke("cancel_download"); }} className="text-red-500 hover:underline">CANCEL</button>
|
||||||
|
</div>
|
||||||
|
<div className="mc-progress-container">
|
||||||
|
<div className="mc-progress-bar transition-all duration-300" style={{ width: `${downloadProgress}%` }}></div>
|
||||||
|
<div className="mc-progress-text">{downloadProgress}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div onClick={() => { playSfx('click.wav'); openUrl("https://github.com/KayJannOnGit"); }} className={`${installingInstance ? "pt-6" : "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-slate-500 text-[10px] uppercase">Developed by</span>
|
||||||
<span className="text-emerald-500 text-sm font-bold group-hover:underline">KayJann</span>
|
<span className="text-emerald-500 text-sm font-bold group-hover:underline">KayJann</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,11 +227,11 @@ export default function App() {
|
|||||||
<div className="bg-black/80 p-8 border-4 border-black w-[550px] flex flex-col gap-6 mt-12">
|
<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 ? (
|
{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">
|
<select value={selectedInstance} onChange={e => { playSfx('click.wav'); setSelectedInstance(e.target.value); }} className="w-full legacy-select p-3 text-2xl outline-none">
|
||||||
{installedStatus.vanilla_tu19 && <option value="vanilla_tu19">Vanilla Nightly (TU19)</option>}
|
{installedStatus.vanilla_tu19 && <option value="vanilla_tu19">Vanilla Nightly (TU19)</option>}
|
||||||
{installedStatus.vanilla_tu24 && <option value="vanilla_tu24">Vanilla TU24</option>}
|
{installedStatus.vanilla_tu24 && <option value="vanilla_tu24">Vanilla TU24</option>}
|
||||||
</select>
|
</select>
|
||||||
<button onClick={fadeAndLaunch} disabled={isRunning || !!installingInstance} className="legacy-btn py-4 text-6xl w-full">{installingInstance ? "DOWNLOADING..." : isRunning ? "RUNNING..." : "PLAY"}</button>
|
<button onClick={fadeAndLaunch} disabled={isRunning || !!installingInstance} className="legacy-btn py-4 text-6xl w-full">{installingInstance ? "WAITING..." : isRunning ? "RUNNING..." : "PLAY"}</button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@@ -209,7 +247,7 @@ export default function App() {
|
|||||||
<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">
|
<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>
|
<h2 className="text-5xl mb-8 border-b-4 border-white/20 pb-4">Instances</h2>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
|
|
||||||
{/* TU19 */}
|
{/* TU19 */}
|
||||||
<div className="flex justify-between items-center bg-[#2a2a2a] border-4 border-black p-6">
|
<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><h3 className="text-2xl font-bold">Vanilla Nightly (TU19)</h3><p className="text-slate-400 text-sm">Leaked 4J Studios build.</p></div>
|
||||||
@@ -247,17 +285,6 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -269,19 +296,40 @@ export default function App() {
|
|||||||
<label className="text-xl text-slate-400 italic">In-game Username</label>
|
<label className="text-xl text-slate-400 italic">In-game Username</label>
|
||||||
<div className="flex gap-4">
|
<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" />
|
<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>
|
<button onClick={() => { playSfx('wood click.wav'); invoke("save_config", { config: { username, linuxRunner: selectedRunner || undefined } }); }} className="legacy-btn px-8 text-2xl relative">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isLinux && (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<label className="text-xl text-slate-400 italic flex items-center gap-2"><Icons.Linux /> Linux Runner</label>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<select
|
||||||
|
value={selectedRunner}
|
||||||
|
onChange={e => { playSfx('click.wav'); setSelectedRunner(e.target.value); }}
|
||||||
|
className="w-full legacy-select p-4 text-2xl outline-none focus:border-emerald-500"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select a runner...</option>
|
||||||
|
{availableRunners.map(r => (
|
||||||
|
<option key={r.id} value={r.id}>{r.name} ({r.type})</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{availableRunners.length === 0 && (
|
||||||
|
<p className="text-red-500 text-sm">No Proton or Wine installations found. Please install Steam or Wine.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 bg-[#2a2a2a] p-6 border-4 border-black shadow-[inset_4px_4px_#555]">
|
<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>
|
<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="grid grid-cols-2 gap-8">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="text-sm uppercase opacity-50">Music {Math.round(musicVol*100)}%</span>
|
<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" />
|
<input type="range" min="0" max="1" step="0.01" value={musicVol} onChange={e => setMusicVol(parseFloat(e.target.value))} className="mc-range" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<span className="text-sm uppercase opacity-50">SFX {Math.round(sfxVol*100)}%</span>
|
<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" />
|
<input type="range" min="0" max="1" step="0.01" value={sfxVol} onChange={e => setSfxVol(parseFloat(e.target.value))} className="mc-range" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,17 +337,17 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="about-section border-4 border-black bg-[#2a2a2a] p-6 shadow-[inset_4px_4px_#555]">
|
<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>
|
<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">
|
<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,
|
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.
|
and my goal is to create a central hub for the LCE community to bring us all together.
|
||||||
</p>
|
</p>
|
||||||
<h3 className="text-sm text-slate-500 mb-4 uppercase tracking-widest">Social Links</h3>
|
<h3 className="text-sm text-slate-500 mb-4 uppercase tracking-widest">Social Links</h3>
|
||||||
<div className="flex gap-6">
|
<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://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://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>
|
<button onClick={() => openUrl("https://reddit.com/user/KayJann")} className="social-btn btn-reddit" title="Reddit"><Icons.Reddit /></button>
|
||||||
</div> </div>
|
</div> </div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||