Upload progress spinner + duplicate game detection
Build & Deploy / build-and-deploy (push) Successful in 1m20s
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:
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user