Upload progress spinner + duplicate game detection
Build & Deploy / build-and-deploy (push) Successful in 1m20s

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.
This commit is contained in:
Hermes Agent
2026-05-28 22:28:27 +02:00
parent a9e52ade68
commit e15ce10f5a
2 changed files with 71 additions and 1 deletions
+62 -1
View File
@@ -16,6 +16,7 @@
let searchError = $state(""); let searchError = $state("");
let igdbResults = $state(null); // null=loading, []=no results, [{...}]=matches let igdbResults = $state(null); // null=loading, []=no results, [{...}]=matches
let manualSearched = $state(false); // true after user clicked Search manually let manualSearched = $state(false); // true after user clicked Search manually
let duplicateError = $state(""); // title of duplicate game if blocked
let fileInput; let fileInput;
@@ -80,12 +81,25 @@
igdbResults = null; igdbResults = null;
searchError = ""; searchError = "";
manualSearched = false; manualSearched = false;
duplicateError = "";
} }
async function doUpload(title, igdbId) { async function doUpload(title, igdbId) {
if (!pendingFile) return; 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; uploading = true;
uploadError = ""; uploadError = "";
duplicateError = "";
try { try {
await uploadGame(pendingFile, title || undefined, igdbId); await uploadGame(pendingFile, title || undefined, igdbId);
cancelUpload(); cancelUpload();
@@ -134,7 +148,18 @@
<p class="upload-file-name">{pendingFileName}.zip</p> <p class="upload-file-name">{pendingFileName}.zip</p>
<div class="upload-dialog-body"> <div class="upload-dialog-body">
{#if searching} {#if uploading}
<div class="upload-progress">
<div class="upload-progress-spinner"></div>
<p class="upload-progress-text">Uploading and processing</p>
<p class="upload-progress-sub">{pendingFileName}.zip</p>
<p class="upload-progress-note">This may take a while for large games</p>
</div>
{:else if duplicateError}
<div class="status-msg error-msg">
⚠️ {duplicateError}
</div>
{:else if searching}
<div class="status-msg">Searching IGDB for "{pendingFileName}"...</div> <div class="status-msg">Searching IGDB for "{pendingFileName}"...</div>
{:else if igdbResults !== null && igdbResults.length > 0} {:else if igdbResults !== null && igdbResults.length > 0}
<p class="match-label">Select a match or enter a custom title:</p> <p class="match-label">Select a match or enter a custom title:</p>
@@ -490,4 +515,40 @@
background: #332211; background: #332211;
border-color: #ffaa44; 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;
}
</style> </style>
@@ -48,6 +48,15 @@ public class UploadResource {
String gameId = GameService.sanitizeId(title); 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 // Ensure unique ID
String baseId = gameId; String baseId = gameId;
int counter = 2; int counter = 2;