feat: 1.5.0 drop 2 (#122)

Co-authored-by: str1k3r <[email protected]>
This commit is contained in:
/home/neo
2026-07-11 21:04:53 +03:00
committed by GitHub
co-authored by str1k3r
parent f46453f1cd
commit 034fbf5737
24 changed files with 900 additions and 1385 deletions
+2 -1
View File
@@ -209,6 +209,7 @@ const CreditsView = memo(function CreditsView() {
{ name: "dr.av", url: "#" },
{ name: "alreadywarned", url: "#" },
{ name: "andrewjcf", url: "#" },
{ name: "dille", url: "#" },
],
},
],
@@ -427,4 +428,4 @@ const CreditsView = memo(function CreditsView() {
);
});
export default CreditsView;
export default CreditsView;
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef, useMemo, useCallback, memo } from "react";
import { useState, useEffect, useRef, useMemo, memo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
useUI,
@@ -6,69 +6,55 @@ import {
useAudio,
useGame,
} from "../../context/LauncherContext";
import {
lceLiveService,
LceLiveAccount,
FriendRequest,
GameInvite,
DeviceLinkStartResponse,
} from "../../services/LceLiveService";
import { TauriService } from "../../services/TauriService";
import ChooseInstanceModal from "../modals/ChooseInstanceModal";
import QRCode from "qrcode";
interface LceLiveViewProps {
import { lceOnlineService } from "../../services/LceOnlineService";
import { TauriService } from "../../services/TauriService";
interface LceOnlineViewProps {
addFriendTarget?: string | null;
onClearAddFriendTarget?: () => void;
invites?: Array<{
inviteid: string;
from: { uuid: string; username: string };
sessionid: string;
}>;
}
const LceLiveView = memo(function LceLiveView({
const LceOnlineView = memo(function LceOnlineView({
addFriendTarget,
onClearAddFriendTarget,
}: LceLiveViewProps) {
invites: invitesProp,
}: LceOnlineViewProps) {
const { setActiveView } = useUI();
const { animationsEnabled } = useConfig();
const { playPressSound, playBackSound } = useAudio();
const { editions, installs } = useGame();
const [isSignedIn, setIsSignedIn] = useState(lceLiveService.signedIn);
const game = useGame();
const [isSignedIn, setIsSignedIn] = useState(lceOnlineService.signedIn);
const [currentTab, setCurrentTab] = useState<
"friends" | "requests" | "invites" | "device_link"
"friends" | "requests" | "invites"
>("friends");
const [focusIndex, setFocusIndex] = useState<number | null>(0);
const [acceptInvite, setAcceptInvite] = useState<GameInvite | null>(null);
const [friends, setFriends] = useState<LceLiveAccount[]>([]);
const [incomingReqs, setIncomingReqs] = useState<FriendRequest[]>([]);
const [outgoingReqs, setOutgoingReqs] = useState<FriendRequest[]>([]);
const [invites, setInvites] = useState<GameInvite[]>([]);
const [linkData, setLinkData] = useState<DeviceLinkStartResponse | null>(
null,
);
const [linkError, setLinkError] = useState<string | null>(null);
const [friends, setFriends] = useState<string[]>([]);
const [incomingReqs, setIncomingReqs] = useState<string[]>([]);
const [outgoingReqs, setOutgoingReqs] = useState<string[]>([]);
const invites = invitesProp ?? [];
const [isHosting, setIsHosting] = useState(false);
const [_hostStatus, setHostStatus] = useState("");
const [hostIp, setHostIp] = useState("");
const [hostPort, setHostPort] = useState(19132);
const [isDiscovering, setIsDiscovering] = useState(false);
const [invitedFriends, setInvitedFriends] = useState<Set<string>>(new Set());
const [showHostMethodPicker, setShowHostMethodPicker] = useState(false);
const [isAddingFriend, setIsAddingFriend] = useState(false);
const [addFriendUsername, setAddFriendUsername] = useState("");
const addFriendInputRef = useRef<HTMLInputElement>(null);
const [errorModal, setErrorModal] = useState<string | null>(null);
const [qrDataUrl, setQrDataUrl] = useState<string>("");
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 (!lceLiveService.signedIn) return;
if (!lceOnlineService.signedIn) return;
try {
const [f, reqs, invs] = await Promise.all([
lceLiveService.getFriends(),
lceLiveService.getPendingRequests(),
lceLiveService.getGameInvites(),
]);
setFriends(f);
setIncomingReqs(reqs.incoming);
setOutgoingReqs(reqs.outgoing);
setInvites(invs.filter((i: GameInvite) => i.status === "pending"));
const lists = await lceOnlineService.getSocialLists();
setFriends(lists.friends);
setIncomingReqs(lists.requests);
setOutgoingReqs([]);
} catch (e: unknown) {
console.error(e);
}
@@ -76,84 +62,57 @@ const LceLiveView = memo(function LceLiveView({
useEffect(() => {
if (isSignedIn) {
if (currentTab === "device_link") setCurrentTab("friends");
fetchSocialData();
const pollInvites = setInterval(async () => {
try {
const invs = await lceLiveService.getGameInvites();
setInvites(invs.filter((i: GameInvite) => i.status === "pending"));
} catch (e) {
console.warn("Failed to poll invites", e);
}
}, 5000);
return () => clearInterval(pollInvites);
} else {
setCurrentTab("device_link");
}
}, [isSignedIn, currentTab]);
}, [isSignedIn]);
useEffect(() => {
if (currentTab !== "device_link") return;
let mounted = true;
let pollInterval: ReturnType<typeof setInterval> | null = null;
const startLink = async () => {
try {
if (!linkData) {
const data = await lceLiveService.startDeviceLink();
if (mounted) setLinkData(data);
}
} catch (e: unknown) {
if (mounted) setLinkError(e instanceof Error ? e.message : String(e));
}
};
return lceOnlineService.onSessionChange(() => {
setIsSignedIn(lceOnlineService.signedIn);
});
}, []);
startLink();
if (linkData?.deviceCode) {
pollInterval = setInterval(
async () => {
try {
const res = await lceLiveService.pollDeviceLink(
linkData.deviceCode,
);
if (res.isLinked && mounted) {
setIsSignedIn(true);
setLinkData(null);
if (pollInterval !== null) clearInterval(pollInterval);
}
} catch (e: unknown) {
console.warn("Poll failed", e);
}
},
Math.max(linkData.intervalSeconds * 1000, 2000),
useEffect(() => {
if (!isSignedIn) {
TauriService.openUrl(
"https://mclegacyedition.xyz/internal/auth?appId=emerald_launcher",
);
}
return () => {
mounted = false;
if (pollInterval !== null) clearInterval(pollInterval);
};
}, [currentTab, linkData]);
}, []);
useEffect(() => {
if (!linkData?.verificationUri || !linkData?.userCode) return;
const authUrl = `${linkData.verificationUri}?code=${linkData.userCode}`;
QRCode.toDataURL(authUrl, { width: 200, margin: 1 }, (err, url) => {
if (!err) setQrDataUrl(url);
});
}, [linkData]);
const openAuthUrl = useCallback(() => {
if (!linkData?.verificationUri || !linkData?.userCode) return;
const authUrl = `${linkData.verificationUri}?code=${linkData.userCode}`;
TauriService.openUrl(authUrl);
}, [linkData]);
if (!addFriendTarget) return;
setCurrentTab("friends");
handleAction(() => lceOnlineService.sendFriendRequest(addFriendTarget));
onClearAddFriendTarget?.();
}, [addFriendTarget, onClearAddFriendTarget]);
const handleLogout = () => {
playPressSound();
lceLiveService.logoutLocal();
lceOnlineService.logoutLocal();
setIsSignedIn(false);
setLinkData(null);
};
const handleStartHosting = async () => {
playPressSound();
try {
const token = lceOnlineService.accessToken ?? "";
if (!token) return;
TauriService.startHostRelay(token, 25565).catch(() => {});
setIsHosting(true);
} catch (e: unknown) {
setErrorModal(e instanceof Error ? e.message : "Failed to start hosting");
}
};
const handleStopHosting = async () => {
playPressSound();
try {
await TauriService.stopAllProxies();
} catch (e: unknown) {
console.warn("Stop hosting failed", e);
}
setIsHosting(false);
};
const handleAction = async (action: () => Promise<void>) => {
@@ -166,113 +125,6 @@ const LceLiveView = memo(function LceLiveView({
}
};
const handleStartHosting = () => {
playPressSound();
setShowHostMethodPicker(true);
setFocusIndex(0);
};
const handleHostDirect = async () => {
setShowHostMethodPicker(false);
setFocusIndex(0);
setIsDiscovering(true);
setHostStatus("Discovering external IP...");
try {
const endpoint = await TauriService.stunDiscover();
setHostIp(endpoint.ip);
setHostPort(25565);
setIsHosting(true);
setHostStatus(`Hosting at ${endpoint.ip}:25565`);
setInvitedFriends(new Set());
} catch (e: unknown) {
const msg =
e instanceof Error
? e.message
: typeof e === "string"
? e
: "Unknown error";
setErrorModal("STUN discovery failed: " + msg);
setHostStatus("");
} finally {
setIsDiscovering(false);
}
};
const handleHostRelay = async () => {
setShowHostMethodPicker(false);
setFocusIndex(0);
setIsDiscovering(true);
setHostStatus("Discovering external IP for invite...");
try {
const endpoint = await TauriService.stunDiscover();
setHostIp(endpoint.ip);
setHostPort(25565);
} catch {
setHostIp("127.0.0.1");
setHostPort(25565);
}
setIsHosting(true);
setHostStatus("Relay ready - invite friends to activate");
setInvitedFriends(new Set());
setIsDiscovering(false);
};
const handleStopHosting = async () => {
playPressSound();
try {
await TauriService.stopAllProxies();
await lceLiveService.deactivateGameInvites();
} catch (e: unknown) {
console.warn("Stop hosting failed", e);
}
setIsHosting(false);
setHostStatus("");
setHostIp("");
setInvitedFriends(new Set());
};
const handleInviteFriend = async (friend: LceLiveAccount) => {
playPressSound();
const name = lceLiveService.displayUsername;
const sessionId = crypto.randomUUID();
try {
await lceLiveService.sendGameInvite(
friend.accountId,
hostIp,
hostPort,
name,
sessionId,
);
setInvitedFriends((prev) => new Set(prev).add(friend.accountId));
setHostStatus("Connecting relay...");
TauriService.startHostRelay(
lceLiveService.apiBaseUrl,
lceLiveService.accessToken ?? "",
sessionId,
25565,
)
.then(() => setHostStatus("Relay active"))
.catch((relayErr: unknown) => {
const relayMsg =
relayErr instanceof Error
? relayErr.message
: typeof relayErr === "string"
? relayErr
: "Unknown error";
console.warn("Relay failed:", relayMsg);
setHostStatus("Relay disconnected");
});
} catch (e: unknown) {
const msg =
e instanceof Error
? e.message
: typeof e === "string"
? e
: "Unknown error";
setErrorModal("Failed to send invite: " + msg);
}
};
type MenuItem = {
id: string;
type: "button" | "friend" | "request_in" | "request_out" | "invite";
@@ -283,35 +135,21 @@ const LceLiveView = memo(function LceLiveView({
const menuItems = useMemo<MenuItem[]>(() => {
const items: MenuItem[] = [];
if (currentTab === "device_link") {
if (linkData) {
if (currentTab === "friends") {
if (!isHosting) {
items.push({
id: "link_retry",
id: "host_game",
type: "button",
label: "Restart Link",
onClick: () => {
setLinkData(null);
playPressSound();
},
label: "Host Game",
onClick: handleStartHosting,
});
} else {
items.push({
id: "stop_hosting",
type: "button",
label: "Stop Hosting",
onClick: handleStopHosting,
});
}
} else if (currentTab === "friends") {
if (!isDiscovering && !showHostMethodPicker) {
if (!isHosting) {
items.push({
id: "host_game",
type: "button",
label: "Host Game",
onClick: handleStartHosting,
});
} else {
items.push({
id: "stop_hosting",
type: "button",
label: "Stop Hosting",
onClick: handleStopHosting,
});
}
}
items.push({
id: "add_friend",
@@ -331,56 +169,57 @@ const LceLiveView = memo(function LceLiveView({
});
friends.forEach((f) => {
items.push({
id: `friend_${f.accountId}`,
id: `friend_${f}`,
type: "friend",
label: f.displayName,
onClick: isHosting
? () => handleInviteFriend(f)
: () =>
handleAction(() => lceLiveService.removeFriend(f.accountId)),
label: f,
onClick: () => handleAction(() => lceOnlineService.removeFriend(f)),
onClickSecondary: isHosting
? () => handleAction(() => lceLiveService.removeFriend(f.accountId))
? () => handleAction(() => lceOnlineService.sendInvite(f))
: undefined,
});
});
} else if (currentTab === "requests") {
incomingReqs.forEach((r) => {
items.push({
id: `req_in_${r.username}`,
id: `req_in_${r}`,
type: "request_in",
label: r.displayName,
label: r,
onClick: () =>
handleAction(() => lceLiveService.sendFriendRequest(r.username)),
handleAction(() => lceOnlineService.acceptFriendRequest(r)),
onClickSecondary: () =>
handleAction(() =>
lceLiveService.declineFriendRequest(r.accountId),
),
handleAction(() => lceOnlineService.declineFriendRequest(r)),
});
});
outgoingReqs.forEach((r) => {
items.push({
id: `req_out_${r.username}`,
id: `req_out_${r}`,
type: "request_out",
label: r.displayName,
label: r,
onClick: () =>
handleAction(() =>
lceLiveService.declineFriendRequest(r.accountId),
),
handleAction(() => lceOnlineService.declineFriendRequest(r)),
});
});
} else if (currentTab === "invites") {
invites.forEach((inv) => {
items.push({
id: `inv_${inv.inviteId}`,
id: `invite_${inv.inviteid}`,
type: "invite",
label:
typeof inv.from === "string" ? "Unknown" : inv.from.displayName,
onClick: () => {
playPressSound();
setAcceptInvite(inv);
},
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(() => lceLiveService.declineGameInvite(inv.inviteId)),
handleAction(() =>
lceOnlineService.declineInvite(inv.from.username),
),
});
});
}
@@ -392,22 +231,10 @@ const LceLiveView = memo(function LceLiveView({
incomingReqs,
outgoingReqs,
invites,
linkData,
playPressSound,
isHosting,
isDiscovering,
showHostMethodPicker,
]);
useEffect(() => {
if (!addFriendTarget) return;
setCurrentTab("friends");
handleAction(() =>
lceLiveService.sendFriendRequest(addFriendTarget),
);
onClearAddFriendTarget?.();
}, [addFriendTarget, onClearAddFriendTarget]);
const tabs: ("friends" | "requests" | "invites")[] = [
"friends",
"requests",
@@ -429,7 +256,7 @@ const LceLiveView = memo(function LceLiveView({
} else if (e.key === "Enter") {
if (addFriendUsername.trim() !== "") {
handleAction(() =>
lceLiveService.sendFriendRequest(addFriendUsername.trim()),
lceOnlineService.sendFriendRequest(addFriendUsername.trim()),
);
setIsAddingFriend(false);
}
@@ -437,23 +264,11 @@ const LceLiveView = memo(function LceLiveView({
return;
}
if (showHostMethodPicker) {
if (!isSignedIn) {
if (e.key === "Escape" || e.key === "Backspace") {
setShowHostMethodPicker(false);
setFocusIndex(0);
playBackSound();
} else if (e.key === "ArrowDown") {
setFocusIndex((prev) => (prev === null || prev >= 2 ? 0 : prev + 1));
} else if (e.key === "ArrowUp") {
setFocusIndex((prev) => (prev === null || prev <= 0 ? 2 : prev - 1));
} else if (e.key === "Enter") {
if (focusIndex === 0) handleHostRelay();
else if (focusIndex === 1) handleHostDirect();
else if (focusIndex === 2) {
setShowHostMethodPicker(false);
setFocusIndex(0);
playBackSound();
}
setActiveView("main");
return;
}
return;
}
@@ -464,22 +279,20 @@ const LceLiveView = memo(function LceLiveView({
return;
}
if (currentTab !== "device_link") {
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 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;
@@ -508,9 +321,7 @@ const LceLiveView = memo(function LceLiveView({
isAddingFriend,
addFriendUsername,
errorModal,
showHostMethodPicker,
handleHostDirect,
handleHostRelay,
isSignedIn,
]);
useEffect(() => {
@@ -532,52 +343,21 @@ const LceLiveView = memo(function LceLiveView({
}
}
}, [focusIndex, isAddingFriend]);
const renderContent = () => {
if (currentTab === "device_link") {
if (!isSignedIn) {
return (
<div className="flex flex-col items-center justify-center flex-1 text-center py-12">
{!linkData ? (
<p className="text-lg text-[#2a2a2a] font-bold">
{linkError || "Starting device link..."}
</p>
) : (
<div className="flex items-start justify-center gap-10 w-full max-w-2xl">
<div className="flex flex-col items-center space-y-4 flex-1 min-w-0">
<p className="text-lg text-[#2a2a2a] font-bold">
Open this link in your browser:
</p>
<p className="text-[#111] text-base font-bold tracking-widest break-all bg-black/10 px-4 py-2 rounded shadow-inner">
{linkData.verificationUri}
</p>
<p className="text-lg text-[#2a2a2a] font-bold">
And enter the code:
</p>
<p className="text-[#111] text-4xl tracking-[0.2em] font-bold bg-black/10 px-6 py-3 rounded shadow-inner">
{linkData.userCode}
</p>
</div>
<div className="flex flex-col items-center gap-3 shrink-0">
{qrDataUrl && (
<img
src={qrDataUrl}
alt="QR Code"
className="w-48 h-48 image-rendering-pixelated"
/>
)}
<button
onClick={openAuthUrl}
className="h-10 px-6 flex items-center justify-center text-white mc-text-shadow text-base font-bold uppercase tracking-widest outline-none border-none hover:text-[#FFFF55] transition-colors"
style={{
backgroundImage: "url('/images/button_highlighted.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Open in Browser
</button>
</div>
</div>
)}
<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">
Awaiting authentication...
</p>
</div>
);
}
@@ -617,10 +397,10 @@ const LceLiveView = memo(function LceLiveView({
<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"
? "Joinable Friends"
: currentTab === "requests"
? "Pending Requests"
: "Game Invites"}
? "Friends"
: currentTab === "invites"
? "Invites"
: "Pending Requests"}
</span>
<span className="text-[#111]">{listItems.length}</span>
</div>
@@ -648,59 +428,28 @@ const LceLiveView = memo(function LceLiveView({
<span className="text-[#2a2a2a] font-bold text-2xl truncate pr-4">
{item.label}
</span>
<span className="text-[#555] text-base font-bold truncate">
@
{menuItems.find((m) => m.id === item.id)?.type ===
"friend"
? friends.find(
(f) => `friend_${f.accountId}` === item.id,
)?.username
: item.type === "request_in"
? incomingReqs.find(
(f) => `req_in_${f.accountId}` === item.id,
)?.username
: item.type === "request_out"
? outgoingReqs.find(
(f) =>
`req_out_${f.accountId}` === item.id,
)?.username
: "Invite"}
</span>
</div>
</div>
<div className="flex space-x-3 pr-2 shrink-0">
{item.type === "friend" && !isHosting && (
<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}
>
REMOVE
</button>
)}
{item.type === "friend" && isHosting && (
{item.type === "friend" && (
<>
<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}
>
{invitedFriends.has(
item.id.replace("friend_", ""),
)
? "INVITED"
: "INVITE"}
</button>
{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?.();
}}
>
INVITE
</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={{
@@ -709,10 +458,7 @@ const LceLiveView = memo(function LceLiveView({
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
onClick={(e) => {
e.stopPropagation();
item.onClickSecondary?.();
}}
onClick={item.onClick}
>
REMOVE
</button>
@@ -732,8 +478,38 @@ const LceLiveView = memo(function LceLiveView({
CANCEL
</button>
)}
{(item.type === "request_in" ||
item.type === "invite") && (
{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}
>
ACCEPT
</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?.();
}}
>
DECLINE
</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"}`}
@@ -775,6 +551,7 @@ const LceLiveView = memo(function LceLiveView({
</div>
);
};
return (
<motion.div
ref={containerRef}
@@ -786,7 +563,7 @@ const LceLiveView = memo(function LceLiveView({
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">
{currentTab !== "device_link" && (
{isSignedIn && (
<div
className="flex z-10 space-x-2 px-12 relative w-full items-end"
style={{ marginBottom: "-4px" }}
@@ -819,7 +596,7 @@ const LceLiveView = memo(function LceLiveView({
)}
{t === "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 === t ? "bg-[#30872a] border-[#1b5e16]" : "bg-[#23681d] border-[#111]"}`}
className={`ml-3 text-white text-base px-3 py-1 rounded-full shadow-inner border-2 font-normal ${currentTab === t ? "bg-[#d72f2f] border-[#8a1a1a]" : "bg-[#a81f1f] border-[#111]"}`}
>
{invites.length}
</span>
@@ -842,14 +619,6 @@ const LceLiveView = memo(function LceLiveView({
>
{renderContent()}
</div>
<div className="flex justify-center pt-4 pb-2">
<img
src="/images/lcelive.png"
alt="LCELive"
className="h-5 opacity-70 cursor-pointer"
onClick={() => TauriService.openUrl("https://lcelive.co.uk")}
/>
</div>
</div>
<AnimatePresence>
@@ -891,7 +660,7 @@ const LceLiveView = memo(function LceLiveView({
playPressSound();
if (addFriendUsername.trim() !== "") {
handleAction(() =>
lceLiveService.sendFriendRequest(
lceOnlineService.sendFriendRequest(
addFriendUsername.trim(),
),
);
@@ -920,83 +689,6 @@ const LceLiveView = memo(function LceLiveView({
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{showHostMethodPicker && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[105] 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 gap-4"
style={{
backgroundImage: "url('/images/frame_background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
<h2 className="text-[#FFFF55] text-3xl mc-text-shadow mb-2 border-b-2 border-[#373737] pb-2 w-full text-center uppercase tracking-widest">
Host Game
</h2>
<button
data-index={0}
onMouseEnter={() => setFocusIndex(0)}
onClick={handleHostRelay}
className={`w-full h-14 flex items-center justify-center text-xl font-bold uppercase tracking-widest outline-none border-none ${focusIndex === 0 ? "text-[#FFFF55] mc-text-shadow" : "text-white mc-text-shadow hover:text-[#FFFF55]"}`}
style={{
backgroundImage:
focusIndex === 0
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Relay
</button>
<button
data-index={1}
onMouseEnter={() => setFocusIndex(1)}
onClick={handleHostDirect}
className={`w-full h-14 flex items-center justify-center text-xl font-bold uppercase tracking-widest outline-none border-none ${focusIndex === 1 ? "text-[#FFFF55] mc-text-shadow" : "text-white mc-text-shadow hover:text-[#FFFF55]"}`}
style={{
backgroundImage:
focusIndex === 1
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Direct (STUN)
</button>
<button
data-index={2}
onMouseEnter={() => setFocusIndex(2)}
onClick={() => {
setShowHostMethodPicker(false);
setFocusIndex(0);
playBackSound();
}}
className={`w-full h-14 flex items-center justify-center text-xl font-bold uppercase tracking-widest outline-none border-none ${focusIndex === 2 ? "text-[#FFFF55] mc-text-shadow" : "text-white mc-text-shadow hover:text-[#FFFF55]"}`}
style={{
backgroundImage:
focusIndex === 2
? "url('/images/button_highlighted.png')"
: "url('/images/Button_Background.png')",
backgroundSize: "100% 100%",
imageRendering: "pixelated",
}}
>
Cancel
</button>
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{errorModal && (
<motion.div
@@ -1034,21 +726,27 @@ const LceLiveView = memo(function LceLiveView({
</motion.div>
)}
</AnimatePresence>
<ChooseInstanceModal
isOpen={acceptInvite !== null}
onClose={() => {
setAcceptInvite(null);
fetchSocialData();
}}
playPressSound={playPressSound}
playBackSound={playBackSound}
editions={editions}
installs={installs}
invite={acceptInvite}
/>
{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 LceLiveView;
export default LceOnlineView;
+33 -22
View File
@@ -83,7 +83,7 @@ interface ServerListing {
id: string;
owner: string;
name: string;
short_descripton: string
short_descripton: string;
description: string;
discord?: string;
version: string;
@@ -510,17 +510,10 @@ const WorkshopView = memo(function WorkshopView({
useEffect(() => {
if (!workshopTarget || loading) return;
if (
workshopTarget.type === "lceonline" && serverPlugins.length === 0
)
if (workshopTarget.type === "lceonline" && serverPlugins.length === 0)
return;
if (
workshopTarget.type === "plugin" && pluginPackages.length === 0
)
return;
if (
workshopTarget.type === "version" && versionPackages.length === 0
)
if (workshopTarget.type === "plugin" && pluginPackages.length === 0) return;
if (workshopTarget.type === "version" && versionPackages.length === 0)
return;
if (!workshopTarget.type && allPackages.length === 0) return;
@@ -1292,9 +1285,10 @@ function PackageModal({
const needsUpdate =
hasInstalled && installedEntries.some((e) => e.version !== pkg.version);
const unresolvedDeps = useMemo(
() => (pkg.dependencies || []).filter(
(depId) => !installedPkgs.some((p) => p.packageId === depId),
),
() =>
(pkg.dependencies || []).filter(
(depId) => !installedPkgs.some((p) => p.packageId === depId),
),
[pkg.dependencies, installedPkgs],
);
const focusOptions: Array<"install" | "uninstall" | "close"> = isGameServerTab
@@ -1351,9 +1345,7 @@ function PackageModal({
["*.dll", "*"],
);
if (!path) return;
const response = await fetch(
`${pkg.download_url}`,
);
const response = await fetch(`${pkg.download_url}`);
const blob = await response.blob();
const buffer = await blob.arrayBuffer();
await TauriService.writeBinaryFile(path, new Uint8Array(buffer));
@@ -1498,7 +1490,13 @@ function PackageModal({
Details
</span>
<div className="text-sm text-white mc-text-shadow leading-relaxed workshop-markdown">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw, [rehypeSanitize, defaultSchema]]}>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[
rehypeRaw,
[rehypeSanitize, defaultSchema],
]}
>
{pkg.extended_description}
</ReactMarkdown>
</div>
@@ -1685,7 +1683,9 @@ function PackageModal({
<div className="flex flex-wrap gap-1.5 mt-1">
{pkg.dependencies.map((depId) => {
const dep = allPackages.find((p) => p.id === depId);
const isDepInstalled = installedPkgs.some((p) => p.packageId === depId);
const isDepInstalled = installedPkgs.some(
(p) => p.packageId === depId,
);
return (
<span
key={depId}
@@ -1711,9 +1711,12 @@ function PackageModal({
<div className="flex flex-wrap gap-1.5 mt-1">
{pkg.required_versions.map((verId) => {
const builtin = BASE_EDITIONS.find((e) => e.id === verId);
const versionPkg = versionPackages.find((v) => v.id === verId);
const versionPkg = versionPackages.find(
(v) => v.id === verId,
);
const customEd = customEditions.find((e) => e.id === verId);
const displayName = builtin?.name || customEd?.name || versionPkg?.name;
const displayName =
builtin?.name || customEd?.name || versionPkg?.name;
if (!displayName) return null;
return (
<span
@@ -1937,8 +1940,16 @@ function InstallModal({
isPluginTab?: boolean;
}) {
const game = useContext(GameContext);
const availableEditions =
const allInstalled =
game?.editions.filter((e) => game.installs.includes(e.id)) || [];
const availableEditions =
pkg.required_versions && pkg.required_versions.length > 0
? allInstalled.filter((e) =>
pkg.required_versions!.some(
(rv) => e.id === rv || e.id.startsWith(rv + "_"),
),
)
: allInstalled;
const [focusedIdx, setFocusedIdx] = useState(0);
const [status, setStatus] = useState<
"idle" | "installing" | "success" | "error"