mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-24 09:03:44 +00:00
934 lines
35 KiB
TypeScript
934 lines
35 KiB
TypeScript
import { useState, useEffect, useRef, useMemo, memo } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import {
|
|
useUI,
|
|
useConfig,
|
|
useAudio,
|
|
useGame,
|
|
} from "../../context/LauncherContext";
|
|
import ChooseInstanceModal from "../modals/ChooseInstanceModal";
|
|
import { usePlatform } from "../../hooks/usePlatform";
|
|
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";
|
|
interface LceOnlineViewProps {
|
|
addFriendTarget?: string | null;
|
|
onClearAddFriendTarget?: () => void;
|
|
invites?: Array<{
|
|
inviteid: string;
|
|
from: { uuid: string; username: string };
|
|
sessionid: string;
|
|
}>;
|
|
}
|
|
const LceOnlineView = memo(function LceOnlineView({
|
|
addFriendTarget,
|
|
onClearAddFriendTarget,
|
|
invites: invitesProp,
|
|
}: LceOnlineViewProps) {
|
|
const { t } = useTranslation();
|
|
const { setActiveView, setIsUiHidden } = useUI();
|
|
const { animationsEnabled } = useConfig();
|
|
const { playPressSound, playBackSound } = useAudio();
|
|
const { isAndroid } = usePlatform();
|
|
const game = useGame();
|
|
const [isSignedIn, setIsSignedIn] = useState(lceOnlineService.signedIn);
|
|
const opened = useRef(false);
|
|
const [currentTab, setCurrentTab] = useState<
|
|
"friends" | "requests" | "invites"
|
|
>("friends");
|
|
const [focusIndex, setFocusIndex] = useState<number | null>(0);
|
|
const [friends, setFriends] = useState<SocialEntry[]>([]);
|
|
const [incomingReqs, setIncomingReqs] = useState<SocialEntry[]>([]);
|
|
const [outgoingReqs, setOutgoingReqs] = useState<SocialEntry[]>([]);
|
|
const invites = invitesProp ?? [];
|
|
const [isHosting, setIsHosting] = useState(lceOnlineService.isHosting);
|
|
const [isAddingFriend, setIsAddingFriend] = useState(false);
|
|
const [addFriendUsername, setAddFriendUsername] = useState("");
|
|
const addFriendInputRef = useRef<HTMLInputElement>(null);
|
|
const [errorModal, setErrorModal] = useState<string | null>(null);
|
|
const [joinTarget, setJoinTarget] = useState<{
|
|
inviteid: string;
|
|
sessionId: string;
|
|
hostName: string;
|
|
} | null>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const fetchSocialData = async () => {
|
|
if (!lceOnlineService.signedIn) return;
|
|
try {
|
|
const lists = await lceOnlineService.getSocialLists();
|
|
setFriends(lists.friends);
|
|
setIncomingReqs(lists.requests);
|
|
setOutgoingReqs([]);
|
|
} catch (e: unknown) {
|
|
console.error(e);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (isSignedIn) {
|
|
fetchSocialData();
|
|
}
|
|
}, [isSignedIn]);
|
|
|
|
useEffect(() => {
|
|
return lceOnlineService.onSessionChange(() => {
|
|
setIsSignedIn(lceOnlineService.signedIn);
|
|
setIsHosting(lceOnlineService.isHosting);
|
|
});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isSignedIn) return;
|
|
|
|
if (!opened.current) {
|
|
opened.current = true;
|
|
if (isAndroid) {
|
|
TauriService.startLceOnlineAuth()
|
|
.then((token) => {
|
|
lceOnlineService
|
|
.loginWithTokenAndFetchAccount(token)
|
|
.catch((e) => console.error(e));
|
|
setIsSignedIn(true);
|
|
})
|
|
.catch((e) => console.error("LCE Online auth failed", e));
|
|
} else {
|
|
new WebviewWindow('LCEOnline', {
|
|
url: "https://mclegacyedition.xyz/internal/auth?appId=emerald_launcher",
|
|
width: 400,
|
|
height: 570,
|
|
resizable: false,
|
|
title: 'Emerald Legacy Launcher - LCEOnline',
|
|
});
|
|
}
|
|
};
|
|
|
|
const unlisten = listen<string[]>('deep-link', async (event) => {
|
|
const authUrl = event.payload.find(u => u.startsWith('emerald://'));
|
|
if (!authUrl) return;
|
|
const token = new URL(authUrl).searchParams.get('token');
|
|
if (token) {
|
|
lceOnlineService
|
|
.loginWithTokenAndFetchAccount(token)
|
|
.catch((e) => console.error(e));
|
|
setIsSignedIn(true);
|
|
}
|
|
(await WebviewWindow.getByLabel('LCEOnline'))?.close();
|
|
});
|
|
|
|
return () => { unlisten.then(f => f()); };
|
|
}, [isSignedIn, isAndroid]);
|
|
|
|
useEffect(() => {
|
|
if (!addFriendTarget) return;
|
|
setCurrentTab("friends");
|
|
handleAction(() => lceOnlineService.sendFriendRequest(addFriendTarget));
|
|
onClearAddFriendTarget?.();
|
|
}, [addFriendTarget, onClearAddFriendTarget]);
|
|
|
|
const handleLogout = () => {
|
|
playPressSound();
|
|
lceOnlineService.logoutLocal();
|
|
setIsSignedIn(false);
|
|
};
|
|
|
|
const handleStartHosting = async () => {
|
|
playPressSound();
|
|
try {
|
|
const token = lceOnlineService.accessToken ?? "";
|
|
if (!token) return;
|
|
TauriService.startHostRelay(token, 25565).catch(() => {});
|
|
lceOnlineService.isHosting = true;
|
|
} catch (e: unknown) {
|
|
setErrorModal(e instanceof Error ? e.message : t("lceOnline.failedToStartHosting"));
|
|
}
|
|
};
|
|
|
|
const handleStopHosting = async () => {
|
|
playPressSound();
|
|
try {
|
|
await TauriService.stopAllProxies();
|
|
} catch (e: unknown) {
|
|
console.warn("Stop hosting failed", e);
|
|
}
|
|
lceOnlineService.isHosting = false;
|
|
};
|
|
|
|
const handleAction = async (action: () => Promise<void>) => {
|
|
playPressSound();
|
|
try {
|
|
await action();
|
|
fetchSocialData();
|
|
} catch (e: unknown) {
|
|
setErrorModal(e instanceof Error ? e.message : t("lceOnline.anErrorOccurred"));
|
|
}
|
|
};
|
|
|
|
type MenuItem = {
|
|
id: string;
|
|
type: "button" | "friend" | "request_in" | "request_out" | "invite";
|
|
label: string;
|
|
onClick: () => void;
|
|
onClickSecondary?: () => void;
|
|
};
|
|
|
|
const menuItems = useMemo<MenuItem[]>(() => {
|
|
const items: MenuItem[] = [];
|
|
if (currentTab === "friends") {
|
|
if (!isHosting) {
|
|
items.push({
|
|
id: "host_game",
|
|
type: "button",
|
|
label: t("lceOnline.hostGame"),
|
|
onClick: handleStartHosting,
|
|
});
|
|
} else {
|
|
items.push({
|
|
id: "stop_hosting",
|
|
type: "button",
|
|
label: t("lceOnline.stopHosting"),
|
|
onClick: handleStopHosting,
|
|
});
|
|
}
|
|
items.push({
|
|
id: "add_friend",
|
|
type: "button",
|
|
label: t("lceOnline.addFriend"),
|
|
onClick: () => {
|
|
playPressSound();
|
|
setIsAddingFriend(true);
|
|
setAddFriendUsername("");
|
|
},
|
|
});
|
|
items.push({
|
|
id: "sign_out",
|
|
type: "button",
|
|
label: t("lceOnline.signOut"),
|
|
onClick: handleLogout,
|
|
});
|
|
friends.forEach((f) => {
|
|
items.push({
|
|
id: `friend_${f.username}`,
|
|
type: "friend",
|
|
label: f.displayName || f.username,
|
|
onClick: () => handleAction(() => lceOnlineService.removeFriend(f.username)),
|
|
onClickSecondary: isHosting
|
|
? () => handleAction(() => lceOnlineService.sendInvite(f.username))
|
|
: undefined,
|
|
});
|
|
});
|
|
} else if (currentTab === "requests") {
|
|
incomingReqs.forEach((r) => {
|
|
items.push({
|
|
id: `req_in_${r.username}`,
|
|
type: "request_in",
|
|
label: r.displayName || r.username,
|
|
onClick: () =>
|
|
handleAction(() => lceOnlineService.acceptFriendRequest(r.username)),
|
|
onClickSecondary: () =>
|
|
handleAction(() => lceOnlineService.declineFriendRequest(r.username)),
|
|
});
|
|
});
|
|
outgoingReqs.forEach((r) => {
|
|
items.push({
|
|
id: `req_out_${r.username}`,
|
|
type: "request_out",
|
|
label: r.displayName || r.username,
|
|
onClick: () =>
|
|
handleAction(() => lceOnlineService.declineFriendRequest(r.username)),
|
|
});
|
|
});
|
|
} else if (currentTab === "invites") {
|
|
invites.forEach((inv) => {
|
|
items.push({
|
|
id: `invite_${inv.inviteid}`,
|
|
type: "invite",
|
|
label: inv.from.username,
|
|
onClick: () =>
|
|
handleAction(async () => {
|
|
const sessionId = await lceOnlineService.acceptInvite(
|
|
inv.from.username,
|
|
);
|
|
setJoinTarget({
|
|
inviteid: inv.inviteid,
|
|
sessionId,
|
|
hostName: inv.from.username,
|
|
});
|
|
}),
|
|
onClickSecondary: () =>
|
|
handleAction(() =>
|
|
lceOnlineService.declineInvite(inv.from.username),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
return items;
|
|
}, [
|
|
currentTab,
|
|
friends,
|
|
incomingReqs,
|
|
outgoingReqs,
|
|
invites,
|
|
playPressSound,
|
|
isHosting,
|
|
t,
|
|
]);
|
|
|
|
const tabs: ("friends" | "requests" | "invites")[] = [
|
|
"friends",
|
|
"requests",
|
|
"invites",
|
|
];
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (errorModal) {
|
|
if (e.key === "Escape" || e.key === "Enter") {
|
|
setErrorModal(null);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (isAddingFriend) {
|
|
if (e.key === "Escape") {
|
|
setIsAddingFriend(false);
|
|
playBackSound();
|
|
} else if (e.key === "Enter") {
|
|
if (addFriendUsername.trim() !== "") {
|
|
handleAction(() =>
|
|
lceOnlineService.sendFriendRequest(addFriendUsername.trim()),
|
|
);
|
|
setIsAddingFriend(false);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!isSignedIn) {
|
|
if (e.key === "Escape" || e.key === "Backspace") {
|
|
playBackSound();
|
|
setActiveView("main");
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (e.key === "Escape" || e.key === "Backspace") {
|
|
playBackSound();
|
|
setActiveView("main");
|
|
return;
|
|
}
|
|
|
|
const curIdx = tabs.indexOf(currentTab);
|
|
if (e.key === "q" || e.key === "Q" || e.key === "ArrowLeft") {
|
|
const next = curIdx > 0 ? tabs[curIdx - 1] : tabs[tabs.length - 1];
|
|
setCurrentTab(next);
|
|
setFocusIndex(0);
|
|
playPressSound();
|
|
return;
|
|
}
|
|
if (e.key === "e" || e.key === "E" || e.key === "ArrowRight") {
|
|
const next = curIdx < tabs.length - 1 ? tabs[curIdx + 1] : tabs[0];
|
|
setCurrentTab(next);
|
|
setFocusIndex(0);
|
|
playPressSound();
|
|
return;
|
|
}
|
|
|
|
const itemCount = menuItems.length;
|
|
if (itemCount > 0) {
|
|
if (e.key === "ArrowDown") {
|
|
setFocusIndex((prev) =>
|
|
prev === null || prev >= itemCount - 1 ? 0 : prev + 1,
|
|
);
|
|
} else if (e.key === "ArrowUp") {
|
|
setFocusIndex((prev) =>
|
|
prev === null || prev <= 0 ? itemCount - 1 : prev - 1,
|
|
);
|
|
} else if (e.key === "Enter" && focusIndex !== null) {
|
|
e.preventDefault();
|
|
menuItems[focusIndex]?.onClick();
|
|
}
|
|
}
|
|
};
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
}, [
|
|
focusIndex,
|
|
menuItems,
|
|
currentTab,
|
|
playBackSound,
|
|
setActiveView,
|
|
isAddingFriend,
|
|
addFriendUsername,
|
|
errorModal,
|
|
isSignedIn,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (isAddingFriend && addFriendInputRef.current) {
|
|
addFriendInputRef.current.focus();
|
|
} else if (focusIndex !== null) {
|
|
const el = containerRef.current?.querySelector(
|
|
`[data-index="${focusIndex}"]`,
|
|
) as HTMLElement;
|
|
if (el) {
|
|
el.focus();
|
|
if (scrollRef.current) {
|
|
const rect = el.getBoundingClientRect();
|
|
const scrollRect = scrollRef.current.getBoundingClientRect();
|
|
if (rect.bottom > scrollRect.bottom || rect.top < scrollRect.top) {
|
|
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, [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 (
|
|
<div className="flex flex-col items-center justify-center flex-1 text-center py-12">
|
|
<h2 className="text-[#FFFF55] text-3xl mc-text-shadow mb-8 pb-2 w-full text-center uppercase tracking-widest">
|
|
<img
|
|
src="/images/lceonline.png"
|
|
alt="LCE Online"
|
|
className="h-16 mx-auto"
|
|
/>
|
|
</h2>
|
|
<p className="text-white text-lg mc-text-shadow mb-8 max-w-sm">
|
|
{t("lceOnline.awaitingAuthentication")}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const topButtons = menuItems.filter((m) => m.type === "button");
|
|
const listItems = menuItems.filter((m) => m.type !== "button");
|
|
return (
|
|
<div className="flex flex-col h-full space-y-4">
|
|
{topButtons.length > 0 && (
|
|
<div className="flex gap-4 flex-wrap">
|
|
{topButtons.map((btn) => {
|
|
const idx = menuItems.indexOf(btn);
|
|
const isFocused = focusIndex === idx;
|
|
return (
|
|
<button
|
|
key={btn.id}
|
|
data-index={idx}
|
|
onMouseEnter={() => setFocusIndex(idx)}
|
|
onClick={btn.onClick}
|
|
className={`flex-1 h-12 flex items-center justify-center text-xl font-bold uppercase tracking-widest outline-none border-none transition-all ${isFocused ? "text-[#FFFF55] mc-text-shadow scale-[1.02] z-10 relative drop-shadow-md" : "text-white mc-text-shadow hover:text-gray-200"}`}
|
|
style={{
|
|
backgroundImage: isFocused
|
|
? "url('/images/button_highlighted.png')"
|
|
: "url('/images/Button_Background.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
>
|
|
{btn.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col flex-1 bg-black/5 shadow-inner rounded overflow-hidden border-4 border-[#222]">
|
|
<div className="bg-black/10 px-4 py-3 text-[#2a2a2a] font-bold tracking-widest uppercase border-b-4 border-[#222] flex justify-between shadow-sm z-10">
|
|
<span>
|
|
{currentTab === "friends"
|
|
? t("lceOnline.friends")
|
|
: currentTab === "invites"
|
|
? t("lceOnline.invites")
|
|
: t("lceOnline.pendingRequests")}
|
|
</span>
|
|
<span className="text-[#111]">{listItems.length}</span>
|
|
</div>
|
|
|
|
<div ref={scrollRef} className="flex-1 overflow-y-auto w-full">
|
|
{listItems.length === 0 ? (
|
|
<div className="flex items-center justify-center h-[200px] text-[#555] font-bold">
|
|
{t("lceOnline.noneAvailable")}
|
|
</div>
|
|
) : (
|
|
<div className="flex flex-col p-2 space-y-2">
|
|
{listItems.map((item) => {
|
|
const idx = menuItems.indexOf(item);
|
|
const isFocused = focusIndex === idx;
|
|
return (
|
|
<div
|
|
key={item.id}
|
|
data-index={idx}
|
|
onMouseEnter={() => setFocusIndex(idx)}
|
|
className={`w-full flex items-center justify-between px-4 py-3 relative outline-none border-none rounded ${isFocused ? "bg-black/15 shadow-inner" : "bg-transparent"}`}
|
|
tabIndex={-1}
|
|
>
|
|
<div className="flex items-center w-full">
|
|
<div className="flex flex-col ml-2 flex-1 min-w-0">
|
|
<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
|
|
: t("lceOnline.invite")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex space-x-3 pr-2 shrink-0">
|
|
{item.type === "friend" && (
|
|
<>
|
|
{item.onClickSecondary && (
|
|
<button
|
|
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
|
style={{
|
|
backgroundImage:
|
|
"url('/images/button_highlighted.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
item.onClickSecondary?.();
|
|
}}
|
|
>
|
|
{t("lceOnline.invite").toUpperCase()}
|
|
</button>
|
|
)}
|
|
<button
|
|
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
|
style={{
|
|
backgroundImage:
|
|
"url('/images/Button_Background.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
onClick={item.onClick}
|
|
>
|
|
{t("lceOnline.remove").toUpperCase()}
|
|
</button>
|
|
</>
|
|
)}
|
|
{item.type === "request_out" && (
|
|
<button
|
|
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
|
style={{
|
|
backgroundImage:
|
|
"url('/images/Button_Background.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
onClick={item.onClick}
|
|
>
|
|
{t("lceOnline.cancel").toUpperCase()}
|
|
</button>
|
|
)}
|
|
{item.type === "invite" && (
|
|
<>
|
|
<button
|
|
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
|
style={{
|
|
backgroundImage:
|
|
"url('/images/button_highlighted.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
onClick={item.onClick}
|
|
>
|
|
{t("lceOnline.accept").toUpperCase()}
|
|
</button>
|
|
<button
|
|
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
|
style={{
|
|
backgroundImage:
|
|
"url('/images/Button_Background.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
item.onClickSecondary?.();
|
|
}}
|
|
>
|
|
{t("lceOnline.decline").toUpperCase()}
|
|
</button>
|
|
</>
|
|
)}
|
|
{item.type === "request_in" && (
|
|
<>
|
|
<button
|
|
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
|
style={{
|
|
backgroundImage:
|
|
"url('/images/button_highlighted.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
onClick={item.onClick}
|
|
>
|
|
{t("lceOnline.accept").toUpperCase()}
|
|
</button>
|
|
<button
|
|
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
|
|
style={{
|
|
backgroundImage:
|
|
"url('/images/Button_Background.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
item.onClickSecondary?.();
|
|
}}
|
|
>
|
|
{t("lceOnline.decline").toUpperCase()}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
ref={containerRef}
|
|
tabIndex={-1}
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
exit={{ opacity: 0, scale: 0.95 }}
|
|
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
|
className="flex flex-col items-center justify-center w-full h-full absolute inset-0 outline-none p-12"
|
|
>
|
|
<div className="w-full max-w-5xl h-full flex flex-col mt-[4vh] mb-[4vh] relative drop-shadow-2xl">
|
|
{isSignedIn && (
|
|
<div
|
|
className="flex z-10 space-x-2 px-12 relative w-full items-end"
|
|
style={{ marginBottom: "-4px" }}
|
|
>
|
|
{tabs.map((tab) => (
|
|
<button
|
|
key={tab}
|
|
className={`flex-1 font-bold text-xl outline-none uppercase transition-all duration-200 ease-in-out ${currentTab === tab ? "text-[#2a2a2a] z-20 pb-6 pt-5 text-2xl drop-shadow-[5px_-5px_15px_rgba(0,0,0,0.3)] rounded-t border-4 border-[#222] border-b-0" : "text-[#555] mt-2 py-4 hover:bg-black/30 bg-black/10 hover:text-[#222] border-4 border-transparent border-b-0"}`}
|
|
style={{
|
|
backgroundImage: "url('/images/background.png')",
|
|
backgroundSize: "100% 100%",
|
|
backgroundRepeat: "no-repeat",
|
|
backgroundPosition: "bottom",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
onClick={() => {
|
|
setCurrentTab(tab);
|
|
setFocusIndex(0);
|
|
playPressSound();
|
|
}}
|
|
>
|
|
<div className="flex items-center justify-center">
|
|
{tab === "friends"
|
|
? t("lceOnline.friends")
|
|
: tab === "requests"
|
|
? t("lceOnline.requests")
|
|
: t("lceOnline.invites")}
|
|
{tab === "requests" && incomingReqs.length > 0 && (
|
|
<span
|
|
className={`ml-3 text-white text-base px-3 py-1 rounded-full shadow-inner border-2 font-normal ${currentTab === tab ? "bg-[#d72f2f] border-[#8a1a1a]" : "bg-[#a81f1f] border-[#111]"}`}
|
|
>
|
|
{incomingReqs.length}
|
|
</span>
|
|
)}
|
|
{tab === "invites" && invites.length > 0 && (
|
|
<span
|
|
className={`ml-3 text-white text-base px-3 py-1 rounded-full shadow-inner border-2 font-normal ${currentTab === tab ? "bg-[#d72f2f] border-[#8a1a1a]" : "bg-[#a81f1f] border-[#111]"}`}
|
|
>
|
|
{invites.length}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
className="flex-1 flex flex-col p-8 z-10 relative overflow-hidden rounded-b shadow-[0_0_30px_rgba(0,0,0,0.6)] border-4 border-[#222] border-t-0"
|
|
style={{
|
|
backgroundImage: "url('/images/background.png')",
|
|
backgroundSize: "100% auto",
|
|
backgroundRepeat: "no-repeat",
|
|
backgroundPosition: "top",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
>
|
|
{renderContent()}
|
|
</div>
|
|
<div className="flex justify-center pt-4 pb-2">
|
|
<img
|
|
src="/images/lceonline.png"
|
|
alt="LCEOnline"
|
|
className="h-5 opacity-70 cursor-pointer"
|
|
onClick={() => TauriService.openUrl("https://mclegacyedition.xyz/")}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<AnimatePresence>
|
|
{isAddingFriend && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm outline-none border-none"
|
|
>
|
|
<div
|
|
className="relative w-[420px] p-8 flex flex-col items-center shadow-2xl"
|
|
style={{
|
|
backgroundImage: "url('/images/frame_background.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
>
|
|
<h2 className="text-[#FFFF55] text-3xl mc-text-shadow mb-6 border-b-2 border-[#373737] pb-2 w-full text-center uppercase tracking-widest">
|
|
{t("lceOnline.addFriend")}
|
|
</h2>
|
|
<input
|
|
ref={addFriendInputRef}
|
|
type="text"
|
|
className="bg-black/20 border-4 border-[#555] text-white p-4 w-full text-2xl font-bold outline-none focus:border-[#FFFF55] transition-colors placeholder:text-[#888] mb-6 mc-text-shadow"
|
|
placeholder={t("lceOnline.username")}
|
|
value={addFriendUsername}
|
|
onChange={(e) => setAddFriendUsername(e.target.value)}
|
|
/>
|
|
<div className="flex gap-4 w-full">
|
|
<button
|
|
className="h-12 flex-1 flex items-center justify-center text-white mc-text-shadow text-xl font-bold uppercase tracking-widest hover:text-[#FFFF55] outline-none border-none"
|
|
style={{
|
|
backgroundImage: "url('/images/button_highlighted.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
onClick={() => {
|
|
playPressSound();
|
|
if (addFriendUsername.trim() !== "") {
|
|
handleAction(() =>
|
|
lceOnlineService.sendFriendRequest(
|
|
addFriendUsername.trim(),
|
|
),
|
|
);
|
|
setIsAddingFriend(false);
|
|
}
|
|
}}
|
|
>
|
|
{t("lceOnline.send")}
|
|
</button>
|
|
<button
|
|
className="h-12 flex-1 flex items-center justify-center text-white mc-text-shadow text-xl font-bold uppercase tracking-widest hover:text-[#FFFF55] outline-none border-none"
|
|
style={{
|
|
backgroundImage: "url('/images/Button_Background.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
onClick={() => {
|
|
setIsAddingFriend(false);
|
|
playBackSound();
|
|
}}
|
|
>
|
|
{t("lceOnline.cancel")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
<AnimatePresence>
|
|
{errorModal && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-[110] flex items-center justify-center bg-black/80 backdrop-blur-sm outline-none border-none"
|
|
>
|
|
<div
|
|
className="relative w-[400px] p-8 flex flex-col items-center shadow-2xl"
|
|
style={{
|
|
backgroundImage: "url('/images/frame_background.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
>
|
|
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase tracking-widest">
|
|
{t("lceOnline.error")}
|
|
</h2>
|
|
<p className="text-white text-lg mc-text-shadow text-center mb-6">
|
|
{errorModal}
|
|
</p>
|
|
<button
|
|
className="h-12 w-48 flex items-center justify-center text-white mc-text-shadow text-xl font-bold uppercase tracking-widest hover:text-[#FFFF55] outline-none border-none"
|
|
style={{
|
|
backgroundImage: "url('/images/button_highlighted.png')",
|
|
backgroundSize: "100% 100%",
|
|
imageRendering: "pixelated",
|
|
}}
|
|
onClick={() => setErrorModal(null)}
|
|
>
|
|
{t("lceOnline.ok")}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
{joinTarget && (
|
|
<ChooseInstanceModal
|
|
isOpen={true}
|
|
onClose={() => setJoinTarget(null)}
|
|
playPressSound={playPressSound}
|
|
playBackSound={playBackSound}
|
|
editions={game.editions}
|
|
installs={game.installs}
|
|
invite={{
|
|
inviteId: joinTarget.inviteid,
|
|
from: joinTarget.hostName,
|
|
hostIp: "",
|
|
hostPort: 0,
|
|
hostName: joinTarget.hostName,
|
|
sessionId: joinTarget.sessionId,
|
|
status: "pending",
|
|
}}
|
|
/>
|
|
)}
|
|
</motion.div>
|
|
);
|
|
});
|
|
|
|
export default LceOnlineView;
|