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

View File

@ -15,6 +15,9 @@ body {
flex-direction: column;
justify-content: space-between;
min-height: 100%;
// FIXME? Only on details?
max-height: 100vh;
}
h1 {
@ -237,39 +240,79 @@ div.group {
justify-content: space-around;
}
}
div.details {
display: flex;
flex-flow: row wrap;
margin: -1ex;
.item, .meta {
margin: 1ex;
}
.item {
align-self: flex-start;
flex-grow: 4;
text-align: center;
width: 30em;
main.details {
margin: 0;
padding: 1ex;
img.item {
height: auto;
width: -moz-available;
width: -webkit-fill-available;
width: available;
&.zoom {
position: fixed;
top: 0;
left: 0;
width: -moz-available;
width: -webkit-fill-available;
width: available;
height: -moz-available;
z-index: 10000;
object-fit: contain;
}
img {
width: -moz-available;
width: -webkit-fill-available;
width: available;
height: auto;
}
}
@media screen and (min-width: 50em) {
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;
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;
}
p.admbuttons {
div.admbuttons {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
margin: 1ex -.1em 0;
button {
flex-grow: .1;
flex: min-content .1 1;
margin: .1em;
padding: 0;
}
@ -310,7 +353,7 @@ form {
justify-content: space-between;
}
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?
form.admin {
position: relative;
padding: 1.8em 1ex;
display: flex;
margin: .3em .1em 0;
padding: 1.6em 1ex 1.2em;
position: relative;
width: -moz-available;
width: -webkit-fill-available;
width: available;
input[type="text"], input[type="range"] {
flex-grow: 1;
flex: min-content 1 1;
margin-right: 1ex;
}
button.close {
@ -360,7 +404,7 @@ form.admin {
right: -1ex;
top: -1ex;
}
&.location {
&.locate {
background: #eee;
box-shadow: .2em .4em 1em rgba(0,0,0,.7);
display: flex;

View File

@ -43,7 +43,7 @@
csslink.rel = 'stylesheet';
csslink.href = '/static/l140/leaflet.css';
h.append(csslink);
let m = d.querySelector('.meta') || d.querySelector('main');
let m = d.querySelector('main');
m.insertAdjacentHTML('beforeend', '<div id="map"></div>');
var slink = d.createElement('script');
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::server::{Context, Link};
@(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:type' content='image' />
<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}'>
}, {
<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]"}>
<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>
<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]"}>
<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">
@if context.is_authorized() {
<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 c) = *camera {<p>Camera: @c.model (@c.manufacturer)</p>}
</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::templates::head;
@(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)
@:base(context, title, lpath, meta, {
<main>
<h1>@title</h1>
@:content()
</main>
<footer>
<p>Managed by
<a href="https://github.com/kaj/rphotos">rphotos
@env!("CARGO_PKG_VERSION")</a>.</p>
</footer>
</body>
</html>
})