Merge branch 'worktree-playwright-tests'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 17:12:37 +02:00
21 changed files with 1656 additions and 160 deletions
@@ -31,22 +31,15 @@
<p x-text="overwriteMsg" class="py-2 text-sm"></p>
<div class="modal-action">
<button class="btn btn-warning btn-sm" @click="confirmOverwrite()">Overwrite</button>
<button class="btn btn-ghost btn-sm" @click="skipOverwrite()">Skip this entry</button>
<button class="btn btn-ghost btn-sm" @click="cancelExport()">Cancel</button>
</div>
</div>
</dialog>
<!-- Results -->
<div x-show="results.length > 0" class="mt-6 space-y-1">
<template x-for="r in results" :key="r.group_id">
<div class="text-sm" :class="r.needs_overwrite ? 'text-warning' : 'text-success'">
<span x-text="r.needs_overwrite ? '⚠ ' + r.title + ' — exists' : '✓ ' + r.title"></span>
<template x-if="r.failed_photos && r.failed_photos.length">
<span class="text-error ml-2" x-text="`(${r.failed_photos.length} photo(s) failed)`"></span>
</template>
</div>
</template>
</div>
<div x-show="successMsg !== ''" class="mt-6 alert alert-success text-sm" x-text="successMsg"></div>
<div x-show="failedCount > 0" class="mt-2 alert alert-warning text-sm"
x-text="`${failedCount} photo(s) failed to download`"></div>
<details class="mt-6">
<summary class="cursor-pointer text-sm opacity-60 skipped-list">
@@ -63,44 +56,47 @@
<script>
function exportApp(albumId) {
return {
results: [],
pendingOverwrites: [],
currentOverwrite: null,
successMsg: '',
failedCount: 0,
conflictPath: null,
overwriteMsg: '',
confirmedIds: [],
async runExport(extraOverwrites = []) {
async runExport() {
const res = await fetch('/export/run', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({album_id: albumId, overwrite_ids: [...this.confirmedIds, ...extraOverwrites]}),
body: JSON.stringify({album_id: albumId}),
});
const data = await res.json();
const needsOverwrite = data.results.filter(r => r.needs_overwrite);
const done = data.results.filter(r => !r.needs_overwrite);
this.results.push(...done);
if (needsOverwrite.length > 0) {
this.pendingOverwrites = needsOverwrite;
this.showNextOverwrite();
if (data.conflict) {
this.conflictPath = data.path;
this.overwriteMsg = `Destination already exists: ${data.path}`;
document.getElementById('overwrite-modal').showModal();
} else if (data.ok) {
this.successMsg = `Exported ${data.exported} entr${data.exported === 1 ? 'y' : 'ies'} successfully.`;
this.failedCount = (data.failed || []).length;
}
},
showNextOverwrite() {
if (this.pendingOverwrites.length === 0) return;
this.currentOverwrite = this.pendingOverwrites.shift();
this.overwriteMsg = `"${this.currentOverwrite.title}" already exists at ${this.currentOverwrite.dest}`;
document.getElementById('overwrite-modal').showModal();
async confirmOverwrite() {
document.getElementById('overwrite-modal').close();
const res = await fetch('/export/overwrite', {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({album_id: albumId, path: this.conflictPath}),
});
const data = await res.json();
if (data.conflict) {
this.conflictPath = data.path;
this.overwriteMsg = `Destination already exists: ${data.path}`;
document.getElementById('overwrite-modal').showModal();
} else if (data.ok) {
this.successMsg = `Exported ${data.exported} entr${data.exported === 1 ? 'y' : 'ies'} successfully.`;
this.failedCount = (data.failed || []).length;
}
},
confirmOverwrite() {
cancelExport() {
document.getElementById('overwrite-modal').close();
this.confirmedIds.push(this.currentOverwrite.group_id);
this.runExport();
},
skipOverwrite() {
document.getElementById('overwrite-modal').close();
this.results.push({...this.currentOverwrite, skipped: true});
this.showNextOverwrite();
this.conflictPath = null;
},
};
}