Merge pull request #2787 from rommapp/feat/patcher.js

[ROMM-2098] Integrated patcher.js
This commit is contained in:
Zurdi
2026-01-02 16:19:49 +01:00
committed by GitHub
61 changed files with 1843 additions and 50 deletions

View File

@@ -37,5 +37,5 @@ jobs:
- name: Lockfile lint
run: |
[ -z "$(jq -r '.packages | to_entries[] | select((.key | contains("node_modules")) and (.value | has("resolved") and has("integrity") | not)) | .key' < package-lock.json)" ]
[ -z "$(jq -r '.packages | to_entries[] | select((.key | contains("node_modules")) and (.value.resolved | contains("git+ssh") | not) and (.value | has("resolved") and has("integrity") | not)) | .key' < package-lock.json)" ]
working-directory: frontend

View File

@@ -37,8 +37,8 @@ lint:
- prettier@3.7.4:
packages:
- "@trivago/prettier-plugin-sort-imports@6.0.0"
- "@vue/compiler-sfc@3.5.25"
- ruff@0.14.9
- "@vue/compiler-sfc@3.5.26"
- ruff@0.14.10
- shellcheck@0.11.0
- shfmt@3.6.0
- taplo@0.10.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -20,6 +20,7 @@
"mitt": "^3.0.1",
"pinia": "^3.0.1",
"qrcode": "^1.5.4",
"rom-patcher": "github:marcrobledo/RomPatcher.js#v3.2.1",
"semver": "^7.6.2",
"socket.io-client": "^4.7.5",
"tailwindcss": "^4.0.0",
@@ -4547,7 +4548,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -5935,7 +5935,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -7836,6 +7835,25 @@
"fsevents": "~2.3.2"
}
},
"node_modules/rom-patcher": {
"version": "3.0.0",
"resolved": "git+ssh://git@github.com/marcrobledo/RomPatcher.js.git#91e522e247f709e894761157ccba3189004d0859",
"dependencies": {
"chalk": "4.1.2",
"commander": "^11.0.0"
},
"bin": {
"RomPatcher": "index.js"
}
},
"node_modules/rom-patcher/node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"engines": {
"node": ">=16"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -8366,7 +8384,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},

View File

@@ -39,6 +39,7 @@
"mitt": "^3.0.1",
"pinia": "^3.0.1",
"qrcode": "^1.5.4",
"rom-patcher": "github:marcrobledo/RomPatcher.js#v3.2.1",
"semver": "^7.6.2",
"socket.io-client": "^4.7.5",
"tailwindcss": "^4.0.0",

View File

@@ -0,0 +1,161 @@
/* eslint-disable no-undef */
/// <reference lib="webworker" />
// Load all patcher scripts
let scriptsLoaded = false;
async function loadScripts() {
if (scriptsLoaded) return;
self.BinFile =
self.IPS =
self.UPS =
self.APS =
self.APSGBA =
self.BPS =
self.RUP =
self.PPF =
self.BDF =
self.PMSR =
self.VCDIFF =
null;
try {
importScripts(
"/node_modules/rom-patcher/rom-patcher-js/modules/BinFile.js",
"/node_modules/rom-patcher/rom-patcher-js/modules/HashCalculator.js",
"/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.aps_gba.js",
"/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.aps_n64.js",
"/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.bdf.js",
"/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.bps.js",
"/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.ips.js",
"/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.pmsr.js",
"/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.ppf.js",
"/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.rup.js",
"/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.ups.js",
"/node_modules/rom-patcher/rom-patcher-js/modules/RomPatcher.format.vcdiff.js",
"/node_modules/rom-patcher/rom-patcher-js/RomPatcher.js",
);
scriptsLoaded = true;
return true;
} catch (error) {
throw new Error(`Failed to load patcher scripts: ${error.message}`);
}
}
// Handle messages from main thread
self.addEventListener("message", async (e) => {
const {
type,
romData,
patchData,
romFileName,
patchFileName,
customFileName,
} = e.data;
if (type === "PATCH") {
try {
// Load scripts if not already loaded
self.postMessage({
type: "STATUS",
message: "Loading patcher libraries...",
});
await loadScripts();
// Extract patch name without extension for custom suffix
const patchNameWithoutExt = patchFileName.replace(/\.[^.]+$/, "");
// Try to create BinFile from Uint8Array
self.postMessage({ type: "STATUS", message: "Reading ROM file..." });
const romUint8 = new Uint8Array(romData);
const romBin = await new Promise((resolve, reject) => {
try {
new BinFile(romUint8, (bf) => {
if (bf) {
bf.fileName = romFileName;
resolve(bf);
} else {
reject(new Error("Failed to create ROM BinFile"));
}
});
} catch (err) {
reject(err);
}
});
self.postMessage({ type: "STATUS", message: "Reading patch file..." });
const patchUint8 = new Uint8Array(patchData);
const patchBin = await new Promise((resolve, reject) => {
try {
new BinFile(patchUint8, (bf) => {
if (bf) {
bf.fileName = patchFileName;
resolve(bf);
} else {
reject(new Error("Failed to create patch BinFile"));
}
});
} catch (err) {
reject(err);
}
});
// Parse patch
self.postMessage({ type: "STATUS", message: "Parsing patch format..." });
const patch = RomPatcher.parsePatchFile(patchBin);
if (!patch) {
throw new Error("Unsupported or invalid patch format.");
}
// Apply patch
self.postMessage({
type: "STATUS",
message: "Applying patch (this may take a moment)...",
});
const patched = RomPatcher.applyPatch(romBin, patch, {
requireValidation: false,
fixChecksum: false,
outputSuffix: false, // Don't add default suffix
});
// Extract the patched binary data
const patchedData = patched._u8array || patched.u8array || patched.data;
if (!patchedData) {
throw new Error("Failed to extract patched ROM data");
}
// Create custom filename with patch name
const romBaseName = romFileName.replace(/\.[^.]+$/, "");
const romExtension = romFileName.match(/\.[^.]+$/)?.[0] || "";
const defaultFileName = `${romBaseName} (patched-${patchNameWithoutExt})${romExtension}`;
// If custom filename provided, strip any extension and add ROM extension
let finalFileName;
if (customFileName && customFileName.trim()) {
const customBase = customFileName.trim().replace(/\.[^.]+$/, "");
finalFileName = `${customBase}${romExtension}`;
} else {
finalFileName = defaultFileName;
}
// Send back the result
self.postMessage(
{
type: "SUCCESS",
patchedData: patchedData.buffer,
fileName: finalFileName,
},
[patchedData.buffer],
); // Transfer ownership of ArrayBuffer
} catch (error) {
self.postMessage({
type: "ERROR",
error: error.message || String(error),
});
}
}
});

View File

@@ -8,6 +8,7 @@ import CollectionsBtn from "@/components/common/Navigation/CollectionsBtn.vue";
import CollectionsDrawer from "@/components/common/Navigation/CollectionsDrawer.vue";
import ConsoleModeBtn from "@/components/common/Navigation/ConsoleModeBtn.vue";
import HomeBtn from "@/components/common/Navigation/HomeBtn.vue";
import PatcherBtn from "@/components/common/Navigation/PatcherBtn.vue";
import PlatformsBtn from "@/components/common/Navigation/PlatformsBtn.vue";
import PlatformsDrawer from "@/components/common/Navigation/PlatformsDrawer.vue";
import ScanBtn from "@/components/common/Navigation/ScanBtn.vue";
@@ -46,7 +47,8 @@ function collapse() {
</template>
<template #append>
<RandomBtn />
<PatcherBtn class="mr-2" />
<RandomBtn class="mr-2" />
<UploadBtn class="mr-2" />
<UserBtn class="mr-1" />
</template>
@@ -109,6 +111,7 @@ function collapse() {
<ConsoleModeBtn :with-tag="!mainBarCollapsed" rounded class="mt-2" block />
<template #append>
<PatcherBtn :with-tag="!mainBarCollapsed" rounded class="mt-2" block />
<RandomBtn :with-tag="!mainBarCollapsed" rounded class="mt-2" block />
<UploadBtn
:with-tag="!mainBarCollapsed"

View File

@@ -0,0 +1,49 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import storeNavigation from "@/stores/navigation";
withDefaults(
defineProps<{
block?: boolean;
height?: string;
rounded?: boolean;
withTag?: boolean;
}>(),
{
block: false,
height: "",
rounded: false,
withTag: false,
},
);
const { t } = useI18n();
const navigationStore = storeNavigation();
</script>
<template>
<v-btn
icon
:block="block"
variant="flat"
color="background"
:height="height"
:class="{ rounded: rounded }"
class="py-4 bg-background d-flex align-center justify-center"
@click="navigationStore.goPatcher"
>
<div class="d-flex flex-column align-center">
<v-icon :color="$route.path.startsWith('/patcher') ? 'primary' : ''">
mdi-file-cog
</v-icon>
<v-expand-transition>
<span
v-if="withTag"
class="text-caption text-center"
:class="{ 'text-primary': $route.path.startsWith('/patcher') }"
>
{{ t("common.patcher") }}
</span>
</v-expand-transition>
</div>
</v-btn>
</template>

View File

@@ -2,6 +2,7 @@
"about": "O aplikaci",
"add": "Přidat",
"administration": "Administrace",
"and": "a",
"apply": "Použít",
"ascii-only": "Pouze ASCII znaky",
"cancel": "Zrušit",
@@ -26,6 +27,7 @@
"library-management": "Správa knihovny",
"logout": "Odhlásit se",
"name": "Název",
"patcher": "Patcher",
"password-length": "Heslo musí mít 6 až 255 znaků",
"platform": "Platforma",
"platforms": "Platformy",

View File

@@ -0,0 +1,34 @@
{
"title": "ROM Patcher",
"subtitle": "Vyberte základní ROM a patch soubor, poté aplikujte pro stažení opatchovaného ROMu.",
"rom-file": "ROM soubor",
"patch-file": "Patch soubor",
"drop-rom-here": "Přetáhněte ROM sem",
"drop-patch-here": "Přetáhněte patch sem",
"drag-drop-rom": "Přetáhněte ROM soubor nebo klikněte pro procházení.",
"drag-drop-patch": "Přetáhněte patch soubor nebo klikněte pro procházení.",
"choose-rom": "Vybrat ROM",
"choose-patch": "Vybrat patch",
"replace": "Nahradit",
"supported-formats": "Podporované formáty patchů",
"download-locally": "Stáhnout opatchovaný ROM",
"upload-to-romm": "Nahrát do RomM",
"output-filename": "Název výstupního souboru (volitelné)",
"apply-download-upload": "Aplikovat, Stáhnout a Nahrát",
"apply-upload": "Aplikovat a Nahrát",
"apply-download": "Aplikovat a Stáhnout",
"powered-by": "Běží na patcherjs",
"error-no-rom": "Prosím vyberte ROM soubor.",
"error-no-patch": "Prosím vyberte patch soubor.",
"error-no-platform": "Prosím vyberte platformu pro nahrání.",
"error-no-action": "Prosím vyberte alespoň jednu akci: stažení nebo nahrání.",
"status-preparing": "Příprava souborů...",
"status-downloading": "Stahování opatchovaného ROMu...",
"status-uploading": "Nahrávání do RomM...",
"success-uploaded": "nahráno",
"success-downloaded": "staženo",
"success-message": "Opatchovaný ROM {actions} úspěšně!",
"error-upload-failed": "Nelze nahrát ROM: {error}",
"upload-success": "Opatchovaný ROM úspěšně nahrán{errors}. Spouštění skenování...",
"upload-errors": " (s některými chybami)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "Vybrat stav",
"change-state": "Změnit stav",
"deselect-state": "Zrušit výběr stavu",
"no-save-selected": "Není vybrána žádná uložená pozice",
"no-state-selected": "Není vybrán žádný stav",
"no-saves-available": "Nejsou k dispozici žádné uložené pozice",
"no-states-available": "Nejsou k dispozici žádné stavy",
"background-color": "Barva pozadí",
"select-background-color": "Vybrat barvu pozadí"
}

View File

@@ -2,6 +2,7 @@
"about": "Über",
"add": "Hinzufügen",
"administration": "Administration",
"and": "und",
"apply": "Anwenden",
"ascii-only": "Nur ASCII-Zeichen",
"cancel": "Abbrechen",
@@ -26,6 +27,7 @@
"library-management": "Bibliothek verwalten",
"logout": "Ausloggen",
"name": "Name",
"patcher": "Patcher",
"password-length": "Passwort muss zwischen 6 und 255 Zeichen lang sein",
"platform": "Plattform",
"platforms": "Plattformen",

View File

@@ -0,0 +1,34 @@
{
"title": "ROM Patcher",
"subtitle": "Wählen Sie eine Basis-ROM und eine Patch-Datei aus und wenden Sie sie an, um die gepatchte ROM herunterzuladen.",
"rom-file": "ROM-Datei",
"patch-file": "Patch-Datei",
"drop-rom-here": "ROM hier ablegen",
"drop-patch-here": "Patch hier ablegen",
"drag-drop-rom": "Ziehen Sie eine ROM-Datei hierher oder klicken Sie zum Durchsuchen.",
"drag-drop-patch": "Ziehen Sie eine Patch-Datei hierher oder klicken Sie zum Durchsuchen.",
"choose-rom": "ROM wählen",
"choose-patch": "Patch wählen",
"replace": "Ersetzen",
"supported-formats": "Unterstützte Patch-Formate",
"download-locally": "Gepatchte ROM herunterladen",
"upload-to-romm": "Zu RomM hochladen",
"output-filename": "Ausgabedateiname (optional)",
"apply-download-upload": "Anwenden, Herunterladen & Hochladen",
"apply-upload": "Anwenden & Hochladen",
"apply-download": "Anwenden & Herunterladen",
"powered-by": "Unterstützt von patcherjs",
"error-no-rom": "Bitte wählen Sie eine ROM-Datei aus.",
"error-no-patch": "Bitte wählen Sie eine Patch-Datei aus.",
"error-no-platform": "Bitte wählen Sie eine Plattform zum Hochladen aus.",
"error-no-action": "Bitte wählen Sie mindestens eine Aktion: Herunterladen oder Hochladen.",
"status-preparing": "Dateien werden vorbereitet...",
"status-downloading": "Gepatchte ROM wird heruntergeladen...",
"status-uploading": "Wird zu RomM hochgeladen...",
"success-uploaded": "hochgeladen",
"success-downloaded": "heruntergeladen",
"success-message": "Gepatchte ROM {actions} erfolgreich!",
"error-upload-failed": "ROM kann nicht hochgeladen werden: {error}",
"upload-success": "Gepatchte ROM erfolgreich hochgeladen{errors}. Scan wird gestartet...",
"upload-errors": " (mit einigen Fehlern)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "Speicherstand auswählen",
"change-state": "Speicherstand ändern",
"deselect-state": "Speicherstand abwählen",
"no-save-selected": "Kein Speicherstand ausgewählt",
"no-state-selected": "Kein Zustand ausgewählt",
"no-saves-available": "Keine Speicherstände verfügbar",
"no-states-available": "Keine Zustände verfügbar",
"background-color": "Hintergrundfarbe",
"select-background-color": "Hintergrundfarbe auswählen"
}

View File

@@ -2,6 +2,7 @@
"about": "About",
"add": "Add",
"administration": "Administration",
"and": "and",
"apply": "Apply",
"ascii-only": "ASCII characters only",
"cancel": "Cancel",
@@ -26,6 +27,7 @@
"library-management": "Library management",
"logout": "Logout",
"name": "Name",
"patcher": "Patcher",
"password-length": "Password must be between 6 and 255 characters",
"platform": "Platform",
"platforms": "Platforms",

View File

@@ -0,0 +1,34 @@
{
"title": "ROM Patcher",
"subtitle": "Choose a base ROM and a patch file, then apply to download the patched ROM.",
"rom-file": "ROM file",
"patch-file": "Patch file",
"drop-rom-here": "Drop ROM here",
"drop-patch-here": "Drop patch here",
"drag-drop-rom": "Drag & drop a ROM file or click to browse.",
"drag-drop-patch": "Drag & drop a patch file or click to browse.",
"choose-rom": "Choose ROM",
"choose-patch": "Choose patch",
"replace": "Replace",
"supported-formats": "Supported patch formats",
"download-locally": "Download patched ROM",
"upload-to-romm": "Upload to RomM",
"output-filename": "Output filename (optional)",
"apply-download-upload": "Apply, Download & Upload",
"apply-upload": "Apply & Upload",
"apply-download": "Apply & Download",
"powered-by": "Powered by patcherjs",
"error-no-rom": "Please select a ROM file.",
"error-no-patch": "Please select a patch file.",
"error-no-platform": "Please select a platform to upload to.",
"error-no-action": "Please select at least one action: download or upload.",
"status-preparing": "Preparing files...",
"status-downloading": "Downloading patched ROM...",
"status-uploading": "Uploading to RomM...",
"success-uploaded": "uploaded",
"success-downloaded": "downloaded",
"success-message": "Patched ROM {actions} successfully!",
"error-upload-failed": "Unable to upload ROM: {error}",
"upload-success": "Patched ROM uploaded successfully{errors}. Starting scan...",
"upload-errors": " (with some errors)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "Select state",
"change-state": "Change state",
"deselect-state": "Deselect state",
"no-save-selected": "No save selected",
"no-state-selected": "No state selected",
"no-saves-available": "No saves available",
"no-states-available": "No states available",
"background-color": "Background color",
"select-background-color": "Select background color"
}

View File

@@ -2,6 +2,7 @@
"about": "About",
"add": "Add",
"administration": "Administration",
"and": "and",
"apply": "Apply",
"ascii-only": "ASCII characters only",
"cancel": "Cancel",
@@ -26,6 +27,7 @@
"library-management": "Library management",
"logout": "Logout",
"name": "Name",
"patcher": "Patcher",
"password-length": "Password must be between 6 and 255 characters",
"platform": "Platform",
"platforms": "Platforms",

View File

@@ -0,0 +1,34 @@
{
"title": "ROM Patcher",
"subtitle": "Choose a base ROM and a patch file, then apply to download the patched ROM.",
"rom-file": "ROM file",
"patch-file": "Patch file",
"drop-rom-here": "Drop ROM here",
"drop-patch-here": "Drop patch here",
"drag-drop-rom": "Drag & drop a ROM file or click to browse.",
"drag-drop-patch": "Drag & drop a patch file or click to browse.",
"choose-rom": "Choose ROM",
"choose-patch": "Choose patch",
"replace": "Replace",
"supported-formats": "Supported patch formats",
"download-locally": "Download patched ROM",
"upload-to-romm": "Upload to RomM",
"output-filename": "Output filename (optional)",
"apply-download-upload": "Apply, Download & Upload",
"apply-upload": "Apply & Upload",
"apply-download": "Apply & Download",
"powered-by": "Powered by patcherjs",
"error-no-rom": "Please select a ROM file.",
"error-no-patch": "Please select a patch file.",
"error-no-platform": "Please select a platform to upload to.",
"error-no-action": "Please select at least one action: download or upload.",
"status-preparing": "Preparing files...",
"status-downloading": "Downloading patched ROM...",
"status-uploading": "Uploading to RomM...",
"success-uploaded": "uploaded",
"success-downloaded": "downloaded",
"success-message": "Patched ROM {actions} successfully!",
"error-upload-failed": "Unable to upload ROM: {error}",
"upload-success": "Patched ROM uploaded successfully{errors}. Starting scan...",
"upload-errors": " (with some errors)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "Select state",
"change-state": "Change state",
"deselect-state": "Deselect state",
"no-save-selected": "No save selected",
"no-state-selected": "No state selected",
"no-saves-available": "No saves available",
"no-states-available": "No states available",
"background-color": "Background color",
"select-background-color": "Select background color"
}

View File

@@ -2,6 +2,7 @@
"about": "Acerca de",
"add": "Añadir",
"administration": "Administración",
"and": "y",
"apply": "Aplicar",
"ascii-only": "Solo caracteres ASCII",
"cancel": "Cancelar",
@@ -26,6 +27,7 @@
"library-management": "Gestionar biblioteca",
"logout": "Cerrar sesión",
"name": "Nombre",
"patcher": "Parchador",
"password-length": "La contraseña debe tener entre 6 y 255 caracteres",
"platform": "Plataforma",
"platforms": "Plataformas",

View File

@@ -0,0 +1,34 @@
{
"title": "Parchador de ROM",
"subtitle": "Elige una ROM base y un archivo de parche, luego aplica para descargar la ROM parcheada.",
"rom-file": "Archivo ROM",
"patch-file": "Archivo de parche",
"drop-rom-here": "Suelta la ROM aquí",
"drop-patch-here": "Suelta el parche aquí",
"drag-drop-rom": "Arrastra y suelta un archivo ROM o haz clic para explorar.",
"drag-drop-patch": "Arrastra y suelta un archivo de parche o haz clic para explorar.",
"choose-rom": "Elegir ROM",
"choose-patch": "Elegir parche",
"replace": "Reemplazar",
"supported-formats": "Formatos de parche compatibles",
"download-locally": "Descargar ROM parcheada",
"upload-to-romm": "Subir a RomM",
"output-filename": "Nombre del archivo de salida (opcional)",
"apply-download-upload": "Aplicar, Descargar y Subir",
"apply-upload": "Aplicar y Subir",
"apply-download": "Aplicar y Descargar",
"powered-by": "Con tecnología de patcherjs",
"error-no-rom": "Por favor, selecciona un archivo ROM.",
"error-no-patch": "Por favor, selecciona un archivo de parche.",
"error-no-platform": "Por favor, selecciona una plataforma para subir.",
"error-no-action": "Por favor, selecciona al menos una acción: descargar o subir.",
"status-preparing": "Preparando archivos...",
"status-downloading": "Descargando ROM parcheada...",
"status-uploading": "Subiendo a RomM...",
"success-uploaded": "subida",
"success-downloaded": "descargada",
"success-message": "ROM parcheada {actions} con éxito!",
"error-upload-failed": "No se pudo subir la ROM: {error}",
"upload-success": "ROM parcheada subida con éxito{errors}. Iniciando escaneo...",
"upload-errors": " (con algunos errores)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "Seleccionar estado",
"change-state": "Cambiar estado",
"deselect-state": "Deseleccionar estado",
"no-save-selected": "Ningún guardado seleccionado",
"no-state-selected": "Ningún estado seleccionado",
"no-saves-available": "No hay guardados disponibles",
"no-states-available": "No hay estados disponibles",
"background-color": "Color de fondo",
"select-background-color": "Seleccionar color de fondo"
}

View File

@@ -2,6 +2,7 @@
"about": "À propos",
"add": "Ajouter",
"administration": "Administration",
"and": "et",
"apply": "Appliquer",
"ascii-only": "Uniquement des caractères ASCII",
"cancel": "Annuler",
@@ -26,6 +27,7 @@
"library-management": "Gestion de la bibliothèque",
"logout": "Se déconnecter",
"name": "Nom",
"patcher": "Patcheur",
"password-length": "Le mot de passe doit contenir entre 6 et 255 caractères",
"platform": "Plateforme",
"platforms": "Plateformes",

View File

@@ -0,0 +1,34 @@
{
"title": "Patcheur de ROM",
"subtitle": "Choisissez une ROM de base et un fichier de patch, puis appliquez pour télécharger la ROM patchée.",
"rom-file": "Fichier ROM",
"patch-file": "Fichier de patch",
"drop-rom-here": "Déposez la ROM ici",
"drop-patch-here": "Déposez le patch ici",
"drag-drop-rom": "Glissez-déposez un fichier ROM ou cliquez pour parcourir.",
"drag-drop-patch": "Glissez-déposez un fichier de patch ou cliquez pour parcourir.",
"choose-rom": "Choisir une ROM",
"choose-patch": "Choisir un patch",
"replace": "Remplacer",
"supported-formats": "Formats de patch pris en charge",
"download-locally": "Télécharger la ROM patchée",
"upload-to-romm": "Téléverser vers RomM",
"output-filename": "Nom du fichier de sortie (optionnel)",
"apply-download-upload": "Appliquer, Télécharger et Téléverser",
"apply-upload": "Appliquer et Téléverser",
"apply-download": "Appliquer et Télécharger",
"powered-by": "Propulsé par patcherjs",
"error-no-rom": "Veuillez sélectionner un fichier ROM.",
"error-no-patch": "Veuillez sélectionner un fichier de patch.",
"error-no-platform": "Veuillez sélectionner une plateforme pour le téléversement.",
"error-no-action": "Veuillez sélectionner au moins une action : télécharger ou téléverser.",
"status-preparing": "Préparation des fichiers...",
"status-downloading": "Téléchargement de la ROM patchée...",
"status-uploading": "Téléversement vers RomM...",
"success-uploaded": "téléversée",
"success-downloaded": "téléchargée",
"success-message": "ROM patchée {actions} avec succès !",
"error-upload-failed": "Impossible de téléverser la ROM : {error}",
"upload-success": "ROM patchée téléversée avec succès{errors}. Démarrage de l'analyse...",
"upload-errors": " (avec quelques erreurs)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "Sélectionner l'état",
"change-state": "Changer l'état",
"deselect-state": "Désélectionner l'état",
"no-save-selected": "Aucune sauvegarde sélectionnée",
"no-state-selected": "Aucun état sélectionné",
"no-saves-available": "Aucune sauvegarde disponible",
"no-states-available": "Aucun état disponible",
"background-color": "Couleur d'arrière-plan",
"select-background-color": "Sélectionner la couleur d'arrière-plan"
}

View File

@@ -2,6 +2,7 @@
"about": "Rólunk",
"add": "Hozzáadás",
"administration": "Adminisztráció",
"and": "és",
"apply": "Alkalmaz",
"ascii-only": "Csak ASCII karakterek",
"cancel": "Mégse",
@@ -26,6 +27,7 @@
"library-management": "Könyvtár Menedzsment",
"logout": "Kijelentkezés",
"name": "Név",
"patcher": "Patcher",
"password-length": "A jelszó hossza 6 és 255 karakter hosszú lehet",
"platform": "Platform",
"platforms": "Platformok",

View File

@@ -0,0 +1,34 @@
{
"title": "ROM Patcher",
"subtitle": "Válasszon egy alap ROM-ot és egy patch fájlt, majd alkalmazza a patchelt ROM letöltéséhez.",
"rom-file": "ROM fájl",
"patch-file": "Patch fájl",
"drop-rom-here": "ROM ide húzása",
"drop-patch-here": "Patch ide húzása",
"drag-drop-rom": "Húzza ide a ROM fájlt vagy kattintson a tallózáshoz.",
"drag-drop-patch": "Húzza ide a patch fájlt vagy kattintson a tallózáshoz.",
"choose-rom": "ROM kiválasztása",
"choose-patch": "Patch kiválasztása",
"replace": "Csere",
"supported-formats": "Támogatott patch formátumok",
"download-locally": "Patchelt ROM letöltése",
"upload-to-romm": "Feltöltés RomM-be",
"output-filename": "Kimeneti fájlnév (opcionális)",
"apply-download-upload": "Alkalmazás, Letöltés és Feltöltés",
"apply-upload": "Alkalmazás és Feltöltés",
"apply-download": "Alkalmazás és Letöltés",
"powered-by": "Működteti: patcherjs",
"error-no-rom": "Kérjük, válasszon egy ROM fájlt.",
"error-no-patch": "Kérjük, válasszon egy patch fájlt.",
"error-no-platform": "Kérjük, válasszon egy platformot a feltöltéshez.",
"error-no-action": "Kérjük, válasszon legalább egy műveletet: letöltés vagy feltöltés.",
"status-preparing": "Fájlok előkészítése...",
"status-downloading": "Patchelt ROM letöltése...",
"status-uploading": "Feltöltés RomM-be...",
"success-uploaded": "feltöltve",
"success-downloaded": "letöltve",
"success-message": "Patchelt ROM {actions} sikeresen!",
"error-upload-failed": "Nem sikerült feltölteni a ROM-ot: {error}",
"upload-success": "Patchelt ROM sikeresen feltöltve{errors}. Szkennelés indítása...",
"upload-errors": " (néhány hibával)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "Állás kiválasztása",
"change-state": "Állás cseréje",
"deselect-state": "Állás kiválasztásának törlése",
"no-save-selected": "Nincs kiválasztott mentés",
"no-state-selected": "Nincs kiválasztott állás",
"no-saves-available": "Nincs elérhető mentés",
"no-states-available": "Nincs elérhető állás",
"background-color": "Háttérszín",
"select-background-color": "Háttérszín kiválasztása"
}

View File

@@ -2,6 +2,7 @@
"about": "Informazioni",
"add": "Aggiungi",
"administration": "Amministrazione",
"and": "e",
"apply": "Applica",
"ascii-only": "Solo caratteri ASCII",
"cancel": "Annulla",
@@ -26,6 +27,7 @@
"library-management": "Gestione Libreria",
"logout": "Logout",
"name": "Nome",
"patcher": "Patcher",
"password-length": "La password deve essere compresa tra 6 e 255 caratteri",
"platform": "Piattaforma",
"platforms": "Piattaforme",

View File

@@ -0,0 +1,34 @@
{
"title": "Patcher ROM",
"subtitle": "Scegli una ROM di base e un file di patch, quindi applica per scaricare la ROM patchata.",
"rom-file": "File ROM",
"patch-file": "File patch",
"drop-rom-here": "Rilascia la ROM qui",
"drop-patch-here": "Rilascia la patch qui",
"drag-drop-rom": "Trascina e rilascia un file ROM o fai clic per sfogliare.",
"drag-drop-patch": "Trascina e rilascia un file patch o fai clic per sfogliare.",
"choose-rom": "Scegli ROM",
"choose-patch": "Scegli patch",
"replace": "Sostituisci",
"supported-formats": "Formati patch supportati",
"download-locally": "Scarica ROM patchata",
"upload-to-romm": "Carica su RomM",
"output-filename": "Nome file di output (opzionale)",
"apply-download-upload": "Applica, Scarica e Carica",
"apply-upload": "Applica e Carica",
"apply-download": "Applica e Scarica",
"powered-by": "Basato su patcherjs",
"error-no-rom": "Seleziona un file ROM.",
"error-no-patch": "Seleziona un file patch.",
"error-no-platform": "Seleziona una piattaforma per il caricamento.",
"error-no-action": "Seleziona almeno un'azione: scarica o carica.",
"status-preparing": "Preparazione file...",
"status-downloading": "Download ROM patchata...",
"status-uploading": "Caricamento su RomM...",
"success-uploaded": "caricata",
"success-downloaded": "scaricata",
"success-message": "ROM patchata {actions} con successo!",
"error-upload-failed": "Impossibile caricare la ROM: {error}",
"upload-success": "ROM patchata caricata con successo{errors}. Avvio scansione...",
"upload-errors": " (con alcuni errori)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "Seleziona Stato",
"change-state": "Cambia Stato",
"deselect-state": "Deseleziona Stato",
"no-save-selected": "Nessun salvataggio selezionato",
"no-state-selected": "Nessuno stato selezionato",
"no-saves-available": "Nessun salvataggio disponibile",
"no-states-available": "Nessuno stato disponibile",
"background-color": "Colore di sfondo",
"select-background-color": "Seleziona colore di sfondo"
}

View File

@@ -2,6 +2,7 @@
"about": "概要",
"add": "追加",
"administration": "管理者メニュー",
"and": "と",
"apply": "適用",
"ascii-only": "ASCII文字のみ",
"cancel": "キャンセル",
@@ -26,6 +27,7 @@
"library-management": "ライブラリ管理",
"logout": "ログアウト",
"name": "名前",
"patcher": "パッチャー",
"password-length": "パスワードは6文字から255文字の間である必要があります",
"platform": "プラットフォーム",
"platforms": "プラットフォーム",

View File

@@ -0,0 +1,34 @@
{
"title": "ROMパッチャー",
"subtitle": "ベースROMとパッチファイルを選択し、適用してパッチされたROMをダウンロードします。",
"rom-file": "ROMファイル",
"patch-file": "パッチファイル",
"drop-rom-here": "ここにROMをドロップ",
"drop-patch-here": "ここにパッチをドロップ",
"drag-drop-rom": "ROMファイルをドラッグドロップするか、クリックして参照してください。",
"drag-drop-patch": "パッチファイルをドラッグ&ドロップするか、クリックして参照してください。",
"choose-rom": "ROMを選択",
"choose-patch": "パッチを選択",
"replace": "置換",
"supported-formats": "サポートされているパッチ形式",
"download-locally": "パッチ済みROMをダウンロード",
"upload-to-romm": "RomMにアップロード",
"output-filename": "出力ファイル名(オプション)",
"apply-download-upload": "適用、ダウンロード&アップロード",
"apply-upload": "適用&アップロード",
"apply-download": "適用&ダウンロード",
"powered-by": "patcherjsで動作",
"error-no-rom": "ROMファイルを選択してください。",
"error-no-patch": "パッチファイルを選択してください。",
"error-no-platform": "アップロード先のプラットフォームを選択してください。",
"error-no-action": "少なくとも1つのアクションダウンロードまたはアップロードを選択してください。",
"status-preparing": "ファイルを準備中...",
"status-downloading": "パッチ済みROMをダウンロード中...",
"status-uploading": "RomMにアップロード中...",
"success-uploaded": "アップロードされました",
"success-downloaded": "ダウンロードされました",
"success-message": "パッチ済みROMが{actions}成功しました!",
"error-upload-failed": "ROMをアップロードできません{error}",
"upload-success": "パッチ済みROMが正常にアップロードされました{errors}。スキャンを開始しています...",
"upload-errors": "(一部エラーあり)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "ステートを選択",
"change-state": "ステートを変更",
"deselect-state": "ステートを解除",
"no-save-selected": "セーブデータが選択されていません",
"no-state-selected": "ステートが選択されていません",
"no-saves-available": "利用可能なセーブデータがありません",
"no-states-available": "利用可能なステートがありません",
"background-color": "背景色",
"select-background-color": "背景色を選択"
}

View File

@@ -2,6 +2,7 @@
"about": "정보",
"add": "추가",
"administration": "유저 관리",
"and": "및",
"apply": "적용",
"ascii-only": "ASCII 문자만",
"cancel": "취소",
@@ -26,6 +27,7 @@
"library-management": "라이브러리 관리",
"logout": "로그아웃",
"name": "이름",
"patcher": "패처",
"password-length": "비밀번호는 6자에서 255자 사이여야 합니다",
"platform": "플랫폼",
"platforms": "플랫폼",

View File

@@ -0,0 +1,34 @@
{
"title": "ROM 패처",
"subtitle": "기본 ROM과 패치 파일을 선택한 다음 적용하여 패치된 ROM을 다운로드하세요.",
"rom-file": "ROM 파일",
"patch-file": "패치 파일",
"drop-rom-here": "여기에 ROM을 드롭하세요",
"drop-patch-here": "여기에 패치를 드롭하세요",
"drag-drop-rom": "ROM 파일을 드래그 앤 드롭하거나 클릭하여 찾아보세요.",
"drag-drop-patch": "패치 파일을 드래그 앤 드롭하거나 클릭하여 찾아보세요.",
"choose-rom": "ROM 선택",
"choose-patch": "패치 선택",
"replace": "교체",
"supported-formats": "지원되는 패치 형식",
"download-locally": "패치된 ROM 다운로드",
"upload-to-romm": "RomM에 업로드",
"output-filename": "출력 파일명 (선택사항)",
"apply-download-upload": "적용, 다운로드 및 업로드",
"apply-upload": "적용 및 업로드",
"apply-download": "적용 및 다운로드",
"powered-by": "patcherjs 기반",
"error-no-rom": "ROM 파일을 선택하세요.",
"error-no-patch": "패치 파일을 선택하세요.",
"error-no-platform": "업로드할 플랫폼을 선택하세요.",
"error-no-action": "최소 하나의 작업을 선택하세요: 다운로드 또는 업로드.",
"status-preparing": "파일 준비 중...",
"status-downloading": "패치된 ROM 다운로드 중...",
"status-uploading": "RomM에 업로드 중...",
"success-uploaded": "업로드됨",
"success-downloaded": "다운로드됨",
"success-message": "패치된 ROM이 성공적으로 {actions}되었습니다!",
"error-upload-failed": "ROM을 업로드할 수 없습니다: {error}",
"upload-success": "패치된 ROM이 성공적으로 업로드되었습니다{errors}. 스캔을 시작합니다...",
"upload-errors": " (일부 오류 발생)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "상태 선택",
"change-state": "상태 변경",
"deselect-state": "상태 선택 해제",
"no-save-selected": "선택된 세이브 없음",
"no-state-selected": "선택된 상태 없음",
"no-saves-available": "사용 가능한 세이브 없음",
"no-states-available": "사용 가능한 상태 없음",
"background-color": "배경색",
"select-background-color": "배경색 선택"
}

View File

@@ -2,6 +2,7 @@
"about": "O aplikacji",
"add": "Dodaj",
"administration": "Administracja",
"and": "i",
"apply": "Zastosuj",
"ascii-only": "Tylko znaki ASCII",
"cancel": "Anuluj",
@@ -26,6 +27,7 @@
"library-management": "Zarządzanie biblioteką",
"logout": "Wyloguj się",
"name": "Nazwa",
"patcher": "Patcher",
"password-length": "Hasło musi mieć od 6 do 255 znaków",
"platform": "Platforma",
"platforms": "Platformy",

View File

@@ -0,0 +1,34 @@
{
"title": "Patcher ROM",
"subtitle": "Wybierz podstawowy ROM i plik łatki, a następnie zastosuj, aby pobrać załatany ROM.",
"rom-file": "Plik ROM",
"patch-file": "Plik łatki",
"drop-rom-here": "Upuść ROM tutaj",
"drop-patch-here": "Upuść łatkę tutaj",
"drag-drop-rom": "Przeciągnij i upuść plik ROM lub kliknij, aby przeglądać.",
"drag-drop-patch": "Przeciągnij i upuść plik łatki lub kliknij, aby przeglądać.",
"choose-rom": "Wybierz ROM",
"choose-patch": "Wybierz łatkę",
"replace": "Zamień",
"supported-formats": "Obsługiwane formaty łatek",
"download-locally": "Pobierz załatany ROM",
"upload-to-romm": "Prześlij do RomM",
"output-filename": "Nazwa pliku wyjściowego (opcjonalnie)",
"apply-download-upload": "Zastosuj, Pobierz i Prześlij",
"apply-upload": "Zastosuj i Prześlij",
"apply-download": "Zastosuj i Pobierz",
"powered-by": "Napędzane przez patcherjs",
"error-no-rom": "Wybierz plik ROM.",
"error-no-patch": "Wybierz plik łatki.",
"error-no-platform": "Wybierz platformę do przesłania.",
"error-no-action": "Wybierz co najmniej jedną akcję: pobierz lub prześlij.",
"status-preparing": "Przygotowywanie plików...",
"status-downloading": "Pobieranie załatanego ROM...",
"status-uploading": "Przesyłanie do RomM...",
"success-uploaded": "przesłano",
"success-downloaded": "pobrano",
"success-message": "Załatany ROM {actions} pomyślnie!",
"error-upload-failed": "Nie można przesłać ROM: {error}",
"upload-success": "Załatany ROM przesłano pomyślnie{errors}. Rozpoczynanie skanowania...",
"upload-errors": " (z niektórymi błędami)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "Wybierz stan",
"change-state": "Zmień stan",
"deselect-state": "Odznacz stan",
"no-save-selected": "Nie wybrano zapisu",
"no-state-selected": "Nie wybrano stanu",
"no-saves-available": "Brak dostępnych zapisów",
"no-states-available": "Brak dostępnych stanów",
"background-color": "Kolor tła",
"select-background-color": "Wybierz kolor tła"
}

View File

@@ -2,6 +2,7 @@
"about": "Sobre",
"add": "Adicionar",
"administration": "Administração",
"and": "e",
"apply": "Aplicar",
"ascii-only": "Somente caracteres ASCII",
"cancel": "Cancelar",
@@ -26,6 +27,7 @@
"library-management": "Gerenciamento de biblioteca",
"logout": "Sair",
"name": "Nome",
"patcher": "Patchador",
"password-length": "Senha deve ter entre 6 e 255 caracteres",
"platform": "Plataforma",
"platforms": "Plataformas",

View File

@@ -0,0 +1,34 @@
{
"title": "Patchador de ROM",
"subtitle": "Escolha uma ROM base e um arquivo de patch, depois aplique para baixar a ROM com patch.",
"rom-file": "Arquivo ROM",
"patch-file": "Arquivo de patch",
"drop-rom-here": "Solte a ROM aqui",
"drop-patch-here": "Solte o patch aqui",
"drag-drop-rom": "Arraste e solte um arquivo ROM ou clique para navegar.",
"drag-drop-patch": "Arraste e solte um arquivo de patch ou clique para navegar.",
"choose-rom": "Escolher ROM",
"choose-patch": "Escolher patch",
"replace": "Substituir",
"supported-formats": "Formatos de patch suportados",
"download-locally": "Baixar ROM com patch",
"upload-to-romm": "Enviar para RomM",
"output-filename": "Nome do arquivo de saída (opcional)",
"apply-download-upload": "Aplicar, Baixar e Enviar",
"apply-upload": "Aplicar e Enviar",
"apply-download": "Aplicar e Baixar",
"powered-by": "Desenvolvido com patcherjs",
"error-no-rom": "Por favor, selecione um arquivo ROM.",
"error-no-patch": "Por favor, selecione um arquivo de patch.",
"error-no-platform": "Por favor, selecione uma plataforma para enviar.",
"error-no-action": "Por favor, selecione pelo menos uma ação: baixar ou enviar.",
"status-preparing": "Preparando arquivos...",
"status-downloading": "Baixando ROM com patch...",
"status-uploading": "Enviando para RomM...",
"success-uploaded": "enviada",
"success-downloaded": "baixada",
"success-message": "ROM com patch {actions} com sucesso!",
"error-upload-failed": "Não foi possível enviar a ROM: {error}",
"upload-success": "ROM com patch enviada com sucesso{errors}. Iniciando varredura...",
"upload-errors": " (com alguns erros)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "Selecionar estado",
"change-state": "Alterar estado",
"deselect-state": "Desmarcar estado",
"no-save-selected": "Nenhum save selecionado",
"no-state-selected": "Nenhum estado selecionado",
"no-saves-available": "Nenhum save disponível",
"no-states-available": "Nenhum estado disponível",
"background-color": "Cor de fundo",
"select-background-color": "Selecionar cor de fundo"
}

View File

@@ -2,6 +2,7 @@
"about": "Despre",
"add": "Adaugă",
"administration": "Administrare",
"and": "și",
"apply": "Aplică",
"ascii-only": "Caractere ASCII numai",
"cancel": "Anulează",
@@ -26,6 +27,7 @@
"library-management": "Gestionare bibliotecă",
"logout": "Deconectare",
"name": "Nume",
"patcher": "Patcher",
"password-length": "Parola trebuie să conțină între 6 și 255 de caractere",
"platform": "Platformă",
"platforms": "Platforme",

View File

@@ -0,0 +1,34 @@
{
"title": "Patcher ROM",
"subtitle": "Alegeți un ROM de bază și un fișier patch, apoi aplicați pentru a descărca ROM-ul patch-uit.",
"rom-file": "Fișier ROM",
"patch-file": "Fișier patch",
"drop-rom-here": "Trageți ROM-ul aici",
"drop-patch-here": "Trageți patch-ul aici",
"drag-drop-rom": "Trageți și plasați un fișier ROM sau faceți clic pentru a naviga.",
"drag-drop-patch": "Trageți și plasați un fișier patch sau faceți clic pentru a naviga.",
"choose-rom": "Alegeți ROM",
"choose-patch": "Alegeți patch",
"replace": "Înlocuiți",
"supported-formats": "Formate patch acceptate",
"download-locally": "Descărcați ROM-ul patch-uit",
"upload-to-romm": "Încărcați în RomM",
"output-filename": "Nume fișier de ieșire (opțional)",
"apply-download-upload": "Aplicați, Descărcați și Încărcați",
"apply-upload": "Aplicați și Încărcați",
"apply-download": "Aplicați și Descărcați",
"powered-by": "Propulsat de patcherjs",
"error-no-rom": "Vă rugăm să selectați un fișier ROM.",
"error-no-patch": "Vă rugăm să selectați un fișier patch.",
"error-no-platform": "Vă rugăm să selectați o platformă pentru încărcare.",
"error-no-action": "Vă rugăm să selectați cel puțin o acțiune: descărcare sau încărcare.",
"status-preparing": "Pregătirea fișierelor...",
"status-downloading": "Descărcarea ROM-ului patch-uit...",
"status-uploading": "Încărcarea în RomM...",
"success-uploaded": "încărcat",
"success-downloaded": "descărcat",
"success-message": "ROM-ul patch-uit {actions} cu succes!",
"error-upload-failed": "Nu s-a putut încărca ROM-ul: {error}",
"upload-success": "ROM-ul patch-uit a fost încărcat cu succes{errors}. Se inițiază scanarea...",
"upload-errors": " (cu unele erori)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "Selectează stare",
"change-state": "Schimbă stare",
"deselect-state": "Deselectează stare",
"no-save-selected": "Nicio salvare selectată",
"no-state-selected": "Nicio stare selectată",
"no-saves-available": "Nicio salvare disponibilă",
"no-states-available": "Nicio stare disponibilă",
"background-color": "Culoare de fundal",
"select-background-color": "Selectează culoarea de fundal"
}

View File

@@ -2,6 +2,7 @@
"about": "О приложении",
"add": "Добавить",
"administration": "Администрирование",
"and": "и",
"apply": "Применить",
"ascii-only": "Только ASCII символы",
"cancel": "Отменить",
@@ -26,6 +27,7 @@
"library-management": "Управление библиотекой",
"logout": "Выйти",
"name": "Имя",
"patcher": "Патчер",
"password-length": "Пароль должен содержать от 6 до 255 символов",
"platform": "Платформа",
"platforms": "Платформы",

View File

@@ -0,0 +1,34 @@
{
"title": "Пропатчивание ROM",
"subtitle": "Выберите базовый ROM и файл патча, затем примените для загрузки пропатченного ROM.",
"rom-file": "Файл ROM",
"patch-file": "Файл патча",
"drop-rom-here": "Перетащите ROM сюда",
"drop-patch-here": "Перетащите патч сюда",
"drag-drop-rom": "Перетащите файл ROM или нажмите для выбора.",
"drag-drop-patch": "Перетащите файл патча или нажмите для выбора.",
"choose-rom": "Выбрать ROM",
"choose-patch": "Выбрать патч",
"replace": "Заменить",
"supported-formats": "Поддерживаемые форматы патчей",
"download-locally": "Загрузить пропатченный ROM",
"upload-to-romm": "Загрузить в RomM",
"output-filename": "Имя выходного файла (необязательно)",
"apply-download-upload": "Применить, Загрузить и Отправить",
"apply-upload": "Применить и Отправить",
"apply-download": "Применить и Загрузить",
"powered-by": "Работает на patcherjs",
"error-no-rom": "Пожалуйста, выберите файл ROM.",
"error-no-patch": "Пожалуйста, выберите файл патча.",
"error-no-platform": "Пожалуйста, выберите платформу для загрузки.",
"error-no-action": "Пожалуйста, выберите хотя бы одно действие: загрузка или отправка.",
"status-preparing": "Подготовка файлов...",
"status-downloading": "Загрузка пропатченного ROM...",
"status-uploading": "Загрузка в RomM...",
"success-uploaded": "загружено",
"success-downloaded": "скачано",
"success-message": "Пропатченный ROM {actions} успешно!",
"error-upload-failed": "Невозможно загрузить ROM: {error}",
"upload-success": "Пропатченный ROM успешно загружен{errors}. Запуск сканирования...",
"upload-errors": " (с некоторыми ошибками)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "Выбрать состояние",
"change-state": "Изменить состояние",
"deselect-state": "Снять выбор состояния",
"no-save-selected": "Сохранение не выбрано",
"no-state-selected": "Состояние не выбрано",
"no-saves-available": "Нет доступных сохранений",
"no-states-available": "Нет доступных состояний",
"background-color": "Цвет фона",
"select-background-color": "Выбрать цвет фона"
}

View File

@@ -2,6 +2,7 @@
"about": "关于",
"add": "添加",
"administration": "管理",
"and": "和",
"apply": "应用",
"ascii-only": "仅限ASCII字符",
"cancel": "取消",
@@ -26,6 +27,7 @@
"library-management": "游戏库管理",
"logout": "注销",
"name": "名称",
"patcher": "补丁工具",
"password-length": "密码必须介于6到255个字符之间",
"platform": "平台",
"platforms": "平台",

View File

@@ -0,0 +1,34 @@
{
"title": "ROM补丁工具",
"subtitle": "选择基础ROM和补丁文件然后应用以下载打过补丁的ROM。",
"rom-file": "ROM文件",
"patch-file": "补丁文件",
"drop-rom-here": "将ROM拖放到此处",
"drop-patch-here": "将补丁拖放到此处",
"drag-drop-rom": "拖放ROM文件或点击浏览。",
"drag-drop-patch": "拖放补丁文件或点击浏览。",
"choose-rom": "选择ROM",
"choose-patch": "选择补丁",
"replace": "替换",
"supported-formats": "支持的补丁格式",
"download-locally": "下载打过补丁的ROM",
"upload-to-romm": "上传到RomM",
"output-filename": "输出文件名(可选)",
"apply-download-upload": "应用、下载和上传",
"apply-upload": "应用和上传",
"apply-download": "应用和下载",
"powered-by": "由patcherjs提供支持",
"error-no-rom": "请选择一个ROM文件。",
"error-no-patch": "请选择一个补丁文件。",
"error-no-platform": "请选择一个平台进行上传。",
"error-no-action": "请至少选择一个操作:下载或上传。",
"status-preparing": "正在准备文件...",
"status-downloading": "正在下载打过补丁的ROM...",
"status-uploading": "正在上传到RomM...",
"success-uploaded": "已上传",
"success-downloaded": "已下载",
"success-message": "打过补丁的ROM {actions} 成功!",
"error-upload-failed": "无法上传ROM{error}",
"upload-success": "打过补丁的ROM上传成功{errors}。正在开始扫描...",
"upload-errors": "(有一些错误)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "选择状态",
"change-state": "更改状态",
"deselect-state": "取消选择状态",
"no-save-selected": "未选择存档",
"no-state-selected": "未选择状态",
"no-saves-available": "无可用存档",
"no-states-available": "无可用状态",
"background-color": "背景颜色",
"select-background-color": "选择背景颜色"
}

View File

@@ -2,6 +2,7 @@
"about": "關於",
"add": "新增",
"administration": "管理系統",
"and": "和",
"apply": "套用",
"ascii-only": "僅限 ASCII 字元",
"cancel": "取消",
@@ -26,6 +27,7 @@
"library-management": "管理遊戲庫",
"logout": "登出",
"name": "名稱",
"patcher": "修補工具",
"password-length": "密碼必須介於 6 到 255 個字元之間",
"platform": "平台",
"platforms": "平台",

View File

@@ -0,0 +1,34 @@
{
"title": "ROM修補程式",
"subtitle": "選擇基礎ROM和修補檔案然後應用以下載修補後的ROM。",
"rom-file": "ROM檔案",
"patch-file": "修補檔案",
"drop-rom-here": "將ROM拖放到此處",
"drop-patch-here": "將修補檔拖放到此處",
"drag-drop-rom": "拖放ROM檔案或點擊瀏覽。",
"drag-drop-patch": "拖放修補檔案或點擊瀏覽。",
"choose-rom": "選擇ROM",
"choose-patch": "選擇修補檔",
"replace": "取代",
"supported-formats": "支援的修補格式",
"download-locally": "下載修補後的ROM",
"upload-to-romm": "上傳到RomM",
"output-filename": "輸出檔案名稱(選填)",
"apply-download-upload": "應用、下載及上傳",
"apply-upload": "應用及上傳",
"apply-download": "應用及下載",
"powered-by": "由patcherjs提供支援",
"error-no-rom": "請選擇一個ROM檔案。",
"error-no-patch": "請選擇一個修補檔案。",
"error-no-platform": "請選擇一個平台進行上傳。",
"error-no-action": "請至少選擇一個操作:下載或上傳。",
"status-preparing": "正在準備檔案...",
"status-downloading": "正在下載修補後的ROM...",
"status-uploading": "正在上傳到RomM...",
"success-uploaded": "已上傳",
"success-downloaded": "已下載",
"success-message": "修補後的ROM {actions} 成功!",
"error-upload-failed": "無法上傳ROM{error}",
"upload-success": "修補後的ROM上傳成功{errors}。正在開始掃描...",
"upload-errors": "(有一些錯誤)"
}

View File

@@ -15,6 +15,10 @@
"select-state": "選擇即時存檔",
"change-state": "更改即時存檔",
"deselect-state": "取消選擇即時存檔",
"no-save-selected": "未選擇存檔",
"no-state-selected": "未選擇即時存檔",
"no-saves-available": "無可用存檔",
"no-states-available": "無可用即時存檔",
"background-color": "背景顏色",
"select-background-color": "選擇背景顏色"
}

View File

@@ -28,6 +28,7 @@ export const ROUTES = {
EMULATORJS: "emulatorjs",
RUFFLE: "ruffle",
SCAN: "scan",
PATCHER: "patcher",
USER_PROFILE: "user-profile",
USER_INTERFACE: "user-interface",
LIBRARY_MANAGEMENT: "library-management",
@@ -188,6 +189,14 @@ const routes = [
},
component: () => import("@/views/Scan.vue"),
},
{
path: "patcher",
name: ROUTES.PATCHER,
meta: {
title: i18n.global.t("common.patcher"),
},
component: () => import("@/views/Patcher.vue"),
},
{
path: "user/:user",
name: ROUTES.USER_PROFILE,

View File

@@ -45,6 +45,10 @@ export default defineStore("navigation", {
this.reset();
this.$router.push({ name: ROUTES.SCAN });
},
goPatcher() {
this.reset();
this.$router.push({ name: ROUTES.PATCHER });
},
goSearch() {
this.reset();
this.$router.push({ name: ROUTES.SEARCH });

6
frontend/src/types/rompatcher.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
declare module "rom-patcher/rom-patcher-js/*" {
const value: any;
export default value;
}

View File

@@ -0,0 +1,894 @@
<script setup lang="ts">
import { useDropZone } from "@vueuse/core";
import type { Emitter } from "mitt";
import { storeToRefs } from "pinia";
import { inject, ref, onMounted, watch, computed } from "vue";
import { useI18n } from "vue-i18n";
import MissingFromFSIcon from "@/components/common/MissingFromFSIcon.vue";
import PlatformIcon from "@/components/common/Platform/PlatformIcon.vue";
import romApi from "@/services/api/rom";
import socket from "@/services/socket";
import storeHeartbeat from "@/stores/heartbeat";
import { type Platform } from "@/stores/platforms";
import storePlatforms from "@/stores/platforms";
import storeScanning from "@/stores/scanning";
import storeUpload from "@/stores/upload";
import type { Events } from "@/types/emitter";
import { formatBytes } from "@/utils";
// Declare global variables for RomPatcher
declare global {
interface Window {
BinFile: any;
IPS: any;
UPS: any;
APS: any;
APSGBA: any;
BPS: any;
RUP: any;
PPF: any;
BDF: any;
PMSR: any;
VCDIFF: any;
}
}
const { t } = useI18n();
const platformsStore = storePlatforms();
const { filteredPlatforms } = storeToRefs(platformsStore);
const loadError = ref<string | null>(null);
const coreLoaded = ref(false);
const romFile = ref<File | null>(null);
const patchFile = ref<File | null>(null);
const romBin = ref<any | null>(null);
const patchBin = ref<any | null>(null);
const romDropZoneRef = ref<HTMLDivElement | null>(null);
const patchDropZoneRef = ref<HTMLDivElement | null>(null);
const romInputRef = ref<HTMLInputElement | null>(null);
const patchInputRef = ref<HTMLInputElement | null>(null);
const applying = ref(false);
const statusMessage = ref<string | null>(null);
const downloadLocally = ref(true);
const saveIntoRomM = ref(true);
const selectedPlatform = ref<Platform | null>(null);
const customFileName = ref("");
const filenamePlaceholder = ref("");
const emitter = inject<Emitter<Events>>("emitter");
const heartbeat = storeHeartbeat();
const scanningStore = storeScanning();
const uploadStore = storeUpload();
const supportedPatchFormats = [
".ips",
".ups",
".bps",
".ppf",
".rup",
".aps",
".bdf",
".pmsr",
".vcdiff",
];
const { isOverDropZone: isOverRomDropZone } = useDropZone(romDropZoneRef, {
onDrop: onRomDrop,
multiple: false,
preventDefaultForUnhandled: true,
});
const { isOverDropZone: isOverPatchDropZone } = useDropZone(patchDropZoneRef, {
onDrop: onPatchDrop,
multiple: false,
preventDefaultForUnhandled: true,
});
// Computed property for ROM extension
const romExtension = computed(() => {
if (!romFile.value) return "";
const match = romFile.value.name.match(/\.[^.]+$/);
return match ? match[0] : "";
});
// Update filename placeholder when files change
watch([romFile, patchFile], ([rom, patch]) => {
if (rom && patch) {
const romBaseName = rom.name.replace(/\.[^.]+$/, "");
const patchNameWithoutExt = patch.name.replace(/\.[^.]+$/, "");
filenamePlaceholder.value = `${romBaseName} (patched-${patchNameWithoutExt})`;
} else {
filenamePlaceholder.value = "";
}
});
async function ensureCoreLoaded() {
if (coreLoaded.value) return;
try {
window.BinFile =
window.IPS =
window.UPS =
window.APS =
window.APSGBA =
window.BPS =
window.RUP =
window.PPF =
window.BDF =
window.PMSR =
window.VCDIFF =
null;
await Promise.all([
import("rom-patcher/rom-patcher-js/modules/BinFile.js"),
import("rom-patcher/rom-patcher-js/modules/HashCalculator.js"),
import("rom-patcher/rom-patcher-js/modules/RomPatcher.format.aps_gba.js"),
import("rom-patcher/rom-patcher-js/modules/RomPatcher.format.aps_n64.js"),
import("rom-patcher/rom-patcher-js/modules/RomPatcher.format.bdf.js"),
import("rom-patcher/rom-patcher-js/modules/RomPatcher.format.bps.js"),
import("rom-patcher/rom-patcher-js/modules/RomPatcher.format.ips.js"),
import("rom-patcher/rom-patcher-js/modules/RomPatcher.format.pmsr.js"),
import("rom-patcher/rom-patcher-js/modules/RomPatcher.format.ppf.js"),
import("rom-patcher/rom-patcher-js/modules/RomPatcher.format.rup.js"),
import("rom-patcher/rom-patcher-js/modules/RomPatcher.format.ups.js"),
import("rom-patcher/rom-patcher-js/modules/RomPatcher.format.vcdiff.js"),
import("rom-patcher/rom-patcher-js/RomPatcher.js"),
]);
coreLoaded.value = true;
} catch (e: any) {
loadError.value = e?.message || String(e);
}
}
function setRomFile(file: File | null) {
romFile.value = file;
romBin.value = null;
}
function setPatchFile(file: File | null) {
patchFile.value = file;
patchBin.value = null;
}
function onRomInput(files: File[] | File | null) {
const first = Array.isArray(files) ? (files[0] ?? null) : files;
setRomFile(first ?? null);
}
function onPatchInput(files: File[] | File | null) {
const first = Array.isArray(files) ? (files[0] ?? null) : files;
setPatchFile(first ?? null);
}
function onRomChange(e: Event) {
const input = e.target as HTMLInputElement;
onRomInput(input.files ? Array.from(input.files) : null);
if (input) input.value = "";
}
function onPatchChange(e: Event) {
const input = e.target as HTMLInputElement;
onPatchInput(input.files ? Array.from(input.files) : null);
if (input) input.value = "";
}
function onRomDrop(files: File[] | null) {
onRomInput(files);
}
function onPatchDrop(files: File[] | null) {
onPatchInput(files);
}
function triggerRomInput() {
romInputRef.value?.click();
}
function triggerPatchInput() {
patchInputRef.value?.click();
}
async function patchRom() {
loadError.value = null;
statusMessage.value = null;
if (!coreLoaded.value) await ensureCoreLoaded();
if (!coreLoaded.value) return; // bail on error
if (!romFile.value) {
loadError.value = t("patcher.error-no-rom");
return;
}
if (!patchFile.value) {
loadError.value = t("patcher.error-no-patch");
return;
}
if (saveIntoRomM.value && !selectedPlatform.value) {
loadError.value = t("patcher.error-no-platform");
return;
}
if (!downloadLocally.value && !saveIntoRomM.value) {
loadError.value = t("patcher.error-no-action");
return;
}
applying.value = true;
try {
// Read files as ArrayBuffers
statusMessage.value = t("patcher.status-preparing");
const romArrayBuffer = await romFile.value.arrayBuffer();
const patchArrayBuffer = await patchFile.value.arrayBuffer();
// Create and use web worker for patching
const worker = new Worker("/assets/patcherjs/patcher.worker.js");
const patchedResult = await new Promise<{
data: Uint8Array;
fileName: string;
}>((resolve, reject) => {
worker.onmessage = (e) => {
const { type, message, patchedData, fileName, error } = e.data;
if (type === "STATUS") {
statusMessage.value = message;
} else if (type === "SUCCESS") {
worker.terminate();
resolve({
data: new Uint8Array(patchedData),
fileName: fileName,
});
} else if (type === "ERROR") {
worker.terminate();
reject(new Error(error));
}
};
worker.onerror = (error) => {
worker.terminate();
reject(new Error(`Worker error: ${error.message}`));
};
// Send data to worker
worker.postMessage(
{
type: "PATCH",
romData: romArrayBuffer,
patchData: patchArrayBuffer,
romFileName: romFile.value?.name,
patchFileName: patchFile.value?.name,
customFileName: customFileName.value || "",
},
[romArrayBuffer, patchArrayBuffer],
); // Transfer ownership
});
// Handle the patched result
let actions = [];
if (downloadLocally.value) {
statusMessage.value = t("patcher.status-downloading");
// Create blob and trigger download
const copy = new Uint8Array(patchedResult.data);
const blob = new Blob([copy], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = patchedResult.fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
actions.push(t("patcher.success-downloaded"));
}
if (saveIntoRomM.value && selectedPlatform.value) {
statusMessage.value = t("patcher.status-uploading");
await uploadPatchedRom(patchedResult.data, patchedResult.fileName);
actions.push(t("patcher.success-uploaded"));
}
if (actions.length > 0) {
statusMessage.value = t("patcher.success-message", {
actions: actions.join(` ${t("common.and")} `),
});
setTimeout(() => {
statusMessage.value = null;
}, 3000);
} else {
statusMessage.value = null;
}
} catch (err: any) {
loadError.value = err?.message || String(err);
statusMessage.value = null;
} finally {
applying.value = false;
}
}
async function uploadPatchedRom(binaryData: Uint8Array, fileName: string) {
if (!selectedPlatform.value) {
throw new Error("No platform selected.");
}
const platformId = selectedPlatform.value.id;
// Convert the binary data to a File object
const copy = new Uint8Array(binaryData);
const file = new File([copy], fileName, { type: "application/octet-stream" });
// Upload the patched ROM
await romApi
.uploadRoms({
filesToUpload: [file],
platformId: platformId,
})
.then((responses: PromiseSettledResult<unknown>[]) => {
const successfulUploads = responses.filter(
(d) => d.status === "fulfilled",
);
const failedUploads = responses.filter((d) => d.status === "rejected");
if (successfulUploads.length === 0) {
// Get detailed error message from the first failed upload
const firstFailure = failedUploads[0] as PromiseRejectedResult;
const errorDetail =
firstFailure?.reason?.response?.data?.detail ||
firstFailure?.reason?.message ||
"Upload failed with unknown error";
console.error("Upload failed:", firstFailure);
throw new Error(errorDetail);
}
if (failedUploads.length === 0) {
uploadStore.reset();
}
emitter?.emit("snackbarShow", {
msg: t("patcher.upload-success", {
errors: failedUploads.length > 0 ? t("patcher.upload-errors") : "",
}),
icon: "mdi-check-bold",
color: "green",
timeout: 3000,
});
// Clear form after successful upload
romFile.value = null;
patchFile.value = null;
if (failedUploads.length === 0) {
uploadStore.reset();
// Clear form only on complete success
romFile.value = null;
patchFile.value = null;
romBin.value = null;
patchBin.value = null;
selectedPlatform.value = null;
saveIntoRomM.value = false;
scanningStore.setScanning(true);
if (!socket.connected) socket.connect();
setTimeout(() => {
socket.emit("scan", {
platforms: [platformId],
type: "quick",
apis: heartbeat.getEnabledMetadataOptions().map((s) => s.value),
});
}, 2000);
}
patchBin.value = null;
selectedPlatform.value = null;
saveIntoRomM.value = false;
scanningStore.setScanning(true);
if (!socket.connected) socket.connect();
setTimeout(() => {
socket.emit("scan", {
platforms: [platformId],
type: "quick",
apis: heartbeat.getEnabledMetadataOptions().map((s) => s.value),
});
}, 2000);
})
.catch(({ response, message }) => {
throw new Error(
t("patcher.error-upload-failed", {
error: response?.data?.detail || response?.statusText || message,
}),
);
});
}
onMounted(async () => {
// Preload core for faster interaction
await ensureCoreLoaded();
});
</script>
<template>
<v-row class="align-center justify-center scroll h-100 px-4" no-gutters>
<v-col cols="12" sm="10" md="8" xl="6">
<v-card class="pa-4 bg-background" elevation="0">
<v-card-title class="pb-2 px-0">{{ t("patcher.title") }}</v-card-title>
<v-card-subtitle class="pb-2 px-0 text-body-2">
{{ t("patcher.subtitle") }}
</v-card-subtitle>
<v-divider class="mt-2 mb-4" />
<v-card-text class="pa-0">
<v-alert
v-if="loadError"
type="error"
class="mb-4"
density="compact"
>{{ loadError }}</v-alert
>
<v-alert
v-if="statusMessage"
class="mb-4 bg-primary"
density="compact"
>
<div class="d-flex align-center">
<v-progress-circular
indeterminate
size="20"
width="2"
class="mr-3"
/>
{{ statusMessage }}
</div>
</v-alert>
<v-row class="mb-2" dense>
<v-col cols="12" md="6">
<v-sheet class="pa-3" rounded="lg" border color="surface">
<div class="text-subtitle-1">{{ t("patcher.rom-file") }}</div>
<div
ref="romDropZoneRef"
class="dropzone-container rounded-lg transition-all duration-300 ease-in-out mt-4"
:class="{
'dropzone-active': isOverRomDropZone,
'dropzone-has-files': !!romFile,
}"
role="button"
tabindex="0"
@click="triggerRomInput"
@keydown.enter.prevent="triggerRomInput"
@keydown.space.prevent="triggerRomInput"
>
<div
v-if="!romFile"
class="flex flex-col items-center justify-center h-full min-h-[180px] p-6 text-center transition-all duration-300 ease-in-out"
>
<v-icon
:class="{ 'animate-pulse-glow': isOverRomDropZone }"
size="40"
color="primary"
>
{{ isOverRomDropZone ? "mdi-file" : "mdi-file-outline" }}
</v-icon>
<div class="text-subtitle-2 mt-3 mb-1">
{{ t("patcher.drop-rom-here") }}
</div>
<p class="text-body-2 text-medium-emphasis mb-3">
{{ t("patcher.drag-drop-rom") }}
</p>
<v-btn color="primary" variant="outlined" size="small">
{{ t("patcher.choose-rom") }}
</v-btn>
</div>
<div
v-else
class="d-flex align-center justify-space-between h-full min-h-[120px] px-4"
>
<div>
<div class="text-subtitle-2">{{ romFile.name }}</div>
<div class="text-caption text-medium-emphasis mt-2">
<v-chip size="small" label>
{{ formatBytes(romFile.size) }}
</v-chip>
</div>
</div>
<div class="d-flex align-center">
<v-btn
color="primary"
variant="outlined"
size="small"
class="mr-2"
@click.stop="triggerRomInput"
>
{{ t("patcher.replace") }}
</v-btn>
<v-btn
icon
variant="plain"
@click.stop="onRomInput(null)"
>
<v-icon color="red"> mdi-close </v-icon>
</v-btn>
</div>
</div>
</div>
<input
ref="romInputRef"
type="file"
class="sr-only"
style="display: none"
@change="onRomChange"
/>
</v-sheet>
</v-col>
<v-col cols="12" md="6">
<v-sheet class="pa-3" rounded="lg" border color="surface">
<div class="text-subtitle-1">{{ t("patcher.patch-file") }}</div>
<div
ref="patchDropZoneRef"
class="dropzone-container rounded-lg transition-all duration-300 ease-in-out mt-4"
:class="{
'dropzone-active': isOverPatchDropZone,
'dropzone-has-files': !!patchFile,
}"
role="button"
tabindex="0"
@click="triggerPatchInput"
@keydown.enter.prevent="triggerPatchInput"
@keydown.space.prevent="triggerPatchInput"
>
<div
v-if="!patchFile"
class="flex flex-col items-center justify-center h-full min-h-[180px] p-6 text-center transition-all duration-300 ease-in-out"
>
<v-icon
:class="{ 'animate-pulse-glow': isOverPatchDropZone }"
size="40"
color="primary"
>
{{
isOverPatchDropZone
? "mdi-file-cog"
: "mdi-file-cog-outline"
}}
</v-icon>
<div class="text-subtitle-2 mt-3 mb-1">
{{ t("patcher.drop-patch-here") }}
</div>
<p class="text-body-2 text-medium-emphasis mb-3">
{{ t("patcher.drag-drop-patch") }}
</p>
<v-btn color="primary" variant="outlined" size="small">
{{ t("patcher.choose-patch") }}
</v-btn>
</div>
<div
v-else
class="d-flex align-center justify-space-between h-full min-h-[120px] px-4"
>
<div>
<div class="text-subtitle-2">{{ patchFile.name }}</div>
<div class="text-caption text-medium-emphasis mt-2">
<v-chip size="small" label>
{{ formatBytes(patchFile.size) }}
</v-chip>
</div>
</div>
<div class="d-flex align-center">
<v-btn
color="primary"
variant="outlined"
size="small"
class="mr-2"
@click.stop="triggerPatchInput"
>
{{ t("patcher.replace") }}
</v-btn>
<v-btn
icon
variant="plain"
@click.stop="onPatchInput(null)"
>
<v-icon color="red"> mdi-close </v-icon>
</v-btn>
</div>
</div>
</div>
<input
ref="patchInputRef"
type="file"
:accept="supportedPatchFormats.join(',')"
class="sr-only"
style="display: none"
@change="onPatchChange"
/>
<div class="text-subtitle-2 text-medium-emphasis mt-4">
{{ t("patcher.supported-formats") }}<br />
<v-chip
v-for="format in supportedPatchFormats"
size="x-small"
class="mr-1 mt-1"
label
>{{ format }}</v-chip
>
</div>
</v-sheet>
<div class="d-flex align-center justify-space-between mt-4">
<v-switch
v-model="downloadLocally"
color="primary"
inset
hide-details
:label="t('patcher.download-locally')"
/>
<v-switch
v-model="saveIntoRomM"
color="primary"
inset
hide-details
:label="t('patcher.upload-to-romm')"
/>
</div>
<v-expand-transition>
<div v-if="saveIntoRomM" class="mt-4">
<v-select
v-model="selectedPlatform"
:items="filteredPlatforms"
:menu-props="{ maxHeight: 650 }"
:label="t('common.platforms')"
:disabled="!saveIntoRomM"
item-title="name"
return-object
prepend-inner-icon="mdi-controller"
variant="outlined"
density="comfortable"
hide-details
clearable
>
<template #item="{ props, item }">
<v-list-item
v-bind="props"
class="py-4"
:title="item.raw.name ?? ''"
:subtitle="item.raw.fs_slug"
>
<template #prepend>
<PlatformIcon
:key="item.raw.slug"
:size="35"
:slug="item.raw.slug"
:name="item.raw.name"
:fs-slug="item.raw.fs_slug"
/>
</template>
<template #append>
<MissingFromFSIcon
v-if="item.raw.missing_from_fs"
text="Missing platform from filesystem"
chip
chip-label
chip-density="compact"
class="ml-2"
/>
<v-row
v-if="item.raw.is_identified"
class="text-white text-shadow text-center"
no-gutters
>
<v-col cols="12">
<v-avatar
v-if="item.raw.igdb_id"
variant="text"
size="25"
rounded
class="mr-1"
>
<v-img src="/assets/scrappers/igdb.png" />
</v-avatar>
<v-avatar
v-if="item.raw.ss_id"
variant="text"
size="25"
rounded
class="mr-1"
>
<v-img src="/assets/scrappers/ss.png" />
</v-avatar>
<v-avatar
v-if="item.raw.moby_slug"
variant="text"
size="25"
rounded
class="mr-1"
>
<v-img src="/assets/scrappers/moby.png" />
</v-avatar>
<v-avatar
v-if="item.raw.ra_id"
variant="text"
size="25"
rounded
class="mr-1"
>
<v-img src="/assets/scrappers/ra.png" />
</v-avatar>
<v-avatar
v-if="item.raw.launchbox_id"
variant="text"
size="25"
rounded
class="mr-1"
style="background: #185a7c"
>
<v-img src="/assets/scrappers/launchbox.png" />
</v-avatar>
<v-avatar
v-if="item.raw.hasheous_id"
variant="text"
size="25"
rounded
class="mr-1"
>
<v-img src="/assets/scrappers/hasheous.png" />
</v-avatar>
<v-avatar
v-if="item.raw.flashpoint_id"
variant="text"
size="25"
rounded
class="mr-1"
>
<v-img src="/assets/scrappers/flashpoint.png" />
</v-avatar>
<v-avatar
v-if="item.raw.hltb_slug"
class="bg-surface"
variant="text"
size="25"
rounded
>
<v-img src="/assets/scrappers/hltb.png" />
</v-avatar>
</v-col>
</v-row>
<v-row
v-else
class="text-white text-shadow text-center"
no-gutters
>
<v-chip color="red" size="small" label>
<v-icon class="mr-1"> mdi-close </v-icon>
{{ t("scan.not-identified").toUpperCase() }}
</v-chip>
</v-row>
<v-chip class="ml-1" size="small" label>
{{ item.raw.rom_count }}
</v-chip>
</template>
</v-list-item>
</template>
<template #chip="{ item }">
<v-chip>
<PlatformIcon
:key="item.raw.slug"
:slug="item.raw.slug"
:name="item.raw.name"
:fs-slug="item.raw.fs_slug"
:size="20"
/>
<div class="ml-1">
{{ item.raw.name }}
</div>
</v-chip>
</template>
</v-select>
</div>
</v-expand-transition>
<v-text-field
v-model="customFileName"
:placeholder="filenamePlaceholder"
:suffix="romExtension"
:label="t('patcher.output-filename')"
variant="outlined"
density="compact"
hide-details
class="mt-4"
clearable
/>
<div class="d-flex align-right justify-space-left mt-4">
<v-spacer />
<v-btn
class="bg-toplayer text-primary"
:disabled="
!romFile ||
!patchFile ||
applying ||
(!downloadLocally && !saveIntoRomM) ||
(saveIntoRomM && !selectedPlatform)
"
:loading="applying"
:variant="
!romFile ||
!patchFile ||
applying ||
(!downloadLocally && !saveIntoRomM) ||
(saveIntoRomM && !selectedPlatform)
? 'plain'
: 'flat'
"
@click="patchRom"
>
{{
downloadLocally && saveIntoRomM
? t("patcher.apply-download-upload")
: saveIntoRomM
? t("patcher.apply-upload")
: t("patcher.apply-download")
}}
</v-btn>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
<v-row class="mb-8 px-4" no-gutters>
<v-col class="text-right align-center">
<span class="text-medium-emphasis text-caption font-italic mr-2">{{
t("patcher.powered-by")
}}</span>
<v-avatar rounded="0">
<v-img src="/assets/patcherjs/patcherjs.png" />
</v-avatar>
</v-col>
</v-row>
</v-col>
</v-row>
</template>
<style scoped>
.dropzone-container {
border: 2px dashed rgba(var(--v-theme-primary), 0.3);
}
.dropzone-container.dropzone-active {
border: 2px dashed rgba(var(--v-theme-primary));
background-color: rgba(var(--v-theme-primary), 0.05);
}
.dropzone-container.dropzone-has-files {
border: none;
background-color: rgba(var(--v-theme-surface), 0.5);
}
.animate-pulse-glow {
animation: pulse-glow 1.5s ease-in-out infinite;
}
@keyframes pulse-glow {
0% {
transform: scale(1);
filter: brightness(1) drop-shadow(0 0 0 rgba(var(--v-theme-primary), 0));
}
50% {
transform: scale(1.1);
filter: brightness(1.2)
drop-shadow(0 0 20px rgba(var(--v-theme-primary), 0.6));
}
100% {
transform: scale(1);
filter: brightness(1) drop-shadow(0 0 0 rgba(var(--v-theme-primary), 0));
}
}
</style>

View File

@@ -304,6 +304,51 @@ function openCacheDialog() {
<v-divider />
<v-card-text class="pa-4" style="min-height: 200px">
<!-- Saves Tab Content -->
<div v-show="isSavesTabSelected">
<!-- Selected Save Preview -->
<div v-if="selectedSave" class="mb-3">
<AssetCard
:asset="selectedSave"
type="save"
:show-hover-actions="false"
:show-close-button="true"
:transform-scale="false"
@close="unselectSave"
/>
</div>
<!-- No Save Selected Message -->
<div v-else class="text-center py-8">
<v-icon size="48" color="medium-emphasis"
>mdi-content-save-outline</v-icon
>
<p class="text-body-2 text-medium-emphasis mt-2">
{{ t("play.no-save-selected") }}
</p>
</div>
<!-- Select Save Button -->
<v-btn
block
variant="tonal"
color="primary"
:prepend-icon="
selectedSave ? 'mdi-swap-horizontal' : 'mdi-plus'
"
:disabled="rom.user_saves.length == 0"
@click="openSaveDialog"
>
{{
rom.user_saves.length == 0
? t("play.no-saves-available")
: selectedSave
? t("play.change-save")
: t("play.select-save")
}}
</v-btn>
</div>
<!-- States Tab Content -->
<div v-show="!isSavesTabSelected">
<!-- Selected State Preview -->
@@ -344,49 +389,13 @@ function openCacheDialog() {
@click="openStateDialog"
>
{{
selectedState
? t("play.change-state")
: t("play.select-state")
}}
</v-btn>
</div>
<!-- Saves Tab Content -->
<div v-show="isSavesTabSelected">
<!-- Selected Save Preview -->
<div v-if="selectedSave" class="mb-3">
<AssetCard
:asset="selectedSave"
type="save"
:show-hover-actions="false"
:show-close-button="true"
:transform-scale="false"
@close="unselectSave"
/>
</div>
<!-- No Save Selected Message -->
<div v-else class="text-center py-8">
<v-icon size="48" color="medium-emphasis"
>mdi-content-save-outline</v-icon
>
<p class="text-body-2 text-medium-emphasis mt-2">
{{ t("play.no-save-selected") }}
</p>
</div>
<!-- Select Save Button -->
<v-btn
block
variant="tonal"
color="primary"
:prepend-icon="
selectedSave ? 'mdi-swap-horizontal' : 'mdi-plus'
"
@click="openSaveDialog"
>
{{
selectedSave ? t("play.change-save") : t("play.select-save")
!rom.user_states.some(
(s) => !s.emulator || s.emulator === selectedCore,
)
? t("play.no-states-available")
: selectedState
? t("play.change-state")
: t("play.select-state")
}}
</v-btn>
</div>