Switch gallery layout engine into a single function
parent
0e54187b15
commit
ff4646598f
|
@ -55,7 +55,7 @@ func sign(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
bucket := lib.Bucket(req.FormValue("bucket"))
|
||||
token := req.FormValue("token")
|
||||
request := SafePathable(req.FormValue("request"))
|
||||
resource := SafePathable(req.FormValue("resource"))
|
||||
|
||||
err = bucket.Validate()
|
||||
if err != nil {
|
||||
|
@ -63,7 +63,7 @@ func sign(w http.ResponseWriter, req *http.Request) {
|
|||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
err = request.Validate()
|
||||
err = resource.Validate()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
|
@ -81,7 +81,7 @@ func sign(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
var signedReq *http.Request
|
||||
unsignedReq, err := http.NewRequest(req.Method, bucket.URL(request).String(), nil)
|
||||
unsignedReq, err := http.NewRequest(req.Method, bucket.URL(resource).String(), nil)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("%w: error creating request: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
|
@ -91,13 +91,14 @@ func sign(w http.ResponseWriter, req *http.Request) {
|
|||
if req.Method == http.MethodGet {
|
||||
signedReq = sig.PreSignRead(unsignedReq, cred)
|
||||
} else if req.Method == http.MethodPost || req.Method == http.MethodPut || req.Method == http.MethodDelete {
|
||||
signedReq = sig.PreSignRead(unsignedReq, cred)
|
||||
signedReq = sig.PreSignWrite(unsignedReq, cred)
|
||||
} else {
|
||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorMethodNotAllowed, req.Method)
|
||||
httphelpers.ErrorResponse(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Add("Location", signedReq.URL.String())
|
||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@ func (s SafePathable) Validate() error {
|
|||
if strings.HasPrefix(string(s), "internal/") {
|
||||
return ErrorInvalidRequest
|
||||
}
|
||||
return nil
|
||||
_, err := url.Parse(string(s))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s SafePathable) Path() *url.URL {
|
||||
return &url.URL{
|
||||
Path: string(s),
|
||||
}
|
||||
u, _ := url.Parse(string(s))
|
||||
return u
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -53,7 +53,7 @@ dev: $(SOURCE)/* $(OUTPUT)/index.html $(shared_copy_files) | container
|
|||
|
||||
$(OUTPUT)/index.html: index.tmpl indextmpl.go
|
||||
mkdir -p $(@D)
|
||||
$(GO) run indextmpl.go -t $< -o $@ -a $(subst $(OUTPUT)/,,$(SHARED_COPY))/ -c $(CONTROL_ENDPOINT) -b $(DEV_BUCKET) -t "[dev] Manage"
|
||||
$(GO) run indextmpl.go -t $< -o $@ -a $(subst $(OUTPUT)/,,$(SHARED_COPY))/ -c $(CONTROL_ENDPOINT) -b $(DEV_BUCKET) -title "[dev] Manage"
|
||||
|
||||
.SECONDEXPANSION:
|
||||
$(shared_copy_files): $$(subst $$(SHARED_COPY)/,$$(SHARED)/,$$@)
|
||||
|
|
|
@ -9,6 +9,6 @@
|
|||
<!-- Svelte! -->
|
||||
<script src="{{if .Development}}{{else}}{{ .Assets }}js/{{end}}manage.js" defer async></script>
|
||||
</head>
|
||||
<body data-bucket="{{ .Bucket }}">
|
||||
<body data-bucket="{{ .Bucket }}" data-control="{{ .Control }}">
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -30,7 +30,7 @@ func main() {
|
|||
flag.StringVar(&data.Assets, "a", "", ".Assets")
|
||||
flag.StringVar(&data.Control, "c", "", ".Control")
|
||||
flag.StringVar(&data.Bucket, "b", "", ".Bucket")
|
||||
flag.StringVar(&data.Title, "t", "", ".Title")
|
||||
flag.StringVar(&data.Title, "title", "", ".Title")
|
||||
flag.Parse()
|
||||
|
||||
f, err := os.Open(tmpl)
|
||||
|
|
|
@ -1,24 +1,59 @@
|
|||
<script>
|
||||
export let bucket;
|
||||
|
||||
import { fetcher, request, listPhotos } from './http.js';
|
||||
import Gallery from './Gallery.svelte';
|
||||
|
||||
let photos = [
|
||||
"photo/Screenshot from 2020-04-23 19-27-53.png",
|
||||
"photo/Screenshot_2020-04-13 Looking Glass - Hurricane Electric (AS6939).png",
|
||||
];
|
||||
async function getMetadataTitle() {
|
||||
const resp = await fetcher(request('GET', 'metadata/title'));
|
||||
return resp.text();
|
||||
}
|
||||
let title = getMetadataTitle();
|
||||
|
||||
async function getPhotos() {
|
||||
const resp = await fetcher(request('GET', listPhotos(10000)));
|
||||
const text = await resp.text();
|
||||
|
||||
const xml = new window.DOMParser().parseFromString(text, "text/xml");
|
||||
const contents = xml.querySelectorAll('ListBucketResult > Contents');
|
||||
|
||||
const photos = [];
|
||||
contents.forEach(c => {
|
||||
photos.push(c.querySelector('Key').innerHTML);
|
||||
});
|
||||
return photos;
|
||||
}
|
||||
let photos = getPhotos();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#await title then title}
|
||||
<title>{title}</title>
|
||||
{:catch error}
|
||||
<title>Error</title>
|
||||
{/await}
|
||||
</svelte:head>
|
||||
|
||||
<div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="..">Gallery</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<header>
|
||||
<h1>{bucket}</h1>
|
||||
{#await title}
|
||||
<h1 class="dim">Loading...</h1>
|
||||
{:then title}
|
||||
<h1>{title}</h1>
|
||||
{:catch error}
|
||||
<h1 class="dim" title="{error}">Error getting title</h1>
|
||||
{/await}
|
||||
</header>
|
||||
<Gallery {photos} />
|
||||
|
||||
{#await photos then photos}
|
||||
<Gallery {photos} />
|
||||
{:catch error}
|
||||
<main>Unable to load photos: {error}</main>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<!-- vim: set ft=html: -->
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
<script>
|
||||
import { onMount, afterUpdate } from 'svelte';
|
||||
import Gallery from '../build/shared/js/gallery.js';
|
||||
import Photo from './Photo.svelte';
|
||||
|
||||
export let photos;
|
||||
|
||||
import Photo from './Photo.svelte';
|
||||
let galleryEle;
|
||||
let gallery;
|
||||
|
||||
onMount(() => {
|
||||
gallery = new Gallery(galleryEle);
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<main class="gallery" bind:this={galleryEle}>
|
||||
{#each photos as photo}
|
||||
<Photo {photo} />
|
||||
<Photo {photo} on:sizechange={gallery.recompute()} />
|
||||
{/each}
|
||||
</main>
|
||||
|
||||
|
|
|
@ -1,38 +1,97 @@
|
|||
<script>
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { fetcher, request } from './http.js';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let photo;
|
||||
$: raw = request('GET', photo).url;
|
||||
|
||||
async function ar() {
|
||||
let title = '';
|
||||
|
||||
}
|
||||
async function preview() {
|
||||
let size = { width: 1, height: 1 };
|
||||
$: ar = size.width / size.height;
|
||||
|
||||
}
|
||||
let tags = [];
|
||||
|
||||
async function getPhotoMetadataTitle() {
|
||||
const objectBase = photo.replace('photo/', 'photometadata/');
|
||||
const resp = await fetcher(request('GET', objectBase + '/title'));
|
||||
return resp.text();
|
||||
}
|
||||
onMount(async () => {
|
||||
try {
|
||||
title = await getPhotoMetadataTitle();
|
||||
} catch (e) {
|
||||
// We can ignore missing titles
|
||||
}
|
||||
});
|
||||
async function getPhotoMetadataSize() {
|
||||
const objectBase = photo.replace('photo/', 'photometadata/');
|
||||
const resp = await fetcher(request('GET', objectBase + '/size'));
|
||||
return resp.json();
|
||||
}
|
||||
onMount(async () => {
|
||||
try {
|
||||
size = await getPhotoMetadataSize();
|
||||
dispatch('sizechange');
|
||||
} catch (e) {
|
||||
// TODO: Emit error
|
||||
}
|
||||
});
|
||||
async function getPhotoMetadataTags() {
|
||||
const objectBase = photo.replace('photo/', 'photometadata/');
|
||||
const resp = await fetcher(request('GET', objectBase + '/tags'));
|
||||
const str = await resp.text();
|
||||
return str.split(",");
|
||||
}
|
||||
onMount(async () => {
|
||||
try {
|
||||
tags = await getPhotoMetadataTags();
|
||||
} catch (e) {
|
||||
// We can ignore missing tags
|
||||
}
|
||||
});
|
||||
|
||||
function preview(height, format, quality) {
|
||||
const objectBase = photo.replace('photo/', 'preview/');
|
||||
const extIndex = objectBase.lastIndexOf(".");
|
||||
const base = objectBase.substring(0, extIndex);
|
||||
const res = base + `_h${height}q${quality}`;
|
||||
const ext = extensionByType(format)
|
||||
return request('GET', res + ext).url;
|
||||
}
|
||||
|
||||
function extensionByType(format) {
|
||||
return {
|
||||
"image/webp": ".webp",
|
||||
"image/jpeg": ".jpg",
|
||||
}[format];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="gallery-item preloaded-thumbnail" data-ar="{ar(photo)}">
|
||||
<a href="{photo}" data-enlarge="{photo}">
|
||||
<div class="gallery-item preloaded-thumbnail" title="{title}" data-ar="{ar}">
|
||||
<picture style="display: none;">
|
||||
<source srcset="{preview(photo, 320, 'image/webp', 70)} 2x, {preview(photo, 160, 'image/webp', 70)} 1x" type="image/webp" media="(max-width: 900px)">
|
||||
<source srcset="{preview(photo, 640, 'image/webp', 70)} 2x, {preview(photo, 320, 'image/webp', 70)} 1x" type="image/webp">
|
||||
<source srcset="{preview(photo, 320, 'image/jpeg', 70)} 2x, {preview(photo, 160, 'image/jpeg', 70)} 1x" media="(max-width: 900px)">
|
||||
<source srcset="{preview(photo, 640, 'image/jpeg', 70)} 2x, {preview(photo, 320, 'image/jpeg', 70)} 1x">
|
||||
<img src="{preview(photo, 320, 'image/jpeg', 70)}"
|
||||
<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(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">
|
||||
<img src="{preview(320, 'image/jpeg', 70)}"
|
||||
alt=""
|
||||
width="{ar(photo) * 320}"
|
||||
width="{ar * 320}"
|
||||
height="320"
|
||||
loading="lazy"
|
||||
data-enlarge-preload-for="{photo}">
|
||||
</picture>
|
||||
<picture class="preloaded-thumbnail-image">
|
||||
<source srcset="{preview(photo, 30, 'image/webp', 34)} 1x" type="image/webp" media="(max-width: 900px)">
|
||||
<source srcset="{preview(photo, 60, 'image/webp', 34)} 1x" type="image/webp">
|
||||
<source srcset="{preview(photo, 30, 'image/jpeg', 34)} 1x" media="(max-width: 900px)">
|
||||
<source srcset="{preview(photo, 60, 'image/jpeg', 34)} 1x">
|
||||
<img src="{preview(photo, 30, 'image/jpeg', 34)}"
|
||||
<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(30, 'image/jpeg', 34)} 1x" media="(max-width: 900px)">
|
||||
<source srcset="{preview(60, 'image/jpeg', 34)} 1x">
|
||||
<img src="{preview(30, 'image/jpeg', 34)}"
|
||||
alt=""
|
||||
width="{ar(photo) * 320}"
|
||||
width="{ar * 320}"
|
||||
height="320">
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
<!-- vim: set ft=html: -->
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { control, bucket, token } from './stores.js';
|
||||
|
||||
let control_value;
|
||||
control.subscribe(value => {
|
||||
control_value = value;
|
||||
});
|
||||
let bucket_value;
|
||||
bucket.subscribe(value => {
|
||||
bucket_value = value;
|
||||
});
|
||||
let token_value;
|
||||
token.subscribe(value => {
|
||||
token_value = value;
|
||||
});
|
||||
|
||||
class RequestError extends Error {
|
||||
constructor(res) {
|
||||
super(`Request returned status ${res.status} ${res.statusText}`);
|
||||
this.res = res;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetcher(request) {
|
||||
const resp = await fetch(request);
|
||||
if (resp.status != 200) {
|
||||
throw new RequestError(resp);
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
export function request(method, resource, body=null) {
|
||||
const sign = new URL(control_value + '/sign');
|
||||
sign.searchParams.append('bucket', bucket_value);
|
||||
sign.searchParams.append('token', token_value);
|
||||
sign.searchParams.append('resource', resource);
|
||||
const req = new Request(sign.href, {
|
||||
method,
|
||||
body,
|
||||
});
|
||||
return req;
|
||||
}
|
||||
|
||||
export function listPhotos(maxKeys=1000, startAfter='') {
|
||||
const prefix = "photo/";
|
||||
const params = new URLSearchParams();
|
||||
params.set("list-type", "2");
|
||||
params.set("metadata", "true");
|
||||
params.set("encoding-type", "url");
|
||||
params.set("prefix", prefix);
|
||||
params.set("delimiter", "");
|
||||
params.set("max-keys", maxKeys);
|
||||
params.set("start-after", startAfter);
|
||||
return '?' + params.toString();
|
||||
}
|
|
@ -2,9 +2,6 @@ import App from './App.svelte';
|
|||
|
||||
const app = new App({
|
||||
target: document.body,
|
||||
props: {
|
||||
bucket: document.body.dataset.bucket,
|
||||
}
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { readable, writable } from 'svelte/store';
|
||||
|
||||
export const control = readable(document.body.dataset.control);
|
||||
export const bucket = readable(document.body.dataset.bucket);
|
||||
|
||||
export const token = writable('todo');
|
|
@ -48,6 +48,10 @@ h4, h5, h6 {
|
|||
font-size: 1.272em;
|
||||
}
|
||||
|
||||
.dim {
|
||||
opacity: 0.333;
|
||||
}
|
||||
|
||||
/*
|
||||
* Molecules
|
||||
*/
|
||||
|
|
|
@ -1,48 +1,90 @@
|
|||
class LayoutEngine {
|
||||
/*
|
||||
rects = [];
|
||||
export default class Gallery {
|
||||
constructor(ele) {
|
||||
this.ele = ele;
|
||||
|
||||
gap = 12;
|
||||
baseHeight = 100;
|
||||
// maxHeight = 320;
|
||||
viewportWidth = 1024;
|
||||
*/
|
||||
|
||||
constructor() {
|
||||
this.rects = [];
|
||||
this.gap = 12;
|
||||
this.baseHeight = 100;
|
||||
this.viewportWidth = 1024;
|
||||
|
||||
this.grabGap();
|
||||
this.grabBaseHeight();
|
||||
this.grabViewportWidth();
|
||||
|
||||
this.registerViewport();
|
||||
this.draw();
|
||||
}
|
||||
|
||||
// Register a new rectangle with an aspect ratio of ar at index
|
||||
insert(ar=1, index=-1) {
|
||||
const rect = {
|
||||
ar: ar,
|
||||
};
|
||||
if (index == -1) {
|
||||
this.rects.push(rect);
|
||||
} else {
|
||||
this.rects.splice(index, 0, rect);
|
||||
// Extract gap values
|
||||
grabGap() {
|
||||
// Simple implementation to guess from padding values
|
||||
const px = window.getComputedStyle(this.ele).getPropertyValue('padding-left').replace('px', '');
|
||||
this.gap = parseInt(px) * 2 || 0;
|
||||
}
|
||||
|
||||
// Extract baseHeight
|
||||
grabBaseHeight() {
|
||||
const px = window.getComputedStyle(this.ele).getPropertyValue('--gallery-base-height').replace('px', '');
|
||||
this.baseHeight = parseInt(px) || 0;
|
||||
}
|
||||
|
||||
// Extract viewport width
|
||||
grabViewportWidth() {
|
||||
this.viewportWidth = this.ele.clientWidth;
|
||||
}
|
||||
|
||||
// Inform our engine of viewport width
|
||||
registerViewport() {
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.viewportWidth == window.innerWidth) {
|
||||
return;
|
||||
}
|
||||
this.recompute();
|
||||
});
|
||||
window.addEventListener('load', () => {
|
||||
this.recompute();
|
||||
});
|
||||
setInterval(this.recompute.bind(this), 500);
|
||||
}
|
||||
|
||||
recompute() {
|
||||
this.grabGap();
|
||||
this.grabBaseHeight();
|
||||
this.grabViewportWidth();
|
||||
this.draw();
|
||||
}
|
||||
|
||||
|
||||
// Calculate dimensions and draw them
|
||||
draw() {
|
||||
const elements = this.ele.querySelectorAll('.gallery-item');
|
||||
const rects = Array.from(elements).map(element => parseFloat(element.dataset.ar));
|
||||
const dimensions = this.layout(rects);
|
||||
|
||||
const galleryItemEles = this.ele.querySelectorAll('.gallery-item');
|
||||
for (const [index, ele] of galleryItemEles.entries()) {
|
||||
const dimension = dimensions[index];
|
||||
if (!dimension) {
|
||||
console.error(`Missing dimensions for element at ${index}`);
|
||||
return;
|
||||
}
|
||||
for (const imgEle of ele.querySelectorAll('img')) {
|
||||
imgEle.style.height = `${Math.floor(dimension.h)}px`;
|
||||
imgEle.style.width = `${dimension.w}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unregister a new rectangle at index
|
||||
pop(index=-1) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// Generate a list of dimensions for each rectangle
|
||||
calculate() {
|
||||
// Perform layout
|
||||
layout(rects) {
|
||||
let dimensions = [];
|
||||
|
||||
let currentWidth = this.gap;
|
||||
let startIndex = 0;
|
||||
|
||||
// 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 < rects.length; index++) {
|
||||
// Add this rectangle width
|
||||
const currentRectWidth = (this.baseHeight * this.rects[index].ar);
|
||||
const currentRectWidth = (this.baseHeight * rects[index]);
|
||||
currentWidth += currentRectWidth + this.gap;
|
||||
|
||||
if (currentWidth > this.viewportWidth) {
|
||||
|
@ -51,8 +93,8 @@ class LayoutEngine {
|
|||
}
|
||||
|
||||
// Get the next width to add
|
||||
const hasNextRect = index + 1 < this.rects.length;
|
||||
const nextRectWidth = hasNextRect ? (this.baseHeight * this.rects[index+1].ar) : 0;
|
||||
const hasNextRect = index + 1 < rects.length;
|
||||
const nextRectWidth = hasNextRect ? (this.baseHeight * rects[index+1]) : 0;
|
||||
const nextRectOverflow = currentWidth + nextRectWidth + this.gap > this.viewportWidth;
|
||||
|
||||
// If the next width is too wide, resolve the current rectangles
|
||||
|
@ -68,7 +110,7 @@ class LayoutEngine {
|
|||
for (; startIndex <= index; startIndex++) {
|
||||
dimensions.push({
|
||||
h: rectHeight,
|
||||
w: rectHeight * this.rects[startIndex].ar,
|
||||
w: rectHeight * rects[startIndex],
|
||||
})
|
||||
}
|
||||
currentWidth = this.gap;
|
||||
|
@ -78,104 +120,4 @@ class LayoutEngine {
|
|||
return dimensions;
|
||||
}
|
||||
|
||||
/*
|
||||
* Code for a future implementation that emits events
|
||||
|
||||
update() {
|
||||
console.debug('updating layout');
|
||||
|
||||
}
|
||||
|
||||
// Schedule an update at maximum 10Hz
|
||||
scheduleUpdate() {
|
||||
if (this.scheduledUpdate) {
|
||||
return;
|
||||
}
|
||||
this.scheduledUpdate = setTimeout(() => {
|
||||
this.scheduledUpdate = null;
|
||||
this.update();
|
||||
}, 100);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
export default class Gallery {
|
||||
constructor(ele) {
|
||||
this.ele = ele;
|
||||
this.layoutEngine = new LayoutEngine();
|
||||
|
||||
this.grabGap();
|
||||
this.grabBaseHeight();
|
||||
this.grabViewportWidth();
|
||||
|
||||
this.registerViewport();
|
||||
this.registerInitial();
|
||||
this.draw();
|
||||
}
|
||||
|
||||
// Extract gap values
|
||||
grabGap() {
|
||||
// Simple implementation to guess from padding values
|
||||
const px = window.getComputedStyle(this.ele).getPropertyValue('padding-left').replace('px', '');
|
||||
this.layoutEngine.gap = parseInt(px) * 2 || 0;
|
||||
}
|
||||
|
||||
// Extract baseHeight
|
||||
grabBaseHeight() {
|
||||
const px = window.getComputedStyle(this.ele).getPropertyValue('--gallery-base-height').replace('px', '');
|
||||
this.layoutEngine.baseHeight = parseInt(px) || 0;
|
||||
}
|
||||
|
||||
// Extract viewport width
|
||||
grabViewportWidth() {
|
||||
this.layoutEngine.viewportWidth = this.ele.clientWidth;
|
||||
}
|
||||
|
||||
// Inform our engine of viewport width
|
||||
registerViewport() {
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.layoutEngine.viewportWidth == window.innerWidth) {
|
||||
return;
|
||||
}
|
||||
this.recompute();
|
||||
});
|
||||
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
|
||||
registerInitial() {
|
||||
const galleryItemEles = this.ele.querySelectorAll('.gallery-item');
|
||||
for (const ele of galleryItemEles) {
|
||||
const ar = ele.dataset.ar;
|
||||
this.layoutEngine.insert(ar);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate dimensions and draw them
|
||||
draw() {
|
||||
const dimensions = this.layoutEngine.calculate();
|
||||
const galleryItemEles = this.ele.querySelectorAll('.gallery-item');
|
||||
for (const [index, ele] of galleryItemEles.entries()) {
|
||||
const dimension = dimensions[index];
|
||||
if (!dimension) {
|
||||
console.error(`Missing dimensions for element at ${index}`);
|
||||
return;
|
||||
}
|
||||
for (const imgEle of ele.querySelectorAll('img')) {
|
||||
imgEle.style.height = `${Math.floor(dimension.h)}px`;
|
||||
imgEle.style.width = `${dimension.w}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,8 @@
|
|||
</header>
|
||||
<main class="gallery">
|
||||
{{ range .Photos }}
|
||||
<div class="gallery-item preloaded-thumbnail" data-ar="{{ ar . }}">
|
||||
<a href="{{ photo . }}" data-enlarge="{{ photo . }}" title="{{ photometadatatitle .Title }}" data-tags="{{ .Tags }}">
|
||||
<div class="gallery-item preloaded-thumbnail" data-ar="{{ ar . }}" title="{{ photometadatatitle .Title }}" data-tags="{{ .Tags }}">
|
||||
<a href="{{ photo . }}" data-enlarge="{{ photo . }}">
|
||||
<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 . 640 "image/webp" 70 }} 2x, {{ preview . 320 "image/webp" 70 }} 1x" type="image/webp">
|
||||
|
|
Loading…
Reference in New Issue