mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-24 17:11:00 +00:00
feat: xbox360+ps3+java->windows64 world conversion (#145)
Co-authored-by: str1k3r <[email protected]>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import { useEffect, useRef, memo } from "react";
|
||||
import * as THREE from "three";
|
||||
interface CapePreviewProps {
|
||||
src: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CapePreview = memo(function CapePreview({
|
||||
src,
|
||||
className,
|
||||
}: CapePreviewProps) {
|
||||
const mountRef = useRef<HTMLDivElement>(null);
|
||||
const groupRef = useRef<THREE.Group | null>(null);
|
||||
useEffect(() => {
|
||||
if (!mountRef.current) return;
|
||||
const width = mountRef.current.clientWidth || 64;
|
||||
const height = mountRef.current.clientHeight || 64;
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 1000);
|
||||
camera.position.set(0, 0, 30);
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
mountRef.current.innerHTML = "";
|
||||
mountRef.current.appendChild(renderer.domElement);
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
|
||||
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 0.6));
|
||||
const dl = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
dl.position.set(10, 20, 10);
|
||||
scene.add(dl);
|
||||
const group = new THREE.Group();
|
||||
group.rotation.y = -0.35;
|
||||
scene.add(group);
|
||||
groupRef.current = group;
|
||||
const render = () => renderer.render(scene, camera);
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
let active = true;
|
||||
textureLoader.load(
|
||||
src,
|
||||
(texture) => {
|
||||
if (!active) return;
|
||||
texture.magFilter = THREE.NearestFilter;
|
||||
texture.minFilter = THREE.NearestFilter;
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
const texW = texture.image.width || 64;
|
||||
const texH = texture.image.height || 32;
|
||||
const createFace = (
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
flipX = false,
|
||||
flipY = false,
|
||||
) => {
|
||||
const matTex = texture.clone();
|
||||
matTex.repeat.set((flipX ? -w : w) / texW, (flipY ? -h : h) / texH);
|
||||
matTex.offset.set(
|
||||
(flipX ? x + w : x) / texW,
|
||||
1 - (flipY ? y : y + h) / texH,
|
||||
);
|
||||
matTex.needsUpdate = true;
|
||||
return new THREE.MeshLambertMaterial({
|
||||
map: matTex,
|
||||
transparent: true,
|
||||
alphaTest: 0.5,
|
||||
side: THREE.FrontSide,
|
||||
});
|
||||
};
|
||||
|
||||
const capeUv = {
|
||||
top: [1, 0, 10, 1],
|
||||
bottom: [11, 0, 10, 1],
|
||||
right: [0, 1, 1, 16],
|
||||
front: [1, 1, 10, 16],
|
||||
left: [11, 1, 1, 16],
|
||||
back: [12, 1, 10, 16],
|
||||
};
|
||||
const geo = new THREE.BoxGeometry(10, 16, 1);
|
||||
const mats = [
|
||||
createFace(
|
||||
capeUv.left[0],
|
||||
capeUv.left[1],
|
||||
capeUv.left[2],
|
||||
capeUv.left[3],
|
||||
),
|
||||
createFace(
|
||||
capeUv.right[0],
|
||||
capeUv.right[1],
|
||||
capeUv.right[2],
|
||||
capeUv.right[3],
|
||||
),
|
||||
createFace(
|
||||
capeUv.top[0],
|
||||
capeUv.top[1],
|
||||
capeUv.top[2],
|
||||
capeUv.top[3],
|
||||
false,
|
||||
true,
|
||||
),
|
||||
createFace(
|
||||
capeUv.bottom[0],
|
||||
capeUv.bottom[1],
|
||||
capeUv.bottom[2],
|
||||
capeUv.bottom[3],
|
||||
false,
|
||||
true,
|
||||
),
|
||||
createFace(
|
||||
capeUv.front[0],
|
||||
capeUv.front[1],
|
||||
capeUv.front[2],
|
||||
capeUv.front[3],
|
||||
),
|
||||
createFace(
|
||||
capeUv.back[0],
|
||||
capeUv.back[1],
|
||||
capeUv.back[2],
|
||||
capeUv.back[3],
|
||||
),
|
||||
];
|
||||
group.add(new THREE.Mesh(geo, mats));
|
||||
render();
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
render();
|
||||
},
|
||||
);
|
||||
|
||||
let isDragging = false;
|
||||
let previousMousePosition = { x: 0, y: 0 };
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
isDragging = true;
|
||||
previousMousePosition = { x: e.clientX, y: e.clientY };
|
||||
};
|
||||
const onMouseUp = () => {
|
||||
isDragging = false;
|
||||
};
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (isDragging && groupRef.current) {
|
||||
groupRef.current.rotation.y +=
|
||||
(e.clientX - previousMousePosition.x) * 0.01;
|
||||
previousMousePosition = { x: e.clientX, y: e.clientY };
|
||||
render();
|
||||
}
|
||||
};
|
||||
renderer.domElement.addEventListener("mousedown", onMouseDown);
|
||||
window.addEventListener("mousemove", onMouseMove);
|
||||
window.addEventListener("mouseup", onMouseUp);
|
||||
return () => {
|
||||
active = false;
|
||||
window.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
scene.traverse((object) => {
|
||||
if (object instanceof THREE.Mesh) {
|
||||
if (object.geometry) object.geometry.dispose();
|
||||
if (object.material) {
|
||||
if (Array.isArray(object.material)) {
|
||||
object.material.forEach((mat) => {
|
||||
if (mat.map) mat.map.dispose();
|
||||
mat.dispose();
|
||||
});
|
||||
} else {
|
||||
if (object.material.map) object.material.map.dispose();
|
||||
object.material.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
renderer.dispose();
|
||||
};
|
||||
}, [src]);
|
||||
return (
|
||||
<div
|
||||
ref={mountRef}
|
||||
className={`w-full h-full cursor-ew-resize ${className ?? ""}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default CapePreview;
|
||||
@@ -0,0 +1,187 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { TauriService } from "../../services/TauriService";
|
||||
const MAX_DISPLAY_LINES = 5000;
|
||||
const WINE_CHANNEL =
|
||||
/^\s*(?:[0-9a-fA-F]{1,8}:)?(err|fixme|warn|trace|debugstr):/;
|
||||
function lineColor(line: string): string {
|
||||
const match = WINE_CHANNEL.exec(line);
|
||||
switch (match?.[1]) {
|
||||
case "err":
|
||||
return "#FF5555";
|
||||
case "fixme":
|
||||
return "#FFFF55";
|
||||
case "warn":
|
||||
return "#FFB347";
|
||||
case "trace":
|
||||
return "#7FFF7F";
|
||||
case "debugstr":
|
||||
return "#55FFFF";
|
||||
default:
|
||||
return "#E0E0E0";
|
||||
}
|
||||
}
|
||||
|
||||
function LogActionButton({
|
||||
children,
|
||||
onClick,
|
||||
className = "",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
return (
|
||||
<button
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
onClick={onClick}
|
||||
className={`h-10 flex items-center justify-center text-lg mc-text-shadow border-none outline-none cursor-pointer text-white hover:text-[#ffff00] ${className}`}
|
||||
style={{
|
||||
backgroundImage: hovered
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GameLogModal({
|
||||
isOpen,
|
||||
log,
|
||||
onClose,
|
||||
playBackSound,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
log: string | null;
|
||||
onClose: () => void;
|
||||
playBackSound: () => void;
|
||||
}) {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [feedback, setFeedback] = useState("");
|
||||
const lines = useMemo(() => (log ?? "").split("\n"), [log]);
|
||||
const truncated = lines.length > MAX_DISPLAY_LINES;
|
||||
const visibleLines = truncated ? lines.slice(-MAX_DISPLAY_LINES) : lines;
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setFeedback("");
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}
|
||||
}, [isOpen, log]);
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
playBackSound();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKey);
|
||||
return () => window.removeEventListener("keydown", handleKey);
|
||||
}, [isOpen, onClose, playBackSound]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
const handleSave = async () => {
|
||||
if (!log) return;
|
||||
try {
|
||||
const path = await TauriService.saveFileDialog(
|
||||
"Save Game Log",
|
||||
"game-log.txt",
|
||||
["txt", "log"],
|
||||
);
|
||||
if (!path) return;
|
||||
await TauriService.writeBinaryFile(path, new TextEncoder().encode(log));
|
||||
setFeedback("Saved!");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setFeedback("Failed to save.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!log) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(log);
|
||||
setFeedback("Copied to clipboard!");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setFeedback("Failed to copy.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-[200] flex items-center justify-center bg-black/80"
|
||||
onClick={() => {
|
||||
playBackSound();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 20, opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex flex-col w-[640px] max-w-[92vw] max-h-[85vh] p-5 font-['Mojangles'] mc-options-bg"
|
||||
>
|
||||
<h3 className="text-2xl font-bold text-[#333333] mb-3 text-left w-full px-2 mc-text-shadow">
|
||||
Game Crash Log
|
||||
</h3>
|
||||
|
||||
{truncated && (
|
||||
<p className="text-[#666666] text-sm mb-3 text-left w-full px-2">
|
||||
Showing last {MAX_DISPLAY_LINES} lines of {lines.length}.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="w-full flex-1 min-h-0 overflow-x-hidden overflow-y-auto bg-black/75 border-2 border-[#555] p-3 mb-4 text-left"
|
||||
>
|
||||
<div className="font-mono text-xs leading-relaxed whitespace-pre-wrap break-words">
|
||||
{visibleLines.map((line, i) => (
|
||||
<span key={i} style={{ color: lineColor(line) }}>
|
||||
{line}
|
||||
{"\n"}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 w-full px-2 mb-2">
|
||||
<LogActionButton onClick={handleSave} className="flex-1">
|
||||
Save As File
|
||||
</LogActionButton>
|
||||
<LogActionButton onClick={handleCopy} className="flex-1">
|
||||
Copy
|
||||
</LogActionButton>
|
||||
<LogActionButton
|
||||
onClick={() => {
|
||||
playBackSound();
|
||||
onClose();
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
Close
|
||||
</LogActionButton>
|
||||
</div>
|
||||
{feedback && (
|
||||
<p className="text-[#333333] text-sm text-center w-full">
|
||||
{feedback}
|
||||
</p>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { TauriService } from "../../services/TauriService";
|
||||
|
||||
export default function ImportWorldModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -20,7 +19,6 @@ export default function ImportWorldModal({
|
||||
const [status, setStatus] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setStatus("");
|
||||
@@ -29,19 +27,18 @@ export default function ImportWorldModal({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleImport = async () => {
|
||||
const handleImportMs = async () => {
|
||||
if (!targetInstanceId) return;
|
||||
playPressSound();
|
||||
setIsImporting(true);
|
||||
setError("");
|
||||
setStatus("Selecting source...");
|
||||
|
||||
try {
|
||||
setStatus("Selecting LCE save folder or .ms file...");
|
||||
const picked = await TauriService.pickFile(
|
||||
"Select saveData.ms or GameHDD folder",
|
||||
["*.ms", "*"],
|
||||
);
|
||||
setStatus("Selecting saveData.ms file...");
|
||||
const picked = await TauriService.pickFile("Select saveData.ms", [
|
||||
"*.ms",
|
||||
"*",
|
||||
]);
|
||||
if (!picked) {
|
||||
setIsImporting(false);
|
||||
return;
|
||||
@@ -64,6 +61,129 @@ export default function ImportWorldModal({
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportXbox = async () => {
|
||||
if (!targetInstanceId) return;
|
||||
playPressSound();
|
||||
setIsImporting(true);
|
||||
setError("");
|
||||
setStatus("Selecting source...");
|
||||
try {
|
||||
setStatus("Selecting Xbox 360 save (.bin)...");
|
||||
const picked = await TauriService.pickFile(
|
||||
"Select Xbox 360 Minecraft save",
|
||||
["*.bin", "*"],
|
||||
);
|
||||
if (!picked) {
|
||||
setIsImporting(false);
|
||||
return;
|
||||
}
|
||||
setStatus("Converting Xbox 360 save...");
|
||||
const instancePath = await TauriService.getInstancePath(targetInstanceId);
|
||||
const gameHdd = `${instancePath}/Windows64/GameHDD`;
|
||||
await TauriService.importLceSave(picked, gameHdd);
|
||||
setStatus(`Xbox 360 save converted into "${targetInstanceName}"!`);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 2000);
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
setStatus("");
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportPs3 = async () => {
|
||||
if (!targetInstanceId) return;
|
||||
playPressSound();
|
||||
setIsImporting(true);
|
||||
setError("");
|
||||
setStatus("Selecting source...");
|
||||
try {
|
||||
setStatus("Selecting PS3 save folder...");
|
||||
const picked = await TauriService.pickFolder();
|
||||
if (!picked) {
|
||||
setIsImporting(false);
|
||||
return;
|
||||
}
|
||||
setStatus("Converting PS3 save...");
|
||||
const instancePath = await TauriService.getInstancePath(targetInstanceId);
|
||||
const gameHdd = `${instancePath}/Windows64/GameHDD`;
|
||||
await TauriService.importLceSave(picked, gameHdd);
|
||||
setStatus(`PS3 save converted into "${targetInstanceName}"!`);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 2000);
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
setStatus("");
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportJava = async () => {
|
||||
if (!targetInstanceId) return;
|
||||
playPressSound();
|
||||
setIsImporting(true);
|
||||
setError("");
|
||||
setStatus("Selecting source...");
|
||||
try {
|
||||
setStatus("Selecting Java world folder...");
|
||||
const picked = await TauriService.pickFolder();
|
||||
if (!picked) {
|
||||
setIsImporting(false);
|
||||
return;
|
||||
}
|
||||
const worldName = deriveWorldName(picked);
|
||||
setStatus("Converting Java world to LCE...");
|
||||
const instancePath = await TauriService.getInstancePath(targetInstanceId);
|
||||
const saveDir = `${instancePath}/Windows64/GameHDD/${worldName}`;
|
||||
await TauriService.javaToLce(picked, `${saveDir}/saveData.ms`);
|
||||
setStatus(`Java world converted into "${targetInstanceName}"!`);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 2000);
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
setStatus("");
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportJava = async () => {
|
||||
if (!targetInstanceId) return;
|
||||
playPressSound();
|
||||
setIsImporting(true);
|
||||
setError("");
|
||||
setStatus("Selecting source...");
|
||||
try {
|
||||
setStatus("Selecting saveData.ms file...");
|
||||
const picked = await TauriService.pickFile("Select saveData.ms", [
|
||||
"*.ms",
|
||||
"*",
|
||||
]);
|
||||
if (!picked) {
|
||||
setIsImporting(false);
|
||||
return;
|
||||
}
|
||||
setStatus("Selecting output folder for Java world...");
|
||||
const outputFolder = await TauriService.pickFolder();
|
||||
if (!outputFolder) {
|
||||
setIsImporting(false);
|
||||
return;
|
||||
}
|
||||
setStatus("Converting LCE save to Java world...");
|
||||
await TauriService.lceToJava(picked, outputFolder);
|
||||
setStatus(`Java world exported to "${outputFolder}"!`);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 2000);
|
||||
} catch (e: unknown) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
setStatus("");
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
playBackSound();
|
||||
@@ -104,9 +224,77 @@ export default function ImportWorldModal({
|
||||
Import into:{" "}
|
||||
<span className="text-[#FFFF55]">{targetInstanceName}</span>
|
||||
</p>
|
||||
<p className="text-gray-400 text-xs mc-text-shadow mb-4 text-center">
|
||||
Select an existing LCE save (.ms file or GameHDD folder)
|
||||
|
||||
<p className="text-gray-400 text-xs mc-text-shadow mb-2 text-center">
|
||||
Import an existing .ms save file:
|
||||
</p>
|
||||
<button
|
||||
onClick={handleImportMs}
|
||||
className="w-48 h-9 flex items-center justify-center text-sm text-white mc-text-shadow hover:text-[#FFFF55] mb-4"
|
||||
style={{
|
||||
backgroundImage: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Select .ms File
|
||||
</button>
|
||||
|
||||
<p className="text-gray-400 text-xs mc-text-shadow mb-2 text-center">
|
||||
Convert an Xbox 360 or PS3 save:
|
||||
</p>
|
||||
<div className="flex gap-3 mb-2">
|
||||
<button
|
||||
onClick={handleImportXbox}
|
||||
className="w-40 h-9 flex items-center justify-center text-sm text-white mc-text-shadow hover:text-[#FFFF55]"
|
||||
style={{
|
||||
backgroundImage: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Xbox 360 (.bin)
|
||||
</button>
|
||||
<button
|
||||
onClick={handleImportPs3}
|
||||
className="w-40 h-9 flex items-center justify-center text-sm text-white mc-text-shadow hover:text-[#FFFF55]"
|
||||
style={{
|
||||
backgroundImage: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
PS3 (Folder)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-400 text-xs mc-text-shadow mb-2 text-center">
|
||||
Java Edition world conversion:
|
||||
</p>
|
||||
<div className="flex gap-3 mb-2">
|
||||
<button
|
||||
onClick={handleImportJava}
|
||||
className="w-40 h-9 flex items-center justify-center text-sm text-white mc-text-shadow hover:text-[#FFFF55]"
|
||||
style={{
|
||||
backgroundImage: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Java → LCE
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportJava}
|
||||
className="w-40 h-9 flex items-center justify-center text-sm text-white mc-text-shadow hover:text-[#FFFF55]"
|
||||
style={{
|
||||
backgroundImage: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
LCE → Java
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-red-500 text-center mc-text-shadow uppercase text-xs tracking-widest mb-3">
|
||||
@@ -114,33 +302,20 @@ export default function ImportWorldModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-4 w-full justify-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
playBackSound();
|
||||
onClose();
|
||||
}}
|
||||
className="w-32 h-10 flex items-center justify-center text-xl text-white mc-text-shadow"
|
||||
style={{
|
||||
backgroundImage: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleImport}
|
||||
className="w-40 h-10 flex items-center justify-center text-xl text-white mc-text-shadow hover:text-[#FFFF55]"
|
||||
style={{
|
||||
backgroundImage: "url('/images/button_highlighted.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Select File
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
playBackSound();
|
||||
onClose();
|
||||
}}
|
||||
className="w-32 h-10 flex items-center justify-center text-xl text-white mc-text-shadow mt-2"
|
||||
style={{
|
||||
backgroundImage: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -319,7 +319,7 @@ export const ArcEditorView: React.FC = () => {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col items-center w-full max-w-7xl h-[85vh] outline-none"
|
||||
className="flex flex-col items-center w-full max-w-7xl h-full outline-none"
|
||||
>
|
||||
<div className="w-full flex justify-between items-center mb-4 px-8">
|
||||
<h2 className="text-2xl text-white mc-text-shadow border-b-2 border-[#373737] pb-1 tracking-widest uppercase font-bold">
|
||||
@@ -567,10 +567,10 @@ export const ArcEditorView: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-center mt-6 h-14 w-full">
|
||||
<div className="flex justify-center mt-6 h-14 w-full shrink-0">
|
||||
<button
|
||||
onClick={() => { playBackSound(); setActiveView("devtools"); }}
|
||||
className="w-72 h-full flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}
|
||||
>
|
||||
Back
|
||||
|
||||
@@ -129,7 +129,7 @@ export default function ColEditorView() {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col w-full h-[85vh] max-w-7xl relative"
|
||||
className="flex flex-col w-full h-full max-w-7xl relative"
|
||||
>
|
||||
<input type="file" ref={fileInputRef} onChange={handleFileLoad} className="hidden" accept=".col" />
|
||||
<div className="flex items-center justify-between mb-6 px-4">
|
||||
@@ -365,10 +365,10 @@ export default function ColEditorView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-center mt-6 h-14">
|
||||
<div className="flex justify-center mt-6 h-14 shrink-0">
|
||||
<button
|
||||
onClick={() => { playBackSound(); setActiveView("devtools"); }}
|
||||
className="w-72 h-full flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}
|
||||
>
|
||||
Back
|
||||
|
||||
@@ -78,14 +78,14 @@ export default function DevtoolsView() {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col items-center w-full max-w-3xl outline-none"
|
||||
className="flex flex-col items-center w-full max-w-3xl h-full outline-none"
|
||||
>
|
||||
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[60%] max-w-75 text-center tracking-widest uppercase opacity-80 font-bold">
|
||||
Developer Tools
|
||||
</h2>
|
||||
|
||||
<div
|
||||
className="w-full max-w-160 h-85 mb-4 p-8 shadow-2xl flex flex-col items-center"
|
||||
className="w-full max-w-160 flex-1 min-h-0 mb-4 p-8 shadow-2xl flex flex-col items-center overflow-hidden"
|
||||
style={{
|
||||
backgroundImage: "url('/images/frame_background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
@@ -150,7 +150,7 @@ export default function DevtoolsView() {
|
||||
playBackSound();
|
||||
setActiveView("main");
|
||||
}}
|
||||
className={`w-72 h-14 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-2 outline-none border-none ${focusIndex === BACK_BUTTON_INDEX ? "text-[#FFFF55]" : "text-white"
|
||||
className={`w-72 h-14 shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-2 outline-none border-none ${focusIndex === BACK_BUTTON_INDEX ? "text-[#FFFF55]" : "text-white"
|
||||
}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function GrfEditorView() {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col w-full h-[85vh] max-w-7xl relative"
|
||||
className="flex flex-col w-full h-full max-w-7xl relative"
|
||||
>
|
||||
<input type="file" ref={fileInputRef} onChange={handleFileLoad} className="hidden" accept=".grf" />
|
||||
<input type="file" ref={addFileInputRef} onChange={handleAddFile} className="hidden" />
|
||||
@@ -223,10 +223,10 @@ export default function GrfEditorView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-center mt-6 h-14">
|
||||
<div className="flex justify-center mt-6 h-14 shrink-0">
|
||||
<button
|
||||
onClick={() => { playBackSound(); setActiveView("devtools"); }}
|
||||
className="w-72 h-full flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}
|
||||
>
|
||||
Back
|
||||
|
||||
@@ -184,7 +184,7 @@ export default function GuidesView() {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col items-center w-full max-w-5xl outline-none"
|
||||
className="flex flex-col items-center w-full max-w-5xl h-full outline-none"
|
||||
>
|
||||
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[60%] max-w-75 text-center tracking-widest uppercase opacity-80 font-bold">
|
||||
{selectedGuide
|
||||
@@ -193,7 +193,7 @@ export default function GuidesView() {
|
||||
</h2>
|
||||
|
||||
<div
|
||||
className="w-full max-w-5xl h-[42rem] mb-4 p-8 shadow-2xl flex flex-col items-center"
|
||||
className="w-full max-w-5xl flex-1 min-h-0 mb-4 p-8 shadow-2xl flex flex-col items-center overflow-hidden"
|
||||
style={{
|
||||
backgroundImage: "url('/images/frame_background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
@@ -265,7 +265,7 @@ export default function GuidesView() {
|
||||
data-index={BACK_BUTTON_INDEX}
|
||||
onMouseEnter={() => setFocusIndex(BACK_BUTTON_INDEX)}
|
||||
onClick={goBack}
|
||||
className={`w-72 h-14 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-2 outline-none border-none ${
|
||||
className={`w-72 h-14 shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-2 outline-none border-none ${
|
||||
focusIndex === BACK_BUTTON_INDEX ? "text-[#FFFF55]" : "text-white"
|
||||
}`}
|
||||
style={{
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
useGame,
|
||||
} from "../../context/LauncherContext";
|
||||
import ChooseInstanceModal from "../modals/ChooseInstanceModal";
|
||||
import { lceOnlineService } from "../../services/LceOnlineService";
|
||||
import { lceOnlineService, SocialEntry } from "../../services/LceOnlineService";
|
||||
import { TauriService } from "../../services/TauriService";
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
@@ -25,7 +25,7 @@ const LceOnlineView = memo(function LceOnlineView({
|
||||
onClearAddFriendTarget,
|
||||
invites: invitesProp,
|
||||
}: LceOnlineViewProps) {
|
||||
const { setActiveView } = useUI();
|
||||
const { setActiveView, setIsUiHidden } = useUI();
|
||||
const { animationsEnabled } = useConfig();
|
||||
const { playPressSound, playBackSound } = useAudio();
|
||||
const game = useGame();
|
||||
@@ -35,9 +35,9 @@ const LceOnlineView = memo(function LceOnlineView({
|
||||
"friends" | "requests" | "invites"
|
||||
>("friends");
|
||||
const [focusIndex, setFocusIndex] = useState<number | null>(0);
|
||||
const [friends, setFriends] = useState<string[]>([]);
|
||||
const [incomingReqs, setIncomingReqs] = useState<string[]>([]);
|
||||
const [outgoingReqs, setOutgoingReqs] = useState<string[]>([]);
|
||||
const [friends, setFriends] = useState<SocialEntry[]>([]);
|
||||
const [incomingReqs, setIncomingReqs] = useState<SocialEntry[]>([]);
|
||||
const [outgoingReqs, setOutgoingReqs] = useState<SocialEntry[]>([]);
|
||||
const invites = invitesProp ?? [];
|
||||
const [isHosting, setIsHosting] = useState(false);
|
||||
const [isAddingFriend, setIsAddingFriend] = useState(false);
|
||||
@@ -189,34 +189,34 @@ const LceOnlineView = memo(function LceOnlineView({
|
||||
});
|
||||
friends.forEach((f) => {
|
||||
items.push({
|
||||
id: `friend_${f}`,
|
||||
id: `friend_${f.username}`,
|
||||
type: "friend",
|
||||
label: f,
|
||||
onClick: () => handleAction(() => lceOnlineService.removeFriend(f)),
|
||||
label: f.displayName,
|
||||
onClick: () => handleAction(() => lceOnlineService.removeFriend(f.username)),
|
||||
onClickSecondary: isHosting
|
||||
? () => handleAction(() => lceOnlineService.sendInvite(f))
|
||||
? () => handleAction(() => lceOnlineService.sendInvite(f.username))
|
||||
: undefined,
|
||||
});
|
||||
});
|
||||
} else if (currentTab === "requests") {
|
||||
incomingReqs.forEach((r) => {
|
||||
items.push({
|
||||
id: `req_in_${r}`,
|
||||
id: `req_in_${r.username}`,
|
||||
type: "request_in",
|
||||
label: r,
|
||||
label: r.displayName,
|
||||
onClick: () =>
|
||||
handleAction(() => lceOnlineService.acceptFriendRequest(r)),
|
||||
handleAction(() => lceOnlineService.acceptFriendRequest(r.username)),
|
||||
onClickSecondary: () =>
|
||||
handleAction(() => lceOnlineService.declineFriendRequest(r)),
|
||||
handleAction(() => lceOnlineService.declineFriendRequest(r.username)),
|
||||
});
|
||||
});
|
||||
outgoingReqs.forEach((r) => {
|
||||
items.push({
|
||||
id: `req_out_${r}`,
|
||||
id: `req_out_${r.username}`,
|
||||
type: "request_out",
|
||||
label: r,
|
||||
label: r.displayName,
|
||||
onClick: () =>
|
||||
handleAction(() => lceOnlineService.declineFriendRequest(r)),
|
||||
handleAction(() => lceOnlineService.declineFriendRequest(r.username)),
|
||||
});
|
||||
});
|
||||
} else if (currentTab === "invites") {
|
||||
@@ -364,6 +364,122 @@ const LceOnlineView = memo(function LceOnlineView({
|
||||
}
|
||||
}, [focusIndex, isAddingFriend]);
|
||||
|
||||
const touhouOverlayRef = useRef<HTMLDivElement | null>(null);
|
||||
const touhouBlobUrlRef = useRef<string | null>(null);
|
||||
const touhouAudioRef = useRef<HTMLAudioElement | null>(null);
|
||||
useEffect(() => {
|
||||
const GIF_URL =
|
||||
"https://raw.githubusercontent.com/neoapps-dev/neoapps-dev/main/badapple_small.gif";
|
||||
const MP3_URL =
|
||||
"https://raw.githubusercontent.com/Soldr/bad-apple-but-its-node.js/master/bad-apple.mp3";
|
||||
|
||||
const stopAudio = () => {
|
||||
if (touhouAudioRef.current) {
|
||||
touhouAudioRef.current.pause();
|
||||
touhouAudioRef.current.src = "";
|
||||
touhouAudioRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const onlineUser = lceOnlineService.account?.username;
|
||||
if (onlineUser === "TOUHOU") {
|
||||
if (!touhouOverlayRef.current) {
|
||||
setIsUiHidden(true);
|
||||
const overlay = document.createElement("div");
|
||||
overlay.style.cssText =
|
||||
"position:fixed;inset:0;z-index:99999;background:#000;display:flex;align-items:center;justify-content:center;cursor:pointer";
|
||||
const spinner = document.createElement("div");
|
||||
spinner.textContent = "Loading...";
|
||||
spinner.style.cssText =
|
||||
"color:#fff;font-family:'Mojangles',monospace;font-size:24px;letter-spacing:4px";
|
||||
overlay.appendChild(spinner);
|
||||
document.body.appendChild(overlay);
|
||||
const img = document.createElement("img");
|
||||
img.style.cssText = "width:100%;height:100%;object-fit:contain";
|
||||
const audio = new Audio(MP3_URL);
|
||||
audio.loop = true;
|
||||
audio.volume = 0.5;
|
||||
touhouAudioRef.current = audio;
|
||||
const audioReady = new Promise<void>((resolve) => {
|
||||
if (audio.readyState >= 3) resolve();
|
||||
else {
|
||||
audio.oncanplaythrough = () => resolve();
|
||||
audio.onerror = () => resolve();
|
||||
}
|
||||
});
|
||||
|
||||
const gifReady = fetch(GIF_URL)
|
||||
.then((r) => r.arrayBuffer())
|
||||
.then((buf) => {
|
||||
const blob = new Blob([buf], { type: "image/gif" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
touhouBlobUrlRef.current = url;
|
||||
img.src = url;
|
||||
return new Promise<void>((resolve) => {
|
||||
if (img.complete) resolve();
|
||||
else {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
img.src = GIF_URL;
|
||||
return new Promise<void>((resolve) => {
|
||||
if (img.complete) resolve();
|
||||
else {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all([gifReady, audioReady]).then(() => {
|
||||
spinner.remove();
|
||||
overlay.appendChild(img);
|
||||
audio.currentTime = 2;
|
||||
audio.play().catch(() => {});
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
stopAudio();
|
||||
setIsUiHidden(false);
|
||||
if (overlay.parentNode) overlay.remove();
|
||||
touhouOverlayRef.current = null;
|
||||
if (touhouBlobUrlRef.current) {
|
||||
URL.revokeObjectURL(touhouBlobUrlRef.current);
|
||||
touhouBlobUrlRef.current = null;
|
||||
}
|
||||
};
|
||||
overlay.onclick = cleanup;
|
||||
touhouOverlayRef.current = overlay;
|
||||
}
|
||||
} else {
|
||||
if (touhouOverlayRef.current) {
|
||||
stopAudio();
|
||||
touhouOverlayRef.current.remove();
|
||||
touhouOverlayRef.current = null;
|
||||
if (touhouBlobUrlRef.current) {
|
||||
URL.revokeObjectURL(touhouBlobUrlRef.current);
|
||||
touhouBlobUrlRef.current = null;
|
||||
}
|
||||
setIsUiHidden(false);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
stopAudio();
|
||||
setIsUiHidden(false);
|
||||
if (touhouOverlayRef.current) {
|
||||
touhouOverlayRef.current.remove();
|
||||
touhouOverlayRef.current = null;
|
||||
if (touhouBlobUrlRef.current) {
|
||||
URL.revokeObjectURL(touhouBlobUrlRef.current);
|
||||
touhouBlobUrlRef.current = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [isSignedIn, setIsUiHidden]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (!isSignedIn) {
|
||||
return (
|
||||
@@ -448,6 +564,16 @@ const LceOnlineView = memo(function LceOnlineView({
|
||||
<span className="text-[#2a2a2a] font-bold text-2xl truncate pr-4">
|
||||
{item.label}
|
||||
</span>
|
||||
<span className="text-[#555] text-base font-bold truncate">
|
||||
@
|
||||
{item.type === "friend"
|
||||
? friends.find((f) => `friend_${f.username}` === item.id)?.username
|
||||
: item.type === "request_in"
|
||||
? incomingReqs.find((r) => `req_in_${r.username}` === item.id)?.username
|
||||
: item.type === "request_out"
|
||||
? outgoingReqs.find((r) => `req_out_${r.username}` === item.id)?.username
|
||||
: "Invite"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-3 pr-2 shrink-0">
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function LocEditorView() {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col w-full h-[85vh] max-w-7xl relative"
|
||||
className="flex flex-col w-full h-full max-w-7xl relative"
|
||||
>
|
||||
<input type="file" ref={fileInputRef} onChange={handleFileLoad} className="hidden" accept=".loc" />
|
||||
<div className="flex items-center justify-between mb-6 px-4">
|
||||
@@ -201,10 +201,10 @@ export default function LocEditorView() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-center mt-6 h-14">
|
||||
<div className="flex justify-center mt-6 h-14 shrink-0">
|
||||
<button
|
||||
onClick={() => { playBackSound(); setActiveView("devtools"); }}
|
||||
className="w-72 h-full flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}
|
||||
>
|
||||
Back
|
||||
|
||||
@@ -314,7 +314,7 @@ export default function ModelEditorView() {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col w-full h-[85vh] max-w-7xl relative"
|
||||
className="flex flex-col w-full h-full max-w-7xl relative"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
|
||||
@@ -80,7 +80,7 @@ export default function OptionsEditorView() {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col w-full h-[85vh] max-w-7xl relative"
|
||||
className="flex flex-col w-full h-full max-w-7xl relative"
|
||||
>
|
||||
<input type="file" ref={fileInputRef} onChange={handleFileLoad} className="hidden" accept=".dat,.bin" />
|
||||
<div className="flex items-center justify-between mb-6 px-4">
|
||||
@@ -211,10 +211,10 @@ export default function OptionsEditorView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-center mt-6 h-14">
|
||||
<div className="flex justify-center mt-6 h-14 shrink-0">
|
||||
<button
|
||||
onClick={() => { playBackSound(); setActiveView("devtools"); }}
|
||||
className="w-72 h-full flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}
|
||||
>
|
||||
Back
|
||||
|
||||
@@ -500,7 +500,7 @@ export default function PckEditorView() {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col items-center w-full max-w-6xl h-[85vh] outline-none"
|
||||
className="flex flex-col items-center w-full max-w-6xl h-full outline-none"
|
||||
>
|
||||
<div className="w-full flex justify-between items-center mb-4 px-8">
|
||||
<h2 className="text-2xl text-white mc-text-shadow border-b-2 border-[#373737] pb-1 tracking-widest uppercase font-bold">
|
||||
@@ -1028,7 +1028,7 @@ export default function PckEditorView() {
|
||||
playBackSound();
|
||||
setActiveView("devtools");
|
||||
}}
|
||||
className="w-72 h-14 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-6 outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
className="w-72 h-14 shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-6 outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
style={{
|
||||
backgroundImage: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useConfig,
|
||||
} from "../../context/LauncherContext";
|
||||
import SkinViewer from "../common/SkinViewer";
|
||||
import CapePreview from "../common/CapePreview";
|
||||
|
||||
interface SavedSkin {
|
||||
id: string;
|
||||
@@ -47,6 +48,13 @@ const DEFAULT_SKINS: SavedSkin[] = [
|
||||
},
|
||||
{ id: "peter", name: "Peter", url: "/Skins/Peter.png", isSlim: false },
|
||||
{ id: "piebot", name: "piebot", url: "/Skins/piebot.png", isSlim: false },
|
||||
{
|
||||
id: "kowhaifan",
|
||||
name: "Kowhaifan",
|
||||
url: "/Skins/kowhaifan.png",
|
||||
isSlim: false,
|
||||
},
|
||||
{ id: "striker", name: "str1k3r", url: "/Skins/str1k3r.png", isSlim: true },
|
||||
{ id: "andipog", name: "Andi_Pog", url: "/Skins/andi.png", isSlim: false },
|
||||
{ id: "sevenhundred", name: "700", url: "/Skins/700.png", isSlim: false },
|
||||
{
|
||||
@@ -55,10 +63,41 @@ const DEFAULT_SKINS: SavedSkin[] = [
|
||||
url: "/Skins/PrismaChunk0.png",
|
||||
isSlim: false,
|
||||
},
|
||||
{ id: "amy", name: "Amy", url: "/Skins/amy.png", isSlim: true },
|
||||
{ id: "amy", name: "Amy", url: "/Skins/amy.png", isSlim: true }, //neo: she's the best btw
|
||||
{ id: "huckle", name: "Huckle", url: "/Skins/huckle.png", isSlim: true },
|
||||
];
|
||||
|
||||
const HeadPreview = memo(function HeadPreview({ src }: { src: string }) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const cvs = canvasRef.current;
|
||||
if (!cvs) return;
|
||||
const ctx = cvs.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
ctx.clearRect(0, 0, cvs.width, cvs.height);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(img, 8, 8, 8, 8, 0, 0, cvs.width, cvs.height);
|
||||
if (img.height !== 32) {
|
||||
ctx.drawImage(img, 40, 8, 8, 8, 0, 0, cvs.width, cvs.height);
|
||||
}
|
||||
};
|
||||
img.src = src;
|
||||
}, [src]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={64}
|
||||
height={64}
|
||||
className="absolute w-full h-full"
|
||||
style={{ imageRendering: "pixelated" }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const SkinsView = memo(function SkinsView() {
|
||||
const { setActiveView, setIsUiHidden } = useUI();
|
||||
const { playPressSound, playBackSound } = useAudio();
|
||||
@@ -271,7 +310,7 @@ const SkinsView = memo(function SkinsView() {
|
||||
if (focusIndex === null || focusIndex < TOP_BUTTONS_COUNT) {
|
||||
setFocusIndex(SKINS_START_INDEX);
|
||||
} else if (focusIndex < BACK_BUTTON_INDEX) {
|
||||
const rowCount = viewMode === "cape" ? 3 : 4;
|
||||
const rowCount = 4;
|
||||
const next = focusIndex + rowCount;
|
||||
setFocusIndex(next >= BACK_BUTTON_INDEX ? BACK_BUTTON_INDEX : next);
|
||||
}
|
||||
@@ -283,7 +322,7 @@ const SkinsView = memo(function SkinsView() {
|
||||
viewMode === "cape" ? storedCapes.length + 1 : savedSkins.length;
|
||||
setFocusIndex(SKINS_START_INDEX + itemCount - 1);
|
||||
} else if (focusIndex >= SKINS_START_INDEX) {
|
||||
const rowCount = viewMode === "cape" ? 3 : 4;
|
||||
const rowCount = 4;
|
||||
const next = focusIndex - rowCount;
|
||||
setFocusIndex(next < SKINS_START_INDEX ? 0 : next);
|
||||
}
|
||||
@@ -432,78 +471,69 @@ const SkinsView = memo(function SkinsView() {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: useConfig().animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col items-center w-full max-w-3xl outline-none"
|
||||
className="flex flex-col items-center w-full max-w-3xl h-full outline-none"
|
||||
>
|
||||
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[60%] max-w-75 text-center tracking-widest uppercase opacity-80 font-bold">
|
||||
{viewMode === "skin" ? "Skin Library" : "Cape Library"}
|
||||
</h2>
|
||||
|
||||
<div
|
||||
className="w-full max-w-160 h-85 mb-4 p-5 shadow-2xl flex flex-col relative"
|
||||
style={{
|
||||
backgroundImage: "url('/images/frame_background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center border-b-2 border-[#373737] pb-4 mb-4 relative min-h-10">
|
||||
<div className="absolute left-0 right-0 flex justify-center gap-4 items-center">
|
||||
<button
|
||||
data-index="0"
|
||||
onMouseEnter={() => setFocusIndex(0)}
|
||||
onClick={() => {
|
||||
playPressSound();
|
||||
if (viewMode === "skin") handleImportClick();
|
||||
else capeFileInputRef.current?.click();
|
||||
}}
|
||||
className={`w-40 h-10 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] ${focusIndex === 0 ? "text-[#FFFF55]" : "text-white"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
focusIndex === 0
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
{viewMode === "skin" ? "Import Skin" : "Import Cape"}
|
||||
</button>
|
||||
<div className="w-full max-w-160 flex-1 min-h-0 mb-4 p-5 flex flex-col relative overflow-hidden">
|
||||
<div className="w-full flex items-center gap-4 ml-5 pb-4 mb-4 min-h-10">
|
||||
<button
|
||||
data-index="0"
|
||||
onMouseEnter={() => setFocusIndex(0)}
|
||||
onClick={() => {
|
||||
playPressSound();
|
||||
if (viewMode === "skin") handleImportClick();
|
||||
else capeFileInputRef.current?.click();
|
||||
}}
|
||||
className={`w-40 h-10 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] ${focusIndex === 0 ? "text-[#FFFF55]" : "text-white"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
focusIndex === 0
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
{viewMode === "skin" ? "Import Skin" : "Import Cape"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
data-index="1"
|
||||
onMouseEnter={() => {
|
||||
if (viewMode === "skin" && !isActiveDefault) setFocusIndex(1);
|
||||
else if (viewMode === "cape" && !isActiveCapeDefault)
|
||||
setFocusIndex(1);
|
||||
}}
|
||||
onClick={() => {
|
||||
playPressSound();
|
||||
if (viewMode === "skin") handleDeleteActive();
|
||||
else handleDeleteActiveCape();
|
||||
}}
|
||||
className={`w-40 h-10 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none ${
|
||||
<button
|
||||
data-index="1"
|
||||
onMouseEnter={() => {
|
||||
if (viewMode === "skin" && !isActiveDefault) setFocusIndex(1);
|
||||
else if (viewMode === "cape" && !isActiveCapeDefault)
|
||||
setFocusIndex(1);
|
||||
}}
|
||||
onClick={() => {
|
||||
playPressSound();
|
||||
if (viewMode === "skin") handleDeleteActive();
|
||||
else handleDeleteActiveCape();
|
||||
}}
|
||||
className={`w-40 h-10 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none ${
|
||||
(viewMode === "skin" && isActiveDefault) ||
|
||||
(viewMode === "cape" && isActiveCapeDefault)
|
||||
? "text-gray-400 opacity-80 cursor-not-allowed"
|
||||
: focusIndex === 1
|
||||
? "text-[#FFFF55]"
|
||||
: "text-white"
|
||||
}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
(viewMode === "skin" && isActiveDefault) ||
|
||||
(viewMode === "cape" && isActiveCapeDefault)
|
||||
? "text-gray-400 opacity-80 cursor-not-allowed"
|
||||
? "url('/images/Button_Background2.png')"
|
||||
: focusIndex === 1
|
||||
? "text-[#FFFF55]"
|
||||
: "text-white"
|
||||
}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
(viewMode === "skin" && isActiveDefault) ||
|
||||
(viewMode === "cape" && isActiveCapeDefault)
|
||||
? "url('/images/Button_Background2.png')"
|
||||
: focusIndex === 1
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
{viewMode === "skin" ? "Delete Skin" : "Delete Cape"}
|
||||
</button>
|
||||
</div>
|
||||
? "url('/images/button_highlighted.png')"
|
||||
: "url('/images/Button_Background.png')",
|
||||
backgroundSize: "100% 100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
>
|
||||
{viewMode === "skin" ? "Delete Skin" : "Delete Cape"}
|
||||
</button>
|
||||
|
||||
<div className="flex-1"></div>
|
||||
<div className="flex justify-end z-10">
|
||||
@@ -551,7 +581,7 @@ const SkinsView = memo(function SkinsView() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pr-2 flex flex-wrap gap-x-8 gap-y-6 items-start content-start justify-center">
|
||||
<div className="flex-1 overflow-y-auto pr-2 flex flex-wrap gap-x-4 gap-y-6 items-start content-start justify-center hidden-scrollbar">
|
||||
{viewMode === "skin" ? (
|
||||
savedSkins.map((skin, i) => {
|
||||
const idx = SKINS_START_INDEX + i;
|
||||
@@ -583,21 +613,7 @@ const SkinsView = memo(function SkinsView() {
|
||||
onClick={() => handleSkinSelect(skin)}
|
||||
className={`w-16 h-16 bg-black/40 border-2 shadow-inner relative cursor-pointer overflow-hidden transition-colors outline-none ${isActive || isFocused ? "border-[#FFFF55]" : "border-[#373737] hover:border-[#A0A0A0]"}`}
|
||||
>
|
||||
<img
|
||||
src={skin.url}
|
||||
draggable={false}
|
||||
alt={skin.name}
|
||||
className="absolute max-w-none"
|
||||
style={{
|
||||
width: "800%",
|
||||
height: "auto",
|
||||
left: "-100%",
|
||||
top: "-100%",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<HeadPreview src={skin.url} />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
@@ -668,21 +684,7 @@ const SkinsView = memo(function SkinsView() {
|
||||
onClick={() => handleCapeSelect(cape)}
|
||||
className={`w-16 h-16 bg-black/40 border-2 shadow-inner relative cursor-pointer overflow-hidden transition-colors outline-none ${isActive || isFocused ? "border-[#FFFF55]" : "border-[#373737] hover:border-[#A0A0A0]"}`}
|
||||
>
|
||||
<img
|
||||
src={cape.url}
|
||||
draggable={false}
|
||||
alt={cape.name}
|
||||
className="absolute max-w-none"
|
||||
style={{
|
||||
width: "auto",
|
||||
height: "100%",
|
||||
left: "0",
|
||||
top: "0",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<CapePreview src={cape.url} />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
@@ -727,7 +729,7 @@ const SkinsView = memo(function SkinsView() {
|
||||
playBackSound();
|
||||
setActiveView("main");
|
||||
}}
|
||||
className={`w-72 h-14 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-2 outline-none border-none hover:text-[#FFFF55] ${focusIndex === BACK_BUTTON_INDEX ? "text-[#FFFF55]" : "text-white"}`}
|
||||
className={`w-72 h-14 shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-2 outline-none border-none hover:text-[#FFFF55] ${focusIndex === BACK_BUTTON_INDEX ? "text-[#FFFF55]" : "text-white"}`}
|
||||
style={{
|
||||
backgroundImage:
|
||||
focusIndex === BACK_BUTTON_INDEX
|
||||
|
||||
@@ -178,7 +178,7 @@ export default function SwfView() {
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col items-center w-full max-w-6xl h-[85vh] outline-none"
|
||||
className="flex flex-col items-center w-full max-w-6xl h-full outline-none"
|
||||
>
|
||||
<div className="w-full flex justify-between items-center mb-4 px-8">
|
||||
<h2 className="text-2xl text-white mc-text-shadow border-b-2 border-[#373737] pb-1 tracking-widest uppercase font-bold">
|
||||
@@ -308,7 +308,7 @@ export default function SwfView() {
|
||||
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="w-72 h-14 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-6 outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
className="w-72 h-14 shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-6 outline-none border-none hover:text-[#FFFF55] text-white"
|
||||
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}
|
||||
>
|
||||
Back
|
||||
|
||||
@@ -124,6 +124,55 @@ interface PluginRegistryEntry {
|
||||
}
|
||||
|
||||
const COLS = 4;
|
||||
interface ZipGroup {
|
||||
main: string;
|
||||
parts: string[];
|
||||
dest: string;
|
||||
}
|
||||
function groupZips(zips: Record<string, string>): ZipGroup[] {
|
||||
const entries = Object.entries(zips).sort(([a], [b]) => a.localeCompare(b));
|
||||
const consumed = new Set<string>();
|
||||
const groups: ZipGroup[] = [];
|
||||
for (const [name, dest] of entries) {
|
||||
if (consumed.has(name)) continue;
|
||||
if (!name.toLowerCase().endsWith(".zip")) continue;
|
||||
const base = name.slice(0, -4);
|
||||
const parts = [name];
|
||||
consumed.add(name);
|
||||
let n = 1;
|
||||
for (;;) {
|
||||
const cand = `${base}.z${String(n).padStart(2, "0")}`;
|
||||
if (zips[cand] !== undefined) {
|
||||
parts.push(cand);
|
||||
consumed.add(cand);
|
||||
n++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (parts.length === 1) {
|
||||
n = 1;
|
||||
for (;;) {
|
||||
const cand = `${base}.zip.${String(n).padStart(3, "0")}`;
|
||||
if (zips[cand] !== undefined) {
|
||||
parts.push(cand);
|
||||
consumed.add(cand);
|
||||
n++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
groups.push({ main: name, parts, dest });
|
||||
}
|
||||
for (const [name, dest] of entries) {
|
||||
if (!consumed.has(name)) {
|
||||
consumed.add(name);
|
||||
groups.push({ main: name, parts: [name], dest });
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
interface WorkshopViewProps {
|
||||
workshopTarget?: { id: string; type?: string } | null;
|
||||
onClearWorkshopTarget?: () => void;
|
||||
@@ -1736,21 +1785,36 @@ function PackageModal({
|
||||
Files
|
||||
</span>
|
||||
<div className="space-y-1.5">
|
||||
{Object.entries(pkg.zips).map(([file, dest]) => (
|
||||
<div
|
||||
key={file}
|
||||
className="flex items-center justify-between gap-4 bg-black/20 p-2 rounded-sm border border-[#222]"
|
||||
>
|
||||
<span className="text-xs text-[#A0A0A0] mc-text-shadow font-mono">
|
||||
{file}
|
||||
</span>
|
||||
{dest && (
|
||||
<span className="text-[9px] text-[#fff] mc-text-shadow truncate uppercase tracking-tighter">
|
||||
{dest}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{groupZips(pkg.zips).map((group) => {
|
||||
const isSplit = group.parts.length > 1;
|
||||
return (
|
||||
<div
|
||||
key={group.main}
|
||||
className="flex items-center justify-between gap-4 bg-black/20 p-2 rounded-sm border border-[#222]"
|
||||
>
|
||||
<div className="flex flex-col gap-0.5 min-w-0">
|
||||
<span className="text-xs text-[#A0A0A0] mc-text-shadow font-mono">
|
||||
{group.main}
|
||||
{isSplit && (
|
||||
<span className="ml-2 text-[8px] text-[#FFFF55] bg-black/60 border border-[#555] px-1.5 py-0.5 uppercase tracking-widest">
|
||||
Split archive · {group.parts.length} parts
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{isSplit && (
|
||||
<span className="text-[9px] text-[#666] mc-text-shadow font-mono">
|
||||
{group.parts.slice(1).join(", ")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{group.dest && (
|
||||
<span className="text-[9px] text-[#fff] mc-text-shadow truncate uppercase tracking-tighter">
|
||||
{group.dest}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1963,10 +2027,37 @@ function InstallModal({
|
||||
"idle" | "installing" | "success" | "error"
|
||||
>("idle");
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [progressLabel, setProgressLabel] = useState<string | null>(null);
|
||||
const [pkgPct, setPkgPct] = useState<Record<string, number>>({});
|
||||
|
||||
const installTargets = useMemo(() => {
|
||||
const targets: { id: string; zips: number }[] = [];
|
||||
for (const depId of dependencies) {
|
||||
const dep = allPackages.find((p) => p.id === depId);
|
||||
if (dep?.zips) targets.push({ id: dep.id, zips: Object.keys(dep.zips).length });
|
||||
}
|
||||
if (pkg.zips) targets.push({ id: pkg.id, zips: Object.keys(pkg.zips).length });
|
||||
return targets;
|
||||
}, [dependencies, allPackages, pkg.zips, pkg.id]);
|
||||
|
||||
const totalZips = installTargets.reduce((acc, t) => acc + t.zips, 0);
|
||||
|
||||
const overallProgress = useMemo(() => {
|
||||
if (totalZips === 0) return 0;
|
||||
let done = 0;
|
||||
for (const t of installTargets) {
|
||||
const pct = pkgPct[t.id];
|
||||
if (pct !== undefined) done += (pct / 100) * t.zips;
|
||||
}
|
||||
return Math.min(100, Math.round((done / totalZips) * 100));
|
||||
}, [pkgPct, installTargets, totalZips]);
|
||||
|
||||
const installPlugin = useCallback(async () => {
|
||||
setStatus("installing");
|
||||
setErrorMsg(null);
|
||||
setProgress(0);
|
||||
setProgressLabel(null);
|
||||
playPressSound();
|
||||
try {
|
||||
const pluginsDir = await TauriService.getPluginsDir();
|
||||
@@ -1994,7 +2085,10 @@ function InstallModal({
|
||||
const pluginBaseUrl = `${RAW_BASE}/.00plugins/${pkg.id}`;
|
||||
|
||||
const allFiles = [pkg.main || "main.js", ...(pkg.files || [])];
|
||||
for (const file of allFiles) {
|
||||
for (let i = 0; i < allFiles.length; i++) {
|
||||
const file = allFiles[i];
|
||||
setProgressLabel(file);
|
||||
setProgress(Math.round((i / allFiles.length) * 100));
|
||||
const res = await TauriService.httpProxyRequest(
|
||||
"GET",
|
||||
`${pluginBaseUrl}/${file}`,
|
||||
@@ -2006,6 +2100,7 @@ function InstallModal({
|
||||
`${pluginDir}/${file}`,
|
||||
encoder.encode(res.body),
|
||||
);
|
||||
setProgress(Math.round(((i + 1) / allFiles.length) * 100));
|
||||
}
|
||||
|
||||
await PluginManager.instance.reload();
|
||||
@@ -2063,6 +2158,7 @@ function InstallModal({
|
||||
for (const depId of dependencies) {
|
||||
const depPkg = allPackages.find((p) => p.id === depId);
|
||||
if (!depPkg || !depPkg.zips) continue;
|
||||
setProgressLabel(depPkg.name);
|
||||
try {
|
||||
await TauriService.workshopInstall(
|
||||
instanceId,
|
||||
@@ -2079,11 +2175,18 @@ function InstallModal({
|
||||
const installTo = async (instanceId: string) => {
|
||||
setStatus("installing");
|
||||
setErrorMsg(null);
|
||||
setProgress(0);
|
||||
setProgressLabel(null);
|
||||
setPkgPct({});
|
||||
playPressSound();
|
||||
const unlisten = await TauriService.onWorkshopProgress((data) => {
|
||||
setPkgPct((prev) => ({ ...prev, [data.packageId]: data.percent }));
|
||||
});
|
||||
try {
|
||||
if (dependencies.length > 0) {
|
||||
await installDeps(instanceId);
|
||||
}
|
||||
setProgressLabel(pkg.name);
|
||||
await TauriService.workshopInstall(
|
||||
instanceId,
|
||||
pkg.id,
|
||||
@@ -2101,6 +2204,8 @@ function InstallModal({
|
||||
? e
|
||||
: "Unknown error",
|
||||
);
|
||||
} finally {
|
||||
unlisten();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2153,6 +2258,25 @@ function InstallModal({
|
||||
? "Downloading and extracting required dependencies"
|
||||
: "Downloading and extracting assets"}
|
||||
</span>
|
||||
<div className="w-full flex flex-col gap-1 px-2 mt-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[11px] text-[#FFFF55] mc-text-shadow truncate">
|
||||
{progressLabel ||
|
||||
(isPluginTab ? "Downloading plugin files" : "Downloading assets")}
|
||||
</span>
|
||||
<span className="text-[11px] text-[#FFFF55] mc-text-shadow shrink-0">
|
||||
{Math.floor(isPluginTab ? progress : overallProgress)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-3 border-2 border-[#3F3F3F] bg-black/60 p-0.5">
|
||||
<div
|
||||
className="h-full bg-[#FFFF55]"
|
||||
style={{
|
||||
width: `${isPluginTab ? progress : overallProgress}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{dependencies.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 justify-center mt-2">
|
||||
{dependencies.map((depId) => {
|
||||
|
||||
@@ -48,6 +48,8 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
||||
setProfile: configRaw.setProfile,
|
||||
customEditions: configRaw.customEditions,
|
||||
setCustomEditions: configRaw.setCustomEditions,
|
||||
customPaths: configRaw.customPaths,
|
||||
setCustomPaths: configRaw.setCustomPaths,
|
||||
customizations: configRaw.customizations,
|
||||
setCustomizations: configRaw.setCustomizations,
|
||||
extraLaunchArgs: configRaw.extraLaunchArgs,
|
||||
@@ -64,6 +66,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
||||
configRaw.username, configRaw.theme, configRaw.layout, configRaw.vfxEnabled,
|
||||
configRaw.rpcEnabled, configRaw.musicVol, configRaw.sfxVol, configRaw.isDayTime,
|
||||
configRaw.profile, configRaw.linuxRunner, configRaw.perfBoost, configRaw.customEditions,
|
||||
configRaw.customPaths,
|
||||
configRaw.customizations,
|
||||
configRaw.legacyMode, configRaw.animationsEnabled, configRaw.mangohudEnabled,
|
||||
configRaw.extraLaunchArgs, configRaw.launchPrefix, configRaw.launchEnvVars, configRaw.startFullscreen,
|
||||
@@ -80,6 +83,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
||||
gameRaw.handleLaunch, gameRaw.stopGame, gameRaw.addCustomEdition,
|
||||
gameRaw.deleteCustomEdition, gameRaw.downloadRunner,
|
||||
gameRaw.customizations, gameRaw.updateCustomization,
|
||||
gameRaw.gameLog, gameRaw.clearGameLog,
|
||||
]);
|
||||
|
||||
const audio = useMemo(() => audioRaw, [
|
||||
@@ -126,6 +130,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
||||
appleSiliconPerformanceBoost: config.perfBoost,
|
||||
profile: config.profile,
|
||||
customEditions: config.customEditions,
|
||||
customPaths: config.customPaths,
|
||||
customizations: config.customizations,
|
||||
animationsEnabled: config.animationsEnabled,
|
||||
vfxEnabled: config.vfxEnabled,
|
||||
@@ -144,6 +149,7 @@ export function LauncherProvider({ children }: { children: React.ReactNode }) {
|
||||
}, [
|
||||
config.username, skinSync.skinBase64, config.theme, config.linuxRunner,
|
||||
config.perfBoost, config.customEditions, config.profile,
|
||||
config.customPaths,
|
||||
config.customizations, config.vfxEnabled, config.animationsEnabled,
|
||||
config.rpcEnabled, config.musicVol, config.sfxVol, config.legacyMode,
|
||||
config.mangohudEnabled, config.extraLaunchArgs, config.launchPrefix,
|
||||
|
||||
@@ -230,6 +230,14 @@ body {
|
||||
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
|
||||
}
|
||||
|
||||
.hidden-scrollbar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.hidden-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-animations * {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
|
||||
@@ -19,6 +19,7 @@ export function useAppConfig() {
|
||||
const [linuxRunner, setLinuxRunner] = useState<string | undefined>();
|
||||
const [perfBoost, setPerfBoost] = useState(false);
|
||||
const [customEditions, setCustomEditions] = useState<CustomEdition[]>([]);
|
||||
const [customPaths, setCustomPaths] = useState<Record<string, string>>({});
|
||||
const [customizations, setCustomizations] = useState<Record<string, { titleImage?: string; panorama?: string }>>({});
|
||||
const [mangohudEnabled, setMangohudEnabled] = useState(false);
|
||||
const [extraLaunchArgs, setExtraLaunchArgs] = useState<string[] | undefined>();
|
||||
@@ -33,6 +34,7 @@ export function useAppConfig() {
|
||||
if (config.appleSiliconPerformanceBoost !== undefined)
|
||||
setPerfBoost(config.appleSiliconPerformanceBoost);
|
||||
if (config.customEditions) setCustomEditions(config.customEditions);
|
||||
if (config.customPaths) setCustomPaths(config.customPaths);
|
||||
if (config.customizations) setCustomizations(config.customizations);
|
||||
if (config.profile) setProfile(config.profile);
|
||||
if (config.vfxEnabled !== undefined) setVfxEnabled(config.vfxEnabled);
|
||||
@@ -60,6 +62,7 @@ export function useAppConfig() {
|
||||
appleSiliconPerformanceBoost: perfBoost,
|
||||
profile,
|
||||
customEditions,
|
||||
customPaths,
|
||||
customizations,
|
||||
animationsEnabled,
|
||||
vfxEnabled,
|
||||
@@ -75,7 +78,7 @@ export function useAppConfig() {
|
||||
skipIntro,
|
||||
}).catch(console.error);
|
||||
}
|
||||
}, [username, theme, linuxRunner, perfBoost, profile, customEditions, customizations, animationsEnabled, vfxEnabled, rpcEnabled, startFullscreen, musicVol, sfxVol, legacyMode, mangohudEnabled, extraLaunchArgs, launchPrefix, launchEnvVars, skipIntro, isLoaded]);
|
||||
}, [username, theme, linuxRunner, perfBoost, profile, customEditions, customPaths, customizations, animationsEnabled, vfxEnabled, rpcEnabled, startFullscreen, musicVol, sfxVol, legacyMode, mangohudEnabled, extraLaunchArgs, launchPrefix, launchEnvVars, skipIntro, isLoaded]);
|
||||
|
||||
return {
|
||||
username,
|
||||
@@ -108,6 +111,8 @@ export function useAppConfig() {
|
||||
setPerfBoost,
|
||||
customEditions,
|
||||
setCustomEditions,
|
||||
customPaths,
|
||||
setCustomPaths,
|
||||
customizations,
|
||||
setCustomizations,
|
||||
isLoaded,
|
||||
|
||||
+32
-10
@@ -71,7 +71,7 @@ export const BASE_EDITIONS = [
|
||||
id: "moon_edition",
|
||||
name: "Minecraft: Moon Edition",
|
||||
desc: "Galacticraft LCE port (Modded build!)",
|
||||
url: "https://github.com/blazin-blaze/moon-edition/releases/download/v1.0.1/moonEditionWindows64.zip",
|
||||
url: "https://github.com/blazin-blaze/moon-edition/releases/latest/download/moonEditionWindows64.zip",
|
||||
titleImage: "/images/minecraft_title_moon.png",
|
||||
supportsSlimSkins: false,
|
||||
logo: "/images/moonEdition.png",
|
||||
@@ -113,6 +113,8 @@ interface GameManagerProps {
|
||||
setProfile: (id: string) => void;
|
||||
customEditions: CustomEdition[];
|
||||
setCustomEditions: (editions: CustomEdition[]) => void;
|
||||
customPaths: Record<string, string>;
|
||||
setCustomPaths: Dispatch<SetStateAction<Record<string, string>>>;
|
||||
customizations: Record<string, { titleImage?: string; panorama?: string }>;
|
||||
setCustomizations: Dispatch<
|
||||
SetStateAction<Record<string, { titleImage?: string; panorama?: string }>>
|
||||
@@ -142,6 +144,7 @@ export function useGameManager({
|
||||
setProfile,
|
||||
customEditions,
|
||||
setCustomEditions,
|
||||
setCustomPaths,
|
||||
customizations,
|
||||
setCustomizations,
|
||||
extraLaunchArgs,
|
||||
@@ -157,6 +160,8 @@ export function useGameManager({
|
||||
number | null
|
||||
>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [gameLog, setGameLog] = useState<string | null>(null);
|
||||
const gameLogRef = useRef(false);
|
||||
const [gameUpdateMessage, setGameUpdateMessage] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
@@ -392,11 +397,18 @@ export function useGameManager({
|
||||
const unlistenRetry = TauriService.onDownloadRetry((attempt) => {
|
||||
setError(`Download failed, retrying (${attempt}/3)...`);
|
||||
});
|
||||
const unlistenGameLog = TauriService.onGameLog((log) => {
|
||||
gameLogRef.current = true;
|
||||
setError(null);
|
||||
setGameLog(log);
|
||||
getCurrentWindow().unminimize();
|
||||
});
|
||||
return () => {
|
||||
unlistenDownload.then((u) => u());
|
||||
unlistenRunner.then((u) => u());
|
||||
unlistenError.then((u) => u());
|
||||
unlistenRetry.then((u) => u());
|
||||
unlistenGameLog.then((u) => u());
|
||||
};
|
||||
}, [customEditions, checkInstalls]);
|
||||
|
||||
@@ -513,13 +525,15 @@ export function useGameManager({
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
console.error(e);
|
||||
setError(
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: typeof e === "string"
|
||||
? e
|
||||
: "Failed to launch game",
|
||||
);
|
||||
if (!gameLogRef.current) {
|
||||
setError(
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: typeof e === "string"
|
||||
? e
|
||||
: "Failed to launch game",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
setIsGameRunning(false);
|
||||
}
|
||||
@@ -597,10 +611,16 @@ export function useGameManager({
|
||||
[instanceId]: path,
|
||||
};
|
||||
await TauriService.saveConfig(config);
|
||||
setCustomPaths((prev) => ({ ...prev, [instanceId]: path }));
|
||||
},
|
||||
[],
|
||||
[setCustomPaths],
|
||||
);
|
||||
|
||||
const clearGameLog = useCallback(() => {
|
||||
gameLogRef.current = false;
|
||||
setGameLog(null);
|
||||
}, []);
|
||||
|
||||
const addToSteam = useCallback(
|
||||
async (
|
||||
id: string,
|
||||
@@ -638,6 +658,8 @@ export function useGameManager({
|
||||
runnerDownloadProgress,
|
||||
error,
|
||||
setError,
|
||||
gameLog,
|
||||
clearGameLog,
|
||||
editions,
|
||||
toggleInstall,
|
||||
handleUninstall,
|
||||
@@ -660,4 +682,4 @@ export function useGameManager({
|
||||
updateCustomization,
|
||||
saveCustomPath,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { lceOnlineService } from "../services/LceOnlineService";
|
||||
import { lceOnlineService, SocialEntry, InviteEntry } from "../services/LceOnlineService";
|
||||
export function useLceOnlineNotifications() {
|
||||
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<InviteEntry[]>([]);
|
||||
const [requests, setRequests] = useState<SocialEntry[]>([]);
|
||||
const seenRequests = useRef<Set<string>>(new Set());
|
||||
const seenInvites = useRef<Set<string>>(new Set());
|
||||
useEffect(() => {
|
||||
@@ -20,11 +15,12 @@ export function useLceOnlineNotifications() {
|
||||
const poll = async () => {
|
||||
if (!lceOnlineService.signedIn) return;
|
||||
try {
|
||||
const lists = await lceOnlineService.getSocialLists();
|
||||
lists.requests.forEach((r: string) => {
|
||||
if (!seenRequests.current.has(r)) {
|
||||
seenRequests.current.add(r);
|
||||
setFriendRequestMessage(`${r} wants to be friends!`);
|
||||
const requestsData = await lceOnlineService.getSocialLists();
|
||||
setRequests(requestsData.requests);
|
||||
requestsData.requests.forEach((r) => {
|
||||
if (!seenRequests.current.has(r.username)) {
|
||||
seenRequests.current.add(r.username);
|
||||
setFriendRequestMessage(`${r.displayName} wants to be friends!`);
|
||||
}
|
||||
});
|
||||
} catch (e) {}
|
||||
@@ -34,7 +30,7 @@ export function useLceOnlineNotifications() {
|
||||
invitesData.forEach((i) => {
|
||||
if (!seenInvites.current.has(i.inviteid)) {
|
||||
seenInvites.current.add(i.inviteid);
|
||||
setInviteMessage(`${i.from.username} invited you to play!`);
|
||||
setInviteMessage(`${i.from.displayName} invited you to play!`);
|
||||
}
|
||||
});
|
||||
} catch {}
|
||||
@@ -43,10 +39,11 @@ export function useLceOnlineNotifications() {
|
||||
const init = async () => {
|
||||
if (lceOnlineService.signedIn) {
|
||||
try {
|
||||
const lists = await lceOnlineService.getSocialLists();
|
||||
lists.requests.forEach((r: string) => {
|
||||
if (!seenRequests.current.has(r)) {
|
||||
seenRequests.current.add(r);
|
||||
const requestData = await lceOnlineService.getSocialLists();
|
||||
setRequests(requestData.requests);
|
||||
requestData.requests.forEach((r) => {
|
||||
if (!seenRequests.current.has(r.username)) {
|
||||
seenRequests.current.add(r.username);
|
||||
}
|
||||
});
|
||||
} catch (e) {}
|
||||
@@ -56,7 +53,7 @@ export function useLceOnlineNotifications() {
|
||||
invitesData.forEach((i) => {
|
||||
if (!seenInvites.current.has(i.inviteid)) {
|
||||
seenInvites.current.add(i.inviteid);
|
||||
setInviteMessage(`${i.from.username} invited you to play!`);
|
||||
setInviteMessage(`${i.from.displayName} invited you to play!`);
|
||||
}
|
||||
});
|
||||
} catch {}
|
||||
@@ -76,5 +73,6 @@ export function useLceOnlineNotifications() {
|
||||
clearFriendRequestMessage: () => setFriendRequestMessage(null),
|
||||
clearInviteMessage: () => setInviteMessage(null),
|
||||
invites,
|
||||
requests,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -103,9 +103,10 @@ export function useSkinSync({ username, profile, editions }: UseSkinSyncProps) {
|
||||
};
|
||||
|
||||
const seededId = getSeededId(username);
|
||||
const packId = seededId.slice(-4);
|
||||
//neo: (comment reason stated below) const packId = seededId.slice(-4);
|
||||
const files: PCKAsset[] = [
|
||||
{
|
||||
/*neo: commented because it causes neoLegacy to think its a Texture Pack, while it does nothing when removed. preserved for future reference.
|
||||
{
|
||||
id: "0",
|
||||
path: "0",
|
||||
type: PCKAssetType.INFO,
|
||||
@@ -117,7 +118,7 @@ export function useSkinSync({ username, profile, editions }: UseSkinSyncProps) {
|
||||
value: packId,
|
||||
},
|
||||
],
|
||||
},
|
||||
},*/
|
||||
{
|
||||
id: `dlcskin${seededId}`,
|
||||
path: `dlcskin${seededId}.png`,
|
||||
|
||||
+30
-22
@@ -27,6 +27,7 @@ import { CinematicIntro } from "../components/common/CinematicIntro";
|
||||
import { DownloadOverlay } from "../components/layout/DownloadOverlay";
|
||||
import { AppHeader } from "../components/layout/AppHeader";
|
||||
import { AchievementToast } from "../components/common/AchievementToast";
|
||||
import GameLogModal from "../components/modals/GameLogModal";
|
||||
import {
|
||||
useUI,
|
||||
useConfig,
|
||||
@@ -74,7 +75,7 @@ export default function App() {
|
||||
InviteMessage,
|
||||
clearFriendRequestMessage,
|
||||
clearInviteMessage,
|
||||
invites
|
||||
invites,
|
||||
} = notifications;
|
||||
const [showSetup, setShowSetup] = useState(false);
|
||||
const [isSetupChecked, setIsSetupChecked] = useState(false);
|
||||
@@ -325,9 +326,9 @@ export default function App() {
|
||||
<div
|
||||
className={`w-screen h-screen overflow-hidden select-none flex flex-col relative bg-black text-white font-['Mojangles'] outline-none focus:outline-none ${!config.animationsEnabled ? "no-animations" : ""}`}
|
||||
>
|
||||
{showHeader && (
|
||||
<AppHeader playPressSound={audio.playPressSound} uiFade={uiFade} />
|
||||
)}
|
||||
{showHeader && (
|
||||
<AppHeader playPressSound={audio.playPressSound} uiFade={uiFade} />
|
||||
)}
|
||||
<div className="absolute inset-0">
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
@@ -398,7 +399,16 @@ export default function App() {
|
||||
editions={game.editions}
|
||||
/>
|
||||
|
||||
<AchievementToast message={game.error} onClose={clearError} />
|
||||
<AchievementToast
|
||||
message={game.gameLog ? null : game.error}
|
||||
onClose={clearError}
|
||||
/>
|
||||
<GameLogModal
|
||||
isOpen={!!game.gameLog}
|
||||
log={game.gameLog}
|
||||
onClose={game.clearGameLog}
|
||||
playBackSound={audio.playBackSound}
|
||||
/>
|
||||
|
||||
<AchievementToast
|
||||
message={updateMessage}
|
||||
@@ -571,25 +581,25 @@ export default function App() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="flex-1 w-full relative">
|
||||
<main className="flex-1 w-full relative min-h-0">
|
||||
<div
|
||||
className={`w-full h-full flex flex-col items-center justify-center ${isUiHidden ? "opacity-0 pointer-events-none" : "opacity-100"}`}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
{activeView === "main" && (
|
||||
<SkinViewer
|
||||
key="skin-viewer"
|
||||
username={config.username}
|
||||
setUsername={config.setUsername}
|
||||
playPressSound={audio.playPressSound}
|
||||
skinUrl={skinUrl}
|
||||
capeUrl={config.legacyMode ? null : capeUrl}
|
||||
setSkinUrl={setSkinUrl}
|
||||
setActiveView={setActiveView}
|
||||
setIsUiHidden={setIsUiHidden}
|
||||
isFocusedSection={focusSection === "skin"}
|
||||
onNavigateRight={onNavigateToMenu}
|
||||
/>
|
||||
<SkinViewer
|
||||
key="skin-viewer"
|
||||
username={config.username}
|
||||
setUsername={config.setUsername}
|
||||
playPressSound={audio.playPressSound}
|
||||
skinUrl={skinUrl}
|
||||
capeUrl={config.legacyMode ? null : capeUrl}
|
||||
setSkinUrl={setSkinUrl}
|
||||
setActiveView={setActiveView}
|
||||
setIsUiHidden={setIsUiHidden}
|
||||
isFocusedSection={focusSection === "skin"}
|
||||
onNavigateRight={onNavigateToMenu}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
@@ -612,9 +622,7 @@ export default function App() {
|
||||
{activeView === "devtools" && (
|
||||
<DevtoolsView key="devtools-view" />
|
||||
)}
|
||||
{activeView === "guides" && (
|
||||
<GuidesView key="guides-view" />
|
||||
)}
|
||||
{activeView === "guides" && <GuidesView key="guides-view" />}
|
||||
{activeView === "pck-editor" && (
|
||||
<PckEditorView key="pck-editor-view" />
|
||||
)}
|
||||
|
||||
@@ -12,9 +12,22 @@ export interface SessionData {
|
||||
account: LceOnlineAccount;
|
||||
}
|
||||
|
||||
export interface FriendRequest {
|
||||
export interface InviteEntry {
|
||||
inviteid: string;
|
||||
from: { uuid: string; displayName: string; username: string };
|
||||
sessionid: string;
|
||||
}
|
||||
|
||||
export interface SocialEntry {
|
||||
username: string;
|
||||
displayName: string;
|
||||
uuid: string;
|
||||
};
|
||||
|
||||
export interface SocialList {
|
||||
friends: SocialEntry[];
|
||||
friendRequests: SocialEntry[];
|
||||
blocked: SocialEntry[];
|
||||
}
|
||||
|
||||
export class LceOnlineService {
|
||||
@@ -220,25 +233,13 @@ export class LceOnlineService {
|
||||
}
|
||||
}
|
||||
|
||||
async getSocialLists(): Promise<{
|
||||
friends: string[];
|
||||
requests: string[];
|
||||
blocked: string[];
|
||||
}> {
|
||||
const raw: string = await this.request<string>(
|
||||
"POST",
|
||||
"/getSocialLists",
|
||||
null,
|
||||
);
|
||||
if (typeof raw !== "string") {
|
||||
return { friends: [], requests: [], blocked: [] };
|
||||
}
|
||||
const withoutPrefix = raw.startsWith("-") ? raw.slice(1) : raw;
|
||||
const parts = withoutPrefix.split("|");
|
||||
async getSocialLists() {
|
||||
const res = await this.request<SocialList>("GET", "/getSocialLists", null);
|
||||
if (typeof res === "string") throw new Error(res);
|
||||
return {
|
||||
friends: parts[0] ? parts[0].split(",").filter(Boolean) : [],
|
||||
requests: parts[1] ? parts[1].split(",").filter(Boolean) : [],
|
||||
blocked: parts[2] ? parts[2].split(",").filter(Boolean) : [],
|
||||
friends: res?.friends ?? [],
|
||||
requests: res?.friendRequests ?? [],
|
||||
blocked: res?.blocked ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -295,7 +296,7 @@ export class LceOnlineService {
|
||||
async getInvites(): Promise<
|
||||
Array<{
|
||||
inviteid: string;
|
||||
from: { uuid: string; username: string };
|
||||
from: { uuid: string; displayName: string; username: string };
|
||||
sessionid: string;
|
||||
}>
|
||||
> {
|
||||
|
||||
@@ -194,6 +194,12 @@ export class TauriService {
|
||||
);
|
||||
}
|
||||
|
||||
static onWorkshopProgress(callback: (data: { packageId: string; percent: number }) => void) {
|
||||
return listen<{ instanceId: string; percent: number }>("workshop-progress", (event) =>
|
||||
callback({ packageId: event.payload.instanceId, percent: event.payload.percent }),
|
||||
);
|
||||
}
|
||||
|
||||
static onRunnerDownloadProgress(callback: (percent: number) => void) {
|
||||
return listen<number>("runner-download-progress", (event) =>
|
||||
callback(event.payload),
|
||||
@@ -212,6 +218,12 @@ export class TauriService {
|
||||
);
|
||||
}
|
||||
|
||||
static onGameLog(callback: (log: string) => void) {
|
||||
return listen<string>("game-log", (event) =>
|
||||
callback(event.payload),
|
||||
);
|
||||
}
|
||||
|
||||
static onDownloadRetry(callback: (attempt: number) => void) {
|
||||
return listen<number>("download-retry", (event) =>
|
||||
callback(event.payload),
|
||||
@@ -420,4 +432,25 @@ export class TauriService {
|
||||
): Promise<string> {
|
||||
return invoke("import_world", { inputPath, outputPath });
|
||||
}
|
||||
|
||||
static async importLceSave(
|
||||
inputPath: string,
|
||||
outputDir: string,
|
||||
): Promise<string> {
|
||||
return invoke("import_lce_save", { inputPath, outputDir });
|
||||
}
|
||||
|
||||
static async javaToLce(
|
||||
javaWorldPath: string,
|
||||
outputMsPath: string,
|
||||
): Promise<string> {
|
||||
return invoke("java_to_lce", { javaWorldPath, outputMsPath });
|
||||
}
|
||||
|
||||
static async lceToJava(
|
||||
inputMsPath: string,
|
||||
javaWorldOutput: string,
|
||||
): Promise<string> {
|
||||
return invoke("lce_to_java", { inputMsPath, javaWorldOutput });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user