Redesign details view.

The details view is now implemented with css grid, in a full-window
no-scroll layout (except for small screens, where the design is still
sequential and scroll is used)

This incudes some cleanup of the markup and corresponding changes in
the admin script.
This commit is contained in:
Rasmus Kaj 2020-07-16 20:12:22 +02:00
parent 89a92c1738
commit 8ee8b809bc
6 changed files with 187 additions and 149 deletions

View File

@ -1,8 +1,8 @@
// Admin functionality for rphotos // Admin functionality for rphotos
(function (d) { (function (d) {
var details = d.querySelector('.details'); var details = d.querySelector('main.details'), p;
if (!details) { if (!details) {
return; return; // Admin is for single image only
} }
function rotate(event) { function rotate(event) {
@ -32,22 +32,29 @@
r.send("angle=" + angle + "&image=" + imgid) r.send("angle=" + angle + "&image=" + imgid)
} }
function tag_form(event, category) { function makeform(category) {
event.target.disabled = true; let oldform = p.querySelector('form');
var imgid = details.dataset.imgid; if (oldform) {
oldform.remove();
}
var f = d.createElement("form"); var f = d.createElement("form");
f.className = "admin " + category; f.className = "admin " + category;
f.action = "/adm/" + category; f.action = "/adm/" + category;
f.method = "post"; f.method = "post";
var i = d.createElement("input");
i.type="hidden";
i.name="image";
i.value = details.dataset.imgid;
f.appendChild(i);
return f;
}
function tag_form(event, category) {
//event.target.disabled = true; - FIXME?
var f = makeform(category);
var l = d.createElement("label"); var l = d.createElement("label");
l.innerHTML = event.target.title; l.innerHTML = event.target.title;
f.appendChild(l); f.appendChild(l);
var i = d.createElement("input"); var i = d.createElement("input");
i.type="hidden";
i.name="image";
i.value = imgid;
f.appendChild(i);
i = d.createElement("input");
i.type = "text"; i.type = "text";
i.autocomplete="off"; i.autocomplete="off";
i.tabindex="1"; i.tabindex="1";
@ -139,27 +146,18 @@
event.target.focus(); event.target.focus();
}; };
f.appendChild(c); f.appendChild(c);
meta.insertBefore(f, meta.querySelector('#map')); p.append(f);
i.focus(); i.focus();
} }
function grade_form(event) { function grade_form(event) {
event.target.disabled = true; //event.target.disabled = true; - FIXME?
var imgid = details.dataset.imgid;
var grade = details.dataset.grade; var grade = details.dataset.grade;
var f = d.createElement("form"); var f = makeform("grade");
f.className = "admin grade";
f.action = "/adm/grade";
f.method = "post";
var l = d.createElement("label"); var l = d.createElement("label");
l.innerHTML = event.target.title; l.innerHTML = event.target.title;
f.appendChild(l); f.appendChild(l);
var i = d.createElement("input"); var i = d.createElement("input");
i.type="hidden";
i.name="image";
i.value = imgid;
f.appendChild(i);
i = d.createElement("input");
i.type="range"; i.type="range";
i.name="grade"; i.name="grade";
if (grade) { if (grade) {
@ -199,23 +197,14 @@
e.stopPropagation(); e.stopPropagation();
return false; return false;
}); });
meta.insertBefore(f, meta.querySelector('#map')); p.append(f);
i.focus(); i.focus();
} }
function location_form(event) { function location_form(event) {
event.target.disabled = true; //event.target.disabled = true; - FIXME?
var imgid = details.dataset.imgid;
var position = details.dataset.position || localStorage.getItem('lastpos'); var position = details.dataset.position || localStorage.getItem('lastpos');
var f = d.createElement("form"); var f = makeform("locate");
f.className = "admin location";
f.action = "/adm/locate";
f.method = "post";
var i = d.createElement("input");
i.type="hidden";
i.name="image";
i.value = imgid;
f.appendChild(i);
var lat = d.createElement("input"); var lat = d.createElement("input");
lat.type="hidden"; lat.type="hidden";
@ -303,58 +292,55 @@
}; };
f.appendChild(c); f.appendChild(c);
f.addEventListener('keydown', keyHandler); f.addEventListener('keydown', keyHandler);
meta.insertBefore(f, meta.querySelector('#map')); p.append(f);
} }
var meta = details.querySelector('.meta'); p = d.createElement("div");
if (meta) { p.className = 'admbuttons';
p = d.createElement("p"); r = d.createElement("button");
p.className = 'admbuttons'; r.onclick = rotate;
r = d.createElement("button"); r.innerHTML = "\u27f2";
r.onclick = rotate; r.dataset.angle = "-90";
r.innerHTML = "\u27f2"; r.title = "Rotate left";
r.dataset.angle = "-90"; p.appendChild(r);
r.title = "Rotate left"; p.appendChild(d.createTextNode(" "));
p.appendChild(r); r = d.createElement("button");
p.appendChild(d.createTextNode(" ")); r.onclick = rotate;
r = d.createElement("button"); r.innerHTML = "\u27f3";
r.onclick = rotate; r.dataset.angle = "90";
r.innerHTML = "\u27f3"; r.title = "Rotate right";
r.dataset.angle = "90"; p.appendChild(r);
r.title = "Rotate right";
p.appendChild(r);
p.appendChild(d.createTextNode(" ")); p.appendChild(d.createTextNode(" "));
r = d.createElement("button"); r = d.createElement("button");
r.onclick = e => tag_form(e, 'tag'); r.onclick = e => tag_form(e, 'tag');
r.innerHTML = "🏷"; r.innerHTML = "🏷";
r.title = "Tag"; r.title = "Tag";
r.accessKey = "t"; r.accessKey = "t";
p.appendChild(r); p.appendChild(r);
p.appendChild(d.createTextNode(" ")); p.appendChild(d.createTextNode(" "));
r = d.createElement("button"); r = d.createElement("button");
r.onclick = e => tag_form(e, 'person'); r.onclick = e => tag_form(e, 'person');
r.innerHTML = "\u263a"; r.innerHTML = "\u263a";
r.title = "Person"; r.title = "Person";
r.accessKey = "p"; r.accessKey = "p";
p.appendChild(r); p.appendChild(r);
p.appendChild(d.createTextNode(" ")); p.appendChild(d.createTextNode(" "));
r = d.createElement("button"); r = d.createElement("button");
r.onclick = e => location_form(e); r.onclick = e => location_form(e);
r.innerHTML = "\u{1f5fa}"; r.innerHTML = "\u{1f5fa}";
r.title = "Location"; r.title = "Location";
r.accessKey = "l"; r.accessKey = "l";
p.appendChild(r); p.appendChild(r);
p.appendChild(d.createTextNode(" ")); p.appendChild(d.createTextNode(" "));
r = d.createElement("button"); r = d.createElement("button");
r.onclick = e => grade_form(e); r.onclick = e => grade_form(e);
r.innerHTML = "\u2606"; r.innerHTML = "\u2606";
r.title = "Grade"; r.title = "Grade";
r.accessKey = "g"; r.accessKey = "g";
p.appendChild(r); p.appendChild(r);
meta.appendChild(p); details.appendChild(p);
}
})(document) })(document)

View File

@ -15,6 +15,9 @@ body {
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
min-height: 100%; min-height: 100%;
// FIXME? Only on details?
max-height: 100vh;
} }
h1 { h1 {
@ -237,39 +240,79 @@ div.group {
justify-content: space-around; justify-content: space-around;
} }
} }
div.details {
display: flex;
flex-flow: row wrap;
margin: -1ex;
.item, .meta { main.details {
margin: 1ex; margin: 0;
} padding: 1ex;
.item {
align-self: flex-start; img.item {
flex-grow: 4; height: auto;
text-align: center; width: -moz-available;
width: 30em; width: -webkit-fill-available;
width: available;
&.zoom { &.zoom {
position: fixed;
top: 0;
left: 0;
width: -moz-available; width: -moz-available;
width: -webkit-fill-available; height: -moz-available;
width: available; z-index: 10000;
object-fit: contain;
} }
img { }
width: -moz-available; }
width: -webkit-fill-available;
width: available; @media screen and (min-width: 50em) {
height: auto; main.details {
align-items: start;
display: grid;
flex: content 1 1;
grid-gap: 1ex;
grid-template-columns: 1fr fit-content(29%);
grid-template-rows: min-content 1fr min-content 1fr;
max-height: -moz-available;
overflow: hidden;
h1 {
grid-column: 2;
margin: 0;
}
img.item {
display: block; display: block;
grid-row: 1 / -1;
margin: 0 auto auto;
max-width: -moz-available;
max-height: -moz-available;
max-height: calc(100% - 2px);
max-width: calc(100% - 2px);
object-fit: scale-down;
width: auto;
height: auto;
.zoom {
grid-column: 1 / 3;
}
}
.places a:nth-child(n+2) {
font-size: 80%;
}
.meta {
overflow: auto;
height: -moz-available;
height: 100%;
}
#map {
margin: 0;
height: calc(100% - 2px) !important;
width: -moz-available;
}
.admbuttons {
flex-flow: row wrap;
margin: 0;
button {
margin: 0;
}
} }
}
.meta {
flex-grow: 1;
flex-basis: 20em;
padding-top: 0;
}
.places a:nth-child(n+2) {
font-size: 80%;
} }
} }
@ -285,13 +328,13 @@ ul.alltags, ul.allpeople, ul.allplaces {
max-height: 60vh; max-height: 60vh;
} }
p.admbuttons { div.admbuttons {
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-between; justify-content: space-between;
margin: 1ex -.1em 0; margin: 1ex -.1em 0;
button { button {
flex-grow: .1; flex: min-content .1 1;
margin: .1em; margin: .1em;
padding: 0; padding: 0;
} }
@ -310,7 +353,7 @@ form {
justify-content: space-between; justify-content: space-between;
} }
label { label {
padding: .2em 1em .2em 0; padding: .2em .6em .2em 0;
} }
} }
@ -341,15 +384,16 @@ form {
// Relevant for admin forms only. Move to separate file? // Relevant for admin forms only. Move to separate file?
form.admin { form.admin {
position: relative;
padding: 1.8em 1ex;
display: flex; display: flex;
margin: .3em .1em 0;
padding: 1.6em 1ex 1.2em;
position: relative;
width: -moz-available; width: -moz-available;
width: -webkit-fill-available; width: -webkit-fill-available;
width: available; width: available;
input[type="text"], input[type="range"] { input[type="text"], input[type="range"] {
flex-grow: 1; flex: min-content 1 1;
margin-right: 1ex; margin-right: 1ex;
} }
button.close { button.close {
@ -360,7 +404,7 @@ form.admin {
right: -1ex; right: -1ex;
top: -1ex; top: -1ex;
} }
&.location { &.locate {
background: #eee; background: #eee;
box-shadow: .2em .4em 1em rgba(0,0,0,.7); box-shadow: .2em .4em 1em rgba(0,0,0,.7);
display: flex; display: flex;

View File

@ -43,7 +43,7 @@
csslink.rel = 'stylesheet'; csslink.rel = 'stylesheet';
csslink.href = '/static/l140/leaflet.css'; csslink.href = '/static/l140/leaflet.css';
h.append(csslink); h.append(csslink);
let m = d.querySelector('.meta') || d.querySelector('main'); let m = d.querySelector('main');
m.insertAdjacentHTML('beforeend', '<div id="map"></div>'); m.insertAdjacentHTML('beforeend', '<div id="map"></div>');
var slink = d.createElement('script'); var slink = d.createElement('script');
slink.type = 'text/javascript'; slink.type = 'text/javascript';

31
templates/base.rs.html Normal file
View File

@ -0,0 +1,31 @@
@use super::statics::{photos_css, admin_js, ux_js};
@use super::head;
@use crate::server::{Context, Link};
@(context: &Context, title: &str, lpath: &[Link], meta: Content, content: Content)
<!doctype html>
<html>
<head>
<title>@title</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/@photos_css.name" type="text/css"/>
@if context.is_authorized() {
<script src="/static/@admin_js.name" type="text/javascript" defer>
</script>
}
<script src="/static/@ux_js.name" type="text/javascript" defer>
</script>
@:meta()
</head>
<body>
@:head(context, lpath)
@:content()
<footer>
<p>Managed by
<a href="https://github.com/kaj/rphotos">rphotos
@env!("CARGO_PKG_VERSION")</a>.</p>
</footer>
</body>
</html>

View File

@ -1,16 +1,17 @@
@use super::page_base; @use super::base;
@use crate::models::{Photo, Person, Place, Tag, Camera, Coord, SizeTag}; @use crate::models::{Photo, Person, Place, Tag, Camera, Coord, SizeTag};
@use crate::server::{Context, Link}; @use crate::server::{Context, Link};
@(context: &Context, lpath: &[Link], people: &[Person], places: &[Place], tags: &[Tag], position: &Option<Coord>, attribution: &Option<String>, camera: &Option<Camera>, photo: &Photo) @(context: &Context, lpath: &[Link], people: &[Person], places: &[Place], tags: &[Tag], position: &Option<Coord>, attribution: &Option<String>, camera: &Option<Camera>, photo: &Photo)
@:page_base(context, "Photo details", lpath, { @:base(context, "Photo details", lpath, {
<meta property='og:title' content='Photo @if let Some(d) = photo.date {(@d.format("%F"))}'> <meta property='og:title' content='Photo @if let Some(d) = photo.date {(@d.format("%F"))}'>
<meta property='og:type' content='image' /> <meta property='og:type' content='image' />
<meta property='og:image' content='/img/@photo.id-m.jpg' /> <meta property='og:image' content='/img/@photo.id-m.jpg' />
<meta property='og:description' content='@for p in people {@p.person_name, }@for t in tags {#@t.tag_name, }@if let Some(p) = places.first() {@p.place_name}'> <meta property='og:description' content='@for p in people {@p.person_name, }@for t in tags {#@t.tag_name, }@if let Some(p) = places.first() {@p.place_name}'>
}, { }, {
<div class="details" data-imgid="@photo.id"@if let Some(g) = photo.grade { data-grade="@g"}@if let Some(ref p) = *position { data-position="[@p.x, @p.y]"}> <main class="details" data-imgid="@photo.id"@if let Some(g) = photo.grade { data-grade="@g"}@if let Some(ref p) = *position { data-position="[@p.x, @p.y]"}>
<div class="item"><img src="/img/@photo.id-m.jpg" width="@photo.get_size(SizeTag::Medium).0" height="@photo.get_size(SizeTag::Medium).1"></div> <h1>Photo details</h1>
<img class="item" src="/img/@photo.id-m.jpg" width="@photo.get_size(SizeTag::Medium).0" height="@photo.get_size(SizeTag::Medium).1">
<div class="meta"> <div class="meta">
@if context.is_authorized() { @if context.is_authorized() {
<p><a href="/img/@photo.id-l.jpg">@photo.path</a></p> <p><a href="/img/@photo.id-l.jpg">@photo.path</a></p>
@ -29,5 +30,5 @@
@if let Some(ref a) = *attribution {<p>Av: @a</p>} @if let Some(ref a) = *attribution {<p>Av: @a</p>}
@if let Some(ref c) = *camera {<p>Camera: @c.model (@c.manufacturer)</p>} @if let Some(ref c) = *camera {<p>Camera: @c.model (@c.manufacturer)</p>}
</div> </div>
</div> </main>
}) })

View File

@ -1,34 +1,10 @@
@use super::statics::{photos_css, admin_js, ux_js}; @use super::base;
@use crate::server::{Context, Link}; @use crate::server::{Context, Link};
@use crate::templates::head;
@(context: &Context, title: &str, lpath: &[Link], meta: Content, content: Content) @(context: &Context, title: &str, lpath: &[Link], meta: Content, content: Content)
@:base(context, title, lpath, meta, {
<!doctype html>
<html>
<head>
<title>@title</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="/static/@photos_css.name" type="text/css"/>
@if context.is_authorized() {
<script src="/static/@admin_js.name" type="text/javascript" defer>
</script>
}
<script src="/static/@ux_js.name" type="text/javascript" defer>
</script>
@:meta()
</head>
<body>
@:head(context, lpath)
<main> <main>
<h1>@title</h1> <h1>@title</h1>
@:content() @:content()
</main> </main>
<footer> })
<p>Managed by
<a href="https://github.com/kaj/rphotos">rphotos
@env!("CARGO_PKG_VERSION")</a>.</p>
</footer>
</body>
</html>