From e15ce10f5a59091a552c707f26b9471477b32416 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 28 May 2026 22:28:27 +0200 Subject: [PATCH] Upload progress spinner + duplicate game detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two features: 1. Progress indicator: when upload starts, the dialog switches from IGDB results to a centered spinner with 'Uploading and processing' text + filename. The Cancel button stays visible but disabled. 2. Duplicate game blocking: before upload, checks if a game with the same title (case-insensitive) already exists in the library. Frontend checks against the loaded games list; backend also checks as a second line of defense. Shows ⚠️ message in dialog. Existing unique-ID suffix logic (foo-2, foo-3) is still in place but now only reached when titles actually differ. --- frontend/src/pages/Library.svelte | 63 ++++++++++++++++++- .../java/com/dostalgia/UploadResource.java | 9 +++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/Library.svelte b/frontend/src/pages/Library.svelte index da90bc0..a947998 100644 --- a/frontend/src/pages/Library.svelte +++ b/frontend/src/pages/Library.svelte @@ -16,6 +16,7 @@ let searchError = $state(""); let igdbResults = $state(null); // null=loading, []=no results, [{...}]=matches let manualSearched = $state(false); // true after user clicked Search manually + let duplicateError = $state(""); // title of duplicate game if blocked let fileInput; @@ -80,12 +81,25 @@ igdbResults = null; searchError = ""; manualSearched = false; + duplicateError = ""; } async function doUpload(title, igdbId) { if (!pendingFile) return; + + // Block duplicates: case-insensitive title match + const finalTitle = title || pendingFileName; + if (finalTitle) { + const dup = games.find(g => g.title?.toLowerCase() === finalTitle.toLowerCase()); + if (dup) { + duplicateError = `"${dup.title}" is already in your library`; + return; + } + } + uploading = true; uploadError = ""; + duplicateError = ""; try { await uploadGame(pendingFile, title || undefined, igdbId); cancelUpload(); @@ -134,7 +148,18 @@

{pendingFileName}.zip

- {#if searching} + {#if uploading} +
+
+

Uploading and processing

+

{pendingFileName}.zip

+

This may take a while for large games

+
+ {:else if duplicateError} +
+ ⚠️ {duplicateError} +
+ {:else if searching}
Searching IGDB for "{pendingFileName}"...
{:else if igdbResults !== null && igdbResults.length > 0}

Select a match or enter a custom title:

@@ -490,4 +515,40 @@ background: #332211; border-color: #ffaa44; } + + /* ─── Upload Progress ──────────────────────────── */ + .upload-progress { + display: flex; + flex-direction: column; + align-items: center; + padding: 40px 20px; + text-align: center; + } + .upload-progress-spinner { + width: 40px; + height: 40px; + border: 3px solid var(--border); + border-top: 3px solid var(--phosphor); + border-radius: 50%; + animation: spin 0.8s linear infinite; + margin-bottom: 16px; + } + @keyframes spin { + to { transform: rotate(360deg); } + } + .upload-progress-text { + color: var(--text); + font-size: 1rem; + margin-bottom: 4px; + } + .upload-progress-sub { + color: var(--text-dim); + font-size: 0.85rem; + } + .upload-progress-note { + color: var(--text-dim); + font-size: 0.75rem; + margin-top: 8px; + opacity: 0.7; + } diff --git a/src/main/java/com/dostalgia/UploadResource.java b/src/main/java/com/dostalgia/UploadResource.java index a7876a8..fe47fb1 100644 --- a/src/main/java/com/dostalgia/UploadResource.java +++ b/src/main/java/com/dostalgia/UploadResource.java @@ -48,6 +48,15 @@ public class UploadResource { String gameId = GameService.sanitizeId(title); + // Check for duplicate by title (case-insensitive) + for (var g : svc.list()) { + if (g.getTitle() != null && g.getTitle().equalsIgnoreCase(title)) { + return Response.status(409) + .entity(Map.of("error", "\"" + title + "\" is already in your library")) + .build(); + } + } + // Ensure unique ID String baseId = gameId; int counter = 2;