Implement web interface generator
parent
787e025a1f
commit
c99274a0cc
|
@ -64,16 +64,31 @@ func read(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket := lib.Bucket(req.FormValue("bucket"))
|
bucket := lib.Bucket(req.FormValue("bucket"))
|
||||||
photo := lib.Photo(req.FormValue("photo"))
|
photo := lib.Photo(req.FormValue("object"))
|
||||||
|
preview := lib.Preview(req.FormValue("object"))
|
||||||
|
|
||||||
url, err := signer.GetPhoto(bucket, photo)
|
if photo.Validate() == nil {
|
||||||
if err != nil {
|
url, err := signer.GetPhoto(bucket, photo)
|
||||||
httphelpers.ErrorResponse(w, err)
|
if err != nil {
|
||||||
|
httphelpers.ErrorResponse(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Location", url)
|
||||||
|
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||||
|
return
|
||||||
|
} else if preview.Validate() == nil {
|
||||||
|
url, err := signer.GetPreview(bucket, preview)
|
||||||
|
if err != nil {
|
||||||
|
httphelpers.ErrorResponse(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Location", url)
|
||||||
|
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
httphelpers.ErrorResponse(w, httphelpers.ErrorBadRequest)
|
||||||
w.Header().Add("Location", url)
|
|
||||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func write(w http.ResponseWriter, req *http.Request) {
|
func write(w http.ResponseWriter, req *http.Request) {
|
||||||
|
@ -84,14 +99,18 @@ func write(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket := lib.Bucket(req.FormValue("bucket"))
|
bucket := lib.Bucket(req.FormValue("bucket"))
|
||||||
photo := lib.Photo(req.FormValue("photo"))
|
photo := lib.Photo(req.FormValue("object"))
|
||||||
|
|
||||||
url, err := signer.PutPhoto(bucket, photo)
|
if photo.Validate() == nil {
|
||||||
if err != nil {
|
url, err := signer.PutPhoto(bucket, photo)
|
||||||
httphelpers.ErrorResponse(w, err)
|
if err != nil {
|
||||||
|
httphelpers.ErrorResponse(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Location", url)
|
||||||
|
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
httphelpers.ErrorResponse(w, httphelpers.ErrorBadRequest)
|
||||||
w.Header().Add("Location", url)
|
|
||||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"strings"
|
||||||
|
|
||||||
lib "git.makerforce.io/photos/photos/pkg/bucket"
|
lib "git.makerforce.io/photos/photos/pkg/bucket"
|
||||||
)
|
)
|
||||||
|
@ -9,6 +10,7 @@ import (
|
||||||
var funcs = template.FuncMap{
|
var funcs = template.FuncMap{
|
||||||
"ar": func(p Photo) float64 { return float64(p.Width) / float64(p.Height) },
|
"ar": func(p Photo) float64 { return float64(p.Width) / float64(p.Height) },
|
||||||
"mul": func(a, b float64) float64 { return a * b },
|
"mul": func(a, b float64) float64 { return a * b },
|
||||||
|
"photo": photo,
|
||||||
"preview": preview,
|
"preview": preview,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,11 +37,16 @@ func mustTemplateAsset(name string) *template.Template {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func photo(p Photo) template.URL {
|
||||||
|
return template.URL(p.Path())
|
||||||
|
}
|
||||||
|
|
||||||
func preview(p Photo, height int, format lib.PhotoFormat, quality int) template.Srcset {
|
func preview(p Photo, height int, format lib.PhotoFormat, quality int) template.Srcset {
|
||||||
preview := p.GetPreview(lib.PreviewOption{
|
preview := p.GetPreview(lib.PreviewOption{
|
||||||
Height: height,
|
Height: height,
|
||||||
Format: format,
|
Format: format,
|
||||||
Quality: quality,
|
Quality: quality,
|
||||||
})
|
})
|
||||||
return template.Srcset(preview.String())
|
path := strings.ReplaceAll(preview.Path(), ",", "%2C")
|
||||||
|
return template.Srcset(path)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var client *lib.Client
|
var client *lib.Client
|
||||||
|
var sharedFileServer http.Handler
|
||||||
|
|
||||||
var endpoint string
|
var endpoint string
|
||||||
|
|
||||||
|
@ -28,12 +29,14 @@ func main() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sharedFileServer = http.FileServer(AssetFile())
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: ":8004",
|
Addr: ":8004",
|
||||||
ReadTimeout: 5 * time.Second,
|
ReadTimeout: 5 * time.Second,
|
||||||
WriteTimeout: 10 * time.Second,
|
WriteTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
http.Handle("/shared/", http.FileServer(AssetFile()))
|
http.HandleFunc("/shared/", shared)
|
||||||
http.HandleFunc("/update", update)
|
http.HandleFunc("/update", update)
|
||||||
err = server.ListenAndServe()
|
err = server.ListenAndServe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -41,6 +44,14 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shared(w http.ResponseWriter, req *http.Request) {
|
||||||
|
// For static files, allow all CORS
|
||||||
|
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
sharedFileServer.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func update(w http.ResponseWriter, req *http.Request) {
|
func update(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != http.MethodPost {
|
if req.Method != http.MethodPost {
|
||||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorMethodNotAllowed, req.Method)
|
err := fmt.Errorf("%w: %v", httphelpers.ErrorMethodNotAllowed, req.Method)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package bucket
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,7 +25,10 @@ func (p Photo) String() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Photo) Path() string {
|
func (p Photo) Path() string {
|
||||||
return "/" + string(p)
|
u := url.URL{
|
||||||
|
Path: string(p),
|
||||||
|
}
|
||||||
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
const photoMetadataPrefix = "photometa/"
|
const photoMetadataPrefix = "photometa/"
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,6 +26,13 @@ func (p Preview) String() string {
|
||||||
return string(p)
|
return string(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p Preview) Path() string {
|
||||||
|
u := url.URL{
|
||||||
|
Path: string(p),
|
||||||
|
}
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
func (p Preview) Format() PhotoFormat {
|
func (p Preview) Format() PhotoFormat {
|
||||||
extIndex := strings.LastIndex(string(p), ".")
|
extIndex := strings.LastIndex(string(p), ".")
|
||||||
return PhotoFormat(mime.TypeByExtension(string(p)[extIndex:]))
|
return PhotoFormat(mime.TypeByExtension(string(p)[extIndex:]))
|
||||||
|
@ -88,10 +97,11 @@ func (p Photo) GetPreview(option PreviewOption) Preview {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
sort.Strings(exts)
|
||||||
if len(exts) < 1 {
|
if len(exts) < 1 {
|
||||||
panic("missing MIME type to extension mapping, please configure within your operating system")
|
panic("missing MIME type to extension mapping, please configure within your operating system")
|
||||||
}
|
}
|
||||||
return Preview(base + res + exts[0])
|
return Preview(base + res + exts[len(exts)-1])
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultSizes = []int{640, 320, 160, 80}
|
var defaultSizes = []int{640, 320, 160, 80}
|
||||||
|
|
|
@ -66,7 +66,7 @@ func (s *Signer) baseBucket(b Bucket) url.URL {
|
||||||
url := url.URL{
|
url := url.URL{
|
||||||
Scheme: "http",
|
Scheme: "http",
|
||||||
Host: b.String(),
|
Host: b.String(),
|
||||||
Path: "",
|
Path: "/",
|
||||||
}
|
}
|
||||||
if s.bucketSecure {
|
if s.bucketSecure {
|
||||||
url.Scheme = "https"
|
url.Scheme = "https"
|
||||||
|
@ -174,3 +174,25 @@ func (s *Signer) PutPhoto(b Bucket, p Photo) (string, error) {
|
||||||
signedReq := signer.PreSignV4(*req, s.accessKey, s.secretKey, "", "sgp1", int64(s.expirations.Write/time.Second))
|
signedReq := signer.PreSignV4(*req, s.accessKey, s.secretKey, "", "sgp1", int64(s.expirations.Write/time.Second))
|
||||||
return signedReq.URL.String(), nil
|
return signedReq.URL.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Signer) GetPreview(b Bucket, p Preview) (string, error) {
|
||||||
|
err := b.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = p.Validate()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
url := s.baseBucket(b)
|
||||||
|
url.Path += p.Path()
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", url.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
signedReq := signer.PreSignV4(*req, s.accessKey, s.secretKey, "", "sgp1", int64(s.expirations.Read/time.Second))
|
||||||
|
return signedReq.URL.String(), nil
|
||||||
|
}
|
||||||
|
|
|
@ -61,6 +61,8 @@ h4, h5, h6 {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
|
display: block !important;
|
||||||
}
|
}
|
||||||
.preloaded-thumbnail-image img {
|
.preloaded-thumbnail-image img {
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
|
|
@ -15,6 +15,9 @@
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
letter-spacing: 0.07em;
|
letter-spacing: 0.07em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
text-shadow: 1px 1px rgba(0, 0, 0, 0.5);
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.enlarge-close {
|
.enlarge-close {
|
||||||
|
@ -24,7 +27,6 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.enlarge-zoom-controls {
|
.enlarge-zoom-controls {
|
||||||
|
@ -39,7 +41,7 @@
|
||||||
padding: calc(var(--spacing) * 2);
|
padding: calc(var(--spacing) * 2);
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.enlarge-contents {
|
.enlarge-contents {
|
||||||
|
|
|
@ -20,6 +20,7 @@ export default class Enlarge {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
constructor(ele, prefix='#view-') {
|
constructor(ele, prefix='#view-') {
|
||||||
|
this.showing = false;
|
||||||
this.prefix = prefix;
|
this.prefix = prefix;
|
||||||
this.zoom = 0;
|
this.zoom = 0;
|
||||||
this.x = 0;
|
this.x = 0;
|
||||||
|
@ -103,7 +104,11 @@ export default class Enlarge {
|
||||||
}
|
}
|
||||||
|
|
||||||
bindWindow() {
|
bindWindow() {
|
||||||
window.addEventListener('resize', this.updateSize.bind(this), false);
|
window.addEventListener('resize', () => {
|
||||||
|
if (this.showing) {
|
||||||
|
this.updateSize();
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
bindTouch() {
|
bindTouch() {
|
||||||
|
@ -111,18 +116,20 @@ export default class Enlarge {
|
||||||
}
|
}
|
||||||
|
|
||||||
bindDrag() {
|
bindDrag() {
|
||||||
this.ele.addEventListener('pointerdown', (e) => {
|
this.contentsEle.addEventListener('pointerdown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
this.mouseDown = true;
|
this.mouseDown = true;
|
||||||
this.contentsEle.style.cursor = 'grabbing';
|
this.contentsEle.style.cursor = 'grabbing';
|
||||||
}, false);
|
}, false);
|
||||||
this.ele.addEventListener('pointermove', (e) => {
|
this.contentsEle.addEventListener('pointermove', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (this.mouseDown) {
|
if (this.mouseDown) {
|
||||||
this.pan(e.movementX, e.movementY);
|
this.pan(e.movementX, e.movementY);
|
||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
}, false);
|
}, false);
|
||||||
this.ele.addEventListener('pointerup', (e) => {
|
this.contentsEle.addEventListener('pointerup', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
this.mouseDown = false;
|
this.mouseDown = false;
|
||||||
this.contentsEle.style.cursor = 'grab';
|
this.contentsEle.style.cursor = 'grab';
|
||||||
}, false);
|
}, false);
|
||||||
|
@ -288,6 +295,8 @@ export default class Enlarge {
|
||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
|
this.showing = true;
|
||||||
|
|
||||||
// Capture scroll position
|
// Capture scroll position
|
||||||
this.preservedScroll.x = window.scrollX;
|
this.preservedScroll.x = window.scrollX;
|
||||||
this.preservedScroll.y = window.scrollY;
|
this.preservedScroll.y = window.scrollY;
|
||||||
|
@ -305,6 +314,8 @@ export default class Enlarge {
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
|
this.showing = false;
|
||||||
|
|
||||||
// Restore scroll position
|
// Restore scroll position
|
||||||
window.scrollTo(this.preservedScroll.x, this.preservedScroll.y);
|
window.scrollTo(this.preservedScroll.x, this.preservedScroll.y);
|
||||||
|
|
||||||
|
|
|
@ -37,42 +37,42 @@ class LayoutEngine {
|
||||||
let dimensions = [];
|
let dimensions = [];
|
||||||
|
|
||||||
let currentWidth = this.gap;
|
let currentWidth = this.gap;
|
||||||
let currentIndex = 0;
|
let startIndex = 0;
|
||||||
|
|
||||||
let lastHeight = this.baseHeight * 1.2;
|
|
||||||
|
|
||||||
// Behave like a browser: try to fit as many in a row with baseHeight
|
// Behave like a browser: try to fit as many in a row with baseHeight
|
||||||
for (let index = 0; index < this.rects.length; index++) {
|
for (let index = 0; index < this.rects.length; index++) {
|
||||||
|
// Add this rectangle width
|
||||||
|
const currentRectWidth = (this.baseHeight * this.rects[index].ar);
|
||||||
|
currentWidth += currentRectWidth + this.gap;
|
||||||
|
|
||||||
|
if (currentWidth > this.viewportWidth) {
|
||||||
|
console.error("TODO: fix cases where viewport smaller than image");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the next width to add
|
// Get the next width to add
|
||||||
const rectWidth = (this.baseHeight * this.rects[index].ar) + this.gap;
|
const hasNextRect = index + 1 < this.rects.length;
|
||||||
|
const nextRectWidth = hasNextRect ? (this.baseHeight * this.rects[index+1].ar) : 0;
|
||||||
|
const nextRectOverflow = currentWidth + nextRectWidth + this.gap > this.viewportWidth;
|
||||||
|
|
||||||
// If the next width is too wide, resolve the current rectangles
|
// If the next width is too wide, resolve the current rectangles
|
||||||
if (currentWidth + rectWidth > this.viewportWidth) {
|
if (!hasNextRect || nextRectOverflow) {
|
||||||
const gapTotal = this.gap * (index - currentIndex + 1);
|
const gapTotal = this.gap * (index - startIndex + 2);
|
||||||
const scale = (this.viewportWidth - gapTotal) / (currentWidth - gapTotal);
|
let scale = (this.viewportWidth - gapTotal) / (currentWidth - gapTotal);
|
||||||
|
if (!hasNextRect) {
|
||||||
|
scale = Math.min(1.8, scale);
|
||||||
|
}
|
||||||
const rectHeight = this.baseHeight * scale;
|
const rectHeight = this.baseHeight * scale;
|
||||||
lastHeight = rectHeight;
|
|
||||||
|
|
||||||
// Scale up every previous rectangle
|
// Scale up every previous rectangle
|
||||||
for (; currentIndex < index; currentIndex++) {
|
for (; startIndex <= index; startIndex++) {
|
||||||
dimensions.push({
|
dimensions.push({
|
||||||
h: rectHeight,
|
h: rectHeight,
|
||||||
w: rectHeight * this.rects[currentIndex].ar,
|
w: rectHeight * this.rects[startIndex].ar,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
currentWidth = this.gap;
|
currentWidth = this.gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
currentWidth += rectWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set remainder to a decent height
|
|
||||||
for (; currentIndex < this.rects.length; currentIndex++) {
|
|
||||||
dimensions.push({
|
|
||||||
h: lastHeight,
|
|
||||||
w: lastHeight * this.rects[currentIndex].ar,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dimensions;
|
return dimensions;
|
||||||
|
@ -128,7 +128,7 @@ export default class Gallery {
|
||||||
|
|
||||||
// Extract viewport width
|
// Extract viewport width
|
||||||
grabViewportWidth() {
|
grabViewportWidth() {
|
||||||
this.layoutEngine.viewportWidth = window.innerWidth;
|
this.layoutEngine.viewportWidth = this.ele.clientWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inform our engine of viewport width
|
// Inform our engine of viewport width
|
||||||
|
@ -137,11 +137,20 @@ export default class Gallery {
|
||||||
if (this.layoutEngine.viewportWidth == window.innerWidth) {
|
if (this.layoutEngine.viewportWidth == window.innerWidth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.grabBaseHeight();
|
this.recompute();
|
||||||
this.grabViewportWidth();
|
|
||||||
//this.layoutEngine.scheduleUpdate();
|
|
||||||
this.draw();
|
|
||||||
});
|
});
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
this.recompute();
|
||||||
|
});
|
||||||
|
setInterval(this.recompute.bind(this), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
recompute() {
|
||||||
|
this.grabGap();
|
||||||
|
this.grabBaseHeight();
|
||||||
|
this.grabViewportWidth();
|
||||||
|
//this.layoutEngine.scheduleUpdate();
|
||||||
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register initial elements
|
// Register initial elements
|
||||||
|
@ -164,7 +173,7 @@ export default class Gallery {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const imgEle of ele.querySelectorAll('img')) {
|
for (const imgEle of ele.querySelectorAll('img')) {
|
||||||
imgEle.style.height = `${dimension.h}px`;
|
imgEle.style.height = `${Math.floor(dimension.h)}px`;
|
||||||
imgEle.style.width = `${dimension.w}px`;
|
imgEle.style.width = `${dimension.w}px`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,14 @@
|
||||||
<link rel="stylesheet" href="{{ .Assets }}css/base.css">
|
<link rel="stylesheet" href="{{ .Assets }}css/base.css">
|
||||||
<link rel="stylesheet" href="{{ .Assets }}css/gallery.css">
|
<link rel="stylesheet" href="{{ .Assets }}css/gallery.css">
|
||||||
<link rel="stylesheet" href="{{ .Assets }}css/enlarge.css">
|
<link rel="stylesheet" href="{{ .Assets }}css/enlarge.css">
|
||||||
|
<!-- This gallery works without JavaScript! -->
|
||||||
|
<script type="module" src="{{ .Assets }}js/view.js" defer async></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="#download">Download</a></li>
|
<li><a href="#download">Download</a></li>
|
||||||
<li><a href="manage">Manage</a></li>
|
<li><a href="manage/">Manage</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<header>
|
<header>
|
||||||
|
@ -21,24 +23,24 @@
|
||||||
<main class="gallery">
|
<main class="gallery">
|
||||||
{{ range .Photos }}
|
{{ range .Photos }}
|
||||||
<div class="gallery-item preloaded-thumbnail" data-ar="{{ ar . }}">
|
<div class="gallery-item preloaded-thumbnail" data-ar="{{ ar . }}">
|
||||||
<a href="{{ . }}" data-enlarge="{{ . }}">
|
<a href="{{ photo . }}" data-enlarge="{{ photo . }}">
|
||||||
<picture>
|
<picture style="display: none;">
|
||||||
<source srcset="{{ preview . 320 "image/webp" 70 }} 2x, {{ preview . 160 "image/webp" 70 }} 1x" type="image/webp" media="(max-width: 900px)">
|
<source srcset="{{ preview . 320 "image/webp" 70 }} 2x, {{ preview . 160 "image/webp" 70 }} 1x" type="image/webp" media="(max-width: 900px)">
|
||||||
<source srcset="{{ preview . 640 "image/webp" 70 }} 2x, {{ preview . 320 "image/webp" 70 }} 1x" type="image/webp">
|
<source srcset="{{ preview . 640 "image/webp" 70 }} 2x, {{ preview . 320 "image/webp" 70 }} 1x" type="image/webp">
|
||||||
<source srcset="{{ preview . 320 "image/jpeg" 70 }} 2x, {{ preview . 160 "image/jpeg" 70 }} 1x" type="image/jpeg" media="(max-width: 900px)">
|
<source srcset="{{ preview . 320 "image/jpeg" 70 }} 2x, {{ preview . 160 "image/jpeg" 70 }} 1x" media="(max-width: 900px)">
|
||||||
<source srcset="{{ preview . 640 "image/jpeg" 70 }} 2x, {{ preview . 320 "image/jpeg" 70 }} 1x" type="image/jpeg">
|
<source srcset="{{ preview . 640 "image/jpeg" 70 }} 2x, {{ preview . 320 "image/jpeg" 70 }} 1x">
|
||||||
<img src="{{ preview . 320 "image/jpeg" 70 }}"
|
<img src="{{ preview . 320 "image/jpeg" 70 }}"
|
||||||
alt=""
|
alt=""
|
||||||
width="{{ ar . | mul 320 }}"
|
width="{{ ar . | mul 320 }}"
|
||||||
height="320"
|
height="320"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
data-enlarge-preload-for="{{ . }}">
|
data-enlarge-preload-for="{{ photo . }}">
|
||||||
</picture>
|
</picture>
|
||||||
<picture class="preloaded-thumbnail-image">
|
<picture class="preloaded-thumbnail-image">
|
||||||
<source srcset="{{ preview . 30 "image/webp" 34 }} 1x" type="image/webp" media="(max-width: 900px)">
|
<source srcset="{{ preview . 30 "image/webp" 34 }} 1x" type="image/webp" media="(max-width: 900px)">
|
||||||
<source srcset="{{ preview . 60 "image/webp" 34 }} 1x" type="image/webp">
|
<source srcset="{{ preview . 60 "image/webp" 34 }} 1x" type="image/webp">
|
||||||
<source srcset="{{ preview . 30 "image/jpeg" 34 }} 1x" type="image/jpeg" media="(max-width: 900px)">
|
<source srcset="{{ preview . 30 "image/jpeg" 34 }} 1x" media="(max-width: 900px)">
|
||||||
<source srcset="{{ preview . 60 "image/jpeg" 34 }} 1x" type="image/jpeg">
|
<source srcset="{{ preview . 60 "image/jpeg" 34 }} 1x">
|
||||||
<img src="{{ preview . 30 "image/jpeg" 34 }}"
|
<img src="{{ preview . 30 "image/jpeg" 34 }}"
|
||||||
alt=""
|
alt=""
|
||||||
width="{{ ar . | mul 320 }}"
|
width="{{ ar . | mul 320 }}"
|
||||||
|
@ -48,8 +50,6 @@
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</main>
|
</main>
|
||||||
<!-- This gallery works without JavaScript! -->
|
|
||||||
<script type="module" src="{{ .Assets }}js/view.js" async></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
<!-- vim: set ft=html: -->
|
<!-- vim: set ft=html: -->
|
||||||
|
|
Loading…
Reference in New Issue