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