diff --git a/public/images/lcelive.png b/public/images/lcelive.png deleted file mode 100644 index 47345a6..0000000 Binary files a/public/images/lcelive.png and /dev/null differ diff --git a/src/components/common/SkinViewer.tsx b/src/components/common/SkinViewer.tsx index 33d0f85..a2491c6 100644 --- a/src/components/common/SkinViewer.tsx +++ b/src/components/common/SkinViewer.tsx @@ -507,7 +507,7 @@ const SkinViewer = memo(function SkinViewer({ setActiveView("screenshots"); } else if (focusIndex === 3) { playPressSound(); - setActiveView("lcelive"); + setActiveView("lceonline"); } } }; @@ -749,7 +749,7 @@ const SkinViewer = memo(function SkinViewer({ onMouseEnter={() => isFocusedSection && setFocusIndex(3)} onClick={() => { playPressSound(); - setActiveView("lcelive"); + setActiveView("lceonline"); }} className={`mc-sq-btn w-12 h-12 flex items-center justify-center outline-none border-none ${isFocusedSection && focusIndex === 3 ? "" : ""}`} style={ @@ -760,11 +760,11 @@ const SkinViewer = memo(function SkinViewer({ } : {} } - title="LCELive" + title="LCE Online" > LCELive diff --git a/src/components/modals/ChooseInstanceModal.tsx b/src/components/modals/ChooseInstanceModal.tsx index 1592e4d..e10925b 100644 --- a/src/components/modals/ChooseInstanceModal.tsx +++ b/src/components/modals/ChooseInstanceModal.tsx @@ -1,8 +1,17 @@ import { useState, useEffect } from "react"; import { motion } from "framer-motion"; import { TauriService } from "../../services/TauriService"; -import { lceLiveService, GameInvite } from "../../services/LceLiveService"; +import { lceOnlineService } from "../../services/LceOnlineService"; import type { Edition } from "../../types/edition"; +interface GameInvite { + inviteId: string; + from: string | { displayName: string }; + hostIp: string; + hostPort: number; + hostName: string; + signalingSessionId?: string; + status: string; +} export default function ChooseInstanceModal({ isOpen, @@ -26,9 +35,8 @@ export default function ChooseInstanceModal({ const [error, setError] = useState(""); const [isJoining, setIsJoining] = useState(false); const [focusIndex, setFocusIndex] = useState(0); - const validInstances = editions.filter((e: Edition) => - installs.includes(e.instanceId) + installs.includes(e.instanceId), ); useEffect(() => { @@ -52,29 +60,33 @@ export default function ChooseInstanceModal({ playPressSound(); setIsJoining(true); setError(""); - setStatus("Accepting invite..."); + setStatus("Launching game..."); try { - const inviteData = await lceLiveService.acceptGameInvite(invite.inviteId) as Record; - const fromIp = typeof invite.from !== 'string' ? (invite.from as unknown as Record).hostIp : undefined; - const hostIp: string = (inviteData.hostIp as string) || fromIp || invite.hostIp; - const hostPort: number = (inviteData.hostPort as number) || invite.hostPort; - const sessionId = (inviteData.signalingSessionId as string) || invite.signalingSessionId || ""; - + const sessionId = invite.signalingSessionId || ""; if (sessionId) { setStatus("Connecting via relay..."); - const accessToken = lceLiveService.accessToken ?? ""; + const accessToken = lceOnlineService.accessToken ?? ""; await TauriService.startRelayProxy(accessToken, sessionId); setStatus("Launching game..."); await TauriService.launchGame( selectedInstance, - [{ name: invite.hostName || "LCELive Game", ip: "127.0.0.1", port: 61000 }], - ["-ip", "127.0.0.1", "-port", "61000", "-quitondisconnect"] + [ + { + name: invite.hostName || "LCE Online Game", + ip: "127.0.0.1", + port: 61000, + }, + ], + ["-ip", "127.0.0.1", "-port", "61000", "-quitondisconnect"], ); } else { - setStatus("Launching game..."); await TauriService.stopAllProxies(); await TauriService.launchGame(selectedInstance, [ - { name: invite.hostName || "LCELive Game", ip: hostIp, port: hostPort } + { + name: invite.hostName || "LCE Online Game", + ip: invite.hostIp, + port: invite.hostPort, + }, ]); } onClose(); @@ -102,13 +114,18 @@ export default function ChooseInstanceModal({ setFocusIndex((prev) => (prev - 1 + max) % max); } else if (e.key === "Enter") { if (focusIndex === 0 && validInstances.length > 0) { - const currentIdx = validInstances.findIndex((i: Edition) => i.instanceId === selectedInstance); + const currentIdx = validInstances.findIndex( + (i: Edition) => i.instanceId === selectedInstance, + ); const next = (currentIdx + 1) % validInstances.length; setSelectedInstance(validInstances[next].instanceId); playPressSound(); } else if (focusIndex === 1 && !isJoining) { handleJoin(); - } else if (focusIndex === (validInstances.length > 0 ? 2 : 1) && !isJoining) { + } else if ( + focusIndex === (validInstances.length > 0 ? 2 : 1) && + !isJoining + ) { onClose(); } } @@ -118,8 +135,11 @@ export default function ChooseInstanceModal({ }, [isOpen, focusIndex, selectedInstance, validInstances, isJoining]); if (!isOpen) return null; - - const hostName = invite ? (typeof invite.from === 'string' ? invite.from : invite.from.displayName) : ""; + const hostName = invite + ? typeof invite.from === "string" + ? invite.from + : invite.from.displayName + : ""; return ( {validInstances.length > 0 ? ( -
+
{validInstances.map((inst: Edition) => { const isSelected = selectedInstance === inst.instanceId; return (
{ playPressSound(); setSelectedInstance(inst.instanceId); }} + onClick={() => { + playPressSound(); + setSelectedInstance(inst.instanceId); + }} onMouseEnter={() => setFocusIndex(0)} className={`w-full px-4 py-3 cursor-pointer flex items-center gap-3 transition-all outline-none border-none ${isSelected ? "bg-white/15 border-l-4 border-[#FFFF55]" : "bg-black/20 hover:bg-black/30 border-l-4 border-transparent"}`} style={{ imageRendering: "pixelated" }} > -
- {isSelected &&
} +
+ {isSelected && ( +
+ )}
- {inst.name} + + {inst.name} + {inst.selectedBranch && ( - {inst.selectedBranch} + + {inst.selectedBranch} + )}
@@ -189,10 +225,15 @@ export default function ChooseInstanceModal({ const cancelIdx = validInstances.length > 0 ? 2 : 1; setFocusIndex(cancelIdx); }} - onClick={() => { playBackSound(); onClose(); }} + onClick={() => { + playBackSound(); + onClose(); + }} className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${(() => { const cancelIdx = validInstances.length > 0 ? 2 : 1; - return focusIndex === cancelIdx ? "text-[#FFFF55]" : "text-white"; + return focusIndex === cancelIdx + ? "text-[#FFFF55]" + : "text-white"; })()}`} style={{ backgroundImage: (() => { @@ -213,9 +254,10 @@ export default function ChooseInstanceModal({ onClick={handleJoin} className={`w-32 h-10 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none ${focusIndex === 1 ? "text-[#FFFF55]" : "text-white"}`} style={{ - backgroundImage: focusIndex === 1 - ? "url('/images/button_highlighted.png')" - : "url('/images/Button_Background.png')", + backgroundImage: + focusIndex === 1 + ? "url('/images/button_highlighted.png')" + : "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated", }} @@ -232,7 +274,9 @@ export default function ChooseInstanceModal({
-

{status}

+

+ {status} +

{error && (
diff --git a/src/components/views/LceLiveView.tsx b/src/components/views/LceOnlineView.tsx similarity index 50% rename from src/components/views/LceLiveView.tsx rename to src/components/views/LceOnlineView.tsx index 5372e0b..b7e7bb5 100644 --- a/src/components/views/LceLiveView.tsx +++ b/src/components/views/LceOnlineView.tsx @@ -1,74 +1,49 @@ -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, - useConfig, - useAudio, - useGame, -} from "../../context/LauncherContext"; -import { - lceLiveService, - LceLiveAccount, - FriendRequest, - GameInvite, - DeviceLinkStartResponse, -} from "../../services/LceLiveService"; +import { useUI, useConfig, useAudio } from "../../context/LauncherContext"; +import { lceOnlineService } from "../../services/LceOnlineService"; import { TauriService } from "../../services/TauriService"; -import ChooseInstanceModal from "../modals/ChooseInstanceModal"; -import QRCode from "qrcode"; - -interface LceLiveViewProps { +interface LceOnlineViewProps { addFriendTarget?: string | null; onClearAddFriendTarget?: () => void; } -const LceLiveView = memo(function LceLiveView({ +const LceOnlineView = memo(function LceOnlineView({ addFriendTarget, onClearAddFriendTarget, -}: LceLiveViewProps) { +}: LceOnlineViewProps) { const { setActiveView } = useUI(); const { animationsEnabled } = useConfig(); const { playPressSound, playBackSound } = useAudio(); - const { editions, installs } = useGame(); - const [isSignedIn, setIsSignedIn] = useState(lceLiveService.signedIn); - const [currentTab, setCurrentTab] = useState< - "friends" | "requests" | "invites" | "device_link" - >("friends"); - const [focusIndex, setFocusIndex] = useState(0); - const [acceptInvite, setAcceptInvite] = useState(null); - const [friends, setFriends] = useState([]); - const [incomingReqs, setIncomingReqs] = useState([]); - const [outgoingReqs, setOutgoingReqs] = useState([]); - const [invites, setInvites] = useState([]); - const [linkData, setLinkData] = useState( - null, + const [isSignedIn, setIsSignedIn] = useState(lceOnlineService.signedIn); + const [currentTab, setCurrentTab] = useState<"friends" | "requests">( + "friends", ); - const [linkError, setLinkError] = useState(null); + const [focusIndex, setFocusIndex] = useState(0); + const [friends, setFriends] = useState([]); + const [incomingReqs, setIncomingReqs] = useState([]); + const [outgoingReqs, setOutgoingReqs] = useState([]); 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>(new Set()); - const [showHostMethodPicker, setShowHostMethodPicker] = useState(false); const [isAddingFriend, setIsAddingFriend] = useState(false); const [addFriendUsername, setAddFriendUsername] = useState(""); const addFriendInputRef = useRef(null); const [errorModal, setErrorModal] = useState(null); - const [qrDataUrl, setQrDataUrl] = useState(""); + const [authMode, setAuthMode] = useState<"login" | "register">("login"); + const [authUsername, setAuthUsername] = useState(""); + const [authPassword, setAuthPassword] = useState(""); + const [isAuthLoading, setIsAuthLoading] = useState(false); + const authUserRef = useRef(null); + const authPassRef = useRef(null); const containerRef = useRef(null); const scrollRef = useRef(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 +51,21 @@ 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 | 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)); - } - }; - - 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), - ); - } - - 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 handleAction = async (action: () => Promise) => { @@ -166,54 +78,15 @@ const LceLiveView = memo(function LceLiveView({ } }; - const handleStartHosting = () => { + const handleStartHosting = async () => { playPressSound(); - setShowHostMethodPicker(true); - setFocusIndex(0); - }; - - const handleHostDirect = async () => { - setShowHostMethodPicker(false); - setFocusIndex(0); setIsDiscovering(true); - setHostStatus("Discovering external IP..."); + setHostStatus("Starting relay..."); 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); - } + await TauriService.stunDiscover(); + } catch {} setIsHosting(true); - setHostStatus("Relay ready - invite friends to activate"); - setInvitedFriends(new Set()); + setHostStatus("Relay ready"); setIsDiscovering(false); }; @@ -221,59 +94,33 @@ const LceLiveView = memo(function LceLiveView({ 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 hostUsername = lceLiveService.account?.username ?? name; + const handleAuth = async () => { + if (!authUsername.trim() || !authPassword.trim()) return; + setIsAuthLoading(true); try { - await lceLiveService.sendGameInvite( - friend.accountId, - hostIp, - hostPort, - name, - hostUsername, - ); - setInvitedFriends((prev) => new Set(prev).add(friend.accountId)); - setHostStatus("Connecting relay..."); - TauriService.startHostRelay( - lceLiveService.accessToken ?? "", - 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"); - }); + if (authMode === "login") { + await lceOnlineService.login(authUsername.trim(), authPassword); + } else { + await lceOnlineService.register(authUsername.trim(), authPassword); + } + setIsSignedIn(true); } catch (e: unknown) { - const msg = - e instanceof Error - ? e.message - : typeof e === "string" - ? e - : "Unknown error"; - setErrorModal("Failed to send invite: " + msg); + setErrorModal(e instanceof Error ? e.message : "Authentication failed"); + } finally { + setIsAuthLoading(false); } }; type MenuItem = { id: string; - type: "button" | "friend" | "request_in" | "request_out" | "invite"; + type: "button" | "friend" | "request_in" | "request_out"; label: string; onClick: () => void; onClickSecondary?: () => void; @@ -281,20 +128,8 @@ const LceLiveView = memo(function LceLiveView({ const menuItems = useMemo(() => { const items: MenuItem[] = []; - if (currentTab === "device_link") { - if (linkData) { - items.push({ - id: "link_retry", - type: "button", - label: "Restart Link", - onClick: () => { - setLinkData(null); - playPressSound(); - }, - }); - } - } else if (currentTab === "friends") { - if (!isDiscovering && !showHostMethodPicker) { + if (currentTab === "friends") { + if (!isDiscovering) { if (!isHosting) { items.push({ id: "host_game", @@ -329,56 +164,31 @@ 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)), - onClickSecondary: isHosting - ? () => handleAction(() => lceLiveService.removeFriend(f.accountId)) - : undefined, + label: f, + onClick: () => handleAction(() => lceOnlineService.removeFriend(f)), }); }); } 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), - ), - }); - }); - } else if (currentTab === "invites") { - invites.forEach((inv) => { - items.push({ - id: `inv_${inv.inviteId}`, - type: "invite", - label: - typeof inv.from === "string" ? "Unknown" : inv.from.displayName, - onClick: () => { - playPressSound(); - setAcceptInvite(inv); - }, - onClickSecondary: () => - handleAction(() => lceLiveService.declineGameInvite(inv.inviteId)), + handleAction(() => lceOnlineService.declineFriendRequest(r)), }); }); } @@ -389,28 +199,12 @@ const LceLiveView = memo(function LceLiveView({ friends, 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", - "invites", - ]; + const tabs: ("friends" | "requests")[] = ["friends", "requests"]; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (errorModal) { @@ -427,7 +221,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); } @@ -435,23 +229,24 @@ 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; + } + if (e.key === "Tab") { + e.preventDefault(); + if (document.activeElement === authUserRef.current) { + authPassRef.current?.focus(); + } else { + authUserRef.current?.focus(); } + return; + } + if (e.key === "Enter") { + handleAuth(); + return; } return; } @@ -462,22 +257,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; @@ -506,9 +299,10 @@ const LceLiveView = memo(function LceLiveView({ isAddingFriend, addFriendUsername, errorModal, - showHostMethodPicker, - handleHostDirect, - handleHostRelay, + handleAuth, + isSignedIn, + authUsername, + authPassword, ]); useEffect(() => { @@ -530,52 +324,65 @@ const LceLiveView = memo(function LceLiveView({ } } }, [focusIndex, isAddingFriend]); + const renderContent = () => { - if (currentTab === "device_link") { + if (!isSignedIn) { return (
- {!linkData ? ( -

- {linkError || "Starting device link..."} -

- ) : ( -
-
-

- Open this link in your browser: -

-

- {linkData.verificationUri} -

-

- And enter the code: -

-

- {linkData.userCode} -

-
-
- {qrDataUrl && ( - QR Code - )} - -
-
- )} +

+ {authMode === "login" ? "Sign In" : "Register"} +

+ setAuthUsername(e.target.value)} + disabled={isAuthLoading} + /> + setAuthPassword(e.target.value)} + disabled={isAuthLoading} + /> +
+ + +
); } @@ -614,11 +421,7 @@ const LceLiveView = memo(function LceLiveView({
- {currentTab === "friends" - ? "Joinable Friends" - : currentTab === "requests" - ? "Pending Requests" - : "Game Invites"} + {currentTab === "friends" ? "Friends" : "Pending Requests"} {listItems.length}
@@ -646,28 +449,10 @@ const LceLiveView = memo(function LceLiveView({ {item.label} - - @ - {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"} -
- {item.type === "friend" && !isHosting && ( + {item.type === "friend" && ( - - - )} {item.type === "request_out" && ( ))} @@ -840,14 +583,6 @@ const LceLiveView = memo(function LceLiveView({ > {renderContent()}
-
- LCELive TauriService.openUrl("https://lcelive.co.uk")} - /> -
@@ -889,7 +624,7 @@ const LceLiveView = memo(function LceLiveView({ playPressSound(); if (addFriendUsername.trim() !== "") { handleAction(() => - lceLiveService.sendFriendRequest( + lceOnlineService.sendFriendRequest( addFriendUsername.trim(), ), ); @@ -918,83 +653,6 @@ const LceLiveView = memo(function LceLiveView({ )} - - - {showHostMethodPicker && ( - -
-

- Host Game -

- - - -
-
- )} -
- {errorModal && ( )} - - { - setAcceptInvite(null); - fetchSocialData(); - }} - playPressSound={playPressSound} - playBackSound={playBackSound} - editions={editions} - installs={installs} - invite={acceptInvite} - /> ); }); -export default LceLiveView; +export default LceOnlineView; diff --git a/src/hooks/useDiscordRPC.ts b/src/hooks/useDiscordRPC.ts index e437d39..343234a 100644 --- a/src/hooks/useDiscordRPC.ts +++ b/src/hooks/useDiscordRPC.ts @@ -62,7 +62,7 @@ export function useDiscordRPC({ devtools: "Developing for LCE", skins: "Changing Skins", workshop: "Browsing Workshop", - lcelive: "Browsing Friends", + lceonline: "Browsing Friends", "pck-editor": "Editing a PCK file", "options-editor": "Editing Options Files", "arc-editor": "Editing an ARC file", diff --git a/src/hooks/useLceLiveNotifications.ts b/src/hooks/useLceLiveNotifications.ts deleted file mode 100644 index 6f8140a..0000000 --- a/src/hooks/useLceLiveNotifications.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { useState, useEffect, useRef } from "react"; -import { lceLiveService, type FriendRequest, type GameInvite } from "../services/LceLiveService"; -export function useLceLiveNotifications() { - const [friendRequestMessage, setFriendRequestMessage] = useState(null); - const [gameInviteMessage, setGameInviteMessage] = useState(null); - const seenRequests = useRef>(new Set()); - const seenInvites = useRef>(new Set()); - useEffect(() => { - let pollInterval: ReturnType; - const init = async () => { - if (lceLiveService.signedIn) { - try { - await lceLiveService.refreshSession(); - } catch (e) { } - } - - if (lceLiveService.signedIn) { - try { - const [reqs, invs] = await Promise.all([ - lceLiveService.getPendingRequests(), - lceLiveService.getGameInvites() - ]); - reqs.incoming.forEach((r: FriendRequest) => seenRequests.current.add(r.accountId)); - invs.filter((i: GameInvite) => i.status === "pending").forEach((i: GameInvite) => seenInvites.current.add(i.inviteId)); - } catch (e) { } - } - - pollInterval = setInterval(async () => { - if (!lceLiveService.signedIn) return; - try { - const [reqs, invs] = await Promise.all([ - lceLiveService.getPendingRequests(), - lceLiveService.getGameInvites() - ]); - - reqs.incoming.forEach((r: FriendRequest) => { - if (!seenRequests.current.has(r.accountId)) { - seenRequests.current.add(r.accountId); - setFriendRequestMessage(`New request from ${r.displayName}`); - } - }); - - invs.filter((i: GameInvite) => i.status === "pending").forEach((i: GameInvite) => { - if (!seenInvites.current.has(i.inviteId)) { - seenInvites.current.add(i.inviteId); - const fromName = typeof i.from === 'string' ? "Unknown" : i.from.displayName; - setGameInviteMessage(`${fromName} invited you to play!`); - } - }); - } catch (e) { } - }, 10000); - }; - - init(); - return () => { - if (pollInterval) clearInterval(pollInterval); - }; - }, []); - - return { - friendRequestMessage, - gameInviteMessage, - clearFriendRequestMessage: () => setFriendRequestMessage(null), - clearGameInviteMessage: () => setGameInviteMessage(null), - }; -} diff --git a/src/hooks/useLceOnlineNotifications.ts b/src/hooks/useLceOnlineNotifications.ts new file mode 100644 index 0000000..b54fc60 --- /dev/null +++ b/src/hooks/useLceOnlineNotifications.ts @@ -0,0 +1,41 @@ +import { useState, useEffect, useRef } from "react"; +import { lceOnlineService } from "../services/LceOnlineService"; +export function useLceOnlineNotifications() { + const [friendRequestMessage, setFriendRequestMessage] = useState(null); + const seenRequests = useRef>(new Set()); + useEffect(() => { + let pollInterval: ReturnType; + + const init = async () => { + if (lceOnlineService.signedIn) { + try { + const lists = await lceOnlineService.getSocialLists(); + lists.requests.forEach((r: string) => seenRequests.current.add(r)); + } catch (e) { } + } + + pollInterval = setInterval(async () => { + if (!lceOnlineService.signedIn) return; + try { + const lists = await lceOnlineService.getSocialLists(); + lists.requests.forEach((r: string) => { + if (!seenRequests.current.has(r)) { + seenRequests.current.add(r); + setFriendRequestMessage(`New request from ${r}`); + } + }); + } catch (e) { } + }, 10000); + }; + + init(); + return () => { + if (pollInterval) clearInterval(pollInterval); + }; + }, []); + + return { + friendRequestMessage, + clearFriendRequestMessage: () => setFriendRequestMessage(null), + }; +} diff --git a/src/pages/App.tsx b/src/pages/App.tsx index bb47a53..b501e3f 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -18,7 +18,7 @@ import OptionsEditorView from "../components/views/OptionsEditorView"; import ModelEditorView from "../components/views/ModelEditorView"; import ScreenshotsView from "../components/views/ScreenshotsView"; import SwfView from "../components/views/SwfView"; -import LceLiveView from "../components/views/LceLiveView"; +import LceOnlineView from "../components/views/LceOnlineView"; import CreditsView from "../components/views/CreditsView"; import SkinViewer from "../components/common/SkinViewer"; import PanoramaBackground from "../components/common/PanoramaBackground"; @@ -35,7 +35,7 @@ import { useSkin, } from "../context/LauncherContext"; import { TauriService } from "../services/TauriService"; -import { useLceLiveNotifications } from "../hooks/useLceLiveNotifications"; +import { useLceOnlineNotifications } from "../hooks/useLceOnlineNotifications"; import { usePluginViews } from "../plugins/PluginContext"; import { usePlatform } from "../hooks/usePlatform"; import { PluginManager } from "../plugins/PluginManager"; @@ -67,12 +67,10 @@ export default function App() { const game = useGame(); const skin = useSkin(); const { skinUrl, setSkinUrl, capeUrl } = skin; - const notifications = useLceLiveNotifications(); + const notifications = useLceOnlineNotifications(); const { friendRequestMessage, - gameInviteMessage, clearFriendRequestMessage, - clearGameInviteMessage, } = notifications; const [showSetup, setShowSetup] = useState(false); const [isSetupChecked, setIsSetupChecked] = useState(false); @@ -158,13 +156,13 @@ export default function App() { } if ( - action === "lcelive" && + action === "lceonline" && parts.length >= 2 && parts[1] === "addfriend" ) { const username = parsed.searchParams.get("username"); if (username) { - setActiveView("lcelive"); + setActiveView("lceonline"); setAddFriendTarget(username); return; } @@ -614,9 +612,9 @@ export default function App() { {activeView === "swf-editor" && ( )} - {activeView === "lcelive" && ( - setAddFriendTarget(null)} /> @@ -662,22 +660,12 @@ export default function App() { onClose={clearFriendRequestMessage} onClick={() => { clearFriendRequestMessage(); - setActiveView("lcelive"); + setActiveView("lceonline"); }} title="Friend Request" variant="update" /> - { - clearGameInviteMessage(); - setActiveView("lcelive"); - }} - title="Game Invite" - variant="update" - />
); diff --git a/src/services/LceLiveService.ts b/src/services/LceLiveService.ts deleted file mode 100644 index 39eeae8..0000000 --- a/src/services/LceLiveService.ts +++ /dev/null @@ -1,384 +0,0 @@ -const LOCAL_STORAGE_KEY = "lcelive_session"; -const DEFAULT_BASE_URL = "https://api.lcelive.co.uk"; -const MAX_REFRESH_RETRIES = 1; -import { TauriService } from "./TauriService"; -export interface LceLiveAccount { - accountId: string; - username: string; - displayName: string; -} - -export interface SessionData { - accessToken: string; - refreshToken: string; - account: LceLiveAccount; -} - -export interface DeviceLinkStartResponse { - deviceCode: string; - userCode: string; - verificationUri: string; - verificationUriComplete?: string; - intervalSeconds: number; - expiresInSeconds?: number; -} - -export interface DeviceLinkPollResponse { - status: string; - isLinked?: boolean; - accessToken?: string; - refreshToken?: string; - account?: LceLiveAccount; -} - -export interface GameInvite { - inviteId: string; - from: LceLiveAccount | string; - hostIp: string; - hostPort: number; - hostName: string; - signalingSessionId?: string; - status: string; -} - -export interface FriendRequest { - accountId: string; - username: string; - displayName: string; -} - -export interface PendingRequests { - incoming: FriendRequest[]; - outgoing: FriendRequest[]; -} - -export class LceLiveService { - private _session: SessionData | null = null; - private baseUrl: string = DEFAULT_BASE_URL; - private _refreshPromise: Promise | null = null; - - constructor() { - this.loadSession(); - } - - setBaseUrl(url: string) { - this.baseUrl = url.replace(/\/$/, ""); - } - - get signedIn(): boolean { - return this._session !== null; - } - - get account(): LceLiveAccount | null { - return this._session?.account || null; - } - - get displayUsername(): string { - if (!this._session) return "Not signed in"; - return ( - this._session.account.displayName || - this._session.account.username || - "Unknown" - ); - } - - get apiBaseUrl(): string { - return this.baseUrl; - } - - get accessToken(): string | null { - return this._session?.accessToken || null; - } - - public generateDeviceId(): string { - let id = localStorage.getItem("lcelive_device_id"); - if (!id) { - id = crypto.randomUUID(); - localStorage.setItem("lcelive_device_id", id); - } - return id; - } - - public getDeviceName(): string { - return "LCE Emerald Launcher"; - } - - private loadSession() { - try { - const data = localStorage.getItem(LOCAL_STORAGE_KEY); - if (data) { - this._session = JSON.parse(data); - } - } catch (e) { - console.warn("Failed to load LceLive session", e); - } - } - - private saveSession() { - if (this._session) { - localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this._session)); - } else { - localStorage.removeItem(LOCAL_STORAGE_KEY); - } - } - - private async request( - method: string, - path: string, - body?: unknown, - authed: boolean = true, - retryCount: number = 0, - ): Promise { - if (authed && this._session?.refreshToken && retryCount === 0) { - try { - await this.refreshSession(); //neo: i do this on every request only because it doesnt always return 401 - } catch (err) { - this.logoutLocal(); - throw new Error("Session expired. Please log in again."); - } - } - - const headers: Record = { - Accept: "application/json", - "User-Agent": "MCLCE-LceLive/1.0", - }; - - if (body) { - headers["Content-Type"] = "application/json"; - } - - if (authed && this._session?.accessToken) { - headers["Authorization"] = `Bearer ${this._session.accessToken}`; - } - - const bodyStr = body ? JSON.stringify(body) : null; - - let res; - try { - res = await TauriService.httpProxyRequest( - method, - `${this.baseUrl}${path}`, - bodyStr, - headers, - ); - } catch (e) { - throw new Error(`Network error when calling ${path}: ${e}`); - } - - if ( - res.status === 401 && - authed && - this._session?.refreshToken && - retryCount < MAX_REFRESH_RETRIES - ) { - try { - await this.refreshSession(); - return this.request(method, path, body, authed, retryCount + 1); - } catch (err) { - this.logoutLocal(); - throw new Error("Session expired. Please log in again."); - } - } - - let data; - try { - data = res.body ? JSON.parse(res.body) : {}; - } catch { - data = { message: res.body }; - } - - if (res.status >= 400) { - const errorMsg = - data.message || - data.detail || - data.title || - data.error || - `HTTP ${res.status}`; - throw new Error(errorMsg); - } - - return data; - } - - async startDeviceLink(): Promise { - return this.request( - "POST", - "/api/auth/device/start", - { - deviceId: this.generateDeviceId(), - deviceName: this.getDeviceName(), - }, - false, - ); - } - - async pollDeviceLink(deviceCode: string): Promise { - const data = await this.request( - "GET", - `/api/auth/device/poll/${deviceCode}`, - null, - false, - ); - if (data.isLinked && data.accessToken) { - this._session = { - accessToken: data.accessToken, - refreshToken: data.refreshToken ?? "", - account: data.account ?? { - accountId: "", - username: "", - displayName: "", - }, - }; - this.saveSession(); - } - return data; - } - - async refreshSession(): Promise { - if (!this._session?.refreshToken) throw new Error("No refresh token"); - - if (this._refreshPromise) { - return this._refreshPromise; - } - - this._refreshPromise = (async () => { - try { - const data = await this.request( - "POST", - "/api/auth/refresh", - { - refreshToken: this._session!.refreshToken, - }, - false, - ); - this._session = { - accessToken: data.accessToken, - refreshToken: data.refreshToken, - account: data.account, - }; - this.saveSession(); - } finally { - this._refreshPromise = null; - } - })(); - - return this._refreshPromise; - } - - async logout(): Promise { - if (this._session?.refreshToken) { - try { - await this.request( - "POST", - "/api/auth/logout", - { - refreshToken: this._session.refreshToken, - }, - true, - ); - } catch (e) { - console.warn("Server logout failed", e); - } - } - this.logoutLocal(); - } - - logoutLocal(): void { - this._session = null; - this.saveSession(); - } - - async getFriends(): Promise { - const data = await this.request("GET", "/api/social/friends"); - return data.friends || []; - } - - async removeFriend(accountId: string): Promise { - await this.request("DELETE", `/api/social/friends/${accountId}`); - } - - async sendFriendRequest(username: string): Promise { - await this.request("POST", "/api/social/request", { username }); - } - - async getPendingRequests(): Promise { - const data = await this.request("GET", "/api/social/requests"); - return { - incoming: (data.incoming || []).map((r: Record) => ({ - accountId: r.requesterUserId || r.accountId || r.userId, - username: r.requesterUsername || r.username, - displayName: r.requesterDisplayName || r.displayName, - })), - outgoing: (data.outgoing || []).map((r: Record) => ({ - accountId: r.targetUserId || r.accountId || r.userId, - username: r.targetUsername || r.username, - displayName: r.targetDisplayName || r.displayName, - })), - }; - } - - async acceptFriendRequest(accountId: string): Promise { - await this.request("POST", `/api/social/requests/${accountId}/accept`, {}); - } - - async declineFriendRequest(accountId: string): Promise { - await this.request("POST", `/api/social/requests/${accountId}/decline`, {}); - } - - async getGameInvites(): Promise { - const data = await this.request("GET", "/api/sessions/invites"); - const incoming = data.incoming || []; - return incoming.map((inv: Record) => ({ - inviteId: inv.inviteId, - from: { - accountId: inv.senderAccountId, - username: inv.senderUsername, - displayName: inv.senderDisplayName, - }, - hostIp: inv.hostIp, - hostPort: inv.hostPort, - hostName: inv.hostName, - status: inv.status, - signalingSessionId: inv.signalingSessionId, - })); - } - - async sendGameInvite( - recipientAccountId: string, - hostIp: string, - hostPort: number, - hostName: string, - signalingSessionId?: string, - ): Promise { - await this.request("POST", "/api/sessions/invites", { - recipientAccountId, - hostIp, - hostPort, - hostName, - signalingSessionId, - }); - } - - async acceptGameInvite(inviteId: string): Promise> { - return this.request("POST", `/api/sessions/invites/${inviteId}/accept`, {}); - } - - async declineGameInvite(inviteId: string): Promise { - await this.request("POST", `/api/sessions/invites/${inviteId}/decline`, {}); - } - - async deactivateGameInvites(): Promise { - await this.request("POST", "/api/sessions/invites/deactivate", {}); - } - - async requestJoinTicket(): Promise { - const data = await this.request("POST", "/api/sessions/ticket", {}); - return data.ticket; - } - - async validateJoinTicket(ticket: string): Promise { - return this.request("POST", "/api/sessions/validate", { ticket }, false); - } -} - -export const lceLiveService = new LceLiveService(); diff --git a/src/services/LceOnlineService.ts b/src/services/LceOnlineService.ts new file mode 100644 index 0000000..8de40ac --- /dev/null +++ b/src/services/LceOnlineService.ts @@ -0,0 +1,209 @@ +const SESSION_KEY = "lceonline_session"; +const SOCIAL_BASE_URL = "https://social.mclegacyedition.xyz"; +const AUTH_BASE_URL = "https://auth.mclegacyedition.xyz"; //neo: yeah bro im hardcoding all three +import { TauriService } from "./TauriService"; +export interface LceOnlineAccount { + username: string; + displayName: string; +} + +export interface SessionData { + accessToken: string; + account: LceOnlineAccount; +} + +export interface FriendRequest { + username: string; + displayName: string; +} + +export class LceOnlineService { + private _session: SessionData | null = null; + private baseUrl: string = SOCIAL_BASE_URL; + + constructor() { + this.loadSession(); + } + + get signedIn(): boolean { + return this._session !== null; + } + + get account(): LceOnlineAccount | null { + return this._session?.account || null; + } + + get displayUsername(): string { + if (!this._session) return "Not signed in"; + return ( + this._session.account.displayName || + this._session.account.username || + "Unknown" + ); + } + + get accessToken(): string | null { + return this._session?.accessToken || null; + } + + logoutLocal(): void { + this._session = null; + this.saveSession(); + } + + async login(username: string, password: string): Promise { + const res = await this.request<{ body: string }>( + "POST", + "/login", + `${username}:${password}`, + AUTH_BASE_URL, + ); + console.log("auth login response", res); + const pretoken = + res && typeof res === "object" && "body" in res + ? (res as any).body + : typeof res === "string" + ? res + : ""; + const token = pretoken.split(":")[1]; + this.loginWithToken(token, username); + } + + async register(username: string, password: string): Promise { + const res = await this.request<{ body: string }>( + "POST", + "/register", + `${username}:${password}`, + AUTH_BASE_URL, + ); + console.log("auth register response", res); + const pretoken = + res && typeof res === "object" && "body" in res + ? (res as any).body + : typeof res === "string" + ? res + : ""; + const token = pretoken.split(":")[1]; + this.loginWithToken(token, username); + } + + loginWithToken(token: string, username: string) { + this._session = { + accessToken: token, + account: { username, displayName: username }, + }; + this.saveSession(); + } + + private loadSession() { + try { + const data = localStorage.getItem(SESSION_KEY); + if (data) { + this._session = JSON.parse(data); + } + } catch (e) { + console.warn("Failed to load LCE Online session", e); + } + } + + private saveSession() { + if (this._session) { + localStorage.setItem(SESSION_KEY, JSON.stringify(this._session)); + } else { + localStorage.removeItem(SESSION_KEY); + } + } + + private async request( + method: string, + path: string, + body?: string | null, + baseUrl?: string, + ): Promise { + const headers: Record = { + Accept: "text/plain, application/json", + "User-Agent": "MCLCE-LCEOnline/1.0", + }; + + if (body) { + headers["Content-Type"] = "text/plain"; + } + + if (this._session?.accessToken) { + headers["Authorization"] = `Bearer ${this._session.accessToken}`; + } + + const url = `${baseUrl || this.baseUrl}${path}`; + let res; + try { + res = await TauriService.httpProxyRequest( + method, + url, + body ?? null, + headers, + ); + } catch (e) { + throw new Error(`Network error when calling ${path}: ${e}`); + } + + let data; + try { + data = res.body ? JSON.parse(res.body) : {}; + } catch { + data = res.body ?? {}; + } + + if (res.status >= 400) { + const errorMsg = + data.message || + data.detail || + data.title || + data.error || + data || + `HTTP ${res.status}`; + throw new Error(errorMsg); + } + + return data; + } + + async getSocialLists(): Promise<{ + friends: string[]; + requests: string[]; + blocked: string[]; + }> { + const raw: string = await this.request( + "POST", + "/getSocialLists", + null, + ); + if (typeof raw !== "string") { + return { friends: [], requests: [], blocked: [] }; + } + const withoutPrefix = raw.startsWith("-") ? raw.slice(1) : raw; + const parts = withoutPrefix.split("|"); + return { + friends: parts[0] ? parts[0].split(",").filter(Boolean) : [], + requests: parts[1] ? parts[1].split(",").filter(Boolean) : [], + blocked: parts[2] ? parts[2].split(",").filter(Boolean) : [], + }; + } + + async sendFriendRequest(target: string): Promise { + await this.request("POST", "/sendrequest", target); + } + + async acceptFriendRequest(from: string): Promise { + await this.request("POST", "/acceptrequest", from); + } + + async declineFriendRequest(from: string): Promise { + await this.request("POST", "/declinerequest", from); + } + + async removeFriend(from: string): Promise { + await this.request("POST", "/removefriend", from); + } +} + +export const lceOnlineService = new LceOnlineService();