diff --git a/frontend/src/locales/bg_BG/rom.json b/frontend/src/locales/bg_BG/rom.json index 776a910cf..a0f36a870 100644 --- a/frontend/src/locales/bg_BG/rom.json +++ b/frontend/src/locales/bg_BG/rom.json @@ -152,6 +152,7 @@ "edit-save": "Редактирай запис", "edit-screenshot": "Редактирай екранна снимка", "edit-state": "Редактирай бърз запис", + "exclude-failed": "{n} изтрити игри не можаха да бъдат изключени от сканиранията", "favorite": "Любим", "favorites-updated-n": "{n} елемент е обновен | {n} елемента са обновени", "file": "Файл", diff --git a/frontend/src/locales/cs_CZ/rom.json b/frontend/src/locales/cs_CZ/rom.json index bfb0c9fe3..2a7530ddd 100644 --- a/frontend/src/locales/cs_CZ/rom.json +++ b/frontend/src/locales/cs_CZ/rom.json @@ -152,6 +152,7 @@ "edit-save": "Upravit uloženou pozici", "edit-screenshot": "Upravit screenshot", "edit-state": "Upravit stav", + "exclude-failed": "{n} smazaných her se nepodařilo vyloučit ze skenování", "favorite": "Oblíbené", "favorites-updated-n": "{n} položka aktualizována | {n} položek aktualizováno", "file": "Soubor", diff --git a/frontend/src/locales/de_DE/rom.json b/frontend/src/locales/de_DE/rom.json index 3f8110310..dd212c715 100644 --- a/frontend/src/locales/de_DE/rom.json +++ b/frontend/src/locales/de_DE/rom.json @@ -152,6 +152,7 @@ "edit-save": "Spielstand bearbeiten", "edit-screenshot": "Screenshot bearbeiten", "edit-state": "Zustand bearbeiten", + "exclude-failed": "{n} gelöschte Spiele konnten nicht von Scans ausgeschlossen werden", "favorite": "Favorit", "favorites-updated-n": "{n} Element aktualisiert | {n} Elemente aktualisiert", "file": "Datei", diff --git a/frontend/src/locales/en_GB/rom.json b/frontend/src/locales/en_GB/rom.json index 404b856a0..3cae3697d 100644 --- a/frontend/src/locales/en_GB/rom.json +++ b/frontend/src/locales/en_GB/rom.json @@ -152,6 +152,7 @@ "edit-save": "Edit save", "edit-screenshot": "Edit screenshot", "edit-state": "Edit state", + "exclude-failed": "Couldn't exclude {n} deleted games from scans", "favorite": "Favourite", "favorites-updated-n": "{n} item updated | {n} items updated", "file": "File", diff --git a/frontend/src/locales/en_US/rom.json b/frontend/src/locales/en_US/rom.json index cfd4293eb..c97e01767 100644 --- a/frontend/src/locales/en_US/rom.json +++ b/frontend/src/locales/en_US/rom.json @@ -152,6 +152,7 @@ "edit-save": "Edit save", "edit-screenshot": "Edit screenshot", "edit-state": "Edit state", + "exclude-failed": "Couldn't exclude {n} deleted games from scans", "favorite": "Favorite", "favorites-updated-n": "{n} item updated | {n} items updated", "file": "File", diff --git a/frontend/src/locales/es_ES/rom.json b/frontend/src/locales/es_ES/rom.json index a47b716bb..5251d09c8 100644 --- a/frontend/src/locales/es_ES/rom.json +++ b/frontend/src/locales/es_ES/rom.json @@ -152,6 +152,7 @@ "edit-save": "Editar partida", "edit-screenshot": "Editar captura", "edit-state": "Editar estado", + "exclude-failed": "No se pudieron excluir {n} juegos eliminados de los escaneos", "favorite": "Favorito", "favorites-updated-n": "{n} elemento actualizado | {n} elementos actualizados", "file": "Archivo", diff --git a/frontend/src/locales/fr_FR/rom.json b/frontend/src/locales/fr_FR/rom.json index a5fb65dbe..1ff9c6218 100644 --- a/frontend/src/locales/fr_FR/rom.json +++ b/frontend/src/locales/fr_FR/rom.json @@ -152,6 +152,7 @@ "edit-save": "Modifier la sauvegarde", "edit-screenshot": "Modifier la capture d'écran", "edit-state": "Modifier l'état", + "exclude-failed": "Impossible d'exclure {n} jeux supprimés des analyses", "favorite": "Favori", "favorites-updated-n": "{n} élément mis à jour | {n} éléments mis à jour", "file": "Fichier", diff --git a/frontend/src/locales/hu_HU/rom.json b/frontend/src/locales/hu_HU/rom.json index da72e9da8..d2b8c0405 100644 --- a/frontend/src/locales/hu_HU/rom.json +++ b/frontend/src/locales/hu_HU/rom.json @@ -152,6 +152,7 @@ "edit-save": "Mentés szerkesztése", "edit-screenshot": "Képernyőkép szerkesztése", "edit-state": "Állás szerkesztése", + "exclude-failed": "{n} törölt játékot nem sikerült kizárni a keresésekből", "favorite": "Kedvenc", "favorites-updated-n": "{n} elem frissítve | {n} elem frissítve", "file": "Fájl", diff --git a/frontend/src/locales/it_IT/rom.json b/frontend/src/locales/it_IT/rom.json index 01dbf6631..b770c2f1a 100644 --- a/frontend/src/locales/it_IT/rom.json +++ b/frontend/src/locales/it_IT/rom.json @@ -152,6 +152,7 @@ "edit-save": "Modifica salvataggio", "edit-screenshot": "Modifica screenshot", "edit-state": "Modifica stato", + "exclude-failed": "Impossibile escludere {n} giochi eliminati dalle scansioni", "favorite": "Preferito", "favorites-updated-n": "{n} elemento aggiornato | {n} elementi aggiornati", "file": "File", diff --git a/frontend/src/locales/ja_JP/rom.json b/frontend/src/locales/ja_JP/rom.json index 574bcf917..2f5720994 100644 --- a/frontend/src/locales/ja_JP/rom.json +++ b/frontend/src/locales/ja_JP/rom.json @@ -152,6 +152,7 @@ "edit-save": "セーブを編集", "edit-screenshot": "スクリーンショットを編集", "edit-state": "ステートセーブを編集", + "exclude-failed": "削除した{n}件のゲームをスキャン対象から除外できませんでした", "favorite": "お気に入り", "favorites-updated-n": "{n} 件を更新しました", "file": "ファイル", diff --git a/frontend/src/locales/ko_KR/rom.json b/frontend/src/locales/ko_KR/rom.json index 588b57ec4..5244df987 100644 --- a/frontend/src/locales/ko_KR/rom.json +++ b/frontend/src/locales/ko_KR/rom.json @@ -152,6 +152,7 @@ "edit-save": "세이브 편집", "edit-screenshot": "스크린샷 편집", "edit-state": "상태 편집", + "exclude-failed": "삭제된 게임 {n}개를 스캔에서 제외하지 못했습니다", "favorite": "즐겨찾기", "favorites-updated-n": "{n}개 항목을 업데이트했습니다", "file": "파일", diff --git a/frontend/src/locales/pl_PL/rom.json b/frontend/src/locales/pl_PL/rom.json index bdfaf2f0c..414eb5cf3 100644 --- a/frontend/src/locales/pl_PL/rom.json +++ b/frontend/src/locales/pl_PL/rom.json @@ -152,6 +152,7 @@ "edit-save": "Edytuj zapis", "edit-screenshot": "Edytuj zrzut ekranu", "edit-state": "Edytuj stan", + "exclude-failed": "Nie udało się wykluczyć {n} usuniętych gier ze skanowania", "favorite": "Ulubione", "favorites-updated-n": "Zaktualizowano {n} element | Zaktualizowano {n} elementów", "file": "Plik", diff --git a/frontend/src/locales/pt_BR/rom.json b/frontend/src/locales/pt_BR/rom.json index c3b2b035c..b78265fe5 100644 --- a/frontend/src/locales/pt_BR/rom.json +++ b/frontend/src/locales/pt_BR/rom.json @@ -152,6 +152,7 @@ "edit-save": "Editar save", "edit-screenshot": "Editar captura de tela", "edit-state": "Editar state", + "exclude-failed": "Não foi possível excluir {n} jogos removidos das verificações", "favorite": "Favorito", "favorites-updated-n": "{n} item atualizado | {n} itens atualizados", "file": "Arquivo", diff --git a/frontend/src/locales/ro_RO/rom.json b/frontend/src/locales/ro_RO/rom.json index 7e699ddc5..811fd98b4 100644 --- a/frontend/src/locales/ro_RO/rom.json +++ b/frontend/src/locales/ro_RO/rom.json @@ -152,6 +152,7 @@ "edit-save": "Editează salvarea", "edit-screenshot": "Editează captura de ecran", "edit-state": "Editează starea", + "exclude-failed": "Nu s-au putut exclude {n} jocuri șterse din scanări", "favorite": "Favorit", "favorites-updated-n": "{n} element actualizat | {n} elemente actualizate", "file": "Fișier", diff --git a/frontend/src/locales/ru_RU/rom.json b/frontend/src/locales/ru_RU/rom.json index ba582ef66..501a53244 100644 --- a/frontend/src/locales/ru_RU/rom.json +++ b/frontend/src/locales/ru_RU/rom.json @@ -152,6 +152,7 @@ "edit-save": "Изменить сохранение", "edit-screenshot": "Изменить скриншот", "edit-state": "Изменить состояние", + "exclude-failed": "Не удалось исключить {n} удалённых игр из сканирования", "favorite": "В избранное", "favorites-updated-n": "Обновлён {n} элемент | Обновлено {n} элементов", "file": "Файл", diff --git a/frontend/src/locales/tr_TR/rom.json b/frontend/src/locales/tr_TR/rom.json index d9074add4..b532a20d4 100644 --- a/frontend/src/locales/tr_TR/rom.json +++ b/frontend/src/locales/tr_TR/rom.json @@ -152,6 +152,7 @@ "edit-save": "Kaydı düzenle", "edit-screenshot": "Ekran görüntüsünü düzenle", "edit-state": "Durum kaydını düzenle", + "exclude-failed": "Silinen {n} oyun taramalardan hariç tutulamadı", "favorite": "Favori", "favorites-updated-n": "{n} öge güncellendi", "file": "Dosya", diff --git a/frontend/src/locales/zh_CN/rom.json b/frontend/src/locales/zh_CN/rom.json index bc5bc042d..afe33db45 100644 --- a/frontend/src/locales/zh_CN/rom.json +++ b/frontend/src/locales/zh_CN/rom.json @@ -152,6 +152,7 @@ "edit-save": "编辑存档", "edit-screenshot": "编辑截图", "edit-state": "编辑状态", + "exclude-failed": "无法将 {n} 个已删除的游戏排除在扫描之外", "favorite": "收藏", "favorites-updated-n": "已更新 {n} 个项目", "file": "文件", diff --git a/frontend/src/locales/zh_TW/rom.json b/frontend/src/locales/zh_TW/rom.json index 7619139dc..b3f95fe10 100644 --- a/frontend/src/locales/zh_TW/rom.json +++ b/frontend/src/locales/zh_TW/rom.json @@ -152,6 +152,7 @@ "edit-save": "編輯存檔", "edit-screenshot": "編輯螢幕截圖", "edit-state": "編輯即時存檔", + "exclude-failed": "無法將 {n} 個已刪除的遊戲排除在掃描之外", "favorite": "喜好", "favorites-updated-n": "已更新 {n} 個項目", "file": "檔案", diff --git a/frontend/src/v2/components/Dialogs/DeleteRomDialog.test.ts b/frontend/src/v2/components/Dialogs/DeleteRomDialog.test.ts index fd30c64ba..356cad8c2 100644 --- a/frontend/src/v2/components/Dialogs/DeleteRomDialog.test.ts +++ b/frontend/src/v2/components/Dialogs/DeleteRomDialog.test.ts @@ -2,13 +2,14 @@ import { flushPromises, mount } from "@vue/test-utils"; import mitt, { type Emitter } from "mitt"; import { createPinia, setActivePinia } from "pinia"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { reactive } from "vue"; +import { defineComponent, reactive } from "vue"; import storeRoms from "@/stores/roms"; import type { Events } from "@/types/emitter"; import { makeDetailedRom, makeRom } from "@/utils/rom.fixtures"; import DeleteRomDialog from "./DeleteRomDialog.vue"; -const { deleteRoms, push, snackbarError } = vi.hoisted(() => ({ +const { addExclusion, deleteRoms, push, snackbarError } = vi.hoisted(() => ({ + addExclusion: vi.fn(), deleteRoms: vi.fn(), push: vi.fn(), snackbarError: vi.fn(), @@ -29,7 +30,7 @@ vi.mock("vue-router", async (importOriginal) => ({ vi.mock("@/plugins/router", () => ({ ROUTES: { PLATFORM: "platform" } })); vi.mock("@/services/api/rom", () => ({ default: { deleteRoms } })); vi.mock("@/services/api/config", () => ({ - default: { addExclusion: vi.fn() }, + default: { addExclusion }, })); vi.mock("@/stores/config", () => ({ default: () => ({ addExclusion: vi.fn() }), @@ -43,19 +44,35 @@ vi.mock("@/v2/composables/useSnackbar", () => ({ const RDialog = { props: { modelValue: { type: Boolean, default: false } }, - template: `
`, + template: `
`, }; const RBtn = { emits: ["click"], template: ``, }; +const RCheckbox = { + emits: ["update:modelValue"], + template: ``, +}; +// happy-dom lays nothing out, so a windowed list would render no rows. +const WholeList = defineComponent({ + props: { items: { type: Array, default: () => [] } }, + template: `
`, +}); +const stubs = { + RDialog, + RBtn, + RCheckbox, + RIcon: true, + RVirtualScroller: WholeList, +}; async function deleteShownGame() { const emitter: Emitter = mitt(); const wrapper = mount(DeleteRomDialog, { global: { provide: { emitter }, - stubs: { RDialog, RBtn, RCheckbox: true, RIcon: true }, + stubs, }, }); emitter.emit("showDeleteRomDialog", [ @@ -106,3 +123,64 @@ 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, + }, + }); + emitter.emit( + "showDeleteRomDialog", + Array.from({ length: count }, (_, i) => + makeRom({ id: i + 1, platform_id: 1 }), + ), + ); + await flushPromises(); + return wrapper; + } + + it("hands every selected game to the virtual scroller", async () => { + const wrapper = await openWith(120); + + expect(wrapper.findComponent(WholeList).props("items")).toHaveLength(120); + }); + + it("waits for the exclusions and reports the ones that failed", async () => { + addExclusion + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error("config not writable")); + const wrapper = await openWith(2); + + await wrapper.find('input[type="checkbox"]').trigger("change"); + await wrapper.findAll("button").at(-1)?.trigger("click"); + await flushPromises(); + + expect(addExclusion).toHaveBeenCalledTimes(2); + expect(snackbarError).toHaveBeenCalledWith( + "rom.exclude-failed", + expect.anything(), + ); + }); + + it("deletes every selected game", 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 c69ea9cf6..ede0477ac 100644 --- a/frontend/src/v2/components/Dialogs/DeleteRomDialog.vue +++ b/frontend/src/v2/components/Dialogs/DeleteRomDialog.vue @@ -2,7 +2,8 @@ // 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. -import { RBtn, RCheckbox, RDialog, RIcon } from "@v2/lib"; +// Rows are virtualised so a whole-library selection doesn't mount every cover. +import { RBtn, RCheckbox, RDialog, RIcon, RVirtualScroller } from "@v2/lib"; import type { Emitter } from "mitt"; import { computed, inject, onBeforeUnmount, ref } from "vue"; import { useI18n } from "vue-i18n"; @@ -16,9 +17,14 @@ 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 }); +// A 66px row plus the 6px gap below it; `.r-v2-del-rom__row` pins the row. +const ROW_HEIGHT_PX = 72; +const LIST_MAX_HEIGHT_PX = 360; + const { t } = useI18n(); const router = useRouter(); const route = useRoute(); @@ -32,6 +38,12 @@ const deleting = ref(false); const emitter = inject>("emitter"); const snackbar = useSnackbar(); const configStore = storeConfig(); +const listHeight = computed(() => + Math.min(roms.value.length * ROW_HEIGHT_PX, LIST_MAX_HEIGHT_PX), +); +const fsIds = computed(() => new Set(romsToDeleteFromFs.value)); +const rowHeight = () => ROW_HEIGHT_PX; +const rowKey = (rom: unknown) => (rom as SimpleRom).id; const openHandler = (romsToDelete: SimpleRom[]) => { roms.value = romsToDelete; @@ -102,15 +114,30 @@ 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"; + // One at a time: each call rewrites the config file server-side. + const results = await settleWithLimit(deletedRoms, 1, (rom) => configApi.addExclusion({ exclusionValue: rom.fs_name, - exclusionType: type, + exclusionType: exclusionType(rom), + }), + ); + let failed = 0; + results.forEach((result, i) => { + const rom = deletedRoms[i]; + if (result.status === "fulfilled") { + configStore.addExclusion(exclusionType(rom), rom.fs_name); + } else { + failed++; + } + }); + if (failed > 0) { + snackbar.error(t("rom.exclude-failed", { n: failed }), { + icon: "mdi-close-circle", }); - configStore.addExclusion(type, rom.fs_name); } } romsStore.resetSelection(); @@ -200,50 +227,55 @@ function closeDialog() {