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:
parent
89a92c1738
commit
8ee8b809bc
150
res/admin.js
150
res/admin.js
@ -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)
|
||||
|
112
res/photos.scss
112
res/photos.scss
@ -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;
|
||||
|
@ -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
31
templates/base.rs.html
Normal 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>
|
@ -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>
|
||||
})
|
||||
|
@ -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>
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user