mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-07-27 12:22:43 +00:00
1.6.0 drop 1 (#132)
This commit is contained in:
parent
0594d330be
commit
0a32333019
|
|
@ -1,25 +1,11 @@
|
|||

|
||||
|
||||
- Fix fresh installation defaulting to Fullscreen
|
||||
- Add a "Download to custom path" button to Versions menu
|
||||
- Fix workshop black screen on server plugins category
|
||||
- New logo for hellish ends
|
||||
- Update neoLegacy download link
|
||||
- (soon!) Partnership with BluerNetwork
|
||||
- Skip Intro on keypress or click
|
||||
- Disable skin remodeling for neoLegacy
|
||||
- Download retry support
|
||||
- Bring back the custom titlebar on non-macOS systems, and show on fullscreen regardless of OS
|
||||
- Fix workshop showing extra stuff that don't exist for plugins
|
||||
- Official external DLC support, used for neoLegacy to make the size smaller (700MiB -> 300MiB)
|
||||
- Add logos to credits menu
|
||||
- Partnership with [Minecraft: Moon Edition](https://www.youtube.com/watch?v=6_ZSUjZfBr0)
|
||||
- Parallel downloads and redesigned download overlay notification
|
||||
- A little bit of UI polish
|
||||
- Community Guides for Developer Tools
|
||||
- (BIG!) End partnership with LCELive. And migration to LCEOnline
|
||||
- Workshop dependencies support
|
||||
- Workshop required versions support
|
||||
- New Model Editor developer tool
|
||||
- New fun easter egg! try changing your username to `TOUHOU`
|
||||
- And much much more you should see yourself!
|
||||
- Fix custom path downloading overwriting non-empty directories ([#127](https://github.com/LCE-Hub/LCE-Emerald-Launcher/issues/127))
|
||||
- Add support for version channels in Workshop
|
||||
- Fix LCEOnline Auth on Linux and macOS
|
||||
- Add `unzip` fallback for Workshop items on Linux
|
||||
- Fix critical bug in Workshop that deleted worlds and DLCs (thanks LeGamer!)
|
||||
- Fix random disconnects on Windows
|
||||
- Show multiplayer status on Discord RPC
|
||||
- Fix skipping intro, fullscreen and legacy mode not importing in Settings (thanks Ryuzaki!)
|
||||
- Remove controls menu from Settings
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ pub async fn workshop_install(app: AppHandle, request: WorkshopInstallRequest) -
|
|||
}
|
||||
let bytes = response.bytes().await.map_err(|e| e.to_string())?;
|
||||
fs::write(&zip_tmp, &bytes).map_err(|e| e.to_string())?;
|
||||
|
||||
let dest_dir = if placeholder.is_empty() {
|
||||
instance_dir.clone()
|
||||
} else {
|
||||
|
|
@ -55,35 +54,96 @@ pub async fn workshop_install(app: AppHandle, request: WorkshopInstallRequest) -
|
|||
};
|
||||
|
||||
fs::create_dir_all(&dest_dir).map_err(|e| e.to_string())?;
|
||||
let (extracted_files, extract_ok) = if cfg!(target_os = "linux") {
|
||||
let bsdtar_list = std::process::Command::new("bsdtar")
|
||||
.args(["-tf", zip_tmp.to_str().unwrap()])
|
||||
.output();
|
||||
if let Ok(out) = bsdtar_list {
|
||||
if out.status.success() {
|
||||
let listing = String::from_utf8_lossy(&out.stdout);
|
||||
let files: Vec<String> = listing.lines()
|
||||
.map(|l| l.trim())
|
||||
.filter(|l| !l.is_empty() && !l.ends_with('/'))
|
||||
.map(|l| dest_dir.join(l).to_string_lossy().to_string())
|
||||
.collect();
|
||||
let st = std::process::Command::new("bsdtar")
|
||||
.args(["-xf", zip_tmp.to_str().unwrap(), "-C", dest_dir.to_str().unwrap()])
|
||||
.status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
(files, st.success())
|
||||
} else {
|
||||
(Vec::new(), false)
|
||||
}
|
||||
} else {
|
||||
(Vec::new(), false)
|
||||
}
|
||||
} else {
|
||||
(Vec::new(), false)
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let status = std::process::Command::new("bsdtar")
|
||||
.args(["-xf", zip_tmp.to_str().unwrap(), "-C", dest_dir.to_str().unwrap()])
|
||||
let (extracted_files, extract_ok) = if !extract_ok {
|
||||
let unzip_list = std::process::Command::new("unzip")
|
||||
.args(["-l", zip_tmp.to_str().unwrap()])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !unzip_list.status.success() {
|
||||
let _ = fs::remove_dir_all(&tmp_dir);
|
||||
return Err(format!("Failed to list contents of {}", zip_name));
|
||||
}
|
||||
let listing = String::from_utf8_lossy(&unzip_list.stdout);
|
||||
let files: Vec<String> = listing.lines()
|
||||
.filter_map(|l| {
|
||||
let mut parts = l.trim().split_whitespace();
|
||||
let size_str = parts.next()?;
|
||||
size_str.parse::<u64>().ok()?;
|
||||
parts.next()?;
|
||||
parts.next()?;
|
||||
Some(parts.collect::<Vec<&str>>().join(" "))
|
||||
})
|
||||
.filter(|l| !l.ends_with('/') && !l.contains('*'))
|
||||
.map(|l| dest_dir.join(l).to_string_lossy().to_string())
|
||||
.collect();
|
||||
let st = std::process::Command::new("unzip")
|
||||
.args(["-o", zip_tmp.to_str().unwrap(), "-d", dest_dir.to_str().unwrap()])
|
||||
.status()
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !status.success() {
|
||||
let _ = fs::remove_dir_all(&tmp_dir);
|
||||
return Err(format!("Extraction failed for {}", zip_name));
|
||||
}
|
||||
}
|
||||
(files, st.success())
|
||||
} else {
|
||||
(extracted_files, extract_ok)
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let status = std::process::Command::new("tar")
|
||||
let (extracted_files, extract_ok) = {
|
||||
let st = std::process::Command::new("tar")
|
||||
.args(["-xf", zip_tmp.to_str().unwrap(), "-C", dest_dir.to_str().unwrap()])
|
||||
.status()
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
if !status.success() {
|
||||
let _ = fs::remove_dir_all(&tmp_dir);
|
||||
return Err(format!("Extraction failed for {}", zip_name));
|
||||
}
|
||||
let listing = std::process::Command::new("tar")
|
||||
.args(["-tf", zip_tmp.to_str().unwrap()])
|
||||
.output()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let listing_str = String::from_utf8_lossy(&listing.stdout);
|
||||
let files: Vec<String> = listing_str.lines()
|
||||
.map(|l| l.trim())
|
||||
.filter(|l| !l.is_empty() && !l.ends_with('/'))
|
||||
.map(|l| dest_dir.join(l).to_string_lossy().to_string())
|
||||
.collect();
|
||||
(files, st.status.success())
|
||||
};
|
||||
|
||||
if !extract_ok {
|
||||
let _ = fs::remove_dir_all(&tmp_dir);
|
||||
return Err(format!("Extraction failed for {}", zip_name));
|
||||
}
|
||||
|
||||
let dest_str = dest_dir.to_string_lossy().to_string();
|
||||
if !workshop_files.contains(&dest_str) {
|
||||
workshop_files.push(dest_str.clone());
|
||||
}
|
||||
if !pkg_dirs.contains(&dest_str) {
|
||||
pkg_dirs.push(dest_str);
|
||||
for f in &extracted_files {
|
||||
if !workshop_files.contains(f) {
|
||||
workshop_files.push(f.clone());
|
||||
}
|
||||
if !pkg_dirs.contains(f) {
|
||||
pkg_dirs.push(f.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -116,11 +176,9 @@ pub async fn workshop_uninstall(app: AppHandle, instance_id: String, package_id:
|
|||
.unwrap_or_default();
|
||||
|
||||
if let Some(pkg) = packages.iter().find(|p| p.id == package_id) {
|
||||
for dir in &pkg.dirs {
|
||||
let path = PathBuf::from(dir);
|
||||
if path.is_dir() {
|
||||
let _ = fs::remove_dir_all(&path);
|
||||
} else if path.is_file() {
|
||||
for file in &pkg.dirs {
|
||||
let path = PathBuf::from(file);
|
||||
if path.is_file() {
|
||||
let _ = fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ pub fn load_config_raw(app: AppHandle) -> AppConfig {
|
|||
music_vol: Some(50),
|
||||
sfx_vol: Some(100),
|
||||
legacy_mode: Some(false),
|
||||
skip_intro: Some(false),
|
||||
mangohud_enabled: None,
|
||||
saved_servers: None,
|
||||
extra_launch_args: None,
|
||||
|
|
|
|||
|
|
@ -26,10 +26,12 @@ use commands::workshop;
|
|||
use networking::relay;
|
||||
use networking::stun;
|
||||
use state::{DownloadState, GameState, ProxyGuard};
|
||||
fn webview_deep_link_interceptor() -> impl tauri::plugin::Plugin<tauri::Wry> { tauri::plugin::Builder::<tauri::Wry>::new("emerald-deep-link-interceptor").on_navigation(|webview, url| { if url.scheme() == "emerald" || url.scheme() == "emeraldlauncher" { let _ = webview.app_handle().emit("deep-link", vec![url.to_string()]); false } else { true }}).build()}
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(webview_deep_link_interceptor())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||
let urls: Vec<String> = args
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use tauri::State;
|
||||
use tauri::webview::cookie::time::Duration;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::time::sleep;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use crate::state::ProxyGuard;
|
||||
const PROXY_ADDR: &str = "proxy.mclegacyedition.xyz:2052"; //neo: yeah bro im hardcoding it
|
||||
|
|
@ -10,7 +12,7 @@ async fn read_line(stream: &mut TcpStream) -> Result<String, String> {
|
|||
loop {
|
||||
stream.read_exact(&mut byte).await.map_err(|e| e.to_string())?;
|
||||
if byte[0] == b'\n' { break; }
|
||||
buf.push(byte[0]);
|
||||
if byte[0] != b'\r' { buf.push(byte[0]); }
|
||||
}
|
||||
String::from_utf8(buf).map_err(|e| e.to_string())
|
||||
}
|
||||
|
|
@ -32,18 +34,23 @@ async fn run_host_relay(
|
|||
.map_err(|e| format!("Proxy connect failed: {}", e))?;
|
||||
|
||||
write_line(&mut host_conn, &format!("HOST {} 0", auth_token)).await?;
|
||||
let game_stream = loop {
|
||||
match TcpStream::connect(format!("127.0.0.1:{}", game_port)).await {
|
||||
Ok(s) => break s,
|
||||
Err(_) => {
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => return Err("Cancelled".into()),
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {}
|
||||
let game_stream = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(30),
|
||||
async {
|
||||
loop {
|
||||
match TcpStream::connect(format!("127.0.0.1:{}", game_port)).await {
|
||||
Ok(s) => return Ok::<_, String>(s),
|
||||
Err(_) => {
|
||||
tokio::select! {
|
||||
_ = cancel.cancelled() => return Err("Cancelled".into()),
|
||||
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
).await
|
||||
.map_err(|_| "Timed out waiting for game to start".to_string())??;
|
||||
let client_line = read_line(&mut host_conn).await?;
|
||||
let client_parts: Vec<&str> = client_line.split_whitespace().collect();
|
||||
if client_parts.len() < 2 || client_parts[0] != "CLIENT" {
|
||||
|
|
@ -167,7 +174,9 @@ pub async fn start_host_relay(
|
|||
let session_id = "__host__".to_string();
|
||||
{
|
||||
let mut tokens = proxy_state.cancel_tokens.lock().await;
|
||||
tokens.insert(session_id.clone(), cancel.clone());
|
||||
if let Some(old) = tokens.insert(session_id.clone(), cancel.clone()) {
|
||||
old.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
let result = run_host_relay(&proxy_state, &addr, &auth_token, game_port, cancel).await;
|
||||
|
|
@ -205,11 +214,12 @@ pub async fn start_relay_proxy(
|
|||
#[tauri::command]
|
||||
pub async fn stop_proxy(proxy_state: State<'_, ProxyGuard>, session_id: String) -> Result<(), String> {
|
||||
let mut tokens = proxy_state.cancel_tokens.lock().await;
|
||||
if let Some(token) = tokens.remove(&session_id) {
|
||||
let found = tokens.remove(&session_id);
|
||||
if let Some(token) = found {
|
||||
token.cancel();
|
||||
let mut port = proxy_state.local_port.lock().await;
|
||||
*port = None;
|
||||
}
|
||||
let mut port = proxy_state.local_port.lock().await;
|
||||
*port = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -241,5 +251,18 @@ pub async fn join_game(
|
|||
ip: host_ip,
|
||||
port: host_port,
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(10);
|
||||
loop {
|
||||
if tokio::net::TcpStream::connect(format!("127.0.0.1:{}", host_port)).await.is_ok() {
|
||||
break;
|
||||
}
|
||||
if tokio::time::Instant::now() >= deadline {
|
||||
return Err("Timed out waiting for relay proxy".into());
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
}
|
||||
} //neo: workaround for Windows having a race condition where the game is launched before the relay proxy
|
||||
crate::commands::game::launch_game(app, game_state, instance_id, vec![server], vec![]).await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ pub struct AppConfig {
|
|||
pub music_vol: Option<u32>,
|
||||
pub sfx_vol: Option<u32>,
|
||||
pub legacy_mode: Option<bool>,
|
||||
pub skip_intro: Option<bool>,
|
||||
pub mangohud_enabled: Option<bool>,
|
||||
pub saved_servers: Option<Vec<McServer>>,
|
||||
pub extra_launch_args: Option<Vec<String>>,
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ export default function ModelEditorView() {
|
|||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (document.activeElement?.tagName === "INPUT") return;
|
||||
if (e.key === "Escape" || e.key === "Backspace") {
|
||||
if (e.key === "Escape") {
|
||||
playBackSound();
|
||||
setActiveView("devtools");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,7 +128,13 @@ export default function PckEditorView() {
|
|||
setExpandedFolders(next);
|
||||
};
|
||||
|
||||
type TreeNode = { name: string; path: string; isFolder: boolean; children: TreeNode[]; asset?: PCKAsset };
|
||||
type TreeNode = {
|
||||
name: string;
|
||||
path: string;
|
||||
isFolder: boolean;
|
||||
children: TreeNode[];
|
||||
asset?: PCKAsset;
|
||||
};
|
||||
|
||||
const renderTree = (nodes: TreeNode[], depth = 0) => {
|
||||
return nodes.map((node) => {
|
||||
|
|
@ -448,7 +454,7 @@ export default function PckEditorView() {
|
|||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (document.activeElement?.tagName === "INPUT") return;
|
||||
if (e.key === "Escape" || e.key === "Backspace") {
|
||||
if (e.key === "Escape") {
|
||||
if (isEditingProperty) {
|
||||
setIsEditingProperty(null);
|
||||
return;
|
||||
|
|
@ -768,18 +774,14 @@ export default function PckEditorView() {
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 shrink-0">
|
||||
<button
|
||||
onClick={() => handleMoveAsset("up")}
|
||||
>
|
||||
<button onClick={() => handleMoveAsset("up")}>
|
||||
<img
|
||||
src="/images/Settings_Arrow_Up.png"
|
||||
className="w-4 h-4 object-contain"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleMoveAsset("down")}
|
||||
>
|
||||
<button onClick={() => handleMoveAsset("down")}>
|
||||
<img
|
||||
src="/images/Settings_Arrow_Down.png"
|
||||
className="w-4 h-4 object-contain"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ const SettingsView = memo(function SettingsView() {
|
|||
sfxVol: sfxVolume,
|
||||
setSfxVol: setSfxVolume,
|
||||
layout,
|
||||
setLayout,
|
||||
linuxRunner,
|
||||
setLinuxRunner,
|
||||
perfBoost,
|
||||
|
|
@ -62,7 +61,7 @@ const SettingsView = memo(function SettingsView() {
|
|||
const { isLinux, isMac } = usePlatform();
|
||||
const [focusIndex, setFocusIndex] = useState<number | null>(null);
|
||||
const [currentSubMenu, setCurrentSubMenu] = useState<
|
||||
"main" | "audio" | "video" | "controls" | "launcher" | "game" | "plugins"
|
||||
"main" | "audio" | "video" | "launcher" | "game" | "plugins"
|
||||
>("main");
|
||||
const [runners, setRunners] = useState<Runner[]>([]);
|
||||
const [pluginsInfo, setPluginsInfo] = useState<PluginInfo[]>([]);
|
||||
|
|
@ -74,9 +73,6 @@ const SettingsView = memo(function SettingsView() {
|
|||
const [showModal, setShowModal] = useState<
|
||||
"args" | "prefix" | "envVars" | null
|
||||
>(null);
|
||||
|
||||
const layouts = ["KBM", "PLAYSTATION", "XBOX"];
|
||||
|
||||
useEffect(() => {
|
||||
TauriService.getAvailableRunners().then(setRunners);
|
||||
}, [isRunnerDownloading]);
|
||||
|
|
@ -91,13 +87,6 @@ const SettingsView = memo(function SettingsView() {
|
|||
return () => PluginManager.instance.setEnabledChangedCallback(null!);
|
||||
}, [refreshPlugins]);
|
||||
|
||||
const handleLayoutToggle = () => {
|
||||
playPressSound();
|
||||
const currentIndex = layouts.indexOf(layout);
|
||||
const nextIndex = (currentIndex + 1) % layouts.length;
|
||||
setLayout(layouts[nextIndex]);
|
||||
};
|
||||
|
||||
const handleVfxToggle = () => {
|
||||
playPressSound();
|
||||
setVfxEnabled(!vfxEnabled);
|
||||
|
|
@ -302,16 +291,6 @@ const SettingsView = memo(function SettingsView() {
|
|||
setFocusIndex(0);
|
||||
},
|
||||
});
|
||||
items.push({
|
||||
id: "controls_menu",
|
||||
label: "Controls",
|
||||
type: "button",
|
||||
onClick: () => {
|
||||
playPressSound();
|
||||
setCurrentSubMenu("controls");
|
||||
setFocusIndex(0);
|
||||
},
|
||||
});
|
||||
items.push({
|
||||
id: "launcher_menu",
|
||||
label: "Launcher",
|
||||
|
|
@ -395,13 +374,6 @@ const SettingsView = memo(function SettingsView() {
|
|||
onClick: handlePerfToggle,
|
||||
});
|
||||
}
|
||||
} else if (currentSubMenu === "controls") {
|
||||
items.push({
|
||||
id: "layout",
|
||||
label: `Layout: ${layout}`,
|
||||
type: "button",
|
||||
onClick: handleLayoutToggle,
|
||||
});
|
||||
} else if (currentSubMenu === "game") {
|
||||
const envVarsCount = launchEnvVars
|
||||
? Object.keys(launchEnvVars).length
|
||||
|
|
@ -521,6 +493,7 @@ const SettingsView = memo(function SettingsView() {
|
|||
playPressSound();
|
||||
try {
|
||||
await TauriService.importSettings();
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
if (e !== "CANCELED") console.error(e);
|
||||
}
|
||||
|
|
@ -585,7 +558,6 @@ const SettingsView = memo(function SettingsView() {
|
|||
handleRpcToggle,
|
||||
handleLegacyToggle,
|
||||
handleAnimationsToggle,
|
||||
handleLayoutToggle,
|
||||
handleRunnerToggle,
|
||||
handlePerfToggle,
|
||||
handleMangohudToggle,
|
||||
|
|
@ -712,13 +684,11 @@ const SettingsView = memo(function SettingsView() {
|
|||
? "Audio"
|
||||
: currentSubMenu === "video"
|
||||
? "Video"
|
||||
: currentSubMenu === "controls"
|
||||
? "Controls"
|
||||
: currentSubMenu === "game"
|
||||
? "Game"
|
||||
: currentSubMenu === "plugins"
|
||||
? "Plugins"
|
||||
: "Launcher"}
|
||||
: currentSubMenu === "game"
|
||||
? "Game"
|
||||
: currentSubMenu === "plugins"
|
||||
? "Plugins"
|
||||
: "Launcher"}
|
||||
</h2>
|
||||
|
||||
{currentSubMenu === "main" ? (
|
||||
|
|
|
|||
|
|
@ -1940,16 +1940,24 @@ function InstallModal({
|
|||
isPluginTab?: boolean;
|
||||
}) {
|
||||
const game = useContext(GameContext);
|
||||
const allInstalled =
|
||||
game?.editions.filter((e) => game.installs.includes(e.id)) || [];
|
||||
const availableEditions =
|
||||
const matchingEditions =
|
||||
pkg.required_versions && pkg.required_versions.length > 0
|
||||
? allInstalled.filter((e) =>
|
||||
? (game?.editions || []).filter((e) =>
|
||||
pkg.required_versions!.some(
|
||||
(rv) => e.id === rv || e.id.startsWith(rv + "_"),
|
||||
),
|
||||
)
|
||||
: allInstalled;
|
||||
: game?.editions || [];
|
||||
const editionOptions = matchingEditions.flatMap((ed) => {
|
||||
const branches =
|
||||
ed.branches && ed.branches.length > 0 ? ed.branches : ["Stable"];
|
||||
return branches
|
||||
.map((branch) => ({
|
||||
label: branches.length > 1 ? `${ed.name} (${branch})` : ed.name,
|
||||
instanceId: branch === "Stable" ? ed.id : `${ed.id}_${branch}`,
|
||||
}))
|
||||
.filter((opt) => game?.installs.includes(opt.instanceId));
|
||||
});
|
||||
const [focusedIdx, setFocusedIdx] = useState(0);
|
||||
const [status, setStatus] = useState<
|
||||
"idle" | "installing" | "success" | "error"
|
||||
|
|
@ -2040,16 +2048,16 @@ function InstallModal({
|
|||
} else if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
playPressSound();
|
||||
setFocusedIdx((p) => Math.min(p + 1, availableEditions.length - 1));
|
||||
setFocusedIdx((p) => Math.min(p + 1, editionOptions.length - 1));
|
||||
} else if (e.key === "Enter") {
|
||||
if (availableEditions.length > 0) {
|
||||
installTo(availableEditions[focusedIdx].id);
|
||||
if (editionOptions.length > 0) {
|
||||
installTo(editionOptions[focusedIdx].instanceId);
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [availableEditions, focusedIdx, status, onClose, playPressSound]);
|
||||
}, [editionOptions, focusedIdx, status, onClose, playPressSound]);
|
||||
|
||||
const installDeps = async (instanceId: string) => {
|
||||
for (const depId of dependencies) {
|
||||
|
|
@ -2216,24 +2224,24 @@ function InstallModal({
|
|||
|
||||
{status === "idle" &&
|
||||
!isPluginTab &&
|
||||
(availableEditions.length === 0 ? (
|
||||
(editionOptions.length === 0 ? (
|
||||
<div className="py-6 flex items-center justify-center">
|
||||
<span className="text-[#FF5555] mc-text-shadow">
|
||||
No installed editions found
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
availableEditions.map((ed, i) => (
|
||||
editionOptions.map((opt, i) => (
|
||||
<div
|
||||
key={ed.id}
|
||||
onClick={() => installTo(ed.id)}
|
||||
key={opt.instanceId}
|
||||
onClick={() => installTo(opt.instanceId)}
|
||||
onMouseEnter={() => setFocusedIdx(i)}
|
||||
className={`flex flex-col p-3 cursor-pointer border-2 transition-none ${focusedIdx === i ? "border-[#FFFF55] bg-black/40" : "border-[#444] bg-black/20"}`}
|
||||
>
|
||||
<span
|
||||
className={`text-lg mc-text-shadow ${focusedIdx === i ? "text-[#FFFF55]" : "text-white"}`}
|
||||
>
|
||||
{ed.name}
|
||||
{opt.label}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ export const SPLASHES = [
|
|||
"I don't know what woke means can someone tell me",
|
||||
"I HATE WHEN PEOPLE USE UPPERCASE LETTERS",
|
||||
"Erm.... He's right behind me, isn't he?",
|
||||
"What does \"the\" even mean man",
|
||||
'What does "the" even mean man',
|
||||
"Tip: Bigger, longer and uncut.",
|
||||
"This is the longest sentence ever",
|
||||
"This is the shortest sentence scientifically possible, according to me.",
|
||||
|
|
@ -231,4 +231,5 @@ export const SPLASHES = [
|
|||
"Edgy humor is soo edgy",
|
||||
"I LOVE YURI!!!!!!! oh and yaoi ig",
|
||||
"RIP PrismaChunk0's Dog 2010-2026",
|
||||
];
|
||||
"Guess who's back, back again!",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@ export function useDiscordRPC({
|
|||
: `Logged in as ${username}`;
|
||||
|
||||
if (isGameRunning) {
|
||||
details = `Playing ${versionName}`;
|
||||
details =
|
||||
activeView === "lceonline"
|
||||
? `Playing ${versionName} Multiplayer`
|
||||
: `Playing ${versionName}`;
|
||||
} else if (downloadingIds.length > 0) {
|
||||
const firstId = downloadingIds[0];
|
||||
const pct = downloadProgress[firstId];
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const BASE_EDITIONS = [
|
|||
id: "legacy_evolved",
|
||||
name: "neoLegacy",
|
||||
desc: "Backporting newer title updates and Minigames back to LCE",
|
||||
url: "https://git.neolegacy.dev/neoStudiosLCE/neoLegacy/releases/download/1.0.8b/neoLegacyWindows64.zip",
|
||||
url: "https://bucket.ibatv.xyz/neolegacy/Release.zip",
|
||||
titleImage: "/images/minecraft_title_neoLegacy.png",
|
||||
supportsSlimSkins: true,
|
||||
logo: "/images/neoLegacy.png",
|
||||
|
|
@ -500,7 +500,8 @@ export function useGameManager({
|
|||
? (extraLaunchArgs ?? []).concat([
|
||||
"-token",
|
||||
localStorage.getItem("lceonline_session")
|
||||
? JSON.parse(localStorage.getItem("lceonline_session")!).accessToken
|
||||
? JSON.parse(localStorage.getItem("lceonline_session")!)
|
||||
.accessToken
|
||||
: "",
|
||||
])
|
||||
: extraLaunchArgs,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
import { useState, useEffect, useRef } from "react";
|
||||
import { lceOnlineService } from "../services/LceOnlineService";
|
||||
export function useLceOnlineNotifications() {
|
||||
const [friendRequestMessage, setFriendRequestMessage] = useState<string | null>(null);
|
||||
const [friendRequestMessage, setFriendRequestMessage] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [InviteMessage, setInviteMessage] = useState<string | null>(null);
|
||||
const [invites, setInvites] = useState<Array<{ inviteid: string; from: { uuid: string; username: string; }; sessionid: string; }>>([]);
|
||||
const [invites, setInvites] = useState<
|
||||
Array<{
|
||||
inviteid: string;
|
||||
from: { uuid: string; username: string };
|
||||
sessionid: string;
|
||||
}>
|
||||
>([]);
|
||||
const seenRequests = useRef<Set<string>>(new Set());
|
||||
const seenInvites = useRef<Set<string>>(new Set());
|
||||
useEffect(() => {
|
||||
|
|
@ -24,12 +32,12 @@ export function useLceOnlineNotifications() {
|
|||
const invitesData = await lceOnlineService.getInvites();
|
||||
setInvites(invitesData);
|
||||
invitesData.forEach((i) => {
|
||||
if (!seenInvites.current.has(i.inviteid)) {
|
||||
seenInvites.current.add(i.inviteid);
|
||||
setInviteMessage(`${i.from.username} invited you to play!`);
|
||||
}
|
||||
});
|
||||
} catch { }
|
||||
if (!seenInvites.current.has(i.inviteid)) {
|
||||
seenInvites.current.add(i.inviteid);
|
||||
setInviteMessage(`${i.from.username} invited you to play!`);
|
||||
}
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
|
|
@ -39,7 +47,6 @@ export function useLceOnlineNotifications() {
|
|||
lists.requests.forEach((r: string) => {
|
||||
if (!seenRequests.current.has(r)) {
|
||||
seenRequests.current.add(r);
|
||||
setFriendRequestMessage(`${r} wants to be friends!`);
|
||||
}
|
||||
});
|
||||
} catch (e) {}
|
||||
|
|
@ -47,12 +54,12 @@ export function useLceOnlineNotifications() {
|
|||
const invitesData = await lceOnlineService.getInvites();
|
||||
setInvites(invitesData);
|
||||
invitesData.forEach((i) => {
|
||||
if (!seenInvites.current.has(i.inviteid)) {
|
||||
seenInvites.current.add(i.inviteid);
|
||||
setInviteMessage(`${i.from.username} invited you to play!`);
|
||||
}
|
||||
});
|
||||
} catch { }
|
||||
if (!seenInvites.current.has(i.inviteid)) {
|
||||
seenInvites.current.add(i.inviteid);
|
||||
setInviteMessage(`${i.from.username} invited you to play!`);
|
||||
}
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
pollInterval = setInterval(poll, 3000);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue