diff --git a/public/fonts/Mojang Font_11.ttf b/public/fonts/Mojang Font_11.ttf new file mode 100644 index 0000000..969cee5 Binary files /dev/null and b/public/fonts/Mojang Font_11.ttf differ diff --git a/public/fonts/Mojang Font_7.ttf b/public/fonts/Mojang Font_7.ttf new file mode 100644 index 0000000..727c5fb Binary files /dev/null and b/public/fonts/Mojang Font_7.ttf differ diff --git a/src/components/views/HomeView.tsx b/src/components/views/HomeView.tsx index e681c76..5139024 100644 --- a/src/components/views/HomeView.tsx +++ b/src/components/views/HomeView.tsx @@ -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 && (
+
-
-
)} diff --git a/src/components/views/SettingsView.tsx b/src/components/views/SettingsView.tsx index 842f84b..4935765 100644 --- a/src/components/views/SettingsView.tsx +++ b/src/components/views/SettingsView.tsx @@ -151,7 +151,7 @@ const SettingsView = memo(function SettingsView() { "fixed inset-0 bg-black/80 flex items-center justify-center z-50"; dialog.innerHTML = `
-

Reset Setup

+

Reset Setup

Are you sure you want to reset launcher setup?

@@ -189,7 +189,7 @@ const SettingsView = memo(function SettingsView() { "fixed inset-0 bg-black/80 flex items-center justify-center z-50"; dialog.innerHTML = `
-

CONFIRM RESET

+

CONFIRM RESET

⚠️ This will:

    @@ -198,7 +198,7 @@ const SettingsView = memo(function SettingsView() {
  • Show setup screen again
  • Require reconfiguration
-

This action cannot be undone!

+

This action cannot be undone!

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

+

{currentSubMenu === "main" ? "Settings" : currentSubMenu === "audio" diff --git a/src/css/index.css b/src/css/index.css index 33554c9..bb82e3d 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -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; } diff --git a/src/hooks/useDiscordRPC.ts b/src/hooks/useDiscordRPC.ts index 5f66430..ccd88b8 100644 --- a/src/hooks/useDiscordRPC.ts +++ b/src/hooks/useDiscordRPC.ts @@ -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(); diff --git a/src/pages/App.tsx b/src/pages/App.tsx index 5a3a327..714747d 100644 --- a/src/pages/App.tsx +++ b/src/pages/App.tsx @@ -742,10 +742,6 @@ export default function App() {
Version: {pkg.version} ({__BUILD_DATE__})
-
- Not affiliated with Mojang AB or Microsoft. "Minecraft" is a - trademark of Mojang Synergies AB. -
{connected && "CONTROLLER CONNECTED"}
diff --git a/src/services/RpcService.ts b/src/services/RpcService.ts index 53839d4..98c3504 100644 --- a/src/services/RpcService.ts +++ b/src/services/RpcService.ts @@ -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 | null = null; private initialized: boolean = false; + private headUrlCache: Map = new Map(); + private headUploadPending: Map> = new Map(); + private async renderHeadBlob(skinUrl: string): Promise { + 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 { + const cached = this.headUrlCache.get(skinUrl); + if (cached) { + return cached; + } + const pending = this.headUploadPending.get(skinUrl); + if (pending) { + return pending; + } + const uploadPromise = (async (): Promise => { + 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"); + } } }