mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-09-24 09:03:44 +00:00
feat: support for translations!
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
Generated
+75
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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%" }}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
← 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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
@@ -838,25 +845,25 @@ const LceOnlineView = memo(function LceOnlineView({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
setIsAddingFriend(false);
|
setIsAddingFriend(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
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"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: "url('/images/Button_Background.png')",
|
backgroundImage: "url('/images/Button_Background.png')",
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: "100% 100%",
|
||||||
imageRendering: "pixelated",
|
imageRendering: "pixelated",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsAddingFriend(false);
|
setIsAddingFriend(false);
|
||||||
playBackSound();
|
playBackSound();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Cancel
|
{t("lceOnline.cancel")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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={() =>
|
||||||
@@ -915,13 +918,13 @@ function PartEditModal({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="px-8 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
|
className="px-8 py-2 text-white mc-text-shadow transition-all hover:text-[#FFFF55] text-lg outline-none"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: "url('/images/Button_Background.png')",
|
backgroundImage: "url('/images/Button_Background.png')",
|
||||||
backgroundSize: "100% 100%",
|
backgroundSize: "100% 100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Save
|
{t("modelEditor.save")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} · v{p.manifest.version}
|
{t("settings.byAuthor", { author: p.manifest.author })}{" "}
|
||||||
|
· 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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,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";
|
||||||
|
|||||||
+18
-16
@@ -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>
|
||||||
@@ -748,12 +750,12 @@ export default function App() {
|
|||||||
className="shrink-0 p-4 flex justify-between items-end text-[10px] text-[#A0A0A0] mc-text-shadow bg-gradient-to-t from-black/80 to-transparent uppercase tracking-widest opacity-60 font-['Mojangles']"
|
className="shrink-0 p-4 flex justify-between items-end text-[10px] text-[#A0A0A0] mc-text-shadow bg-gradient-to-t from-black/80 to-transparent uppercase tracking-widest opacity-60 font-['Mojangles']"
|
||||||
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user