diff options
| author | Ken D'Ambrosio <ken@jots.org> | 2026-06-12 13:57:11 +0000 |
|---|---|---|
| committer | Ken D'Ambrosio <ken@jots.org> | 2026-06-12 13:57:11 +0000 |
| commit | b9a3ce6942e917c8e5046d652b7742cfe5f960ec (patch) | |
| tree | 2d57c8205f085a16c3ea86d5339f4dbcae028a4d /views | |
| parent | a942b4e83d8c3c71020fdc6ae93954ddfa2ea338 (diff) | |
Add bulk photo selection panel to person page (admin)
On /people/:slug, admins see a sticky left panel and selectable photo
tiles. Clicking a tile (or its checkbox overlay) toggles selection;
clicking without modifier still opens the photo in a new tab. The panel
shows the selection count and two actions:
- Reassign to person: moves the selected photos' face entries from the
current person's cluster to the chosen person.
- Move to album: moves the photo files on disk and updates album.json,
faces.json, and people.json rel paths accordingly. Album paths are
offered via a datalist autocomplete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'views')
| -rw-r--r-- | views/person.erb | 129 |
1 files changed, 122 insertions, 7 deletions
diff --git a/views/person.erb b/views/person.erb index 1bb25e2..b64dcbd 100644 --- a/views/person.erb +++ b/views/person.erb @@ -1,3 +1,57 @@ +<% if admin? %> +<div class="person-page-wrap"> + <aside class="bulk-panel" id="bulk-panel"> + <h2 class="bulk-panel-title">Selection</h2> + <p class="bulk-panel-hint" id="bulk-hint">Click photos to select them.</p> + <p class="bulk-selected-count" id="bulk-count" style="display:none"> + <strong id="bulk-n">0</strong> selected + <a href="#" id="bulk-clear" style="font-size:.8rem">Clear</a> + </p> + + <div id="bulk-actions" style="display:none"> + <hr class="bulk-sep"> + + <% unless @named_people.empty? %> + <form id="reassign-form" method="post" + action="/admin/people/<%= ERB::Util.url_encode(@person_slug) %>/bulk_reassign"> + <label class="bulk-label">Reassign to person</label> + <select name="to_uuid" class="bulk-select" required> + <option value="">— Select person —</option> + <% @named_people.each do |p| %> + <option value="<%= ERB::Util.html_escape(p[:uuid]) %>"><%= ERB::Util.html_escape(p[:name]) %></option> + <% end %> + </select> + <div id="reassign-rels"></div> + <button type="submit" class="btn btn-sm bulk-action-btn" + onclick="return populateRels('reassign-rels') && confirm('Reassign ' + selectedRels.size + ' photo(s)?')"> + Reassign + </button> + </form> + <% end %> + + <form id="move-form" method="post" action="/admin/photos/move_album" + style="margin-top:16px"> + <input type="hidden" name="return_to" value="/people/<%= ERB::Util.url_encode(@person_slug) %>"> + <label class="bulk-label">Move to album</label> + <input type="text" name="dst_rel" list="album-dirs" class="bulk-input" + placeholder="album/path" autocomplete="off"> + <datalist id="album-dirs"> + <% (@album_dirs || []).each do |d| %> + <option value="<%= ERB::Util.html_escape(d) %>"> + <% end %> + </datalist> + <div id="move-rels"></div> + <button type="submit" class="btn btn-sm bulk-action-btn" + onclick="return populateRels('move-rels') && confirm('Move ' + selectedRels.size + ' photo(s) to this album?')"> + Move + </button> + </form> + </div> + </aside> + + <div class="person-page-main"> +<% end %> + <div class="album-header"> <div class="breadcrumbs"> <a href="/people">People</a> @@ -11,16 +65,77 @@ <% if @photos.empty? %> <p class="empty-album">No photos found.</p> <% else %> - <div class="grid"> + <div class="grid" id="photo-grid"> <% @photos.each do |photo| %> <% album_url = photo[:dir_rel].empty? ? '/browse/' : "/browse/#{ERB::Util.html_escape(photo[:dir_rel])}" %> - <a href="<%= album_url %>?photo=<%= ERB::Util.url_encode(photo[:filename]) %>" - class="card" style="text-decoration:none"> - <div class="thumb-wrap"> - <img src="/thumb/<%= ERB::Util.html_escape(photo[:rel]) %>" - loading="lazy" alt="<%= ERB::Util.html_escape(photo[:filename]) %>"> + <% if admin? %> + <div class="card selectable-card" data-rel="<%= ERB::Util.html_escape(photo[:rel]) %>" + data-href="<%= album_url %>?photo=<%= ERB::Util.url_encode(photo[:filename]) %>"> + <div class="thumb-wrap"> + <img src="/thumb/<%= ERB::Util.html_escape(photo[:rel]) %>" + loading="lazy" alt="<%= ERB::Util.html_escape(photo[:filename]) %>"> + <span class="select-checkbox" aria-hidden="true"></span> + </div> </div> - </a> + <% else %> + <a href="<%= album_url %>?photo=<%= ERB::Util.url_encode(photo[:filename]) %>" + class="card" style="text-decoration:none"> + <div class="thumb-wrap"> + <img src="/thumb/<%= ERB::Util.html_escape(photo[:rel]) %>" + loading="lazy" alt="<%= ERB::Util.html_escape(photo[:filename]) %>"> + </div> + </a> + <% end %> <% end %> </div> <% end %> + +<% if admin? %> + </div><%# .person-page-main %> +</div><%# .person-page-wrap %> + +<script> +var selectedRels = new Set(); + +function updatePanel() { + var n = selectedRels.size; + document.getElementById('bulk-n').textContent = n; + document.getElementById('bulk-count').style.display = n ? '' : 'none'; + document.getElementById('bulk-hint').style.display = n ? 'none' : ''; + document.getElementById('bulk-actions').style.display = n ? '' : 'none'; +} + +function populateRels(containerId) { + if (!selectedRels.size) { alert('No photos selected.'); return false; } + var c = document.getElementById(containerId); + c.innerHTML = ''; + selectedRels.forEach(function(rel) { + var inp = document.createElement('input'); + inp.type = 'hidden'; inp.name = 'rels[]'; inp.value = rel; + c.appendChild(inp); + }); + return true; +} + +document.querySelectorAll('.selectable-card').forEach(function(card) { + card.addEventListener('click', function(e) { + if (e.target.closest('.select-checkbox') || e.shiftKey || e.ctrlKey || e.metaKey) { + e.preventDefault(); + var rel = card.dataset.rel; + if (selectedRels.has(rel)) { selectedRels.delete(rel); card.classList.remove('selected'); } + else { selectedRels.add(rel); card.classList.add('selected'); } + updatePanel(); + } else { + window.open(card.dataset.href, '_blank'); + } + }); +}); + +document.getElementById('bulk-clear').addEventListener('click', function(e) { + e.preventDefault(); + selectedRels.clear(); + document.querySelectorAll('.selectable-card.selected').forEach(function(c) { c.classList.remove('selected'); }); + updatePanel(); +}); +</script> +<% end %> |
