feat: xbox360+ps3+java->windows64 world conversion (#145)

Co-authored-by: str1k3r <[email protected]>
This commit is contained in:
/home/neo
2026-08-07 19:01:24 +03:00
committed by GitHub
co-authored by str1k3r
parent cb684177fe
commit d536a6590b
52 changed files with 8530 additions and 476 deletions
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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:
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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={{
+142 -16
View File
@@ -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">
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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"
+3 -3
View 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
+2 -2
View File
@@ -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%",
+100 -98
View File
@@ -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
+2 -2
View File
@@ -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
+140 -16
View File
@@ -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) => {