feat: add playerhead to rpc

This commit is contained in:
neoapps-dev 2026-08-16 22:57:42 +03:00
parent eefd64771b
commit abf62839e1
8 changed files with 271 additions and 113 deletions

Binary file not shown.

Binary file not shown.

View file

@ -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>

View file

@ -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"

View file

@ -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;
}

View file

@ -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();

View file

@ -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>

View file

@ -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");
}
}
}