b5c90a1e81
Add group.py route, phase4.html template, and supporting state changes. Photos are shown as a flat stream; clicking divider zones inserts entry-break boundaries that split photos into labelled groups. Labels persist via group_labels dict. Done materialises groups into state.groups and advances to write phase. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
3.9 KiB
HTML
100 lines
3.9 KiB
HTML
{% extends "base.html" %}
|
|
{% block content %}
|
|
<div class="p-4 max-w-3xl mx-auto" x-data="groupApp('{{ album_id }}')">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h1 class="text-xl font-bold">Group</h1>
|
|
<button id="done-btn" class="btn btn-primary btn-sm" @click="done()">Grouping done →</button>
|
|
</div>
|
|
|
|
<div class="space-y-1">
|
|
{% for grp in groups %}
|
|
<div class="group-block border border-base-300 rounded-lg p-2 space-y-1">
|
|
{% if grp.label %}
|
|
<div class="text-xs font-semibold opacity-70 px-1">{{ grp.label }}</div>
|
|
{% endif %}
|
|
{% for photo in grp.photos %}
|
|
<div class="stream-photo flex items-center gap-3 bg-base-100 rounded p-1"
|
|
data-order="{{ photo.order }}">
|
|
<img src="/proxy/thumb/{{ photo.id }}" class="w-16 h-16 object-cover rounded">
|
|
<span class="text-xs opacity-60">{{ photo.local_datetime[11:16] }}</span>
|
|
<span class="badge badge-xs {% if photo.tag == 'story' %}badge-info{% else %}badge-success{% endif %}">
|
|
{{ photo.tag }}
|
|
</span>
|
|
</div>
|
|
{% if not loop.last %}
|
|
<div class="divider-zone group relative h-4 flex items-center cursor-pointer"
|
|
data-after-order="{{ photo.order }}">
|
|
<div class="absolute inset-x-0 h-0.5 bg-base-300 group-hover:bg-primary transition"></div>
|
|
<button class="insert-divider-btn absolute left-1/2 -translate-x-1/2 btn btn-xs btn-primary opacity-0 group-hover:opacity-100 transition z-10"
|
|
@click="addDivider({{ photo.order }})">✂ cut here</button>
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{% if grp.divider_id %}
|
|
<div class="flex items-center gap-2 my-1 px-1">
|
|
<input class="group-label input input-sm input-bordered flex-1"
|
|
value="{{ grp.label }}"
|
|
placeholder="Label this entry…"
|
|
@change="setLabel('{{ grp.divider_id }}', $el.value)"
|
|
@keydown.enter="$el.blur()">
|
|
<button class="remove-divider-btn btn btn-xs btn-ghost opacity-60"
|
|
@click="removeDivider('{{ grp.divider_id }}')">✕</button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if not loop.last and not grp.divider_id %}
|
|
<div class="divider-zone group relative h-4 flex items-center cursor-pointer"
|
|
data-after-order="{{ grp.photos[-1].order }}">
|
|
<div class="absolute inset-x-0 h-0.5 bg-base-300 group-hover:bg-primary transition"></div>
|
|
<button class="insert-divider-btn absolute left-1/2 -translate-x-1/2 btn btn-xs btn-primary opacity-0 group-hover:opacity-100 transition z-10"
|
|
@click="addDivider({{ grp.photos[-1].order }})">✂ cut here</button>
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function groupApp(albumId) {
|
|
return {
|
|
async addDivider(afterOrder) {
|
|
await fetch('/group/divider', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({album_id: albumId, after_order: afterOrder})
|
|
});
|
|
window.location.reload();
|
|
},
|
|
async removeDivider(dividerId) {
|
|
await fetch('/group/remove-divider', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({album_id: albumId, divider_id: dividerId})
|
|
});
|
|
window.location.reload();
|
|
},
|
|
async setLabel(dividerId, label) {
|
|
await fetch('/group/label', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({album_id: albumId, divider_id: dividerId, label: label})
|
|
});
|
|
},
|
|
async done() {
|
|
var res = await fetch('/group/done', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({album_id: albumId})
|
|
});
|
|
var data = await res.json();
|
|
if (data.redirect) window.location = data.redirect;
|
|
},
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|