Merge remote-tracking branch 'origin/master' into feature/retroachievements

This commit is contained in:
zurdi
2025-04-24 16:40:22 +00:00
44 changed files with 283 additions and 154 deletions

View File

@@ -212,7 +212,6 @@ Here are a few projects maintained by members of our community. Please note that
- [romm-comm][romm-comm-discord-bot]: Discord Bot by @idio-sync
- [DeckRommSync][deck-romm-sync]: SteamOS downloader and sync by @PeriBluGaming
- CasaOS app via the [BigBear App Store][big-bear-casaos]
- [Helm Chart to deploy on Kubernetes][kubernetes-helm-chart] by @psych0d0g
Join us on Discord, where you can ask questions, submit ideas, get help, showcase your collection, and discuss RomM with other users.
@@ -283,7 +282,6 @@ Here are a few projects that we think you might like:
[screenscraper-api]: https://www.screenscraper.fr/membreinscription.php
[mobygames-api]: https://www.mobygames.com/info/api/
[big-bear-casaos]: https://github.com/bigbeartechworld/big-bear-casaos
[kubernetes-helm-chart]: https://artifacthub.io/packages/helm/crystalnet/romm
[romm-comm-discord-bot]: https://github.com/idio-sync/romm-comm
[deck-romm-sync]: https://github.com/PeriBluGaming/DeckRommSync-Standalone
[playnite-app]: https://github.com/rommapp/playnite-plugin

View File

@@ -0,0 +1,92 @@
"""empty message
Revision ID: 0038_add_ssid_to_sibling_roms
Revises: 0037_virtual_rom_columns
Create Date: 2025-04-23 00:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
from utils.database import is_postgresql
# revision identifiers, used by Alembic.
revision = "0038_add_ssid_to_sibling_roms"
down_revision = "0037_virtual_rom_columns"
branch_labels = None
depends_on = None
def upgrade() -> None:
connection = op.get_bind()
null_safe_equal_operator = (
"IS NOT DISTINCT FROM" if is_postgresql(connection) else "<=>"
)
connection.execute(
sa.text(
f"""
CREATE OR REPLACE VIEW sibling_roms AS
SELECT
r1.id AS rom_id,
r2.id AS sibling_rom_id,
r1.platform_id AS platform_id,
NOW() AS created_at,
NOW() AS updated_at,
CASE WHEN r1.igdb_id {null_safe_equal_operator} r2.igdb_id THEN r1.igdb_id END AS igdb_id,
CASE WHEN r1.moby_id {null_safe_equal_operator} r2.moby_id THEN r1.moby_id END AS moby_id,
CASE WHEN r1.ss_id {null_safe_equal_operator} r2.ss_id THEN r1.ss_id END AS ss_id
FROM
roms r1
JOIN
roms r2
ON
r1.platform_id = r2.platform_id
AND r1.id != r2.id
AND (
(r1.igdb_id = r2.igdb_id AND r1.igdb_id IS NOT NULL)
OR
(r1.moby_id = r2.moby_id AND r1.moby_id IS NOT NULL)
OR
(r1.ss_id = r2.ss_id AND r1.ss_id IS NOT NULL)
);
""" # nosec B608
),
)
def downgrade() -> None:
connection = op.get_bind()
null_safe_equal_operator = (
"IS NOT DISTINCT FROM" if is_postgresql(connection) else "<=>"
)
connection.execute(sa.text("DROP VIEW IF EXISTS sibling_roms;"))
connection.execute(
sa.text(
f"""
CREATE VIEW sibling_roms AS
SELECT
r1.id AS rom_id,
r2.id AS sibling_rom_id,
r1.platform_id AS platform_id,
NOW() AS created_at,
NOW() AS updated_at,
CASE WHEN r1.igdb_id {null_safe_equal_operator} r2.igdb_id THEN r1.igdb_id END AS igdb_id,
CASE WHEN r1.moby_id {null_safe_equal_operator} r2.moby_id THEN r1.moby_id END AS moby_id
FROM
roms r1
JOIN
roms r2
ON
r1.platform_id = r2.platform_id
AND r1.id != r2.id
AND (
(r1.igdb_id = r2.igdb_id AND r1.igdb_id IS NOT NULL)
OR
(r1.moby_id = r2.moby_id AND r1.moby_id IS NOT NULL)
);
""" # nosec B608
),
)

View File

@@ -476,7 +476,6 @@ async def get_rom_content(
async def update_rom(
request: Request,
id: int,
rename_as_source: bool = False,
remove_cover: bool = False,
artwork: UploadFile | None = None,
unmatch_metadata: bool = False,
@@ -486,12 +485,11 @@ async def update_rom(
Args:
request (Request): Fastapi Request object
id (Rom): Rom internal id
rename_as_source (bool, optional): Flag to rename rom file as matched IGDB game. Defaults to False.
artwork (UploadFile, optional): Custom artwork to set as cover. Defaults to File(None).
unmatch_metadata: Remove the metadata matches for this game. Defaults to False.
Raises:
HTTPException: If a rom already have that name when enabling the rename_as_source flag
HTTPException: Rom not found in database
Returns:
DetailedRomSchema: Rom stored in the database
@@ -542,9 +540,13 @@ async def update_rom(
"ss_id": data.get("ss_id", rom.ss_id),
}
moby_id = cleaned_data["moby_id"]
if moby_id and int(moby_id) != rom.moby_id:
moby_rom = await meta_moby_handler.get_rom_by_id(int(moby_id))
if (
cleaned_data.get("moby_id", "")
and int(cleaned_data.get("moby_id", "")) != rom.moby_id
):
moby_rom = await meta_moby_handler.get_rom_by_id(
int(cleaned_data.get("moby_id", ""))
)
cleaned_data.update(moby_rom)
path_screenshots = await fs_resource_handler.get_rom_screenshots(
rom=rom,
@@ -653,33 +655,22 @@ async def update_rom(
# Rename the file/folder if the name has changed
should_update_fs = new_fs_name != rom.fs_name
try:
if rename_as_source:
new_fs_name = rom.fs_name.replace(
rom.fs_name_no_tags or rom.fs_name_no_ext,
rom.name or rom.fs_name,
)
if should_update_fs:
try:
new_fs_name = sanitize_filename(new_fs_name)
fs_rom_handler.rename_fs_rom(
old_name=rom.fs_name,
new_name=new_fs_name,
fs_path=rom.fs_path,
)
elif should_update_fs:
new_fs_name = sanitize_filename(new_fs_name)
fs_rom_handler.rename_fs_rom(
old_name=rom.fs_name,
new_name=new_fs_name,
fs_path=rom.fs_path,
)
except RomAlreadyExistsException as exc:
log.error(exc)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=exc
) from exc
except RomAlreadyExistsException as exc:
log.error(exc)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=exc
) from exc
# Update the rom files with the new fs_name
if rename_as_source or should_update_fs:
if should_update_fs:
for file in rom.files:
db_rom_handler.update_rom_file(
file.id,

View File

@@ -49,7 +49,6 @@ def test_update_rom(rename_fs_rom_mock, get_rom_by_id_mock, client, access_token
response = client.put(
f"/api/roms/{rom.id}",
headers={"Authorization": f"Bearer {access_token}"},
params={"rename_as_source": True},
data={
"igdb_id": "236663",
"name": "Metroid Prime Remastered",

View File

@@ -101,14 +101,13 @@ class FSHandler:
file_name_no_extension = self.get_file_name_with_no_extension(file_name)
return TAG_REGEX.split(file_name_no_extension)[0].strip()
def parse_file_extension(self, file_name) -> str:
def parse_file_extension(self, file_name: str) -> str:
match = EXTENSION_REGEX.search(file_name)
return match.group(1) if match else ""
def _exclude_files(self, files, filetype) -> list[str]:
cnfg = cm.get_config()
excluded_extensions = getattr(cnfg, f"EXCLUDED_{filetype.upper()}_EXT")
excluded_names = getattr(cnfg, f"EXCLUDED_{filetype.upper()}_FILES")
def exclude_single_files(self, files: list[str]) -> list[str]:
excluded_extensions = cm.get_config().EXCLUDED_SINGLE_EXT
excluded_names = cm.get_config().EXCLUDED_SINGLE_FILES
excluded_files: list = []
for file_name in files:
@@ -120,10 +119,9 @@ class FSHandler:
excluded_files.append(file_name)
# Additionally, check if the file name mathes a pattern in the excluded list.
if len(excluded_names) > 0:
for name in excluded_names:
if file_name == name or fnmatch.fnmatch(file_name, name):
excluded_files.append(file_name)
for name in excluded_names:
if file_name == name or fnmatch.fnmatch(file_name, name):
excluded_files.append(file_name)
# Return files that are not in the filtered list.
return [f for f in files if f not in excluded_files]

View File

@@ -43,7 +43,7 @@ class FSFirmwareHandler(FSHandler):
except IndexError as exc:
raise FirmwareNotFoundException(platform_fs_slug) from exc
return [f for f in self._exclude_files(fs_firmware_files, "single")]
return [f for f in self.exclude_single_files(fs_firmware_files)]
def get_firmware_file_size(self, firmware_path: str, file_name: str):
files = [f"{LIBRARY_BASE_PATH}/{firmware_path}/{file_name}"]

View File

@@ -1,5 +1,6 @@
import binascii
import bz2
import fnmatch
import hashlib
import os
import re
@@ -274,11 +275,27 @@ class FSRomsHandler(FSHandler):
abs_fs_path = f"{LIBRARY_BASE_PATH}/{roms_path}" # Absolute path to roms
rom_files: list[RomFile] = []
excluded_file_names = cm.get_config().EXCLUDED_MULTI_PARTS_FILES
excluded_file_exts = cm.get_config().EXCLUDED_MULTI_PARTS_EXT
# Check if rom is a multi-part rom
if os.path.isdir(f"{abs_fs_path}/{rom}"):
for f_path, file in iter_files(f"{abs_fs_path}/{rom}", recursive=True):
for f_path, file_name in iter_files(f"{abs_fs_path}/{rom}", recursive=True):
# Check if file is excluded
ext = self.parse_file_extension(file_name)
if not ext or ext in excluded_file_exts:
continue
if any(
file_name == exc_name or fnmatch.fnmatch(file_name, exc_name)
for exc_name in excluded_file_names
):
continue
rom_files.append(
self._build_rom_file(f_path.relative_to(LIBRARY_BASE_PATH), file)
self._build_rom_file(
f_path.relative_to(LIBRARY_BASE_PATH), file_name
)
)
else:
rom_files.append(self._build_rom_file(Path(roms_path), rom))
@@ -433,7 +450,7 @@ class FSRomsHandler(FSHandler):
fs_roms: list[dict] = [
{"multi": False, "fs_name": rom}
for rom in self._exclude_files(fs_single_roms, "single")
for rom in self.exclude_single_files(fs_single_roms)
] + [
{"multi": True, "fs_name": rom}
for rom in self._exclude_multi_roms(fs_multi_roms)

View File

@@ -39,7 +39,7 @@ def test_get_roms():
assert roms[1]["multi"]
def test_exclude_files():
def testexclude_single_files():
from config.config_manager import ConfigManager
empty_config_file = os.path.join(
@@ -52,50 +52,46 @@ def test_exclude_files():
cm.add_exclusion("EXCLUDED_SINGLE_FILES", "Super Mario 64 (J) (Rev A) [Part 1].z64")
filtered_files = fs_rom_handler._exclude_files(
filtered_files = fs_rom_handler.exclude_single_files(
files=[
"Super Mario 64 (J) (Rev A) [Part 1].z64",
"Super Mario 64 (J) (Rev A) [Part 2].z64",
],
filetype="single",
)
assert len(filtered_files) == 1
cm.add_exclusion("EXCLUDED_SINGLE_EXT", "z64")
filtered_files = fs_rom_handler._exclude_files(
filtered_files = fs_rom_handler.exclude_single_files(
files=[
"Super Mario 64 (J) (Rev A) [Part 1].z64",
"Super Mario 64 (J) (Rev A) [Part 2].z64",
],
filetype="single",
)
assert len(filtered_files) == 0
cm.add_exclusion("EXCLUDED_SINGLE_FILES", "*.z64")
filtered_files = fs_rom_handler._exclude_files(
filtered_files = fs_rom_handler.exclude_single_files(
files=[
"Super Mario 64 (J) (Rev A) [Part 1].z64",
"Super Mario 64 (J) (Rev A) [Part 2].z64",
],
filetype="single",
)
assert len(filtered_files) == 0
cm.add_exclusion("EXCLUDED_SINGLE_FILES", "_.*")
filtered_files = fs_rom_handler._exclude_files(
filtered_files = fs_rom_handler.exclude_single_files(
files=[
"Links Awakening.nsp",
"_.Links Awakening.nsp",
"Kirby's Adventure.nsp",
"_.Kirby's Adventure.nsp",
],
filetype="single",
)
assert len(filtered_files) == 2

View File

@@ -52,7 +52,6 @@ class MobyMetadata(TypedDict):
class MobyGamesRom(TypedDict):
moby_id: int | None
slug: NotRequired[str]
name: NotRequired[str]
summary: NotRequired[str]
url_cover: NotRequired[str]
@@ -280,7 +279,6 @@ class MobyGamesHandler(MetadataHandler):
rom = {
"moby_id": res["game_id"],
"name": res["title"],
"slug": res["moby_url"].split("/")[-1],
"summary": res.get("description", ""),
"url_cover": pydash.get(res, "sample_cover.image", ""),
"url_screenshots": [s["image"] for s in res.get("sample_screenshots", [])],
@@ -303,7 +301,6 @@ class MobyGamesHandler(MetadataHandler):
rom = {
"moby_id": res["game_id"],
"name": res["title"],
"slug": res["moby_url"].split("/")[-1],
"summary": res.get("description", None),
"url_cover": pydash.get(res, "sample_cover.image", None),
"url_screenshots": [s["image"] for s in res.get("sample_screenshots", [])],
@@ -341,7 +338,6 @@ class MobyGamesHandler(MetadataHandler):
for k, v in {
"moby_id": rom["game_id"],
"name": rom["title"],
"slug": rom["moby_url"].split("/")[-1],
"summary": rom.get("description", ""),
"url_cover": pydash.get(rom, "sample_cover.image", ""),
"url_screenshots": [

View File

@@ -129,7 +129,6 @@ class SSMetadata(TypedDict):
class SSRom(TypedDict):
ss_id: int | None
slug: NotRequired[str]
name: NotRequired[str]
summary: NotRequired[str]
url_cover: NotRequired[str]
@@ -422,35 +421,44 @@ class SSHandler(MetadataHandler):
if res:
break
if not res or not res.get("id", None):
if not res:
return fallback_rom
ss_id: int = int(res.get("id", None))
res_ss_id = res.get("id", None)
if not res_ss_id:
return fallback_rom
rom = {
"ss_id": ss_id,
"name": pydash.chain(res.get("noms", []))
ss_id: int = int(res_ss_id)
res_name = (
pydash.chain(res.get("noms", []))
.filter({"region": "ss"})
.map("text")
.head()
.value(),
"slug": pydash.chain(res.get("noms", []))
.filter({"region": "ss"})
.map("text")
.head()
.value(),
"summary": pydash.chain(res.get("synopsis", []))
.value()
)
res_summary = (
pydash.chain(res.get("synopsis", []))
.filter({"langue": "en"})
.map("text")
.head()
.value(),
"url_cover": pydash.chain(res.get("medias", []))
.value()
)
res_url_cover = (
pydash.chain(res.get("medias", []))
.filter({"region": "us", "type": "box-2D", "parent": "jeu"})
.map("url")
.head()
.value()
or "",
"url_manual": pydash.chain(res.get("medias", []))
or pydash.chain(res.get("medias", []))
.filter({"region": "ss", "type": "box-2D", "parent": "jeu"})
.map("url")
.head()
.value()
or ""
)
res_url_manual = (
pydash.chain(res.get("medias", []))
.filter(
{"region": "us", "type": "manuel", "parent": "jeu", "format": "pdf"}
)
@@ -464,7 +472,15 @@ class SSHandler(MetadataHandler):
.map("url")
.head()
.value()
or "",
or ""
)
rom = {
"ss_id": ss_id,
"name": res_name,
"summary": res_summary,
"url_cover": res_url_cover,
"url_manual": res_url_manual,
"url_screenshots": [],
"ss_metadata": extract_metadata_from_ss_rom(res),
}
@@ -481,30 +497,35 @@ class SSHandler(MetadataHandler):
if not res:
return SSRom(ss_id=None)
rom = {
"ss_id": res.get("id"),
"name": pydash.chain(res.get("noms", []))
res_name = (
pydash.chain(res.get("noms", []))
.filter({"region": "ss"})
.map("text")
.head()
.value(),
"slug": pydash.chain(res.get("noms", []))
.filter({"region": "ss"})
.map("text")
.head()
.value(),
"summary": pydash.chain(res.get("synopsis", []))
.value()
)
res_summary = (
pydash.chain(res.get("synopsis", []))
.filter({"langue": "en"})
.map("text")
.head()
.value(),
"url_cover": pydash.chain(res.get("medias", []))
.value()
)
res_url_cover = (
pydash.chain(res.get("medias", []))
.filter({"region": "us", "type": "box-2D", "parent": "jeu"})
.map("url")
.head()
.value()
or "",
"url_manual": pydash.chain(res.get("medias", []))
or pydash.chain(res.get("medias", []))
.filter({"region": "ss", "type": "box-2D", "parent": "jeu"})
.map("url")
.head()
.value()
or ""
)
res_url_manual = (
pydash.chain(res.get("medias", []))
.filter(
{"region": "us", "type": "manuel", "parent": "jeu", "format": "pdf"}
)
@@ -518,7 +539,15 @@ class SSHandler(MetadataHandler):
.map("url")
.head()
.value()
or "",
or ""
)
rom = {
"ss_id": res.get("id"),
"name": res_name,
"summary": res_summary,
"url_cover": res_url_cover,
"url_manual": res_url_manual,
"url_screenshots": [],
"ss_metadata": extract_metadata_from_ss_rom(res),
}
@@ -533,7 +562,7 @@ class SSHandler(MetadataHandler):
return rom if rom.get("ss_id", "") else None
async def get_matched_roms_by_name(
self, search_term: str, platform_ss_id: int
self, search_term: str, platform_ss_id: int | None
) -> list[SSRom]:
if not SS_API_ENABLED:
return []
@@ -559,15 +588,6 @@ class SSHandler(MetadataHandler):
.value()
)
def _get_slug(rom: dict) -> str | None:
return (
pydash.chain(rom.get("noms", []))
.filter({"region": "ss"})
.map("text")
.head()
.value()
)
def _get_summary(rom: dict) -> str | None:
return (
pydash.chain(rom.get("synopsis", []))
@@ -584,6 +604,11 @@ class SSHandler(MetadataHandler):
.map("url")
.head()
.value()
or pydash.chain(rom.get("medias", []))
.filter({"region": "ss", "type": "box-2D", "parent": "jeu"})
.map("url")
.head()
.value()
or ""
)
@@ -632,7 +657,6 @@ class SSHandler(MetadataHandler):
for k, v in {
"ss_id": rom.get("id"),
"name": _get_name(rom),
"slug": _get_slug(rom),
"summary": _get_summary(rom),
"url_cover": _get_url_cover(rom),
"url_manual": _get_url_manual(rom),

View File

@@ -14,7 +14,7 @@
"cronstrue": "^2.57.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"md-editor-v3": "^5.4.5",
"md-editor-v3": "^5.5.0",
"mitt": "^3.0.1",
"nanoid": "^5.1.4",
"pinia": "^3.0.1",
@@ -6316,10 +6316,9 @@
}
},
"node_modules/md-editor-v3": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/md-editor-v3/-/md-editor-v3-5.4.5.tgz",
"integrity": "sha512-Vw0mjlhGArlCt5aG15a2/W3BoQJLCACUGROgR64uVvRzepsh/26Pj1C3oGlU2lis4FrlnX7rzAN+m8g76eVFRA==",
"license": "MIT",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/md-editor-v3/-/md-editor-v3-5.5.0.tgz",
"integrity": "sha512-+YVHwbcUh7nPn5i+tD2QLQBuDlShJAuEzkhkFMJpN+UYMjfQG27rE3+cMfArP1C0xodpf4POn32jr7MlOfFHdA==",
"dependencies": {
"@codemirror/lang-markdown": "^6.3.0",
"@codemirror/language-data": "^6.5.1",

View File

@@ -32,7 +32,7 @@
"cronstrue": "^2.57.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"md-editor-v3": "^5.4.5",
"md-editor-v3": "^5.5.0",
"mitt": "^3.0.1",
"nanoid": "^5.1.4",
"pinia": "^3.0.1",

View File

@@ -170,16 +170,19 @@ watch(
class="my-1 text-grey-lighten-2"
style="padding: 10px 14px"
@click="toggleMainSibling"
><v-icon
>
<v-icon
:class="romUser.is_main_sibling ? '' : 'mr-1'"
:color="romUser.is_main_sibling ? 'primary' : ''"
>{{
>
{{
romUser.is_main_sibling
? "mdi-checkbox-outline"
: "mdi-checkbox-blank-outline"
}}</v-icon
>{{ romUser.is_main_sibling ? "" : t("rom.default") }}</v-btn
>
}}
</v-icon>
{{ romUser.is_main_sibling ? "" : t("rom.default") }}
</v-btn>
</template>
</v-tooltip>
</v-row>

View File

@@ -5,9 +5,11 @@ import storeUsers from "@/stores/users";
import type { Events } from "@/types/emitter";
import type { Emitter } from "mitt";
import { inject, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useDisplay } from "vuetify";
// Props
const { t } = useI18n();
const user = ref({
username: "",
password: "",
@@ -60,7 +62,7 @@ function closeDialog() {
<v-text-field
v-model="user.username"
variant="outlined"
label="username"
:label="t('settings.username')"
required
hide-details
clearable
@@ -72,7 +74,7 @@ function closeDialog() {
<v-text-field
v-model="user.password"
variant="outlined"
label="Password"
:label="t('settings.password')"
required
hide-details
clearable
@@ -84,7 +86,7 @@ function closeDialog() {
<v-text-field
v-model="user.email"
variant="outlined"
label="email"
:label="t('settings.email')"
required
hide-details
clearable
@@ -97,7 +99,7 @@ function closeDialog() {
v-model="user.role"
variant="outlined"
:items="['viewer', 'editor', 'admin']"
label="Role"
:label="t('settings.role')"
required
hide-details
/>
@@ -109,14 +111,16 @@ function closeDialog() {
<template #append>
<v-row class="justify-center mb-2" no-gutters>
<v-btn-group divided density="compact">
<v-btn class="bg-toplayer" @click="closeDialog"> Cancel </v-btn>
<v-btn class="bg-toplayer" @click="closeDialog">
{{ t("common.cancel") }}
</v-btn>
<v-btn
:disabled="!user.username || !user.password"
:variant="!user.username || !user.password ? 'plain' : 'flat'"
class="text-romm-green bg-toplayer"
@click="createUser()"
>
Create
{{ t("common.create") }}
</v-btn>
</v-btn-group>
</v-row>

View File

@@ -120,7 +120,7 @@ function closeDialog() {
<v-text-field
v-model="user.email"
variant="outlined"
label="email"
:label="t('settings.email')"
required
hide-details
clearable

View File

@@ -87,7 +87,6 @@ const noMetadataMatch = computed(() => {
async function handleRomUpdate(
options: {
rom: UpdateRom;
renameAsSource?: boolean;
removeCover?: boolean;
unmatch?: boolean;
},

View File

@@ -37,7 +37,7 @@ const matchedRoms = ref<SearchRomSchema[]>([]);
const filteredMatchedRoms = ref<SearchRomSchema[]>();
const emitter = inject<Emitter<Events>>("emitter");
const showSelectSource = ref(false);
const renameAsSource = ref(false);
const renameFromSource = ref(false);
const selectedMatchRom = ref<SearchRomSchema>();
const selectedCover = ref<MatchedSource>();
const sources = ref<MatchedSource[]>([]);
@@ -182,7 +182,7 @@ function confirm() {
}
function toggleRenameAsSource() {
renameAsSource.value = !renameAsSource.value;
renameFromSource.value = !renameFromSource.value;
}
function backToMatched() {
@@ -190,7 +190,7 @@ function backToMatched() {
selectedCover.value = undefined;
selectedMatchRom.value = undefined;
sources.value = [];
renameAsSource.value = false;
renameFromSource.value = false;
}
async function updateRom(
@@ -205,16 +205,20 @@ async function updateRom(
// Set the properties from the selected rom
rom.value = {
...rom.value,
fs_name:
renameFromSource.value && selectedMatchRom.value
? `${selectedMatchRom.value.name}.${rom.value.fs_extension}`
: rom.value.fs_name,
igdb_id: selectedRom.igdb_id || null,
moby_id: selectedRom.moby_id || null,
ss_id: selectedRom.ss_id || null,
name: selectedRom.name,
slug: selectedRom.slug,
summary: selectedRom.summary,
name: selectedRom.name || null,
slug: selectedRom.slug || null,
summary: selectedRom.summary || null,
url_cover:
urlCover ||
selectedRom.ss_url_cover ||
selectedRom.igdb_url_cover ||
selectedRom.ss_url_cover ||
selectedRom.moby_url_cover ||
null,
};
@@ -225,7 +229,7 @@ async function updateRom(
}
await romApi
.updateRom({ rom: rom.value, renameAsSource: renameAsSource.value })
.updateRom({ rom: rom.value })
.then(({ data }) => {
emitter?.emit("snackbarShow", {
msg: "Rom updated successfully!",
@@ -256,7 +260,7 @@ function closeDialog() {
showSelectSource.value = false;
selectedCover.value = undefined;
selectedMatchRom.value = undefined;
renameAsSource.value = false;
renameFromSource.value = false;
}
onBeforeUnmount(() => {
@@ -499,16 +503,16 @@ onBeforeUnmount(() => {
</v-col>
</v-row>
</v-col>
<v-col cols="12">
<v-col cols="12" v-if="selectedMatchRom">
<v-row class="mt-4 text-center" no-gutters>
<v-col>
<v-chip
@click="toggleRenameAsSource"
:variant="renameAsSource ? 'flat' : 'outlined'"
:color="renameAsSource ? 'primary' : ''"
:variant="renameFromSource ? 'flat' : 'outlined'"
:color="renameFromSource ? 'primary' : ''"
:disabled="selectedCover == undefined"
><v-icon class="mr-1">{{
selectedCover && renameAsSource
selectedCover && renameFromSource
? "mdi-checkbox-outline"
: "mdi-checkbox-blank-outline"
}}</v-icon
@@ -516,17 +520,15 @@ onBeforeUnmount(() => {
t("rom.rename-file-part1", { source: selectedCover?.name })
}}</v-chip
>
<v-list-item v-if="renameAsSource" class="mt-2">
<v-list-item v-if="rom && renameFromSource" class="mt-2">
<span>{{ t("rom.rename-file-part2") }}</span>
<br />
<span>{{ t("rom.rename-file-part3") }}</span
><span class="text-primary ml-1"
>{{ rom?.fs_name_no_tags }}.{{ rom?.fs_extension }}</span
>
><span class="text-primary ml-1">{{ rom.fs_name }}</span>
<br />
<span class="mx-1">{{ t("rom.rename-file-part4") }}</span
><span class="text-secondary"
>{{ selectedMatchRom?.name }}.{{ rom?.fs_extension }}</span
>{{ selectedMatchRom.name }}.{{ rom.fs_extension }}</span
>
<br />
<span class="text-caption font-italic font-weight-bold"

View File

@@ -26,7 +26,7 @@
"hidden": "Versteckt",
"info": "Info",
"languages": "Sprachen",
"manual": "Benutzerhandbuch",
"manual": "Handbuch",
"manual-match": "Manuell zuweisen",
"my-notes": "Meine Notizen",
"no-metadata-source": "Keine Quelle für Metadaten aktiv",

View File

@@ -1,6 +1,7 @@
{
"continue-playing-as-grid": "Weiterspielen als Raster",
"continue-playing-as-grid-desc": "Zeige die Weiterspielen-Karten als Raster auf der Startseite",
"email": "E-Mail",
"excluded": "Ausnahmen",
"excluded-multi-rom-files": "Unterverzeichnisse in Plattform-Verzeichnissen",
"excluded-multi-rom-parts-extensions": "Dateiendungen in Unterverzeichnisse in Plattform-Verzeichnissen",

View File

@@ -26,7 +26,7 @@
"hidden": "Hidden",
"info": "Info",
"languages": "Languages",
"manual": "User manual",
"manual": "Manual",
"manual-match": "Manual match",
"my-notes": "My notes",
"no-metadata-source": "No metadata source enabled",

View File

@@ -1,6 +1,7 @@
{
"continue-playing-as-grid": "Continue playing as grid",
"continue-playing-as-grid-desc": "View continue playing rom cards as a grid at the home page",
"email": "Email",
"excluded": "Excluded",
"excluded-multi-rom-files": "Multi rom files",
"excluded-multi-rom-parts-extensions": "Multi rom parts extensions",

View File

@@ -26,7 +26,7 @@
"hidden": "Hidden",
"info": "Info",
"languages": "Languages",
"manual": "User manual",
"manual": "Manual",
"manual-match": "Manual match",
"my-notes": "My notes",
"no-metadata-source": "No metadata source enabled",

View File

@@ -1,6 +1,7 @@
{
"continue-playing-as-grid": "Continue playing as grid",
"continue-playing-as-grid-desc": "View continue playing rom cards as a grid at the home page",
"email": "Email",
"excluded": "Excluded",
"excluded-multi-rom-files": "Multi rom files",
"excluded-multi-rom-parts-extensions": "Multi rom parts extensions",

View File

@@ -26,7 +26,7 @@
"hidden": "Oculto",
"info": "Info",
"languages": "Idiomas",
"manual": "Manual de usuario",
"manual": "Manual",
"manual-match": "Identificación manual",
"my-notes": "Mis notas",
"no-metadata-source": "No hay fuente de metadatos habilitada",

View File

@@ -1,6 +1,7 @@
{
"continue-playing-as-grid": "Continuar jugando como cuadrícula",
"continue-playing-as-grid-desc": "Ver tarjetas de continuar jugando como cuadrícula en la página principal",
"email": "Correo electrónico",
"excluded": "Exclusiones",
"excluded-multi-rom-files": "Roms multi fichero",
"excluded-multi-rom-parts-extensions": "Extensiones de partes de roms multi fichero",

View File

@@ -26,7 +26,7 @@
"hidden": "Caché",
"info": "Info",
"languages": "Langues",
"manual": "Manuel utilisateur",
"manual": "Manuel",
"manual-match": "Correspondance manuelle",
"my-notes": "Mes notes",
"no-metadata-source": "Aucune source de métadonnées activée",

View File

@@ -1,6 +1,7 @@
{
"continue-playing-as-grid": "Continuer à jouer en grille",
"continue-playing-as-grid-desc": "Voir les cartes de continuer à jouer en grille sur la page d'accueil",
"email": "E-mail",
"excluded": "Exclu",
"excluded-multi-rom-files": "Fichiers de rom multiples",
"excluded-multi-rom-parts-extensions": "Extensions de parties de rom multiples",

View File

@@ -26,7 +26,7 @@
"hidden": "Nascosto",
"info": "Info",
"languages": "Lingue",
"manual": "Manuale utente",
"manual": "Manuale",
"manual-match": "Associazione manuale",
"my-notes": "Mie note",
"no-metadata-source": "Nessuna fonte di metadati abilitata",

View File

@@ -1,6 +1,7 @@
{
"continue-playing-as-grid": "\"Continua a giocare\" come griglia",
"continue-playing-as-grid-desc": "Visualizza le rom di \"Continua a giocare\" come una griglia nella home",
"email": "Email",
"excluded": "Escluso",
"excluded-multi-rom-files": "File rom multiple",
"excluded-multi-rom-parts-extensions": "Estensioni parti di rom multiple",

View File

@@ -26,7 +26,7 @@
"hidden": "非表示",
"info": "詳細",
"languages": "言語",
"manual": "ユーザーマニュアル",
"manual": "マニュアル",
"manual-match": "手動マッチ",
"my-notes": "メモ",
"no-metadata-source": "メタデータ検索が無効です",

View File

@@ -1,6 +1,7 @@
{
"continue-playing-as-grid": "プレイを継続をグリッドで表示",
"continue-playing-as-grid-desc": "ホームのプレイを継続をグリッドで表示",
"email": "メール",
"excluded": "除外",
"excluded-multi-rom-files": "複数ファイル",
"excluded-multi-rom-parts-extensions": "複数ファイルの拡張子",

View File

@@ -26,7 +26,7 @@
"hidden": "숨김",
"info": "정보",
"languages": "언어",
"manual": "사용자 설명서",
"manual": "매뉴얼",
"manual-match": "수동으로 DB 대응",
"my-notes": "개인 노트",
"no-metadata-source": "메타데이터 DB 연동 안됨",

View File

@@ -1,6 +1,7 @@
{
"continue-playing-as-grid": "계속 플레이하기 그리드 뷰",
"continue-playing-as-grid-desc": "홈페이지에 계속 플레이하기를 그리드 뷰로 보여줍니다",
"email": "이메일",
"excluded": "제외됨",
"excluded-multi-rom-files": "멀티 롬 파일",
"excluded-multi-rom-parts-extensions": "멀티 롬 부분 확장자",

View File

@@ -26,7 +26,7 @@
"hidden": "Oculto",
"info": "Informações",
"languages": "Idiomas",
"manual": "Manual do usuário",
"manual": "Manual",
"manual-match": "Correspondência manual",
"my-notes": "Minhas notas",
"no-metadata-source": "Nenhuma fonte de metadados habilitada",

View File

@@ -1,6 +1,7 @@
{
"continue-playing-as-grid": "Continuar jogando como grade",
"continue-playing-as-grid-desc": "Ver cartões de continuar jogando como uma grade na página inicial",
"email": "E-mail",
"excluded": "Excluído",
"excluded-multi-rom-files": "Arquivos de rom múltiplos",
"excluded-multi-rom-parts-extensions": "Extensões de partes de rom múltiplos",

View File

@@ -26,7 +26,7 @@
"hidden": "Ascuns",
"info": "Informații",
"languages": "Limbi",
"manual": "Manual utilizator",
"manual": "Manual",
"manual-match": "Potrivire manuală",
"my-notes": "Notițele mele",
"no-metadata-source": "Nicio sursă de metadate activată",

View File

@@ -1,6 +1,7 @@
{
"continue-playing-as-grid": "Continuă jocul în grilă",
"continue-playing-as-grid-desc": "Vezi cardurile „Continuă jocul” în grilă pe pagina principală",
"email": "Email",
"excluded": "Exclus",
"excluded-multi-rom-files": "Fișiere rom multiple",
"excluded-multi-rom-parts-extensions": "Extensii părți rom multiple",

View File

@@ -26,7 +26,7 @@
"hidden": "Скрыто",
"info": "Информация",
"languages": "Языки",
"manual": "Руководство пользователя",
"manual": "Руководство",
"manual-match": "Ручное совпадение",
"my-notes": "Мои заметки",
"no-metadata-source": "Источник метаданных не включен",

View File

@@ -1,6 +1,7 @@
{
"continue-playing-as-grid": "Продолжение игры в виде сетки",
"continue-playing-as-grid-desc": "Просмотр карточек продолжения игры в виде сетки на главной странице",
"email": "Электронная почта",
"excluded": "Исключено",
"excluded-multi-rom-files": "Многосоставные файлы ромов",
"excluded-multi-rom-parts-extensions": "Расширения частей многосоставных ромов",

View File

@@ -26,7 +26,7 @@
"hidden": "隐藏",
"info": "信息",
"languages": "语言",
"manual": "用户手册",
"manual": "手册",
"manual-match": "手动匹配",
"my-notes": "我的笔记",
"name": "名称",

View File

@@ -1,6 +1,7 @@
{
"continue-playing-as-grid": "继续游玩(网格)",
"continue-playing-as-grid-desc": "以网格形式在主页上显示继续游玩(卡片)部分",
"email": "电子邮件",
"excluded": "排除",
"excluded-multi-rom-files": "多个 Rom 文件",
"excluded-multi-rom-parts-extensions": "多个 Rom 部分扩展",

View File

@@ -192,12 +192,10 @@ export type UpdateRom = SimpleRom & {
async function updateRom({
rom,
renameAsSource = false,
removeCover = false,
unmatch = false,
}: {
rom: UpdateRom;
renameAsSource?: boolean;
removeCover?: boolean;
unmatch?: boolean;
}): Promise<{ data: DetailedRom }> {
@@ -213,7 +211,6 @@ async function updateRom({
return api.put(`/roms/${rom.id}`, formData, {
params: {
rename_as_source: renameAsSource,
remove_cover: removeCover,
unmatch_metadata: unmatch,
},

View File

@@ -9,8 +9,10 @@ import { ROUTES } from "@/plugins/router";
import type { Emitter } from "mitt";
import { computed, inject, ref } from "vue";
import { useDisplay } from "vuetify";
import { useI18n } from "vue-i18n";
// Props
const { t } = useI18n();
const { xs } = useDisplay();
const emitter = inject<Emitter<Events>>("emitter");
const heartbeat = storeHeartbeat();
@@ -121,7 +123,7 @@ async function finishWizard() {
<v-form @submit.prevent>
<v-text-field
v-model="defaultAdminUser.username"
label="Username *"
:label="`${t('settings.username')} *`"
type="text"
required
autocomplete="on"
@@ -130,7 +132,7 @@ async function finishWizard() {
/>
<v-text-field
v-model="defaultAdminUser.email"
label="Email"
:label="`${t('settings.email')} *`"
type="text"
required
autocomplete="on"
@@ -139,7 +141,7 @@ async function finishWizard() {
/>
<v-text-field
v-model="defaultAdminUser.password"
label="Password *"
:label="`${t('settings.password')} *`"
:type="visiblePassword ? 'text' : 'password'"
required
autocomplete="on"

View File

@@ -143,7 +143,7 @@ onUnmounted(() => {
<v-text-field
v-model="userToEdit.email"
variant="outlined"
label="email"
:label="t('settings.email')"
required
hide-details
clearable