diff options
Diffstat (limited to 'public')
| -rw-r--r-- | public/css/style.css | 351 | ||||
| -rw-r--r-- | public/js/album.js | 111 | ||||
| -rw-r--r-- | public/js/slideshow.js | 113 |
3 files changed, 575 insertions, 0 deletions
diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..1f31e42 --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,351 @@ +/* ── Reset & base ─────────────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +:root { + --bg: #111; + --bg2: #1c1c1c; + --bg3: #2a2a2a; + --border: #333; + --text: #e0e0e0; + --text-dim: #888; + --accent: #4a90d9; + --accent-hv: #6aaff7; + --radius: 6px; + --thumb: 220px; + --gap: 12px; + --font: system-ui, -apple-system, sans-serif; +} + +body { + background: var(--bg); + color: var(--text); + font-family: var(--font); + font-size: 15px; + line-height: 1.5; + min-height: 100vh; +} + +a { color: var(--accent); text-decoration: none; } +a:hover { color: var(--accent-hv); text-decoration: underline; } + +/* ── Site header ───────────────────────────────────────────────────────── */ +.site-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 24px; + background: var(--bg2); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 100; +} + +.site-logo { + font-size: 1.2rem; + font-weight: 700; + letter-spacing: .04em; + color: var(--text); +} +.site-logo:hover { color: var(--accent-hv); text-decoration: none; } + +.site-nav { display: flex; gap: 16px; align-items: center; } +.site-nav a { color: var(--text-dim); font-size: .9rem; } +.site-nav a:hover { color: var(--text); text-decoration: none; } + +/* ── Buttons ───────────────────────────────────────────────────────────── */ +.btn { + display: inline-block; + padding: 6px 16px; + background: var(--bg3); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + cursor: pointer; + font-size: .9rem; + transition: background .15s; +} +.btn:hover { background: var(--accent); border-color: var(--accent); color: #fff; text-decoration: none; } +.btn-sm { padding: 4px 10px; font-size: .8rem; } + +/* ── Main content ──────────────────────────────────────────────────────── */ +main { max-width: 1400px; margin: 0 auto; padding: 24px; } + +/* ── Album header ──────────────────────────────────────────────────────── */ +.album-header { margin-bottom: 24px; } +.album-header h1 { font-size: 1.6rem; font-weight: 600; margin-bottom: 6px; } + +.breadcrumbs { font-size: .85rem; color: var(--text-dim); margin-bottom: 8px; } +.breadcrumbs a { color: var(--text-dim); } +.breadcrumbs a:hover { color: var(--text); } +.breadcrumbs .sep { margin: 0 4px; } + +.album-desc { color: var(--text-dim); margin-bottom: 10px; } +.album-actions { margin-top: 10px; } + +/* ── Grid ──────────────────────────────────────────────────────────────── */ +.grid-section { margin-bottom: 32px; } +.section-label { font-size: .85rem; text-transform: uppercase; letter-spacing: .08em; color: var(--text-dim); margin-bottom: 12px; } + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(var(--thumb), 1fr)); + gap: var(--gap); +} + +/* ── Cards ─────────────────────────────────────────────────────────────── */ +.card { + border-radius: var(--radius); + overflow: hidden; + background: var(--bg2); + cursor: pointer; + transition: transform .15s, box-shadow .15s; +} +.card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,.5); } + +.thumb-wrap { + position: relative; + width: 100%; + aspect-ratio: 1; + overflow: hidden; + background: var(--bg3); +} +.thumb-wrap img { + width: 100%; height: 100%; + object-fit: cover; + display: block; + transition: transform .2s; +} +.card:hover .thumb-wrap img { transform: scale(1.04); } + +.thumb-placeholder { + width: 100%; height: 100%; + display: flex; align-items: center; justify-content: center; + font-size: 3rem; + background: var(--bg3); +} + +.card-overlay { + position: absolute; + bottom: 0; left: 0; right: 0; + padding: 24px 8px 8px; + background: linear-gradient(transparent, rgba(0,0,0,.7)); + opacity: 0; + transition: opacity .2s; +} +.card:hover .card-overlay { opacity: 1; } +.card-label { color: #fff; font-size: .85rem; font-weight: 500; } + +.type-badge { + position: absolute; + top: 6px; right: 6px; + background: rgba(0,0,0,.6); + border-radius: 4px; + padding: 2px 6px; + font-size: .8rem; + color: #fff; + pointer-events: none; +} + +.album-label { padding: 6px 8px; font-size: .88rem; font-weight: 500; color: var(--text); } +.card-meta { padding: 6px 8px; } +.card-caption { font-size: .8rem; color: var(--text-dim); } + +.hidden-item { opacity: .45; outline: 1px dashed var(--border); } +.empty-album { color: var(--text-dim); text-align: center; padding: 60px 0; } + +/* ── Lightbox ──────────────────────────────────────────────────────────── */ +.lightbox { + position: fixed; inset: 0; + z-index: 1000; + background: rgba(0,0,0,.92); + display: flex; flex-direction: column; + align-items: center; justify-content: center; +} +.lightbox.hidden { display: none; } + +.lb-stage { + flex: 1; + display: flex; align-items: center; justify-content: center; + width: 100%; + cursor: zoom-out; + overflow: hidden; + padding: 16px; +} +/* inline-block shrink-wraps to the rendered image size; + arrows inside are then anchored to the photo's actual edges */ +.lb-media { + position: relative; + display: inline-block; + line-height: 0; + cursor: default; +} +#lb-img { max-width: calc(100vw - 32px); max-height: calc(100vh - 130px); object-fit: contain; display: block; } +#lb-video { max-width: calc(100vw - 32px); max-height: calc(100vh - 130px); display: block; } +#lb-audio { width: 400px; max-width: 90vw; display: block; } + +.lb-caption-bar { + padding: 8px 80px; + text-align: center; + font-size: .9rem; + color: var(--text-dim); + display: flex; gap: 16px; justify-content: center; align-items: baseline; + flex-shrink: 0; + width: 100%; +} +#lb-title { color: var(--text); font-weight: 500; } +#lb-counter { margin-left: auto; font-size: .8rem; } +.lb-action { font-size: .78rem; padding: 3px 10px; opacity: .7; } +.lb-action:hover { opacity: 1; } + +.lb-btn { + position: absolute; + background: rgba(0,0,0,.45); + border: none; + color: #fff; + cursor: pointer; + border-radius: var(--radius); + transition: background .15s, opacity .15s; + z-index: 10; +} +.lb-btn:hover { background: rgba(0,0,0,.75); } + +/* Close stays fixed to the viewport corner */ +.lb-close { + position: fixed; + top: 12px; right: 16px; + font-size: 1.1rem; + padding: 6px 12px; + z-index: 1001; +} + +/* Prev/next are anchored to the photo's own edges */ +.lb-prev, .lb-next { + top: 50%; transform: translateY(-50%); + font-size: 2.8rem; + padding: 20px 12px; + opacity: 0; + transition: opacity .2s, background .15s; +} +.lb-media:hover .lb-prev, +.lb-media:hover .lb-next { opacity: .85; } +.lb-prev:hover, .lb-next:hover { opacity: 1 !important; } +.lb-prev { left: 0; border-radius: 0 var(--radius) var(--radius) 0; } +.lb-next { right: 0; border-radius: var(--radius) 0 0 var(--radius); } + +/* ── Slideshow ─────────────────────────────────────────────────────────── */ +.slideshow-page { background: #000; overflow: hidden; height: 100vh; display: flex; } + +#slideshow { + width: 100%; height: 100vh; + display: flex; flex-direction: column; + background: #000; +} +#ss-stage { + flex: 1; + display: flex; align-items: center; justify-content: center; + overflow: hidden; + position: relative; +} +#ss-img, #ss-video { + max-width: 100%; max-height: 100%; + object-fit: contain; + opacity: 1; + transition: opacity .5s ease; +} +#ss-img.fading, #ss-video.fading { opacity: 0; } + +#ss-caption-bar { + position: absolute; + bottom: 0; left: 0; right: 0; + padding: 32px 24px 8px; + background: linear-gradient(transparent, rgba(0,0,0,.7)); + color: #fff; + text-align: center; + font-size: .95rem; + pointer-events: none; +} +#ss-title { font-weight: 600; display: block; } + +#ss-controls { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 20px; + background: rgba(0,0,0,.7); + flex-shrink: 0; +} +.ss-interval-label { color: var(--text-dim); font-size: .85rem; display: flex; align-items: center; gap: 4px; margin-left: auto; } +.ss-interval-label input { width: 48px; background: var(--bg3); border: 1px solid var(--border); color: var(--text); border-radius: var(--radius); padding: 2px 6px; } + +#ss-counter { position: absolute; top: 12px; right: 16px; color: rgba(255,255,255,.5); font-size: .8rem; pointer-events: none; } + +/* ── Admin ─────────────────────────────────────────────────────────────── */ +.admin-login { + max-width: 360px; margin: 80px auto; + padding: 32px; + background: var(--bg2); + border: 1px solid var(--border); + border-radius: var(--radius); +} +.admin-login h1 { margin-bottom: 20px; font-size: 1.3rem; } +.admin-login label { display: block; margin-bottom: 16px; color: var(--text-dim); font-size: .9rem; } +.admin-login input { display: block; width: 100%; margin-top: 4px; padding: 8px 12px; background: var(--bg3); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-size: 1rem; } +.form-error { color: #f55; margin-bottom: 12px; font-size: .9rem; } + +.admin-album { max-width: 1200px; } +.admin-album h1 { font-size: 1.4rem; margin: 16px 0; } +.admin-nav { display: flex; gap: 8px; margin-bottom: 8px; } + +fieldset.album-settings { + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px; + margin-bottom: 24px; +} +legend { padding: 0 8px; color: var(--text-dim); font-size: .85rem; } + +.form-row { margin-bottom: 12px; } +.form-row label { display: block; color: var(--text-dim); font-size: .85rem; margin-bottom: 4px; } +.form-row input[type=text], .form-row textarea, .form-row select { + width: 100%; + background: var(--bg3); border: 1px solid var(--border); + border-radius: var(--radius); color: var(--text); + padding: 7px 10px; font-size: .9rem; +} +.form-row textarea { resize: vertical; } +.form-row-inline { display: flex; gap: 20px; align-items: flex-end; flex-wrap: wrap; } +.form-row-inline label { flex: 1; min-width: 180px; } +.checkbox-label { display: flex !important; align-items: center; gap: 6px; flex: 0 0 auto !important; color: var(--text) !important; } +.checkbox-label input { width: auto !important; } + +.files-section h2 { font-size: 1rem; color: var(--text-dim); margin-bottom: 10px; } +.files-table { width: 100%; border-collapse: collapse; font-size: .88rem; } +.files-table th { text-align: left; padding: 6px 10px; border-bottom: 2px solid var(--border); color: var(--text-dim); font-weight: 500; } +.files-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); vertical-align: middle; } +.files-table tr:hover td { background: var(--bg2); } +.filename { color: var(--text-dim); font-size: .8rem; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.visible-cell { text-align: center; } +.files-table input[type=text] { + width: 100%; + background: var(--bg3); border: 1px solid transparent; + border-radius: 4px; color: var(--text); + padding: 4px 8px; font-size: .85rem; +} +.files-table input[type=text]:focus { border-color: var(--accent); outline: none; } + +.form-actions { margin-top: 20px; } +.sub-albums-section { margin-top: 32px; } +.sub-albums-section h2 { font-size: 1rem; color: var(--text-dim); margin-bottom: 10px; } +.sub-album-list { list-style: none; display: flex; flex-wrap: wrap; gap: 8px; } +.sub-album-list a { padding: 4px 12px; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); font-size: .9rem; } + +/* ── Responsive ────────────────────────────────────────────────────────── */ +@media (max-width: 600px) { + :root { --thumb: 140px; --gap: 8px; } + main { padding: 12px; } + .lb-prev { left: 4px; } + .lb-next { right: 4px; } + .lb-stage { padding: 48px 40px; } + .files-table { font-size: .78rem; } +} diff --git a/public/js/album.js b/public/js/album.js new file mode 100644 index 0000000..74d524c --- /dev/null +++ b/public/js/album.js @@ -0,0 +1,111 @@ +'use strict'; + +let lbIndex = 0; + +function openLightbox(i) { + lbIndex = i; + renderLightbox(); + document.getElementById('lightbox').classList.remove('hidden'); + document.body.style.overflow = 'hidden'; + document.addEventListener('keydown', lbKey); +} + +function closeLightbox() { + document.getElementById('lightbox').classList.add('hidden'); + document.body.style.overflow = ''; + document.removeEventListener('keydown', lbKey); + ['lb-video', 'lb-audio'].forEach(id => { + const el = document.getElementById(id); + el.pause && el.pause(); + }); + history.replaceState(null, '', location.pathname + location.search); +} + +function lbNav(delta) { + let next = lbIndex + delta; + while (next >= 0 && next < ENTRIES.length && !ENTRIES[next].type) next += delta; + if (next >= 0 && next < ENTRIES.length) { + lbIndex = next; + renderLightbox(); + } +} + +function renderLightbox() { + const e = ENTRIES[lbIndex]; + const img = document.getElementById('lb-img'); + const vid = document.getElementById('lb-video'); + const aud = document.getElementById('lb-audio'); + + [img, vid, aud].forEach(el => { el.classList.add('hidden'); el.pause && el.pause(); }); + + if (e.type === 'image') { + img.src = e.src; img.alt = e.title; + img.classList.remove('hidden'); + } else if (e.type === 'video') { + vid.src = e.src; + vid.classList.remove('hidden'); + vid.play().catch(() => {}); + } else if (e.type === 'audio') { + aud.src = e.src; + aud.classList.remove('hidden'); + aud.play().catch(() => {}); + } + + document.getElementById('lb-title').textContent = e.title !== e.name ? e.title : ''; + document.getElementById('lb-caption').textContent = e.caption || ''; + document.getElementById('lb-counter').textContent = `${lbIndex + 1} / ${ENTRIES.length}`; + + const dl = document.getElementById('lb-download'); + dl.href = e.src; + dl.download = e.name; + + // Update URL hash so the address bar is the shareable link + history.replaceState(null, '', location.pathname + location.search + '#photo=' + encodeURIComponent(e.name)); +} + +function lbCopyLink() { + navigator.clipboard.writeText(location.href).then(() => { + const btn = document.getElementById('lb-copylink'); + const orig = btn.textContent; + btn.textContent = '✓ Copied!'; + setTimeout(() => { btn.textContent = orig; }, 1800); + }).catch(() => { + // Fallback: select a temporary input + const tmp = document.createElement('input'); + tmp.value = location.href; + document.body.appendChild(tmp); + tmp.select(); + document.execCommand('copy'); + document.body.removeChild(tmp); + }); +} + +function lbKey(ev) { + if (ev.key === 'Escape') closeLightbox(); + else if (ev.key === 'ArrowLeft') lbNav(-1); + else if (ev.key === 'ArrowRight') lbNav(1); +} + +// Restore lightbox from URL hash on page load +window.addEventListener('DOMContentLoaded', () => { + const m = location.hash.match(/^#photo=(.+)$/); + if (m) { + const name = decodeURIComponent(m[1]); + const idx = ENTRIES.findIndex(e => e.name === name); + if (idx >= 0) openLightbox(idx); + } +}); + +// Touch swipe +(function () { + let startX = null; + const lb = document.getElementById('lightbox'); + if (!lb) return; + lb.addEventListener('touchstart', e => { startX = e.changedTouches[0].clientX; }, { passive: true }); + lb.addEventListener('touchend', e => { + if (startX === null) return; + const dx = e.changedTouches[0].clientX - startX; + if (Math.abs(dx) > 50) lbNav(dx < 0 ? 1 : -1); + startX = null; + }, { passive: true }); +})(); diff --git a/public/js/slideshow.js b/public/js/slideshow.js new file mode 100644 index 0000000..2260034 --- /dev/null +++ b/public/js/slideshow.js @@ -0,0 +1,113 @@ +'use strict'; + +let ssIdx = 0; +let ssTimer = null; +let ssPlaying = true; + +const img = document.getElementById('ss-img'); +const vid = document.getElementById('ss-video'); +const title = document.getElementById('ss-title'); +const cap = document.getElementById('ss-caption'); +const ctr = document.getElementById('ss-counter'); +const btn = document.getElementById('ss-play-btn'); + +function ssInterval() { + return (parseFloat(document.getElementById('ss-interval').value) || 4) * 1000; +} + +function ssShow(i, instant) { + const e = SS_ENTRIES[i]; + if (!e) return; + ssIdx = i; + + title.textContent = e.title !== e.name ? e.title : ''; + cap.textContent = e.caption || ''; + ctr.textContent = `${i + 1} / ${SS_ENTRIES.length}`; + + if (instant) { + // First load — show without transition + applyEntry(e); + return; + } + + if (e.type === 'video') { + // No preload possible for video; fade out then swap + crossFade(() => applyEntry(e)); + } else { + // Preload the image so the fade-in shows it immediately + const preload = new Image(); + preload.onload = preload.onerror = () => crossFade(() => applyEntry(e)); + preload.src = e.src; + } +} + +function applyEntry(e) { + if (e.type === 'video') { + img.style.display = 'none'; + vid.style.display = ''; + vid.src = e.src; + void vid.offsetWidth; // flush so transition fires + vid.classList.remove('fading'); + vid.play().catch(() => {}); + } else { + vid.pause && vid.pause(); + vid.style.display = 'none'; + img.style.display = ''; + img.src = e.src; + img.alt = e.title; + void img.offsetWidth; // flush so transition fires + img.classList.remove('fading'); + } +} + +function crossFade(cb) { + img.classList.add('fading'); + vid.classList.add('fading'); + setTimeout(cb, 500); // matches CSS transition duration +} + +function ssSchedule() { + clearTimeout(ssTimer); + if (ssPlaying && SS_ENTRIES.length > 1) { + ssTimer = setTimeout(() => { + ssShow((ssIdx + 1) % SS_ENTRIES.length); + ssSchedule(); + }, ssInterval()); + } +} + +function ssNext() { + ssShow((ssIdx + 1) % SS_ENTRIES.length); + if (ssPlaying) ssSchedule(); +} + +function ssPrev() { + ssShow((ssIdx - 1 + SS_ENTRIES.length) % SS_ENTRIES.length); + if (ssPlaying) ssSchedule(); +} + +function ssToggle() { + ssPlaying = !ssPlaying; + btn.textContent = ssPlaying ? '⏸ Pause' : '▶ Play'; + if (ssPlaying) ssSchedule(); else clearTimeout(ssTimer); +} + +document.addEventListener('keydown', e => { + if (e.key === 'ArrowRight') ssNext(); + if (e.key === 'ArrowLeft') ssPrev(); + if (e.key === ' ') { e.preventDefault(); ssToggle(); } +}); + +let swipeX = null; +document.addEventListener('touchstart', e => { swipeX = e.changedTouches[0].clientX; }, { passive: true }); +document.addEventListener('touchend', e => { + if (swipeX === null) return; + const dx = e.changedTouches[0].clientX - swipeX; + if (Math.abs(dx) > 50) (dx < 0 ? ssNext : ssPrev)(); + swipeX = null; +}, { passive: true }); + +if (SS_ENTRIES.length > 0) { + ssShow(0, true); + ssSchedule(); +} |
