diff --git a/README.md b/README.md index 04cd6c29b..8a11b95e8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/backend/alembic/versions/0038_add_ssid_to_sibling_roms.py b/backend/alembic/versions/0038_add_ssid_to_sibling_roms.py new file mode 100644 index 000000000..9fa40e7b1 --- /dev/null +++ b/backend/alembic/versions/0038_add_ssid_to_sibling_roms.py @@ -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 + ), + ) diff --git a/backend/endpoints/rom.py b/backend/endpoints/rom.py index 61ea7f048..b50622a37 100644 --- a/backend/endpoints/rom.py +++ b/backend/endpoints/rom.py @@ -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, diff --git a/backend/endpoints/tests/test_rom.py b/backend/endpoints/tests/test_rom.py index 92c1d2a45..74cac13bd 100644 --- a/backend/endpoints/tests/test_rom.py +++ b/backend/endpoints/tests/test_rom.py @@ -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", diff --git a/backend/handler/filesystem/base_handler.py b/backend/handler/filesystem/base_handler.py index 297b3b017..64de4f8d8 100644 --- a/backend/handler/filesystem/base_handler.py +++ b/backend/handler/filesystem/base_handler.py @@ -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] diff --git a/backend/handler/filesystem/firmware_handler.py b/backend/handler/filesystem/firmware_handler.py index 82b70d798..897366a82 100644 --- a/backend/handler/filesystem/firmware_handler.py +++ b/backend/handler/filesystem/firmware_handler.py @@ -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}"] diff --git a/backend/handler/filesystem/roms_handler.py b/backend/handler/filesystem/roms_handler.py index c4c065d51..d846d26cf 100644 --- a/backend/handler/filesystem/roms_handler.py +++ b/backend/handler/filesystem/roms_handler.py @@ -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) diff --git a/backend/handler/filesystem/tests/test_fs.py b/backend/handler/filesystem/tests/test_fs.py index 1cc9d9192..ba4178c3c 100644 --- a/backend/handler/filesystem/tests/test_fs.py +++ b/backend/handler/filesystem/tests/test_fs.py @@ -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 diff --git a/backend/handler/metadata/moby_handler.py b/backend/handler/metadata/moby_handler.py index 6a8f4c9f7..02fd309b8 100644 --- a/backend/handler/metadata/moby_handler.py +++ b/backend/handler/metadata/moby_handler.py @@ -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": [ diff --git a/backend/handler/metadata/ss_handler.py b/backend/handler/metadata/ss_handler.py index 043ab096a..9b28ae94b 100644 --- a/backend/handler/metadata/ss_handler.py +++ b/backend/handler/metadata/ss_handler.py @@ -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), diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c42e4754b..6bb36c8eb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 29b4ae907..4df6ead69 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/components/Details/Info/FileInfo.vue b/frontend/src/components/Details/Info/FileInfo.vue index f3c19ccd1..476fa731f 100644 --- a/frontend/src/components/Details/Info/FileInfo.vue +++ b/frontend/src/components/Details/Info/FileInfo.vue @@ -170,16 +170,19 @@ watch( class="my-1 text-grey-lighten-2" style="padding: 10px 14px" @click="toggleMainSibling" - > + {{ + > + {{ romUser.is_main_sibling ? "mdi-checkbox-outline" : "mdi-checkbox-blank-outline" - }}{{ romUser.is_main_sibling ? "" : t("rom.default") }} + }} + + {{ romUser.is_main_sibling ? "" : t("rom.default") }} + diff --git a/frontend/src/components/Settings/Administration/Users/Dialog/CreateUser.vue b/frontend/src/components/Settings/Administration/Users/Dialog/CreateUser.vue index 4c89b900d..3e7c4b9b7 100644 --- a/frontend/src/components/Settings/Administration/Users/Dialog/CreateUser.vue +++ b/frontend/src/components/Settings/Administration/Users/Dialog/CreateUser.vue @@ -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() { @@ -109,14 +111,16 @@ function closeDialog() {