feat: Phase 6 export — writes Grav entry folders from Immich originals
Implements GET /export summary view and POST /export/run which downloads originals from Immich, writes entry.md with YAML frontmatter, and sets group status to exported. Includes POST /export/overwrite for single-group re-export. All 42 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="p-6 max-w-3xl mx-auto" x-data="exportApp('{{ album_id }}')">
|
||||
<h1 class="text-2xl font-bold mb-4">Export</h1>
|
||||
<div class="stats shadow mb-6">
|
||||
<div class="stat"><div class="stat-title">Ready to export</div>
|
||||
<div class="stat-value text-primary">{{ to_export | length }}</div></div>
|
||||
<div class="stat"><div class="stat-title">Skipped</div>
|
||||
<div class="stat-value opacity-40">{{ skipped | length }}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 mb-6">
|
||||
{% for group in to_export %}
|
||||
<div class="export-item card card-compact bg-base-100 shadow">
|
||||
<div class="card-body">
|
||||
<p class="font-semibold">{{ group.title }}</p>
|
||||
<p class="text-xs opacity-60">{{ group.date }} · {{ group.entry_type }} · {{ group.photo_ids | length }} photos</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button id="export-btn" class="btn btn-primary" @click="runExport()">
|
||||
Export {{ to_export | length }} entries
|
||||
</button>
|
||||
|
||||
<!-- Overwrite confirmation modal -->
|
||||
<dialog id="overwrite-modal" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold">Destination exists</h3>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<details class="mt-6">
|
||||
<summary class="cursor-pointer text-sm opacity-60 skipped-list">
|
||||
Skipped ({{ skipped | length }}) — not exported
|
||||
</summary>
|
||||
<ul class="mt-2 space-y-1 text-sm opacity-60">
|
||||
{% for g in skipped %}<li>{{ g.title or g.date }}</li>{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
function exportApp(albumId) {
|
||||
return {
|
||||
results: [],
|
||||
pendingOverwrites: [],
|
||||
currentOverwrite: null,
|
||||
overwriteMsg: '',
|
||||
confirmedIds: [],
|
||||
|
||||
async runExport(extraOverwrites = []) {
|
||||
const res = await fetch('/export/run', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({album_id: albumId, overwrite_ids: [...this.confirmedIds, ...extraOverwrites]}),
|
||||
});
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
confirmOverwrite() {
|
||||
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();
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user