diff options
| author | Ken <ken@jots.org> | 2026-05-09 04:41:03 +0000 |
|---|---|---|
| committer | Ken <ken@jots.org> | 2026-05-09 04:41:03 +0000 |
| commit | c75beda743dfd6af63f512e928d0889d9ead3973 (patch) | |
| tree | bed91fd4f9d36a905be0b1ef990457a1e37e567b /views | |
Initial commit — Albumen photo album
Ruby/Sinatra self-hosted photo album with directory hierarchy,
per-photo captions and visibility, lightbox, slideshow, admin UI,
and Let's Encrypt HTTPS via Apache reverse proxy on prouter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'views')
| -rw-r--r-- | views/admin/album.erb | 101 | ||||
| -rw-r--r-- | views/admin/login.erb | 14 | ||||
| -rw-r--r-- | views/album.erb | 99 | ||||
| -rw-r--r-- | views/layout.erb | 25 | ||||
| -rw-r--r-- | views/slideshow.erb | 41 |
5 files changed, 280 insertions, 0 deletions
diff --git a/views/admin/album.erb b/views/admin/album.erb new file mode 100644 index 0000000..f6b80d6 --- /dev/null +++ b/views/admin/album.erb @@ -0,0 +1,101 @@ +<div class="admin-album"> + <div class="admin-nav"> + <% if @rel.empty? %> + <a href="/browse/" class="btn btn-sm">View Root</a> + <% else %> + <% parent = @rel.include?('/') ? @rel.split('/')[0..-2].join('/') : '' %> + <a href="/admin/edit/<%= parent %>" class="btn btn-sm">← Parent</a> + <a href="/browse/<%= @rel %>" class="btn btn-sm">View Album</a> + <% end %> + </div> + + <h1>Edit: <%= @title %></h1> + + <form method="post" action="/admin/edit/<%= @rel %>"> + <fieldset class="album-settings"> + <legend>Album</legend> + <div class="form-row"> + <label>Title override + <input type="text" name="album_title" value="<%= @data['title'] %>" placeholder="(use directory name)"> + </label> + </div> + <div class="form-row"> + <label>Description + <textarea name="album_description" rows="2"><%= @data['description'] %></textarea> + </label> + </div> + <div class="form-row form-row-inline"> + <label>Cover image + <select name="album_cover"> + <option value="">— auto (first image) —</option> + <% @files.each do |name| %> + <option value="<%= name %>"<%= ' selected' if @data['cover'] == name %>><%= name %></option> + <% end %> + </select> + </label> + <label class="checkbox-label"> + <input type="checkbox" name="album_cover_dynamic" value="1"<%= ' checked' if @data['cover_dynamic'] %>> + Dynamic cover + </label> + <label class="checkbox-label"> + <input type="checkbox" name="album_sort_reverse" value="1"<%= ' checked' if @data['sort_reverse'] %>> + Reverse sub-album order + </label> + <label class="checkbox-label"> + <input type="hidden" name="album_visible" value="0"> + <input type="checkbox" name="album_visible" value="1"<%= ' checked' if @data['visible'] != false %>> + Visible + </label> + </div> + </fieldset> + + <% unless @files.empty? %> + <div class="files-section"> + <h2>Files</h2> + <table class="files-table"> + <thead> + <tr> + <th>Thumb</th> + <th>Filename</th> + <th>Title</th> + <th>Caption</th> + <th>Visible</th> + </tr> + </thead> + <tbody> + <% @files.each do |name| %> + <% meta = (@data['files'] || {})[name] || {} %> + <% file_rel = @rel.empty? ? name : "#{@rel}/#{name}" %> + <tr> + <td><img src="/thumb/<%= file_rel %>" width="60" height="60" loading="lazy" style="object-fit:cover"></td> + <td class="filename"><code><%= name %></code></td> + <td><input type="text" name="file_title[<%= name %>]" value="<%= ERB::Util.html_escape(meta['title'].to_s) %>" placeholder="<%= ERB::Util.html_escape(name) %>"></td> + <td><input type="text" name="file_caption[<%= name %>]" value="<%= ERB::Util.html_escape(meta['caption'].to_s) %>" placeholder="caption…"></td> + <td class="visible-cell"> + <input type="hidden" name="file_visible[<%= name %>]" value="0"> + <input type="checkbox" name="file_visible[<%= name %>]" value="1"<%= ' checked' if meta['visible'] != false %>> + </td> + </tr> + <% end %> + </tbody> + </table> + </div> + <% end %> + + <div class="form-actions"> + <button type="submit" class="btn">Save</button> + </div> + </form> + + <% unless @sub_dirs.empty? %> + <section class="sub-albums-section"> + <h2>Sub-albums</h2> + <ul class="sub-album-list"> + <% @sub_dirs.each do |name| %> + <% sub_rel = @rel.empty? ? name : "#{@rel}/#{name}" %> + <li><a href="/admin/edit/<%= sub_rel %>"><%= name %></a></li> + <% end %> + </ul> + </section> + <% end %> +</div> diff --git a/views/admin/login.erb b/views/admin/login.erb new file mode 100644 index 0000000..16f12d2 --- /dev/null +++ b/views/admin/login.erb @@ -0,0 +1,14 @@ +<div class="admin-login"> + <h1>Admin Login</h1> + <% if defined?(@error) && @error %> + <p class="form-error"><%= @error %></p> + <% end %> + <form method="post" action="/admin/login"> + <input type="hidden" name="return_to" value="<%= ERB::Util.html_escape(params['return_to'].to_s) %>"> + <label> + Password + <input type="password" name="password" autofocus autocomplete="current-password"> + </label> + <button type="submit" class="btn">Login</button> + </form> +</div> diff --git a/views/album.erb b/views/album.erb new file mode 100644 index 0000000..077e119 --- /dev/null +++ b/views/album.erb @@ -0,0 +1,99 @@ +<div class="album-header"> + <% unless @crumbs.empty? %> + <nav class="breadcrumbs"> + <a href="/browse/">Home</a> + <% @crumbs.each do |c| %> + <span class="sep">›</span> + <a href="/browse/<%= c[:path] %>"><%= c[:name] %></a> + <% end %> + </nav> + <% end %> + <h1><%= @title %></h1> + <% if @desc %><p class="album-desc"><%= @desc %></p><% end %> + <div class="album-actions"> + <% if @entries.any? { |e| %i[image video].include?(e[:type]) } %> + <a href="/slideshow/<%= @rel %>" class="btn">Slideshow</a> + <% end %> + </div> +</div> + +<% unless @albums.empty? %> +<section class="grid-section"> + <% if @entries.any? %><h2 class="section-label">Albums</h2><% end %> + <div class="grid"> + <% @albums.each do |a| %> + <% href = @rel.empty? ? "/browse/#{a[:name]}" : "/browse/#{@rel}/#{a[:name]}" %> + <a href="<%= href %>" class="card album-card"> + <div class="thumb-wrap"> + <% cover_rel = @rel.empty? ? "#{a[:name]}/#{a[:cover]}" : "#{@rel}/#{a[:name]}/#{a[:cover]}" %> + <% if a[:cover] %> + <img src="/thumb/<%= cover_rel %>" alt="<%= a[:title] %>" loading="lazy"> + <% else %> + <div class="thumb-placeholder">📁</div> + <% end %> + </div> + <div class="album-label"><%= a[:title] %></div> + </a> + <% end %> + </div> +</section> +<% end %> + +<% unless @entries.empty? %> +<section class="grid-section"> + <% if @albums.any? %><h2 class="section-label">Photos & Videos</h2><% end %> + <div class="grid" id="photo-grid"> + <% @entries.each_with_index do |e, i| %> + <% file_rel = @rel.empty? ? e[:name] : "#{@rel}/#{e[:name]}" %> + <div class="card media-card<%= ' hidden-item' unless e[:visible] %>" + data-index="<%= i %>" + data-type="<%= e[:type] %>" + data-src="/media/<%= file_rel %>" + data-title="<%= ERB::Util.html_escape(e[:title]) %>" + data-caption="<%= ERB::Util.html_escape(e[:caption].to_s) %>" + onclick="openLightbox(<%= i %>)"> + <div class="thumb-wrap"> + <img src="/thumb/<%= file_rel %>" alt="<%= ERB::Util.html_escape(e[:title]) %>" loading="lazy"> + <% if e[:type] == :video %><span class="type-badge video-badge">▶</span><% end %> + <% if e[:type] == :audio %><span class="type-badge audio-badge">♪</span><% end %> + </div> + <% if e[:caption] %> + <div class="card-meta"><p class="card-caption"><%= e[:caption] %></p></div> + <% end %> + </div> + <% end %> + </div> +</section> +<% end %> + +<% if @albums.empty? && @entries.empty? %> + <p class="empty-album">This album is empty.</p> +<% end %> + +<div id="lightbox" class="lightbox hidden" role="dialog" aria-modal="true"> + <button class="lb-btn lb-close" onclick="closeLightbox()" aria-label="Close">✕</button> + <div class="lb-stage" onclick="closeLightbox()"> + <div class="lb-media" onclick="event.stopPropagation()"> + <img id="lb-img" src="" alt="" class="hidden"> + <video id="lb-video" controls class="hidden"></video> + <audio id="lb-audio" controls class="hidden"></audio> + <button class="lb-btn lb-prev" onclick="lbNav(-1)" aria-label="Previous">‹</button> + <button class="lb-btn lb-next" onclick="lbNav(1)" aria-label="Next">›</button> + </div> + </div> + <div class="lb-caption-bar"> + <span id="lb-title"></span> + <span id="lb-caption"></span> + <a id="lb-download" href="" download class="btn btn-sm lb-action">↓ Original</a> + <button id="lb-copylink" onclick="lbCopyLink()" class="btn btn-sm lb-action">⧉ Copy link</button> + <span id="lb-counter"></span> + </div> +</div> + +<script> +const ENTRIES = <%= @entries.map { |e| + file_rel = @rel.empty? ? e[:name] : "#{@rel}/#{e[:name]}" + e.merge(src: "/media/#{file_rel}") +}.to_json %>; +</script> +<script src="/js/album.js"></script> diff --git a/views/layout.erb b/views/layout.erb new file mode 100644 index 0000000..3e798c4 --- /dev/null +++ b/views/layout.erb @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title><%= @title %> — Albumen</title> + <link rel="stylesheet" href="/css/style.css"> +</head> +<body> + <header class="site-header"> + <a href="/browse/" class="site-logo">Albumen</a> + <nav class="site-nav"> + <% if admin? %> + <a href="/admin/edit/<%= defined?(@rel) ? @rel : '' %>">Edit Album</a> + <a href="/admin/logout">Logout</a> + <% else %> + <a href="/admin/login" class="nav-admin">Admin</a> + <% end %> + </nav> + </header> + <main> + <%= yield %> + </main> +</body> +</html> diff --git a/views/slideshow.erb b/views/slideshow.erb new file mode 100644 index 0000000..847da65 --- /dev/null +++ b/views/slideshow.erb @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title><%= @title %> — Slideshow</title> + <link rel="stylesheet" href="/css/style.css"> +</head> +<body class="slideshow-page"> + <div id="slideshow"> + <div id="ss-stage"> + <img id="ss-img" src="" alt="" style="display:none"> + <video id="ss-video" playsinline style="display:none"></video> + <div id="ss-caption-bar"> + <span id="ss-title"></span> + <span id="ss-caption"></span> + </div> + </div> + <div id="ss-controls"> + <a href="/browse/<%= @rel %>" class="btn btn-sm">← Album</a> + <button onclick="ssPrev()" class="btn btn-sm">‹ Prev</button> + <button onclick="ssToggle()" id="ss-play-btn" class="btn btn-sm">⏸ Pause</button> + <button onclick="ssNext()" class="btn btn-sm">Next ›</button> + <label class="ss-interval-label"> + Interval + <input type="number" id="ss-interval" value="4" min="1" max="60" step="1"> s + </label> + </div> + <div id="ss-counter"></div> + </div> + + <script> + const SS_ENTRIES = <%= @entries.map { |e| + file_rel = @rel.empty? ? e[:name] : "#{@rel}/#{e[:name]}" + e.merge(src: "/media/#{file_rel}") + }.to_json %>; + const SS_REL = <%= @rel.to_json %>; + </script> + <script src="/js/slideshow.js"></script> +</body> +</html> |
