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
66
res/admin.js
66
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,12 +292,10 @@
|
||||
};
|
||||
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 = d.createElement("div");
|
||||
p.className = 'admbuttons';
|
||||
r = d.createElement("button");
|
||||
r.onclick = rotate;
|
||||
@ -355,6 +342,5 @@
|
||||
r.title = "Grade";
|
||||
r.accessKey = "g";
|
||||
p.appendChild(r);
|
||||
meta.appendChild(p);
|
||||
}
|
||||
details.appendChild(p);
|
||||
})(document)
|
||||
|
110
res/photos.scss
110
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,40 +240,80 @@ 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;
|
||||
&.zoom {
|
||||
width: -moz-available;
|
||||
width: -webkit-fill-available;
|
||||
width: available;
|
||||
}
|
||||
img {
|
||||
width: -moz-available;
|
||||
width: -webkit-fill-available;
|
||||
width: available;
|
||||
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;
|
||||
height: -moz-available;
|
||||
z-index: 10000;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
.meta {
|
||||
flex-grow: 1;
|
||||
flex-basis: 20em;
|
||||
padding-top: 0;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul.alltags, ul.allpeople, ul.allplaces {
|
||||
@ -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