From e4cf33a344d5f5a6a0555f96a799b2151330b8c2 Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Mon, 28 Sep 2026 14:46:30 -0400 Subject: [PATCH 1/3] Size gallery bulk actions for whole-result selections Bulk status bounds its per-ROM fan-out to four requests in flight. Bulk download posts the ID list to a new POST /roms/download/selection, which stores it in Redis and returns a token that GET /roms/download reads, so the list no longer rides in the URL and cannot overflow it. The download now reports a failure instead of always showing the optimistic snackbar. The delete dialog renders its rows 50 at a time behind a "Show more" button, reads fs membership from a Set, and writes scan exclusions one at a time since each call rewrites the config file. Fixes #4460 Co-Authored-By: Claude Opus 5.5 (1M context) --- backend/endpoints/responses/__init__.py | 4 ++ backend/endpoints/roms/__init__.py | 59 ++++++++++++++++- backend/tests/endpoints/roms/test_rom.py | 65 +++++++++++++++++++ docs/BACKEND_ARCHITECTURE.md | 2 + frontend/src/__generated__/index.ts | 2 + ...ection_api_roms_download_selection_post.ts | 11 ++++ .../models/BulkDownloadSelectionSchema.ts | 8 +++ frontend/src/locales/bg_BG/gallery.json | 1 + frontend/src/locales/bg_BG/rom.json | 1 + frontend/src/locales/cs_CZ/gallery.json | 1 + frontend/src/locales/cs_CZ/rom.json | 1 + frontend/src/locales/de_DE/gallery.json | 1 + frontend/src/locales/de_DE/rom.json | 1 + frontend/src/locales/en_GB/gallery.json | 1 + frontend/src/locales/en_GB/rom.json | 1 + frontend/src/locales/en_US/gallery.json | 1 + frontend/src/locales/en_US/rom.json | 1 + frontend/src/locales/es_ES/gallery.json | 1 + frontend/src/locales/es_ES/rom.json | 1 + frontend/src/locales/fr_FR/gallery.json | 1 + frontend/src/locales/fr_FR/rom.json | 1 + frontend/src/locales/hu_HU/gallery.json | 1 + frontend/src/locales/hu_HU/rom.json | 1 + frontend/src/locales/it_IT/gallery.json | 1 + frontend/src/locales/it_IT/rom.json | 1 + frontend/src/locales/ja_JP/gallery.json | 1 + frontend/src/locales/ja_JP/rom.json | 1 + frontend/src/locales/ko_KR/gallery.json | 1 + frontend/src/locales/ko_KR/rom.json | 1 + frontend/src/locales/pl_PL/gallery.json | 1 + frontend/src/locales/pl_PL/rom.json | 1 + frontend/src/locales/pt_BR/gallery.json | 1 + frontend/src/locales/pt_BR/rom.json | 1 + frontend/src/locales/ro_RO/gallery.json | 1 + frontend/src/locales/ro_RO/rom.json | 1 + frontend/src/locales/ru_RU/gallery.json | 1 + frontend/src/locales/ru_RU/rom.json | 1 + frontend/src/locales/tr_TR/gallery.json | 1 + frontend/src/locales/tr_TR/rom.json | 1 + frontend/src/locales/zh_CN/gallery.json | 1 + frontend/src/locales/zh_CN/rom.json | 1 + frontend/src/locales/zh_TW/gallery.json | 1 + frontend/src/locales/zh_TW/rom.json | 1 + frontend/src/services/api/rom.test.ts | 48 +++++++++++++- frontend/src/services/api/rom.ts | 12 ++-- .../Dialogs/DeleteRomDialog.test.ts | 50 ++++++++++++++ .../v2/components/Dialogs/DeleteRomDialog.vue | 55 +++++++++++----- .../components/Gallery/SelectionBar.test.ts | 23 +++++++ .../v2/components/Gallery/SelectionBar.vue | 51 ++++++++------- frontend/src/v2/utils/settleWithLimit.test.ts | 43 ++++++++++++ frontend/src/v2/utils/settleWithLimit.ts | 24 +++++++ 51 files changed, 449 insertions(+), 44 deletions(-) create mode 100644 frontend/src/__generated__/models/Body_create_download_selection_api_roms_download_selection_post.ts create mode 100644 frontend/src/__generated__/models/BulkDownloadSelectionSchema.ts create mode 100644 frontend/src/v2/utils/settleWithLimit.test.ts create mode 100644 frontend/src/v2/utils/settleWithLimit.ts diff --git a/backend/endpoints/responses/__init__.py b/backend/endpoints/responses/__init__.py index 6f1fa89c2..609134bf8 100644 --- a/backend/endpoints/responses/__init__.py +++ b/backend/endpoints/responses/__init__.py @@ -166,3 +166,7 @@ class BulkOperationResponse(TypedDict): successful_items: int failed_ids: list[int] errors: list[str] + + +class BulkDownloadSelectionSchema(TypedDict): + token: str diff --git a/backend/endpoints/roms/__init__.py b/backend/endpoints/roms/__init__.py index b2f219c15..580272f07 100644 --- a/backend/endpoints/roms/__init__.py +++ b/backend/endpoints/roms/__init__.py @@ -1,6 +1,7 @@ import binascii import json import re +import secrets from base64 import b64encode from dataclasses import replace from datetime import datetime, timezone @@ -33,7 +34,7 @@ from config import ( LIBRARY_BASE_PATH, ) from decorators.auth import protected_route -from endpoints.responses import BulkOperationResponse +from endpoints.responses import BulkDownloadSelectionSchema, BulkOperationResponse from endpoints.responses.base import PAGE_QUERY, LimitOffsetPage, PageParams from endpoints.responses.recommendation import SimilarRomSchema from endpoints.responses.rom import ( @@ -98,6 +99,7 @@ from handler.metadata.ss_handler import ( get_preferred_media_types, ) from handler.recommendation import similar_roms +from handler.redis_handler import async_cache from handler.rom_conversion import promote_single_file_to_folder from handler.scan_handler import ( MetadataSource, @@ -911,6 +913,53 @@ def _bulk_download_target( return AuditTarget(target.type, target.id, None) +BULK_DOWNLOAD_SELECTION_TTL: Final[int] = 3600 + + +def _bulk_download_selection_key(token: str) -> str: + return f"romm:bulk-download-selection:{token}" + + +@protected_route( + router.post, + "/download/selection", + [Scope.ROMS_READ], +) +async def create_download_selection( + request: Request, + rom_ids: Annotated[ + list[int], + Body( + description="ROM IDs to download as a zip file.", + embed=True, + min_length=1, + ), + ], +) -> BulkDownloadSelectionSchema: + """Store a list of ROM IDs too long for a URL, for `GET /roms/download?selection=`.""" + token = secrets.token_urlsafe(16) + user_id = request.user.id if request.user.is_authenticated else None + # The TTL outlives the download itself so a resumed transfer still resolves. + await async_cache.set( + _bulk_download_selection_key(token), + json.dumps({"user_id": user_id, "rom_ids": rom_ids}), + ex=BULK_DOWNLOAD_SELECTION_TTL, + ) + return {"token": token} + + +async def _resolve_download_selection(request: Request, token: str) -> list[int]: + raw = await async_cache.get(_bulk_download_selection_key(token)) + user_id = request.user.id if request.user.is_authenticated else None + stored = json.loads(raw) if raw else None + if not stored or stored["user_id"] != user_id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Download selection not found or expired", + ) + return cast(list[int], stored["rom_ids"]) + + @protected_route( router.get, "/download", @@ -945,6 +994,12 @@ async def download_roms( ge=1, ), ] = None, + selection: Annotated[ + str | None, + Query( + description="Token from `POST /roms/download/selection` naming the ROMs to download.", + ), + ] = None, filename: Annotated[ str | None, Query( @@ -971,6 +1026,8 @@ async def download_roms( hidden_rom_ids=list(perms.hidden_rom_ids), **HAS_FILE_ON_DISK_FILTERS, ) + elif selection: + rom_id_list = await _resolve_download_selection(request, selection) elif rom_ids: try: rom_id_list = parse_comma_separated_ids(rom_ids, "ROM ID") diff --git a/backend/tests/endpoints/roms/test_rom.py b/backend/tests/endpoints/roms/test_rom.py index 6f4e3798d..e5142b458 100644 --- a/backend/tests/endpoints/roms/test_rom.py +++ b/backend/tests/endpoints/roms/test_rom.py @@ -296,6 +296,71 @@ def test_download_roms_by_collection( assert rom_file.file_name in response.text +def test_download_roms_by_selection( + client: TestClient, access_token: str, rom_file: RomFile +): + """A stored selection carries the ID list in a body rather than the URL.""" + headers = {"Authorization": f"Bearer {access_token}"} + created = client.post( + "/api/roms/download/selection", + headers=headers, + json={"rom_ids": [rom_file.rom_id]}, + ) + assert created.status_code == status.HTTP_200_OK + token = created.json()["token"] + + response = client.get( + f"/api/roms/download?selection={token}", + headers=headers, + ) + + assert response.status_code == status.HTTP_200_OK + assert response.headers["X-Archive-Files"] == "zip" + assert rom_file.file_name in response.text + + +def test_download_roms_by_unknown_selection_is_not_found( + client: TestClient, access_token: str +): + response = client.get( + "/api/roms/download?selection=missing", + headers={"Authorization": f"Bearer {access_token}"}, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + +def test_download_selection_is_bound_to_its_creator( + client: TestClient, + access_token: str, + editor_access_token: str, + rom_file: RomFile, +): + created = client.post( + "/api/roms/download/selection", + headers={"Authorization": f"Bearer {access_token}"}, + json={"rom_ids": [rom_file.rom_id]}, + ) + token = created.json()["token"] + + response = client.get( + f"/api/roms/download?selection={token}", + headers={"Authorization": f"Bearer {editor_access_token}"}, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + +def test_download_selection_requires_rom_ids(client: TestClient, access_token: str): + response = client.post( + "/api/roms/download/selection", + headers={"Authorization": f"Bearer {access_token}"}, + json={"rom_ids": []}, + ) + + assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT + + def test_download_roms_without_selector_is_bad_request( client: TestClient, access_token: str ): diff --git a/docs/BACKEND_ARCHITECTURE.md b/docs/BACKEND_ARCHITECTURE.md index de978ac1f..5c4b18bb2 100644 --- a/docs/BACKEND_ARCHITECTURE.md +++ b/docs/BACKEND_ARCHITECTURE.md @@ -900,6 +900,8 @@ A reset link is emailed when SMTP is set up, the user has an address and `ROMM_B | PUT | `/{id}/user` | ME_WRITE | Update user-specific ROM data | | DELETE | `/{id}` | ROMS_WRITE | Delete ROM | | POST | `/delete` | ROMS_WRITE | Bulk delete | +| GET | `/download` | ROMS_READ | Bulk download as a zip (ids, selector, or token) | +| POST | `/download/selection` | ROMS_READ | Store an id list, returning a download token | | POST | `/download/{id}/{file_name}` | ROMS_READ | Download ROM | | POST | `/unidentified` | ROMS_READ | Get unidentified ROMs | diff --git a/frontend/src/__generated__/index.ts b/frontend/src/__generated__/index.ts index a184c8d5b..7604f37c1 100644 --- a/frontend/src/__generated__/index.ts +++ b/frontend/src/__generated__/index.ts @@ -29,6 +29,7 @@ export type { Body_add_smart_collection_api_collections_smart_post } from './mod export type { Body_add_state_api_states_post } from './models/Body_add_state_api_states_post'; export type { Body_add_user_api_users_post } from './models/Body_add_user_api_users_post'; export type { Body_confirm_download_api_saves__id__downloaded_post } from './models/Body_confirm_download_api_saves__id__downloaded_post'; +export type { Body_create_download_selection_api_roms_download_selection_post } from './models/Body_create_download_selection_api_roms_download_selection_post'; export type { Body_create_user_from_invite_api_users_register_post } from './models/Body_create_user_from_invite_api_users_register_post'; export type { Body_delete_firmware_api_firmware_delete_post } from './models/Body_delete_firmware_api_firmware_delete_post'; export type { Body_delete_memory_cards_api_memory_cards_delete_post } from './models/Body_delete_memory_cards_api_memory_cards_delete_post'; @@ -63,6 +64,7 @@ export type { Body_update_state_favorite_api_states__id__favorite_put } from './ export type { Body_update_state_labels_api_states__id__labels_put } from './models/Body_update_state_labels_api_states__id__labels_put'; export type { Body_update_state_visibility_api_states__id__visibility_put } from './models/Body_update_state_visibility_api_states__id__visibility_put'; export type { Body_upload_memory_card_version_api_memory_cards__id__versions_post } from './models/Body_upload_memory_card_version_api_memory_cards__id__versions_post'; +export type { BulkDownloadSelectionSchema } from './models/BulkDownloadSelectionSchema'; export type { BulkOperationResponse } from './models/BulkOperationResponse'; export type { ClaimStreamingSessionRequest } from './models/ClaimStreamingSessionRequest'; export type { CleanupTaskMeta } from './models/CleanupTaskMeta'; diff --git a/frontend/src/__generated__/models/Body_create_download_selection_api_roms_download_selection_post.ts b/frontend/src/__generated__/models/Body_create_download_selection_api_roms_download_selection_post.ts new file mode 100644 index 000000000..088ec8a53 --- /dev/null +++ b/frontend/src/__generated__/models/Body_create_download_selection_api_roms_download_selection_post.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type Body_create_download_selection_api_roms_download_selection_post = { + /** + * ROM IDs to download as a zip file. + */ + rom_ids: Array; +}; + diff --git a/frontend/src/__generated__/models/BulkDownloadSelectionSchema.ts b/frontend/src/__generated__/models/BulkDownloadSelectionSchema.ts new file mode 100644 index 000000000..7c4a5f148 --- /dev/null +++ b/frontend/src/__generated__/models/BulkDownloadSelectionSchema.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type BulkDownloadSelectionSchema = { + token: string; +}; + diff --git a/frontend/src/locales/bg_BG/gallery.json b/frontend/src/locales/bg_BG/gallery.json index 079732048..421b554c2 100644 --- a/frontend/src/locales/bg_BG/gallery.json +++ b/frontend/src/locales/bg_BG/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Изтрий", "selection-deselect-all": "Отмени всички", "selection-download": "Свали", + "selection-download-fail": "Неуспешно стартиране на свалянето", "selection-download-many": "Сваляне на {n} игри…", "selection-favorite": "Добави в любими", "selection-favorite-fail": "Неуспешно обновяване на любими", diff --git a/frontend/src/locales/bg_BG/rom.json b/frontend/src/locales/bg_BG/rom.json index b60ac6748..96fd9db34 100644 --- a/frontend/src/locales/bg_BG/rom.json +++ b/frontend/src/locales/bg_BG/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "\"{name}\" ще бъде премахната от този ROM и от файловата система. Това действие е необратимо.", "delete-screenshot-title": "Изтриване на екранната снимка?", "delete-select-instruction": "Избери игрите които искаш да премахнеш от файловата система, иначе ще бъдат изтрити само от базата данни на RomM.", + "delete-show-more": "Покажи още {n}", "delete-state": "Изтрий бърз запис", "delete-state-body-named": "\"{name}\" ще бъде премахнат от този ROM и от файловата система. Това действие е необратимо.", "delete-state-title": "Изтриване на бързия запис?", diff --git a/frontend/src/locales/cs_CZ/gallery.json b/frontend/src/locales/cs_CZ/gallery.json index 47e53a273..82bd26570 100644 --- a/frontend/src/locales/cs_CZ/gallery.json +++ b/frontend/src/locales/cs_CZ/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Smazat", "selection-deselect-all": "Zrušit výběr všeho", "selection-download": "Stáhnout", + "selection-download-fail": "Stahování se nepodařilo spustit", "selection-download-many": "Stahování {n} her…", "selection-favorite": "Přidat mezi oblíbené", "selection-favorite-fail": "Nepodařilo se aktualizovat oblíbené", diff --git a/frontend/src/locales/cs_CZ/rom.json b/frontend/src/locales/cs_CZ/rom.json index 6704a415e..c3d893e8d 100644 --- a/frontend/src/locales/cs_CZ/rom.json +++ b/frontend/src/locales/cs_CZ/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "„{name}“ bude odebráno z této ROM a ze souborového systému. Tuto akci nelze vrátit zpět.", "delete-screenshot-title": "Smazat screenshot?", "delete-select-instruction": "Vyberte hry, které chcete odstranit ze souborového systému, jinak budou smazány pouze z databáze RomM.", + "delete-show-more": "Zobrazit dalších {n}", "delete-state": "Smazat stav", "delete-state-body-named": "„{name}“ bude odebráno z této ROM a ze souborového systému. Tuto akci nelze vrátit zpět.", "delete-state-title": "Smazat stav?", diff --git a/frontend/src/locales/de_DE/gallery.json b/frontend/src/locales/de_DE/gallery.json index e3fc1da44..c01b67eda 100644 --- a/frontend/src/locales/de_DE/gallery.json +++ b/frontend/src/locales/de_DE/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Löschen", "selection-deselect-all": "Alle abwählen", "selection-download": "Herunterladen", + "selection-download-fail": "Download konnte nicht gestartet werden", "selection-download-many": "{n} Spiele werden heruntergeladen…", "selection-favorite": "Zu Favoriten hinzufügen", "selection-favorite-fail": "Favoriten konnten nicht aktualisiert werden", diff --git a/frontend/src/locales/de_DE/rom.json b/frontend/src/locales/de_DE/rom.json index e8387fac9..26daf700d 100644 --- a/frontend/src/locales/de_DE/rom.json +++ b/frontend/src/locales/de_DE/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "\"{name}\" wird aus dieser ROM und aus dem Dateisystem entfernt. Dies kann nicht rückgängig gemacht werden.", "delete-screenshot-title": "Screenshot löschen?", "delete-select-instruction": "Wählen Sie die Spiele aus, die Sie aus Ihrem Dateisystem entfernen möchten, andernfalls werden sie nur aus der RomM-Datenbank gelöscht.", + "delete-show-more": "{n} weitere anzeigen", "delete-state": "Zustand löschen", "delete-state-body-named": "\"{name}\" wird aus dieser ROM und dem Dateisystem entfernt. Dies kann nicht rückgängig gemacht werden.", "delete-state-title": "Zustand löschen?", diff --git a/frontend/src/locales/en_GB/gallery.json b/frontend/src/locales/en_GB/gallery.json index cf084c935..8807da30e 100644 --- a/frontend/src/locales/en_GB/gallery.json +++ b/frontend/src/locales/en_GB/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Delete", "selection-deselect-all": "Deselect all", "selection-download": "Download", + "selection-download-fail": "Failed to start the download", "selection-download-many": "Downloading {n} games…", "selection-favorite": "Add to favourites", "selection-favorite-fail": "Failed to update favourites", diff --git a/frontend/src/locales/en_GB/rom.json b/frontend/src/locales/en_GB/rom.json index 594667bc2..53e9b6a7f 100644 --- a/frontend/src/locales/en_GB/rom.json +++ b/frontend/src/locales/en_GB/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "\"{name}\" will be removed from this ROM and from the file system. This can't be undone.", "delete-screenshot-title": "Delete screenshot?", "delete-select-instruction": "Select the games you want to remove from your filesystem, otherwise they will only be deleted from RomM database.", + "delete-show-more": "Show {n} more", "delete-state": "Delete state", "delete-state-body-named": "\"{name}\" will be removed from this ROM and from the file system. This can't be undone.", "delete-state-title": "Delete state?", diff --git a/frontend/src/locales/en_US/gallery.json b/frontend/src/locales/en_US/gallery.json index 9a7781dc3..c760acf01 100644 --- a/frontend/src/locales/en_US/gallery.json +++ b/frontend/src/locales/en_US/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Delete", "selection-deselect-all": "Deselect all", "selection-download": "Download", + "selection-download-fail": "Failed to start the download", "selection-download-many": "Downloading {n} games…", "selection-favorite": "Add to favorites", "selection-favorite-fail": "Failed to update favorites", diff --git a/frontend/src/locales/en_US/rom.json b/frontend/src/locales/en_US/rom.json index 2514d1282..2ec77d737 100644 --- a/frontend/src/locales/en_US/rom.json +++ b/frontend/src/locales/en_US/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "\"{name}\" will be removed from this ROM and from the file system. This can't be undone.", "delete-screenshot-title": "Delete screenshot?", "delete-select-instruction": "Select the games you want to remove from your filesystem, otherwise they will only be deleted from RomM database.", + "delete-show-more": "Show {n} more", "delete-state": "Delete state", "delete-state-body-named": "\"{name}\" will be removed from this ROM and from the file system. This can't be undone.", "delete-state-title": "Delete state?", diff --git a/frontend/src/locales/es_ES/gallery.json b/frontend/src/locales/es_ES/gallery.json index 6c215a008..cac83ce13 100644 --- a/frontend/src/locales/es_ES/gallery.json +++ b/frontend/src/locales/es_ES/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Eliminar", "selection-deselect-all": "Deseleccionar todo", "selection-download": "Descargar", + "selection-download-fail": "Error al iniciar la descarga", "selection-download-many": "Descargando {n} juegos…", "selection-favorite": "Añadir a favoritos", "selection-favorite-fail": "Error al actualizar favoritos", diff --git a/frontend/src/locales/es_ES/rom.json b/frontend/src/locales/es_ES/rom.json index c3fb69270..8b5caae71 100644 --- a/frontend/src/locales/es_ES/rom.json +++ b/frontend/src/locales/es_ES/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "\"{name}\" será eliminada de esta ROM y del sistema de archivos. No se puede deshacer.", "delete-screenshot-title": "¿Eliminar captura?", "delete-select-instruction": "Selecciona los juegos que quieres eliminar de tu sistema de archivos, de lo contrario solo se eliminarán de la base de datos de RomM.", + "delete-show-more": "Mostrar {n} más", "delete-state": "Eliminar estado", "delete-state-body-named": "\"{name}\" será eliminado de esta ROM y del sistema de archivos. No se puede deshacer.", "delete-state-title": "¿Eliminar estado?", diff --git a/frontend/src/locales/fr_FR/gallery.json b/frontend/src/locales/fr_FR/gallery.json index 7b0cd4799..30c2cb91f 100644 --- a/frontend/src/locales/fr_FR/gallery.json +++ b/frontend/src/locales/fr_FR/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Supprimer", "selection-deselect-all": "Tout désélectionner", "selection-download": "Télécharger", + "selection-download-fail": "Échec du lancement du téléchargement", "selection-download-many": "Téléchargement de {n} jeux…", "selection-favorite": "Ajouter aux favoris", "selection-favorite-fail": "Échec de la mise à jour des favoris", diff --git a/frontend/src/locales/fr_FR/rom.json b/frontend/src/locales/fr_FR/rom.json index ca68bca2e..3bdc5445b 100644 --- a/frontend/src/locales/fr_FR/rom.json +++ b/frontend/src/locales/fr_FR/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "« {name} » sera supprimée de cette ROM et du système de fichiers. Cette action est irréversible.", "delete-screenshot-title": "Supprimer la capture d'écran ?", "delete-select-instruction": "Sélectionnez les jeux à supprimer de votre système de fichiers, sinon ils seront uniquement supprimés de la base de données RomM.", + "delete-show-more": "Afficher {n} de plus", "delete-state": "Supprimer l'état", "delete-state-body-named": "« {name} » sera supprimé de cette ROM et du système de fichiers. Cette action est irréversible.", "delete-state-title": "Supprimer l'état ?", diff --git a/frontend/src/locales/hu_HU/gallery.json b/frontend/src/locales/hu_HU/gallery.json index 9949047a6..bce7bff28 100644 --- a/frontend/src/locales/hu_HU/gallery.json +++ b/frontend/src/locales/hu_HU/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Törlés", "selection-deselect-all": "Kijelölés megszüntetése", "selection-download": "Letöltés", + "selection-download-fail": "A letöltés indítása sikertelen", "selection-download-many": "{n} játék letöltése folyamatban…", "selection-favorite": "Hozzáadás a kedvencekhez", "selection-favorite-fail": "A kedvencek frissítése sikertelen", diff --git a/frontend/src/locales/hu_HU/rom.json b/frontend/src/locales/hu_HU/rom.json index 952f73599..553208ec2 100644 --- a/frontend/src/locales/hu_HU/rom.json +++ b/frontend/src/locales/hu_HU/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "A(z) „{name}” eltávolításra kerül erről a ROM-ról és a fájlrendszerből. Ez nem vonható vissza.", "delete-screenshot-title": "Törlöd a képernyőképet?", "delete-select-instruction": "Válasszd ki azokat a játékokat, amelyeket el szeretnél távolítani a fájlrendszeredből, ellenkező esetben azok csak a RomM adatbázisból lesznek törölve.", + "delete-show-more": "További {n} megjelenítése", "delete-state": "Állás törlése", "delete-state-body-named": "A(z) „{name}” eltávolításra kerül erről a ROM-ról és a fájlrendszerből. Ez nem vonható vissza.", "delete-state-title": "Törlöd az állást?", diff --git a/frontend/src/locales/it_IT/gallery.json b/frontend/src/locales/it_IT/gallery.json index c0aab8eeb..f2b598e84 100644 --- a/frontend/src/locales/it_IT/gallery.json +++ b/frontend/src/locales/it_IT/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Elimina", "selection-deselect-all": "Deseleziona tutti", "selection-download": "Scarica", + "selection-download-fail": "Impossibile avviare il download", "selection-download-many": "Download di {n} giochi…", "selection-favorite": "Aggiungi ai preferiti", "selection-favorite-fail": "Impossibile aggiornare i preferiti", diff --git a/frontend/src/locales/it_IT/rom.json b/frontend/src/locales/it_IT/rom.json index a041dac75..07aeaaf22 100644 --- a/frontend/src/locales/it_IT/rom.json +++ b/frontend/src/locales/it_IT/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "\"{name}\" verrà rimosso da questa ROM e dal filesystem. L'operazione non può essere annullata.", "delete-screenshot-title": "Eliminare lo screenshot?", "delete-select-instruction": "Seleziona i giochi che vuoi rimuovere dal tuo filesystem, altrimenti verranno eliminati solo dal database RomM.", + "delete-show-more": "Mostra altri {n}", "delete-state": "Elimina stato", "delete-state-body-named": "\"{name}\" verrà rimosso da questa ROM e dal filesystem. L'operazione non può essere annullata.", "delete-state-title": "Eliminare lo stato?", diff --git a/frontend/src/locales/ja_JP/gallery.json b/frontend/src/locales/ja_JP/gallery.json index aeeb2b351..062dabf07 100644 --- a/frontend/src/locales/ja_JP/gallery.json +++ b/frontend/src/locales/ja_JP/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "削除", "selection-deselect-all": "すべての選択を解除", "selection-download": "ダウンロード", + "selection-download-fail": "ダウンロードを開始できませんでした", "selection-download-many": "{n}件のゲームをダウンロード中…", "selection-favorite": "お気に入りに追加", "selection-favorite-fail": "お気に入りの更新に失敗しました", diff --git a/frontend/src/locales/ja_JP/rom.json b/frontend/src/locales/ja_JP/rom.json index 66e0ac3a8..976351e7b 100644 --- a/frontend/src/locales/ja_JP/rom.json +++ b/frontend/src/locales/ja_JP/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "「{name}」はこのROMとファイルシステムから削除されます。この操作は取り消せません。", "delete-screenshot-title": "スクリーンショットを削除しますか?", "delete-select-instruction": "ファイルシステムから削除するゲームを選択してください。選択しない場合は、RomMデータベースからのみ削除されます。", + "delete-show-more": "さらに{n}件を表示", "delete-state": "ステートセーブを削除", "delete-state-body-named": "「{name}」はこのROMとファイルシステムから削除されます。この操作は取り消せません。", "delete-state-title": "ステートセーブを削除しますか?", diff --git a/frontend/src/locales/ko_KR/gallery.json b/frontend/src/locales/ko_KR/gallery.json index 336cd930c..73b52062a 100644 --- a/frontend/src/locales/ko_KR/gallery.json +++ b/frontend/src/locales/ko_KR/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "삭제", "selection-deselect-all": "모두 선택 해제", "selection-download": "다운로드", + "selection-download-fail": "다운로드를 시작하지 못했습니다", "selection-download-many": "{n}개의 게임 다운로드 중…", "selection-favorite": "즐겨찾기에 추가", "selection-favorite-fail": "즐겨찾기를 업데이트하지 못했습니다", diff --git a/frontend/src/locales/ko_KR/rom.json b/frontend/src/locales/ko_KR/rom.json index 4ef035f6e..aee8f1347 100644 --- a/frontend/src/locales/ko_KR/rom.json +++ b/frontend/src/locales/ko_KR/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "\"{name}\"이(가) 이 ROM과 파일 시스템에서 제거됩니다. 이 작업은 되돌릴 수 없습니다.", "delete-screenshot-title": "스크린샷을 삭제하시겠습니까?", "delete-select-instruction": "파일 시스템에서 제거할 게임을 선택하세요. 그렇지 않으면 RomM 데이터베이스에서만 삭제됩니다.", + "delete-show-more": "{n}개 더 보기", "delete-state": "상태 삭제", "delete-state-body-named": "\"{name}\"이(가) 이 ROM과 파일 시스템에서 제거됩니다. 이 작업은 되돌릴 수 없습니다.", "delete-state-title": "상태를 삭제하시겠습니까?", diff --git a/frontend/src/locales/pl_PL/gallery.json b/frontend/src/locales/pl_PL/gallery.json index 5fea0901b..3617606bb 100644 --- a/frontend/src/locales/pl_PL/gallery.json +++ b/frontend/src/locales/pl_PL/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Usuń", "selection-deselect-all": "Odznacz wszystkie", "selection-download": "Pobierz", + "selection-download-fail": "Nie udało się rozpocząć pobierania", "selection-download-many": "Pobieranie {n} gier…", "selection-favorite": "Dodaj do ulubionych", "selection-favorite-fail": "Nie udało się zaktualizować ulubionych", diff --git a/frontend/src/locales/pl_PL/rom.json b/frontend/src/locales/pl_PL/rom.json index afbff35c5..21f9128d8 100644 --- a/frontend/src/locales/pl_PL/rom.json +++ b/frontend/src/locales/pl_PL/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "\"{name}\" zostanie usunięty z tego ROM-u oraz z systemu plików. Tej operacji nie można cofnąć.", "delete-screenshot-title": "Usunąć zrzut ekranu?", "delete-select-instruction": "Wybierz gry, które chcesz usunąć z systemu plików, w przeciwnym razie zostaną usunięte tylko z bazy danych RomM.", + "delete-show-more": "Pokaż {n} więcej", "delete-state": "Usuń stan", "delete-state-body-named": "\"{name}\" zostanie usunięty z tego ROM-u oraz z systemu plików. Tej operacji nie można cofnąć.", "delete-state-title": "Usunąć stan?", diff --git a/frontend/src/locales/pt_BR/gallery.json b/frontend/src/locales/pt_BR/gallery.json index adaaf9596..4dcb72a85 100644 --- a/frontend/src/locales/pt_BR/gallery.json +++ b/frontend/src/locales/pt_BR/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Excluir", "selection-deselect-all": "Desmarcar todos", "selection-download": "Baixar", + "selection-download-fail": "Falha ao iniciar o download", "selection-download-many": "Baixando {n} jogos…", "selection-favorite": "Adicionar aos favoritos", "selection-favorite-fail": "Falha ao atualizar favoritos", diff --git a/frontend/src/locales/pt_BR/rom.json b/frontend/src/locales/pt_BR/rom.json index c98e0bfe9..b832c0f79 100644 --- a/frontend/src/locales/pt_BR/rom.json +++ b/frontend/src/locales/pt_BR/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "\"{name}\" será removido desta ROM e do sistema de arquivos. Esta ação não pode ser desfeita.", "delete-screenshot-title": "Excluir captura de tela?", "delete-select-instruction": "Selecione os jogos que deseja remover do seu sistema de arquivos, caso contrário, eles serão apenas excluídos do banco de dados do RomM.", + "delete-show-more": "Mostrar mais {n}", "delete-state": "Excluir state", "delete-state-body-named": "\"{name}\" será removido desta ROM e do sistema de arquivos. Esta ação não pode ser desfeita.", "delete-state-title": "Excluir state?", diff --git a/frontend/src/locales/ro_RO/gallery.json b/frontend/src/locales/ro_RO/gallery.json index 865db6bde..61f6bf63c 100644 --- a/frontend/src/locales/ro_RO/gallery.json +++ b/frontend/src/locales/ro_RO/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Șterge", "selection-deselect-all": "Deselectează toate", "selection-download": "Descarcă", + "selection-download-fail": "Pornirea descărcării a eșuat", "selection-download-many": "Se descarcă {n} jocuri…", "selection-favorite": "Adaugă la favorite", "selection-favorite-fail": "Actualizarea favoritelor a eșuat", diff --git a/frontend/src/locales/ro_RO/rom.json b/frontend/src/locales/ro_RO/rom.json index 9d173668d..8b7726bcb 100644 --- a/frontend/src/locales/ro_RO/rom.json +++ b/frontend/src/locales/ro_RO/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "„{name}” va fi eliminată din acest ROM și din sistemul de fișiere. Această acțiune nu poate fi anulată.", "delete-screenshot-title": "Ștergi captura de ecran?", "delete-select-instruction": "Selectează jocurile pe care vrei să le elimini din sistemul de fișiere, altfel vor fi șterse doar din baza de date RomM.", + "delete-show-more": "Afișează încă {n}", "delete-state": "Șterge starea", "delete-state-body-named": "„{name}” va fi eliminată din acest ROM și din sistemul de fișiere. Această acțiune nu poate fi anulată.", "delete-state-title": "Ștergi starea?", diff --git a/frontend/src/locales/ru_RU/gallery.json b/frontend/src/locales/ru_RU/gallery.json index d386572f3..784c31bb5 100644 --- a/frontend/src/locales/ru_RU/gallery.json +++ b/frontend/src/locales/ru_RU/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Удалить", "selection-deselect-all": "Снять выбор со всех", "selection-download": "Скачать", + "selection-download-fail": "Не удалось начать скачивание", "selection-download-many": "Скачивание {n} игр…", "selection-favorite": "Добавить в избранное", "selection-favorite-fail": "Не удалось обновить избранное", diff --git a/frontend/src/locales/ru_RU/rom.json b/frontend/src/locales/ru_RU/rom.json index 516f87d8a..b1af7f42d 100644 --- a/frontend/src/locales/ru_RU/rom.json +++ b/frontend/src/locales/ru_RU/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "\"{name}\" будет удалён из этого ROM и из файловой системы. Это действие нельзя отменить.", "delete-screenshot-title": "Удалить скриншот?", "delete-select-instruction": "Выберите игры, которые хотите удалить из файловой системы, иначе они будут удалены только из базы данных RomM.", + "delete-show-more": "Показать ещё {n}", "delete-state": "Удалить состояние", "delete-state-body-named": "\"{name}\" будет удалено из этого ROM и из файловой системы. Это действие нельзя отменить.", "delete-state-title": "Удалить состояние?", diff --git a/frontend/src/locales/tr_TR/gallery.json b/frontend/src/locales/tr_TR/gallery.json index 0e0260d81..8ef2d81d3 100644 --- a/frontend/src/locales/tr_TR/gallery.json +++ b/frontend/src/locales/tr_TR/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "Sil", "selection-deselect-all": "Tümünün seçimini kaldır", "selection-download": "İndir", + "selection-download-fail": "İndirme başlatılamadı", "selection-download-many": "{n} oyun indiriliyor…", "selection-favorite": "Favorilere ekle", "selection-favorite-fail": "Favoriler güncellenemedi", diff --git a/frontend/src/locales/tr_TR/rom.json b/frontend/src/locales/tr_TR/rom.json index 7f955c655..10c7346ec 100644 --- a/frontend/src/locales/tr_TR/rom.json +++ b/frontend/src/locales/tr_TR/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "\"{name}\", bu ROM'dan ve dosya sisteminden kaldırılacak. Bu işlem geri alınamaz.", "delete-screenshot-title": "Ekran görüntüsü silinsin mi?", "delete-select-instruction": "Dosya sisteminizden kaldırmak istediğiniz oyunları seçin; aksi takdirde yalnızca RomM veritabanından silinirler.", + "delete-show-more": "{n} tane daha göster", "delete-state": "Durum kaydını sil", "delete-state-body-named": "\"{name}\", bu ROM'dan ve dosya sisteminden kaldırılacak. Bu işlem geri alınamaz.", "delete-state-title": "Durum kaydı silinsin mi?", diff --git a/frontend/src/locales/zh_CN/gallery.json b/frontend/src/locales/zh_CN/gallery.json index ed8b62c0a..c2a5bdf66 100644 --- a/frontend/src/locales/zh_CN/gallery.json +++ b/frontend/src/locales/zh_CN/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "删除", "selection-deselect-all": "取消全选", "selection-download": "下载", + "selection-download-fail": "无法开始下载", "selection-download-many": "正在下载 {n} 个游戏…", "selection-favorite": "添加到喜好", "selection-favorite-fail": "更新喜好失败", diff --git a/frontend/src/locales/zh_CN/rom.json b/frontend/src/locales/zh_CN/rom.json index f37ade125..5cca02b49 100644 --- a/frontend/src/locales/zh_CN/rom.json +++ b/frontend/src/locales/zh_CN/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "\"{name}\" 将从此 ROM 及文件系统中移除。此操作无法撤销。", "delete-screenshot-title": "确定要删除截图吗?", "delete-select-instruction": "选择要从文件系统中删除的游戏,否则它们只会从 RomM 数据库中删除。", + "delete-show-more": "再显示 {n} 个", "delete-state": "删除状态", "delete-state-body-named": "\"{name}\" 将从此 ROM 及文件系统中移除。此操作无法撤销。", "delete-state-title": "确定要删除状态吗?", diff --git a/frontend/src/locales/zh_TW/gallery.json b/frontend/src/locales/zh_TW/gallery.json index 20567ac18..190fe82d4 100644 --- a/frontend/src/locales/zh_TW/gallery.json +++ b/frontend/src/locales/zh_TW/gallery.json @@ -11,6 +11,7 @@ "selection-delete": "刪除", "selection-deselect-all": "取消選擇全部", "selection-download": "下載", + "selection-download-fail": "無法開始下載", "selection-download-many": "正在下載 {n} 個遊戲…", "selection-favorite": "加入喜好", "selection-favorite-fail": "更新喜好失敗", diff --git a/frontend/src/locales/zh_TW/rom.json b/frontend/src/locales/zh_TW/rom.json index 7cb77d555..54d1b8445 100644 --- a/frontend/src/locales/zh_TW/rom.json +++ b/frontend/src/locales/zh_TW/rom.json @@ -128,6 +128,7 @@ "delete-screenshot-body-named": "「{name}」將從此 ROM 及檔案系統中移除。此操作無法復原。", "delete-screenshot-title": "刪除螢幕截圖?", "delete-select-instruction": "選擇要從檔案系統中刪除的遊戲,否則它們只會從 RomM 資料庫中刪除。", + "delete-show-more": "再顯示 {n} 個", "delete-state": "刪除即時存檔", "delete-state-body-named": "「{name}」將從此 ROM 及檔案系統中移除。此操作無法復原。", "delete-state-title": "刪除即時存檔?", diff --git a/frontend/src/services/api/rom.test.ts b/frontend/src/services/api/rom.test.ts index 6cc9d8575..d9520eaba 100644 --- a/frontend/src/services/api/rom.test.ts +++ b/frontend/src/services/api/rom.test.ts @@ -1,5 +1,5 @@ import { createPinia, setActivePinia } from "pinia"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import romApi, { type UpdateRom } from "@/services/api/rom"; import storeUpload from "@/stores/upload"; @@ -216,3 +216,49 @@ describe("getRoms game-length range", () => { expect(params.hltb_main_story_max).toBe(36000); }); }); + +describe("bulkDownloadRoms", () => { + beforeEach(() => { + post.mockReset(); + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + function clickedHref(): Promise { + return new Promise((resolve) => { + vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementationOnce( + function (this: HTMLAnchorElement) { + resolve(this.getAttribute("href") ?? ""); + }, + ); + }); + } + + it("posts an id list and downloads it by token, not in the URL", async () => { + post.mockResolvedValue({ data: { token: "tok" } }); + const href = clickedHref(); + + const done = romApi.bulkDownloadRoms({ romIDs: [1, 2, 3] }); + + expect(await href).toBe("/api/roms/download?selection=tok"); + expect(post).toHaveBeenCalledWith("/roms/download/selection", { + rom_ids: [1, 2, 3], + }); + await vi.runAllTimersAsync(); + await done; + }); + + it("keeps a platform selector in the URL without a round trip", async () => { + const href = clickedHref(); + + const done = romApi.bulkDownloadRoms({ platformId: 7 }); + + expect(await href).toBe("/api/roms/download?platform_id=7"); + expect(post).not.toHaveBeenCalled(); + await vi.runAllTimersAsync(); + await done; + }); +}); diff --git a/frontend/src/services/api/rom.ts b/frontend/src/services/api/rom.ts index f78e71de9..a14d8e8e5 100644 --- a/frontend/src/services/api/rom.ts +++ b/frontend/src/services/api/rom.ts @@ -3,6 +3,7 @@ import Bowser from "bowser"; import type { Body_delete_roms_api_roms_delete_post as DeleteRomsInput, Body_update_rom_api_roms__id__put as UpdateRomInput, + BulkDownloadSelectionSchema, BulkOperationResponse, DetailedRomSchema, ManualMetadata, @@ -632,9 +633,8 @@ async function downloadRom({ } // A platform/collection selector is expanded server-side into the full ROM -// list, keeping the URL short (an explicit `romIDs` list can overflow the -// browser's URL length limit for large libraries). Pass exactly one selector; -// `romIDs` stays the fallback for ad-hoc multi-selections. +// list. Pass exactly one selector; `romIDs` is the fallback for ad-hoc +// multi-selections and is posted first, since the list can overflow a URL. async function bulkDownloadRoms({ romIDs, platformId, @@ -660,7 +660,11 @@ async function bulkDownloadRoms({ } else if (smartCollectionId != null) { queryParams.append("smart_collection_id", String(smartCollectionId)); } else if (romIDs && romIDs.length > 0) { - queryParams.append("rom_ids", romIDs.join(",")); + const { data } = await api.post( + "/roms/download/selection", + { rom_ids: romIDs }, + ); + queryParams.append("selection", data.token); } else { return; } diff --git a/frontend/src/v2/components/Dialogs/DeleteRomDialog.test.ts b/frontend/src/v2/components/Dialogs/DeleteRomDialog.test.ts index fd30c64ba..5f7d1aeb9 100644 --- a/frontend/src/v2/components/Dialogs/DeleteRomDialog.test.ts +++ b/frontend/src/v2/components/Dialogs/DeleteRomDialog.test.ts @@ -106,3 +106,53 @@ describe("DeleteRomDialog", () => { expect(storeRoms().getDetailedRom(6)).toBeNull(); }); }); + +describe("DeleteRomDialog with a large selection", () => { + beforeEach(() => { + setActivePinia(createPinia()); + vi.clearAllMocks(); + deleteRoms.mockResolvedValue({ + data: { failed_ids: [], successful_items: 120 }, + }); + }); + + async function openWith(count: number) { + const emitter: Emitter = mitt(); + const wrapper = mount(DeleteRomDialog, { + global: { + provide: { emitter }, + stubs: { RDialog, RBtn, RCheckbox: true, RIcon: true }, + }, + }); + emitter.emit( + "showDeleteRomDialog", + Array.from({ length: count }, (_, i) => + makeRom({ id: i + 1, platform_id: 1 }), + ), + ); + await flushPromises(); + return wrapper; + } + + it("renders rows a page at a time", async () => { + const wrapper = await openWith(120); + expect(wrapper.findAll(".r-v2-del-rom__row")).toHaveLength(50); + + await wrapper.find(".r-v2-del-rom__more button").trigger("click"); + expect(wrapper.findAll(".r-v2-del-rom__row")).toHaveLength(100); + + await wrapper.find(".r-v2-del-rom__more button").trigger("click"); + expect(wrapper.findAll(".r-v2-del-rom__row")).toHaveLength(120); + expect(wrapper.find(".r-v2-del-rom__more").exists()).toBe(false); + }); + + it("still deletes the rows that were never shown", async () => { + const wrapper = await openWith(120); + + await wrapper.findAll("button").at(-1)?.trigger("click"); + await flushPromises(); + + const sent = deleteRoms.mock.calls[0][0] as { roms: { id: number }[] }; + expect(sent.roms).toHaveLength(120); + }); +}); diff --git a/frontend/src/v2/components/Dialogs/DeleteRomDialog.vue b/frontend/src/v2/components/Dialogs/DeleteRomDialog.vue index b28dd25aa..bb1eca900 100644 --- a/frontend/src/v2/components/Dialogs/DeleteRomDialog.vue +++ b/frontend/src/v2/components/Dialogs/DeleteRomDialog.vue @@ -2,6 +2,7 @@ // DeleteRomDialog: single or multi-ROM delete flow. Each row has a // "also remove file from disk" checkbox; a global "exclude on delete" flag // adds deleted filenames to the scan exclusion list so they don't re-appear. +// Rows render in pages so a whole-library selection doesn't mount every cover. import { RBtn, RCheckbox, RDialog, RIcon } from "@v2/lib"; import type { Emitter } from "mitt"; import { computed, inject, onBeforeUnmount, ref } from "vue"; @@ -16,9 +17,12 @@ import type { Events } from "@/types/emitter"; import { useRomSync } from "@/v2/composables/useRomSync"; import { romIdFromRoute } from "@/v2/composables/useRouteRom"; import { useSnackbar } from "@/v2/composables/useSnackbar"; +import { settleWithLimit } from "@/v2/utils/settleWithLimit"; defineOptions({ inheritAttrs: false }); +const ROWS_PAGE_SIZE = 50; + const { t } = useI18n(); const router = useRouter(); const route = useRoute(); @@ -33,9 +37,20 @@ const deleting = ref(false); const emitter = inject>("emitter"); const snackbar = useSnackbar(); const configStore = storeConfig(); +const shownCount = ref(ROWS_PAGE_SIZE); +const shownRoms = computed(() => roms.value.slice(0, shownCount.value)); +const nextPageCount = computed(() => + Math.min(ROWS_PAGE_SIZE, roms.value.length - shownRoms.value.length), +); +const fsIds = computed(() => new Set(romsToDeleteFromFs.value)); + +function showMore() { + shownCount.value += ROWS_PAGE_SIZE; +} const openHandler = (romsToDelete: SimpleRom[]) => { roms.value = romsToDelete; + shownCount.value = ROWS_PAGE_SIZE; platformId.value = romsToDelete[0]?.platform_id ?? 0; show.value = true; }; @@ -104,16 +119,20 @@ async function deleteRoms() { { icon: "mdi-check-bold" }, ); if (exclude) { - for (const rom of deletedRoms) { - const type = rom.has_simple_single_file + const exclusionType = (rom: SimpleRom) => + rom.has_simple_single_file ? "EXCLUDED_SINGLE_FILES" : "EXCLUDED_MULTI_FILES"; + for (const rom of deletedRoms) { + configStore.addExclusion(exclusionType(rom), rom.fs_name); + } + // One at a time: each call rewrites the config file server-side. + void settleWithLimit(deletedRoms, 1, (rom) => configApi.addExclusion({ exclusionValue: rom.fs_name, - exclusionType: type, - }); - configStore.addExclusion(type, rom.fs_name); - } + exclusionType: exclusionType(rom), + }), + ); } romsStore.resetSelection(); removeCachedRoms(deletedRoms); @@ -204,12 +223,10 @@ function closeDialog() {