mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-08-20 04:27:29 +00:00
feat: add playerhead to rpc
This commit is contained in:
parent
eefd64771b
commit
abf62839e1
BIN
public/fonts/Mojang Font_11.ttf
Normal file
BIN
public/fonts/Mojang Font_11.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Mojang Font_7.ttf
Normal file
BIN
public/fonts/Mojang Font_7.ttf
Normal file
Binary file not shown.
|
|
@ -11,10 +11,9 @@ import { usePlatform } from "../../hooks/usePlatform";
|
|||
import type { Edition } from "../../types/edition";
|
||||
|
||||
const HomeView = memo(function HomeView() {
|
||||
const { setActiveView, focusSection, onNavigateToSkin } =
|
||||
useUI();
|
||||
const { setActiveView, focusSection, onNavigateToSkin } = useUI();
|
||||
const { profile, legacyMode } = useConfig();
|
||||
const { playPressSound, playSfx } = useAudio();
|
||||
const { playPressSound } = useAudio();
|
||||
const {
|
||||
handleLaunch,
|
||||
editions,
|
||||
|
|
@ -37,79 +36,85 @@ const HomeView = memo(function HomeView() {
|
|||
|
||||
const hasAnyInstall = installs.length > 0;
|
||||
|
||||
const buttonsVal = useMemo(
|
||||
() => {
|
||||
const mainBtn = {
|
||||
label: !hasAnyInstall
|
||||
? "Install a version"
|
||||
: isGameRunning
|
||||
? "Stop Game"
|
||||
: isDownloading
|
||||
? "Installation in progress..."
|
||||
: isInstalled
|
||||
? "Play Game"
|
||||
: `Download ${selectedVersionName}`,
|
||||
action: !hasAnyInstall
|
||||
? () => setActiveView("versions")
|
||||
: isGameRunning
|
||||
? stopGame
|
||||
: isDownloading
|
||||
? () => {}
|
||||
: isInstalled
|
||||
? handleLaunch
|
||||
: () => toggleInstall(profile),
|
||||
isDanger: isGameRunning,
|
||||
disabled: isDownloading,
|
||||
id: "main-action",
|
||||
};
|
||||
const buttonsVal = useMemo(() => {
|
||||
const mainBtn = {
|
||||
label: !hasAnyInstall
|
||||
? "Install a version"
|
||||
: isGameRunning
|
||||
? "Stop Game"
|
||||
: isDownloading
|
||||
? "Installation in progress..."
|
||||
: isInstalled
|
||||
? "Play Game"
|
||||
: `Download ${selectedVersionName}`,
|
||||
action: !hasAnyInstall
|
||||
? () => setActiveView("versions")
|
||||
: isGameRunning
|
||||
? stopGame
|
||||
: isDownloading
|
||||
? () => {}
|
||||
: isInstalled
|
||||
? handleLaunch
|
||||
: () => toggleInstall(profile),
|
||||
isDanger: isGameRunning,
|
||||
disabled: isDownloading,
|
||||
id: "main-action",
|
||||
};
|
||||
|
||||
const pluginBtns = pluginActions.map((a) => ({
|
||||
label: a.label,
|
||||
action: () => a.onClick(),
|
||||
const pluginBtns = pluginActions.map((a) => ({
|
||||
label: a.label,
|
||||
action: () => a.onClick(),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: a.id,
|
||||
}));
|
||||
|
||||
const menuBtns = [
|
||||
{
|
||||
label: "Help & Options",
|
||||
action: () => setActiveView("settings"),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: a.id,
|
||||
}));
|
||||
id: "settings",
|
||||
},
|
||||
{
|
||||
label: "Versions",
|
||||
action: () => setActiveView("versions"),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: "versions",
|
||||
},
|
||||
{
|
||||
label: "Workshop",
|
||||
action: () => setActiveView("workshop"),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: "workshop",
|
||||
},
|
||||
{
|
||||
label: "Developer Tools",
|
||||
action: () => setActiveView("devtools"),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: "devtools",
|
||||
},
|
||||
].filter((b) => !(isAndroid && b.id === "devtools"));
|
||||
|
||||
const menuBtns = [
|
||||
{
|
||||
label: "Help & Options",
|
||||
action: () => setActiveView("settings"),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: "settings",
|
||||
},
|
||||
{
|
||||
label: "Versions",
|
||||
action: () => setActiveView("versions"),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: "versions",
|
||||
},
|
||||
{
|
||||
label: "Workshop",
|
||||
action: () => setActiveView("workshop"),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: "workshop",
|
||||
},
|
||||
{
|
||||
label: "Developer Tools",
|
||||
action: () => setActiveView("devtools"),
|
||||
isDanger: false,
|
||||
disabled: false,
|
||||
id: "devtools",
|
||||
},
|
||||
].filter((b) => !(isAndroid && b.id === "devtools"));
|
||||
|
||||
return [mainBtn, ...pluginBtns, ...menuBtns];
|
||||
},
|
||||
[
|
||||
isDownloading, hasAnyInstall, isInstalled, selectedVersionName,
|
||||
handleLaunch, toggleInstall, profile, setActiveView,
|
||||
isGameRunning, stopGame, pluginActions, isAndroid,
|
||||
],
|
||||
);
|
||||
return [mainBtn, ...pluginBtns, ...menuBtns];
|
||||
}, [
|
||||
isDownloading,
|
||||
hasAnyInstall,
|
||||
isInstalled,
|
||||
selectedVersionName,
|
||||
handleLaunch,
|
||||
toggleInstall,
|
||||
profile,
|
||||
setActiveView,
|
||||
isGameRunning,
|
||||
stopGame,
|
||||
pluginActions,
|
||||
isAndroid,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFocusedSection) {
|
||||
|
|
@ -127,7 +132,8 @@ const HomeView = memo(function HomeView() {
|
|||
prev === null ? buttonsVal.length - 1 : prev > 0 ? prev - 1 : prev,
|
||||
);
|
||||
if (e.key === "ArrowLeft") onNavigateToSkin();
|
||||
if (e.key === "Enter" && menuFocus !== null) buttonsVal[menuFocus].action();
|
||||
if (e.key === "Enter" && menuFocus !== null)
|
||||
buttonsVal[menuFocus].action();
|
||||
};
|
||||
window.addEventListener("keydown", handleKey);
|
||||
return () => window.removeEventListener("keydown", handleKey);
|
||||
|
|
@ -189,6 +195,7 @@ const HomeView = memo(function HomeView() {
|
|||
|
||||
{!legacyMode && (
|
||||
<div className="pt-4 flex flex-col items-center w-full gap-3">
|
||||
<div className="border-b-[3px] border-[#A0A0A0] w-48 opacity-60" />
|
||||
<div className="flex gap-8">
|
||||
<a
|
||||
href="https://discord.gg/cQVKhQXcCx"
|
||||
|
|
@ -225,18 +232,6 @@ const HomeView = memo(function HomeView() {
|
|||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="border-b-[3px] border-[#A0A0A0] w-48 opacity-60" />
|
||||
<button
|
||||
onClick={() => {
|
||||
if (isFocusedSection) {
|
||||
playSfx("orb.ogg");
|
||||
setActiveView("credits");
|
||||
}
|
||||
}}
|
||||
className={`text-white hover:text-[#FFFF55] text-xl mc-text-shadow tracking-widest transition-colors mt-1 bg-transparent border-none outline-none ${!isFocusedSection ? "pointer-events-none" : ""}`}
|
||||
>
|
||||
CREDITS
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ const SettingsView = memo(function SettingsView() {
|
|||
"fixed inset-0 bg-black/80 flex items-center justify-center z-50";
|
||||
dialog.innerHTML = `
|
||||
<div class="w-[420px] p-4 flex flex-col items-center mc-options-bg">
|
||||
<h3 class="text-2xl font-bold text-[#333333] mb-4 text-left w-full px-4 mc-text-shadow">Reset Setup</h3>
|
||||
<h3 class="text-2xl text-[#333333] mb-4 text-left w-full px-4 mc-text-shadow">Reset Setup</h3>
|
||||
<p class="text-[#333333] mb-8 text-left w-full px-4">Are you sure you want to reset launcher setup?</p>
|
||||
<div class="flex flex-col gap-3 w-full px-4">
|
||||
<button id="reset-cancel" class="w-full h-10 flex items-center justify-center text-lg mc-text-shadow text-white hover:text-[#ffff00]" style="background-image: url('/images/Button_Background.png'); background-size: 100% 100%; image-rendering: pixelated; border: none; cursor: pointer;" onmouseenter="this.style.backgroundImage='url(/images/button_highlighted.png)'" onmouseleave="this.style.backgroundImage='url(/images/Button_Background.png)'">Cancel</button>
|
||||
|
|
@ -189,7 +189,7 @@ const SettingsView = memo(function SettingsView() {
|
|||
"fixed inset-0 bg-black/80 flex items-center justify-center z-50";
|
||||
dialog.innerHTML = `
|
||||
<div class="w-[420px] p-4 flex flex-col items-center mc-options-bg">
|
||||
<h3 class="text-2xl font-bold text-[#333333] mb-2 text-left w-full px-4 mc-text-shadow">CONFIRM RESET</h3>
|
||||
<h3 class="text-2xl text-[#333333] mb-2 text-left w-full px-4 mc-text-shadow">CONFIRM RESET</h3>
|
||||
<div class="text-[#333333] mb-6 text-left w-full px-4">
|
||||
<p class="mb-2">⚠️ This will:</p>
|
||||
<ul class="list-none space-y-1 text-sm">
|
||||
|
|
@ -198,7 +198,7 @@ const SettingsView = memo(function SettingsView() {
|
|||
<li>Show setup screen again</li>
|
||||
<li>Require reconfiguration</li>
|
||||
</ul>
|
||||
<p class="mt-3 text-[#333333] font-bold">This action cannot be undone!</p>
|
||||
<p class="mt-3 text-[#333333]">This action cannot be undone!</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 w-full px-4">
|
||||
<button id="reset-final-cancel" class="w-full h-10 flex items-center justify-center text-lg mc-text-shadow text-white hover:text-[#ffff00]" style="background-image: url('/images/Button_Background.png'); background-size: 100% 100%; image-rendering: pixelated; border: none; cursor: pointer;" onmouseenter="this.style.backgroundImage='url(/images/button_highlighted.png)'" onmouseleave="this.style.backgroundImage='url(/images/Button_Background.png)'">Cancel</button>
|
||||
|
|
@ -355,6 +355,15 @@ const SettingsView = memo(function SettingsView() {
|
|||
},
|
||||
});
|
||||
}
|
||||
items.push({
|
||||
id: "credits",
|
||||
label: "Credits",
|
||||
type: "button",
|
||||
onClick: () => {
|
||||
playPressSound();
|
||||
setActiveView("credits");
|
||||
},
|
||||
});
|
||||
} else if (currentSubMenu === "audio") {
|
||||
items.push({
|
||||
id: "music",
|
||||
|
|
@ -708,7 +717,7 @@ const SettingsView = memo(function SettingsView() {
|
|||
transition={{ duration: animationsEnabled ? 0.3 : 0 }}
|
||||
className="flex flex-col items-center w-full max-w-5xl outline-none"
|
||||
>
|
||||
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[40%] max-w-[200px] text-center tracking-widest uppercase opacity-80 font-bold whitespace-nowrap px-4">
|
||||
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[40%] max-w-[200px] text-center tracking-widest uppercase opacity-80 whitespace-nowrap px-4">
|
||||
{currentSubMenu === "main"
|
||||
? "Settings"
|
||||
: currentSubMenu === "audio"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,23 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
-webkit-font-smoothing: none;
|
||||
-moz-osx-font-smoothing: auto;
|
||||
}
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-weight: normal;
|
||||
}
|
||||
body {
|
||||
font-synthesis: none;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Mojangles";
|
||||
src: url("/fonts/Mojangles.ttf") format("truetype");
|
||||
|
|
@ -7,6 +25,20 @@
|
|||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Mojang7";
|
||||
src: url("/fonts/Mojang Font_7.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Mojang11";
|
||||
src: url("/fonts/Mojang Font_11.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.gamepad-connected-indicator {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
|
|
@ -122,7 +154,6 @@ body {
|
|||
margin: 0;
|
||||
background-color: #000;
|
||||
font-family: "Mojangles", monospace;
|
||||
-webkit-font-smoothing: none;
|
||||
}
|
||||
|
||||
.mc-button {
|
||||
|
|
@ -188,12 +219,19 @@ body {
|
|||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-image: url("/images/Slider_Handle_top.png"),
|
||||
background-image:
|
||||
url("/images/Slider_Handle_top.png"),
|
||||
url("/images/Slider_Handle_mid.png"),
|
||||
url("/images/Slider_Handle_bottom.png");
|
||||
background-repeat: no-repeat, no-repeat, no-repeat;
|
||||
background-position: 50% 4px, 50% 8px, 50% 100%;
|
||||
background-size: 16px 4px, 16px calc(100% - 14px), 16px 6px;
|
||||
background-position:
|
||||
50% 4px,
|
||||
50% 8px,
|
||||
50% 100%;
|
||||
background-size:
|
||||
16px 4px,
|
||||
16px calc(100% - 14px),
|
||||
16px 6px;
|
||||
image-rendering: pixelated;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
|
@ -209,12 +247,19 @@ body {
|
|||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-image: url("/images/Slider_Handle_top.png"),
|
||||
background-image:
|
||||
url("/images/Slider_Handle_top.png"),
|
||||
url("/images/Slider_Handle_mid.png"),
|
||||
url("/images/Slider_Handle_bottom.png");
|
||||
background-repeat: no-repeat, no-repeat, no-repeat;
|
||||
background-position: 50% 4px, 50% 8px, 50% 100%;
|
||||
background-size: 16px 4px, 16px calc(100% - 14px), 16px 6px;
|
||||
background-position:
|
||||
50% 4px,
|
||||
50% 8px,
|
||||
50% 100%;
|
||||
background-size:
|
||||
16px 4px,
|
||||
16px calc(100% - 14px),
|
||||
16px 6px;
|
||||
image-rendering: pixelated;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,22 @@ export function useDiscordRPC({
|
|||
details = tabNames[activeView] || "In Menus";
|
||||
}
|
||||
|
||||
await RpcService.updateActivity(details, state, isGameRunning, username);
|
||||
const skinUrl = (() => {
|
||||
try {
|
||||
return (
|
||||
JSON.parse(localStorage.getItem("lce-skin") || "null") || undefined
|
||||
);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
await RpcService.updateActivity(
|
||||
details,
|
||||
state,
|
||||
isGameRunning,
|
||||
username,
|
||||
skinUrl,
|
||||
);
|
||||
};
|
||||
|
||||
updateRPC();
|
||||
|
|
|
|||
|
|
@ -742,10 +742,6 @@ export default function App() {
|
|||
<div className="flex-1 text-left whitespace-nowrap">
|
||||
Version: {pkg.version} ({__BUILD_DATE__})
|
||||
</div>
|
||||
<div className="flex-[2] text-center whitespace-nowrap">
|
||||
Not affiliated with Mojang AB or Microsoft. "Minecraft" is a
|
||||
trademark of Mojang Synergies AB.
|
||||
</div>
|
||||
<div className="flex-1 text-right whitespace-nowrap">
|
||||
{connected && "CONTROLLER CONNECTED"}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { setActivity, start, clearActivity, stop } from "tauri-plugin-drpc";
|
||||
import {
|
||||
Activity,
|
||||
|
|
@ -11,6 +12,95 @@ class RPC {
|
|||
private startTime: number = Date.now();
|
||||
private initializationPromise: Promise<void> | null = null;
|
||||
private initialized: boolean = false;
|
||||
private headUrlCache: Map<string, string> = new Map();
|
||||
private headUploadPending: Map<string, Promise<string | null>> = new Map();
|
||||
private async renderHeadBlob(skinUrl: string): Promise<Blob | null> {
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(img, 8, 8, 8, 8, 0, 0, 64, 64);
|
||||
if (img.height !== 32) {
|
||||
ctx.drawImage(img, 40, 8, 8, 8, 0, 0, 64, 64);
|
||||
}
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(blob);
|
||||
}, "image/png");
|
||||
};
|
||||
img.onerror = () => {
|
||||
resolve(null);
|
||||
};
|
||||
img.src = skinUrl;
|
||||
});
|
||||
}
|
||||
|
||||
private async uploadHead(skinUrl: string): Promise<string | null> {
|
||||
const cached = this.headUrlCache.get(skinUrl);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const pending = this.headUploadPending.get(skinUrl);
|
||||
if (pending) {
|
||||
return pending;
|
||||
}
|
||||
const uploadPromise = (async (): Promise<string | null> => {
|
||||
const blob = await this.renderHeadBlob(skinUrl);
|
||||
if (!blob) {
|
||||
return null;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append("file", blob, "head.png");
|
||||
formData.append("expire", "21600");
|
||||
try {
|
||||
const res = await fetch("https://tmpfiles.org/api/v1/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const json = await res.json();
|
||||
if (json.status === "success" && json.data?.url) {
|
||||
const pageUrl = json.data.url;
|
||||
const pageRes = await invoke<{ status: number; body: string }>(
|
||||
"http_proxy_request",
|
||||
{
|
||||
method: "GET",
|
||||
url: pageUrl,
|
||||
body: null,
|
||||
headers: {},
|
||||
},
|
||||
);
|
||||
const html = pageRes.body;
|
||||
const match = html.match(/id="img_preview"\s+src="([^"]+)"/);
|
||||
if (match) {
|
||||
const directUrl = match[1];
|
||||
this.headUrlCache.set(skinUrl, directUrl);
|
||||
return directUrl;
|
||||
}
|
||||
console.error(
|
||||
"[RPC] uploadHead: could not extract direct image url from page",
|
||||
);
|
||||
}
|
||||
console.error("[RPC] uploadHead: upload failed or no url in response");
|
||||
} catch (e) {
|
||||
console.error("[RPC] uploadHead: fetch error:", e);
|
||||
}
|
||||
return null;
|
||||
})();
|
||||
this.headUploadPending.set(skinUrl, uploadPromise);
|
||||
try {
|
||||
return await uploadPromise;
|
||||
} finally {
|
||||
this.headUploadPending.delete(skinUrl);
|
||||
}
|
||||
}
|
||||
public async StartRPC() {
|
||||
if (this.initialized) return;
|
||||
if (sessionStorage.getItem("lce_rpc_started") === "true") {
|
||||
|
|
@ -38,6 +128,7 @@ class RPC {
|
|||
state: string,
|
||||
isPlaying: boolean = false,
|
||||
username: string,
|
||||
skinUrl?: string,
|
||||
) {
|
||||
if (!this.initialized) {
|
||||
await this.StartRPC();
|
||||
|
|
@ -54,8 +145,15 @@ class RPC {
|
|||
const assets = new Assets();
|
||||
assets.setLargeImage("logo");
|
||||
assets.setLargeText("LCE Emerald Launcher");
|
||||
assets.setSmallImage("app-icon");
|
||||
assets.setSmallText(isPlaying ? "Playing" : "In Menus");
|
||||
const headUrl = skinUrl ? await this.uploadHead(skinUrl) : null;
|
||||
console.log("[RPC] updateActivity: headUrl:", headUrl);
|
||||
if (headUrl) {
|
||||
assets.setSmallImage(headUrl);
|
||||
assets.setSmallText(username);
|
||||
} else {
|
||||
assets.setSmallImage("app-icon");
|
||||
assets.setSmallText(isPlaying ? "Playing" : "In Menus");
|
||||
}
|
||||
activity.setAssets(assets);
|
||||
activity.setTimestamps(new Timestamps(this.startTime));
|
||||
activity.setButton([
|
||||
|
|
@ -74,16 +172,16 @@ class RPC {
|
|||
}
|
||||
|
||||
public async StopRPC() {
|
||||
try {
|
||||
await clearActivity();
|
||||
await stop();
|
||||
} catch (e) {
|
||||
// no need to handle errors here as the launcher will close anyways!
|
||||
} finally {
|
||||
this.initialized = false;
|
||||
this.initializationPromise = null;
|
||||
sessionStorage.removeItem("lce_rpc_started");
|
||||
}
|
||||
try {
|
||||
await clearActivity();
|
||||
await stop();
|
||||
} catch (e) {
|
||||
// no need to handle errors here as the launcher will close anyways!
|
||||
} finally {
|
||||
this.initialized = false;
|
||||
this.initializationPromise = null;
|
||||
sessionStorage.removeItem("lce_rpc_started");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue