summaryrefslogtreecommitdiffstats
path: root/views/admin
diff options
context:
space:
mode:
Diffstat (limited to 'views/admin')
-rw-r--r--views/admin/album.erb122
1 files changed, 121 insertions, 1 deletions
diff --git a/views/admin/album.erb b/views/admin/album.erb
index 15a043f..14a8f07 100644
--- a/views/admin/album.erb
+++ b/views/admin/album.erb
@@ -140,6 +140,9 @@
<section class="admin-update">
<h2>Update</h2>
<p class="update-hint">Scans this album for new/removed files, extracts EXIF data, and generates missing thumbnails.</p>
+ <label class="update-force-label">
+ <input type="checkbox" id="update-force"> Force rescan all (slow)
+ </label>
<button id="update-btn" class="btn" onclick="startUpdate()">Run Update</button>
<div id="update-panel" class="update-panel hidden">
<div id="update-status" class="update-status"></div>
@@ -147,6 +150,31 @@
</div>
</section>
+ <section class="admin-upload">
+ <h2>Upload</h2>
+ <p class="update-hint">Add photos or videos. To create a new sub-album, fill in a name below. Up to 2 GB per file (reverse proxy may impose a lower limit).</p>
+ <div class="form-row" style="max-width:380px">
+ <label>New sub-album name <small style="color:var(--text-dim)">(optional — leave blank to add to this album)</small>
+ <input type="text" id="upload-subalbum" placeholder="e.g. 2024-12-Hawaii" autocomplete="off">
+ </label>
+ </div>
+ <div class="upload-file-row">
+ <input type="file" id="upload-files" multiple
+ accept="image/*,video/*,audio/*,.heic,.heif,.flac,.wav,.m4a,.aac"
+ style="display:none">
+ <button type="button" class="btn" onclick="document.getElementById('upload-files').click()">Choose Files</button>
+ <span id="upload-file-count" class="upload-file-count">No files chosen</span>
+ <button type="button" id="upload-btn" class="btn" onclick="startUpload()" disabled>Upload</button>
+ </div>
+ <div id="upload-panel" class="upload-panel hidden">
+ <div class="upload-progress-wrap">
+ <div id="upload-progress-bar" class="upload-progress-bar"></div>
+ </div>
+ <div id="upload-status" class="update-status"></div>
+ <pre id="upload-log" class="update-log"></pre>
+ </div>
+ </section>
+
<script>
async function startUpdate() {
const btn = document.getElementById('update-btn');
@@ -162,10 +190,11 @@
status.className = 'update-status running';
panel.classList.remove('hidden');
+ const force = document.getElementById('update-force').checked;
const res = await fetch('/admin/update', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
- body: new URLSearchParams({ rel })
+ body: new URLSearchParams({ rel, force: force ? '1' : '' })
});
const { job_id } = await res.json();
@@ -193,5 +222,96 @@
}
}, 1500);
}
+ document.getElementById('upload-files').addEventListener('change', function () {
+ const n = this.files.length;
+ document.getElementById('upload-file-count').textContent =
+ n > 0 ? `${n} file${n !== 1 ? 's' : ''} chosen` : 'No files chosen';
+ document.getElementById('upload-btn').disabled = n === 0;
+ });
+
+ function startUpload() {
+ const fileInput = document.getElementById('upload-files');
+ if (fileInput.files.length === 0) return;
+
+ const subAlbum = document.getElementById('upload-subalbum').value.trim();
+ const btn = document.getElementById('upload-btn');
+ const panel = document.getElementById('upload-panel');
+ const progressBar = document.getElementById('upload-progress-bar');
+ const status = document.getElementById('upload-status');
+ const log = document.getElementById('upload-log');
+ const rel = <%= @rel.to_json %>;
+
+ btn.disabled = true;
+ panel.classList.remove('hidden');
+ log.textContent = '';
+ progressBar.style.width = '0%';
+ status.textContent = 'Uploading…';
+ status.className = 'update-status running';
+
+ const fd = new FormData();
+ fd.append('rel', rel);
+ if (subAlbum) fd.append('new_album_name', subAlbum);
+ for (const f of fileInput.files) fd.append('files[]', f);
+
+ const xhr = new XMLHttpRequest();
+ xhr.upload.addEventListener('progress', (e) => {
+ if (e.lengthComputable) {
+ progressBar.style.width = Math.round(e.loaded / e.total * 100) + '%';
+ }
+ });
+ xhr.addEventListener('load', () => {
+ progressBar.style.width = '100%';
+ if (xhr.status !== 200) {
+ status.textContent = 'Upload failed ✗';
+ status.className = 'update-status error';
+ btn.disabled = false;
+ return;
+ }
+ let resp;
+ try { resp = JSON.parse(xhr.responseText); } catch (e) {
+ status.textContent = 'Upload failed ✗';
+ status.className = 'update-status error';
+ btn.disabled = false;
+ return;
+ }
+ const saved = resp.saved;
+ status.textContent = `${saved} file${saved !== 1 ? 's' : ''} saved — running update…`;
+ pollUploadJob(resp.job_id, resp.album_rel, btn, status, log);
+ });
+ xhr.addEventListener('error', () => {
+ status.textContent = 'Network error ✗';
+ status.className = 'update-status error';
+ btn.disabled = false;
+ });
+ xhr.open('POST', '/admin/upload');
+ xhr.send(fd);
+ }
+
+ function pollUploadJob(jobId, albumRel, btn, status, log) {
+ let seen = 0;
+ const poll = setInterval(async () => {
+ const r = await fetch('/admin/update/' + jobId);
+ const data = await r.json();
+ const fresh = data.lines.slice(seen);
+ if (fresh.length) {
+ log.textContent += fresh.join('\n') + '\n';
+ log.scrollTop = log.scrollHeight;
+ seen = data.lines.length;
+ }
+ if (data.status !== 'running') {
+ clearInterval(poll);
+ btn.disabled = false;
+ if (data.status === 'done') {
+ status.innerHTML =
+ `Done ✓ &nbsp;<a href="/admin/edit/${albumRel}" class="btn btn-sm">Edit album</a>` +
+ ` <a href="/browse/${albumRel}" class="btn btn-sm">View</a>`;
+ status.className = 'update-status done';
+ } else {
+ status.textContent = 'Error ✗';
+ status.className = 'update-status error';
+ }
+ }
+ }, 1500);
+ }
</script>
</div>