feat: support for translations!

This commit is contained in:
neoapps-dev
2026-08-31 22:24:05 +03:00
parent 7c566e3ff0
commit d594d50ff6
39 changed files with 1900 additions and 801 deletions
+3
View File
@@ -21,10 +21,13 @@
"@tauri-apps/plugin-process": "~2.3.1", "@tauri-apps/plugin-process": "~2.3.1",
"@tauri-apps/plugin-updater": "~2.10.1", "@tauri-apps/plugin-updater": "~2.10.1",
"framer-motion": "^12.36.0", "framer-motion": "^12.36.0",
"i18next": "^26.4.0",
"i18next-browser-languagedetector": "^8.2.1",
"pako": "^2.1.0", "pako": "^2.1.0",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-i18next": "^17.0.12",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0", "rehype-sanitize": "^6.0.0",
+75
View File
@@ -26,6 +26,12 @@ importers:
framer-motion: framer-motion:
specifier: ^12.36.0 specifier: ^12.36.0
version: 12.38.0([email protected]([email protected]))([email protected]) version: 12.38.0([email protected]([email protected]))([email protected])
i18next:
specifier: ^26.4.0
version: 26.4.0([email protected])
i18next-browser-languagedetector:
specifier: ^8.2.1
version: 8.2.1
pako: pako:
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.0 version: 2.1.0
@@ -38,6 +44,9 @@ importers:
react-dom: react-dom:
specifier: ^19.1.0 specifier: ^19.1.0
version: 19.2.4([email protected]) version: 19.2.4([email protected])
react-i18next:
specifier: ^17.0.12
version: 17.0.12([email protected]([email protected]))([email protected]([email protected]))([email protected])([email protected])
react-markdown: react-markdown:
specifier: ^10.1.0 specifier: ^10.1.0
version: 10.1.0(@types/[email protected])([email protected]) version: 10.1.0(@types/[email protected])([email protected])
@@ -180,6 +189,10 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0-0 '@babel/core': ^7.0.0-0
'@babel/[email protected]':
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
engines: {node: '>=6.9.0'}
'@babel/[email protected]': '@babel/[email protected]':
resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -962,12 +975,26 @@ packages:
[email protected]: [email protected]:
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
[email protected]:
resolution: {integrity: sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==}
[email protected]: [email protected]:
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
[email protected]: [email protected]:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
[email protected]:
resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==}
[email protected]:
resolution: {integrity: sha512-rsmK5bFqsD1AetSFSIa43wtNR4WpvvH4p0tLEsTxkC7QTrfdFm06nbQ95bh8Og4wwaCnUEcm9DVYL2cgxitiQg==}
peerDependencies:
typescript: ^5 || ^6 || ^7
peerDependenciesMeta:
typescript:
optional: true
[email protected]: [email protected]:
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
@@ -1299,6 +1326,22 @@ packages:
peerDependencies: peerDependencies:
react: ^19.2.4 react: ^19.2.4
[email protected]:
resolution: {integrity: sha512-lFWPEGkxQ6RhusdUkysFBD58VHfSSzvHBzqMgN0SvfVpdQGfwtNkStTqdy08/sJd7s807qqutgx93fRpD0DJ3Q==}
peerDependencies:
i18next: '>= 26.2.0'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
typescript: ^5 || ^6 || ^7
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
typescript:
optional: true
[email protected]: [email protected]:
resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
peerDependencies: peerDependencies:
@@ -1440,6 +1483,11 @@ packages:
peerDependencies: peerDependencies:
browserslist: '>= 4.21.0' browserslist: '>= 4.21.0'
[email protected]:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
[email protected]: [email protected]:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
@@ -1609,6 +1657,8 @@ snapshots:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
'@babel/helper-plugin-utils': 7.28.6 '@babel/helper-plugin-utils': 7.28.6
'@babel/[email protected]': {}
'@babel/[email protected]': '@babel/[email protected]':
dependencies: dependencies:
'@babel/code-frame': 7.29.0 '@babel/code-frame': 7.29.0
@@ -2285,10 +2335,20 @@ snapshots:
property-information: 7.1.0 property-information: 7.1.0
space-separated-tokens: 2.0.2 space-separated-tokens: 2.0.2
[email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
'@babel/runtime': 7.29.7
[email protected]([email protected]):
optionalDependencies:
typescript: 5.8.3
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -2792,6 +2852,17 @@ snapshots:
react: 19.2.4 react: 19.2.4
scheduler: 0.27.0 scheduler: 0.27.0
[email protected]([email protected]([email protected]))([email protected]([email protected]))([email protected])([email protected]):
dependencies:
'@babel/runtime': 7.29.7
html-parse-stringify: 4.0.1
i18next: 26.4.0([email protected])
react: 19.2.4
use-sync-external-store: 1.6.0([email protected])
optionalDependencies:
react-dom: 19.2.4([email protected])
typescript: 5.8.3
[email protected](@types/[email protected])([email protected]): [email protected](@types/[email protected])([email protected]):
dependencies: dependencies:
'@types/hast': 3.0.4 '@types/hast': 3.0.4
@@ -2996,6 +3067,10 @@ snapshots:
escalade: 3.2.0 escalade: 3.2.0
picocolors: 1.1.1 picocolors: 1.1.1
[email protected]([email protected]):
dependencies:
react: 19.2.4
[email protected]: [email protected]:
dependencies: dependencies:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
+5 -2
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { useAudio } from "../../context/LauncherContext"; import { useAudio } from "../../context/LauncherContext";
@@ -14,10 +15,12 @@ export function AchievementToast({
message, message,
onClose, onClose,
onClick, onClick,
title = "Error!", title,
variant = "error", variant = "error",
}: AchievementToastProps) { }: AchievementToastProps) {
const { t } = useTranslation();
const { playSfx } = useAudio(); const { playSfx } = useAudio();
const displayTitle = title ?? t("common.error");
const prevMessage = useRef(message); const prevMessage = useRef(message);
useEffect(() => { useEffect(() => {
const wasNull = !prevMessage.current; const wasNull = !prevMessage.current;
@@ -125,7 +128,7 @@ export function AchievementToast({
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-[#FFFF55] text-lg font-bold mc-text-shadow leading-tight"> <span className="text-[#FFFF55] text-lg font-bold mc-text-shadow leading-tight">
{title} {displayTitle}
</span> </span>
<span className="text-white text-base mc-text-shadow leading-tight break-words"> <span className="text-white text-base mc-text-shadow leading-tight break-words">
{message} {message}
+3 -1
View File
@@ -1,6 +1,7 @@
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { memo } from "react"; import { memo } from "react";
import { useTranslation } from "react-i18next";
const appWindow = getCurrentWindow(); const appWindow = getCurrentWindow();
@@ -13,6 +14,7 @@ export const AppHeader = memo(function AppHeader({
playPressSound, playPressSound,
uiFade, uiFade,
}: AppHeaderProps) { }: AppHeaderProps) {
const { t } = useTranslation();
return ( return (
<motion.div <motion.div
key="header" key="header"
@@ -30,7 +32,7 @@ export const AppHeader = memo(function AppHeader({
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
<span className="text-xs text-gray-30 mc-text-shadow block pt-[1px] pointer-events-none"> <span className="text-xs text-gray-30 mc-text-shadow block pt-[1px] pointer-events-none">
Emerald Legacy Launcher {t("header.title")}
</span> </span>
</div> </div>
<div className="flex items-center gap-1 pr-2"> <div className="flex items-center gap-1 pr-2">
+4 -2
View File
@@ -1,5 +1,6 @@
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { memo } from "react"; import { memo } from "react";
import { useTranslation } from "react-i18next";
import { ScreenshotImage } from "../common/ScreenshotImage"; import { ScreenshotImage } from "../common/ScreenshotImage";
import type { Edition } from "../../types/edition"; import type { Edition } from "../../types/edition";
@@ -10,6 +11,7 @@ interface DownloadOverlayProps {
} }
export const DownloadOverlay = memo(function DownloadOverlay({ downloadProgress, downloadingIds, editions }: DownloadOverlayProps) { export const DownloadOverlay = memo(function DownloadOverlay({ downloadProgress, downloadingIds, editions }: DownloadOverlayProps) {
const { t } = useTranslation();
if (downloadingIds.length === 0) return null; if (downloadingIds.length === 0) return null;
return ( return (
@@ -23,14 +25,14 @@ export const DownloadOverlay = memo(function DownloadOverlay({ downloadProgress,
> >
<div className="px-3 pt-2.5 pb-2 border-b border-white/10"> <div className="px-3 pt-2.5 pb-2 border-b border-white/10">
<span className="text-[13px] text-[#FFFF55] mc-text-shadow uppercase tracking-widest"> <span className="text-[13px] text-[#FFFF55] mc-text-shadow uppercase tracking-widest">
Downloads {t("download.title")}
</span> </span>
</div> </div>
<div className="flex flex-col gap-1.5 px-3 py-2 max-h-[260px] overflow-y-auto custom-scrollbar"> <div className="flex flex-col gap-1.5 px-3 py-2 max-h-[260px] overflow-y-auto custom-scrollbar">
{downloadingIds.map((id) => { {downloadingIds.map((id) => {
const pct = downloadProgress[id] ?? 0; const pct = downloadProgress[id] ?? 0;
const edition = editions.find((e) => e.instanceId === id || e.id === id); const edition = editions.find((e) => e.instanceId === id || e.id === id);
const name = edition?.name || "Game Files"; const name = edition?.name || t("download.gameFiles");
return ( return (
<div key={id} className="flex items-center gap-2.5"> <div key={id} className="flex items-center gap-2.5">
{edition?.logo ? ( {edition?.logo ? (
+13 -9
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { TauriService } from "../../services/TauriService"; import { TauriService } from "../../services/TauriService";
import { lceOnlineService } from "../../services/LceOnlineService"; import { lceOnlineService } from "../../services/LceOnlineService";
@@ -30,6 +31,7 @@ export default function ChooseInstanceModal({
installs: string[]; installs: string[];
invite: GameInvite | null; invite: GameInvite | null;
}) { }) {
const { t } = useTranslation();
const [selectedInstance, setSelectedInstance] = useState<string>(""); const [selectedInstance, setSelectedInstance] = useState<string>("");
const [status, setStatus] = useState<string>(""); const [status, setStatus] = useState<string>("");
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
@@ -60,17 +62,17 @@ export default function ChooseInstanceModal({
playPressSound(); playPressSound();
setIsJoining(true); setIsJoining(true);
setError(""); setError("");
setStatus("Launching game..."); setStatus(t("modals.chooseInstance.launching"));
try { try {
const sessionId = invite.sessionId || ""; const sessionId = invite.sessionId || "";
if (sessionId) { if (sessionId) {
setStatus("Connecting via relay..."); setStatus(t("modals.chooseInstance.connectingRelay"));
const accessToken = lceOnlineService.accessToken ?? ""; const accessToken = lceOnlineService.accessToken ?? "";
const relayPromise = TauriService.startRelayProxy( const relayPromise = TauriService.startRelayProxy(
accessToken, accessToken,
sessionId, sessionId,
); );
setStatus("Launching game..."); setStatus(t("modals.chooseInstance.launching"));
await TauriService.launchGame( await TauriService.launchGame(
selectedInstance, selectedInstance,
[ [
@@ -163,10 +165,12 @@ export default function ChooseInstanceModal({
{!isJoining ? ( {!isJoining ? (
<> <>
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-2 border-b-2 border-[#373737] pb-2 w-full text-center uppercase"> <h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-2 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
Join Game {t("modals.chooseInstance.title")}
</h2> </h2>
<p className="text-white text-sm mc-text-shadow mb-4 text-center"> <p className="text-white text-sm mc-text-shadow mb-4 text-center">
Joining {hostName}'s game. Choose an instance: {t("modals.chooseInstance.joinPrompt", {
name: hostName,
})}
</p> </p>
{validInstances.length > 0 ? ( {validInstances.length > 0 ? (
@@ -213,7 +217,7 @@ export default function ChooseInstanceModal({
</div> </div>
) : ( ) : (
<p className="text-red-400 text-sm mc-text-shadow mb-4 text-center"> <p className="text-red-400 text-sm mc-text-shadow mb-4 text-center">
No installed instances available. Install a version first. {t("modals.chooseInstance.noInstances")}
</p> </p>
)} )}
@@ -250,7 +254,7 @@ export default function ChooseInstanceModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Cancel {t("modals.chooseInstance.cancel")}
</button> </button>
{validInstances.length > 0 && ( {validInstances.length > 0 && (
<button <button
@@ -266,7 +270,7 @@ export default function ChooseInstanceModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Join {t("modals.chooseInstance.join")}
</button> </button>
)} )}
</div> </div>
@@ -274,7 +278,7 @@ export default function ChooseInstanceModal({
) : ( ) : (
<> <>
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase"> <h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
Joining Game {t("modals.chooseInstance.joiningTitle")}
</h2> </h2>
<div className="flex flex-col items-center gap-4 py-8"> <div className="flex flex-col items-center gap-4 py-8">
<div className="w-12 h-12 border-4 border-[#FFFF55] border-t-transparent rounded-full animate-spin" /> <div className="w-12 h-12 border-4 border-[#FFFF55] border-t-transparent rounded-full animate-spin" />
+16 -14
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, memo } from "react"; import { useState, useEffect, memo } from "react";
import { useTranslation } from "react-i18next";
interface ModalButtonProps { interface ModalButtonProps {
label: string; label: string;
@@ -50,6 +51,7 @@ export default function CustomTUModal({
editingEdition?: { name: string; desc: string; url: string; path?: string } | null; editingEdition?: { name: string; desc: string; url: string; path?: string } | null;
initialPath?: string; initialPath?: string;
}) { }) {
const { t } = useTranslation();
const [name, setName] = useState(""); const [name, setName] = useState("");
const [desc, setDesc] = useState(""); const [desc, setDesc] = useState("");
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
@@ -107,19 +109,19 @@ export default function CustomTUModal({
const handleImport = () => { const handleImport = () => {
if (!name) { if (!name) {
setError("Name is required"); setError(t("modals.customTu.nameRequired"));
return; return;
} }
if (!url && !path) { if (!url && !path) {
setError("URL or Path is required"); setError(t("modals.customTu.urlOrPathRequired"));
return; return;
} }
if (url && !url.startsWith("http")) { if (url && !url.startsWith("http")) {
setError("Invalid URL"); setError(t("modals.customTu.invalidUrl"));
return; return;
} }
setError(""); setError("");
onImport({ name, desc: desc || "Custom imported TU", url, path: path || undefined }); onImport({ name, desc: desc || t("modals.customTu.defaultDesc"), url, path: path || undefined });
onClose(); onClose();
setName(""); setName("");
setDesc(""); setDesc("");
@@ -133,13 +135,13 @@ export default function CustomTUModal({
className="relative w-[400px] p-6 flex flex-col items-center mc-options-bg" className="relative w-[400px] p-6 flex flex-col items-center mc-options-bg"
> >
<h2 className="text-xl text-black mc-text-shadow mb-4 text-center"> <h2 className="text-xl text-black mc-text-shadow mb-4 text-center">
{editingEdition ? "Edit Custom TU" : "Import Custom TU"} {editingEdition ? t("modals.customTu.editTitle") : t("modals.customTu.importTitle")}
</h2> </h2>
<div className="flex flex-col gap-4 w-full"> <div className="flex flex-col gap-4 w-full">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-gray text-sm mc-text-shadow uppercase tracking-widest"> <label className="text-gray text-sm mc-text-shadow uppercase tracking-widest">
TU Name {t("modals.customTu.tuName")}
</label> </label>
<input <input
type="text" type="text"
@@ -147,7 +149,7 @@ export default function CustomTUModal({
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
onFocus={() => setFocusIndex(0)} onFocus={() => setFocusIndex(0)}
placeholder="e.g. My Awesome Mod" placeholder={t("modals.customTu.namePlaceholder")}
className="w-full h-10 px-3 bg-black/40 border-2 border-[#373737] text-white text-base outline-none font-['Mojangles']" className="w-full h-10 px-3 bg-black/40 border-2 border-[#373737] text-white text-base outline-none font-['Mojangles']"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
@@ -155,14 +157,14 @@ export default function CustomTUModal({
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-gray text-sm mc-text-shadow uppercase tracking-widest"> <label className="text-gray text-sm mc-text-shadow uppercase tracking-widest">
Description (Optional) {t("modals.customTu.description")}
</label> </label>
<input <input
type="text" type="text"
value={desc} value={desc}
onChange={(e) => setDesc(e.target.value)} onChange={(e) => setDesc(e.target.value)}
onFocus={() => setFocusIndex(1)} onFocus={() => setFocusIndex(1)}
placeholder="A brief description..." placeholder={t("modals.customTu.descPlaceholder")}
className="w-full h-10 px-3 bg-black/40 border-2 border-[#373737] text-white text-base outline-none font-['Mojangles']" className="w-full h-10 px-3 bg-black/40 border-2 border-[#373737] text-white text-base outline-none font-['Mojangles']"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
@@ -170,14 +172,14 @@ export default function CustomTUModal({
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-gray text-sm mc-text-shadow uppercase tracking-widest"> <label className="text-gray text-sm mc-text-shadow uppercase tracking-widest">
Download URL (.zip) {t("modals.customTu.downloadUrl")}
</label> </label>
<input <input
type="text" type="text"
value={url} value={url}
onChange={(e) => setUrl(e.target.value)} onChange={(e) => setUrl(e.target.value)}
onFocus={() => setFocusIndex(2)} onFocus={() => setFocusIndex(2)}
placeholder="optional if path is set" placeholder={t("modals.customTu.urlPlaceholder")}
className="w-full h-10 px-3 bg-black/40 border-2 border-[#373737] text-white text-base outline-none font-['Mojangles']" className="w-full h-10 px-3 bg-black/40 border-2 border-[#373737] text-white text-base outline-none font-['Mojangles']"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
@@ -186,7 +188,7 @@ export default function CustomTUModal({
{path && ( {path && (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<label className="text-gray text-sm mc-text-shadow uppercase tracking-widest"> <label className="text-gray text-sm mc-text-shadow uppercase tracking-widest">
Local Path {t("modals.customTu.localPath")}
</label> </label>
<input <input
type="text" type="text"
@@ -207,14 +209,14 @@ export default function CustomTUModal({
<div className="flex gap-4 mt-6 w-full"> <div className="flex gap-4 mt-6 w-full">
<ModalButton <ModalButton
label="Cancel" label={t("modals.customTu.cancel")}
onClick={() => { onClick={() => {
playBackSound("close_click.wav"); playBackSound("close_click.wav");
onClose(); onClose();
}} }}
/> />
<ModalButton <ModalButton
label={editingEdition ? "Save" : "Import"} label={editingEdition ? t("modals.customTu.save") : t("modals.customTu.import")}
onClick={() => { onClick={() => {
playPressSound("save_click.wav"); playPressSound("save_click.wav");
handleImport(); handleImport();
+10 -8
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { TauriService } from "../../services/TauriService"; import { TauriService } from "../../services/TauriService";
@@ -37,6 +38,7 @@ export default function CustomizeModal({
currentPanorama?: string; currentPanorama?: string;
onSave: (updates: { titleImage?: string; panorama?: string }) => void; onSave: (updates: { titleImage?: string; panorama?: string }) => void;
}) { }) {
const { t } = useTranslation();
const [titleImage, setTitleImage] = useState(currentTitleImage || ""); const [titleImage, setTitleImage] = useState(currentTitleImage || "");
const [panorama, setPanorama] = useState(currentPanorama || ""); const [panorama, setPanorama] = useState(currentPanorama || "");
const [focusIndex, setFocusIndex] = useState(0); const [focusIndex, setFocusIndex] = useState(0);
@@ -90,7 +92,7 @@ export default function CustomizeModal({
const handlePickFile = async (field: "titleImage" | "panorama") => { const handlePickFile = async (field: "titleImage" | "panorama") => {
try { try {
const path = await TauriService.pickFile( const path = await TauriService.pickFile(
field === "titleImage" ? "Select Title Image" : "Select Panorama Background", field === "titleImage" ? t("modals.customize.selectTitleImage") : t("modals.customize.selectPanorama"),
["png", "jpg", "jpeg", "bmp"], ["png", "jpg", "jpeg", "bmp"],
); );
if (path) { if (path) {
@@ -119,7 +121,7 @@ export default function CustomizeModal({
) => ( ) => (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-[#AAAAAA] text-xs mc-text-shadow uppercase tracking-widest"> <label className="text-[#AAAAAA] text-xs mc-text-shadow uppercase tracking-widest">
{field === "titleImage" ? "Title Image" : "Panorama Background"} {field === "titleImage" ? t("modals.customize.titleImage") : t("modals.customize.panoramaBackground")}
</label> </label>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
@@ -135,7 +137,7 @@ export default function CustomizeModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
{value ? "Change Image" : "Pick Image"} {value ? t("modals.customize.changeImage") : t("modals.customize.pickImage")}
</button> </button>
{value && ( {value && (
<button <button
@@ -151,7 +153,7 @@ export default function CustomizeModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Reset {t("modals.customize.reset")}
</button> </button>
)} )}
</div> </div>
@@ -159,7 +161,7 @@ export default function CustomizeModal({
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
<img <img
src={value} src={value}
alt="Preview" alt={t("modals.customize.preview")}
className="w-10 h-6 object-contain border border-[#555]" className="w-10 h-6 object-contain border border-[#555]"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
onError={(e) => { onError={(e) => {
@@ -190,7 +192,7 @@ export default function CustomizeModal({
}} }}
> >
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase"> <h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
Customize {t("modals.customize.title")}
</h2> </h2>
<p className="text-white/70 text-xs mc-text-shadow mb-4 -mt-2 truncate max-w-full"> <p className="text-white/70 text-xs mc-text-shadow mb-4 -mt-2 truncate max-w-full">
{editionName} {editionName}
@@ -220,7 +222,7 @@ export default function CustomizeModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Cancel {t("modals.customize.cancel")}
</button> </button>
<button <button
onMouseEnter={() => setFocusIndex(5)} onMouseEnter={() => setFocusIndex(5)}
@@ -237,7 +239,7 @@ export default function CustomizeModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Save {t("modals.customize.save")}
</button> </button>
</div> </div>
</div> </div>
+20 -11
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useCallback } from "react"; import { useState, useEffect, useRef, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { TauriService } from "../../services/TauriService"; import { TauriService } from "../../services/TauriService";
interface DlcEntry { interface DlcEntry {
@@ -23,6 +24,7 @@ export default function DownloadDlcModal({
instanceId: string; instanceId: string;
officialDLC: string; officialDLC: string;
}) { }) {
const { t } = useTranslation();
const [dlcList, setDlcList] = useState<DlcEntry[]>([]); const [dlcList, setDlcList] = useState<DlcEntry[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -185,7 +187,11 @@ export default function DownloadDlcModal({
succeeded.push(selected[i].name); succeeded.push(selected[i].name);
} else { } else {
setError( setError(
`Failed to download ${selected[i].name}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`, t("modals.downloadDlc.failedToDownload", {
name: selected[i].name,
reason:
r.reason instanceof Error ? r.reason.message : String(r.reason),
}),
); );
} }
} }
@@ -211,7 +217,7 @@ export default function DownloadDlcModal({
}} }}
> >
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-1 w-full text-center uppercase"> <h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-1 w-full text-center uppercase">
Download DLC {t("modals.downloadDlc.title")}
</h2> </h2>
<p className="text-white text-sm mc-text-shadow mb-4 text-center"> <p className="text-white text-sm mc-text-shadow mb-4 text-center">
{editionName} {editionName}
@@ -219,7 +225,7 @@ export default function DownloadDlcModal({
{loading && ( {loading && (
<div className="text-white text-sm mc-text-shadow mb-4 py-8"> <div className="text-white text-sm mc-text-shadow mb-4 py-8">
Loading available DLCs... {t("modals.downloadDlc.loading")}
</div> </div>
)} )}
@@ -231,7 +237,7 @@ export default function DownloadDlcModal({
{!loading && !error && dlcList.length === 0 && !downloading && ( {!loading && !error && dlcList.length === 0 && !downloading && (
<div className="text-white text-sm mc-text-shadow mb-4 py-8"> <div className="text-white text-sm mc-text-shadow mb-4 py-8">
No DLC folders found in the repository. {t("modals.downloadDlc.noneFound")}
</div> </div>
)} )}
@@ -239,7 +245,7 @@ export default function DownloadDlcModal({
<> <>
<div className="flex items-center justify-between w-full mb-2 gap-2"> <div className="flex items-center justify-between w-full mb-2 gap-2">
<span className="text-white text-[10px] mc-text-shadow uppercase tracking-widest"> <span className="text-white text-[10px] mc-text-shadow uppercase tracking-widest">
{dlcList.length} available {t("modals.downloadDlc.available", { count: dlcList.length })}
</span> </span>
<div className="flex gap-1"> <div className="flex gap-1">
<button <button
@@ -247,14 +253,14 @@ export default function DownloadDlcModal({
disabled={downloading} disabled={downloading}
className="text-[9px] px-1.5 py-0.5 border border-[#555] text-white bg-black/20 hover:border-[#FFFF55] hover:text-[#FFFF55] mc-text-shadow uppercase tracking-wider transition-colors disabled:opacity-40" className="text-[9px] px-1.5 py-0.5 border border-[#555] text-white bg-black/20 hover:border-[#FFFF55] hover:text-[#FFFF55] mc-text-shadow uppercase tracking-wider transition-colors disabled:opacity-40"
> >
All {t("modals.downloadDlc.all")}
</button> </button>
<button <button
onClick={deselectAll} onClick={deselectAll}
disabled={downloading} disabled={downloading}
className="text-[9px] px-1.5 py-0.5 border border-[#555] text-white bg-black/20 hover:border-[#FFFF55] hover:text-[#FFFF55] mc-text-shadow uppercase tracking-wider transition-colors disabled:opacity-40" className="text-[9px] px-1.5 py-0.5 border border-[#555] text-white bg-black/20 hover:border-[#FFFF55] hover:text-[#FFFF55] mc-text-shadow uppercase tracking-wider transition-colors disabled:opacity-40"
> >
None {t("modals.downloadDlc.none")}
</button> </button>
</div> </div>
</div> </div>
@@ -327,7 +333,10 @@ export default function DownloadDlcModal({
<div className="flex items-center justify-center gap-4 mt-4"> <div className="flex items-center justify-center gap-4 mt-4">
{downloading && ( {downloading && (
<div className="text-[#FFFF55] text-sm mc-text-shadow"> <div className="text-[#FFFF55] text-sm mc-text-shadow">
Downloaded {downloaded.length}/{selectedCount} {t("modals.downloadDlc.downloaded", {
downloaded: downloaded.length,
selected: selectedCount,
})}
</div> </div>
)} )}
</div> </div>
@@ -343,7 +352,7 @@ export default function DownloadDlcModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Download ({selectedCount}) {t("modals.downloadDlc.download", { count: selectedCount })}
</button> </button>
)} )}
{downloading && downloaded.length === selectedCount && ( {downloading && downloaded.length === selectedCount && (
@@ -359,7 +368,7 @@ export default function DownloadDlcModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Done {t("modals.downloadDlc.done")}
</button> </button>
)} )}
<button <button
@@ -375,7 +384,7 @@ export default function DownloadDlcModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Close {t("modals.downloadDlc.close")}
</button> </button>
</div> </div>
</div> </div>
+15 -10
View File
@@ -1,4 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { TauriService } from "../../services/TauriService"; import { TauriService } from "../../services/TauriService";
const MAX_DISPLAY_LINES = 5000; const MAX_DISPLAY_LINES = 5000;
@@ -62,6 +63,7 @@ export default function GameLogModal({
onClose: () => void; onClose: () => void;
playBackSound: () => void; playBackSound: () => void;
}) { }) {
const { t } = useTranslation();
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const [feedback, setFeedback] = useState(""); const [feedback, setFeedback] = useState("");
const lines = useMemo(() => (log ?? "").split("\n"), [log]); const lines = useMemo(() => (log ?? "").split("\n"), [log]);
@@ -92,16 +94,16 @@ export default function GameLogModal({
if (!log) return; if (!log) return;
try { try {
const path = await TauriService.saveFileDialog( const path = await TauriService.saveFileDialog(
"Save Game Log", t("modals.gameLog.saveDialog"),
"game-log.txt", "game-log.txt",
["txt", "log"], ["txt", "log"],
); );
if (!path) return; if (!path) return;
await TauriService.writeBinaryFile(path, new TextEncoder().encode(log)); await TauriService.writeBinaryFile(path, new TextEncoder().encode(log));
setFeedback("Saved!"); setFeedback(t("modals.gameLog.saved"));
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setFeedback("Failed to save."); setFeedback(t("modals.gameLog.failedToSave"));
} }
}; };
@@ -109,10 +111,10 @@ export default function GameLogModal({
if (!log) return; if (!log) return;
try { try {
await navigator.clipboard.writeText(log); await navigator.clipboard.writeText(log);
setFeedback("Copied to clipboard!"); setFeedback(t("modals.gameLog.copied"));
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setFeedback("Failed to copy."); setFeedback(t("modals.gameLog.failedToCopy"));
} }
}; };
@@ -136,12 +138,15 @@ export default function GameLogModal({
className="flex flex-col w-[640px] max-w-[92vw] max-h-[85vh] p-5 font-['Mojangles'] mc-options-bg" className="flex flex-col w-[640px] max-w-[92vw] max-h-[85vh] p-5 font-['Mojangles'] mc-options-bg"
> >
<h3 className="text-2xl font-bold text-[#333333] mb-3 text-left w-full px-2 mc-text-shadow"> <h3 className="text-2xl font-bold text-[#333333] mb-3 text-left w-full px-2 mc-text-shadow">
Game Crash Log {t("modals.gameLog.title")}
</h3> </h3>
{truncated && ( {truncated && (
<p className="text-[#666666] text-sm mb-3 text-left w-full px-2"> <p className="text-[#666666] text-sm mb-3 text-left w-full px-2">
Showing last {MAX_DISPLAY_LINES} lines of {lines.length}. {t("modals.gameLog.showingLast", {
shown: MAX_DISPLAY_LINES,
total: lines.length,
})}
</p> </p>
)} )}
@@ -161,10 +166,10 @@ export default function GameLogModal({
<div className="flex gap-3 w-full px-2 mb-2"> <div className="flex gap-3 w-full px-2 mb-2">
<LogActionButton onClick={handleSave} className="flex-1"> <LogActionButton onClick={handleSave} className="flex-1">
Save As File {t("modals.gameLog.saveAsFile")}
</LogActionButton> </LogActionButton>
<LogActionButton onClick={handleCopy} className="flex-1"> <LogActionButton onClick={handleCopy} className="flex-1">
Copy {t("modals.gameLog.copy")}
</LogActionButton> </LogActionButton>
<LogActionButton <LogActionButton
onClick={() => { onClick={() => {
@@ -173,7 +178,7 @@ export default function GameLogModal({
}} }}
className="flex-1" className="flex-1"
> >
Close {t("modals.gameLog.close")}
</LogActionButton> </LogActionButton>
</div> </div>
{feedback && ( {feedback && (
+33 -21
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { TauriService } from "../../services/TauriService"; import { TauriService } from "../../services/TauriService";
export default function ImportWorldModal({ export default function ImportWorldModal({
@@ -16,6 +17,7 @@ export default function ImportWorldModal({
targetInstanceId: string; targetInstanceId: string;
targetInstanceName: string; targetInstanceName: string;
}) { }) {
const { t } = useTranslation();
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [isImporting, setIsImporting] = useState(false); const [isImporting, setIsImporting] = useState(false);
@@ -32,9 +34,9 @@ export default function ImportWorldModal({
playPressSound(); playPressSound();
setIsImporting(true); setIsImporting(true);
setError(""); setError("");
setStatus("Selecting source..."); setStatus(t("modals.importWorld.selectingSource"));
try { try {
setStatus("Selecting saveData.ms file..."); setStatus(t("modals.importWorld.selectingMsFile"));
const picked = await TauriService.pickFile("Select saveData.ms", [ const picked = await TauriService.pickFile("Select saveData.ms", [
"*.ms", "*.ms",
"*", "*",
@@ -44,13 +46,15 @@ export default function ImportWorldModal({
return; return;
} }
const worldName = deriveWorldName(picked); const worldName = deriveWorldName(picked);
setStatus("Copying LCE save..."); setStatus(t("modals.importWorld.copyingLceSave"));
const instancePath = await TauriService.getInstancePath(targetInstanceId); const instancePath = await TauriService.getInstancePath(targetInstanceId);
const saveDir = `${instancePath}/Windows64/GameHDD/${worldName}`; const saveDir = `${instancePath}/Windows64/GameHDD/${worldName}`;
await TauriService.importWorld(picked, `${saveDir}/saveData.ms`); await TauriService.importWorld(picked, `${saveDir}/saveData.ms`);
setStatus(`World imported into "${targetInstanceName}"!`); setStatus(
t("modals.importWorld.worldImported", { name: targetInstanceName }),
);
setTimeout(() => { setTimeout(() => {
onClose(); onClose();
}, 2000); }, 2000);
@@ -66,9 +70,9 @@ export default function ImportWorldModal({
playPressSound(); playPressSound();
setIsImporting(true); setIsImporting(true);
setError(""); setError("");
setStatus("Selecting source..."); setStatus(t("modals.importWorld.selectingSource"));
try { try {
setStatus("Selecting Xbox 360 save (.bin)..."); setStatus(t("modals.importWorld.selectingXboxSave"));
const picked = await TauriService.pickFile( const picked = await TauriService.pickFile(
"Select Xbox 360 Minecraft save", "Select Xbox 360 Minecraft save",
["*.bin", "*"], ["*.bin", "*"],
@@ -77,11 +81,13 @@ export default function ImportWorldModal({
setIsImporting(false); setIsImporting(false);
return; return;
} }
setStatus("Converting Xbox 360 save..."); setStatus(t("modals.importWorld.convertingXbox"));
const instancePath = await TauriService.getInstancePath(targetInstanceId); const instancePath = await TauriService.getInstancePath(targetInstanceId);
const gameHdd = `${instancePath}/Windows64/GameHDD`; const gameHdd = `${instancePath}/Windows64/GameHDD`;
await TauriService.importLceSave(picked, gameHdd); await TauriService.importLceSave(picked, gameHdd);
setStatus(`Xbox 360 save converted into "${targetInstanceName}"!`); setStatus(
t("modals.importWorld.xboxConverted", { name: targetInstanceName }),
);
setTimeout(() => { setTimeout(() => {
onClose(); onClose();
}, 2000); }, 2000);
@@ -97,19 +103,21 @@ export default function ImportWorldModal({
playPressSound(); playPressSound();
setIsImporting(true); setIsImporting(true);
setError(""); setError("");
setStatus("Selecting source..."); setStatus(t("modals.importWorld.selectingSource"));
try { try {
setStatus("Selecting PS3 save folder..."); setStatus(t("modals.importWorld.selectingPs3Folder"));
const picked = await TauriService.pickFolder(); const picked = await TauriService.pickFolder();
if (!picked) { if (!picked) {
setIsImporting(false); setIsImporting(false);
return; return;
} }
setStatus("Converting PS3 save..."); setStatus(t("modals.importWorld.convertingPs3"));
const instancePath = await TauriService.getInstancePath(targetInstanceId); const instancePath = await TauriService.getInstancePath(targetInstanceId);
const gameHdd = `${instancePath}/Windows64/GameHDD`; const gameHdd = `${instancePath}/Windows64/GameHDD`;
await TauriService.importLceSave(picked, gameHdd); await TauriService.importLceSave(picked, gameHdd);
setStatus(`PS3 save converted into "${targetInstanceName}"!`); setStatus(
t("modals.importWorld.ps3Converted", { name: targetInstanceName }),
);
setTimeout(() => { setTimeout(() => {
onClose(); onClose();
}, 2000); }, 2000);
@@ -125,20 +133,22 @@ export default function ImportWorldModal({
playPressSound(); playPressSound();
setIsImporting(true); setIsImporting(true);
setError(""); setError("");
setStatus("Selecting source..."); setStatus(t("modals.importWorld.selectingSource"));
try { try {
setStatus("Selecting Java world folder..."); setStatus(t("modals.importWorld.selectingJavaFolder"));
const picked = await TauriService.pickFolder(); const picked = await TauriService.pickFolder();
if (!picked) { if (!picked) {
setIsImporting(false); setIsImporting(false);
return; return;
} }
const worldName = deriveWorldName(picked); const worldName = deriveWorldName(picked);
setStatus("Converting Java world to LCE..."); setStatus(t("modals.importWorld.convertingJava"));
const instancePath = await TauriService.getInstancePath(targetInstanceId); const instancePath = await TauriService.getInstancePath(targetInstanceId);
const saveDir = `${instancePath}/Windows64/GameHDD/${worldName}`; const saveDir = `${instancePath}/Windows64/GameHDD/${worldName}`;
await TauriService.javaToLce(picked, `${saveDir}/saveData.ms`); await TauriService.javaToLce(picked, `${saveDir}/saveData.ms`);
setStatus(`Java world converted into "${targetInstanceName}"!`); setStatus(
t("modals.importWorld.javaConverted", { name: targetInstanceName }),
);
setTimeout(() => { setTimeout(() => {
onClose(); onClose();
}, 2000); }, 2000);
@@ -154,9 +164,9 @@ export default function ImportWorldModal({
playPressSound(); playPressSound();
setIsImporting(true); setIsImporting(true);
setError(""); setError("");
setStatus("Selecting source..."); setStatus(t("modals.importWorld.selectingSource"));
try { try {
setStatus("Selecting saveData.ms file..."); setStatus(t("modals.importWorld.selectingMsFile"));
const picked = await TauriService.pickFile("Select saveData.ms", [ const picked = await TauriService.pickFile("Select saveData.ms", [
"*.ms", "*.ms",
"*", "*",
@@ -165,15 +175,17 @@ export default function ImportWorldModal({
setIsImporting(false); setIsImporting(false);
return; return;
} }
setStatus("Selecting output folder for Java world..."); setStatus(t("modals.importWorld.selectingOutputFolder"));
const outputFolder = await TauriService.pickFolder(); const outputFolder = await TauriService.pickFolder();
if (!outputFolder) { if (!outputFolder) {
setIsImporting(false); setIsImporting(false);
return; return;
} }
setStatus("Converting LCE save to Java world..."); setStatus(t("modals.importWorld.convertingLceToJava"));
await TauriService.lceToJava(picked, outputFolder); await TauriService.lceToJava(picked, outputFolder);
setStatus(`Java world exported to "${outputFolder}"!`); setStatus(
t("modals.importWorld.javaExported", { path: outputFolder }),
);
setTimeout(() => { setTimeout(() => {
onClose(); onClose();
}, 2000); }, 2000);
+11 -9
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useMemo, useRef, type RefObject } from "react"; import { useState, useEffect, useMemo, useRef, type RefObject } from "react";
import { useTranslation } from "react-i18next";
import { TauriService } from "../../services/TauriService"; import { TauriService } from "../../services/TauriService";
import { import {
parseSchema, parseSchema,
@@ -33,6 +34,7 @@ export default function OptionsModal({
args: string[], args: string[],
) => void; ) => void;
}) { }) {
const { t } = useTranslation();
const [schema, setSchema] = useState<ArgsSchema | null>(null); const [schema, setSchema] = useState<ArgsSchema | null>(null);
const [values, setValues] = useState<Record<string, unknown>>({}); const [values, setValues] = useState<Record<string, unknown>>({});
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -56,13 +58,13 @@ export default function OptionsModal({
.then((raw) => { .then((raw) => {
if (cancelled) return; if (cancelled) return;
if (!raw) { if (!raw) {
setError("This instance does not provide a launch options schema."); setError(t("modals.options.noSchema"));
setLoading(false); setLoading(false);
return; return;
} }
const parsed = parseSchema(raw); const parsed = parseSchema(raw);
if (!parsed) { if (!parsed) {
setError("The launch options schema is invalid or unsupported."); setError(t("modals.options.invalidSchema"));
setLoading(false); setLoading(false);
return; return;
} }
@@ -433,7 +435,7 @@ export default function OptionsModal({
> >
<div className="relative w-[620px] max-w-[95vw] max-h-[88vh] p-5 flex flex-col items-center font-['Mojangles'] mc-options-bg"> <div className="relative w-[620px] max-w-[95vw] max-h-[88vh] p-5 flex flex-col items-center font-['Mojangles'] mc-options-bg">
<h2 className="text-xl text-black mc-text-shadow mb-1 text-center"> <h2 className="text-xl text-black mc-text-shadow mb-1 text-center">
Options {t("modals.options.title")}
</h2> </h2>
<p className="text-[#333333] text-sm mb-4 text-center truncate max-w-full"> <p className="text-[#333333] text-sm mb-4 text-center truncate max-w-full">
{instanceName} {instanceName}
@@ -443,7 +445,7 @@ export default function OptionsModal({
<div className="flex flex-col items-center gap-4 py-10"> <div className="flex flex-col items-center gap-4 py-10">
<div className="w-12 h-12 border-4 border-[#FFFF55] border-t-transparent rounded-full animate-spin" /> <div className="w-12 h-12 border-4 border-[#FFFF55] border-t-transparent rounded-full animate-spin" />
<p className="text-black text-lg mc-text-shadow"> <p className="text-black text-lg mc-text-shadow">
Loading options... {t("modals.options.loading")}
</p> </p>
</div> </div>
) : error ? ( ) : error ? (
@@ -452,7 +454,7 @@ export default function OptionsModal({
{error} {error}
</p> </p>
<div className="flex gap-4 mt-2 w-full"> <div className="flex gap-4 mt-2 w-full">
{actionButton(cancelRef, optionOrder.length + 1, "OK", () => { {actionButton(cancelRef, optionOrder.length + 1, t("modals.options.ok"), () => {
playBackSound(); playBackSound();
onClose(); onClose();
})} })}
@@ -477,7 +479,7 @@ export default function OptionsModal({
{sections.general.length > 0 && ( {sections.general.length > 0 && (
<div className="mb-3"> <div className="mb-3">
<h3 className="text-[#333333] mc-text-shadow uppercase tracking-widest text-sm px-3 pt-2 pb-1"> <h3 className="text-[#333333] mc-text-shadow uppercase tracking-widest text-sm px-3 pt-2 pb-1">
General {t("modals.options.general")}
</h3> </h3>
{sections.general.map(renderOption)} {sections.general.map(renderOption)}
</div> </div>
@@ -488,18 +490,18 @@ export default function OptionsModal({
{actionButton( {actionButton(
resetRef, resetRef,
optionOrder.length, optionOrder.length,
"Reset", t("modals.options.reset"),
handleReset, handleReset,
true, true,
)} )}
{actionButton(cancelRef, optionOrder.length + 1, "Cancel", () => { {actionButton(cancelRef, optionOrder.length + 1, t("modals.options.cancel"), () => {
playBackSound(); playBackSound();
onClose(); onClose();
})} })}
{actionButton( {actionButton(
saveRef, saveRef,
optionOrder.length + 2, optionOrder.length + 2,
"Save", t("modals.options.save"),
handleSave, handleSave,
)} )}
</div> </div>
+10 -8
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { import {
TauriService, TauriService,
@@ -75,6 +76,7 @@ export default function PlaytimeModal({
instanceName: string; instanceName: string;
playBackSound: (s?: string) => void; playBackSound: (s?: string) => void;
}) { }) {
const { t } = useTranslation();
const [playtime, setPlaytime] = useState<PlaytimeResponse | null>(null); const [playtime, setPlaytime] = useState<PlaytimeResponse | null>(null);
const [dailyData, setDailyData] = useState<PlaytimeDayEntry[]>([]); const [dailyData, setDailyData] = useState<PlaytimeDayEntry[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -127,7 +129,7 @@ export default function PlaytimeModal({
}} }}
> >
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase"> <h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
Playtime {t("modals.playtime.title")}
</h2> </h2>
<p className="text-white text-sm mc-text-shadow mb-4 text-center"> <p className="text-white text-sm mc-text-shadow mb-4 text-center">
@@ -136,14 +138,14 @@ export default function PlaytimeModal({
{loading ? ( {loading ? (
<div className="text-gray-400 text-sm mc-text-shadow mb-4"> <div className="text-gray-400 text-sm mc-text-shadow mb-4">
Loading... {t("modals.playtime.loading")}
</div> </div>
) : playtime ? ( ) : playtime ? (
<> <>
<div className="w-full flex flex-col gap-2 mb-4"> <div className="w-full flex flex-col gap-2 mb-4">
<div className="flex justify-between items-center bg-black/30 px-4 py-2 border border-[#373737]"> <div className="flex justify-between items-center bg-black/30 px-4 py-2 border border-[#373737]">
<span className="text-[#AAAAAA] text-sm mc-text-shadow uppercase tracking-wider"> <span className="text-[#AAAAAA] text-sm mc-text-shadow uppercase tracking-wider">
Total {t("modals.playtime.total")}
</span> </span>
<span className="text-white text-lg mc-text-shadow font-bold"> <span className="text-white text-lg mc-text-shadow font-bold">
{formatTime(playtime.totalSeconds)} {formatTime(playtime.totalSeconds)}
@@ -151,7 +153,7 @@ export default function PlaytimeModal({
</div> </div>
<div className="flex justify-between items-center bg-black/30 px-4 py-2 border border-[#373737]"> <div className="flex justify-between items-center bg-black/30 px-4 py-2 border border-[#373737]">
<span className="text-[#AAAAAA] text-sm mc-text-shadow uppercase tracking-wider"> <span className="text-[#AAAAAA] text-sm mc-text-shadow uppercase tracking-wider">
This Week {t("modals.playtime.thisWeek")}
</span> </span>
<span className="text-white text-lg mc-text-shadow font-bold"> <span className="text-white text-lg mc-text-shadow font-bold">
{formatTime(playtime.weekSeconds)} {formatTime(playtime.weekSeconds)}
@@ -159,7 +161,7 @@ export default function PlaytimeModal({
</div> </div>
<div className="flex justify-between items-center bg-black/30 px-4 py-2 border border-[#373737]"> <div className="flex justify-between items-center bg-black/30 px-4 py-2 border border-[#373737]">
<span className="text-[#AAAAAA] text-sm mc-text-shadow uppercase tracking-wider"> <span className="text-[#AAAAAA] text-sm mc-text-shadow uppercase tracking-wider">
Today {t("modals.playtime.today")}
</span> </span>
<span className="text-white text-lg mc-text-shadow font-bold"> <span className="text-white text-lg mc-text-shadow font-bold">
{formatTime(playtime.daySeconds)} {formatTime(playtime.daySeconds)}
@@ -171,7 +173,7 @@ export default function PlaytimeModal({
<div className="w-full mb-3"> <div className="w-full mb-3">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-[#888] text-[10px] mc-text-shadow uppercase tracking-widest"> <span className="text-[#888] text-[10px] mc-text-shadow uppercase tracking-widest">
Daily Breakdown {t("modals.playtime.dailyBreakdown")}
</span> </span>
<div className="flex gap-1"> <div className="flex gap-1">
{[3, 7, 14].map((d) => ( {[3, 7, 14].map((d) => (
@@ -197,7 +199,7 @@ export default function PlaytimeModal({
</> </>
) : ( ) : (
<div className="text-red-400 text-sm mc-text-shadow mb-4"> <div className="text-red-400 text-sm mc-text-shadow mb-4">
Failed to load playtime data. {t("modals.playtime.failedToLoad")}
</div> </div>
)} )}
@@ -213,7 +215,7 @@ export default function PlaytimeModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Close {t("modals.playtime.close")}
</button> </button>
</div> </div>
</motion.div> </motion.div>
+15 -13
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { TauriService } from "../../services/TauriService"; import { TauriService } from "../../services/TauriService";
import type { Edition } from "../../types/edition"; import type { Edition } from "../../types/edition";
@@ -20,6 +21,7 @@ export default function SetUidModal({
installedVersions: string[]; installedVersions: string[];
targetInstanceId: string; targetInstanceId: string;
}) { }) {
const { t } = useTranslation();
const [mode, setMode] = useState<"manual" | "copy">("manual"); const [mode, setMode] = useState<"manual" | "copy">("manual");
const [uid, setUid] = useState("0xFF02F0C87E8AC1F2"); const [uid, setUid] = useState("0xFF02F0C87E8AC1F2");
const [selectedInstance, setSelectedInstance] = useState(""); const [selectedInstance, setSelectedInstance] = useState("");
@@ -60,7 +62,7 @@ export default function SetUidModal({
let finalUid = uid; let finalUid = uid;
if (mode === "copy") { if (mode === "copy") {
if (!selectedInstance) { if (!selectedInstance) {
setError("Select an instance to copy from."); setError(t("modals.setUid.selectToCopy"));
return; return;
} }
const sourcePath = await TauriService.getInstancePath(selectedInstance); const sourcePath = await TauriService.getInstancePath(selectedInstance);
@@ -68,13 +70,13 @@ export default function SetUidModal({
const sourceData = await TauriService.readBinaryFile(`${sourcePath}/uid.dat`); const sourceData = await TauriService.readBinaryFile(`${sourcePath}/uid.dat`);
finalUid = new TextDecoder().decode(sourceData); finalUid = new TextDecoder().decode(sourceData);
} catch (e) { } catch (e) {
setError("Source instance has no uid.dat or it could not be read."); setError(t("modals.setUid.sourceNoUid"));
return; return;
} }
} }
if (!finalUid) { if (!finalUid) {
setError("UID cannot be empty."); setError(t("modals.setUid.uidEmpty"));
return; return;
} }
@@ -140,7 +142,7 @@ export default function SetUidModal({
}} }}
> >
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase"> <h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
Set UID {t("modals.setUid.title")}
</h2> </h2>
<div className="flex gap-4 mb-5 w-full justify-center"> <div className="flex gap-4 mb-5 w-full justify-center">
@@ -156,7 +158,7 @@ export default function SetUidModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Manual {t("modals.setUid.manual")}
</button> </button>
<button <button
onMouseEnter={() => setFocusIndex(1)} onMouseEnter={() => setFocusIndex(1)}
@@ -170,7 +172,7 @@ export default function SetUidModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Copy {t("modals.setUid.copy")}
</button> </button>
</div> </div>
@@ -178,7 +180,7 @@ export default function SetUidModal({
{mode === "manual" ? ( {mode === "manual" ? (
<div className="flex flex-col gap-1 items-center w-full"> <div className="flex flex-col gap-1 items-center w-full">
<label className="text-gray text-xs mc-text-shadow uppercase tracking-widest text-[#AAAAAA]"> <label className="text-gray text-xs mc-text-shadow uppercase tracking-widest text-[#AAAAAA]">
Enter UID {t("modals.setUid.enterUid")}
</label> </label>
<input <input
type="text" type="text"
@@ -194,7 +196,7 @@ export default function SetUidModal({
) : ( ) : (
<div className="flex flex-col gap-1 items-center w-full relative"> <div className="flex flex-col gap-1 items-center w-full relative">
<label className="text-gray text-xs mc-text-shadow uppercase tracking-widest text-[#AAAAAA]"> <label className="text-gray text-xs mc-text-shadow uppercase tracking-widest text-[#AAAAAA]">
Select Installed Instance {t("modals.setUid.selectInstance")}
</label> </label>
<div <div
@@ -208,9 +210,9 @@ export default function SetUidModal({
{selectedInstance {selectedInstance
? (() => { ? (() => {
const i = validInstances.find((inst: Edition) => inst.instanceId === selectedInstance); const i = validInstances.find((inst: Edition) => inst.instanceId === selectedInstance);
return i ? `${i.name} ${i.selectedBranch ? `(${i.selectedBranch})` : ""}` : "-- Select an Instance --"; return i ? `${i.name} ${i.selectedBranch ? `(${i.selectedBranch})` : ""}` : t("modals.setUid.selectPlaceholder");
})() })()
: "-- Select an Instance --"} : t("modals.setUid.selectPlaceholder")}
</span> </span>
<span className="text-xs">▼</span> <span className="text-xs">▼</span>
</div> </div>
@@ -234,7 +236,7 @@ export default function SetUidModal({
)} )}
{validInstances.length === 0 && ( {validInstances.length === 0 && (
<p className="text-red-400 text-xs text-center mt-1">No other installed instances available.</p> <p className="text-red-400 text-xs text-center mt-1">{t("modals.setUid.noInstances")}</p>
)} )}
</div> </div>
)} )}
@@ -262,7 +264,7 @@ export default function SetUidModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Cancel {t("modals.setUid.cancel")}
</button> </button>
<button <button
onMouseEnter={() => setFocusIndex(4)} onMouseEnter={() => setFocusIndex(4)}
@@ -276,7 +278,7 @@ export default function SetUidModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Save {t("modals.setUid.save")}
</button> </button>
</div> </div>
</div> </div>
+74 -70
View File
@@ -1,4 +1,5 @@
import { useState, useRef, useMemo } from "react"; import { useState, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; import { useUI, useAudio, useConfig } from "../../context/LauncherContext";
import { ArcService } from "../../services/ArcService"; import { ArcService } from "../../services/ArcService";
@@ -6,6 +7,7 @@ import { ArcFile, ArcEntry, LocFile, LocLanguage } from "../../types/arc";
import { TauriService } from "../../services/TauriService"; import { TauriService } from "../../services/TauriService";
export const ArcEditorView: React.FC = () => { export const ArcEditorView: React.FC = () => {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { animationsEnabled } = useConfig(); const { animationsEnabled } = useConfig();
@@ -48,7 +50,7 @@ export const ArcEditorView: React.FC = () => {
const handleFileLoad = async () => { const handleFileLoad = async () => {
try { try {
const path = await TauriService.pickFile("Open ARC", ["arc"]); const path = await TauriService.pickFile(t("arcEditor.openArc"), ["arc"]);
if (!path) return; if (!path) return;
playPressSound(); playPressSound();
const bytes = await TauriService.readBinaryFile(path); const bytes = await TauriService.readBinaryFile(path);
@@ -70,11 +72,11 @@ export const ArcEditorView: React.FC = () => {
setLoc(null); setLoc(null);
} }
setSelectedEntryIdx(null); setSelectedEntryIdx(null);
showNotification(`Loaded ${parsed.name}`); showNotification(t("arcEditor.loaded", { name: parsed.name }));
} catch (err: unknown) { } catch (err: unknown) {
if (err !== "CANCELED") { if (err !== "CANCELED") {
console.error("Failed to parse ARC", err); console.error("Failed to parse ARC", err);
showNotification("Failed to parse ARC", "error"); showNotification(t("arcEditor.failedToParse"), "error");
} }
} }
}; };
@@ -88,29 +90,29 @@ export const ArcEditorView: React.FC = () => {
try { try {
let targetPath = openedPath; let targetPath = openedPath;
if (!targetPath) { if (!targetPath) {
targetPath = await TauriService.saveFileDialog("Save ARC", arc.name || "archive.arc", ["arc"]); targetPath = await TauriService.saveFileDialog(t("arcEditor.saveArcDialog"), arc.name || "archive.arc", ["arc"]);
} }
if (targetPath) { if (targetPath) {
await TauriService.writeBinaryFile(targetPath, data); await TauriService.writeBinaryFile(targetPath, data);
setOpenedPath(targetPath); setOpenedPath(targetPath);
showNotification("ARC Saved Successfully"); showNotification(t("arcEditor.arcSaved"));
} }
} catch (err: unknown) { } catch (err: unknown) {
if (err !== "CANCELED") showNotification("Save failed", "error"); if (err !== "CANCELED") showNotification(t("arcEditor.saveFailed"), "error");
} }
}; };
const handleExtractEntry = async (entry: ArcEntry) => { const handleExtractEntry = async (entry: ArcEntry) => {
try { try {
const fileName = entry.filename.split("/").pop() || "asset"; const fileName = entry.filename.split("/").pop() || "asset";
const path = await TauriService.saveFileDialog("Export Asset", fileName, []); const path = await TauriService.saveFileDialog(t("arcEditor.exportAssetDialog"), fileName, []);
if (!path) return; if (!path) return;
playPressSound(); playPressSound();
await TauriService.writeBinaryFile(path, entry.data); await TauriService.writeBinaryFile(path, entry.data);
showNotification(`Extracted: ${entry.filename}`); showNotification(t("arcEditor.extracted", { name: entry.filename }));
} catch (err: unknown) { } catch (err: unknown) {
if (err !== "CANCELED") showNotification("Extraction failed", "error"); if (err !== "CANCELED") showNotification(t("arcEditor.extractionFailed"), "error");
} }
}; };
@@ -122,7 +124,7 @@ export const ArcEditorView: React.FC = () => {
newEntries.splice(idx, 1); newEntries.splice(idx, 1);
setArc({ ...arc, entries: newEntries }); setArc({ ...arc, entries: newEntries });
setSelectedEntryIdx(null); setSelectedEntryIdx(null);
showNotification(`Deleted: ${name}`); showNotification(t("arcEditor.deleted", { name }));
}; };
const handleSaveLocToArc = () => { const handleSaveLocToArc = () => {
@@ -133,10 +135,10 @@ export const ArcEditorView: React.FC = () => {
const newEntries = [...arc.entries]; const newEntries = [...arc.entries];
if (locIdx >= 0) { if (locIdx >= 0) {
newEntries[locIdx] = { ...newEntries[locIdx], data, size: data.length }; newEntries[locIdx] = { ...newEntries[locIdx], data, size: data.length };
showNotification("languages.loc updated in archive"); showNotification(t("arcEditor.locUpdatedInArchive"));
} else { } else {
newEntries.push({ filename: "languages.loc", ptr: 0, size: data.length, isCompressed: false, data }); newEntries.push({ filename: "languages.loc", ptr: 0, size: data.length, isCompressed: false, data });
showNotification("languages.loc added to archive"); showNotification(t("arcEditor.locAddedToArchive"));
} }
setArc({ ...arc, entries: newEntries }); setArc({ ...arc, entries: newEntries });
}; };
@@ -157,7 +159,7 @@ export const ArcEditorView: React.FC = () => {
}; };
setArc({ ...arc, entries: [...arc.entries, newEntry] }); setArc({ ...arc, entries: [...arc.entries, newEntry] });
e.target.value = ""; e.target.value = "";
showNotification("Entry Added"); showNotification(t("arcEditor.entryAdded"));
setIsAddModalOpen(false); setIsAddModalOpen(false);
}; };
@@ -172,7 +174,7 @@ export const ArcEditorView: React.FC = () => {
newEntries[selectedEntryIdx] = { ...newEntries[selectedEntryIdx], data, size: data.length }; newEntries[selectedEntryIdx] = { ...newEntries[selectedEntryIdx], data, size: data.length };
setArc({ ...arc, entries: newEntries }); setArc({ ...arc, entries: newEntries });
e.target.value = ""; e.target.value = "";
showNotification("Entry Replaced"); showNotification(t("arcEditor.entryReplaced"));
setIsReplaceModalOpen(false); setIsReplaceModalOpen(false);
}; };
@@ -183,7 +185,7 @@ export const ArcEditorView: React.FC = () => {
newEntries[selectedEntryIdx] = { ...newEntries[selectedEntryIdx], filename: newPath, isCompressed }; newEntries[selectedEntryIdx] = { ...newEntries[selectedEntryIdx], filename: newPath, isCompressed };
setArc({ ...arc, entries: newEntries }); setArc({ ...arc, entries: newEntries });
setIsRenameModalOpen(false); setIsRenameModalOpen(false);
showNotification("Entry Renamed"); showNotification(t("arcEditor.entryRenamed"));
}; };
const handleLocStringEdit = (langIdx: number, strIdx: number, isNew: boolean, key: string, value: string) => { const handleLocStringEdit = (langIdx: number, strIdx: number, isNew: boolean, key: string, value: string) => {
@@ -199,7 +201,7 @@ export const ArcEditorView: React.FC = () => {
} }
setLoc(newLoc); setLoc(newLoc);
setIsLocEditModalOpen(null); setIsLocEditModalOpen(null);
showNotification(isNew ? "String Added" : "String Updated"); showNotification(isNew ? t("arcEditor.stringAdded") : t("arcEditor.stringUpdated"));
}; };
const handleLocStringDelete = (langIdx: number, strIdx: number) => { const handleLocStringDelete = (langIdx: number, strIdx: number) => {
@@ -208,7 +210,7 @@ export const ArcEditorView: React.FC = () => {
const newLoc = { ...loc }; const newLoc = { ...loc };
newLoc.languages[langIdx].strings.splice(strIdx, 1); newLoc.languages[langIdx].strings.splice(strIdx, 1);
setLoc(newLoc); setLoc(newLoc);
showNotification("String Deleted"); showNotification(t("arcEditor.stringDeleted"));
}; };
const treeData = useMemo(() => { const treeData = useMemo(() => {
@@ -254,7 +256,7 @@ export const ArcEditorView: React.FC = () => {
{node.filename.split("/").pop()} {node.filename.split("/").pop()}
</span> </span>
{node.isCompressed && ( {node.isCompressed && (
<span className="ml-auto text-[8px] opacity-40 border border-white/20 px-1">ZLIB</span> <span className="ml-auto text-[8px] opacity-40 border border-white/20 px-1">{t("arcEditor.zlib")}</span>
)} )}
</div> </div>
); );
@@ -299,15 +301,15 @@ export const ArcEditorView: React.FC = () => {
const baseFolder = await TauriService.pickFolder(); const baseFolder = await TauriService.pickFolder();
if (!baseFolder) return; if (!baseFolder) return;
playPressSound(); playPressSound();
showNotification("Exporting all archive entries..."); showNotification(t("arcEditor.exportingAll"));
for (const entry of arc.entries) { for (const entry of arc.entries) {
const fileName = entry.filename.replace(/\//g, "_"); const fileName = entry.filename.replace(/\//g, "_");
await TauriService.writeBinaryFile(`${baseFolder}/${fileName}`, entry.data); await TauriService.writeBinaryFile(`${baseFolder}/${fileName}`, entry.data);
} }
showNotification("All Entries Exported"); showNotification(t("arcEditor.allEntriesExported"));
} catch (err: unknown) { } catch (err: unknown) {
if (err !== "CANCELED") showNotification("Export failed", "error"); if (err !== "CANCELED") showNotification(t("arcEditor.exportFailed"), "error");
} }
}; };
@@ -323,7 +325,7 @@ export const ArcEditorView: React.FC = () => {
> >
<div className="w-full flex justify-between items-center mb-4 px-8"> <div className="w-full flex justify-between items-center mb-4 px-8">
<h2 className="text-2xl text-white mc-text-shadow border-b-2 border-[#373737] pb-1 tracking-widest uppercase font-bold"> <h2 className="text-2xl text-white mc-text-shadow border-b-2 border-[#373737] pb-1 tracking-widest uppercase font-bold">
ARC Editor {t("arcEditor.title")}
</h2> </h2>
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
@@ -331,7 +333,7 @@ export const ArcEditorView: React.FC = () => {
className="px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none" className="px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Open ARC {t("arcEditor.openArc")}
</button> </button>
<button <button
onClick={handleExportAll} onClick={handleExportAll}
@@ -339,7 +341,7 @@ export const ArcEditorView: React.FC = () => {
className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${(!arc || arc.entries.length === 0) ? "opacity-50 grayscale" : ""}`} className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${(!arc || arc.entries.length === 0) ? "opacity-50 grayscale" : ""}`}
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Export All {t("arcEditor.exportAll")}
</button> </button>
<button <button
onClick={handleSaveArc} onClick={handleSaveArc}
@@ -347,7 +349,7 @@ export const ArcEditorView: React.FC = () => {
className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${!arc ? "opacity-50 grayscale" : ""}`} className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${!arc ? "opacity-50 grayscale" : ""}`}
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Save ARC {t("arcEditor.saveArc")}
</button> </button>
</div> </div>
</div> </div>
@@ -359,7 +361,7 @@ export const ArcEditorView: React.FC = () => {
<div className="flex-1 w-full flex flex-col items-center justify-center p-12" <div className="flex-1 w-full flex flex-col items-center justify-center p-12"
style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
<img src="/images/tools/arc.png" className="w-32 h-32 mb-8 opacity-20 grayscale" style={{ imageRendering: "pixelated" }} /> <img src="/images/tools/arc.png" className="w-32 h-32 mb-8 opacity-20 grayscale" style={{ imageRendering: "pixelated" }} />
<h3 className="text-2xl text-white/40 mc-text-shadow italic">Open an ARC file to begin editing</h3> <h3 className="text-2xl text-white/40 mc-text-shadow italic">{t("arcEditor.openArcFile")}</h3>
</div> </div>
) : ( ) : (
<div className="flex-1 w-full flex flex-col overflow-hidden" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> <div className="flex-1 w-full flex flex-col overflow-hidden" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
@@ -369,14 +371,14 @@ export const ArcEditorView: React.FC = () => {
className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === "arc" ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"}`} className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === "arc" ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"}`}
> >
<img src="/images/tools/arc.png" className={`w-5 h-5 object-contain ${activeTab === "arc" ? "" : "grayscale opacity-50"}`} style={{ imageRendering: "pixelated" }} /> <img src="/images/tools/arc.png" className={`w-5 h-5 object-contain ${activeTab === "arc" ? "" : "grayscale opacity-50"}`} style={{ imageRendering: "pixelated" }} />
<span className="text-lg uppercase tracking-wider font-bold">Archive</span> <span className="text-lg uppercase tracking-wider font-bold">{t("arcEditor.archive")}</span>
</button> </button>
<button <button
onClick={() => { playPressSound(); setActiveTab("loc"); }} onClick={() => { playPressSound(); setActiveTab("loc"); }}
className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === "loc" ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"}`} className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === "loc" ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"}`}
> >
<img src="/images/tools/loc.png" className={`w-5 h-5 object-contain ${activeTab === "loc" ? "" : "grayscale opacity-50"}`} style={{ imageRendering: "pixelated" }} /> <img src="/images/tools/loc.png" className={`w-5 h-5 object-contain ${activeTab === "loc" ? "" : "grayscale opacity-50"}`} style={{ imageRendering: "pixelated" }} />
<span className="text-lg uppercase tracking-wider font-bold">Languages (LOC)</span> <span className="text-lg uppercase tracking-wider font-bold">{t("arcEditor.languagesLoc")}</span>
</button> </button>
</div> </div>
@@ -387,7 +389,7 @@ export const ArcEditorView: React.FC = () => {
<div className="mb-4 flex gap-4"> <div className="mb-4 flex gap-4">
<input <input
type="text" type="text"
placeholder="Search entries..." placeholder={t("arcEditor.searchEntries")}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors" className="flex-1 bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors"
@@ -397,7 +399,7 @@ export const ArcEditorView: React.FC = () => {
className="px-6 py-2 text-white mc-text-shadow text-sm" className="px-6 py-2 text-white mc-text-shadow text-sm"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Add Entry {t("arcEditor.addEntry")}
</button> </button>
</div> </div>
<div className="flex-1 overflow-y-auto pr-2 custom-scrollbar"> <div className="flex-1 overflow-y-auto pr-2 custom-scrollbar">
@@ -408,17 +410,17 @@ export const ArcEditorView: React.FC = () => {
{selectedEntry ? ( {selectedEntry ? (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="p-4 bg-black/40 border-2 border-[#373737]"> <div className="p-4 bg-black/40 border-2 border-[#373737]">
<h4 className="text-[#FFFF55] mc-text-shadow font-bold text-sm uppercase tracking-widest mb-1">Entry Details</h4> <h4 className="text-[#FFFF55] mc-text-shadow font-bold text-sm uppercase tracking-widest mb-1">{t("arcEditor.entryDetails")}</h4>
<p className="text-white text-xs break-all font-mono opacity-80">{selectedEntry.filename}</p> <p className="text-white text-xs break-all font-mono opacity-80">{selectedEntry.filename}</p>
</div> </div>
<div className="grid grid-cols-2 gap-4 text-center"> <div className="grid grid-cols-2 gap-4 text-center">
<div className="bg-black/20 p-3"> <div className="bg-black/20 p-3">
<span className="block text-[10px] text-white/40 uppercase tracking-tighter text-left">Size</span> <span className="block text-[10px] text-white/40 uppercase tracking-tighter text-left">{t("arcEditor.size")}</span>
<span className="text-white text-sm">{(selectedEntry.size / 1024).toFixed(1)} KB</span> <span className="text-white text-sm">{(selectedEntry.size / 1024).toFixed(1)} KB</span>
</div> </div>
<div className="bg-black/20 p-3"> <div className="bg-black/20 p-3">
<span className="block text-[10px] text-white/40 uppercase tracking-tighter text-left">Format</span> <span className="block text-[10px] text-white/40 uppercase tracking-tighter text-left">{t("arcEditor.format")}</span>
<span className="text-white text-sm uppercase">{selectedEntry.isCompressed ? "Compressed" : "Raw"}</span> <span className="text-white text-sm uppercase">{selectedEntry.isCompressed ? t("arcEditor.compressed") : t("arcEditor.raw")}</span>
</div> </div>
</div> </div>
@@ -428,21 +430,21 @@ export const ArcEditorView: React.FC = () => {
className="w-full py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]" className="w-full py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Export File {t("arcEditor.exportFile")}
</button> </button>
<button <button
onClick={() => setIsReplaceModalOpen(true)} onClick={() => setIsReplaceModalOpen(true)}
className="w-full py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]" className="w-full py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Replace Data {t("arcEditor.replaceData")}
</button> </button>
<button <button
onClick={() => setIsRenameModalOpen(true)} onClick={() => setIsRenameModalOpen(true)}
className="w-full py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]" className="w-full py-2 text-white mc-text-shadow text-sm transition-all hover:text-[#FFFF55]"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Rename / Path {t("arcEditor.renamePath")}
</button> </button>
<button <button
onClick={() => { onClick={() => {
@@ -450,12 +452,12 @@ export const ArcEditorView: React.FC = () => {
const newEntries = [...arc.entries]; const newEntries = [...arc.entries];
newEntries[selectedEntryIdx!] = { ...selectedEntry, isCompressed: !selectedEntry.isCompressed }; newEntries[selectedEntryIdx!] = { ...selectedEntry, isCompressed: !selectedEntry.isCompressed };
setArc({ ...arc, entries: newEntries }); setArc({ ...arc, entries: newEntries });
showNotification(`Compression ${!selectedEntry.isCompressed ? "Enabled" : "Disabled"}`); showNotification(!selectedEntry.isCompressed ? t("arcEditor.compressionEnabled") : t("arcEditor.compressionDisabled"));
}} }}
className={`w-full py-2 text-white mc-text-shadow text-sm transition-all ${selectedEntry.isCompressed ? "text-[#FFFF55]" : "opacity-60"}`} className={`w-full py-2 text-white mc-text-shadow text-sm transition-all ${selectedEntry.isCompressed ? "text-[#FFFF55]" : "opacity-60"}`}
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
{selectedEntry.isCompressed ? "ZLIB Compressed" : "Uncompressed"} {selectedEntry.isCompressed ? t("arcEditor.zlibCompressed") : t("arcEditor.uncompressed")}
</button> </button>
<div className="mt-4 pt-4 border-t border-white/10"> <div className="mt-4 pt-4 border-t border-white/10">
<button <button
@@ -463,7 +465,7 @@ export const ArcEditorView: React.FC = () => {
className="w-full py-2 text-red-500/80 mc-text-shadow text-sm hover:text-red-500" className="w-full py-2 text-red-500/80 mc-text-shadow text-sm hover:text-red-500"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Delete Entry {t("arcEditor.deleteEntry")}
</button> </button>
</div> </div>
</div> </div>
@@ -471,7 +473,7 @@ export const ArcEditorView: React.FC = () => {
) : ( ) : (
<div className="flex-1 flex flex-col items-center justify-center opacity-20 italic"> <div className="flex-1 flex flex-col items-center justify-center opacity-20 italic">
<img src="/images/tools/arc.png" className="w-16 h-16 mb-4 grayscale" style={{ imageRendering: "pixelated" }} /> <img src="/images/tools/arc.png" className="w-16 h-16 mb-4 grayscale" style={{ imageRendering: "pixelated" }} />
<p className="text-white">Select an entry to view details</p> <p className="text-white">{t("arcEditor.selectEntry")}</p>
</div> </div>
)} )}
</div> </div>
@@ -480,16 +482,16 @@ export const ArcEditorView: React.FC = () => {
<div className="flex-1 flex flex-col p-4 overflow-hidden h-full"> <div className="flex-1 flex flex-col p-4 overflow-hidden h-full">
{!loc ? ( {!loc ? (
<div className="flex-1 flex flex-col items-center justify-center p-12 text-center"> <div className="flex-1 flex flex-col items-center justify-center p-12 text-center">
<h4 className="text-xl text-white/40 mc-text-shadow italic mb-4">No languages.loc found in archive</h4> <h4 className="text-xl text-white/40 mc-text-shadow italic mb-4">{t("arcEditor.noLocFound")}</h4>
<button <button
onClick={() => { onClick={() => {
setLoc({ version: 0, languages: [{ id: "en_US", version: 1, isStatic: false, langId: "en_US", strings: [] }] }); setLoc({ version: 0, languages: [{ id: "en_US", version: 1, isStatic: false, langId: "en_US", strings: [] }] });
showNotification("Created new locale structure"); showNotification(t("arcEditor.createdNewLocaleStructure"));
}} }}
className="px-6 py-2 text-white mc-text-shadow text-lg" className="px-6 py-2 text-white mc-text-shadow text-lg"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Create languages.loc {t("arcEditor.createLoc")}
</button> </button>
</div> </div>
) : ( ) : (
@@ -501,12 +503,12 @@ export const ArcEditorView: React.FC = () => {
className="bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors" className="bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors"
> >
{loc.languages.map((lang, idx) => ( {loc.languages.map((lang, idx) => (
<option key={idx} value={idx}>{lang.id} {lang.isStatic ? "[Static]" : "[Keyed]"}</option> <option key={idx} value={idx}>{lang.id} {lang.isStatic ? `[${t("arcEditor.static")}]` : `[${t("arcEditor.keyed")}]`}</option>
))} ))}
</select> </select>
<input <input
type="text" type="text"
placeholder="Search strings..." placeholder={t("arcEditor.searchStrings")}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors" className="flex-1 bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors"
@@ -516,23 +518,23 @@ export const ArcEditorView: React.FC = () => {
className="px-6 py-2 text-white mc-text-shadow text-sm" className="px-6 py-2 text-white mc-text-shadow text-sm"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Add String {t("arcEditor.addString")}
</button> </button>
<button <button
onClick={handleSaveLocToArc} onClick={handleSaveLocToArc}
className="px-6 py-2 text-[#FFFF55] mc-text-shadow text-sm" className="px-6 py-2 text-[#FFFF55] mc-text-shadow text-sm"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Write to ARC {t("arcEditor.writeToArc")}
</button> </button>
</div> </div>
<div className="flex-1 overflow-y-auto pr-2 custom-scrollbar"> <div className="flex-1 overflow-y-auto pr-2 custom-scrollbar">
<table className="w-full text-left border-collapse"> <table className="w-full text-left border-collapse">
<thead className="sticky top-0 bg-[#252525] z-10"> <thead className="sticky top-0 bg-[#252525] z-10">
<tr className="border-b-2 border-[#373737]"> <tr className="border-b-2 border-[#373737]">
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">Key / Index</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">{t("arcEditor.keyOrIndex")}</th>
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">Value</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">{t("arcEditor.value")}</th>
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold text-right">Actions</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold text-right">{t("arcEditor.actions")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -548,7 +550,7 @@ export const ArcEditorView: React.FC = () => {
onClick={() => setIsLocEditModalOpen({ langIdx: selectedLocLangIdx, strIdx: str.originalIdx, isNew: false })} onClick={() => setIsLocEditModalOpen({ langIdx: selectedLocLangIdx, strIdx: str.originalIdx, isNew: false })}
className="px-2 py-1 text-[10px] bg-white/10 hover:bg-[#FFFF55]/20 hover:text-[#FFFF55] border border-white/20 uppercase" className="px-2 py-1 text-[10px] bg-white/10 hover:bg-[#FFFF55]/20 hover:text-[#FFFF55] border border-white/20 uppercase"
> >
Edit {t("arcEditor.edit")}
</button> </button>
<button onClick={() => handleLocStringDelete(selectedLocLangIdx, str.originalIdx)} className="p-1 hover:text-red-500 opacity-60"> <button onClick={() => handleLocStringDelete(selectedLocLangIdx, str.originalIdx)} className="p-1 hover:text-red-500 opacity-60">
<img src="/images/Trash_Bin_Icon.png" className="w-4 h-4 object-contain" style={{ imageRendering: "pixelated" }} /> <img src="/images/Trash_Bin_Icon.png" className="w-4 h-4 object-contain" style={{ imageRendering: "pixelated" }} />
@@ -573,7 +575,7 @@ export const ArcEditorView: React.FC = () => {
className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white" className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}
> >
Back {t("arcEditor.back")}
</button> </button>
</div> </div>
<AnimatePresence> <AnimatePresence>
@@ -594,17 +596,17 @@ export const ArcEditorView: React.FC = () => {
{isAddModalOpen && ( {isAddModalOpen && (
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/60 backdrop-blur-sm"> <div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-md p-8" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> <div className="w-full max-w-md p-8" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow mb-6">Add File to Archive</h3> <h3 className="text-2xl text-[#FFFF55] mc-text-shadow mb-6">{t("arcEditor.addFileToArchive")}</h3>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<button <button
onClick={() => injectInputRef.current?.click()} onClick={() => injectInputRef.current?.click()}
className="w-full py-3 text-white mc-text-shadow" className="w-full py-3 text-white mc-text-shadow"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Select Source File {t("arcEditor.selectSourceFile")}
</button> </button>
<div className="flex justify-end gap-4 mt-4"> <div className="flex justify-end gap-4 mt-4">
<button onClick={() => setIsAddModalOpen(false)} className="px-6 py-2 text-white/60 hover:text-white transition-colors">Cancel</button> <button onClick={() => setIsAddModalOpen(false)} className="px-6 py-2 text-white/60 hover:text-white transition-colors">{t("arcEditor.cancel")}</button>
</div> </div>
</div> </div>
</div> </div>
@@ -614,7 +616,7 @@ export const ArcEditorView: React.FC = () => {
{isReplaceModalOpen && selectedEntryIdx !== null && ( {isReplaceModalOpen && selectedEntryIdx !== null && (
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/60 backdrop-blur-sm"> <div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-md p-8" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> <div className="w-full max-w-md p-8" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow mb-4">Replace File Data</h3> <h3 className="text-2xl text-[#FFFF55] mc-text-shadow mb-4">{t("arcEditor.replaceFileData")}</h3>
<p className="text-white/60 mb-6 truncate">{arc?.entries[selectedEntryIdx].filename}</p> <p className="text-white/60 mb-6 truncate">{arc?.entries[selectedEntryIdx].filename}</p>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<button <button
@@ -622,10 +624,10 @@ export const ArcEditorView: React.FC = () => {
className="w-full py-3 text-white mc-text-shadow" className="w-full py-3 text-white mc-text-shadow"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Select New File {t("arcEditor.selectNewFile")}
</button> </button>
<div className="flex justify-end gap-4 mt-4"> <div className="flex justify-end gap-4 mt-4">
<button onClick={() => setIsReplaceModalOpen(false)} className="px-6 py-2 text-white/60 hover:text-white transition-colors">Cancel</button> <button onClick={() => setIsReplaceModalOpen(false)} className="px-6 py-2 text-white/60 hover:text-white transition-colors">{t("arcEditor.cancel")}</button>
</div> </div>
</div> </div>
</div> </div>
@@ -653,15 +655,16 @@ export const ArcEditorView: React.FC = () => {
} }
function RenameModal({ initialName, initialCompressed, onClose, onConfirm }: { initialName: string, initialCompressed: boolean, onClose: () => void, onConfirm: (name: string, comp: boolean) => void }) { function RenameModal({ initialName, initialCompressed, onClose, onConfirm }: { initialName: string, initialCompressed: boolean, onClose: () => void, onConfirm: (name: string, comp: boolean) => void }) {
const { t } = useTranslation();
const [name, setName] = useState(initialName); const [name, setName] = useState(initialName);
const [comp, setComp] = useState(initialCompressed); const [comp, setComp] = useState(initialCompressed);
return ( return (
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/60 backdrop-blur-sm"> <div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-lg p-8" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> <div className="w-full max-w-lg p-8" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow mb-6 uppercase tracking-widest">Rename Entry</h3> <h3 className="text-2xl text-[#FFFF55] mc-text-shadow mb-6 uppercase tracking-widest">{t("arcEditor.renameEntry")}</h3>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div> <div>
<label className="text-white/40 text-xs uppercase mb-2 block">New Archive Path</label> <label className="text-white/40 text-xs uppercase mb-2 block">{t("arcEditor.newArchivePath")}</label>
<input <input
type="text" type="text"
value={name} value={name}
@@ -671,16 +674,16 @@ function RenameModal({ initialName, initialCompressed, onClose, onConfirm }: { i
</div> </div>
<label className="flex items-center gap-3 cursor-pointer group"> <label className="flex items-center gap-3 cursor-pointer group">
<input type="checkbox" checked={comp} onChange={(e) => setComp(e.target.checked)} className="w-5 h-5 accent-[#FFFF55]" /> <input type="checkbox" checked={comp} onChange={(e) => setComp(e.target.checked)} className="w-5 h-5 accent-[#FFFF55]" />
<span className="text-white group-hover:text-[#FFFF55] transition-colors">Mark as compressed (zlib)</span> <span className="text-white group-hover:text-[#FFFF55] transition-colors">{t("arcEditor.markAsCompressed")}</span>
</label> </label>
<div className="flex justify-end gap-4 mt-6"> <div className="flex justify-end gap-4 mt-6">
<button onClick={onClose} className="px-6 py-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest">Cancel</button> <button onClick={onClose} className="px-6 py-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest">{t("arcEditor.cancel")}</button>
<button <button
onClick={() => onConfirm(name, comp)} onClick={() => onConfirm(name, comp)}
className="px-8 py-2 text-white mc-text-shadow" className="px-8 py-2 text-white mc-text-shadow"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Rename {t("arcEditor.rename")}
</button> </button>
</div> </div>
</div> </div>
@@ -690,16 +693,17 @@ function RenameModal({ initialName, initialCompressed, onClose, onConfirm }: { i
} }
function LocEditModal({ data, lang, onClose, onConfirm }: { data: { langIdx: number, strIdx: number, isNew: boolean }, lang: LocLanguage, onClose: () => void, onConfirm: (langIdx: number, strIdx: number, isNew: boolean, key: string, val: string) => void }) { function LocEditModal({ data, lang, onClose, onConfirm }: { data: { langIdx: number, strIdx: number, isNew: boolean }, lang: LocLanguage, onClose: () => void, onConfirm: (langIdx: number, strIdx: number, isNew: boolean, key: string, val: string) => void }) {
const { t } = useTranslation();
const [key, setKey] = useState(!data.isNew ? (lang.strings[data.strIdx].key || "") : ""); const [key, setKey] = useState(!data.isNew ? (lang.strings[data.strIdx].key || "") : "");
const [val, setVal] = useState(!data.isNew ? lang.strings[data.strIdx].value : ""); const [val, setVal] = useState(!data.isNew ? lang.strings[data.strIdx].value : "");
return ( return (
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/60 backdrop-blur-sm"> <div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-2xl p-8" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> <div className="w-full max-w-2xl p-8" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow mb-6 uppercase tracking-widest">{data.isNew ? "Add" : "Edit"} String</h3> <h3 className="text-2xl text-[#FFFF55] mc-text-shadow mb-6 uppercase tracking-widest">{data.isNew ? t("arcEditor.add") : t("arcEditor.edit")} {t("arcEditor.stringTitle")}</h3>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{!lang.isStatic ? ( {!lang.isStatic ? (
<div> <div>
<label className="text-white/40 text-xs uppercase mb-2 block">String Key</label> <label className="text-white/40 text-xs uppercase mb-2 block">{t("arcEditor.stringKey")}</label>
<input <input
type="text" type="text"
value={key} value={key}
@@ -708,10 +712,10 @@ function LocEditModal({ data, lang, onClose, onConfirm }: { data: { langIdx: num
/> />
</div> </div>
) : ( ) : (
<div className="text-white/40 italic mb-2">Static entry - Index: {data.isNew ? lang.strings.length : data.strIdx}</div> <div className="text-white/40 italic mb-2">{t("arcEditor.staticEntry", { index: data.isNew ? lang.strings.length : data.strIdx })}</div>
)} )}
<div> <div>
<label className="text-white/40 text-xs uppercase mb-2 block">String Value</label> <label className="text-white/40 text-xs uppercase mb-2 block">{t("arcEditor.stringValue")}</label>
<textarea <textarea
value={val} value={val}
onChange={(e) => setVal(e.target.value)} onChange={(e) => setVal(e.target.value)}
@@ -720,13 +724,13 @@ function LocEditModal({ data, lang, onClose, onConfirm }: { data: { langIdx: num
/> />
</div> </div>
<div className="flex justify-end gap-4 mt-6"> <div className="flex justify-end gap-4 mt-6">
<button onClick={onClose} className="px-6 py-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest">Cancel</button> <button onClick={onClose} className="px-6 py-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest">{t("arcEditor.cancel")}</button>
<button <button
onClick={() => onConfirm(data.langIdx, data.strIdx, data.isNew, key, val)} onClick={() => onConfirm(data.langIdx, data.strIdx, data.isNew, key, val)}
className="px-8 py-2 text-white mc-text-shadow" className="px-8 py-2 text-white mc-text-shadow"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
{data.isNew ? "Add" : "Save"} {data.isNew ? t("arcEditor.add") : t("arcEditor.save")}
</button> </button>
</div> </div>
</div> </div>
+24 -22
View File
@@ -1,4 +1,5 @@
import { useState, useRef, useMemo } from "react"; import { useState, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; import { useUI, useAudio, useConfig } from "../../context/LauncherContext";
import { ColService } from "../../services/ColService"; import { ColService } from "../../services/ColService";
@@ -13,6 +14,7 @@ function hexToArgb(hex: string) {
} }
export default function ColEditorView() { export default function ColEditorView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { animationsEnabled } = useConfig(); const { animationsEnabled } = useConfig();
@@ -46,10 +48,10 @@ export default function ColEditorView() {
try { try {
const parsedCol = ColService.readCOL(buffer); const parsedCol = ColService.readCOL(buffer);
setCol(parsedCol); setCol(parsedCol);
showNotification(`Loaded ${file.name}`); showNotification(t("colEditor.loaded", { name: file.name }));
} catch (err: unknown) { } catch (err: unknown) {
console.error("Failed to parse COL", err); console.error("Failed to parse COL", err);
showNotification(err instanceof Error ? err.message : "Failed to parse COL", "error"); showNotification(err instanceof Error ? err.message : t("colEditor.failedToParse"), "error");
} }
e.target.value = ""; e.target.value = "";
}; };
@@ -66,10 +68,10 @@ export default function ColEditorView() {
a.download = "colours.col"; a.download = "colours.col";
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
showNotification("COL Saved Successfully"); showNotification(t("colEditor.savedSuccessfully"));
} catch (err: unknown) { } catch (err: unknown) {
console.error("Failed to save COL", err); console.error("Failed to save COL", err);
showNotification(err instanceof Error ? err.message : "Failed to save COL", "error"); showNotification(err instanceof Error ? err.message : t("colEditor.failedToSave"), "error");
} }
}; };
@@ -94,7 +96,7 @@ export default function ColEditorView() {
...col, ...col,
colors: [{ name: "NewColor", color: 0xFFFFFFFF }, ...col.colors] colors: [{ name: "NewColor", color: 0xFFFFFFFF }, ...col.colors]
}); });
showNotification("Color Added"); showNotification(t("colEditor.colorAdded"));
}; };
const handleAddWorldColor = () => { const handleAddWorldColor = () => {
@@ -104,7 +106,7 @@ export default function ColEditorView() {
...col, ...col,
worldColors: [{ name: "NewWorldColor", waterColor: 0xFFFFFFFF, underwaterColor: 0xFFFFFFFF, fogColor: 0xFFFFFFFF }, ...col.worldColors] worldColors: [{ name: "NewWorldColor", waterColor: 0xFFFFFFFF, underwaterColor: 0xFFFFFFFF, fogColor: 0xFFFFFFFF }, ...col.worldColors]
}); });
showNotification("World Color Added"); showNotification(t("colEditor.worldColorAdded"));
}; };
const handleDeleteColor = (idx: number) => { const handleDeleteColor = (idx: number) => {
@@ -134,8 +136,8 @@ export default function ColEditorView() {
<input type="file" ref={fileInputRef} onChange={handleFileLoad} className="hidden" accept=".col" /> <input type="file" ref={fileInputRef} onChange={handleFileLoad} className="hidden" accept=".col" />
<div className="flex items-center justify-between mb-6 px-4"> <div className="flex items-center justify-between mb-6 px-4">
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<h2 className="text-3xl text-white mc-text-shadow tracking-widest uppercase font-bold">COL Editor</h2> <h2 className="text-3xl text-white mc-text-shadow tracking-widest uppercase font-bold">{t("colEditor.title")}</h2>
{col && <span className="text-white/40 mc-text-shadow italic">Version: <span className="text-[#FFFF55]">{col.version}</span></span>} {col && <span className="text-white/40 mc-text-shadow italic">{t("colEditor.version")} <span className="text-[#FFFF55]">{col.version}</span></span>}
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
@@ -143,7 +145,7 @@ export default function ColEditorView() {
className="px-6 py-2 text-white mc-text-shadow text-lg" className="px-6 py-2 text-white mc-text-shadow text-lg"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Open COL {t("colEditor.openCol")}
</button> </button>
<button <button
onClick={handleSaveCol} onClick={handleSaveCol}
@@ -151,7 +153,7 @@ export default function ColEditorView() {
className={`px-6 py-2 text-white mc-text-shadow text-lg ${!col ? "opacity-50 grayscale" : ""}`} className={`px-6 py-2 text-white mc-text-shadow text-lg ${!col ? "opacity-50 grayscale" : ""}`}
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Save COL {t("colEditor.saveCol")}
</button> </button>
</div> </div>
</div> </div>
@@ -159,7 +161,7 @@ export default function ColEditorView() {
{!col ? ( {!col ? (
<div className="flex-1 w-full flex flex-col items-center justify-center p-12" <div className="flex-1 w-full flex flex-col items-center justify-center p-12"
style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
<h3 className="text-2xl text-white/40 mc-text-shadow italic">Open a COL file to begin editing</h3> <h3 className="text-2xl text-white/40 mc-text-shadow italic">{t("colEditor.openToBegin")}</h3>
</div> </div>
) : ( ) : (
<div className="flex-1 w-full flex flex-col overflow-hidden" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> <div className="flex-1 w-full flex flex-col overflow-hidden" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
@@ -168,14 +170,14 @@ export default function ColEditorView() {
onClick={() => { playPressSound(); setActiveTab("colors"); }} onClick={() => { playPressSound(); setActiveTab("colors"); }}
className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === "colors" ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"}`} className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === "colors" ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"}`}
> >
<span className="text-lg">Colors ({col.colors.length})</span> <span className="text-lg">{t("colEditor.colors", { count: col.colors.length })}</span>
</button> </button>
<button <button
onClick={() => { playPressSound(); setActiveTab("worldColors"); }} onClick={() => { playPressSound(); setActiveTab("worldColors"); }}
disabled={col.version === 0} disabled={col.version === 0}
className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === "worldColors" ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"} ${col.version === 0 ? "opacity-20 cursor-not-allowed" : ""}`} className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === "worldColors" ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"} ${col.version === 0 ? "opacity-20 cursor-not-allowed" : ""}`}
> >
<span className="text-lg">World Colors ({col.worldColors.length})</span> <span className="text-lg">{t("colEditor.worldColors", { count: col.worldColors.length })}</span>
</button> </button>
</div> </div>
@@ -183,7 +185,7 @@ export default function ColEditorView() {
<div className="mb-4 flex gap-4"> <div className="mb-4 flex gap-4">
<input <input
type="text" type="text"
placeholder="Search colors..." placeholder={t("colEditor.searchColors")}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors" className="flex-1 bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors"
@@ -193,7 +195,7 @@ export default function ColEditorView() {
className="px-6 py-2 text-white mc-text-shadow text-sm" className="px-6 py-2 text-white mc-text-shadow text-sm"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Add {activeTab === "colors" ? "Color" : "World Color"} {activeTab === "colors" ? t("colEditor.addColor") : t("colEditor.addWorldColor")}
</button> </button>
</div> </div>
@@ -201,16 +203,16 @@ export default function ColEditorView() {
<table className="w-full text-left border-collapse"> <table className="w-full text-left border-collapse">
<thead className="sticky top-0 bg-[#252525] z-10 w-full mb-2"> <thead className="sticky top-0 bg-[#252525] z-10 w-full mb-2">
<tr className="border-b-2 border-[#373737]"> <tr className="border-b-2 border-[#373737]">
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold w-1/4">Name</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold w-1/4">{t("colEditor.name")}</th>
{activeTab === "colors" && <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">Color (ARGB)</th>} {activeTab === "colors" && <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">{t("colEditor.color")}</th>}
{activeTab === "worldColors" && ( {activeTab === "worldColors" && (
<> <>
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">Water (ARGB)</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">{t("colEditor.water")}</th>
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">Underwater (ARGB)</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">{t("colEditor.underwater")}</th>
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">Fog (ARGB)</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">{t("colEditor.fog")}</th>
</> </>
)} )}
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold text-right w-16">Remove</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold text-right w-16">{t("colEditor.remove")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -371,7 +373,7 @@ export default function ColEditorView() {
className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white" className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}
> >
Back {t("colEditor.back")}
</button> </button>
</div> </div>
+4 -3
View File
@@ -1,4 +1,5 @@
import { useEffect, useMemo, memo, useState } from "react"; import { useEffect, useMemo, memo, useState } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useUI, useAudio } from "../../context/LauncherContext"; import { useUI, useAudio } from "../../context/LauncherContext";
import { usePlatform } from "../../hooks/usePlatform"; import { usePlatform } from "../../hooks/usePlatform";
@@ -30,6 +31,7 @@ interface CreditCategory {
} }
const CreditsView = memo(function CreditsView() { const CreditsView = memo(function CreditsView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound } = useAudio(); const { playPressSound } = useAudio();
const { isAndroid } = usePlatform(); const { isAndroid } = usePlatform();
@@ -338,9 +340,8 @@ const CreditsView = memo(function CreditsView() {
color: isHovered ? "#FFFF55" : "white", color: isHovered ? "#FFFF55" : "white",
}} }}
> >
Back to Menu {t("credits.backToMenu")}
</button> </button> )}
)}
<motion.div <motion.div
initial={{ y: "50%" }} initial={{ y: "50%" }}
+30 -6
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; import { useUI, useAudio, useConfig } from "../../context/LauncherContext";
@@ -21,12 +22,35 @@ const DEV_TOOLS: DevTool[] = [
]; ];
export default function DevtoolsView() { export default function DevtoolsView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { animationsEnabled } = useConfig(); const { animationsEnabled } = useConfig();
const [focusIndex, setFocusIndex] = useState<number>(0); const [focusIndex, setFocusIndex] = useState<number>(0);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const BACK_BUTTON_INDEX = DEV_TOOLS.length; const BACK_BUTTON_INDEX = DEV_TOOLS.length;
const toolName = (id: string): string => {
switch (id) {
case "guides":
return t("devtools.tools.guides");
case "pck":
return t("devtools.tools.pck");
case "arc":
return t("devtools.tools.arc");
case "loc":
return t("devtools.tools.loc");
case "grf":
return t("devtools.tools.grf");
case "col":
return t("devtools.tools.col");
case "options":
return t("devtools.tools.options");
case "model":
return t("devtools.tools.model");
default:
return id;
}
};
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" || e.key === "Backspace") { if (e.key === "Escape" || e.key === "Backspace") {
@@ -81,7 +105,7 @@ export default function DevtoolsView() {
className="flex flex-col items-center w-full max-w-3xl h-full outline-none" className="flex flex-col items-center w-full max-w-3xl h-full outline-none"
> >
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[60%] max-w-75 text-center tracking-widest uppercase opacity-80 font-bold"> <h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[60%] max-w-75 text-center tracking-widest uppercase opacity-80 font-bold">
Developer Tools {t("home.developerTools")}
</h2> </h2>
<div <div
@@ -109,7 +133,7 @@ export default function DevtoolsView() {
<div className="w-20 h-20 bg-black/40 border-2 border-[#373737] flex items-center justify-center relative shadow-inner"> <div className="w-20 h-20 bg-black/40 border-2 border-[#373737] flex items-center justify-center relative shadow-inner">
<img <img
src={`/images/tools/${tool.id}.png`} src={`/images/tools/${tool.id}.png`}
alt={tool.name} alt={toolName(tool.id)}
className="w-12 h-12 object-contain opacity-50 grayscale" className="w-12 h-12 object-contain opacity-50 grayscale"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
onError={(e) => { onError={(e) => {
@@ -119,7 +143,7 @@ export default function DevtoolsView() {
if (parent && !parent.querySelector(".tool-fallback")) { if (parent && !parent.querySelector(".tool-fallback")) {
const fallback = document.createElement("span"); const fallback = document.createElement("span");
fallback.className = "tool-fallback text-2xl text-white/30 mc-text-shadow uppercase font-bold"; fallback.className = "tool-fallback text-2xl text-white/30 mc-text-shadow uppercase font-bold";
fallback.textContent = tool.name.charAt(0); fallback.textContent = toolName(tool.id).charAt(0);
parent.appendChild(fallback); parent.appendChild(fallback);
} }
}} }}
@@ -127,7 +151,7 @@ export default function DevtoolsView() {
{tool.comingSoon && ( {tool.comingSoon && (
<div className="absolute inset-0 flex items-center justify-center bg-black/60"> <div className="absolute inset-0 flex items-center justify-center bg-black/60">
<span className="text-[10px] text-[#FFFF55] mc-text-shadow uppercase tracking-tighter text-center px-1"> <span className="text-[10px] text-[#FFFF55] mc-text-shadow uppercase tracking-tighter text-center px-1">
Coming Soon {t("devtools.comingSoon")}
</span> </span>
</div> </div>
)} )}
@@ -136,7 +160,7 @@ export default function DevtoolsView() {
className={`text-center text-lg mc-text-shadow transition-colors ${focusIndex === i ? "text-[#FFFF55]" : "text-white" className={`text-center text-lg mc-text-shadow transition-colors ${focusIndex === i ? "text-[#FFFF55]" : "text-white"
}`} }`}
> >
{tool.name} {toolName(tool.id)}
</span> </span>
</div> </div>
))} ))}
@@ -161,7 +185,7 @@ export default function DevtoolsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Back {t("common.back")}
</button> </button>
</motion.div> </motion.div>
); );
+24 -22
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useCallback, useMemo, memo } from "react"; import { useState, useEffect, useRef, useCallback, useMemo, memo } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { import {
TauriService, TauriService,
@@ -18,11 +19,6 @@ const KEY_FALLBACK = [
]; ];
const MOUSE_IDS = ["MOUSE_LEFT", "MOUSE_MIDDLE", "MOUSE_RIGHT"]; const MOUSE_IDS = ["MOUSE_LEFT", "MOUSE_MIDDLE", "MOUSE_RIGHT"];
const MOUSE_LABELS: Record<string, string> = {
MOUSE_LEFT: "Left Click",
MOUSE_MIDDLE: "Middle Click",
MOUSE_RIGHT: "Right Click",
};
const CONTROLLER_FALLBACK = [ const CONTROLLER_FALLBACK = [
"PAD_A", "PAD_A",
@@ -63,6 +59,7 @@ const stoneButtonStyle = (highlighted: boolean) => ({
}); });
const GoldMapperView = memo(function GoldMapperView() { const GoldMapperView = memo(function GoldMapperView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { animationsEnabled, goldmapperEnabled, setGoldmapperEnabled } = const { animationsEnabled, goldmapperEnabled, setGoldmapperEnabled } =
useConfig(); useConfig();
@@ -108,6 +105,11 @@ const GoldMapperView = memo(function GoldMapperView() {
}, []); }, []);
const getTo = useCallback((id: string) => binds[id] ?? id, [binds]); const getTo = useCallback((id: string) => binds[id] ?? id, [binds]);
const mouseLabel = (id: string) => {
if (id === "MOUSE_LEFT") return t("goldMapper.leftClick");
if (id === "MOUSE_MIDDLE") return t("goldMapper.middleClick");
return t("goldMapper.rightClick");
};
const buildPayload = useCallback( const buildPayload = useCallback(
(nextBinds: Record<string, string>): GoldMapperMapping[] => { (nextBinds: Record<string, string>): GoldMapperMapping[] => {
const rows: GoldMapperMapping[] = []; const rows: GoldMapperMapping[] = [];
@@ -182,11 +184,11 @@ const GoldMapperView = memo(function GoldMapperView() {
.replace(/\s+/g, "_") .replace(/\s+/g, "_")
.replace(/^KEY_/, ""); .replace(/^KEY_/, "");
if (!norm || !keyboardIds.includes(`KEY_${norm}`)) { if (!norm || !keyboardIds.includes(`KEY_${norm}`)) {
setKeyInputError("Unknown key name"); setKeyInputError(t("goldMapper.unknownKeyName"));
return; return;
} }
pickTarget(editing, `KEY_${norm}`); pickTarget(editing, `KEY_${norm}`);
}, [editing, keyInput, keyboardIds, pickTarget]); }, [editing, keyInput, keyboardIds, pickTarget, t]);
const rows: Row[] = useMemo(() => { const rows: Row[] = useMemo(() => {
const list: Row[] = [{ kind: "reset", key: "reset" }]; const list: Row[] = [{ kind: "reset", key: "reset" }];
@@ -194,7 +196,7 @@ const GoldMapperView = memo(function GoldMapperView() {
list.push({ list.push({
kind: "header", kind: "header",
key: "controller_header", key: "controller_header",
label: "Controller", label: t("goldMapper.controller"),
}); });
for (const id of controllerIds) { for (const id of controllerIds) {
list.push({ list.push({
@@ -204,16 +206,16 @@ const GoldMapperView = memo(function GoldMapperView() {
label: displayName(id), label: displayName(id),
}); });
} }
list.push({ kind: "header", key: "mouse_header", label: "Mouse" }); list.push({ kind: "header", key: "mouse_header", label: t("goldMapper.mouse") });
for (const id of MOUSE_IDS) { for (const id of MOUSE_IDS) {
list.push({ list.push({
kind: "bind", kind: "bind",
key: `mouse_${id}`, key: `mouse_${id}`,
id, id,
label: MOUSE_LABELS[id], label: mouseLabel(id),
}); });
} }
list.push({ kind: "header", key: "keyboard_header", label: "Keyboard" }); list.push({ kind: "header", key: "keyboard_header", label: t("goldMapper.keyboard") });
for (const id of keyboardIds) { for (const id of keyboardIds) {
list.push({ list.push({
kind: "bind", kind: "bind",
@@ -223,7 +225,7 @@ const GoldMapperView = memo(function GoldMapperView() {
}); });
} }
return list; return list;
}, [controllerIds, keyboardIds]); }, [controllerIds, keyboardIds, t, mouseLabel]);
const focusableCount = rows.filter((r) => r.kind !== "header").length; const focusableCount = rows.filter((r) => r.kind !== "header").length;
const modalItemCount = MOUSE_IDS.length + controllerIds.length + 2; const modalItemCount = MOUSE_IDS.length + controllerIds.length + 2;
@@ -384,9 +386,9 @@ const GoldMapperView = memo(function GoldMapperView() {
}`} }`}
> >
{isReset {isReset
? "Reset to Defaults" ? t("goldMapper.resetToDefaults")
: isEnable : isEnable
? "Enable GoldMapper" ? t("goldMapper.enableGoldMapper")
: row.label} : row.label}
</span> </span>
{!isReset && !isEnable && row.kind === "bind" && ( {!isReset && !isEnable && row.kind === "bind" && (
@@ -431,7 +433,7 @@ const GoldMapperView = memo(function GoldMapperView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Back {t("goldMapper.back")}
</button> </button>
{editing !== null && ( {editing !== null && (
@@ -443,13 +445,13 @@ const GoldMapperView = memo(function GoldMapperView() {
> >
<div className="relative w-[620px] max-w-[95vw] h-[580px] max-h-[88vh] p-5 flex flex-col font-['Mojangles'] mc-options-bg"> <div className="relative w-[620px] max-w-[95vw] h-[580px] max-h-[88vh] p-5 flex flex-col font-['Mojangles'] mc-options-bg">
<h2 className="text-xl text-black mc-text-shadow mb-4 text-center"> <h2 className="text-xl text-black mc-text-shadow mb-4 text-center">
Assign {displayName(editing)} {t("goldMapper.assign", { name: displayName(editing) })}
</h2> </h2>
<div className="w-full flex-1 min-h-0 overflow-y-auto custom-scrollbar mb-4"> <div className="w-full flex-1 min-h-0 overflow-y-auto custom-scrollbar mb-4">
<div className="mb-3"> <div className="mb-3">
<h3 className="text-[#333333] mc-text-shadow uppercase tracking-widest text-sm px-3 pt-2 pb-1"> <h3 className="text-[#333333] mc-text-shadow uppercase tracking-widest text-sm px-3 pt-2 pb-1">
Mouse {t("goldMapper.mouse")}
</h3> </h3>
<div className="grid grid-cols-4 gap-2 p-1 content-start"> <div className="grid grid-cols-4 gap-2 p-1 content-start">
{MOUSE_IDS.map((id, i) => ( {MOUSE_IDS.map((id, i) => (
@@ -464,7 +466,7 @@ const GoldMapperView = memo(function GoldMapperView() {
}`} }`}
style={stoneButtonStyle(modalFocusIndex === i)} style={stoneButtonStyle(modalFocusIndex === i)}
> >
<span className="truncate">{MOUSE_LABELS[id]}</span> <span className="truncate">{mouseLabel(id)}</span>
</button> </button>
))} ))}
</div> </div>
@@ -472,7 +474,7 @@ const GoldMapperView = memo(function GoldMapperView() {
<div className="mb-3"> <div className="mb-3">
<h3 className="text-[#333333] mc-text-shadow uppercase tracking-widest text-sm px-3 pt-2 pb-1"> <h3 className="text-[#333333] mc-text-shadow uppercase tracking-widest text-sm px-3 pt-2 pb-1">
Controller {t("goldMapper.controller")}
</h3> </h3>
<div className="grid grid-cols-4 gap-2 p-1 content-start"> <div className="grid grid-cols-4 gap-2 p-1 content-start">
{controllerIds.map((id, i) => { {controllerIds.map((id, i) => {
@@ -500,7 +502,7 @@ const GoldMapperView = memo(function GoldMapperView() {
<div className="mb-3"> <div className="mb-3">
<h3 className="text-[#333333] mc-text-shadow uppercase tracking-widest text-sm px-3 pt-2 pb-1"> <h3 className="text-[#333333] mc-text-shadow uppercase tracking-widest text-sm px-3 pt-2 pb-1">
Keyboard {t("goldMapper.keyboard")}
</h3> </h3>
<input <input
data-modal-index={MOUSE_IDS.length + controllerIds.length} data-modal-index={MOUSE_IDS.length + controllerIds.length}
@@ -518,7 +520,7 @@ const GoldMapperView = memo(function GoldMapperView() {
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") submitKeyInput(); if (e.key === "Enter") submitKeyInput();
}} }}
placeholder="Type a key name and press Enter" placeholder={t("goldMapper.typeKeyName")}
className={`w-full h-10 px-3 bg-black/40 border-2 text-white text-base outline-none text-center ${ className={`w-full h-10 px-3 bg-black/40 border-2 text-white text-base outline-none text-center ${
keyInputError keyInputError
? "border-red-600" ? "border-red-600"
@@ -558,7 +560,7 @@ const GoldMapperView = memo(function GoldMapperView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Cancel {t("goldMapper.cancel")}
</button> </button>
</div> </div>
</div> </div>
+27 -24
View File
@@ -1,9 +1,11 @@
import { useState, useRef } from "react"; import { useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; import { useUI, useAudio, useConfig } from "../../context/LauncherContext";
import { GrfService } from "../../services/GrfService"; import { GrfService } from "../../services/GrfService";
import { GrfFile, GrfNode, GrfFileEntry } from "../../types/grf"; import { GrfFile, GrfNode, GrfFileEntry } from "../../types/grf";
export default function GrfEditorView() { export default function GrfEditorView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { animationsEnabled } = useConfig(); const { animationsEnabled } = useConfig();
@@ -27,10 +29,10 @@ export default function GrfEditorView() {
const parsedGrf = GrfService.readGRF(buffer); const parsedGrf = GrfService.readGRF(buffer);
setGrf(parsedGrf); setGrf(parsedGrf);
setFilename(file.name); setFilename(file.name);
showNotification(`Loaded ${file.name}`); showNotification(t("grfEditor.loaded", { name: file.name }));
} catch (err: unknown) { } catch (err: unknown) {
console.error("Failed to parse GRF", err); console.error("Failed to parse GRF", err);
showNotification(err instanceof Error ? err.message : "Failed to parse GRF", "error"); showNotification(err instanceof Error ? err.message : t("grfEditor.failedToParse"), "error");
} }
e.target.value = ""; e.target.value = "";
}; };
@@ -39,7 +41,7 @@ export default function GrfEditorView() {
playPressSound(); playPressSound();
setGrf(GrfService.createDefaultGRF()); setGrf(GrfService.createDefaultGRF());
setFilename("new_rules.grf"); setFilename("new_rules.grf");
showNotification("New GRF Created"); showNotification(t("grfEditor.newGrfCreated"));
}; };
const handleAddFile = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleAddFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -55,7 +57,7 @@ export default function GrfEditorView() {
...grf, ...grf,
files: [...grf.files, newFile] files: [...grf.files, newFile]
}); });
showNotification(`Added ${file.name}`); showNotification(t("grfEditor.added", { name: file.name }));
e.target.value = ""; e.target.value = "";
}; };
@@ -65,7 +67,7 @@ export default function GrfEditorView() {
const newFiles = [...grf.files]; const newFiles = [...grf.files];
const removed = newFiles.splice(index, 1)[0]; const removed = newFiles.splice(index, 1)[0];
setGrf({ ...grf, files: newFiles }); setGrf({ ...grf, files: newFiles });
showNotification(`Removed ${removed.filename}`); showNotification(t("grfEditor.removed", { name: removed.filename }));
}; };
const handleUpdateParameter = (nodePath: string[], paramIndex: number, value: string) => { const handleUpdateParameter = (nodePath: string[], paramIndex: number, value: string) => {
@@ -101,10 +103,10 @@ export default function GrfEditorView() {
a.download = filename; a.download = filename;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
showNotification("GRF Saved Successfully"); showNotification(t("grfEditor.savedSuccessfully"));
} catch (err: unknown) { } catch (err: unknown) {
console.error("Failed to save GRF", err); console.error("Failed to save GRF", err);
showNotification(err instanceof Error ? err.message : "Failed to save GRF", "error"); showNotification(err instanceof Error ? err.message : t("grfEditor.failedToSave"), "error");
} }
}; };
@@ -120,8 +122,8 @@ export default function GrfEditorView() {
<input type="file" ref={addFileInputRef} onChange={handleAddFile} className="hidden" /> <input type="file" ref={addFileInputRef} onChange={handleAddFile} className="hidden" />
<div className="flex items-center justify-between mb-6 px-4"> <div className="flex items-center justify-between mb-6 px-4">
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<h2 className="text-3xl text-white mc-text-shadow tracking-widest uppercase font-bold">GRF Editor</h2> <h2 className="text-3xl text-white mc-text-shadow tracking-widest uppercase font-bold">{t("grfEditor.title")}</h2>
{grf && <span className="text-white/40 mc-text-shadow italic">editing: <span className="text-[#FFFF55]">{filename}</span></span>} {grf && <span className="text-white/40 mc-text-shadow italic">{t("grfEditor.editing")} <span className="text-[#FFFF55]">{filename}</span></span>}
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
@@ -129,14 +131,14 @@ export default function GrfEditorView() {
className="px-6 py-2 text-white mc-text-shadow text-lg" className="px-6 py-2 text-white mc-text-shadow text-lg"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
New GRF {t("grfEditor.newGrf")}
</button> </button>
<button <button
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
className="px-6 py-2 text-white mc-text-shadow text-lg" className="px-6 py-2 text-white mc-text-shadow text-lg"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Open GRF {t("grfEditor.openGrf")}
</button> </button>
<button <button
onClick={handleSaveGrf} onClick={handleSaveGrf}
@@ -144,7 +146,7 @@ export default function GrfEditorView() {
className={`px-6 py-2 text-white mc-text-shadow text-lg ${!grf ? "opacity-50 grayscale" : ""}`} className={`px-6 py-2 text-white mc-text-shadow text-lg ${!grf ? "opacity-50 grayscale" : ""}`}
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Save GRF {t("grfEditor.saveGrf")}
</button> </button>
</div> </div>
</div> </div>
@@ -152,7 +154,7 @@ export default function GrfEditorView() {
{!grf ? ( {!grf ? (
<div className="flex-1 w-full flex flex-col items-center justify-center p-12" <div className="flex-1 w-full flex flex-col items-center justify-center p-12"
style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
<h3 className="text-2xl text-white/40 mc-text-shadow italic">Open a GRF file to begin editing</h3> <h3 className="text-2xl text-white/40 mc-text-shadow italic">{t("grfEditor.openToBegin")}</h3>
</div> </div>
) : ( ) : (
<div className="flex-1 w-full flex flex-col overflow-hidden" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> <div className="flex-1 w-full flex flex-col overflow-hidden" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
@@ -161,13 +163,13 @@ export default function GrfEditorView() {
onClick={() => { playPressSound(); setActiveTab("rules"); }} onClick={() => { playPressSound(); setActiveTab("rules"); }}
className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === "rules" ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"}`} className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === "rules" ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"}`}
> >
<span className="text-lg">Game Rules</span> <span className="text-lg">{t("grfEditor.gameRules")}</span>
</button> </button>
<button <button
onClick={() => { playPressSound(); setActiveTab("files"); }} onClick={() => { playPressSound(); setActiveTab("files"); }}
className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === "files" ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"}`} className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === "files" ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"}`}
> >
<span className="text-lg">Files ({grf.files.length})</span> <span className="text-lg">{t("grfEditor.files", { count: grf.files.length })}</span>
</button> </button>
</div> </div>
@@ -177,7 +179,7 @@ export default function GrfEditorView() {
{grf.root.children.map((node, i) => ( {grf.root.children.map((node, i) => (
<GrfNodeView key={i} node={node} level={0} path={[]} onUpdate={handleUpdateParameter} /> <GrfNodeView key={i} node={node} level={0} path={[]} onUpdate={handleUpdateParameter} />
))} ))}
{grf.root.children.length === 0 && <span className="text-white/40 italic">No rules found</span>} {grf.root.children.length === 0 && <span className="text-white/40 italic">{t("grfEditor.noRulesFound")}</span>}
</div> </div>
)} )}
{activeTab === "files" && ( {activeTab === "files" && (
@@ -187,19 +189,19 @@ export default function GrfEditorView() {
className="self-start px-6 py-2 text-white mc-text-shadow text-sm" className="self-start px-6 py-2 text-white mc-text-shadow text-sm"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Add File {t("grfEditor.addFile")}
</button> </button>
<table className="w-full text-left border-collapse"> <table className="w-full text-left border-collapse">
<thead className="bg-[#252525]"> <thead className="bg-[#252525]">
<tr className="border-b-2 border-[#373737]"> <tr className="border-b-2 border-[#373737]">
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">Filename</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">{t("grfEditor.filename")}</th>
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold text-right">Size</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold text-right">{t("grfEditor.size")}</th>
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold text-center">Actions</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold text-center">{t("grfEditor.actions")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{grf.files.length === 0 && ( {grf.files.length === 0 && (
<tr><td colSpan={3} className="p-4 text-center text-white/40">No files in GRF</td></tr> <tr><td colSpan={3} className="p-4 text-center text-white/40">{t("grfEditor.noFilesInGrf")}</td></tr>
)} )}
{grf.files.map((f, i) => ( {grf.files.map((f, i) => (
<tr key={i} className="border-b border-[#373737]/30"> <tr key={i} className="border-b border-[#373737]/30">
@@ -210,7 +212,7 @@ export default function GrfEditorView() {
onClick={() => handleDeleteFile(i)} onClick={() => handleDeleteFile(i)}
className="text-[#FF5555] hover:text-[#FF8888] transition-colors" className="text-[#FF5555] hover:text-[#FF8888] transition-colors"
> >
Delete {t("grfEditor.delete")}
</button> </button>
</td> </td>
</tr> </tr>
@@ -229,7 +231,7 @@ export default function GrfEditorView() {
className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white" className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}
> >
Back {t("grfEditor.back")}
</button> </button>
</div> </div>
@@ -253,6 +255,7 @@ export default function GrfEditorView() {
} }
function GrfNodeView({ node, level, path, onUpdate }: { node: GrfNode, level: number, path: string[], onUpdate: (path: string[], paramIdx: number, val: string) => void }) { function GrfNodeView({ node, level, path, onUpdate }: { node: GrfNode, level: number, path: string[], onUpdate: (path: string[], paramIdx: number, val: string) => void }) {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(level < 1); const [expanded, setExpanded] = useState(level < 1);
const currentPath = [...path, node.name]; const currentPath = [...path, node.name];
@@ -279,7 +282,7 @@ function GrfNodeView({ node, level, path, onUpdate }: { node: GrfNode, level: nu
onError={(e) => (e.currentTarget.src = "/images/tools/pck.png")} onError={(e) => (e.currentTarget.src = "/images/tools/pck.png")}
/> />
<span className="text-[#FFFF55] font-bold">{node.name}</span> <span className="text-[#FFFF55] font-bold">{node.name}</span>
{node.parameters.length > 0 && <span className="text-white/40 text-xs ml-2">[{node.parameters.length} props]</span>} {node.parameters.length > 0 && <span className="text-white/40 text-xs ml-2">{t("grfEditor.props", { count: node.parameters.length })}</span>}
</div> </div>
{expanded && ( {expanded && (
<div className="flex flex-col border-l-2 border-[#373737] ml-2 pl-2"> <div className="flex flex-col border-l-2 border-[#373737] ml-2 pl-2">
+9 -7
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useCallback } from "react"; import { useState, useEffect, useRef, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@@ -16,6 +17,7 @@ interface GuideEntry {
} }
export default function GuidesView() { export default function GuidesView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { animationsEnabled } = useConfig(); const { animationsEnabled } = useConfig();
@@ -130,7 +132,7 @@ export default function GuidesView() {
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<span className="text-white mc-text-shadow text-lg"> <span className="text-white mc-text-shadow text-lg">
Loading guide... {t("guides.loadingGuide")}
</span> </span>
</div> </div>
); );
@@ -145,7 +147,7 @@ export default function GuidesView() {
}} }}
className="text-[#FFFF55] mc-text-shadow text-sm hover:underline mb-2 block" className="text-[#FFFF55] mc-text-shadow text-sm hover:underline mb-2 block"
> >
&larr; Back to guides {t("guides.backToGuides")}
</button> </button>
<h2 className="text-2xl text-white mc-text-shadow font-bold border-b border-[#373737] pb-2"> <h2 className="text-2xl text-white mc-text-shadow font-bold border-b border-[#373737] pb-2">
{selectedGuide.name.replace(/\.md$/i, "")} {selectedGuide.name.replace(/\.md$/i, "")}
@@ -189,7 +191,7 @@ export default function GuidesView() {
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[60%] max-w-75 text-center tracking-widest uppercase opacity-80 font-bold"> <h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[60%] max-w-75 text-center tracking-widest uppercase opacity-80 font-bold">
{selectedGuide {selectedGuide
? selectedGuide.name.replace(/\.md$/i, "") ? selectedGuide.name.replace(/\.md$/i, "")
: "Community Guides"} : t("guides.communityGuides")}
</h2> </h2>
<div <div
@@ -205,13 +207,13 @@ export default function GuidesView() {
) : loading ? ( ) : loading ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<span className="text-white mc-text-shadow text-lg"> <span className="text-white mc-text-shadow text-lg">
Loading guides... {t("guides.loadingGuides")}
</span> </span>
</div> </div>
) : error ? ( ) : error ? (
<div className="flex flex-col items-center justify-center h-full gap-4"> <div className="flex flex-col items-center justify-center h-full gap-4">
<span className="text-red-400 mc-text-shadow text-lg"> <span className="text-red-400 mc-text-shadow text-lg">
Failed to load guides {t("guides.failedToLoadGuides")}
</span> </span>
<span className="text-[#AAAAAA] mc-text-shadow text-sm text-center px-8"> <span className="text-[#AAAAAA] mc-text-shadow text-sm text-center px-8">
{error} {error}
@@ -220,7 +222,7 @@ export default function GuidesView() {
) : guides.length === 0 ? ( ) : guides.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full gap-4"> <div className="flex flex-col items-center justify-center h-full gap-4">
<span className="text-[#AAAAAA] mc-text-shadow text-lg"> <span className="text-[#AAAAAA] mc-text-shadow text-lg">
No guides found {t("guides.noGuidesFound")}
</span> </span>
</div> </div>
) : ( ) : (
@@ -277,7 +279,7 @@ export default function GuidesView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Back {t("common.back")}
</button> </button>
</motion.div> </motion.div>
); );
+12 -9
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useMemo, memo } from "react"; import { useState, useEffect, useMemo, memo } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { import {
useUI, useUI,
@@ -11,6 +12,7 @@ import { usePlatform } from "../../hooks/usePlatform";
import type { Edition } from "../../types/edition"; import type { Edition } from "../../types/edition";
const HomeView = memo(function HomeView() { const HomeView = memo(function HomeView() {
const { t } = useTranslation();
const { setActiveView, focusSection, onNavigateToSkin } = useUI(); const { setActiveView, focusSection, onNavigateToSkin } = useUI();
const { profile, legacyMode } = useConfig(); const { profile, legacyMode } = useConfig();
const { playPressSound } = useAudio(); const { playPressSound } = useAudio();
@@ -39,14 +41,14 @@ const HomeView = memo(function HomeView() {
const buttonsVal = useMemo(() => { const buttonsVal = useMemo(() => {
const mainBtn = { const mainBtn = {
label: !hasAnyInstall label: !hasAnyInstall
? "Install a version" ? t("home.installVersion")
: isGameRunning : isGameRunning
? "Stop Game" ? t("home.stopGame")
: isDownloading : isDownloading
? "Installation in progress..." ? t("home.installing")
: isInstalled : isInstalled
? "Play Game" ? t("home.playGame")
: `Download ${selectedVersionName}`, : t("home.downloadVersion", { name: selectedVersionName }),
action: !hasAnyInstall action: !hasAnyInstall
? () => setActiveView("versions") ? () => setActiveView("versions")
: isGameRunning : isGameRunning
@@ -71,28 +73,28 @@ const HomeView = memo(function HomeView() {
const menuBtns = [ const menuBtns = [
{ {
label: "Help & Options", label: t("home.helpAndOptions"),
action: () => setActiveView("settings"), action: () => setActiveView("settings"),
isDanger: false, isDanger: false,
disabled: false, disabled: false,
id: "settings", id: "settings",
}, },
{ {
label: "Versions", label: t("home.versions"),
action: () => setActiveView("versions"), action: () => setActiveView("versions"),
isDanger: false, isDanger: false,
disabled: false, disabled: false,
id: "versions", id: "versions",
}, },
{ {
label: "Workshop", label: t("home.workshop"),
action: () => setActiveView("workshop"), action: () => setActiveView("workshop"),
isDanger: false, isDanger: false,
disabled: false, disabled: false,
id: "workshop", id: "workshop",
}, },
{ {
label: "Developer Tools", label: t("home.developerTools"),
action: () => setActiveView("devtools"), action: () => setActiveView("devtools"),
isDanger: false, isDanger: false,
disabled: false, disabled: false,
@@ -102,6 +104,7 @@ const HomeView = memo(function HomeView() {
return [mainBtn, ...pluginBtns, ...menuBtns]; return [mainBtn, ...pluginBtns, ...menuBtns];
}, [ }, [
t,
isDownloading, isDownloading,
hasAnyInstall, hasAnyInstall,
isInstalled, isInstalled,
+41 -34
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useMemo, memo } from "react"; import { useState, useEffect, useRef, useMemo, memo } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { import {
useUI, useUI,
@@ -26,6 +27,7 @@ const LceOnlineView = memo(function LceOnlineView({
onClearAddFriendTarget, onClearAddFriendTarget,
invites: invitesProp, invites: invitesProp,
}: LceOnlineViewProps) { }: LceOnlineViewProps) {
const { t } = useTranslation();
const { setActiveView, setIsUiHidden } = useUI(); const { setActiveView, setIsUiHidden } = useUI();
const { animationsEnabled } = useConfig(); const { animationsEnabled } = useConfig();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
@@ -140,7 +142,7 @@ const LceOnlineView = memo(function LceOnlineView({
TauriService.startHostRelay(token, 25565).catch(() => {}); TauriService.startHostRelay(token, 25565).catch(() => {});
lceOnlineService.isHosting = true; lceOnlineService.isHosting = true;
} catch (e: unknown) { } catch (e: unknown) {
setErrorModal(e instanceof Error ? e.message : "Failed to start hosting"); setErrorModal(e instanceof Error ? e.message : t("lceOnline.failedToStartHosting"));
} }
}; };
@@ -160,7 +162,7 @@ const LceOnlineView = memo(function LceOnlineView({
await action(); await action();
fetchSocialData(); fetchSocialData();
} catch (e: unknown) { } catch (e: unknown) {
setErrorModal(e instanceof Error ? e.message : "An error occurred"); setErrorModal(e instanceof Error ? e.message : t("lceOnline.anErrorOccurred"));
} }
}; };
@@ -179,21 +181,21 @@ const LceOnlineView = memo(function LceOnlineView({
items.push({ items.push({
id: "host_game", id: "host_game",
type: "button", type: "button",
label: "Host Game", label: t("lceOnline.hostGame"),
onClick: handleStartHosting, onClick: handleStartHosting,
}); });
} else { } else {
items.push({ items.push({
id: "stop_hosting", id: "stop_hosting",
type: "button", type: "button",
label: "Stop Hosting", label: t("lceOnline.stopHosting"),
onClick: handleStopHosting, onClick: handleStopHosting,
}); });
} }
items.push({ items.push({
id: "add_friend", id: "add_friend",
type: "button", type: "button",
label: "Add Friend", label: t("lceOnline.addFriend"),
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
setIsAddingFriend(true); setIsAddingFriend(true);
@@ -203,7 +205,7 @@ const LceOnlineView = memo(function LceOnlineView({
items.push({ items.push({
id: "sign_out", id: "sign_out",
type: "button", type: "button",
label: "Sign Out", label: t("lceOnline.signOut"),
onClick: handleLogout, onClick: handleLogout,
}); });
friends.forEach((f) => { friends.forEach((f) => {
@@ -272,6 +274,7 @@ const LceOnlineView = memo(function LceOnlineView({
invites, invites,
playPressSound, playPressSound,
isHosting, isHosting,
t,
]); ]);
const tabs: ("friends" | "requests" | "invites")[] = [ const tabs: ("friends" | "requests" | "invites")[] = [
@@ -511,7 +514,7 @@ const LceOnlineView = memo(function LceOnlineView({
/> />
</h2> </h2>
<p className="text-white text-lg mc-text-shadow mb-8 max-w-sm"> <p className="text-white text-lg mc-text-shadow mb-8 max-w-sm">
Awaiting authentication... {t("lceOnline.awaitingAuthentication")}
</p> </p>
</div> </div>
); );
@@ -552,10 +555,10 @@ const LceOnlineView = memo(function LceOnlineView({
<div className="bg-black/10 px-4 py-3 text-[#2a2a2a] font-bold tracking-widest uppercase border-b-4 border-[#222] flex justify-between shadow-sm z-10"> <div className="bg-black/10 px-4 py-3 text-[#2a2a2a] font-bold tracking-widest uppercase border-b-4 border-[#222] flex justify-between shadow-sm z-10">
<span> <span>
{currentTab === "friends" {currentTab === "friends"
? "Friends" ? t("lceOnline.friends")
: currentTab === "invites" : currentTab === "invites"
? "Invites" ? t("lceOnline.invites")
: "Pending Requests"} : t("lceOnline.pendingRequests")}
</span> </span>
<span className="text-[#111]">{listItems.length}</span> <span className="text-[#111]">{listItems.length}</span>
</div> </div>
@@ -563,7 +566,7 @@ const LceOnlineView = memo(function LceOnlineView({
<div ref={scrollRef} className="flex-1 overflow-y-auto w-full"> <div ref={scrollRef} className="flex-1 overflow-y-auto w-full">
{listItems.length === 0 ? ( {listItems.length === 0 ? (
<div className="flex items-center justify-center h-[200px] text-[#555] font-bold"> <div className="flex items-center justify-center h-[200px] text-[#555] font-bold">
None available {t("lceOnline.noneAvailable")}
</div> </div>
) : ( ) : (
<div className="flex flex-col p-2 space-y-2"> <div className="flex flex-col p-2 space-y-2">
@@ -591,7 +594,7 @@ const LceOnlineView = memo(function LceOnlineView({
? incomingReqs.find((r) => `req_in_${r.username}` === item.id)?.username ? incomingReqs.find((r) => `req_in_${r.username}` === item.id)?.username
: item.type === "request_out" : item.type === "request_out"
? outgoingReqs.find((r) => `req_out_${r.username}` === item.id)?.username ? outgoingReqs.find((r) => `req_out_${r.username}` === item.id)?.username
: "Invite"} : t("lceOnline.invite")}
</span> </span>
</div> </div>
</div> </div>
@@ -612,7 +615,7 @@ const LceOnlineView = memo(function LceOnlineView({
item.onClickSecondary?.(); item.onClickSecondary?.();
}} }}
> >
INVITE {t("lceOnline.invite").toUpperCase()}
</button> </button>
)} )}
<button <button
@@ -625,7 +628,7 @@ const LceOnlineView = memo(function LceOnlineView({
}} }}
onClick={item.onClick} onClick={item.onClick}
> >
REMOVE {t("lceOnline.remove").toUpperCase()}
</button> </button>
</> </>
)} )}
@@ -640,7 +643,7 @@ const LceOnlineView = memo(function LceOnlineView({
}} }}
onClick={item.onClick} onClick={item.onClick}
> >
CANCEL {t("lceOnline.cancel").toUpperCase()}
</button> </button>
)} )}
{item.type === "invite" && ( {item.type === "invite" && (
@@ -655,7 +658,7 @@ const LceOnlineView = memo(function LceOnlineView({
}} }}
onClick={item.onClick} onClick={item.onClick}
> >
ACCEPT {t("lceOnline.accept").toUpperCase()}
</button> </button>
<button <button
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`} className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
@@ -670,7 +673,7 @@ const LceOnlineView = memo(function LceOnlineView({
item.onClickSecondary?.(); item.onClickSecondary?.();
}} }}
> >
DECLINE {t("lceOnline.decline").toUpperCase()}
</button> </button>
</> </>
)} )}
@@ -686,7 +689,7 @@ const LceOnlineView = memo(function LceOnlineView({
}} }}
onClick={item.onClick} onClick={item.onClick}
> >
ACCEPT {t("lceOnline.accept").toUpperCase()}
</button> </button>
<button <button
className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`} className={`px-6 h-12 flex items-center justify-center font-bold text-base outline-none uppercase tracking-widest mc-text-shadow ${isFocused ? "text-white shadow-md" : "text-gray-300"}`}
@@ -701,7 +704,7 @@ const LceOnlineView = memo(function LceOnlineView({
item.onClickSecondary?.(); item.onClickSecondary?.();
}} }}
> >
DECLINE {t("lceOnline.decline").toUpperCase()}
</button> </button>
</> </>
)} )}
@@ -733,10 +736,10 @@ const LceOnlineView = memo(function LceOnlineView({
className="flex z-10 space-x-2 px-12 relative w-full items-end" className="flex z-10 space-x-2 px-12 relative w-full items-end"
style={{ marginBottom: "-4px" }} style={{ marginBottom: "-4px" }}
> >
{tabs.map((t) => ( {tabs.map((tab) => (
<button <button
key={t} key={tab}
className={`flex-1 font-bold text-xl outline-none uppercase transition-all duration-200 ease-in-out ${currentTab === t ? "text-[#2a2a2a] z-20 pb-6 pt-5 text-2xl drop-shadow-[5px_-5px_15px_rgba(0,0,0,0.3)] rounded-t border-4 border-[#222] border-b-0" : "text-[#555] mt-2 py-4 hover:bg-black/30 bg-black/10 hover:text-[#222] border-4 border-transparent border-b-0"}`} className={`flex-1 font-bold text-xl outline-none uppercase transition-all duration-200 ease-in-out ${currentTab === tab ? "text-[#2a2a2a] z-20 pb-6 pt-5 text-2xl drop-shadow-[5px_-5px_15px_rgba(0,0,0,0.3)] rounded-t border-4 border-[#222] border-b-0" : "text-[#555] mt-2 py-4 hover:bg-black/30 bg-black/10 hover:text-[#222] border-4 border-transparent border-b-0"}`}
style={{ style={{
backgroundImage: "url('/images/background.png')", backgroundImage: "url('/images/background.png')",
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
@@ -745,23 +748,27 @@ const LceOnlineView = memo(function LceOnlineView({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
onClick={() => { onClick={() => {
setCurrentTab(t); setCurrentTab(tab);
setFocusIndex(0); setFocusIndex(0);
playPressSound(); playPressSound();
}} }}
> >
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
{t} {tab === "friends"
{t === "requests" && incomingReqs.length > 0 && ( ? t("lceOnline.friends")
: tab === "requests"
? t("lceOnline.requests")
: t("lceOnline.invites")}
{tab === "requests" && incomingReqs.length > 0 && (
<span <span
className={`ml-3 text-white text-base px-3 py-1 rounded-full shadow-inner border-2 font-normal ${currentTab === t ? "bg-[#d72f2f] border-[#8a1a1a]" : "bg-[#a81f1f] border-[#111]"}`} className={`ml-3 text-white text-base px-3 py-1 rounded-full shadow-inner border-2 font-normal ${currentTab === tab ? "bg-[#d72f2f] border-[#8a1a1a]" : "bg-[#a81f1f] border-[#111]"}`}
> >
{incomingReqs.length} {incomingReqs.length}
</span> </span>
)} )}
{t === "invites" && invites.length > 0 && ( {tab === "invites" && invites.length > 0 && (
<span <span
className={`ml-3 text-white text-base px-3 py-1 rounded-full shadow-inner border-2 font-normal ${currentTab === t ? "bg-[#d72f2f] border-[#8a1a1a]" : "bg-[#a81f1f] border-[#111]"}`} className={`ml-3 text-white text-base px-3 py-1 rounded-full shadow-inner border-2 font-normal ${currentTab === tab ? "bg-[#d72f2f] border-[#8a1a1a]" : "bg-[#a81f1f] border-[#111]"}`}
> >
{invites.length} {invites.length}
</span> </span>
@@ -811,13 +818,13 @@ const LceOnlineView = memo(function LceOnlineView({
}} }}
> >
<h2 className="text-[#FFFF55] text-3xl mc-text-shadow mb-6 border-b-2 border-[#373737] pb-2 w-full text-center uppercase tracking-widest"> <h2 className="text-[#FFFF55] text-3xl mc-text-shadow mb-6 border-b-2 border-[#373737] pb-2 w-full text-center uppercase tracking-widest">
Add Friend {t("lceOnline.addFriend")}
</h2> </h2>
<input <input
ref={addFriendInputRef} ref={addFriendInputRef}
type="text" type="text"
className="bg-black/20 border-4 border-[#555] text-white p-4 w-full text-2xl font-bold outline-none focus:border-[#FFFF55] transition-colors placeholder:text-[#888] mb-6 mc-text-shadow" className="bg-black/20 border-4 border-[#555] text-white p-4 w-full text-2xl font-bold outline-none focus:border-[#FFFF55] transition-colors placeholder:text-[#888] mb-6 mc-text-shadow"
placeholder="Username" placeholder={t("lceOnline.username")}
value={addFriendUsername} value={addFriendUsername}
onChange={(e) => setAddFriendUsername(e.target.value)} onChange={(e) => setAddFriendUsername(e.target.value)}
/> />
@@ -841,7 +848,7 @@ const LceOnlineView = memo(function LceOnlineView({
} }
}} }}
> >
Send {t("lceOnline.send")}
</button> </button>
<button <button
className="h-12 flex-1 flex items-center justify-center text-white mc-text-shadow text-xl font-bold uppercase tracking-widest hover:text-[#FFFF55] outline-none border-none" className="h-12 flex-1 flex items-center justify-center text-white mc-text-shadow text-xl font-bold uppercase tracking-widest hover:text-[#FFFF55] outline-none border-none"
@@ -855,7 +862,7 @@ const LceOnlineView = memo(function LceOnlineView({
playBackSound(); playBackSound();
}} }}
> >
Cancel {t("lceOnline.cancel")}
</button> </button>
</div> </div>
</div> </div>
@@ -879,7 +886,7 @@ const LceOnlineView = memo(function LceOnlineView({
}} }}
> >
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase tracking-widest"> <h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase tracking-widest">
Error {t("lceOnline.error")}
</h2> </h2>
<p className="text-white text-lg mc-text-shadow text-center mb-6"> <p className="text-white text-lg mc-text-shadow text-center mb-6">
{errorModal} {errorModal}
@@ -893,7 +900,7 @@ const LceOnlineView = memo(function LceOnlineView({
}} }}
onClick={() => setErrorModal(null)} onClick={() => setErrorModal(null)}
> >
OK {t("lceOnline.ok")}
</button> </button>
</div> </div>
</motion.div> </motion.div>
+27 -25
View File
@@ -1,9 +1,11 @@
import { useState, useRef, useMemo } from "react"; import { useState, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; import { useUI, useAudio, useConfig } from "../../context/LauncherContext";
import { ArcService } from "../../services/ArcService"; import { ArcService } from "../../services/ArcService";
import { LocFile, LocLanguage } from "../../types/arc"; import { LocFile, LocLanguage } from "../../types/arc";
export default function LocEditorView() { export default function LocEditorView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { animationsEnabled } = useConfig(); const { animationsEnabled } = useConfig();
@@ -38,10 +40,10 @@ export default function LocEditorView() {
const parsedLoc = ArcService.parseLOC(new Uint8Array(buffer)); const parsedLoc = ArcService.parseLOC(new Uint8Array(buffer));
setLoc(parsedLoc); setLoc(parsedLoc);
setSelectedLocLangIdx(0); setSelectedLocLangIdx(0);
showNotification(`Loaded ${file.name}`); showNotification(t("locEditor.loaded", { name: file.name }));
} catch (err) { } catch (err) {
console.error("Failed to parse LOC", err); console.error("Failed to parse LOC", err);
showNotification("Failed to parse LOC", "error"); showNotification(t("locEditor.failedToParse"), "error");
} }
e.target.value = ""; e.target.value = "";
}; };
@@ -57,7 +59,7 @@ export default function LocEditorView() {
a.download = "languages.loc"; a.download = "languages.loc";
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
showNotification("LOC Saved Successfully"); showNotification(t("locEditor.savedSuccessfully"));
}; };
const handleLocStringEdit = (langIdx: number, strIdx: number, isNew: boolean, key: string, value: string) => { const handleLocStringEdit = (langIdx: number, strIdx: number, isNew: boolean, key: string, value: string) => {
@@ -82,7 +84,7 @@ export default function LocEditorView() {
}) })
}); });
setIsLocEditModalOpen(null); setIsLocEditModalOpen(null);
showNotification(isNew ? "String Added" : "String Updated"); showNotification(isNew ? t("locEditor.stringAdded") : t("locEditor.stringUpdated"));
}; };
const handleLocStringDelete = (langIdx: number, strIdx: number) => { const handleLocStringDelete = (langIdx: number, strIdx: number) => {
@@ -94,7 +96,7 @@ export default function LocEditorView() {
lIdx !== langIdx ? lang : { ...lang, strings: lang.strings.filter((_, sIdx) => sIdx !== strIdx) } lIdx !== langIdx ? lang : { ...lang, strings: lang.strings.filter((_, sIdx) => sIdx !== strIdx) }
) )
}); });
showNotification("String Deleted"); showNotification(t("locEditor.stringDeleted"));
}; };
return ( return (
@@ -108,8 +110,8 @@ export default function LocEditorView() {
<input type="file" ref={fileInputRef} onChange={handleFileLoad} className="hidden" accept=".loc" /> <input type="file" ref={fileInputRef} onChange={handleFileLoad} className="hidden" accept=".loc" />
<div className="flex items-center justify-between mb-6 px-4"> <div className="flex items-center justify-between mb-6 px-4">
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<h2 className="text-3xl text-white mc-text-shadow tracking-widest uppercase font-bold">LOC Editor</h2> <h2 className="text-3xl text-white mc-text-shadow tracking-widest uppercase font-bold">{t("locEditor.title")}</h2>
{loc && <span className="text-white/40 mc-text-shadow italic">editing loaded loc</span>} {loc && <span className="text-white/40 mc-text-shadow italic">{t("locEditor.editingLoadedLoc")}</span>}
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
@@ -117,7 +119,7 @@ export default function LocEditorView() {
className="px-6 py-2 text-white mc-text-shadow text-lg" className="px-6 py-2 text-white mc-text-shadow text-lg"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Open LOC {t("locEditor.openLoc")}
</button> </button>
<button <button
onClick={handleSaveLoc} onClick={handleSaveLoc}
@@ -125,16 +127,15 @@ export default function LocEditorView() {
className={`px-6 py-2 text-white mc-text-shadow text-lg ${!loc ? "opacity-50 grayscale" : ""}`} className={`px-6 py-2 text-white mc-text-shadow text-lg ${!loc ? "opacity-50 grayscale" : ""}`}
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Save LOC {t("locEditor.saveLoc")}
</button> </button>
</div> </div>
</div> </div>
{!loc ? ( {!loc ? (
<div className="flex-1 w-full flex flex-col items-center justify-center p-12" <div className="flex-1 w-full flex flex-col items-center justify-center p-12"
style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
<img src="/images/tools/loc.png" className="w-32 h-32 mb-8 opacity-20 grayscale" style={{ imageRendering: "pixelated" }} /> <img src="/images/tools/loc.png" className="w-32 h-32 mb-8 opacity-20 grayscale" style={{ imageRendering: "pixelated" }} />
<h3 className="text-2xl text-white/40 mc-text-shadow italic">Open a LOC file to begin editing</h3> <h3 className="text-2xl text-white/40 mc-text-shadow italic">{t("locEditor.openToBegin")}</h3>
</div> </div>
) : ( ) : (
<div className="flex-1 w-full flex flex-col overflow-hidden" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> <div className="flex-1 w-full flex flex-col overflow-hidden" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
@@ -146,12 +147,12 @@ export default function LocEditorView() {
className="bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors" className="bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors"
> >
{loc.languages.map((lang, idx) => ( {loc.languages.map((lang, idx) => (
<option key={idx} value={idx}>{lang.id} {lang.isStatic ? "[Static]" : "[Keyed]"}</option> <option key={idx} value={idx}>{lang.id} {lang.isStatic ? `[${t("locEditor.static")}]` : `[${t("locEditor.keyed")}]`}</option>
))} ))}
</select> </select>
<input <input
type="text" type="text"
placeholder="Search strings..." placeholder={t("locEditor.searchStrings")}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors" className="flex-1 bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors"
@@ -161,16 +162,16 @@ export default function LocEditorView() {
className="px-6 py-2 text-white mc-text-shadow text-sm" className="px-6 py-2 text-white mc-text-shadow text-sm"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Add String {t("locEditor.addString")}
</button> </button>
</div> </div>
<div className="flex-1 overflow-y-auto pr-2 custom-scrollbar"> <div className="flex-1 overflow-y-auto pr-2 custom-scrollbar">
<table className="w-full text-left border-collapse"> <table className="w-full text-left border-collapse">
<thead className="sticky top-0 bg-[#252525] z-10"> <thead className="sticky top-0 bg-[#252525] z-10">
<tr className="border-b-2 border-[#373737]"> <tr className="border-b-2 border-[#373737]">
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">Key / Index</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">{t("locEditor.keyOrIndex")}</th>
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">Value</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold">{t("locEditor.value")}</th>
<th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold text-right">Actions</th> <th className="p-3 text-white/40 uppercase text-xs tracking-widest font-bold text-right">{t("locEditor.actions")}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -186,7 +187,7 @@ export default function LocEditorView() {
onClick={() => setIsLocEditModalOpen({ langIdx: selectedLocLangIdx, strIdx: str.originalIdx, isNew: false })} onClick={() => setIsLocEditModalOpen({ langIdx: selectedLocLangIdx, strIdx: str.originalIdx, isNew: false })}
className="px-2 py-1 text-[10px] bg-white/10 hover:bg-[#FFFF55]/20 hover:text-[#FFFF55] border border-white/20 uppercase" className="px-2 py-1 text-[10px] bg-white/10 hover:bg-[#FFFF55]/20 hover:text-[#FFFF55] border border-white/20 uppercase"
> >
Edit {t("locEditor.edit")}
</button> </button>
<button onClick={() => handleLocStringDelete(selectedLocLangIdx, str.originalIdx)} className="p-1 hover:text-red-500 opacity-60"> <button onClick={() => handleLocStringDelete(selectedLocLangIdx, str.originalIdx)} className="p-1 hover:text-red-500 opacity-60">
<img src="/images/Trash_Bin_Icon.png" className="w-4 h-4 object-contain" style={{ imageRendering: "pixelated" }} /> <img src="/images/Trash_Bin_Icon.png" className="w-4 h-4 object-contain" style={{ imageRendering: "pixelated" }} />
@@ -207,7 +208,7 @@ export default function LocEditorView() {
className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white" className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}
> >
Back {t("locEditor.back")}
</button> </button>
</div> </div>
<AnimatePresence> <AnimatePresence>
@@ -238,16 +239,17 @@ export default function LocEditorView() {
} }
function LocEditModal({ data, lang, onClose, onConfirm }: { data: { langIdx: number, strIdx: number, isNew: boolean }, lang: LocLanguage, onClose: () => void, onConfirm: (langIdx: number, strIdx: number, isNew: boolean, key: string, val: string) => void }) { function LocEditModal({ data, lang, onClose, onConfirm }: { data: { langIdx: number, strIdx: number, isNew: boolean }, lang: LocLanguage, onClose: () => void, onConfirm: (langIdx: number, strIdx: number, isNew: boolean, key: string, val: string) => void }) {
const { t } = useTranslation();
const [key, setKey] = useState(!data.isNew ? (lang.strings[data.strIdx].key || "") : ""); const [key, setKey] = useState(!data.isNew ? (lang.strings[data.strIdx].key || "") : "");
const [val, setVal] = useState(!data.isNew ? lang.strings[data.strIdx].value : ""); const [val, setVal] = useState(!data.isNew ? lang.strings[data.strIdx].value : "");
return ( return (
<div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/60 backdrop-blur-sm"> <div className="fixed inset-0 z-[110] flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-2xl p-8" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> <div className="w-full max-w-2xl p-8" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow mb-6 uppercase tracking-widest">{data.isNew ? "Add" : "Edit"} String</h3> <h3 className="text-2xl text-[#FFFF55] mc-text-shadow mb-6 uppercase tracking-widest">{data.isNew ? t("locEditor.add") : t("locEditor.edit")} {t("locEditor.stringTitle")}</h3>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{!lang.isStatic ? ( {!lang.isStatic ? (
<div> <div>
<label className="text-white/40 text-xs uppercase mb-2 block">String Key</label> <label className="text-white/40 text-xs uppercase mb-2 block">{t("locEditor.stringKey")}</label>
<input <input
type="text" type="text"
value={key} value={key}
@@ -256,10 +258,10 @@ function LocEditModal({ data, lang, onClose, onConfirm }: { data: { langIdx: num
/> />
</div> </div>
) : ( ) : (
<div className="text-white/40 italic mb-2">Static entry - Index: {data.isNew ? lang.strings.length : data.strIdx}</div> <div className="text-white/40 italic mb-2">{t("locEditor.staticEntry", { index: data.isNew ? lang.strings.length : data.strIdx })}</div>
)} )}
<div> <div>
<label className="text-white/40 text-xs uppercase mb-2 block">String Value</label> <label className="text-white/40 text-xs uppercase mb-2 block">{t("locEditor.stringValue")}</label>
<textarea <textarea
value={val} value={val}
onChange={(e) => setVal(e.target.value)} onChange={(e) => setVal(e.target.value)}
@@ -268,13 +270,13 @@ function LocEditModal({ data, lang, onClose, onConfirm }: { data: { langIdx: num
/> />
</div> </div>
<div className="flex justify-end gap-4 mt-6"> <div className="flex justify-end gap-4 mt-6">
<button onClick={onClose} className="px-6 py-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest">Cancel</button> <button onClick={onClose} className="px-6 py-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest">{t("locEditor.cancel")}</button>
<button <button
onClick={() => onConfirm(data.langIdx, data.strIdx, data.isNew, key, val)} onClick={() => onConfirm(data.langIdx, data.strIdx, data.isNew, key, val)}
className="px-8 py-2 text-white mc-text-shadow" className="px-8 py-2 text-white mc-text-shadow"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
{data.isNew ? "Add" : "Save"} {data.isNew ? t("locEditor.add") : t("locEditor.save")}
</button> </button>
</div> </div>
</div> </div>
+50 -46
View File
@@ -1,4 +1,5 @@
import { useState, useRef, useEffect, useMemo, useCallback } from "react"; import { useState, useRef, useEffect, useMemo, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; import { useUI, useAudio, useConfig } from "../../context/LauncherContext";
import { ModelFile, ModelPart, ModelBox, Vec3, Vec2 } from "../../types/model"; import { ModelFile, ModelPart, ModelBox, Vec3, Vec2 } from "../../types/model";
@@ -13,6 +14,7 @@ interface Selection {
} }
export default function ModelEditorView() { export default function ModelEditorView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { animationsEnabled } = useConfig(); const { animationsEnabled } = useConfig();
@@ -73,16 +75,16 @@ export default function ModelEditorView() {
const idx = models.length; const idx = models.length;
setExpandedModels((prev) => new Set(prev).add(idx)); setExpandedModels((prev) => new Set(prev).add(idx));
setSelection({ type: "model", modelIdx: idx }); setSelection({ type: "model", modelIdx: idx });
showNotify(`Imported: ${model.name}`); showNotify(t("modelEditor.imported", { name: model.name }));
} else { } else {
const text = new TextDecoder("utf-8").decode(buffer); const text = new TextDecoder("utf-8").decode(buffer);
const imported = ModelService.importFromJSON(text); const imported = ModelService.importFromJSON(text);
setModels((prev) => [...prev, ...imported]); setModels((prev) => [...prev, ...imported]);
showNotify(`Loaded ${imported.length} model(s)`); showNotify(t("modelEditor.loadedModels", { count: imported.length }));
} }
} catch (err: unknown) { } catch (err: unknown) {
showNotify( showNotify(
err instanceof Error ? err.message : "Failed to parse", err instanceof Error ? err.message : t("modelEditor.failedToParse"),
"error", "error",
); );
} }
@@ -106,7 +108,7 @@ export default function ModelEditorView() {
next[mi] = { ...model, textures }; next[mi] = { ...model, textures };
return next; return next;
}); });
showNotify(`Added texture: ${name}`); showNotify(t("modelEditor.addedTexture", { name }));
e.target.value = ""; e.target.value = "";
}; };
@@ -118,7 +120,7 @@ export default function ModelEditorView() {
setModels((prev) => [...prev, model]); setModels((prev) => [...prev, model]);
setExpandedModels((prev) => new Set(prev).add(idx)); setExpandedModels((prev) => new Set(prev).add(idx));
setSelection({ type: "model", modelIdx: idx }); setSelection({ type: "model", modelIdx: idx });
showNotify(`Created: ${name}`); showNotify(t("modelEditor.created", { name }));
}; };
const handleExport = () => { const handleExport = () => {
@@ -133,9 +135,9 @@ export default function ModelEditorView() {
a.download = `${activeModel.name}.bbmodel`; a.download = `${activeModel.name}.bbmodel`;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
showNotify(`Exported: ${activeModel.name}.bbmodel`); showNotify(t("modelEditor.exported", { name: `${activeModel.name}.bbmodel` }));
} catch { } catch {
showNotify("Export failed", "error"); showNotify(t("modelEditor.exportFailed"), "error");
} }
}; };
@@ -148,7 +150,7 @@ export default function ModelEditorView() {
playBackSound(); playBackSound();
setModels((prev) => prev.filter((_, i) => i !== idx)); setModels((prev) => prev.filter((_, i) => i !== idx));
if (selection?.modelIdx === idx) setSelection(null); if (selection?.modelIdx === idx) setSelection(null);
showNotify("Model deleted"); showNotify(t("modelEditor.modelDeleted"));
}; };
const handleDeletePart = (modelIdx: number, partIdx: number) => { const handleDeletePart = (modelIdx: number, partIdx: number) => {
@@ -333,7 +335,7 @@ export default function ModelEditorView() {
<div className="flex items-center justify-between mb-4 px-4 shrink-0"> <div className="flex items-center justify-between mb-4 px-4 shrink-0">
<h2 className="text-2xl text-white mc-text-shadow tracking-widest uppercase font-bold"> <h2 className="text-2xl text-white mc-text-shadow tracking-widest uppercase font-bold">
Model Editor {t("modelEditor.title")}
</h2> </h2>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
@@ -344,7 +346,7 @@ export default function ModelEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Import {t("modelEditor.import")}
</button> </button>
<button <button
onClick={handleExport} onClick={handleExport}
@@ -355,7 +357,7 @@ export default function ModelEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Export {t("modelEditor.export")}
</button> </button>
<button <button
onClick={handleSave} onClick={handleSave}
@@ -366,7 +368,7 @@ export default function ModelEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Save {t("modelEditor.save")}
</button> </button>
<div className="w-px h-6 bg-[#373737]" /> <div className="w-px h-6 bg-[#373737]" />
<label className="flex items-center gap-2 cursor-pointer select-none"> <label className="flex items-center gap-2 cursor-pointer select-none">
@@ -375,7 +377,7 @@ export default function ModelEditorView() {
className={`w-4 h-4 border-2 transition-colors ${showBounds ? "bg-[#FFFF55] border-[#FFFF55]" : "bg-black/40 border-[#373737]"}`} className={`w-4 h-4 border-2 transition-colors ${showBounds ? "bg-[#FFFF55] border-[#FFFF55]" : "bg-black/40 border-[#373737]"}`}
/> />
<span className="text-white/50 text-[10px] uppercase tracking-widest"> <span className="text-white/50 text-[10px] uppercase tracking-widest">
Bounds {t("modelEditor.bounds")}
</span> </span>
</label> </label>
</div> </div>
@@ -391,7 +393,7 @@ export default function ModelEditorView() {
}} }}
> >
<h3 className="text-2xl text-white/40 mc-text-shadow italic"> <h3 className="text-2xl text-white/40 mc-text-shadow italic">
Import a .bbmodel or .json file to begin {t("modelEditor.importToBegin")}
</h3> </h3>
<button <button
onClick={handleNewModel} onClick={handleNewModel}
@@ -401,7 +403,7 @@ export default function ModelEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Start with Default Model {t("modelEditor.startDefaultModel")}
</button> </button>
</div> </div>
) : ( ) : (
@@ -416,13 +418,13 @@ export default function ModelEditorView() {
<div className="w-72 min-w-72 border-r-2 border-[#373737] flex flex-col overflow-hidden"> <div className="w-72 min-w-72 border-r-2 border-[#373737] flex flex-col overflow-hidden">
<div className="p-3 pt-4 pl-5 border-b-2 border-[#373737] flex items-center justify-between"> <div className="p-3 pt-4 pl-5 border-b-2 border-[#373737] flex items-center justify-between">
<span className="text-white/60 uppercase text-xs tracking-widest font-bold pl-1"> <span className="text-white/60 uppercase text-xs tracking-widest font-bold pl-1">
Models ({models.length}) {t("modelEditor.models", { count: models.length })}
</span> </span>
<button <button
onClick={handleNewModel} onClick={handleNewModel}
className="text-[#FFFF55] text-sm hover:opacity-80 pr-1" className="text-[#FFFF55] text-sm hover:opacity-80 pr-1"
> >
+ Add {t("modelEditor.add")}
</button> </button>
</div> </div>
<div className="flex-1 overflow-y-auto custom-scrollbar p-3 pl-5"> <div className="flex-1 overflow-y-auto custom-scrollbar p-3 pl-5">
@@ -436,14 +438,14 @@ export default function ModelEditorView() {
onContextMenu={(e) => onContextMenu={(e) =>
handleContextMenu(e, [ handleContextMenu(e, [
{ {
label: "Export", label: t("modelEditor.export"),
action: () => { action: () => {
setSelection({ type: "model", modelIdx: mi }); setSelection({ type: "model", modelIdx: mi });
setTimeout(handleExport, 0); setTimeout(handleExport, 0);
}, },
}, },
{ {
label: "Remove", label: t("modelEditor.remove"),
action: () => handleDeleteModel(mi), action: () => handleDeleteModel(mi),
}, },
]) ])
@@ -482,10 +484,10 @@ export default function ModelEditorView() {
onClick={() => handleAddPart(mi)} onClick={() => handleAddPart(mi)}
> >
<span className="text-[10px] uppercase tracking-widest text-white/40"> <span className="text-[10px] uppercase tracking-widest text-white/40">
Parts ({model.parts.length}) {t("modelEditor.parts", { count: model.parts.length })}
</span> </span>
<span className="text-[#FFFF55] text-xs cursor-pointer"> <span className="text-[#FFFF55] text-xs cursor-pointer">
+ Add {t("modelEditor.add")}
</span> </span>
</div> </div>
{model.parts.map((part, pi) => { {model.parts.map((part, pi) => {
@@ -505,7 +507,7 @@ export default function ModelEditorView() {
onContextMenu={(e) => onContextMenu={(e) =>
handleContextMenu(e, [ handleContextMenu(e, [
{ {
label: "Edit", label: t("modelEditor.edit"),
action: () => action: () =>
setEditingPart({ setEditingPart({
modelIdx: mi, modelIdx: mi,
@@ -513,7 +515,7 @@ export default function ModelEditorView() {
}), }),
}, },
{ {
label: "Remove", label: t("modelEditor.remove"),
action: () => handleDeletePart(mi, pi), action: () => handleDeletePart(mi, pi),
}, },
]) ])
@@ -549,10 +551,10 @@ export default function ModelEditorView() {
onClick={() => handleAddBox(mi, pi)} onClick={() => handleAddBox(mi, pi)}
> >
<span className="text-[9px] uppercase tracking-widest text-white/40"> <span className="text-[9px] uppercase tracking-widest text-white/40">
Boxes ({part.boxes.length}) {t("modelEditor.boxes", { count: part.boxes.length })}
</span> </span>
<span className="text-[#FFFF55] text-[10px] cursor-pointer"> <span className="text-[#FFFF55] text-[10px] cursor-pointer">
+ Add {t("modelEditor.add")}
</span> </span>
</div> </div>
{part.boxes.map((_box, bi) => ( {part.boxes.map((_box, bi) => (
@@ -569,7 +571,7 @@ export default function ModelEditorView() {
onContextMenu={(e) => onContextMenu={(e) =>
handleContextMenu(e, [ handleContextMenu(e, [
{ {
label: "Edit", label: t("modelEditor.edit"),
action: () => action: () =>
setEditingBox({ setEditingBox({
modelIdx: mi, modelIdx: mi,
@@ -578,7 +580,7 @@ export default function ModelEditorView() {
}), }),
}, },
{ {
label: "Remove", label: t("modelEditor.remove"),
action: () => action: () =>
handleDeleteBox(mi, pi, bi), handleDeleteBox(mi, pi, bi),
}, },
@@ -588,7 +590,7 @@ export default function ModelEditorView() {
> >
<div className="w-2 h-2 rounded-sm border border-white/20 shrink-0" /> <div className="w-2 h-2 rounded-sm border border-white/20 shrink-0" />
<span className="truncate text-[10px] mc-text-shadow"> <span className="truncate text-[10px] mc-text-shadow">
Box {bi + 1} {t("modelEditor.box", { index: bi + 1 })}
</span> </span>
<button <button
onClick={(e) => { onClick={(e) => {
@@ -631,19 +633,19 @@ export default function ModelEditorView() {
<div className="h-44 shrink-0 border-t-2 border-[#373737] bg-black/10 overflow-y-auto custom-scrollbar"> <div className="h-44 shrink-0 border-t-2 border-[#373737] bg-black/10 overflow-y-auto custom-scrollbar">
<div className="p-2 border-b border-[#373737]/50 flex items-center justify-between"> <div className="p-2 border-b border-[#373737]/50 flex items-center justify-between">
<span className="text-white/40 text-[10px] uppercase tracking-widest font-bold"> <span className="text-white/40 text-[10px] uppercase tracking-widest font-bold">
Textures ({activeTextures.length}) {t("modelEditor.textures", { count: activeTextures.length })}
</span> </span>
<button <button
onClick={() => textureInputRef.current?.click()} onClick={() => textureInputRef.current?.click()}
className="text-white/40 hover:text-white text-xs uppercase tracking-widest transition-colors pr-2" className="text-white/40 hover:text-white text-xs uppercase tracking-widest transition-colors pr-2"
> >
Add {t("modelEditor.add")}
</button> </button>
</div> </div>
{activeTextures.length === 0 ? ( {activeTextures.length === 0 ? (
<div className="flex items-center justify-center h-20"> <div className="flex items-center justify-center h-20">
<span className="text-white/20 text-xs italic"> <span className="text-white/20 text-xs italic">
No textures {t("modelEditor.noTextures")}
</span> </span>
</div> </div>
) : ( ) : (
@@ -674,7 +676,7 @@ export default function ModelEditorView() {
) : ( ) : (
<div className="flex-1 flex items-center justify-center"> <div className="flex-1 flex items-center justify-center">
<span className="text-white/20 mc-text-shadow text-lg italic"> <span className="text-white/20 mc-text-shadow text-lg italic">
Select a model to preview {t("modelEditor.selectModel")}
</span> </span>
</div> </div>
)} )}
@@ -695,7 +697,7 @@ export default function ModelEditorView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Back {t("modelEditor.back")}
</button> </button>
</div> </div>
@@ -830,6 +832,7 @@ function PartEditModal({
onClose: () => void; onClose: () => void;
onConfirm: (updated: { name: string; translation: Vec3 | undefined }) => void; onConfirm: (updated: { name: string; translation: Vec3 | undefined }) => void;
}) { }) {
const { t } = useTranslation();
const [name, setName] = useState(part.name); const [name, setName] = useState(part.name);
const [tx, setTx] = useState(part.translation?.X ?? 0); const [tx, setTx] = useState(part.translation?.X ?? 0);
const [ty, setTy] = useState(part.translation?.Y ?? 0); const [ty, setTy] = useState(part.translation?.Y ?? 0);
@@ -855,12 +858,12 @@ function PartEditModal({
}} }}
> >
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase"> <h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase">
Edit Part {t("modelEditor.editPart")}
</h3> </h3>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div> <div>
<label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block"> <label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block">
Name {t("modelEditor.name")}
</label> </label>
<input <input
type="text" type="text"
@@ -872,7 +875,7 @@ function PartEditModal({
</div> </div>
<div> <div>
<label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block"> <label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block">
Translation {t("modelEditor.translation")}
</label> </label>
<div className="flex gap-3"> <div className="flex gap-3">
{(["X", "Y", "Z"] as const).map((axis) => ( {(["X", "Y", "Z"] as const).map((axis) => (
@@ -902,7 +905,7 @@ function PartEditModal({
onClick={onClose} onClick={onClose}
className="px-6 py-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest text-sm" className="px-6 py-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest text-sm"
> >
Cancel {t("modelEditor.cancel")}
</button> </button>
<button <button
onClick={() => onClick={() =>
@@ -920,7 +923,7 @@ function PartEditModal({
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Save {t("modelEditor.save")}
</button> </button>
</div> </div>
</motion.div> </motion.div>
@@ -937,6 +940,7 @@ function BoxEditModal({
onClose: () => void; onClose: () => void;
onConfirm: (updated: ModelBox) => void; onConfirm: (updated: ModelBox) => void;
}) { }) {
const { t } = useTranslation();
const [posX, setPosX] = useState(box.pos.X); const [posX, setPosX] = useState(box.pos.X);
const [posY, setPosY] = useState(box.pos.Y); const [posY, setPosY] = useState(box.pos.Y);
const [posZ, setPosZ] = useState(box.pos.Z); const [posZ, setPosZ] = useState(box.pos.Z);
@@ -968,12 +972,12 @@ function BoxEditModal({
}} }}
> >
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase"> <h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase">
Edit Box {t("modelEditor.editBox")}
</h3> </h3>
<div className="grid grid-cols-2 gap-x-6 gap-y-4"> <div className="grid grid-cols-2 gap-x-6 gap-y-4">
<div className="col-span-2"> <div className="col-span-2">
<label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block"> <label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block">
Position {t("modelEditor.position")}
</label> </label>
<div className="flex gap-3"> <div className="flex gap-3">
{(["X", "Y", "Z"] as const).map((axis) => ( {(["X", "Y", "Z"] as const).map((axis) => (
@@ -999,7 +1003,7 @@ function BoxEditModal({
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block"> <label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block">
Size {t("modelEditor.size")}
</label> </label>
<div className="flex gap-3"> <div className="flex gap-3">
{(["X", "Y", "Z"] as const).map((axis) => ( {(["X", "Y", "Z"] as const).map((axis) => (
@@ -1026,7 +1030,7 @@ function BoxEditModal({
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block"> <label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block">
UV Offset {t("modelEditor.uvOffset")}
</label> </label>
<div className="flex gap-3"> <div className="flex gap-3">
{(["X", "Y"] as const).map((axis) => ( {(["X", "Y"] as const).map((axis) => (
@@ -1051,7 +1055,7 @@ function BoxEditModal({
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<label className="text-white/40 text-[10px] uppercase tracking-widest"> <label className="text-white/40 text-[10px] uppercase tracking-widest">
Inflate {t("modelEditor.inflate")}
</label> </label>
<input <input
type="number" type="number"
@@ -1063,7 +1067,7 @@ function BoxEditModal({
</div> </div>
<div className="flex items-center gap-3 justify-end"> <div className="flex items-center gap-3 justify-end">
<label className="text-white/40 text-[10px] uppercase tracking-widest"> <label className="text-white/40 text-[10px] uppercase tracking-widest">
Mirror UV {t("modelEditor.mirrorUv")}
</label> </label>
<div <div
onClick={() => setMirror(!mirror)} onClick={() => setMirror(!mirror)}
@@ -1076,7 +1080,7 @@ function BoxEditModal({
onClick={onClose} onClick={onClose}
className="px-6 py-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest text-sm" className="px-6 py-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest text-sm"
> >
Cancel {t("modelEditor.cancel")}
</button> </button>
<button <button
onClick={() => onClick={() =>
@@ -1094,7 +1098,7 @@ function BoxEditModal({
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Save {t("modelEditor.save")}
</button> </button>
</div> </div>
</motion.div> </motion.div>
+15 -13
View File
@@ -1,4 +1,5 @@
import { useState, useRef } from "react"; import { useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; import { useUI, useAudio, useConfig } from "../../context/LauncherContext";
import { OptionsService } from "../../services/OptionsService"; import { OptionsService } from "../../services/OptionsService";
@@ -11,6 +12,7 @@ const BUTTONS: Record<number, string> = {
}; };
export default function OptionsEditorView() { export default function OptionsEditorView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { animationsEnabled } = useConfig(); const { animationsEnabled } = useConfig();
@@ -31,10 +33,10 @@ export default function OptionsEditorView() {
try { try {
const parsed = OptionsService.readOptions(buffer); const parsed = OptionsService.readOptions(buffer);
setOpt(parsed); setOpt(parsed);
showNotification(`Loaded options.dat`); showNotification(t("optionsEditor.loaded"));
} catch (err: unknown) { } catch (err: unknown) {
console.error("Failed to parse Options", err); console.error("Failed to parse Options", err);
showNotification(err instanceof Error ? err.message : "Failed to parse Options", "error"); showNotification(err instanceof Error ? err.message : t("optionsEditor.failedToParse"), "error");
} }
e.target.value = ""; e.target.value = "";
}; };
@@ -51,9 +53,9 @@ export default function OptionsEditorView() {
a.download = "options.dat"; a.download = "options.dat";
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
showNotification("Options Saved"); showNotification(t("optionsEditor.saved"));
} catch (err: unknown) { } catch (err: unknown) {
showNotification("Failed to save", "error"); showNotification(t("optionsEditor.failedToSave"), "error");
} }
}; };
@@ -85,7 +87,7 @@ export default function OptionsEditorView() {
<input type="file" ref={fileInputRef} onChange={handleFileLoad} className="hidden" accept=".dat,.bin" /> <input type="file" ref={fileInputRef} onChange={handleFileLoad} className="hidden" accept=".dat,.bin" />
<div className="flex items-center justify-between mb-6 px-4"> <div className="flex items-center justify-between mb-6 px-4">
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<h2 className="text-3xl text-white mc-text-shadow tracking-widest uppercase font-bold">Options Editor</h2> <h2 className="text-3xl text-white mc-text-shadow tracking-widest uppercase font-bold">{t("optionsEditor.title")}</h2>
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
@@ -93,7 +95,7 @@ export default function OptionsEditorView() {
className="px-6 py-2 text-white mc-text-shadow text-lg" className="px-6 py-2 text-white mc-text-shadow text-lg"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Open settings.dat {t("optionsEditor.openSettings")}
</button> </button>
<button <button
onClick={handleSaveOptions} onClick={handleSaveOptions}
@@ -101,7 +103,7 @@ export default function OptionsEditorView() {
className={`px-6 py-2 text-white mc-text-shadow text-lg ${!opt ? "opacity-50 grayscale" : ""}`} className={`px-6 py-2 text-white mc-text-shadow text-lg ${!opt ? "opacity-50 grayscale" : ""}`}
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Save settings.dat {t("optionsEditor.saveSettings")}
</button> </button>
</div> </div>
</div> </div>
@@ -109,7 +111,7 @@ export default function OptionsEditorView() {
{!opt ? ( {!opt ? (
<div className="flex-1 w-full flex flex-col items-center justify-center p-12" <div className="flex-1 w-full flex flex-col items-center justify-center p-12"
style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
<h3 className="text-2xl text-white/40 mc-text-shadow italic">Open settings.dat to begin</h3> <h3 className="text-2xl text-white/40 mc-text-shadow italic">{t("optionsEditor.openToBegin")}</h3>
</div> </div>
) : ( ) : (
<div className="flex-1 w-full flex flex-col overflow-hidden" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> <div className="flex-1 w-full flex flex-col overflow-hidden" style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
@@ -120,7 +122,7 @@ export default function OptionsEditorView() {
onClick={() => { playPressSound(); setActiveTab(tab); }} onClick={() => { playPressSound(); setActiveTab(tab); }}
className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === tab ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"}`} className={`flex items-center gap-3 px-6 py-2 mc-text-shadow ${activeTab === tab ? "text-[#FFFF55] opacity-100" : "text-white opacity-40"}`}
> >
<span className="text-lg capitalize">{tab}</span> <span className="text-lg capitalize">{tab === "settings" ? t("optionsEditor.tabSettings") : tab === "skins" ? t("optionsEditor.tabSkins") : t("optionsEditor.tabActions")}</span>
</button> </button>
))} ))}
</div> </div>
@@ -158,21 +160,21 @@ export default function OptionsEditorView() {
{activeTab === "skins" && ( {activeTab === "skins" && (
<div className="flex flex-col gap-8 max-w-xl mx-auto"> <div className="flex flex-col gap-8 max-w-xl mx-auto">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-white/80 uppercase tracking-widest">Chosen Skin ID</span> <span className="text-white/80 uppercase tracking-widest">{t("optionsEditor.chosenSkinId")}</span>
<input <input
type="number" value={opt.chosenSkin} onChange={(e) => updateSetting("chosenSkin", parseInt(e.target.value) || 0)} type="number" value={opt.chosenSkin} onChange={(e) => updateSetting("chosenSkin", parseInt(e.target.value) || 0)}
className="bg-black/40 border border-[#373737] p-2 text-white outline-none focus:border-[#FFFF55] font-mono text-lg" className="bg-black/40 border border-[#373737] p-2 text-white outline-none focus:border-[#FFFF55] font-mono text-lg"
/> />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-white/80 uppercase tracking-widest">Player Cape ID</span> <span className="text-white/80 uppercase tracking-widest">{t("optionsEditor.playerCapeId")}</span>
<input <input
type="number" value={opt.playerCape} onChange={(e) => updateSetting("playerCape", parseInt(e.target.value) || 0)} type="number" value={opt.playerCape} onChange={(e) => updateSetting("playerCape", parseInt(e.target.value) || 0)}
className="bg-black/40 border border-[#373737] p-2 text-white outline-none focus:border-[#FFFF55] font-mono text-lg" className="bg-black/40 border border-[#373737] p-2 text-white outline-none focus:border-[#FFFF55] font-mono text-lg"
/> />
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<span className="text-white/80 uppercase tracking-widest">Favorite Skins</span> <span className="text-white/80 uppercase tracking-widest">{t("optionsEditor.favoriteSkins")}</span>
{opt.favoriteSkins.map((s, i) => ( {opt.favoriteSkins.map((s, i) => (
<div key={i} className="flex items-center gap-4"> <div key={i} className="flex items-center gap-4">
<span className="text-white/40 w-8">{i + 1}.</span> <span className="text-white/40 w-8">{i + 1}.</span>
@@ -217,7 +219,7 @@ export default function OptionsEditorView() {
className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white" className="w-72 h-full shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow outline-none border-none hover:text-[#FFFF55] text-white"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}
> >
Back {t("optionsEditor.back")}
</button> </button>
</div> </div>
+54 -47
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useMemo } from "react"; import { useState, useEffect, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { useUI, useAudio, useConfig } from "../../context/LauncherContext"; import { useUI, useAudio, useConfig } from "../../context/LauncherContext";
import { PckService } from "../../services/PckService"; import { PckService } from "../../services/PckService";
@@ -6,6 +7,7 @@ import { PCKFile, PCKAsset, PCKAssetType } from "../../types/pck";
import SkinPreview3D from "../common/SkinPreview3D"; import SkinPreview3D from "../common/SkinPreview3D";
import { TauriService } from "../../services/TauriService"; import { TauriService } from "../../services/TauriService";
export default function PckEditorView() { export default function PckEditorView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { animationsEnabled } = useConfig(); const { animationsEnabled } = useConfig();
@@ -201,7 +203,7 @@ export default function PckEditorView() {
const handleFileLoad = async () => { const handleFileLoad = async () => {
try { try {
const path = await TauriService.pickFile("Open PCK", ["pck"]); const path = await TauriService.pickFile(t("pckEditor.openPck"), ["pck"]);
if (!path) return; if (!path) return;
playPressSound(); playPressSound();
const bytes = await TauriService.readBinaryFile(path); const bytes = await TauriService.readBinaryFile(path);
@@ -213,7 +215,7 @@ export default function PckEditorView() {
} catch (err: unknown) { } catch (err: unknown) {
if (err !== "CANCELED") { if (err !== "CANCELED") {
console.error("Failed to parse PCK", err); console.error("Failed to parse PCK", err);
showNotification("Failed to parse PCK", "error"); showNotification(t("pckEditor.failedToParse"), "error");
} }
} }
}; };
@@ -231,7 +233,7 @@ export default function PckEditorView() {
setOpenedPath(null); setOpenedPath(null);
setSelectedAssetId(null); setSelectedAssetId(null);
setExpandedFolders(new Set()); setExpandedFolders(new Set());
showNotification("New PCK Created"); showNotification(t("pckEditor.newPckCreated"));
}; };
const showNotification = ( const showNotification = (
@@ -246,16 +248,16 @@ export default function PckEditorView() {
try { try {
const fileName = asset.path.split("/").pop() || "asset"; const fileName = asset.path.split("/").pop() || "asset";
const path = await TauriService.saveFileDialog( const path = await TauriService.saveFileDialog(
"Export Asset", t("pckEditor.exportAssetDialog"),
fileName, fileName,
[], [],
); );
if (!path) return; if (!path) return;
playPressSound(); playPressSound();
await TauriService.writeBinaryFile(path, asset.data); await TauriService.writeBinaryFile(path, asset.data);
showNotification(`Exported: ${fileName}`); showNotification(t("pckEditor.exported", { name: fileName }));
} catch (err: unknown) { } catch (err: unknown) {
if (err !== "CANCELED") showNotification("Export failed", "error"); if (err !== "CANCELED") showNotification(t("pckEditor.exportFailed"), "error");
} }
}; };
@@ -266,7 +268,9 @@ export default function PckEditorView() {
const assetPath = pck.files.find((f) => f.id === id)?.path; const assetPath = pck.files.find((f) => f.id === id)?.path;
setPck({ ...pck, files: newFiles }); setPck({ ...pck, files: newFiles });
if (selectedAssetId === id) setSelectedAssetId(newFiles[0]?.id || null); if (selectedAssetId === id) setSelectedAssetId(newFiles[0]?.id || null);
showNotification(`Deleted: ${assetPath?.split("/").pop()}`); showNotification(
t("pckEditor.deleted", { name: assetPath?.split("/").pop() }),
);
}; };
const handleReplaceAsset = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleReplaceAsset = async (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -283,7 +287,7 @@ export default function PckEditorView() {
); );
setPck({ ...pck, files: newFiles }); setPck({ ...pck, files: newFiles });
e.target.value = ""; e.target.value = "";
showNotification("Asset Replaced"); showNotification(t("pckEditor.assetReplaced"));
}; };
const handleAddAsset = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleAddAsset = async (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -316,7 +320,7 @@ export default function PckEditorView() {
setPck({ ...pck, files: [...pck.files, newAsset] }); setPck({ ...pck, files: [...pck.files, newAsset] });
setSelectedAssetId(newAsset.id); setSelectedAssetId(newAsset.id);
setShowTypeModal(null); setShowTypeModal(null);
showNotification("Asset Added"); showNotification(t("pckEditor.assetAdded"));
}; };
const handlePropertyEdit = (idx: number, newVal: string, isKey = false) => { const handlePropertyEdit = (idx: number, newVal: string, isKey = false) => {
@@ -402,7 +406,7 @@ export default function PckEditorView() {
return f; return f;
}); });
setPck({ ...pck, files: newFiles }); setPck({ ...pck, files: newFiles });
showNotification("Asset Renamed"); showNotification(t("pckEditor.assetRenamed"));
}; };
const handleExportAll = async () => { const handleExportAll = async () => {
@@ -411,7 +415,7 @@ export default function PckEditorView() {
const baseFolder = await TauriService.pickFolder(); const baseFolder = await TauriService.pickFolder();
if (!baseFolder) return; if (!baseFolder) return;
playPressSound(); playPressSound();
showNotification("Exporting all assets..."); showNotification(t("pckEditor.exportingAllAssets"));
for (const asset of pck.files) { for (const asset of pck.files) {
const parts = asset.path.split("/"); const parts = asset.path.split("/");
const fileName = parts.join("_"); const fileName = parts.join("_");
@@ -420,9 +424,10 @@ export default function PckEditorView() {
asset.data, asset.data,
); );
} }
showNotification("All Assets Exported"); showNotification(t("pckEditor.allAssetsExported"));
} catch (err: unknown) { } catch (err: unknown) {
if (err !== "CANCELED") showNotification("Export failed", "error"); if (err !== "CANCELED")
showNotification(t("pckEditor.exportFailed"), "error");
} }
}; };
@@ -435,7 +440,7 @@ export default function PckEditorView() {
let targetPath = openedPath; let targetPath = openedPath;
if (!targetPath) { if (!targetPath) {
targetPath = await TauriService.saveFileDialog( targetPath = await TauriService.saveFileDialog(
"Save PCK", t("pckEditor.savePckDialog"),
pck.files.length > 0 ? "new.pck" : "empty.pck", pck.files.length > 0 ? "new.pck" : "empty.pck",
["pck"], ["pck"],
); );
@@ -444,10 +449,11 @@ export default function PckEditorView() {
if (targetPath) { if (targetPath) {
await TauriService.writeBinaryFile(targetPath, data); await TauriService.writeBinaryFile(targetPath, data);
setOpenedPath(targetPath); setOpenedPath(targetPath);
showNotification("PCK Saved Successfully"); showNotification(t("pckEditor.pckSaved"));
} }
} catch (err: unknown) { } catch (err: unknown) {
if (err !== "CANCELED") showNotification("Export failed", "error"); if (err !== "CANCELED")
showNotification(t("pckEditor.exportFailed"), "error");
} }
}; };
@@ -504,7 +510,7 @@ export default function PckEditorView() {
> >
<div className="w-full flex justify-between items-center mb-4 px-8"> <div className="w-full flex justify-between items-center mb-4 px-8">
<h2 className="text-2xl text-white mc-text-shadow border-b-2 border-[#373737] pb-1 tracking-widest uppercase font-bold"> <h2 className="text-2xl text-white mc-text-shadow border-b-2 border-[#373737] pb-1 tracking-widest uppercase font-bold">
PCK Editor {t("pckEditor.title")}
</h2> </h2>
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
@@ -515,7 +521,7 @@ export default function PckEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Open PCK {t("pckEditor.openPck")}
</button> </button>
<button <button
onClick={handleNewPCK} onClick={handleNewPCK}
@@ -525,7 +531,7 @@ export default function PckEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
New PCK {t("pckEditor.newPck")}
</button> </button>
<button <button
onClick={handleExportAll} onClick={handleExportAll}
@@ -536,7 +542,7 @@ export default function PckEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Export All {t("pckEditor.exportAll")}
</button> </button>
<button <button
onClick={handleSavePCK} onClick={handleSavePCK}
@@ -547,7 +553,7 @@ export default function PckEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Save PCK {t("pckEditor.savePck")}
</button> </button>
</div> </div>
</div> </div>
@@ -557,7 +563,7 @@ export default function PckEditorView() {
<div className="flex gap-6 bg-black/40 border-2 border-[#373737] p-3 w-full"> <div className="flex gap-6 bg-black/40 border-2 border-[#373737] p-3 w-full">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-white/40 text-xs uppercase font-bold tracking-widest"> <span className="text-white/40 text-xs uppercase font-bold tracking-widest">
Endianness: {t("pckEditor.endianness")}
</span> </span>
<button <button
onClick={() => { onClick={() => {
@@ -574,7 +580,7 @@ export default function PckEditorView() {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-white/40 text-xs uppercase font-bold tracking-widest"> <span className="text-white/40 text-xs uppercase font-bold tracking-widest">
XML Support: {t("pckEditor.xmlSupport")}
</span> </span>
<button <button
onClick={() => { onClick={() => {
@@ -583,12 +589,12 @@ export default function PckEditorView() {
}} }}
className={`${pck.xmlSupport ? "text-[#FFFF55]" : "text-white/20"} text-sm uppercase hover:underline`} className={`${pck.xmlSupport ? "text-[#FFFF55]" : "text-white/20"} text-sm uppercase hover:underline`}
> >
{pck.xmlSupport ? "Enabled" : "Disabled"} {pck.xmlSupport ? t("pckEditor.enabled") : t("pckEditor.disabled")}
</button> </button>
</div> </div>
<div className="flex items-center gap-2 ml-auto"> <div className="flex items-center gap-2 ml-auto">
<span className="text-white/40 text-xs uppercase font-bold tracking-widest"> <span className="text-white/40 text-xs uppercase font-bold tracking-widest">
Version: {t("pckEditor.version")}
</span> </span>
<span className="text-white text-sm">{pck.version}</span> <span className="text-white text-sm">{pck.version}</span>
</div> </div>
@@ -623,7 +629,7 @@ export default function PckEditorView() {
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
<h3 className="text-2xl text-white/40 mc-text-shadow italic"> <h3 className="text-2xl text-white/40 mc-text-shadow italic">
Open a PCK file to begin editing {t("pckEditor.openToBegin")}
</h3> </h3>
</div> </div>
) : ( ) : (
@@ -639,7 +645,7 @@ export default function PckEditorView() {
<div className="mb-4 flex gap-4"> <div className="mb-4 flex gap-4">
<input <input
type="text" type="text"
placeholder="Search assets..." placeholder={t("pckEditor.searchAssets")}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors" className="flex-1 bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors"
@@ -652,7 +658,7 @@ export default function PckEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Add Asset {t("pckEditor.addAsset")}
</button> </button>
</div> </div>
<div className="flex-1 overflow-y-auto pr-2 custom-scrollbar"> <div className="flex-1 overflow-y-auto pr-2 custom-scrollbar">
@@ -675,7 +681,7 @@ export default function PckEditorView() {
className="w-16 h-16 opacity-10 grayscale" className="w-16 h-16 opacity-10 grayscale"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
<span>Select an asset to view details</span> <span>{t("pckEditor.selectAsset")}</span>
</div> </div>
) : ( ) : (
<motion.div <motion.div
@@ -811,10 +817,10 @@ export default function PckEditorView() {
)} )}
<div className="absolute top-2 right-2 bg-black/60 px-2 py-1 rounded text-[10px] text-white/60 pointer-events-none uppercase tracking-widest"> <div className="absolute top-2 right-2 bg-black/60 px-2 py-1 rounded text-[10px] text-white/60 pointer-events-none uppercase tracking-widest">
{selectedAsset.type === PCKAssetType.SKIN {selectedAsset.type === PCKAssetType.SKIN
? "3D Skin View" ? t("pckEditor.view3dSkin")
: selectedAsset.type === PCKAssetType.CAPE : selectedAsset.type === PCKAssetType.CAPE
? "3D Cape View" ? t("pckEditor.view3dCape")
: "Texture Preview"} : t("pckEditor.texturePreview")}
</div> </div>
</div> </div>
)} )}
@@ -823,13 +829,13 @@ export default function PckEditorView() {
<div> <div>
<div className="flex justify-between items-end mb-2 px-1"> <div className="flex justify-between items-end mb-2 px-1">
<div className="text-white/40 text-[10px] uppercase tracking-widest text-[#FFFF55]/60"> <div className="text-white/40 text-[10px] uppercase tracking-widest text-[#FFFF55]/60">
Metadata Properties {t("pckEditor.metadataProperties")}
</div> </div>
<button <button
onClick={handleAddProperty} onClick={handleAddProperty}
className="text-[#FFFF55] text-[10px] uppercase hover:underline" className="text-[#FFFF55] text-[10px] uppercase hover:underline"
> >
+ Add Property + {t("pckEditor.addProperty")}
</button> </button>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
@@ -851,7 +857,7 @@ export default function PckEditorView() {
onClick={() => handleRemoveProperty(idx)} onClick={() => handleRemoveProperty(idx)}
className="text-red-500/0 group-hover/prop:text-red-500/40 hover:text-red-500 transition-colors text-[10px] uppercase" className="text-red-500/0 group-hover/prop:text-red-500/40 hover:text-red-500 transition-colors text-[10px] uppercase"
> >
Remove {t("pckEditor.remove")}
</button> </button>
</div> </div>
<div className="relative"> <div className="relative">
@@ -961,7 +967,7 @@ export default function PckEditorView() {
))} ))}
{selectedAsset.properties.length === 0 && ( {selectedAsset.properties.length === 0 && (
<div className="text-white/20 italic text-sm px-1 py-4 border-2 border-dashed border-[#373737] text-center"> <div className="text-white/20 italic text-sm px-1 py-4 border-2 border-dashed border-[#373737] text-center">
No metadata properties {t("pckEditor.noMetadataProperties")}
</div> </div>
)} )}
</div> </div>
@@ -978,7 +984,7 @@ export default function PckEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Export {t("pckEditor.export")}
</button> </button>
<button <button
onClick={() => replaceInputRef.current?.click()} onClick={() => replaceInputRef.current?.click()}
@@ -989,7 +995,7 @@ export default function PckEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Replace {t("pckEditor.replace")}
</button> </button>
</div> </div>
<button <button
@@ -1001,7 +1007,7 @@ export default function PckEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Rename Asset (Path) {t("pckEditor.renameAsset")}
</button> </button>
<button <button
onClick={() => handleDeleteAsset(selectedAsset.id)} onClick={() => handleDeleteAsset(selectedAsset.id)}
@@ -1012,7 +1018,7 @@ export default function PckEditorView() {
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Delete This Asset {t("pckEditor.deleteAsset")}
</button> </button>
</div> </div>
</div> </div>
@@ -1035,7 +1041,7 @@ export default function PckEditorView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Back {t("pckEditor.back")}
</button> </button>
<AnimatePresence> <AnimatePresence>
@@ -1080,7 +1086,7 @@ export default function PckEditorView() {
}} }}
> >
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase"> <h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase">
Select Asset Type {t("pckEditor.selectAssetType")}
</h3> </h3>
<div className="grid grid-cols-2 gap-4 w-full"> <div className="grid grid-cols-2 gap-4 w-full">
{Object.keys(PCKAssetType) {Object.keys(PCKAssetType)
@@ -1103,7 +1109,7 @@ export default function PckEditorView() {
onClick={() => setShowTypeModal(null)} onClick={() => setShowTypeModal(null)}
className="mt-8 px-8 py-2 text-white/60 mc-text-shadow text-sm hover:text-white transition-colors" className="mt-8 px-8 py-2 text-white/60 mc-text-shadow text-sm hover:text-white transition-colors"
> >
Cancel {t("pckEditor.cancel")}
</button> </button>
</motion.div> </motion.div>
</div> </div>
@@ -1136,6 +1142,7 @@ function RenameAssetModal({
onClose: () => void; onClose: () => void;
onConfirm: (path: string) => void; onConfirm: (path: string) => void;
}) { }) {
const { t } = useTranslation();
const [path, setPath] = useState(initialPath); const [path, setPath] = useState(initialPath);
return ( return (
<div className="fixed inset-0 z-[150] flex items-center justify-center p-4"> <div className="fixed inset-0 z-[150] flex items-center justify-center p-4">
@@ -1158,12 +1165,12 @@ function RenameAssetModal({
}} }}
> >
<h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase"> <h3 className="text-2xl text-[#FFFF55] mc-text-shadow font-bold mb-6 tracking-widest uppercase">
Rename Asset {t("pckEditor.renameTitle")}
</h3> </h3>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div> <div>
<label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block"> <label className="text-white/40 text-[10px] uppercase tracking-widest mb-1 block">
New Asset Path {t("pckEditor.newAssetPath")}
</label> </label>
<input <input
type="text" type="text"
@@ -1178,7 +1185,7 @@ function RenameAssetModal({
onClick={onClose} onClick={onClose}
className="px-6 py-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest text-sm" className="px-6 py-2 text-white/60 hover:text-white transition-colors uppercase tracking-widest text-sm"
> >
Cancel {t("pckEditor.cancel")}
</button> </button>
<button <button
onClick={() => onConfirm(path)} onClick={() => onConfirm(path)}
@@ -1188,7 +1195,7 @@ function RenameAssetModal({
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
}} }}
> >
Rename {t("pckEditor.rename")}
</button> </button>
</div> </div>
</div> </div>
+18 -16
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, memo } from "react"; import { useState, useEffect, memo } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { usePlatform } from "../../hooks/usePlatform"; import { usePlatform } from "../../hooks/usePlatform";
import { import {
@@ -14,6 +15,7 @@ import {
} from "../../services/ScreenshotService"; } from "../../services/ScreenshotService";
import { ScreenshotImage } from "../common/ScreenshotImage"; import { ScreenshotImage } from "../common/ScreenshotImage";
const ScreenshotsView = memo(function ScreenshotsView() { const ScreenshotsView = memo(function ScreenshotsView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { isAndroid } = usePlatform(); const { isAndroid } = usePlatform();
@@ -163,7 +165,7 @@ const ScreenshotsView = memo(function ScreenshotsView() {
className="flex flex-col items-center w-full h-full max-w-6xl relative font-['Mojangles'] text-white select-none outline-none focus:outline-none" className="flex flex-col items-center w-full h-full max-w-6xl relative font-['Mojangles'] text-white select-none outline-none focus:outline-none"
> >
<h2 className="text-2xl text-white mc-text-shadow mt-4 mb-6 border-b-2 border-[#373737] pb-2 w-[30%] max-w-[250px] text-center tracking-widest uppercase opacity-80 font-bold whitespace-nowrap px-4"> <h2 className="text-2xl text-white mc-text-shadow mt-4 mb-6 border-b-2 border-[#373737] pb-2 w-[30%] max-w-[250px] text-center tracking-widest uppercase opacity-80 font-bold whitespace-nowrap px-4">
Screenshots {t("screenshots.title")}
</h2> </h2>
<div className="w-[98%] flex-1 relative overflow-hidden"> <div className="w-[98%] flex-1 relative overflow-hidden">
@@ -171,16 +173,16 @@ const ScreenshotsView = memo(function ScreenshotsView() {
{loading ? ( {loading ? (
<div className="flex flex-col items-center justify-center h-full gap-4"> <div className="flex flex-col items-center justify-center h-full gap-4">
<span className="text-3xl text-[#FFFF55] mc-text-shadow tracking-widest animate-pulse uppercase"> <span className="text-3xl text-[#FFFF55] mc-text-shadow tracking-widest animate-pulse uppercase">
Scanning Archives... {t("screenshots.scanning")}
</span> </span>
</div> </div>
) : screenshots.length === 0 ? ( ) : screenshots.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full space-y-4 opacity-40"> <div className="flex flex-col items-center justify-center h-full space-y-4 opacity-40">
<span className="text-2xl mc-text-shadow uppercase tracking-widest"> <span className="text-2xl mc-text-shadow uppercase tracking-widest">
No screenshots found {t("screenshots.noScreenshots")}
</span> </span>
<span className="text-sm mc-text-shadow tracking-widest italic"> <span className="text-sm mc-text-shadow tracking-widest italic">
Take some in-game with F2 {t("screenshots.takeInGameWithF2")}
</span> </span>
</div> </div>
) : ( ) : (
@@ -263,7 +265,7 @@ const ScreenshotsView = memo(function ScreenshotsView() {
"url('/images/Button_Background.png')"; "url('/images/Button_Background.png')";
}} }}
> >
Back {t("common.back")}
</button> </button>
</div> </div>
)} )}
@@ -302,10 +304,11 @@ const ScreenshotsView = memo(function ScreenshotsView() {
{selectedScreenshot.name} {selectedScreenshot.name}
</span> </span>
<span className="text-sm text-[#FFFF55] mc-text-shadow uppercase tracking-widest opacity-90"> <span className="text-sm text-[#FFFF55] mc-text-shadow uppercase tracking-widest opacity-90">
Captured on{" "} {t("screenshots.capturedOn", {
{new Date( date: new Date(
selectedScreenshot.date * 1000, selectedScreenshot.date * 1000,
).toLocaleString()} ).toLocaleString(),
})}
</span> </span>
</div> </div>
{getEditionLogo(selectedScreenshot.instanceId) && ( {getEditionLogo(selectedScreenshot.instanceId) && (
@@ -335,9 +338,8 @@ const ScreenshotsView = memo(function ScreenshotsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
OPEN FOLDER {t("screenshots.openFolder")}
</button> </button> <button
<button
onMouseEnter={() => setModalFocusIndex(1)} onMouseEnter={() => setModalFocusIndex(1)}
onClick={() => { onClick={() => {
playPressSound(); playPressSound();
@@ -357,7 +359,7 @@ const ScreenshotsView = memo(function ScreenshotsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
DELETE {t("screenshots.delete")}
</button> </button>
<button <button
onMouseEnter={() => setModalFocusIndex(2)} onMouseEnter={() => setModalFocusIndex(2)}
@@ -375,7 +377,7 @@ const ScreenshotsView = memo(function ScreenshotsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
CLOSE {t("screenshots.close")}
</button> </button>
</div> </div>
</motion.div> </motion.div>
@@ -404,7 +406,7 @@ const ScreenshotsView = memo(function ScreenshotsView() {
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<span className="text-2xl text-white mc-text-shadow text-center mb-6 px-4"> <span className="text-2xl text-white mc-text-shadow text-center mb-6 px-4">
Are you sure you want to delete this screenshot? {t("screenshots.deleteConfirm")}
</span> </span>
<div className="flex gap-4 w-full"> <div className="flex gap-4 w-full">
@@ -427,7 +429,7 @@ const ScreenshotsView = memo(function ScreenshotsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
CANCEL {t("common.cancel")}
</button> </button>
<button <button
onMouseEnter={() => setDeleteConfirmFocusIndex(1)} onMouseEnter={() => setDeleteConfirmFocusIndex(1)}
@@ -445,7 +447,7 @@ const ScreenshotsView = memo(function ScreenshotsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
DELETE {t("screenshots.delete")}
</button> </button>
</div> </div>
</motion.div> </motion.div>
+133 -65
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useMemo, memo, useCallback } from "react"; import { useState, useEffect, useRef, useMemo, memo, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { TauriService, Runner } from "../../services/TauriService"; import { TauriService, Runner } from "../../services/TauriService";
import { usePlatform } from "../../hooks/usePlatform"; import { usePlatform } from "../../hooks/usePlatform";
@@ -10,8 +11,10 @@ import {
} from "../../context/LauncherContext"; } from "../../context/LauncherContext";
import { PluginManager, type PluginInfo } from "../../plugins/PluginManager"; import { PluginManager, type PluginInfo } from "../../plugins/PluginManager";
import { usePluginActions } from "../../plugins/PluginContext"; import { usePluginActions } from "../../plugins/PluginContext";
import i18n from "../../i18n/config";
const SettingsView = memo(function SettingsView() { const SettingsView = memo(function SettingsView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { const {
vfxEnabled, vfxEnabled,
@@ -61,7 +64,14 @@ const SettingsView = memo(function SettingsView() {
const { isLinux, isMac, isAndroid } = usePlatform(); const { isLinux, isMac, isAndroid } = usePlatform();
const [focusIndex, setFocusIndex] = useState<number | null>(null); const [focusIndex, setFocusIndex] = useState<number | null>(null);
const [currentSubMenu, setCurrentSubMenu] = useState< const [currentSubMenu, setCurrentSubMenu] = useState<
"main" | "audio" | "video" | "launcher" | "game" | "plugins" | "android" | "main"
| "audio"
| "video"
| "launcher"
| "game"
| "plugins"
| "android"
| "language"
>("main"); >("main");
const [runners, setRunners] = useState<Runner[]>([]); const [runners, setRunners] = useState<Runner[]>([]);
const [pluginsInfo, setPluginsInfo] = useState<PluginInfo[]>([]); const [pluginsInfo, setPluginsInfo] = useState<PluginInfo[]>([]);
@@ -155,11 +165,11 @@ const SettingsView = memo(function SettingsView() {
"fixed inset-0 bg-black/80 flex items-center justify-center z-50"; "fixed inset-0 bg-black/80 flex items-center justify-center z-50";
dialog.innerHTML = ` dialog.innerHTML = `
<div class="w-[420px] p-4 flex flex-col items-center mc-options-bg"> <div class="w-[420px] p-4 flex flex-col items-center mc-options-bg">
<h3 class="text-2xl 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">${t("settings.reset.title")}</h3>
<p class="text-[#333333] mb-8 text-left w-full px-4">Are you sure you want to reset launcher setup?</p> <p class="text-[#333333] mb-8 text-left w-full px-4">${t("settings.reset.confirm")}</p>
<div class="flex flex-col gap-3 w-full px-4"> <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> <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)'">${t("common.cancel")}</button>
<button id="reset-ok" 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)'">OK</button> <button id="reset-ok" 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)'">${t("common.ok")}</button>
</div> </div>
</div> </div>
`; `;
@@ -193,20 +203,20 @@ const SettingsView = memo(function SettingsView() {
"fixed inset-0 bg-black/80 flex items-center justify-center z-50"; "fixed inset-0 bg-black/80 flex items-center justify-center z-50";
dialog.innerHTML = ` dialog.innerHTML = `
<div class="w-[420px] p-4 flex flex-col items-center mc-options-bg"> <div class="w-[420px] p-4 flex flex-col items-center mc-options-bg">
<h3 class="text-2xl 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">${t("settings.reset.confirmTitle")}</h3>
<div class="text-[#333333] mb-6 text-left w-full px-4"> <div class="text-[#333333] mb-6 text-left w-full px-4">
<p class="mb-2">⚠️ This will:</p> <p class="mb-2">${t("settings.reset.warning")}</p>
<ul class="list-none space-y-1 text-sm"> <ul class="list-none space-y-1 text-sm">
<li>Clear all launcher settings</li> <li>${t("settings.reset.clearAllSettings")}</li>
<li>Reset your username</li> <li>${t("settings.reset.resetUsername")}</li>
<li>Show setup screen again</li> <li>${t("settings.reset.showSetupAgain")}</li>
<li>Require reconfiguration</li> <li>${t("settings.reset.requireReconfiguration")}</li>
</ul> </ul>
<p class="mt-3 text-[#333333]">This action cannot be undone!</p> <p class="mt-3 text-[#333333]">${t("settings.reset.cannotBeUndone")}</p>
</div> </div>
<div class="flex flex-col gap-3 w-full px-4"> <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> <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)'">${t("common.cancel")}</button>
<button id="reset-final-ok" 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)'">OK</button> <button id="reset-final-ok" 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)'">${t("common.ok")}</button>
</div> </div>
</div> </div>
`; `;
@@ -276,6 +286,7 @@ const SettingsView = memo(function SettingsView() {
onClick: () => void; onClick: () => void;
small?: boolean; small?: boolean;
color?: string; color?: string;
textured?: boolean;
}; };
const settingsItems = useMemo<SettingsItem[]>(() => { const settingsItems = useMemo<SettingsItem[]>(() => {
@@ -284,7 +295,7 @@ const SettingsView = memo(function SettingsView() {
if (currentSubMenu === "main") { if (currentSubMenu === "main") {
items.push({ items.push({
id: "audio_menu", id: "audio_menu",
label: "Audio", label: t("settings.audio"),
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -294,7 +305,7 @@ const SettingsView = memo(function SettingsView() {
}); });
items.push({ items.push({
id: "video_menu", id: "video_menu",
label: "Video", label: t("settings.video"),
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -304,7 +315,7 @@ const SettingsView = memo(function SettingsView() {
}); });
items.push({ items.push({
id: "launcher_menu", id: "launcher_menu",
label: "Launcher", label: t("settings.launcher"),
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -314,7 +325,7 @@ const SettingsView = memo(function SettingsView() {
}); });
items.push({ items.push({
id: "game_menu", id: "game_menu",
label: "Game", label: t("settings.game"),
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -325,7 +336,7 @@ const SettingsView = memo(function SettingsView() {
if (!isAndroid) { if (!isAndroid) {
items.push({ items.push({
id: "controls_menu", id: "controls_menu",
label: "Controls", label: t("settings.controls"),
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -335,7 +346,7 @@ const SettingsView = memo(function SettingsView() {
} }
items.push({ items.push({
id: "plugins_menu", id: "plugins_menu",
label: "Plugins", label: t("settings.plugins"),
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -346,7 +357,7 @@ const SettingsView = memo(function SettingsView() {
if (isAndroid) { if (isAndroid) {
items.push({ items.push({
id: "android_menu", id: "android_menu",
label: "Android", label: t("settings.android"),
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -368,7 +379,7 @@ const SettingsView = memo(function SettingsView() {
} }
items.push({ items.push({
id: "credits", id: "credits",
label: "Credits", label: t("settings.credits"),
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -378,14 +389,14 @@ const SettingsView = memo(function SettingsView() {
} else if (currentSubMenu === "audio") { } else if (currentSubMenu === "audio") {
items.push({ items.push({
id: "music", id: "music",
label: `Music: ${musicVolume ?? 50}%`, label: t("settings.music", { value: musicVolume ?? 50 }),
type: "slider", type: "slider",
value: musicVolume ?? 50, value: musicVolume ?? 50,
onChange: setMusicVolume, onChange: setMusicVolume,
}); });
items.push({ items.push({
id: "sfx", id: "sfx",
label: `Sound: ${sfxVolume ?? 100}%`, label: t("settings.sound", { value: sfxVolume ?? 100 }),
type: "slider", type: "slider",
value: sfxVolume ?? 100, value: sfxVolume ?? 100,
onChange: setSfxVolume, onChange: setSfxVolume,
@@ -399,20 +410,20 @@ const SettingsView = memo(function SettingsView() {
} else if (currentSubMenu === "video") { } else if (currentSubMenu === "video") {
items.push({ items.push({
id: "vfx", id: "vfx",
label: `Click effects: ${vfxEnabled ? "ON" : "OFF"}`, label: `${t("settings.clickEffects")}: ${vfxEnabled ? t("common.on") : t("common.off")}`,
type: "button", type: "button",
onClick: handleVfxToggle, onClick: handleVfxToggle,
}); });
items.push({ items.push({
id: "animations", id: "animations",
label: `Animations: ${animationsEnabled ? "ON" : "OFF"}`, label: `${t("settings.animations")}: ${animationsEnabled ? t("common.on") : t("common.off")}`,
type: "button", type: "button",
onClick: handleAnimationsToggle, onClick: handleAnimationsToggle,
}); });
if (isMac) { if (isMac) {
items.push({ items.push({
id: "perf", id: "perf",
label: `Apple silicon performance boost: ${perfBoost ? "Enabled" : "Disabled"}`, label: `${t("settings.appleSiliconPerfBoost")}: ${perfBoost ? t("common.enabled") : t("common.disabled")}`,
type: "button", type: "button",
onClick: handlePerfToggle, onClick: handlePerfToggle,
}); });
@@ -425,8 +436,8 @@ const SettingsView = memo(function SettingsView() {
id: "extra_launch_args", id: "extra_launch_args",
label: label:
extraLaunchArgs && extraLaunchArgs.length > 0 extraLaunchArgs && extraLaunchArgs.length > 0
? `Extra Args: ${extraLaunchArgs.join(" ")}` ? `${t("settings.extraLaunchArgs")}: ${extraLaunchArgs.join(" ")}`
: "Extra Launch Args: None", : `${t("settings.extraLaunchArgs")}: ${t("common.none")}`,
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -436,7 +447,9 @@ const SettingsView = memo(function SettingsView() {
}); });
items.push({ items.push({
id: "launch_prefix", id: "launch_prefix",
label: launchPrefix ? `Prefix: ${launchPrefix}` : "Launch Prefix: None", label: launchPrefix
? `${t("settings.launchPrefix")}: ${launchPrefix}`
: `${t("settings.launchPrefix")}: ${t("common.none")}`,
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -446,7 +459,11 @@ const SettingsView = memo(function SettingsView() {
}); });
items.push({ items.push({
id: "launch_env_vars", id: "launch_env_vars",
label: `Launch Env Vars: ${envVarsCount > 0 ? `${envVarsCount} set` : "None"}`, label: `${t("settings.launchEnvVars")}: ${
envVarsCount > 0
? `${envVarsCount} ${t("common.set")}`
: t("common.none")
}`,
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -463,47 +480,60 @@ const SettingsView = memo(function SettingsView() {
if (!isAndroid) { if (!isAndroid) {
items.push({ items.push({
id: "fullscreen", id: "fullscreen",
label: `Start in Fullscreen: ${startFullscreen ? "ON" : "OFF"}`, label: `${t("settings.startInFullscreen")}: ${startFullscreen ? t("common.on") : t("common.off")}`,
type: "button", type: "button",
onClick: handleFullscreenToggle, onClick: handleFullscreenToggle,
}); });
items.push({ items.push({
id: "rpc", id: "rpc",
label: `Discord RPC: ${rpcEnabled ? "ON" : "OFF"}`, label: `${t("settings.discordRpc")}: ${rpcEnabled ? t("common.on") : t("common.off")}`,
type: "button", type: "button",
onClick: handleRpcToggle, onClick: handleRpcToggle,
}); });
} }
items.push({ items.push({
id: "skip_intro", id: "skip_intro",
label: `Skip Intro: ${skipIntro ? "ON" : "OFF"}`, label: `${t("settings.skipIntro")}: ${skipIntro ? t("common.on") : t("common.off")}`,
type: "button", type: "button",
onClick: handleSkipIntroToggle, onClick: handleSkipIntroToggle,
}); });
items.push({ items.push({
id: "legacy", id: "legacy",
label: `Legacy Mode: ${legacyMode ? "ON" : "OFF"}`, label: `${t("settings.legacyMode")}: ${legacyMode ? t("common.on") : t("common.off")}`,
type: "button", type: "button",
onClick: handleLegacyToggle, onClick: handleLegacyToggle,
}); });
items.push({
id: "languages",
label: t("settings.languages"),
type: "button",
textured: true,
onClick: () => {
playPressSound();
setCurrentSubMenu("language");
setFocusIndex(0);
},
});
if (isLinux && !isAndroid) { if (isLinux && !isAndroid) {
items.push({ items.push({
id: "runner", id: "runner",
label: `Runner: ${selectedRunnerName}`, label: `${t("settings.runner")}: ${selectedRunnerName}`,
type: "button", type: "button",
onClick: handleRunnerToggle, onClick: handleRunnerToggle,
}); });
items.push({ items.push({
id: "mangohud", id: "mangohud",
label: `MangoHud: ${mangohudEnabled ? "ON" : "OFF"}`, label: `${t("settings.mangohud")}: ${mangohudEnabled ? t("common.on") : t("common.off")}`,
type: "button", type: "button",
onClick: handleMangohudToggle, onClick: handleMangohudToggle,
}); });
items.push({ items.push({
id: "download_runner", id: "download_runner",
label: isRunnerDownloading label: isRunnerDownloading
? `Downloading Runner... ${Math.floor(runnerDownloadProgress || 0)}%` ? t("settings.downloadingRunner", {
: "Download GE-Proton (Recommended)", progress: Math.floor(runnerDownloadProgress || 0),
})
: t("settings.downloadRunner"),
type: "button", type: "button",
onClick: () => { onClick: () => {
if (!isRunnerDownloading) { if (!isRunnerDownloading) {
@@ -520,7 +550,7 @@ const SettingsView = memo(function SettingsView() {
if (!isAndroid) { if (!isAndroid) {
items.push({ items.push({
id: "export_settings", id: "export_settings",
label: "Export Settings", label: t("settings.exportSettings"),
type: "button", type: "button",
onClick: async () => { onClick: async () => {
playPressSound(); playPressSound();
@@ -535,7 +565,7 @@ const SettingsView = memo(function SettingsView() {
if (!isAndroid) { if (!isAndroid) {
items.push({ items.push({
id: "import_settings", id: "import_settings",
label: "Import Settings", label: t("settings.importSettings"),
type: "button", type: "button",
onClick: async () => { onClick: async () => {
playPressSound(); playPressSound();
@@ -550,16 +580,33 @@ const SettingsView = memo(function SettingsView() {
} }
items.push({ items.push({
id: "reset_setup", id: "reset_setup",
label: "Reset Setup", label: t("settings.resetSetup"),
type: "button", type: "button",
onClick: handleResetSetup, onClick: handleResetSetup,
color: "orange", color: "orange",
}); });
} else if (currentSubMenu === "language") {
const availableLanguages = [
//neo: here goes the list, dont ask why its here
{ code: "en", label: "English" },
];
for (const lang of availableLanguages) {
const isCurrent = i18n.resolvedLanguage?.startsWith(lang.code);
items.push({
id: `lang_${lang.code}`,
label: `${lang.label}${isCurrent ? ` (${t("common.enabled")})` : ""}`,
type: "button",
onClick: () => {
playPressSound();
void i18n.changeLanguage(lang.code);
},
});
}
} else if (currentSubMenu === "android") { } else if (currentSubMenu === "android") {
if (profile) { if (profile) {
items.push({ items.push({
id: "android_container_settings", id: "android_container_settings",
label: "Container Settings", label: t("settings.containerSettings"),
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -568,7 +615,7 @@ const SettingsView = memo(function SettingsView() {
}); });
items.push({ items.push({
id: "android_open_container", id: "android_open_container",
label: "Open Container", label: t("settings.openContainer"),
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -598,7 +645,10 @@ const SettingsView = memo(function SettingsView() {
});*/ });*/
items.push({ items.push({
id: "android_audio", id: "android_audio",
label: `Audio: ${androidAudioBackend === "alsa" ? "ALSA" : "PulseAudio (Default)"}`, label: t("settings.androidAudio", {
backend:
androidAudioBackend === "alsa" ? "ALSA" : "PulseAudio (Default)",
}),
type: "button", type: "button",
onClick: () => { onClick: () => {
playPressSound(); playPressSound();
@@ -612,7 +662,7 @@ const SettingsView = memo(function SettingsView() {
if (isGameRunning) { if (isGameRunning) {
items.push({ items.push({
id: "stop", id: "stop",
label: "STOP GAME", label: t("settings.stopGameButton"),
type: "button", type: "button",
onClick: stopGame, onClick: stopGame,
color: "red", color: "red",
@@ -621,12 +671,15 @@ const SettingsView = memo(function SettingsView() {
items.push({ items.push({
id: "back", id: "back",
label: currentSubMenu === "main" ? "Done" : "Back", label: currentSubMenu === "main" ? t("common.done") : t("common.back"),
type: "button", type: "button",
onClick: () => { onClick: () => {
playBackSound(); playBackSound();
if (currentSubMenu === "main") { if (currentSubMenu === "main") {
setActiveView("main"); setActiveView("main");
} else if (currentSubMenu === "language") {
setCurrentSubMenu("launcher");
setFocusIndex(0);
} else { } else {
setCurrentSubMenu("main"); setCurrentSubMenu("main");
setFocusIndex(0); setFocusIndex(0);
@@ -636,6 +689,7 @@ const SettingsView = memo(function SettingsView() {
return items; return items;
}, [ }, [
t,
currentSubMenu, currentSubMenu,
pluginSettingsActions, pluginSettingsActions,
musicVolume, musicVolume,
@@ -690,7 +744,9 @@ const SettingsView = memo(function SettingsView() {
} }
playBackSound(); playBackSound();
if (currentSubMenu !== "main") { if (currentSubMenu !== "main") {
setCurrentSubMenu("main"); setCurrentSubMenu(
currentSubMenu === "language" ? "launcher" : "main",
);
setFocusIndex(0); setFocusIndex(0);
} else { } else {
setActiveView("main"); setActiveView("main");
@@ -854,7 +910,7 @@ const SettingsView = memo(function SettingsView() {
<div className="w-full space-y-3 flex flex-col items-center overflow-y-auto max-h-[50vh] py-2 settings-scrollbar"> <div className="w-full space-y-3 flex flex-col items-center overflow-y-auto max-h-[50vh] py-2 settings-scrollbar">
{pluginsInfo.length === 0 ? ( {pluginsInfo.length === 0 ? (
<div className="text-[#888888] text-lg mc-text-shadow py-8"> <div className="text-[#888888] text-lg mc-text-shadow py-8">
No plugins installed {t("settings.noPluginsInstalled")}
</div> </div>
) : ( ) : (
pluginsInfo.map((p, index) => { pluginsInfo.map((p, index) => {
@@ -908,7 +964,8 @@ const SettingsView = memo(function SettingsView() {
{p.manifest.description} {p.manifest.description}
</span> </span>
<span className="text-xs mc-text-shadow opacity-50"> <span className="text-xs mc-text-shadow opacity-50">
by {p.manifest.author} &middot; v{p.manifest.version} {t("settings.byAuthor", { author: p.manifest.author })}{" "}
&middot; v{p.manifest.version}
</span> </span>
</div> </div>
</div> </div>
@@ -957,16 +1014,27 @@ const SettingsView = memo(function SettingsView() {
const isRed = item.type === "button" && item.color === "red"; const isRed = item.type === "button" && item.color === "red";
const isSmall = item.type === "button" && !!item.small; const isSmall = item.type === "button" && !!item.small;
const isTextured = item.type === "button" && !!item.textured;
const isToggle = isToggleOption(item.label); const isToggle = isToggleOption(item.label);
const toggleState = isToggle ? getToggleState(item.label) : false; const toggleState = isToggle ? getToggleState(item.label) : false;
return ( return (
<button <button
key={item.id} key={item.id}
data-index={index} data-index={index}
onMouseEnter={() => setFocusIndex(index)} onMouseEnter={() => setFocusIndex(index)}
onClick={item.onClick} onClick={item.onClick}
className={`w-[600px] h-10 flex items-center pl-1.5 pr-4 relative z-30 outline-none border-none shrink-0 rounded ${focusIndex === index ? "text-[#ffff00]" : isRed ? "text-red-600" : "text-[#333333]"}`} className={`w-[600px] h-10 flex items-center relative z-30 outline-none border-none shrink-0 rounded ${isTextured ? "pl-3" : "pl-1.5 pr-4"} ${
isTextured
? focusIndex === index
? "text-[#ffff00]"
: "text-white"
: focusIndex === index
? "text-[#ffff00]"
: isRed
? "text-red-600"
: "text-[#333333]"
}`}
style={isTextured ? getItemStyle(index) : undefined}
> >
{isToggle && ( {isToggle && (
<div className="relative w-6 h-6 mr-3 shrink-0 flex items-center justify-center"> <div className="relative w-6 h-6 mr-3 shrink-0 flex items-center justify-center">
@@ -1023,7 +1091,7 @@ const SettingsView = memo(function SettingsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Back {t("common.back")}
</button> </button>
); );
})()} })()}
@@ -1043,10 +1111,10 @@ const SettingsView = memo(function SettingsView() {
}} }}
> >
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase"> <h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
Extra Launch Args {t("settings.extraLaunchArgs")}
</h2> </h2>
<p className="text-[#AAAAAA] text-xs mb-4 text-center mc-text-shadow"> <p className="text-[#AAAAAA] text-xs mb-4 text-center mc-text-shadow">
Space-separated arguments passed to the game executable {t("settings.extraLaunchArgsDesc")}
</p> </p>
<input <input
autoFocus autoFocus
@@ -1077,7 +1145,7 @@ const SettingsView = memo(function SettingsView() {
"url('/images/Button_Background.png')") "url('/images/Button_Background.png')")
} }
> >
Cancel {t("common.cancel")}
</button> </button>
<button <button
onClick={() => { onClick={() => {
@@ -1101,7 +1169,7 @@ const SettingsView = memo(function SettingsView() {
"url('/images/Button_Background.png')") "url('/images/Button_Background.png')")
} }
> >
Save {t("common.save")}
</button> </button>
</div> </div>
</div> </div>
@@ -1123,10 +1191,10 @@ const SettingsView = memo(function SettingsView() {
}} }}
> >
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase"> <h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
Launch Prefix {t("settings.launchPrefix")}
</h2> </h2>
<p className="text-[#AAAAAA] text-xs mb-4 text-center mc-text-shadow"> <p className="text-[#AAAAAA] text-xs mb-4 text-center mc-text-shadow">
Command that wraps the entire launch (e.g. gamemoderun) {t("settings.launchPrefixDesc")}
</p> </p>
<input <input
autoFocus autoFocus
@@ -1157,7 +1225,7 @@ const SettingsView = memo(function SettingsView() {
"url('/images/Button_Background.png')") "url('/images/Button_Background.png')")
} }
> >
Cancel {t("common.cancel")}
</button> </button>
<button <button
onClick={() => { onClick={() => {
@@ -1180,7 +1248,7 @@ const SettingsView = memo(function SettingsView() {
"url('/images/Button_Background.png')") "url('/images/Button_Background.png')")
} }
> >
Save {t("common.save")}
</button> </button>
</div> </div>
</div> </div>
@@ -1202,10 +1270,10 @@ const SettingsView = memo(function SettingsView() {
}} }}
> >
<h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase"> <h2 className="text-[#FFFF55] text-2xl mc-text-shadow mb-4 border-b-2 border-[#373737] pb-2 w-full text-center uppercase">
Launch Env Vars {t("settings.launchEnvVars")}
</h2> </h2>
<p className="text-[#AAAAAA] text-xs mb-4 text-center mc-text-shadow"> <p className="text-[#AAAAAA] text-xs mb-4 text-center mc-text-shadow">
One KEY=VALUE per line {t("settings.launchEnvVarsDesc")}
</p> </p>
<textarea <textarea
autoFocus autoFocus
@@ -1237,7 +1305,7 @@ const SettingsView = memo(function SettingsView() {
"url('/images/Button_Background.png')") "url('/images/Button_Background.png')")
} }
> >
Cancel {t("common.cancel")}
</button> </button>
<button <button
onClick={() => { onClick={() => {
@@ -1276,7 +1344,7 @@ const SettingsView = memo(function SettingsView() {
"url('/images/Button_Background.png')") "url('/images/Button_Background.png')")
} }
> >
Save {t("common.save")}
</button> </button>
</div> </div>
</div> </div>
+36 -38
View File
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { TauriService, Runner } from "../../services/TauriService"; import { TauriService, Runner } from "../../services/TauriService";
import { usePlatform } from "../../hooks/usePlatform"; import { usePlatform } from "../../hooks/usePlatform";
import { useConfig, useAudio } from "../../context/LauncherContext"; import { useConfig, useAudio } from "../../context/LauncherContext";
@@ -7,6 +8,7 @@ interface SetupViewProps {
} }
const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => { const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
const { t } = useTranslation();
const { isLinux, isMac, isAndroid } = usePlatform(); const { isLinux, isMac, isAndroid } = usePlatform();
const { const {
username, username,
@@ -181,14 +183,14 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
setIsSettingUpRuntime(true); setIsSettingUpRuntime(true);
setSetupProgress({ setSetupProgress({
stage: "preparing", stage: "preparing",
message: "Preparing macOS runtime setup...", message: t("setup.macosPreparing"),
percent: 0, percent: 0,
}); });
try { try {
await TauriService.setupMacosRuntime(); await TauriService.setupMacosRuntime();
setSetupProgress({ setSetupProgress({
stage: "completed", stage: "completed",
message: "Setup completed successfully!", message: t("setup.macosCompleted"),
percent: 100, percent: 100,
}); });
localStorage.setItem("lce-macos-runtime-installed", "true"); localStorage.setItem("lce-macos-runtime-installed", "true");
@@ -201,7 +203,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
} catch (e) { } catch (e) {
setSetupProgress({ setSetupProgress({
stage: "error", stage: "error",
message: `Setup failed: ${e}`, message: t("setup.macosFailed", { error: e }),
percent: 0, percent: 0,
}); });
setIsSettingUpRuntime(false); setIsSettingUpRuntime(false);
@@ -243,17 +245,17 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
> >
{currentStep === 0 && ( {currentStep === 0 && (
<p className="text-white text-sm tracking-widest text-center uppercase mb-4"> <p className="text-white text-sm tracking-widest text-center uppercase mb-4">
Let's configure your launcher {t("setup.letsConfigure")}
</p> </p>
)} )}
{currentStep === 1 && ( {currentStep === 1 && (
<p className="text-white text-xs tracking-widest text-center uppercase mb-4"> <p className="text-white text-xs tracking-widest text-center uppercase mb-4">
Compatibility Runtime {t("setup.compatibilityRuntime")}
</p> </p>
)} )}
{currentStep === 2 && ( {currentStep === 2 && (
<p className="text-white text-xs tracking-widest text-center uppercase mb-4"> <p className="text-white text-xs tracking-widest text-center uppercase mb-4">
Choose your preferred options and behaviors {t("setup.choosePreferredOptions")}
</p> </p>
)} )}
<div <div
@@ -267,7 +269,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
<div className="p-5 flex flex-col gap-4 mc-options-bg"> <div className="p-5 flex flex-col gap-4 mc-options-bg">
<label className="block relative"> <label className="block relative">
<span className="text-black font-bold uppercase tracking-widest text-sm block mb-2"> <span className="text-black font-bold uppercase tracking-widest text-sm block mb-2">
Username {t("setup.username")}
</span> </span>
<div className="relative"> <div className="relative">
<input <input
@@ -330,7 +332,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
backgroundColor: "#646464", backgroundColor: "#646464",
caretColor: "transparent", caretColor: "transparent",
}} }}
placeholder="Enter your username" placeholder={t("setup.enterUsername")}
maxLength={16} maxLength={16}
autoFocus autoFocus
/> />
@@ -350,7 +352,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
</label> </label>
{tempUsername.trim().length === 0 && ( {tempUsername.trim().length === 0 && (
<p className="text-gray-500 text-xs text-center uppercase tracking-widest"> <p className="text-gray-500 text-xs text-center uppercase tracking-widest">
A username is required to continue {t("setup.usernameRequired")}
</p> </p>
)} )}
</div> </div>
@@ -376,13 +378,13 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
className={`font-bold text-sm uppercase tracking-widest ${runtimeAlreadyInstalled ? "text-green-700" : "text-yellow-700"}`} className={`font-bold text-sm uppercase tracking-widest ${runtimeAlreadyInstalled ? "text-green-700" : "text-yellow-700"}`}
> >
{runtimeAlreadyInstalled {runtimeAlreadyInstalled
? "Runtime Detected" ? t("setup.runtimeDetected")
: "Runtime Not Detected"} : t("setup.runtimeNotDetected")}
</p> </p>
<p className="text-gray-700 text-xs mt-0.5"> <p className="text-gray-700 text-xs mt-0.5">
{runtimeAlreadyInstalled {runtimeAlreadyInstalled
? "Game Porting Toolkit 3 and Wine installed" ? t("setup.gptInstalled")
: "You must install the runtime before proceeding."} : t("setup.mustInstallRuntime")}
</p> </p>
</div> </div>
</div> </div>
@@ -417,10 +419,10 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
> >
<span className="tracking-widest uppercase text-lg"> <span className="tracking-widest uppercase text-lg">
{isSettingUpRuntime {isSettingUpRuntime
? "Installing..." ? t("setup.installing")
: runtimeAlreadyInstalled : runtimeAlreadyInstalled
? "Reinstall Runtime" ? t("setup.reinstallRuntime")
: "Install Runtime"} : t("setup.installRuntime")}
</span> </span>
</button> </button>
</div> </div>
@@ -432,8 +434,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
{runners.length === 0 ? ( {runners.length === 0 ? (
<div className="p-3 border-2 border-yellow-400/50 bg-yellow-50"> <div className="p-3 border-2 border-yellow-400/50 bg-yellow-50">
<p className="text-yellow-600 text-sm text-center"> <p className="text-yellow-600 text-sm text-center">
No compatible runners found. Please install Wine or {t("setup.noCompatibleRunners")}
Proton.
</p> </p>
</div> </div>
) : ( ) : (
@@ -459,7 +460,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
</div> </div>
)} )}
<p className="text-xs text-gray-500 text-center uppercase tracking-widest mt-1"> <p className="text-xs text-gray-500 text-center uppercase tracking-widest mt-1">
You can change this later in settings {t("setup.changeLaterInSettings")}
</p> </p>
</div> </div>
)} )}
@@ -470,11 +471,10 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
<span className="text-green-400 text-xl">✓</span> <span className="text-green-400 text-xl">✓</span>
<div> <div>
<p className="text-green-600 font-bold text-sm uppercase tracking-widest"> <p className="text-green-600 font-bold text-sm uppercase tracking-widest">
Windows Native Support {t("setup.windowsNativeSupport")}
</p> </p>
<p className="text-gray-600 text-xs mt-0.5"> <p className="text-gray-600 text-xs mt-0.5">
Emerald Legacy runs natively on Windows without {t("setup.windowsNativeDesc")}
additional requirements.
</p> </p>
</div> </div>
</div> </div>
@@ -487,12 +487,10 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
<span className="text-green-400 text-xl">✓</span> <span className="text-green-400 text-xl">✓</span>
<div> <div>
<p className="text-green-600 font-bold text-sm uppercase tracking-widest"> <p className="text-green-600 font-bold text-sm uppercase tracking-widest">
DiamondRuntime Support {t("setup.diamondRuntimeSupport")}
</p> </p>
<p className="text-gray-600 text-xs mt-0.5"> <p className="text-gray-600 text-xs mt-0.5">
Emerald Launcher runs natively on Android without {t("setup.diamondRuntimeDesc")}
additional requirements. Powered by our own
DiamondRuntime.
</p> </p>
</div> </div>
</div> </div>
@@ -514,7 +512,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
<span <span
className={`tracking-widest uppercase text-lg ${focusIndex === 0 ? "text-[#FFFF55]" : "text-gray-800"}`} className={`tracking-widest uppercase text-lg ${focusIndex === 0 ? "text-[#FFFF55]" : "text-gray-800"}`}
> >
Discord RPC {t("setup.discordRpc")}
</span> </span>
<div className="relative w-6 h-6 shrink-0"> <div className="relative w-6 h-6 shrink-0">
<img <img
@@ -549,7 +547,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
<span <span
className={`tracking-widest uppercase text-lg ${focusIndex === (isAndroid ? 0 : 1) ? "text-[#FFFF55]" : "text-gray-800"}`} className={`tracking-widest uppercase text-lg ${focusIndex === (isAndroid ? 0 : 1) ? "text-[#FFFF55]" : "text-gray-800"}`}
> >
Animations {t("setup.animations")}
</span> </span>
<div className="relative w-6 h-6 shrink-0"> <div className="relative w-6 h-6 shrink-0">
<img <img
@@ -574,7 +572,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
</button> </button>
<p className="text-xs text-gray-500 text-center uppercase tracking-widest mt-2"> <p className="text-xs text-gray-500 text-center uppercase tracking-widest mt-2">
You can change these later in settings {t("setup.changeTheseLater")}
</p> </p>
</div> </div>
)} )}
@@ -582,13 +580,13 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
{currentStep === 3 && ( {currentStep === 3 && (
<div className="p-5 flex flex-col gap-3 mc-options-bg"> <div className="p-5 flex flex-col gap-3 mc-options-bg">
<p className="text-gray-700 text-xs tracking-widest text-center uppercase"> <p className="text-gray-700 text-xs tracking-widest text-center uppercase">
Emerald Launcher is now configured and ready to use! {t("setup.setupComplete")}
</p> </p>
<div className="flex flex-col gap-1 mt-1"> <div className="flex flex-col gap-1 mt-1">
<div className="flex items-center justify-between px-4 h-10 border-b border-white/10"> <div className="flex items-center justify-between px-4 h-10 border-b border-white/10">
<span className="text-gray-600 text-sm uppercase tracking-widest"> <span className="text-gray-600 text-sm uppercase tracking-widest">
Username {t("setup.username")}
</span> </span>
<span className="text-[#FFFF55] font-bold mc-text-shadow"> <span className="text-[#FFFF55] font-bold mc-text-shadow">
{tempUsername} {tempUsername}
@@ -597,15 +595,15 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
{isMac && ( {isMac && (
<div className="flex items-center justify-between px-4 h-10 border-b border-white/10"> <div className="flex items-center justify-between px-4 h-10 border-b border-white/10">
<span className="text-gray-600 text-sm uppercase tracking-widest"> <span className="text-gray-600 text-sm uppercase tracking-widest">
Runtime {t("setup.runtime")}
</span> </span>
<span className="text-green-400 font-bold">Ready</span> <span className="text-green-400 font-bold">{t("setup.ready")}</span>
</div> </div>
)} )}
{isLinux && selectedRunner && ( {isLinux && selectedRunner && (
<div className="flex items-center justify-between px-4 h-10 border-b border-white/10"> <div className="flex items-center justify-between px-4 h-10 border-b border-white/10">
<span className="text-gray-600 text-sm uppercase tracking-widest"> <span className="text-gray-600 text-sm uppercase tracking-widest">
Runner {t("setup.runner")}
</span> </span>
<span className="text-green-400 font-bold"> <span className="text-green-400 font-bold">
{runners.find((r) => r.id === selectedRunner)?.name} {runners.find((r) => r.id === selectedRunner)?.name}
@@ -614,7 +612,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
)} )}
<div className="flex items-center justify-between px-4 h-10 border-b border-white/10"> <div className="flex items-center justify-between px-4 h-10 border-b border-white/10">
<span className="text-gray-600 text-sm uppercase tracking-widest"> <span className="text-gray-600 text-sm uppercase tracking-widest">
Animations {t("setup.animations")}
</span> </span>
<div className="relative w-5 h-5"> <div className="relative w-5 h-5">
<img <img
@@ -636,7 +634,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
{!isAndroid && ( {!isAndroid && (
<div className="flex items-center justify-between px-4 h-10"> <div className="flex items-center justify-between px-4 h-10">
<span className="text-gray-600 text-sm uppercase tracking-widest"> <span className="text-gray-600 text-sm uppercase tracking-widest">
Discord RPC {t("setup.discordRpc")}
</span> </span>
<div className="relative w-5 h-5"> <div className="relative w-5 h-5">
<img <img
@@ -695,7 +693,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
)} )}
> >
<span className="tracking-widest uppercase text-xl"> <span className="tracking-widest uppercase text-xl">
Back {t("common.back")}
</span> </span>
</button> </button>
) : ( ) : (
@@ -737,7 +735,7 @@ const SetupView: React.FC<SetupViewProps> = ({ onComplete }) => {
)} )}
> >
<span className="tracking-widest uppercase text-xl"> <span className="tracking-widest uppercase text-xl">
{currentStep === totalSteps - 1 ? "Finish" : "Next"} {currentStep === totalSteps - 1 ? t("common.finish") : t("common.next")}
</span> </span>
</button> </button>
</div> </div>
+17 -15
View File
@@ -1,4 +1,5 @@
import { memo, useCallback, useEffect, useRef, useState } from "react"; import { memo, useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useUI, useAudio, useConfig, useSkin } from "../../context/LauncherContext"; import { useUI, useAudio, useConfig, useSkin } from "../../context/LauncherContext";
import { useLocalStorage } from "../../hooks/useLocalStorage"; import { useLocalStorage } from "../../hooks/useLocalStorage";
@@ -110,6 +111,7 @@ function fillRegion(data: Uint8ClampedArray, w: number, h: number, x: number, y:
} }
const SkinEditorView = memo(function SkinEditorView() { const SkinEditorView = memo(function SkinEditorView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { animationsEnabled, username } = useConfig(); const { animationsEnabled, username } = useConfig();
@@ -392,7 +394,7 @@ const SkinEditorView = memo(function SkinEditorView() {
if (existing) { if (existing) {
setStoredSkins(storedSkins.map((s) => (s.id === existing.id ? { ...s, url: dataUrl, isSlim: slim } : s))); setStoredSkins(storedSkins.map((s) => (s.id === existing.id ? { ...s, url: dataUrl, isSlim: slim } : s)));
} else { } else {
setStoredSkins([...storedSkins, { id: Date.now().toString(), name: "Custom Skin", url: dataUrl, isSlim: slim }]); setStoredSkins([...storedSkins, { id: Date.now().toString(), name: t("skinEditor.customSkin"), url: dataUrl, isSlim: slim }]);
} }
setSkinUrl(dataUrl); setSkinUrl(dataUrl);
setSkinIsSlim(slim); setSkinIsSlim(slim);
@@ -406,7 +408,7 @@ const SkinEditorView = memo(function SkinEditorView() {
try { try {
const safeName = (username || "skin").replace(/[^a-zA-Z0-9_-]/g, "") || "skin"; const safeName = (username || "skin").replace(/[^a-zA-Z0-9_-]/g, "") || "skin";
const fileName = `${safeName}.png`; const fileName = `${safeName}.png`;
const path = await TauriService.saveFileDialog("Export Skin", fileName, []); const path = await TauriService.saveFileDialog(t("skinEditor.exportSkin"), fileName, []);
if (!path) return; if (!path) return;
const outPath = path.toLowerCase().endsWith(".png") ? path : `${path}.png`; const outPath = path.toLowerCase().endsWith(".png") ? path : `${path}.png`;
const res = await fetch(cvs.toDataURL("image/png")); const res = await fetch(cvs.toDataURL("image/png"));
@@ -444,10 +446,10 @@ const SkinEditorView = memo(function SkinEditorView() {
}, [playBackSound, setActiveView, handleUndo, showPicker]); }, [playBackSound, setActiveView, handleUndo, showPicker]);
const tools: { id: Tool; label: string }[] = [ const tools: { id: Tool; label: string }[] = [
{ id: "pencil", label: "Pencil" }, { id: "pencil", label: t("skinEditor.pencil") },
{ id: "eraser", label: "Eraser" }, { id: "eraser", label: t("skinEditor.eraser") },
{ id: "eyedropper", label: "Pick" }, { id: "eyedropper", label: t("skinEditor.pick") },
{ id: "fill", label: "Fill" }, { id: "fill", label: t("skinEditor.fill") },
]; ];
const gridScale = 8; const gridScale = 8;
@@ -518,7 +520,7 @@ const SkinEditorView = memo(function SkinEditorView() {
> >
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 pb-2 w-[60%] <h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 pb-2 w-[60%]
text-center tracking-widest uppercase opacity-80 font-bold border-b-2 border-[#373737]"> text-center tracking-widest uppercase opacity-80 font-bold border-b-2 border-[#373737]">
Skin Editor {t("skinEditor.title")}
</h2> </h2>
<div className="flex flex-1 min-h-0 w-full gap-4 px-4 overflow-hidden"> <div className="flex flex-1 min-h-0 w-full gap-4 px-4 overflow-hidden">
@@ -533,7 +535,7 @@ const SkinEditorView = memo(function SkinEditorView() {
<div className="relative" ref={pickerRef}> <div className="relative" ref={pickerRef}>
<button <button
type="button" type="button"
title="Color" title={t("skinEditor.color")}
onClick={() => { onClick={() => {
playPressSound(); playPressSound();
setShowPicker((open) => !open); setShowPicker((open) => !open);
@@ -655,7 +657,7 @@ const SkinEditorView = memo(function SkinEditorView() {
<div className="relative w-full h-10 flex items-center justify-center" style={sliderBg}> <div className="relative w-full h-10 flex items-center justify-center" style={sliderBg}>
<span className="absolute z-10 text-sm text-white mc-text-shadow tracking-widest pointer-events-none"> <span className="absolute z-10 text-sm text-white mc-text-shadow tracking-widest pointer-events-none">
Alpha {Math.round(alpha * 100)}% {t("skinEditor.alpha")} {Math.round(alpha * 100)}%
</span> </span>
<input <input
type="range" type="range"
@@ -668,9 +670,9 @@ const SkinEditorView = memo(function SkinEditorView() {
className="mc-slider-custom w-[calc(100%+8px)] h-full z-20" className="mc-slider-custom w-[calc(100%+8px)] h-full z-20"
/> />
</div> </div>
{renderToolBtn("slim", slim ? "Alex" : "Steve", slim, () => { playPressSound(); setSlim((v) => !v); })} {renderToolBtn("slim", slim ? t("skinEditor.alex") : t("skinEditor.steve"), slim, () => { playPressSound(); setSlim((v) => !v); })}
{renderToolBtn("grid", "Grid", showGrid, () => { playPressSound(); setShowGrid((v) => !v); })} {renderToolBtn("grid", t("skinEditor.grid"), showGrid, () => { playPressSound(); setShowGrid((v) => !v); })}
{renderToolBtn("undo", "Undo", false, () => { playPressSound(); handleUndo(); })} {renderToolBtn("undo", t("skinEditor.undo"), false, () => { playPressSound(); handleUndo(); })}
</div> </div>
<div className="flex flex-1 items-center justify-center min-w-0 min-h-0 mt-[-200px]"> <div className="flex flex-1 items-center justify-center min-w-0 min-h-0 mt-[-200px]">
@@ -698,9 +700,9 @@ const SkinEditorView = memo(function SkinEditorView() {
<div className="flex gap-4 mt-3 mb-2"> <div className="flex gap-4 mt-3 mb-2">
{[ {[
{ id: "save", label: "Save", onClick: handleSave }, { id: "save", label: t("skinEditor.save"), onClick: handleSave },
{ id: "export", label: "Export", onClick: handleExport }, { id: "export", label: t("skinEditor.export"), onClick: handleExport },
{ id: "back", label: "Back", onClick: () => { playBackSound(); setActiveView("skins"); } }, { id: "back", label: t("skinEditor.back"), onClick: () => { playBackSound(); setActiveView("skins"); } },
].map((b) => ( ].map((b) => (
<button <button
key={b.id} key={b.id}
+28 -20
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, memo } from "react"; import { useState, useEffect, useRef, memo } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useLocalStorage } from "../../hooks/useLocalStorage"; import { useLocalStorage } from "../../hooks/useLocalStorage";
import { TauriService } from "../../services/TauriService"; import { TauriService } from "../../services/TauriService";
@@ -124,6 +125,7 @@ const HeadPreview = memo(function HeadPreview({ src }: { src: string }) {
}); });
const SkinsView = memo(function SkinsView() { const SkinsView = memo(function SkinsView() {
const { t } = useTranslation();
const { setActiveView, setIsUiHidden } = useUI(); const { setActiveView, setIsUiHidden } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { isAndroid } = usePlatform(); const { isAndroid } = usePlatform();
@@ -250,7 +252,7 @@ const SkinsView = memo(function SkinsView() {
? e.message ? e.message
: typeof e === "string" : typeof e === "string"
? e ? e
: "Failed to fetch", : t("skins.failedToFetch"),
); );
} finally { } finally {
setIsImporting(false); setIsImporting(false);
@@ -535,7 +537,7 @@ const SkinsView = memo(function SkinsView() {
className="flex flex-col items-center w-full max-w-3xl h-full outline-none" className="flex flex-col items-center w-full max-w-3xl h-full outline-none"
> >
<h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[60%] max-w-75 text-center tracking-widest uppercase opacity-80 font-bold"> <h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 border-b-2 border-[#373737] pb-2 w-[60%] max-w-75 text-center tracking-widest uppercase opacity-80 font-bold">
{viewMode === "skin" ? "Skin Library" : "Cape Library"} {viewMode === "skin" ? t("skins.skinLibrary") : t("skins.capeLibrary")}
</h2> </h2>
<div className="w-full max-w-160 flex-1 min-h-0 mb-4 p-5 flex flex-col relative overflow-hidden"> <div className="w-full max-w-160 flex-1 min-h-0 mb-4 p-5 flex flex-col relative overflow-hidden">
@@ -558,7 +560,7 @@ const SkinsView = memo(function SkinsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
{viewMode === "skin" ? "Import Skin" : "Import Cape"} {viewMode === "skin" ? t("skins.importSkin") : t("skins.importCape")}
</button> </button>
<button <button
@@ -593,7 +595,7 @@ const SkinsView = memo(function SkinsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
{viewMode === "skin" ? "Delete Skin" : "Delete Cape"} {viewMode === "skin" ? t("skins.deleteSkin") : t("skins.deleteCape")}
</button> </button>
{viewMode === "skin" && ( {viewMode === "skin" && (
@@ -617,7 +619,7 @@ const SkinsView = memo(function SkinsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Edit Skin {t("skins.editSkin")}
</button> </button>
)} )}
@@ -642,7 +644,11 @@ const SkinsView = memo(function SkinsView() {
> >
<img <img
src="/images/Update_Icon.png" src="/images/Update_Icon.png"
alt={viewMode === "skin" ? "Switch to Cape" : "Switch to Skin"} alt={
viewMode === "skin"
? t("skins.switchToCape")
: t("skins.switchToSkin")
}
className="w-8 h-8 object-contain pointer-events-none drop-shadow-md" className="w-8 h-8 object-contain pointer-events-none drop-shadow-md"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
loading="lazy" loading="lazy"
@@ -686,12 +692,12 @@ const SkinsView = memo(function SkinsView() {
<div className="h-4 flex items-center justify-center gap-1"> <div className="h-4 flex items-center justify-center gap-1">
{isActive && ( {isActive && (
<span className="text-[#FFFF55] text-xs mc-text-shadow uppercase tracking-widest"> <span className="text-[#FFFF55] text-xs mc-text-shadow uppercase tracking-widest">
Active {t("skins.active")}
</span> </span>
)} )}
{skin.isSlim && ( {skin.isSlim && (
<span className="bg-purple-500/50 border border-purple-500/80 text-white px-1 text-[10px] uppercase rounded"> <span className="bg-purple-500/50 border border-purple-500/80 text-white px-1 text-[10px] uppercase rounded">
Slim {t("skins.slim")}
</span> </span>
)} )}
</div> </div>
@@ -742,7 +748,7 @@ const SkinsView = memo(function SkinsView() {
<span <span
className={`text-center outline-none border-none text-base mc-text-shadow w-full truncate transition-colors ${isActiveCapeDefault || focusIndex === SKINS_START_INDEX ? "text-[#FFFF55]" : "text-white"}`} className={`text-center outline-none border-none text-base mc-text-shadow w-full truncate transition-colors ${isActiveCapeDefault || focusIndex === SKINS_START_INDEX ? "text-[#FFFF55]" : "text-white"}`}
> >
No Cape {t("skins.noCape")}
</span> </span>
</div> </div>
{savedCapes.map((cape, i) => { {savedCapes.map((cape, i) => {
@@ -828,7 +834,7 @@ const SkinsView = memo(function SkinsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Back {t("common.back")}
</button> </button>
)} )}
@@ -844,7 +850,7 @@ const SkinsView = memo(function SkinsView() {
}} }}
> >
<h2 className="text-2xl text-white mc-text-shadow mb-6 tracking-widest uppercase font-bold text-center"> <h2 className="text-2xl text-white mc-text-shadow mb-6 tracking-widest uppercase font-bold text-center">
Import Skin {t("skins.importSkin")}
</h2> </h2>
{!importMode ? ( {!importMode ? (
@@ -865,7 +871,7 @@ const SkinsView = memo(function SkinsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
From File {t("skins.fromFile")}
</button> </button>
<button <button
onMouseEnter={() => setModalFocusIndex(1)} onMouseEnter={() => setModalFocusIndex(1)}
@@ -884,14 +890,14 @@ const SkinsView = memo(function SkinsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
From Username {t("skins.fromUsername")}
</button> </button>
</div> </div>
) : importMode === "username" ? ( ) : importMode === "username" ? (
<div className="flex flex-col gap-4 w-full px-4 mb-2"> <div className="flex flex-col gap-4 w-full px-4 mb-2">
<input <input
type="text" type="text"
placeholder="Minecraft Username" placeholder={t("skins.minecraftUsername")}
value={importUsername} value={importUsername}
onChange={(e) => setImportUsername(e.target.value)} onChange={(e) => setImportUsername(e.target.value)}
onFocus={() => setModalFocusIndex(0)} onFocus={() => setModalFocusIndex(0)}
@@ -921,14 +927,16 @@ const SkinsView = memo(function SkinsView() {
}} }}
> >
{isImporting {isImporting
? "Fetching..." ? t("skins.fetching")
: `Fetch ${viewMode === "skin" ? "Skin" : "Cape"}`} : t("skins.fetch", {
type: viewMode === "skin" ? "Skin" : "Cape",
})}
</button> </button>
</div> </div>
) : importMode === "model" ? ( ) : importMode === "model" ? (
<div className="flex flex-col gap-4 w-full px-4 mb-2"> <div className="flex flex-col gap-4 w-full px-4 mb-2">
<span className="text-white/80 text-sm text-center mb-2 mc-text-shadow"> <span className="text-white/80 text-sm text-center mb-2 mc-text-shadow">
Choose the player model type for this skin: {t("skins.chooseModelType")}:
</span> </span>
<button <button
onMouseEnter={() => setModalFocusIndex(0)} onMouseEnter={() => setModalFocusIndex(0)}
@@ -946,7 +954,7 @@ const SkinsView = memo(function SkinsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Normal (Wide Arms) {t("skins.normalWideArms")}
</button> </button>
<button <button
onMouseEnter={() => setModalFocusIndex(1)} onMouseEnter={() => setModalFocusIndex(1)}
@@ -964,7 +972,7 @@ const SkinsView = memo(function SkinsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Slim (Thin Arms) {t("skins.slimThinArms")}
</button> </button>
</div> </div>
) : null} ) : null}
@@ -989,7 +997,7 @@ const SkinsView = memo(function SkinsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Cancel {t("common.cancel")}
</button> </button>
</div> </div>
</div> </div>
+20 -18
View File
@@ -1,8 +1,10 @@
import React, { useState, useCallback, useMemo, useRef, useEffect } from "react"; import React, { useState, useCallback, useMemo, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { useConfig, useAudio, useUI } from "../../context/LauncherContext"; import { useConfig, useAudio, useUI } from "../../context/LauncherContext";
import { SwfImage, SwfService, SwfTag } from "../../services/SwfService"; import { SwfImage, SwfService, SwfTag } from "../../services/SwfService";
export default function SwfView() { export default function SwfView() {
const { t } = useTranslation();
const { animationsEnabled } = useConfig(); const { animationsEnabled } = useConfig();
const { playBackSound, playPressSound } = useAudio(); const { playBackSound, playPressSound } = useAudio();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
@@ -79,10 +81,10 @@ export default function SwfView() {
if (extracted.length > 0) { if (extracted.length > 0) {
setSelectedImageId(extracted[0].id); setSelectedImageId(extracted[0].id);
} }
showNotification(`Loaded ${file.name}`); showNotification(t("swfEditor.loaded", { name: file.name }));
} catch (e: unknown) { } catch (e: unknown) {
console.error(e); console.error(e);
showNotification("Failed to process SWF", "error"); showNotification(t("swfEditor.failedToProcess"), "error");
setImages([]); setImages([]);
setSwfData(null); setSwfData(null);
} }
@@ -128,7 +130,7 @@ export default function SwfView() {
a.download = `${img.name || `image_${img.id}`}.${ext}`; a.download = `${img.name || `image_${img.id}`}.${ext}`;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
showNotification(`Exported: ${img.name || img.id}`); showNotification(t("swfEditor.exported", { name: img.name || img.id }));
}; };
const handleReplace = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleReplace = async (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -154,7 +156,7 @@ export default function SwfView() {
return next; return next;
}); });
showNotification("Image Replaced", "success"); showNotification(t("swfEditor.imageReplaced"), "success");
e.target.value = ""; e.target.value = "";
}; };
@@ -169,7 +171,7 @@ export default function SwfView() {
a.download = fileName || "output.swf"; a.download = fileName || "output.swf";
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
showNotification("SWF Saved Successfully"); showNotification(t("swfEditor.savedSuccessfully"));
}; };
return ( return (
@@ -182,7 +184,7 @@ export default function SwfView() {
> >
<div className="w-full flex justify-between items-center mb-4 px-8"> <div className="w-full flex justify-between items-center mb-4 px-8">
<h2 className="text-2xl text-white mc-text-shadow border-b-2 border-[#373737] pb-1 tracking-widest uppercase font-bold"> <h2 className="text-2xl text-white mc-text-shadow border-b-2 border-[#373737] pb-1 tracking-widest uppercase font-bold">
SWF Editor {t("swfEditor.title")}
</h2> </h2>
<div className="flex gap-4"> <div className="flex gap-4">
<button <button
@@ -190,7 +192,7 @@ export default function SwfView() {
className="px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none" className="px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Open SWF {t("swfEditor.openSwf")}
</button> </button>
<button <button
onClick={handleSaveSwf} onClick={handleSaveSwf}
@@ -198,7 +200,7 @@ export default function SwfView() {
className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${!swfData ? "opacity-50 grayscale" : ""}`} className={`px-6 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none ${!swfData ? "opacity-50 grayscale" : ""}`}
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Save SWF {t("swfEditor.saveSwf")}
</button> </button>
</div> </div>
</div> </div>
@@ -210,7 +212,7 @@ export default function SwfView() {
<div className="flex-1 w-full flex flex-col items-center justify-center p-12" <div className="flex-1 w-full flex flex-col items-center justify-center p-12"
style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}> style={{ backgroundImage: "url('/images/frame_background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}>
<img src="/images/tools/arc.png" className="w-32 h-32 mb-8 opacity-20 grayscale" style={{ imageRendering: "pixelated" }} /> <img src="/images/tools/arc.png" className="w-32 h-32 mb-8 opacity-20 grayscale" style={{ imageRendering: "pixelated" }} />
<h3 className="text-2xl text-white/40 mc-text-shadow italic">Open an SWF file to begin editing</h3> <h3 className="text-2xl text-white/40 mc-text-shadow italic">{t("swfEditor.openToBegin")}</h3>
</div> </div>
) : ( ) : (
<div className="flex-1 w-full flex gap-4 overflow-hidden"> <div className="flex-1 w-full flex gap-4 overflow-hidden">
@@ -218,7 +220,7 @@ export default function SwfView() {
<div className="mb-4"> <div className="mb-4">
<input <input
type="text" type="text"
placeholder="Search assets..." placeholder={t("swfEditor.searchAssets")}
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors" className="w-full bg-black/40 border-2 border-[#373737] text-white px-4 py-2 outline-none focus:border-[#FFFF55] transition-colors"
@@ -248,7 +250,7 @@ export default function SwfView() {
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{!selectedImage ? ( {!selectedImage ? (
<div className="flex-1 flex flex-col items-center justify-center text-white/20 italic gap-4"> <div className="flex-1 flex flex-col items-center justify-center text-white/20 italic gap-4">
<span>Select an image to view details</span> <span>{t("swfEditor.selectImageViewDetails")}</span>
</div> </div>
) : ( ) : (
<motion.div <motion.div
@@ -263,9 +265,9 @@ export default function SwfView() {
{selectedImage.name || `Bitmap ${selectedImage.id}`} {selectedImage.name || `Bitmap ${selectedImage.id}`}
</h3> </h3>
<div className="flex gap-4 text-xs uppercase tracking-widest text-white/40"> <div className="flex gap-4 text-xs uppercase tracking-widest text-white/40">
<span>Character ID: <span className="text-white/80">{selectedImage.id}</span></span> <span>{t("swfEditor.characterId")} <span className="text-white/80">{selectedImage.id}</span></span>
<span>Type: <span className="text-white/80">{selectedImage.type}</span></span> <span>{t("swfEditor.type")} <span className="text-white/80">{selectedImage.type}</span></span>
{selectedImage.width && <span>Size: <span className="text-white/80">{selectedImage.width}x{selectedImage.height}</span></span>} {selectedImage.width && <span>{t("swfEditor.size")} <span className="text-white/80">{selectedImage.width}x{selectedImage.height}</span></span>}
</div> </div>
</div> </div>
</div> </div>
@@ -279,7 +281,7 @@ export default function SwfView() {
alt={`Bitmap ${selectedImage.id}`} alt={`Bitmap ${selectedImage.id}`}
/> />
) : ( ) : (
<div className="text-white/20 italic">Loading Preview...</div> <div className="text-white/20 italic">{t("swfEditor.loadingPreview")}</div>
)} )}
</div> </div>
@@ -289,14 +291,14 @@ export default function SwfView() {
className="flex-1 py-3 text-white mc-text-shadow text-lg transition-all hover:text-[#FFFF55]" className="flex-1 py-3 text-white mc-text-shadow text-lg transition-all hover:text-[#FFFF55]"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Export Bitmap {t("swfEditor.exportBitmap")}
</button> </button>
<button <button
onClick={() => replaceInputRef.current?.click()} onClick={() => replaceInputRef.current?.click()}
className="flex-1 py-3 text-white mc-text-shadow text-lg transition-all hover:text-[#FFFF55]" className="flex-1 py-3 text-white mc-text-shadow text-lg transition-all hover:text-[#FFFF55]"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%" }}
> >
Replace Bitmap {t("swfEditor.replaceBitmap")}
</button> </button>
</div> </div>
</motion.div> </motion.div>
@@ -311,7 +313,7 @@ export default function SwfView() {
className="w-72 h-14 shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-6 outline-none border-none hover:text-[#FFFF55] text-white" className="w-72 h-14 shrink-0 flex items-center justify-center transition-colors text-2xl mc-text-shadow mt-6 outline-none border-none hover:text-[#FFFF55] text-white"
style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }} style={{ backgroundImage: "url('/images/Button_Background.png')", backgroundSize: "100% 100%", imageRendering: "pixelated" }}
> >
Back {t("swfEditor.back")}
</button> </button>
<AnimatePresence> <AnimatePresence>
+41 -51
View File
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, memo } from "react"; import { useState, useEffect, useRef, memo } from "react";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { TauriService, PlaytimeResponse } from "../../services/TauriService"; import { TauriService, PlaytimeResponse } from "../../services/TauriService";
import CustomTUModal from "../modals/CustomTUModal"; import CustomTUModal from "../modals/CustomTUModal";
@@ -62,6 +63,7 @@ function formatPlaytime(seconds: number): string {
} }
const VersionsView = memo(function VersionsView() { const VersionsView = memo(function VersionsView() {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { const {
profile: selectedProfile, profile: selectedProfile,
@@ -339,7 +341,7 @@ const VersionsView = memo(function VersionsView() {
className="flex flex-col items-center w-full max-w-5xl outline-none" 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 pb-2 w-[40%] max-w-[200px] text-center tracking-widest uppercase font-bold"> <h2 className="text-2xl text-white mc-text-shadow mt-2 mb-4 pb-2 w-[40%] max-w-[200px] text-center tracking-widest uppercase font-bold">
Versions {t("versions.title")}
</h2> </h2>
<div className="w-full min-w-[480px] p-6 mb-4 mc-options-bg"> <div className="w-full min-w-[480px] p-6 mb-4 mc-options-bg">
@@ -429,7 +431,7 @@ const VersionsView = memo(function VersionsView() {
setIsPlaytimeModalOpen(true); setIsPlaytimeModalOpen(true);
}} }}
className="flex items-center gap-1.5 px-2 py-1 bg-black/60 border border-[#555] hover:border-[#FFFF55] group transition-colors flex-shrink-0" className="flex items-center gap-1.5 px-2 py-1 bg-black/60 border border-[#555] hover:border-[#FFFF55] group transition-colors flex-shrink-0"
title="View playtime" title={t("versions.viewPlaytime")}
> >
<svg <svg
width="12" width="12"
@@ -465,7 +467,7 @@ const VersionsView = memo(function VersionsView() {
))} ))}
{isCustom && !edition.category && ( {isCustom && !edition.category && (
<span className="text-[10px] px-1.5 py-0.5 bg-[#777] text-[#222] font-bold uppercase"> <span className="text-[10px] px-1.5 py-0.5 bg-[#777] text-[#222] font-bold uppercase">
Custom {t("versions.custom")}
</span> </span>
)} )}
</div> </div>
@@ -586,11 +588,10 @@ const VersionsView = memo(function VersionsView() {
className="w-3 h-3 object-contain" className="w-3 h-3 object-contain"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
Update Available! {t("versions.updateAvailable")}
</button> </button>
)} )}
{!isAndroid && !isInstalled && ( {!isAndroid && !isInstalled && ( <button
<button
onClick={async (e) => { onClick={async (e) => {
e.stopPropagation(); e.stopPropagation();
playPressSound(); playPressSound();
@@ -607,10 +608,10 @@ const VersionsView = memo(function VersionsView() {
"fixed inset-0 bg-black/80 flex items-center justify-center z-50"; "fixed inset-0 bg-black/80 flex items-center justify-center z-50";
dialog.innerHTML = ` dialog.innerHTML = `
<div class="w-[420px] p-4 flex flex-col items-center mc-options-bg"> <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">Invalid Directory</h3> <h3 class="text-2xl font-bold text-[#333333] mb-4 text-left w-full px-4 mc-text-shadow">${t("versions.invalidDirectory")}</h3>
<p class="text-[#333333] mb-6 text-left w-full px-4">Please select an empty directory for installation.</p> <p class="text-[#333333] mb-6 text-left w-full px-4">${t("versions.selectEmptyDirectory")}</p>
<div class="flex flex-col gap-3 w-full px-4"> <div class="flex flex-col gap-3 w-full px-4">
<button id="empty-dir-ok" 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)'">OK</button> <button id="empty-dir-ok" 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)'">${t("common.ok")}</button>
</div> </div>
</div> </div>
`; `;
@@ -643,7 +644,7 @@ const VersionsView = memo(function VersionsView() {
className="w-3.5 h-3.5 object-contain" className="w-3.5 h-3.5 object-contain"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
Download to custom path {t("versions.downloadToCustomPath")}
</button> </button>
)} )}
{edition.officialDLC && isInstalled ? ( {edition.officialDLC && isInstalled ? (
@@ -670,10 +671,9 @@ const VersionsView = memo(function VersionsView() {
<polyline points="17 8 12 3 7 8" /> <polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" /> <line x1="12" y1="3" x2="12" y2="15" />
</svg> </svg>
Download DLC {t("versions.downloadDlc")}
</button> </button>
) : null} ) : null} {argsSchemas[edition.instanceId] && (
{argsSchemas[edition.instanceId] && (
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -706,7 +706,7 @@ const VersionsView = memo(function VersionsView() {
<line x1="9" y1="8" x2="15" y2="8" /> <line x1="9" y1="8" x2="15" y2="8" />
<line x1="17" y1="16" x2="23" y2="16" /> <line x1="17" y1="16" x2="23" y2="16" />
</svg> </svg>
Options {t("versions.options")}
</button> </button>
)} )}
{Array.isArray(edition.branches) && {Array.isArray(edition.branches) &&
@@ -720,10 +720,10 @@ const VersionsView = memo(function VersionsView() {
className="w-full text-left px-3 py-1.5 text-[10px] text-white mc-text-shadow flex items-center justify-between group" className="w-full text-left px-3 py-1.5 text-[10px] text-white mc-text-shadow flex items-center justify-between group"
> >
<span className="text-[#AAAAAA] font-bold"> <span className="text-[#AAAAAA] font-bold">
Channel {t("versions.channel")}
</span> </span>
<span className="text-[#ffff55] font-bold"> <span className="text-[#ffff55] font-bold">
{edition.selectedBranch ?? "Latest"} {edition.selectedBranch ?? t("versions.latest")}
</span> </span>
</button> </button>
)} )}
@@ -746,10 +746,9 @@ const VersionsView = memo(function VersionsView() {
className="w-3.5 h-3.5 object-contain" className="w-3.5 h-3.5 object-contain"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
Open Folder {t("versions.openFolder")}
</button> </button>
)} )} {!isAndroid && (
{!isAndroid && (
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -782,10 +781,9 @@ const VersionsView = memo(function VersionsView() {
className="w-3.5 h-3.5 object-contain invert brightness-0" className="w-3.5 h-3.5 object-contain invert brightness-0"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
Add to Steam {t("versions.addToSteam")}
</button> </button>
)} )} {!isAndroid && (
{!isAndroid && (
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -808,10 +806,9 @@ const VersionsView = memo(function VersionsView() {
<polyline points="7 10 12 15 17 10" /> <polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" /> <line x1="12" y1="15" x2="12" y2="3" />
</svg> </svg>
Import World {t("versions.importWorld")}
</button> </button>
)} )} {!isAndroid && (
{!isAndroid && (
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -838,10 +835,9 @@ const VersionsView = memo(function VersionsView() {
<polyline points="17 21 17 13 7 13 7 21" /> <polyline points="17 21 17 13 7 13 7 21" />
<polyline points="7 3 7 8 15 8" /> <polyline points="7 3 7 8 15 8" />
</svg> </svg>
Backup {t("versions.backup")}
</button> </button>
)} )} {!isAndroid && (
{!isAndroid && (
<button <button
onClick={async (e) => { onClick={async (e) => {
e.stopPropagation(); e.stopPropagation();
@@ -867,10 +863,9 @@ const VersionsView = memo(function VersionsView() {
<path d="M21 12a9 9 0 1 1-9-9 9 9 0 0 1 9 9z" /> <path d="M21 12a9 9 0 1 1-9-9 9 9 0 0 1 9 9z" />
<polyline points="12 7 12 12 15 15" /> <polyline points="12 7 12 12 15 15" />
</svg> </svg>
Restore {t("versions.restore")}
</button> </button>
)} )} {!isAndroid && (
{!isAndroid && (
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -892,10 +887,9 @@ const VersionsView = memo(function VersionsView() {
> >
<path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" /> <path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
</svg> </svg>
Customize {t("versions.customize")}
</button> </button>
)} )} <button
<button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
playPressSound(); playPressSound();
@@ -917,9 +911,8 @@ const VersionsView = memo(function VersionsView() {
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path> <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle> <circle cx="12" cy="7" r="4"></circle>
</svg> </svg>
Set UID {t("versions.setUid")}
</button> </button> {isCustom ? (
{isCustom ? (
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -941,10 +934,9 @@ const VersionsView = memo(function VersionsView() {
> >
<path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" /> <path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
</svg> </svg>
Edit Custom {t("versions.editCustom")}
</button> </button>
) : null} ) : null} <div className="h-[2px] bg-[#555] my-0.5 mx-1" />
<div className="h-[2px] bg-[#555] my-0.5 mx-1" />
<button <button
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -964,7 +956,7 @@ const VersionsView = memo(function VersionsView() {
className="w-3.5 h-3.5 object-contain" className="w-3.5 h-3.5 object-contain"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
{isCustom ? "Remove Custom" : "Uninstall"} {isCustom ? t("versions.removeCustom") : t("versions.uninstall")}
</button> </button>
</div> </div>
)} )}
@@ -1015,7 +1007,7 @@ const VersionsView = memo(function VersionsView() {
}} }}
onMouseEnter={() => setFocusIndex(visibleEditions.length + 1)} onMouseEnter={() => setFocusIndex(visibleEditions.length + 1)}
onMouseLeave={() => setHoveredBtn(null)} onMouseLeave={() => setHoveredBtn(null)}
title="Import Custom TU" title={t("modals.customTu.importTitle")}
className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]" className="w-8 h-8 flex items-center justify-center text-[#3a3a3a]"
style={{ style={{
backgroundImage: backgroundImage:
@@ -1030,7 +1022,7 @@ const VersionsView = memo(function VersionsView() {
> >
<img <img
src="/images/Folder_Icon.png" src="/images/Folder_Icon.png"
alt="Import Custom TU" alt={t("modals.customTu.importTitle")}
className="w-5 h-5 object-contain" className="w-5 h-5 object-contain"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
@@ -1060,9 +1052,8 @@ const VersionsView = memo(function VersionsView() {
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Done {t("common.done")}
</button> </button> </div>
</div>
)} )}
<CustomTUModal <CustomTUModal
@@ -1200,22 +1191,21 @@ const VersionsView = memo(function VersionsView() {
}} }}
> >
<h3 className="text-xl text-white mc-text-shadow mb-4 text-center"> <h3 className="text-xl text-white mc-text-shadow mb-4 text-center">
Delete {deleteConfirmEdition.name}? {t("versions.deleteTitle", { name: deleteConfirmEdition.name })}
</h3> </h3>
<p className="text-sm text-white mb-6 text-center leading-relaxed"> <p className="text-sm text-white mb-6 text-center leading-relaxed">
Warning: All your saves and worlds for this version will be {t("versions.deleteWarning")}
permanently deleted!
</p> </p>
<div className="flex justify-center gap-4"> <div className="flex justify-center gap-4">
<DeleteConfirmButton <DeleteConfirmButton
label="Cancel" label={t("common.cancel")}
onClick={() => { onClick={() => {
playBackSound(); playBackSound();
setDeleteConfirmEdition(null); setDeleteConfirmEdition(null);
}} }}
/> />
<DeleteConfirmButton <DeleteConfirmButton
label="Delete" label={t("common.delete")}
isDanger isDanger
onClick={() => { onClick={() => {
playPressSound(); playPressSound();
+145 -102
View File
@@ -7,6 +7,7 @@ import React, {
useContext, useContext,
useMemo, useMemo,
} from "react"; } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@@ -182,6 +183,7 @@ const WorkshopView = memo(function WorkshopView({
workshopTarget, workshopTarget,
onClearWorkshopTarget, onClearWorkshopTarget,
}: WorkshopViewProps) { }: WorkshopViewProps) {
const { t } = useTranslation();
const { setActiveView } = useUI(); const { setActiveView } = useUI();
const { playPressSound, playBackSound } = useAudio(); const { playPressSound, playBackSound } = useAudio();
const { isAndroid } = usePlatform(); const { isAndroid } = usePlatform();
@@ -271,7 +273,7 @@ const WorkshopView = memo(function WorkshopView({
setLoading(false); setLoading(false);
}) })
.catch((e) => { .catch((e) => {
setError(e.message ?? "Failed to load registry"); setError(e.message ?? t("workshop.failedToLoadRegistry"));
setLoading(false); setLoading(false);
}); });
refreshInstalledPlugins(); refreshInstalledPlugins();
@@ -703,6 +705,35 @@ const WorkshopView = memo(function WorkshopView({
openModal, openModal,
]); ]);
const tabLabel = (tab: TabType): string => {
switch (tab) {
case "Skin":
return t("workshop.tabs.skin");
case "Texture":
return t("workshop.tabs.texture");
case "World":
return t("workshop.tabs.world");
case "Mod":
return t("workshop.tabs.mod");
case "DLC":
return t("workshop.tabs.dlc");
case "Plugins":
return t("workshop.tabs.plugins");
case "Versions":
return t("workshop.tabs.versions");
case "Installed":
return t("workshop.tabs.installed");
case "Search":
return t("workshop.tabs.search");
case "Server":
return t("workshop.tabs.server");
case "Server Plugins":
return t("workshop.tabs.serverPlugins");
default:
return tab;
}
};
const isSearchTab = activeTab === "Search"; const isSearchTab = activeTab === "Search";
const isInstalledTab = activeTab === "Installed"; const isInstalledTab = activeTab === "Installed";
const isVersionTab = activeTab === "Versions"; const isVersionTab = activeTab === "Versions";
@@ -742,7 +773,7 @@ const WorkshopView = memo(function WorkshopView({
: "text-[#aaa] hover:text-white hover:bg-white/5" : "text-[#aaa] hover:text-white hover:bg-white/5"
}`} }`}
> >
{tab.toUpperCase()} {tabLabel(tab).toUpperCase()}
</button> </button>
</React.Fragment> </React.Fragment>
); );
@@ -799,7 +830,7 @@ const WorkshopView = memo(function WorkshopView({
: "text-[#aaa] hover:text-white hover:bg-white/5" : "text-[#aaa] hover:text-white hover:bg-white/5"
}`} }`}
> >
{tab.toUpperCase()} {tabLabel(tab).toUpperCase()}
</button> </button>
</React.Fragment> </React.Fragment>
); );
@@ -829,16 +860,16 @@ const WorkshopView = memo(function WorkshopView({
}} }}
placeholder={ placeholder={
isInstalledTab isInstalledTab
? "FILTER INSTALLED..." ? t("workshop.filterInstalled")
: isVersionTab : isVersionTab
? "FILTER VERSIONS..." ? t("workshop.filterVersions")
: isPluginTab : isPluginTab
? "FILTER PLUGINS..." ? t("workshop.filterPlugins")
: activeTab === "Server Plugins" : activeTab === "Server Plugins"
? "FILTER PLUGINS..." ? t("workshop.filterPlugins")
: activeTab === "Server" : activeTab === "Server"
? "FILTER SERVERS..." ? t("workshop.filterServers")
: "ENTER KEYWORDS..." : t("workshop.enterKeywords")
} }
spellCheck={false} spellCheck={false}
autoFocus={isSearchTab} autoFocus={isSearchTab}
@@ -869,7 +900,7 @@ const WorkshopView = memo(function WorkshopView({
: "text-[#A0A0A0] bg-black/30 border-[#444] hover:text-white hover:border-[#888]" : "text-[#A0A0A0] bg-black/30 border-[#444] hover:text-white hover:border-[#888]"
}`} }`}
> >
{cat === "all" ? "ALL" : cat.toUpperCase()} {cat === "all" ? t("common.all").toUpperCase() : cat.toUpperCase()}
</button> </button>
))} ))}
</div> </div>
@@ -886,7 +917,7 @@ const WorkshopView = memo(function WorkshopView({
: "text-[#A0A0A0] bg-black/30 border-[#444] hover:text-white hover:border-[#888]" : "text-[#A0A0A0] bg-black/30 border-[#444] hover:text-white hover:border-[#888]"
}`} }`}
> >
{cat === "all" ? "ALL" : cat.toUpperCase()} {cat === "all" ? t("common.all").toUpperCase() : cat.toUpperCase()}
</button> </button>
))} ))}
</div> </div>
@@ -928,9 +959,11 @@ const WorkshopView = memo(function WorkshopView({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Update All ( {t("workshop.updateAll", {
{filteredItems.filter((p) => hasUpdate(p)).length}) count: filteredItems.filter((p) => hasUpdate(p)).length,
})}
</button> </button>
<button <button
onClick={async () => { onClick={async () => {
if (filteredItems.length === 0) return; if (filteredItems.length === 0) return;
@@ -961,34 +994,36 @@ const WorkshopView = memo(function WorkshopView({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Reinstall All ({filteredItems.length}) {t("workshop.reinstallAll", {
count: filteredItems.length,
})}
</button> </button>
</div> </div>
)} )}
{isSearchTab && !search.trim() ? ( {isSearchTab && !search.trim() ? (
<div className="flex flex-col items-center justify-center h-[200px] opacity-40"> <div className="flex flex-col items-center justify-center h-[200px] opacity-40">
<span className="text-xl mc-text-shadow tracking-widest uppercase"> <span className="text-xl mc-text-shadow tracking-widest uppercase">
Start typing to search... {t("workshop.startTypingToSearch")}
</span> </span>
</div> </div>
) : loading ? ( ) : loading ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<span className="text-3xl text-[#FFFF55] mc-text-shadow tracking-widest animate-pulse uppercase"> <span className="text-3xl text-[#FFFF55] mc-text-shadow tracking-widest animate-pulse uppercase">
Searching Archives... {t("workshop.searchingArchives")}
</span> </span>
</div> </div>
) : filteredItems.length === 0 ? ( ) : filteredItems.length === 0 ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<span className="text-2xl text-[#E0E0E0] mc-text-shadow uppercase tracking-widest opacity-60"> <span className="text-2xl text-[#E0E0E0] mc-text-shadow uppercase tracking-widest opacity-60">
{isInstalledTab {isInstalledTab
? "Nothing Installed" ? t("workshop.nothingInstalled")
: activeTab === "Plugins" : activeTab === "Plugins"
? "No plugins available" ? t("workshop.noPluginsAvailable")
: activeTab === "Server Plugins" : activeTab === "Server Plugins"
? "No plugins available" ? t("workshop.noPluginsAvailable")
: activeTab === "Server" : activeTab === "Server"
? "No servers available" ? t("workshop.noServersAvailable")
: "No results"} : t("common.noResults")}
</span> </span>
</div> </div>
) : isPluginTab ? ( ) : isPluginTab ? (
@@ -1045,7 +1080,7 @@ const WorkshopView = memo(function WorkshopView({
) : loading ? ( ) : loading ? (
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<span className="text-3xl text-[#FFFF55] mc-text-shadow tracking-widest animate-pulse uppercase"> <span className="text-3xl text-[#FFFF55] mc-text-shadow tracking-widest animate-pulse uppercase">
Searching Archives... {t("workshop.searchingArchives")}
</span> </span>
</div> </div>
) : error ? ( ) : error ? (
@@ -1062,7 +1097,7 @@ const WorkshopView = memo(function WorkshopView({
{filteredItems.length === 0 ? ( {filteredItems.length === 0 ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<span className="text-2xl text-[#E0E0E0] mc-text-shadow uppercase tracking-widest opacity-40"> <span className="text-2xl text-[#E0E0E0] mc-text-shadow uppercase tracking-widest opacity-40">
Empty category {t("workshop.emptyCategory")}
</span> </span>
</div> </div>
) : isPluginTab ? ( ) : isPluginTab ? (
@@ -1129,7 +1164,7 @@ const WorkshopView = memo(function WorkshopView({
"url('/images/Button_Background.png')"; "url('/images/Button_Background.png')";
}} }}
> >
Back {t("workshop.back")}
</button> </button>
</div> </div>
)} )}
@@ -1194,6 +1229,7 @@ function PackageCard({
isVersionTab?: boolean; isVersionTab?: boolean;
isPluginTab?: boolean; isPluginTab?: boolean;
}) { }) {
const { t } = useTranslation();
const thumbnailUrl = pkg.thumbnail.startsWith("http") const thumbnailUrl = pkg.thumbnail.startsWith("http")
? pkg.thumbnail ? pkg.thumbnail
: isVersionTab : isVersionTab
@@ -1221,7 +1257,7 @@ function PackageCard({
> >
{!thumbnailUrl || imgError ? ( {!thumbnailUrl || imgError ? (
<span className="text-[#555] text-sm mc-text-shadow uppercase tracking-widest"> <span className="text-[#555] text-sm mc-text-shadow uppercase tracking-widest">
No Image {t("workshop.noImage")}
</span> </span>
) : ( ) : (
<img <img
@@ -1245,14 +1281,14 @@ function PackageCard({
{hasUpdate && ( {hasUpdate && (
<div className="absolute top-1 left-1"> <div className="absolute top-1 left-1">
<span className="text-[8px] bg-[#FF8800]/90 border border-[#FF6600] px-1.5 py-0.5 text-white mc-text-shadow uppercase tracking-tighter"> <span className="text-[8px] bg-[#FF8800]/90 border border-[#FF6600] px-1.5 py-0.5 text-white mc-text-shadow uppercase tracking-tighter">
Update {t("workshop.update")}
</span> </span>
</div> </div>
)} )}
{installed && !hasUpdate && ( {installed && !hasUpdate && (
<div className="absolute top-1 left-1"> <div className="absolute top-1 left-1">
<span className="text-[8px] bg-[#55FF55] border border-[#55FF55]/60 px-1.5 py-0.5 text-[#003300] mc-text-shadow uppercase tracking-tighter shadow-sm font-bold"> <span className="text-[8px] bg-[#55FF55] border border-[#55FF55]/60 px-1.5 py-0.5 text-[#003300] mc-text-shadow uppercase tracking-tighter shadow-sm font-bold">
{isVersionTab ? "Added" : "Installed"} {isVersionTab ? t("workshop.added") : t("workshop.installed")}
</span> </span>
</div> </div>
)} )}
@@ -1315,6 +1351,7 @@ function PackageModal({
isSaved?: boolean; isSaved?: boolean;
onToggleSave?: (pkg: RegistryPackage) => void; onToggleSave?: (pkg: RegistryPackage) => void;
}) { }) {
const { t } = useTranslation();
const { addCustomEdition } = useGame(); const { addCustomEdition } = useGame();
const thumbnailUrl = pkg.thumbnail.startsWith("http") const thumbnailUrl = pkg.thumbnail.startsWith("http")
? pkg.thumbnail ? pkg.thumbnail
@@ -1389,7 +1426,7 @@ function PackageModal({
playPressSound(); playPressSound();
try { try {
const path = await TauriService.saveFileDialog( const path = await TauriService.saveFileDialog(
"Save Server Plugin", t("workshop.saveServerPlugin"),
`${pkg.name}.dll`, `${pkg.name}.dll`,
["*.dll", "*"], ["*.dll", "*"],
); );
@@ -1435,25 +1472,25 @@ function PackageModal({
const installLabel = isGameServerTab const installLabel = isGameServerTab
? isSaved ? isSaved
? "ADDED" ? t("workshop.added")
: "ADD" : t("workshop.add")
: isServerTab : isServerTab
? "DOWNLOAD" ? t("workshop.download")
: isVersionTab : isVersionTab
? hasInstalled ? hasInstalled
? "ADDED" ? t("workshop.added")
: "ADD" : t("workshop.add")
: isPluginTab : isPluginTab
? hasInstalled ? hasInstalled
? needsUpdate ? needsUpdate
? "UPDATE" ? t("workshop.update")
: "REINSTALL" : t("workshop.reinstall")
: "INSTALL" : t("workshop.install")
: !hasInstalled : !hasInstalled
? "INSTALL" ? t("workshop.install")
: needsUpdate : needsUpdate
? "UPDATE" ? t("workshop.update")
: "REINSTALL"; : t("workshop.reinstall");
return ( return (
<> <>
<div <div
@@ -1471,7 +1508,7 @@ function PackageModal({
{pkg.name} {pkg.name}
</span> </span>
<span className="text-base text-[#FFFF55] mc-text-shadow uppercase tracking-widest opacity-90"> <span className="text-base text-[#FFFF55] mc-text-shadow uppercase tracking-widest opacity-90">
By {pkg.author} {t("workshop.byAuthor", { author: pkg.author })}
</span> </span>
</div> </div>
</div> </div>
@@ -1480,7 +1517,7 @@ function PackageModal({
{!thumbnailUrl || imgError ? ( {!thumbnailUrl || imgError ? (
<div className="absolute inset-0 flex items-center justify-center opacity-20"> <div className="absolute inset-0 flex items-center justify-center opacity-20">
<span className="text-4xl mc-text-shadow uppercase tracking-widest"> <span className="text-4xl mc-text-shadow uppercase tracking-widest">
No Image {t("workshop.noImage")}
</span> </span>
</div> </div>
) : ( ) : (
@@ -1498,27 +1535,27 @@ function PackageModal({
{pkg.name} {pkg.name}
</span> </span>
<span className="text-base text-[#FFFF55] mc-text-shadow uppercase tracking-widest opacity-90"> <span className="text-base text-[#FFFF55] mc-text-shadow uppercase tracking-widest opacity-90">
By {pkg.author} {t("workshop.byAuthor", { author: pkg.author })}
</span> </span>
</div> </div>
{needsUpdate && ( {needsUpdate && (
<div className="absolute top-3 right-3 bg-[#FF8800] border border-[#FF6600] px-2 py-1"> <div className="absolute top-3 right-3 bg-[#FF8800] border border-[#FF6600] px-2 py-1">
<span className="text-[10px] text-white mc-text-shadow uppercase tracking-widest"> <span className="text-[10px] text-white mc-text-shadow uppercase tracking-widest">
Update Available {t("workshop.updateAvailable")}
</span> </span>
</div> </div>
)} )}
{hasInstalled && !needsUpdate && ( {hasInstalled && !needsUpdate && (
<div className="absolute top-3 right-3 bg-[#003300] border border-[#55FF55]/60 px-2 py-1"> <div className="absolute top-3 right-3 bg-[#003300] border border-[#55FF55]/60 px-2 py-1">
<span className="text-[10px] text-[#55FF55] mc-text-shadow uppercase tracking-widest"> <span className="text-[10px] text-[#55FF55] mc-text-shadow uppercase tracking-widest">
Installed {t("workshop.installed")}
</span> </span>
</div> </div>
)} )}
{isGameServerTab && isSaved && ( {isGameServerTab && isSaved && (
<div className="absolute top-3 right-3 bg-[#003300] border border-[#55FF55]/60 px-2 py-1"> <div className="absolute top-3 right-3 bg-[#003300] border border-[#55FF55]/60 px-2 py-1">
<span className="text-[10px] text-[#55FF55] mc-text-shadow uppercase tracking-widest"> <span className="text-[10px] text-[#55FF55] mc-text-shadow uppercase tracking-widest">
Saved {t("workshop.saved")}
</span> </span>
</div> </div>
)} )}
@@ -1536,7 +1573,7 @@ function PackageModal({
pkg.extended_description.trim() !== "" && ( pkg.extended_description.trim() !== "" && (
<div className="space-y-2 p-4 border border-[#444] bg-black/40"> <div className="space-y-2 p-4 border border-[#444] bg-black/40">
<span className="text-[10px] text-[#AAAAAA] mc-text-shadow uppercase tracking-[0.2em] font-bold"> <span className="text-[10px] text-[#AAAAAA] mc-text-shadow uppercase tracking-[0.2em] font-bold">
Details {t("workshop.details")}
</span> </span>
<div className="text-sm text-white mc-text-shadow leading-relaxed workshop-markdown"> <div className="text-sm text-white mc-text-shadow leading-relaxed workshop-markdown">
<ReactMarkdown <ReactMarkdown
@@ -1556,14 +1593,14 @@ function PackageModal({
<div className="grid grid-cols-2 gap-8 pt-4 border-t border-[#444]"> <div className="grid grid-cols-2 gap-8 pt-4 border-t border-[#444]">
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<span className="text-[10px] text-[#888] mc-text-shadow uppercase tracking-[0.2em] font-bold"> <span className="text-[10px] text-[#888] mc-text-shadow uppercase tracking-[0.2em] font-bold">
Metadata {t("workshop.metadata")}
</span> </span>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{isGameServerTab ? ( {isGameServerTab ? (
<> <>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
Address: {t("workshop.address")}
</span> </span>
<span className="text-[#55FF55] mc-text-shadow"> <span className="text-[#55FF55] mc-text-shadow">
{pkg.server_address || "N/A"} {pkg.server_address || "N/A"}
@@ -1571,7 +1608,7 @@ function PackageModal({
</div> </div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
Type: {t("workshop.type")}
</span> </span>
<span className="text-white mc-text-shadow"> <span className="text-white mc-text-shadow">
{pkg.server_type || "N/A"} {pkg.server_type || "N/A"}
@@ -1579,7 +1616,7 @@ function PackageModal({
</div> </div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
Version: {t("workshop.version")}
</span> </span>
<span className="text-white mc-text-shadow"> <span className="text-white mc-text-shadow">
{pkg.version} {pkg.version}
@@ -1587,7 +1624,7 @@ function PackageModal({
</div> </div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
Owner: {t("workshop.owner")}
</span> </span>
<span className="text-white mc-text-shadow"> <span className="text-white mc-text-shadow">
{pkg.author} {pkg.author}
@@ -1596,7 +1633,7 @@ function PackageModal({
{pkg.server_discord && ( {pkg.server_discord && (
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
Discord Server: {t("workshop.discordServer")}
</span> </span>
<a <a
href={pkg.server_discord} href={pkg.server_discord}
@@ -1615,7 +1652,7 @@ function PackageModal({
<> <>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
Downloads: {t("workshop.downloads")}
</span> </span>
<span className="text-white mc-text-shadow"> <span className="text-white mc-text-shadow">
{pkg.download_count?.toLocaleString() ?? 0} {pkg.download_count?.toLocaleString() ?? 0}
@@ -1623,7 +1660,7 @@ function PackageModal({
</div> </div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
Likes: {t("workshop.likes")}
</span> </span>
<span className="text-white mc-text-shadow"> <span className="text-white mc-text-shadow">
{pkg.likes?.toLocaleString() ?? 0} {pkg.likes?.toLocaleString() ?? 0}
@@ -1631,7 +1668,7 @@ function PackageModal({
</div> </div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
Server Type: {t("workshop.serverType")}
</span> </span>
<span className="text-[#55FF55] mc-text-shadow"> <span className="text-[#55FF55] mc-text-shadow">
{pkg.game_version || "N/A"} {pkg.game_version || "N/A"}
@@ -1639,7 +1676,7 @@ function PackageModal({
</div> </div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
File: {t("workshop.file")}
</span> </span>
<span className="text-white mc-text-shadow truncate ml-2"> <span className="text-white mc-text-shadow truncate ml-2">
{pkg.name || "N/A"} {pkg.name || "N/A"}
@@ -1648,7 +1685,7 @@ function PackageModal({
{pkg.file_size && ( {pkg.file_size && (
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
File Size: {t("workshop.fileSize")}
</span> </span>
<span className="text-white mc-text-shadow"> <span className="text-white mc-text-shadow">
{(pkg.file_size / 1024).toFixed(1)} KB {(pkg.file_size / 1024).toFixed(1)} KB
@@ -1658,7 +1695,7 @@ function PackageModal({
{pkg.github_url && ( {pkg.github_url && (
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
GitHub: {t("workshop.github")}
</span> </span>
<a <a
href={pkg.github_url} href={pkg.github_url}
@@ -1677,7 +1714,7 @@ function PackageModal({
<> <>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
Version: {t("workshop.version")}
</span> </span>
<span className="text-white mc-text-shadow"> <span className="text-white mc-text-shadow">
v{pkg.version} v{pkg.version}
@@ -1685,7 +1722,7 @@ function PackageModal({
</div> </div>
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
Package ID: {t("workshop.packageId")}
</span> </span>
<span className="text-[#55FF55] mc-text-shadow truncate ml-2"> <span className="text-[#55FF55] mc-text-shadow truncate ml-2">
{pkg.id} {pkg.id}
@@ -1696,13 +1733,13 @@ function PackageModal({
{hasInstalled && ( {hasInstalled && (
<div className="flex justify-between text-xs"> <div className="flex justify-between text-xs">
<span className="text-[#888] mc-text-shadow"> <span className="text-[#888] mc-text-shadow">
Installed: {t("workshop.installed")}
</span> </span>
<span <span
className={`mc-text-shadow truncate ml-2 ${needsUpdate ? "text-[#FF8800]" : "text-[#55FF55]"}`} className={`mc-text-shadow truncate ml-2 ${needsUpdate ? "text-[#FF8800]" : "text-[#55FF55]"}`}
> >
v{installedEntries[0]?.version} v{installedEntries[0]?.version}
{needsUpdate ? " (outdated)" : ""} {needsUpdate ? t("workshop.outdated") : ""}
</span> </span>
</div> </div>
)} )}
@@ -1710,7 +1747,7 @@ function PackageModal({
</div> </div>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<span className="text-[10px] text-[#666] mc-text-shadow uppercase tracking-[0.2em] font-bold"> <span className="text-[10px] text-[#666] mc-text-shadow uppercase tracking-[0.2em] font-bold">
Categories {t("workshop.categories")}
</span> </span>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{pkg.category.map((c) => ( {pkg.category.map((c) => (
@@ -1727,7 +1764,7 @@ function PackageModal({
{pkg.dependencies && pkg.dependencies.length > 0 && ( {pkg.dependencies && pkg.dependencies.length > 0 && (
<div className="flex flex-col gap-1.5 pt-4 border-t border-[#333]"> <div className="flex flex-col gap-1.5 pt-4 border-t border-[#333]">
<span className="text-[10px] text-[#FFAA33] mc-text-shadow uppercase tracking-[0.2em] font-bold"> <span className="text-[10px] text-[#FFAA33] mc-text-shadow uppercase tracking-[0.2em] font-bold">
Required Dependencies {t("workshop.requiredDependencies")}
</span> </span>
<div className="flex flex-wrap gap-1.5 mt-1"> <div className="flex flex-wrap gap-1.5 mt-1">
{pkg.dependencies.map((depId) => { {pkg.dependencies.map((depId) => {
@@ -1745,7 +1782,7 @@ function PackageModal({
}`} }`}
> >
{dep?.name || depId} {dep?.name || depId}
{isDepInstalled ? " (installed)" : ""} {isDepInstalled ? t("workshop.installedSuffix") : ""}
</span> </span>
); );
})} })}
@@ -1755,7 +1792,7 @@ function PackageModal({
{pkg.required_versions && pkg.required_versions.length > 0 && ( {pkg.required_versions && pkg.required_versions.length > 0 && (
<div className="flex flex-col gap-1.5 pt-4 border-t border-[#333]"> <div className="flex flex-col gap-1.5 pt-4 border-t border-[#333]">
<span className="text-[10px] text-[#55AAFF] mc-text-shadow uppercase tracking-[0.2em] font-bold"> <span className="text-[10px] text-[#55AAFF] mc-text-shadow uppercase tracking-[0.2em] font-bold">
Required Editions {t("workshop.requiredEditions")}
</span> </span>
<div className="flex flex-wrap gap-1.5 mt-1"> <div className="flex flex-wrap gap-1.5 mt-1">
{pkg.required_versions.map((verId) => { {pkg.required_versions.map((verId) => {
@@ -1782,7 +1819,7 @@ function PackageModal({
{pkg.zips && Object.keys(pkg.zips).length > 0 && ( {pkg.zips && Object.keys(pkg.zips).length > 0 && (
<div className="flex flex-col gap-3 pt-4 border-t border-[#333]"> <div className="flex flex-col gap-3 pt-4 border-t border-[#333]">
<span className="text-[10px] text-[#666] mc-text-shadow uppercase tracking-[0.2em] font-bold"> <span className="text-[10px] text-[#666] mc-text-shadow uppercase tracking-[0.2em] font-bold">
Files {t("workshop.files")}
</span> </span>
<div className="space-y-1.5"> <div className="space-y-1.5">
{groupZips(pkg.zips).map((group) => { {groupZips(pkg.zips).map((group) => {
@@ -1797,7 +1834,9 @@ function PackageModal({
{group.main} {group.main}
{isSplit && ( {isSplit && (
<span className="ml-2 text-[8px] text-[#FFFF55] bg-black/60 border border-[#555] px-1.5 py-0.5 uppercase tracking-widest"> <span className="ml-2 text-[8px] text-[#FFFF55] bg-black/60 border border-[#555] px-1.5 py-0.5 uppercase tracking-widest">
Split archive · {group.parts.length} parts {t("workshop.splitArchive", {
count: group.parts.length,
})}
</span> </span>
)} )}
</span> </span>
@@ -1849,7 +1888,7 @@ function PackageModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
REMOVE {t("workshop.remove")}
</button> </button>
)} )}
<button <button
@@ -1865,7 +1904,7 @@ function PackageModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
BACK {t("workshop.back")}
</button> </button>
</div> </div>
</div> </div>
@@ -1896,10 +1935,10 @@ function PackageModal({
> >
<div className="p-6 border-b border-[#555] bg-black/60"> <div className="p-6 border-b border-[#555] bg-black/60">
<span className="text-2xl mc-text-shadow block font-bold tracking-wide text-[#FFAA33]"> <span className="text-2xl mc-text-shadow block font-bold tracking-wide text-[#FFAA33]">
Dependencies Required {t("workshop.dependenciesRequired")}
</span> </span>
<span className="text-sm text-[#A0A0A0] mc-text-shadow uppercase tracking-widest opacity-80 mt-1"> <span className="text-sm text-[#A0A0A0] mc-text-shadow uppercase tracking-widest opacity-80 mt-1">
This package requires the following to be installed first {t("workshop.dependenciesRequiredDesc")}
</span> </span>
</div> </div>
<div className="p-4 flex flex-col gap-2 max-h-[300px] overflow-y-auto"> <div className="p-4 flex flex-col gap-2 max-h-[300px] overflow-y-auto">
@@ -1920,7 +1959,7 @@ function PackageModal({
); );
})} })}
<span className="text-xs text-[#888] mc-text-shadow mt-3 text-center"> <span className="text-xs text-[#888] mc-text-shadow mt-3 text-center">
These will be installed automatically before the main package. {t("workshop.dependenciesAutoInstall")}
</span> </span>
</div> </div>
<div className="flex items-center gap-4 p-4 border-t border-[#555] bg-black/40"> <div className="flex items-center gap-4 p-4 border-t border-[#555] bg-black/40">
@@ -1933,7 +1972,7 @@ function PackageModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
CANCEL {t("common.cancel").toUpperCase()}
</button> </button>
<button <button
onClick={() => { onClick={() => {
@@ -1947,7 +1986,7 @@ function PackageModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
INSTALL {t("workshop.install").toUpperCase()}
</button> </button>
</div> </div>
</motion.div> </motion.div>
@@ -2003,6 +2042,7 @@ function InstallModal({
playPressSound: () => void; playPressSound: () => void;
isPluginTab?: boolean; isPluginTab?: boolean;
}) { }) {
const { t } = useTranslation();
const game = useContext(GameContext); const game = useContext(GameContext);
const matchingEditions = const matchingEditions =
pkg.required_versions && pkg.required_versions.length > 0 pkg.required_versions && pkg.required_versions.length > 0
@@ -2234,12 +2274,14 @@ function InstallModal({
> >
<div className="p-6 border-b border-[#555] bg-black/60"> <div className="p-6 border-b border-[#555] bg-black/60">
<span className="text-2xl mc-text-shadow block font-bold tracking-wide"> <span className="text-2xl mc-text-shadow block font-bold tracking-wide">
{isPluginTab ? "INSTALL PLUGIN" : "INSTALL CONTENT"} {isPluginTab
? t("workshop.installPluginTitle")
: t("workshop.installContentTitle")}
</span> </span>
<span className="text-sm text-[#A0A0A0] mc-text-shadow uppercase tracking-widest opacity-80 mt-1"> <span className="text-sm text-[#A0A0A0] mc-text-shadow uppercase tracking-widest opacity-80 mt-1">
{isPluginTab {isPluginTab
? `Installing "${pkg.name}"` ? t("workshop.installingPackage", { name: pkg.name })
: `Target Edition for "${pkg.name}"`} : t("workshop.targetEdition", { name: pkg.name })}
</span> </span>
</div> </div>
@@ -2248,25 +2290,23 @@ function InstallModal({
<div className="py-8 flex flex-col items-center justify-center gap-3"> <div className="py-8 flex flex-col items-center justify-center gap-3">
<span className="text-2xl text-[#FFFF55] mc-text-shadow animate-pulse"> <span className="text-2xl text-[#FFFF55] mc-text-shadow animate-pulse">
{dependencies.length > 0 {dependencies.length > 0
? "Installing dependencies..." ? t("workshop.installingDependencies")
: isPluginTab : t("workshop.installing")}
? "Installing..."
: "Installing..."}
</span> </span>
<span className="text-xs text-[#A0A0A0] mc-text-shadow"> <span className="text-xs text-[#A0A0A0] mc-text-shadow">
{isPluginTab {isPluginTab
? "Downloading plugin files" ? t("workshop.downloadingPluginFiles")
: dependencies.length > 0 : dependencies.length > 0
? "Downloading and extracting required dependencies" ? t("workshop.downloadingDependencies")
: "Downloading and extracting assets"} : t("workshop.downloadingAssets")}
</span> </span>
<div className="w-full flex flex-col gap-1 px-2 mt-1"> <div className="w-full flex flex-col gap-1 px-2 mt-1">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-[11px] text-[#FFFF55] mc-text-shadow truncate"> <span className="text-[11px] text-[#FFFF55] mc-text-shadow truncate">
{progressLabel || {progressLabel ||
(isPluginTab (isPluginTab
? "Downloading plugin files" ? t("workshop.downloadingPluginFiles")
: "Downloading assets")} : t("workshop.downloadingAssets"))}
</span> </span>
<span className="text-[11px] text-[#FFFF55] mc-text-shadow shrink-0"> <span className="text-[11px] text-[#FFFF55] mc-text-shadow shrink-0">
{Math.floor(isPluginTab ? progress : overallProgress)}% {Math.floor(isPluginTab ? progress : overallProgress)}%
@@ -2299,7 +2339,7 @@ function InstallModal({
{isPluginTab && pkg.permissions && pkg.permissions.length > 0 && ( {isPluginTab && pkg.permissions && pkg.permissions.length > 0 && (
<div className="flex flex-col gap-1.5 mt-2 w-full px-4"> <div className="flex flex-col gap-1.5 mt-2 w-full px-4">
<span className="text-[10px] text-[#888] mc-text-shadow uppercase tracking-[0.2em] font-bold text-center"> <span className="text-[10px] text-[#888] mc-text-shadow uppercase tracking-[0.2em] font-bold text-center">
Requested Permissions {t("workshop.requestedPermissions")}
</span> </span>
<div className="flex flex-wrap gap-1.5 justify-center"> <div className="flex flex-wrap gap-1.5 justify-center">
{pkg.permissions.map((perm) => ( {pkg.permissions.map((perm) => (
@@ -2318,17 +2358,17 @@ function InstallModal({
{status === "success" && ( {status === "success" && (
<div className="py-8 flex flex-col items-center justify-center gap-3"> <div className="py-8 flex flex-col items-center justify-center gap-3">
<span className="text-2xl text-[#55FF55] mc-text-shadow"> <span className="text-2xl text-[#55FF55] mc-text-shadow">
Installed Successfully! {t("workshop.installedSuccessfully")}
</span> </span>
<span className="text-xs text-[#A0A0A0] mc-text-shadow"> <span className="text-xs text-[#A0A0A0] mc-text-shadow">
Press any key or click to continue {t("workshop.pressAnyKeyToContinue")}
</span> </span>
</div> </div>
)} )}
{status === "error" && ( {status === "error" && (
<div className="py-6 flex flex-col items-center justify-center gap-3"> <div className="py-6 flex flex-col items-center justify-center gap-3">
<span className="text-xl text-[#FF5555] mc-text-shadow"> <span className="text-xl text-[#FF5555] mc-text-shadow">
Installation Failed {t("workshop.installationFailed")}
</span> </span>
<span className="text-xs text-[#A0A0A0] mc-text-shadow text-center"> <span className="text-xs text-[#A0A0A0] mc-text-shadow text-center">
{errorMsg} {errorMsg}
@@ -2345,7 +2385,7 @@ function InstallModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Retry {t("workshop.retry")}
</button> </button>
</div> </div>
)} )}
@@ -2355,7 +2395,7 @@ function InstallModal({
(editionOptions.length === 0 ? ( (editionOptions.length === 0 ? (
<div className="py-6 flex items-center justify-center"> <div className="py-6 flex items-center justify-center">
<span className="text-[#FF5555] mc-text-shadow"> <span className="text-[#FF5555] mc-text-shadow">
No installed editions found {t("workshop.noInstalledEditions")}
</span> </span>
</div> </div>
) : ( ) : (
@@ -2395,6 +2435,7 @@ function UninstallModal({
isVersionTab?: boolean; isVersionTab?: boolean;
isPluginTab?: boolean; isPluginTab?: boolean;
}) { }) {
const { t } = useTranslation();
const { deleteCustomEdition } = useGame(); const { deleteCustomEdition } = useGame();
const game = useContext(GameContext); const game = useContext(GameContext);
const [focusedIdx, setFocusedIdx] = useState(0); const [focusedIdx, setFocusedIdx] = useState(0);
@@ -2511,12 +2552,14 @@ function UninstallModal({
> >
<div className="p-6 border-b border-[#555] bg-black/60"> <div className="p-6 border-b border-[#555] bg-black/60">
<span className="text-2xl mc-text-shadow block font-bold tracking-wide text-[#FF5555]"> <span className="text-2xl mc-text-shadow block font-bold tracking-wide text-[#FF5555]">
{isPluginTab ? "REMOVE PLUGIN" : "REMOVE CONTENT"} {isPluginTab
? t("workshop.removePluginTitle")
: t("workshop.removeContentTitle")}
</span> </span>
<span className="text-sm text-[#A0A0A0] mc-text-shadow uppercase tracking-widest opacity-80 mt-1"> <span className="text-sm text-[#A0A0A0] mc-text-shadow uppercase tracking-widest opacity-80 mt-1">
{isPluginTab {isPluginTab
? `Remove "${pkg.name}"` ? t("workshop.removePackage", { name: pkg.name })
: `Select edition to remove "${pkg.name}"`} : t("workshop.selectEditionToRemove", { name: pkg.name })}
</span> </span>
</div> </div>
@@ -2524,27 +2567,27 @@ function UninstallModal({
{status === "removing" && ( {status === "removing" && (
<div className="py-8 flex flex-col items-center justify-center gap-3"> <div className="py-8 flex flex-col items-center justify-center gap-3">
<span className="text-2xl text-[#FF5555] mc-text-shadow animate-pulse"> <span className="text-2xl text-[#FF5555] mc-text-shadow animate-pulse">
Removing... {t("workshop.removing")}
</span> </span>
<span className="text-xs text-[#A0A0A0] mc-text-shadow"> <span className="text-xs text-[#A0A0A0] mc-text-shadow">
Deleting installed files {t("workshop.deletingFiles")}
</span> </span>
</div> </div>
)} )}
{status === "success" && ( {status === "success" && (
<div className="py-8 flex flex-col items-center justify-center gap-3"> <div className="py-8 flex flex-col items-center justify-center gap-3">
<span className="text-2xl text-[#55FF55] mc-text-shadow"> <span className="text-2xl text-[#55FF55] mc-text-shadow">
Removed Successfully! {t("workshop.removedSuccessfully")}
</span> </span>
<span className="text-xs text-[#A0A0A0] mc-text-shadow"> <span className="text-xs text-[#A0A0A0] mc-text-shadow">
Press any key or click to continue {t("workshop.pressAnyKeyToContinue")}
</span> </span>
</div> </div>
)} )}
{status === "error" && ( {status === "error" && (
<div className="py-6 flex flex-col items-center justify-center gap-3"> <div className="py-6 flex flex-col items-center justify-center gap-3">
<span className="text-xl text-[#FF5555] mc-text-shadow"> <span className="text-xl text-[#FF5555] mc-text-shadow">
Removal Failed {t("workshop.removalFailed")}
</span> </span>
<span className="text-xs text-[#A0A0A0] mc-text-shadow text-center"> <span className="text-xs text-[#A0A0A0] mc-text-shadow text-center">
{errorMsg} {errorMsg}
@@ -2561,7 +2604,7 @@ function UninstallModal({
imageRendering: "pixelated", imageRendering: "pixelated",
}} }}
> >
Retry {t("workshop.retry")}
</button> </button>
</div> </div>
)} )}
+22
View File
@@ -0,0 +1,22 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import en from "./locales/en.json";
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: { translation: en },
},
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
detection: {
order: ["navigator", "localStorage"],
caches: ["localStorage"],
},
});
export default i18n;
+773
View File
@@ -0,0 +1,773 @@
{
"common": {
"back": "Back",
"done": "Done",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"ok": "OK",
"next": "Next",
"finish": "Finish",
"error": "Error!",
"on": "ON",
"off": "OFF",
"enabled": "Enabled",
"disabled": "Disabled",
"none": "None",
"unknown": "Unknown",
"all": "All",
"noResults": "No results",
"set": "set"
},
"header": {
"title": "Emerald Legacy Launcher"
},
"download": {
"title": "Downloads",
"gameFiles": "Game Files",
"updateAvailable": "Update Available!",
"gameUpdateAvailable": "Game Update Available!",
"steamIntegration": "Steam Integration",
"friendRequest": "Friend Request",
"gameInvite": "Game Invite"
},
"app": {
"day": "Day",
"night": "Night",
"toggleTime": "Toggle Time",
"swfEditor": "SWF Editor",
"controllerConnected": "CONTROLLER CONNECTED",
"welcome": "Welcome {{name}}!",
"version": "Version: {{version}}"
},
"home": {
"installVersion": "Install a version",
"stopGame": "Stop Game",
"installing": "Installation in progress...",
"playGame": "Play Game",
"downloadVersion": "Download {{name}}",
"helpAndOptions": "Help & Options",
"versions": "Versions",
"workshop": "Workshop",
"developerTools": "Developer Tools"
},
"settings": {
"audio": "Audio",
"video": "Video",
"launcher": "Launcher",
"game": "Game",
"controls": "Controls",
"plugins": "Plugins",
"android": "Android",
"credits": "Credits",
"music": "Music: {{value}}%",
"sound": "Sound: {{value}}%",
"clickEffects": "Click effects",
"animations": "Animations",
"appleSiliconPerfBoost": "Apple silicon performance boost",
"extraLaunchArgs": "Extra Launch Args",
"extraLaunchArgsDesc": "Space-separated arguments passed to the game executable",
"launchPrefix": "Launch Prefix",
"launchPrefixDesc": "Command that wraps the entire launch (e.g. gamemoderun)",
"launchEnvVars": "Launch Env Vars",
"launchEnvVarsDesc": "One KEY=VALUE per line",
"startInFullscreen": "Start in Fullscreen",
"discordRpc": "Discord RPC",
"skipIntro": "Skip Intro",
"legacyMode": "Legacy Mode",
"runner": "Runner",
"mangohud": "MangoHud",
"downloadRunner": "Download GE-Proton (Recommended)",
"downloadingRunner": "Downloading Runner... {{progress}}%",
"exportSettings": "Export Settings",
"importSettings": "Import Settings",
"resetSetup": "Reset Setup",
"languages": "Languages",
"stopGameButton": "STOP GAME",
"noPluginsInstalled": "No plugins installed",
"byAuthor": "by {{author}}",
"containerSettings": "Container Settings",
"openContainer": "Open Container",
"androidAudio": "Audio: {{backend}}",
"reset": {
"title": "Reset Setup",
"confirm": "Are you sure you want to reset launcher setup?",
"confirmTitle": "CONFIRM RESET",
"warning": "⚠️ This will:",
"clearAllSettings": "Clear all launcher settings",
"resetUsername": "Reset your username",
"showSetupAgain": "Show setup screen again",
"requireReconfiguration": "Require reconfiguration",
"cannotBeUndone": "This action cannot be undone!"
}
},
"setup": {
"letsConfigure": "Let's configure your launcher",
"compatibilityRuntime": "Compatibility Runtime",
"choosePreferredOptions": "Choose your preferred options and behaviors",
"username": "Username",
"enterUsername": "Enter your username",
"usernameRequired": "A username is required to continue",
"runtimeDetected": "Runtime Detected",
"runtimeNotDetected": "Runtime Not Detected",
"gptInstalled": "Game Porting Toolkit 3 and Wine installed",
"mustInstallRuntime": "You must install the runtime before proceeding.",
"installRuntime": "Install Runtime",
"reinstallRuntime": "Reinstall Runtime",
"noCompatibleRunners": "No compatible runners found. Please install Wine or Proton.",
"changeLaterInSettings": "You can change this later in settings",
"windowsNativeSupport": "Windows Native Support",
"windowsNativeDesc": "Emerald Legacy runs natively on Windows without additional requirements.",
"diamondRuntimeSupport": "DiamondRuntime Support",
"diamondRuntimeDesc": "Emerald Launcher runs natively on Android without additional requirements. Powered by our own DiamondRuntime.",
"setupComplete": "Emerald Launcher is now configured and ready to use!",
"discordRpc": "Discord RPC",
"animations": "Animations",
"changeTheseLater": "You can change these later in settings",
"ready": "Ready",
"installing": "Installing...",
"runtime": "Runtime",
"runner": "Runner",
"macosPreparing": "Preparing macOS runtime setup...",
"macosCompleted": "Setup completed successfully!",
"macosFailed": "Setup failed: {{error}}"
},
"versions": {
"title": "Versions",
"custom": "Custom",
"updateAvailable": "Update Available!",
"downloadToCustomPath": "Download to custom path",
"downloadDlc": "Download DLC",
"options": "Options",
"channel": "Channel",
"latest": "Latest",
"openFolder": "Open Folder",
"addToSteam": "Add to Steam",
"importWorld": "Import World",
"backup": "Backup",
"restore": "Restore",
"customize": "Customize",
"setUid": "Set UID",
"editCustom": "Edit Custom",
"removeCustom": "Remove Custom",
"uninstall": "Uninstall",
"viewPlaytime": "View playtime",
"invalidDirectory": "Invalid Directory",
"selectEmptyDirectory": "Please select an empty directory for installation.",
"deleteTitle": "Delete {{name}}?",
"deleteWarning": "Warning: All your saves and worlds for this version will be permanently deleted!"
},
"lceOnline": {
"hostGame": "Host Game",
"stopHosting": "Stop Hosting",
"addFriend": "Add Friend",
"signOut": "Sign Out",
"awaitingAuthentication": "Awaiting authentication...",
"friends": "Friends",
"invites": "Invites",
"requests": "Requests",
"pendingRequests": "Pending Requests",
"noneAvailable": "None available",
"invite": "Invite",
"remove": "Remove",
"cancel": "Cancel",
"accept": "Accept",
"decline": "Decline",
"username": "Username",
"send": "Send",
"error": "Error",
"ok": "OK",
"failedToStartHosting": "Failed to start hosting",
"anErrorOccurred": "An error occurred"
},
"devtools": {
"comingSoon": "Coming Soon",
"tools": {
"guides": "Community Guides",
"pck": "PCK Editor",
"arc": "ARC Editor",
"loc": "LOC Editor",
"grf": "GRF Editor",
"col": "COL Editor",
"options": "Options Editor",
"model": "Model Editor"
}
},
"guides": {
"communityGuides": "Community Guides",
"loadingGuide": "Loading guide...",
"backToGuides": "← Back to guides",
"loadingGuides": "Loading guides...",
"failedToLoadGuides": "Failed to load guides",
"noGuidesFound": "No guides found"
},
"skins": {
"skinLibrary": "Skin Library",
"capeLibrary": "Cape Library",
"importSkin": "Import Skin",
"importCape": "Import Cape",
"deleteSkin": "Delete Skin",
"deleteCape": "Delete Cape",
"editSkin": "Edit Skin",
"switchToCape": "Switch to Cape",
"switchToSkin": "Switch to Skin",
"failedToFetch": "Failed to fetch",
"active": "Active",
"slim": "Slim",
"noCape": "No Cape",
"fromFile": "From File",
"fromUsername": "From Username",
"minecraftUsername": "Minecraft Username",
"fetching": "Fetching...",
"fetch": "Fetch {{type}}",
"chooseModelType": "Choose the player model type for this skin",
"normalWideArms": "Normal (Wide Arms)",
"slimThinArms": "Slim (Thin Arms)"
},
"skinEditor": {
"title": "Skin Editor",
"pencil": "Pencil",
"eraser": "Eraser",
"pick": "Pick",
"fill": "Fill",
"color": "Color",
"alpha": "Alpha",
"alex": "Alex",
"steve": "Steve",
"grid": "Grid",
"undo": "Undo",
"save": "Save",
"export": "Export",
"back": "Back",
"customSkin": "Custom Skin",
"exportSkin": "Export Skin"
},
"goldMapper": {
"controller": "Controller",
"mouse": "Mouse",
"keyboard": "Keyboard",
"leftClick": "Left Click",
"middleClick": "Middle Click",
"rightClick": "Right Click",
"resetToDefaults": "Reset to Defaults",
"enableGoldMapper": "Enable GoldMapper",
"back": "Back",
"assign": "Assign {{name}}",
"typeKeyName": "Type a key name and press Enter",
"unknownKeyName": "Unknown key name",
"cancel": "Cancel"
},
"locEditor": {
"title": "LOC Editor",
"editingLoadedLoc": "editing loaded loc",
"openLoc": "Open LOC",
"saveLoc": "Save LOC",
"openToBegin": "Open a LOC file to begin editing",
"loaded": "Loaded {{name}}",
"failedToParse": "Failed to parse LOC",
"savedSuccessfully": "LOC Saved Successfully",
"stringAdded": "String Added",
"stringUpdated": "String Updated",
"stringDeleted": "String Deleted",
"static": "Static",
"keyed": "Keyed",
"searchStrings": "Search strings...",
"addString": "Add String",
"keyOrIndex": "Key / Index",
"value": "Value",
"actions": "Actions",
"edit": "Edit",
"back": "Back",
"stringTitle": "String",
"add": "Add",
"stringKey": "String Key",
"staticEntry": "Static entry - Index: {{index}}",
"stringValue": "String Value",
"cancel": "Cancel",
"save": "Save"
},
"grfEditor": {
"title": "GRF Editor",
"editing": "editing:",
"newGrf": "New GRF",
"openGrf": "Open GRF",
"saveGrf": "Save GRF",
"openToBegin": "Open a GRF file to begin editing",
"gameRules": "Game Rules",
"files": "Files ({{count}})",
"noRulesFound": "No rules found",
"addFile": "Add File",
"filename": "Filename",
"size": "Size",
"actions": "Actions",
"noFilesInGrf": "No files in GRF",
"delete": "Delete",
"back": "Back",
"loaded": "Loaded {{name}}",
"failedToParse": "Failed to parse GRF",
"newGrfCreated": "New GRF Created",
"added": "Added {{name}}",
"removed": "Removed {{name}}",
"savedSuccessfully": "GRF Saved Successfully",
"failedToSave": "Failed to save GRF",
"props": "[{{count}} props]"
},
"optionsEditor": {
"title": "Options Editor",
"openSettings": "Open settings.dat",
"saveSettings": "Save settings.dat",
"openToBegin": "Open settings.dat to begin",
"tabSettings": "Settings",
"tabSkins": "Skins",
"tabActions": "Actions",
"chosenSkinId": "Chosen Skin ID",
"playerCapeId": "Player Cape ID",
"favoriteSkins": "Favorite Skins",
"back": "Back",
"loaded": "Loaded options.dat",
"failedToParse": "Failed to parse Options",
"saved": "Options Saved",
"failedToSave": "Failed to save"
},
"swfEditor": {
"title": "SWF Editor",
"openSwf": "Open SWF",
"saveSwf": "Save SWF",
"openToBegin": "Open an SWF file to begin editing",
"searchAssets": "Search assets...",
"selectImageViewDetails": "Select an image to view details",
"characterId": "Character ID:",
"type": "Type:",
"size": "Size:",
"loadingPreview": "Loading Preview...",
"exportBitmap": "Export Bitmap",
"replaceBitmap": "Replace Bitmap",
"back": "Back",
"loaded": "Loaded {{name}}",
"failedToProcess": "Failed to process SWF",
"exported": "Exported: {{name}}",
"imageReplaced": "Image Replaced",
"savedSuccessfully": "SWF Saved Successfully"
},
"colEditor": {
"title": "COL Editor",
"version": "Version:",
"openCol": "Open COL",
"saveCol": "Save COL",
"openToBegin": "Open a COL file to begin editing",
"colors": "Colors ({{count}})",
"worldColors": "World Colors ({{count}})",
"searchColors": "Search colors...",
"addColor": "Add Color",
"addWorldColor": "Add World Color",
"name": "Name",
"color": "Color (ARGB)",
"water": "Water (ARGB)",
"underwater": "Underwater (ARGB)",
"fog": "Fog (ARGB)",
"remove": "Remove",
"back": "Back",
"loaded": "Loaded {{name}}",
"failedToParse": "Failed to parse COL",
"savedSuccessfully": "COL Saved Successfully",
"failedToSave": "Failed to save COL",
"colorAdded": "Color Added",
"worldColorAdded": "World Color Added"
},
"workshop": {
"tabs": {
"skin": "Skin",
"texture": "Texture",
"world": "World",
"mod": "Mod",
"dlc": "DLC",
"plugins": "Plugins",
"versions": "Versions",
"installed": "Installed",
"search": "Search",
"server": "Server",
"serverPlugins": "Server Plugins"
},
"filterInstalled": "FILTER INSTALLED...",
"filterVersions": "FILTER VERSIONS...",
"filterPlugins": "FILTER PLUGINS...",
"filterServers": "FILTER SERVERS...",
"enterKeywords": "ENTER KEYWORDS...",
"startTypingToSearch": "Start typing to search...",
"searchingArchives": "Searching Archives...",
"nothingInstalled": "Nothing Installed",
"noPluginsAvailable": "No plugins available",
"noServersAvailable": "No servers available",
"emptyCategory": "Empty category",
"noImage": "No Image",
"updateAll": "Update All ({{count}})",
"reinstallAll": "Reinstall All ({{count}})",
"update": "Update",
"installed": "Installed",
"added": "Added",
"install": "INSTALL",
"reinstall": "REINSTALL",
"add": "ADD",
"download": "DOWNLOAD",
"remove": "REMOVE",
"byAuthor": "By {{author}}",
"updateAvailable": "Update Available",
"saved": "Saved",
"back": "Back",
"failedToLoadRegistry": "Failed to load registry",
"saveServerPlugin": "Save Server Plugin",
"details": "Details",
"metadata": "Metadata",
"address": "Address:",
"type": "Type:",
"version": "Version:",
"owner": "Owner:",
"discordServer": "Discord Server:",
"downloads": "Downloads:",
"likes": "Likes:",
"serverType": "Server Type:",
"file": "File:",
"fileSize": "File Size:",
"github": "GitHub:",
"packageId": "Package ID:",
"outdated": " (outdated)",
"categories": "Categories",
"requiredDependencies": "Required Dependencies",
"installedSuffix": " (installed)",
"requiredEditions": "Required Editions",
"files": "Files",
"splitArchive": "Split archive · {{count}} parts",
"dependenciesRequired": "Dependencies Required",
"dependenciesRequiredDesc": "This package requires the following to be installed first",
"dependenciesAutoInstall": "These will be installed automatically before the main package.",
"installPluginTitle": "INSTALL PLUGIN",
"installContentTitle": "INSTALL CONTENT",
"installingPackage": "Installing \"{{name}}\"",
"targetEdition": "Target Edition for \"{{name}}\"",
"installingDependencies": "Installing dependencies...",
"installing": "Installing...",
"downloadingPluginFiles": "Downloading plugin files",
"downloadingDependencies": "Downloading and extracting required dependencies",
"downloadingAssets": "Downloading and extracting assets",
"requestedPermissions": "Requested Permissions",
"installedSuccessfully": "Installed Successfully!",
"pressAnyKeyToContinue": "Press any key or click to continue",
"installationFailed": "Installation Failed",
"retry": "Retry",
"noInstalledEditions": "No installed editions found",
"removePluginTitle": "REMOVE PLUGIN",
"removeContentTitle": "REMOVE CONTENT",
"removePackage": "Remove \"{{name}}\"",
"selectEditionToRemove": "Select edition to remove \"{{name}}\"",
"removing": "Removing...",
"deletingFiles": "Deleting installed files",
"removedSuccessfully": "Removed Successfully!",
"removalFailed": "Removal Failed"
},
"screenshots": {
"title": "Screenshots",
"noScreenshots": "No screenshots found",
"takeInGameWithF2": "Take some in-game with F2",
"openFolder": "OPEN FOLDER",
"delete": "DELETE",
"close": "CLOSE",
"scanning": "Scanning Archives...",
"capturedOn": "Captured on {{date}}",
"deleteConfirm": "Are you sure you want to delete this screenshot?"
},
"pckEditor": {
"openPck": "Open PCK",
"newPck": "New PCK",
"exportAll": "Export All",
"savePck": "Save PCK",
"searchAssets": "Search assets...",
"addAsset": "Add Asset",
"selectAsset": "Select an asset to view details",
"metadataProperties": "Metadata Properties",
"addProperty": "+ Add Property",
"remove": "Remove",
"export": "Export",
"replace": "Replace",
"renameAsset": "Rename Asset (Path)",
"deleteAsset": "Delete This Asset",
"selectAssetType": "Select Asset Type",
"rename": "Rename",
"assetRenamed": "Asset Renamed",
"exportingAllAssets": "Exporting all assets...",
"allAssetsExported": "All Assets Exported",
"exportFailed": "Export failed",
"pckSaved": "PCK Saved Successfully",
"newPckCreated": "New PCK Created",
"title": "PCK Editor",
"openToBegin": "Open a PCK file to begin editing",
"endianness": "Endianness:",
"xmlSupport": "XML Support:",
"enabled": "Enabled",
"disabled": "Disabled",
"version": "Version:",
"exportAssetDialog": "Export Asset",
"savePckDialog": "Save PCK",
"failedToParse": "Failed to parse PCK",
"exported": "Exported: {{name}}",
"deleted": "Deleted: {{name}}",
"assetReplaced": "Asset Replaced",
"assetAdded": "Asset Added",
"noMetadataProperties": "No metadata properties",
"view3dSkin": "3D Skin View",
"view3dCape": "3D Cape View",
"texturePreview": "Texture Preview",
"back": "Back",
"renameTitle": "Rename Asset",
"newAssetPath": "New Asset Path",
"cancel": "Cancel"
},
"modelEditor": {
"title": "Model Editor",
"import": "Import",
"export": "Export",
"save": "Save",
"bounds": "Bounds",
"importToBegin": "Import a .bbmodel or .json file to begin",
"startDefaultModel": "Start with Default Model",
"models": "Models ({{count}})",
"add": "+ Add",
"remove": "Remove",
"edit": "Edit",
"parts": "Parts ({{count}})",
"boxes": "Boxes ({{count}})",
"box": "Box {{index}}",
"textures": "Textures ({{count}})",
"noTextures": "No textures",
"selectModel": "Select a model to preview",
"back": "Back",
"editPart": "Edit Part",
"name": "Name",
"translation": "Translation",
"editBox": "Edit Box",
"position": "Position",
"size": "Size",
"uvOffset": "UV Offset",
"inflate": "Inflate",
"mirrorUv": "Mirror UV",
"cancel": "Cancel",
"save": "Save",
"imported": "Imported: {{name}}",
"loadedModels": "Loaded {{count}} model(s)",
"failedToParse": "Failed to parse",
"addedTexture": "Added texture: {{name}}",
"created": "Created: {{name}}",
"exported": "Exported: {{name}}",
"exportFailed": "Export failed",
"modelDeleted": "Model deleted"
},
"arcEditor": {
"openArc": "Open ARC",
"exportAll": "Export All",
"saveArc": "Save ARC",
"openArcFile": "Open an ARC file to begin editing",
"archive": "Archive",
"languagesLoc": "Languages (LOC)",
"searchEntries": "Search entries...",
"addEntry": "Add Entry",
"entryDetails": "Entry Details",
"exportFile": "Export File",
"replaceData": "Replace Data",
"renamePath": "Rename / Path",
"deleteEntry": "Delete Entry",
"zlibCompressed": "ZLIB Compressed",
"uncompressed": "Uncompressed",
"noLocFound": "No languages.loc found in archive",
"createLoc": "Create languages.loc",
"writeToArc": "Write to ARC",
"addString": "Add String",
"title": "ARC Editor",
"size": "Size",
"format": "Format",
"compressed": "Compressed",
"raw": "Raw",
"selectEntry": "Select an entry to view details",
"zlib": "ZLIB",
"searchStrings": "Search strings...",
"keyOrIndex": "Key / Index",
"value": "Value",
"actions": "Actions",
"edit": "Edit",
"static": "Static",
"keyed": "Keyed",
"back": "Back",
"saveArcDialog": "Save ARC",
"exportAssetDialog": "Export Asset",
"loaded": "Loaded {{name}}",
"failedToParse": "Failed to parse ARC",
"arcSaved": "ARC Saved Successfully",
"saveFailed": "Save failed",
"extracted": "Extracted: {{name}}",
"extractionFailed": "Extraction failed",
"deleted": "Deleted: {{name}}",
"locUpdatedInArchive": "languages.loc updated in archive",
"locAddedToArchive": "languages.loc added to archive",
"entryAdded": "Entry Added",
"entryReplaced": "Entry Replaced",
"entryRenamed": "Entry Renamed",
"stringAdded": "String Added",
"stringUpdated": "String Updated",
"stringDeleted": "String Deleted",
"exportingAll": "Exporting all archive entries...",
"allEntriesExported": "All Entries Exported",
"exportFailed": "Export failed",
"compressionEnabled": "Compression Enabled",
"compressionDisabled": "Compression Disabled",
"createdNewLocaleStructure": "Created new locale structure",
"addFileToArchive": "Add File to Archive",
"selectSourceFile": "Select Source File",
"cancel": "Cancel",
"replaceFileData": "Replace File Data",
"selectNewFile": "Select New File",
"renameEntry": "Rename Entry",
"newArchivePath": "New Archive Path",
"markAsCompressed": "Mark as compressed (zlib)",
"rename": "Rename",
"stringTitle": "String",
"add": "Add",
"stringKey": "String Key",
"staticEntry": "Static entry - Index: {{index}}",
"stringValue": "String Value",
"save": "Save"
},
"credits": {
"backToMenu": "Back to Menu"
},
"modals": {
"downloadDlc": {
"title": "Download DLC",
"loading": "Loading available DLCs...",
"noneFound": "No DLC folders found in the repository.",
"available": "{{count}} available",
"all": "All",
"none": "None",
"download": "Download ({{count}})",
"done": "Done",
"close": "Close",
"downloaded": "Downloaded {{downloaded}}/{{selected}}",
"failedToDownload": "Failed to download {{name}}: {{reason}}"
},
"gameLog": {
"title": "Game Crash Log",
"saveDialog": "Save Game Log",
"saveAsFile": "Save As File",
"copy": "Copy",
"close": "Close",
"saved": "Saved!",
"failedToSave": "Failed to save.",
"copied": "Copied to clipboard!",
"failedToCopy": "Failed to copy.",
"showingLast": "Showing last {{shown}} lines of {{total}}."
},
"setUid": {
"title": "Set UID",
"manual": "Manual",
"copy": "Copy",
"enterUid": "Enter UID",
"selectInstance": "Select Installed Instance",
"selectPlaceholder": "-- Select an Instance --",
"noInstances": "No other installed instances available.",
"selectToCopy": "Select an instance to copy from.",
"sourceNoUid": "Source instance has no uid.dat or it could not be read.",
"uidEmpty": "UID cannot be empty.",
"cancel": "Cancel",
"save": "Save"
},
"playtime": {
"title": "Playtime",
"loading": "Loading...",
"total": "Total",
"thisWeek": "This Week",
"today": "Today",
"dailyBreakdown": "Daily Breakdown",
"failedToLoad": "Failed to load playtime data.",
"close": "Close"
},
"customize": {
"title": "Customize",
"titleImage": "Title Image",
"panoramaBackground": "Panorama Background",
"changeImage": "Change Image",
"pickImage": "Pick Image",
"reset": "Reset",
"preview": "Preview",
"selectTitleImage": "Select Title Image",
"selectPanorama": "Select Panorama Background",
"cancel": "Cancel",
"save": "Save"
},
"chooseInstance": {
"title": "Join Game",
"joiningTitle": "Joining Game",
"joinPrompt": "Joining {{name}}'s game. Choose an instance:",
"join": "Join",
"cancel": "Cancel",
"noInstances": "No installed instances available. Install a version first.",
"launching": "Launching game...",
"connectingRelay": "Connecting via relay..."
},
"importWorld": {
"title": "Import World",
"importingTitle": "Importing World",
"importInto": "Import into:",
"importMsPrompt": "Import an existing .ms save file:",
"selectMsFile": "Select .ms File",
"convertConsolePrompt": "Convert an Xbox 360 or PS3 save:",
"xbox360": "Xbox 360 (.bin)",
"ps3Folder": "PS3 (Folder)",
"javaConversionPrompt": "Java Edition world conversion:",
"javaToLce": "Java → LCE",
"lceToJava": "LCE → Java",
"cancel": "Cancel",
"selectingSource": "Selecting source...",
"selectingMsFile": "Selecting saveData.ms file...",
"copyingLceSave": "Copying LCE save...",
"worldImported": "World imported into \"{{name}}\"!",
"selectingXboxSave": "Selecting Xbox 360 save (.bin)...",
"convertingXbox": "Converting Xbox 360 save...",
"xboxConverted": "Xbox 360 save converted into \"{{name}}\"!",
"selectingPs3Folder": "Selecting PS3 save folder...",
"convertingPs3": "Converting PS3 save...",
"ps3Converted": "PS3 save converted into \"{{name}}\"!",
"selectingJavaFolder": "Selecting Java world folder...",
"convertingJava": "Converting Java world to LCE...",
"javaConverted": "Java world converted into \"{{name}}\"!",
"selectingOutputFolder": "Selecting output folder for Java world...",
"convertingLceToJava": "Converting LCE save to Java world...",
"javaExported": "Java world exported to \"{{path}}\"!"
},
"customTu": {
"importTitle": "Import Custom TU",
"editTitle": "Edit Custom TU",
"tuName": "TU Name",
"description": "Description (Optional)",
"downloadUrl": "Download URL (.zip)",
"localPath": "Local Path",
"namePlaceholder": "e.g. My Awesome Mod",
"descPlaceholder": "A brief description...",
"urlPlaceholder": "optional if path is set",
"nameRequired": "Name is required",
"urlOrPathRequired": "URL or Path is required",
"invalidUrl": "Invalid URL",
"defaultDesc": "Custom imported TU",
"cancel": "Cancel",
"save": "Save",
"import": "Import"
},
"options": {
"title": "Options",
"general": "General",
"loading": "Loading options...",
"reset": "Reset",
"cancel": "Cancel",
"save": "Save",
"ok": "OK",
"noSchema": "This instance does not provide a launch options schema.",
"invalidSchema": "The launch options schema is invalid or unsupported."
}
}
}
+1
View File
@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import "tauri-plugin-gamepad-api"; import "tauri-plugin-gamepad-api";
import "./i18n/config";
import App from "./pages/App"; import App from "./pages/App";
import "./css/index.css"; import "./css/index.css";
import "./css/App.css"; import "./css/App.css";
+14 -12
View File
@@ -1,4 +1,5 @@
import { useEffect, useState, useMemo, useCallback, useRef } from "react"; import { useEffect, useState, useMemo, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { motion, AnimatePresence, MotionConfig } from "framer-motion"; import { motion, AnimatePresence, MotionConfig } from "framer-motion";
import "../css/App.css"; import "../css/App.css";
import HomeView from "../components/views/HomeView"; import HomeView from "../components/views/HomeView";
@@ -51,6 +52,7 @@ import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
export default function App() { export default function App() {
const { t } = useTranslation();
const ui = useUI(); const ui = useUI();
const { const {
showIntro, showIntro,
@@ -437,7 +439,7 @@ export default function App() {
"https://github.com/LCE-Hub/LCE-Emerald-Launcher/releases/latest", "https://github.com/LCE-Hub/LCE-Emerald-Launcher/releases/latest",
) )
} }
title="Update Available!" title={t("download.updateAvailable")}
variant="update" variant="update"
/> />
@@ -448,14 +450,14 @@ export default function App() {
clearGameUpdate(); clearGameUpdate();
setActiveView("versions"); setActiveView("versions");
}} }}
title="Game Update Available!" title={t("download.gameUpdateAvailable")}
variant="update" variant="update"
/> />
<AchievementToast <AchievementToast
message={game.steamSuccessMessage} message={game.steamSuccessMessage}
onClose={clearSteamSuccess} onClose={clearSteamSuccess}
title="Steam Integration" title={t("download.steamIntegration")}
variant="steam" variant="steam"
/> />
@@ -503,7 +505,7 @@ export default function App() {
setActiveView("main"); setActiveView("main");
}} }}
className="outline-none border-none flex items-center justify-center w-10 h-10 cursor-pointer" className="outline-none border-none flex items-center justify-center w-10 h-10 cursor-pointer"
aria-label="Back" aria-label={t("common.back")}
style={{ style={{
backgroundImage: "url('/images/Button_Square.png')", backgroundImage: "url('/images/Button_Square.png')",
backgroundSize: "100% 100%", backgroundSize: "100% 100%",
@@ -544,7 +546,7 @@ export default function App() {
className="absolute bottom-6 right-8 z-50 flex items-center gap-3" className="absolute bottom-6 right-8 z-50 flex items-center gap-3"
> >
<span className="text-[#E0E0E0] text-[10px] mc-text-shadow tracking-widest uppercase opacity-70 mt-1"> <span className="text-[#E0E0E0] text-[10px] mc-text-shadow tracking-widest uppercase opacity-70 mt-1">
{displayIsDay ? "Day" : "Night"} {displayIsDay ? t("app.day") : t("app.night")}
</span> </span>
<button <button
onClick={() => { onClick={() => {
@@ -559,7 +561,7 @@ export default function App() {
? "/images/Day_Toggle.png" ? "/images/Day_Toggle.png"
: "/images/Night_Toggle.png" : "/images/Night_Toggle.png"
} }
alt="Toggle Time" alt={t("app.toggleTime")}
className="w-12 h-12 cursor-pointer block object-contain" className="w-12 h-12 cursor-pointer block object-contain"
style={{ imageRendering: "pixelated" }} style={{ imageRendering: "pixelated" }}
/> />
@@ -592,7 +594,7 @@ export default function App() {
}} }}
/> />
<span className="text-[#FFFF55] text-sm mc-text-shadow"> <span className="text-[#FFFF55] text-sm mc-text-shadow">
SWF Editor {t("app.swfEditor")}
</span> </span>
</button> </button>
</motion.div> </motion.div>
@@ -626,7 +628,7 @@ export default function App() {
style={{ textShadow: "2px 2px 0px #3F3F00" }} style={{ textShadow: "2px 2px 0px #3F3F00" }}
> >
{audio.splashIndex === -1 {audio.splashIndex === -1
? `Welcome ${config.username}!` ? t("app.welcome", { name: config.username })
: audio.splashes[audio.splashIndex]} : audio.splashes[audio.splashIndex]}
</div> </div>
</motion.div> </motion.div>
@@ -749,10 +751,10 @@ export default function App() {
style={{ fontWeight: "normal" }} style={{ fontWeight: "normal" }}
> >
<div className="flex-1 text-left whitespace-nowrap"> <div className="flex-1 text-left whitespace-nowrap">
Version: {pkg.version} ({__BUILD_DATE__}) {t("app.version", { version: `${pkg.version} (${__BUILD_DATE__})` })}
</div> </div>
<div className="flex-1 text-right whitespace-nowrap"> <div className="flex-1 text-right whitespace-nowrap">
{connected && "CONTROLLER CONNECTED"} {connected && t("app.controllerConnected")}
</div> </div>
</motion.footer> </motion.footer>
</motion.div> </motion.div>
@@ -764,7 +766,7 @@ export default function App() {
clearFriendRequestMessage(); clearFriendRequestMessage();
setActiveView("lceonline"); setActiveView("lceonline");
}} }}
title="Friend Request" title={t("download.friendRequest")}
variant="update" variant="update"
/> />
@@ -775,7 +777,7 @@ export default function App() {
clearInviteMessage(); clearInviteMessage();
setActiveView("lceonline"); setActiveView("lceonline");
}} }}
title="Game Invite" title={t("download.gameInvite")}
variant="update" variant="update"
/> />
</div> </div>