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"))
|
bucket := lib.Bucket(req.FormValue("bucket"))
|
||||||
token := req.FormValue("token")
|
token := req.FormValue("token")
|
||||||
request := SafePathable(req.FormValue("request"))
|
resource := SafePathable(req.FormValue("resource"))
|
||||||
|
|
||||||
err = bucket.Validate()
|
err = bucket.Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -63,7 +63,7 @@ func sign(w http.ResponseWriter, req *http.Request) {
|
||||||
httphelpers.ErrorResponse(w, err)
|
httphelpers.ErrorResponse(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = request.Validate()
|
err = resource.Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorBadRequest, err.Error())
|
err := fmt.Errorf("%w: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||||
httphelpers.ErrorResponse(w, err)
|
httphelpers.ErrorResponse(w, err)
|
||||||
|
@ -81,7 +81,7 @@ func sign(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var signedReq *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 {
|
if err != nil {
|
||||||
err := fmt.Errorf("%w: error creating request: %v", httphelpers.ErrorBadRequest, err.Error())
|
err := fmt.Errorf("%w: error creating request: %v", httphelpers.ErrorBadRequest, err.Error())
|
||||||
httphelpers.ErrorResponse(w, err)
|
httphelpers.ErrorResponse(w, err)
|
||||||
|
@ -91,13 +91,14 @@ func sign(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method == http.MethodGet {
|
if req.Method == http.MethodGet {
|
||||||
signedReq = sig.PreSignRead(unsignedReq, cred)
|
signedReq = sig.PreSignRead(unsignedReq, cred)
|
||||||
} else if req.Method == http.MethodPost || req.Method == http.MethodPut || req.Method == http.MethodDelete {
|
} 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 {
|
} else {
|
||||||
err := fmt.Errorf("%w: %v", httphelpers.ErrorMethodNotAllowed, req.Method)
|
err := fmt.Errorf("%w: %v", httphelpers.ErrorMethodNotAllowed, req.Method)
|
||||||
httphelpers.ErrorResponse(w, err)
|
httphelpers.ErrorResponse(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Add("Location", signedReq.URL.String())
|
w.Header().Add("Location", signedReq.URL.String())
|
||||||
w.WriteHeader(http.StatusTemporaryRedirect)
|
w.WriteHeader(http.StatusTemporaryRedirect)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,11 +17,11 @@ func (s SafePathable) Validate() error {
|
||||||
if strings.HasPrefix(string(s), "internal/") {
|
if strings.HasPrefix(string(s), "internal/") {
|
||||||
return ErrorInvalidRequest
|
return ErrorInvalidRequest
|
||||||
}
|
}
|
||||||
return nil
|
_, err := url.Parse(string(s))
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SafePathable) Path() *url.URL {
|
func (s SafePathable) Path() *url.URL {
|
||||||
return &url.URL{
|
u, _ := url.Parse(string(s))
|
||||||
Path: 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
|
$(OUTPUT)/index.html: index.tmpl indextmpl.go
|
||||||
mkdir -p $(@D)
|
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:
|
.SECONDEXPANSION:
|
||||||
$(shared_copy_files): $$(subst $$(SHARED_COPY)/,$$(SHARED)/,$$@)
|
$(shared_copy_files): $$(subst $$(SHARED_COPY)/,$$(SHARED)/,$$@)
|
||||||
|
|
|
@ -9,6 +9,6 @@
|
||||||
<!-- Svelte! -->
|
<!-- Svelte! -->
|
||||||
<script src="{{if .Development}}{{else}}{{ .Assets }}js/{{end}}manage.js" defer async></script>
|
<script src="{{if .Development}}{{else}}{{ .Assets }}js/{{end}}manage.js" defer async></script>
|
||||||
</head>
|
</head>
|
||||||
<body data-bucket="{{ .Bucket }}">
|
<body data-bucket="{{ .Bucket }}" data-control="{{ .Control }}">
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -30,7 +30,7 @@ func main() {
|
||||||
flag.StringVar(&data.Assets, "a", "", ".Assets")
|
flag.StringVar(&data.Assets, "a", "", ".Assets")
|
||||||
flag.StringVar(&data.Control, "c", "", ".Control")
|
flag.StringVar(&data.Control, "c", "", ".Control")
|
||||||
flag.StringVar(&data.Bucket, "b", "", ".Bucket")
|
flag.StringVar(&data.Bucket, "b", "", ".Bucket")
|
||||||
flag.StringVar(&data.Title, "t", "", ".Title")
|
flag.StringVar(&data.Title, "title", "", ".Title")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
f, err := os.Open(tmpl)
|
f, err := os.Open(tmpl)
|
||||||
|
|
|
@ -1,24 +1,59 @@
|
||||||
<script>
|
<script>
|
||||||
export let bucket;
|
import { fetcher, request, listPhotos } from './http.js';
|
||||||
|
|
||||||
import Gallery from './Gallery.svelte';
|
import Gallery from './Gallery.svelte';
|
||||||
|
|
||||||
let photos = [
|
async function getMetadataTitle() {
|
||||||
"photo/Screenshot from 2020-04-23 19-27-53.png",
|
const resp = await fetcher(request('GET', 'metadata/title'));
|
||||||
"photo/Screenshot_2020-04-13 Looking Glass - Hurricane Electric (AS6939).png",
|
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>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
{#await title then title}
|
||||||
|
<title>{title}</title>
|
||||||
|
{:catch error}
|
||||||
|
<title>Error</title>
|
||||||
|
{/await}
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="..">Gallery</a></li>
|
<li><a href="..">Gallery</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<header>
|
<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>
|
</header>
|
||||||
<Gallery {photos} />
|
|
||||||
|
{#await photos then photos}
|
||||||
|
<Gallery {photos} />
|
||||||
|
{:catch error}
|
||||||
|
<main>Unable to load photos: {error}</main>
|
||||||
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- vim: set ft=html: -->
|
<!-- vim: set ft=html: -->
|
||||||
|
|
|
@ -1,12 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { onMount, afterUpdate } from 'svelte';
|
||||||
|
import Gallery from '../build/shared/js/gallery.js';
|
||||||
|
import Photo from './Photo.svelte';
|
||||||
|
|
||||||
export let photos;
|
export let photos;
|
||||||
|
|
||||||
import Photo from './Photo.svelte';
|
let galleryEle;
|
||||||
|
let gallery;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
gallery = new Gallery(galleryEle);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main class="gallery" bind:this={galleryEle}>
|
||||||
{#each photos as photo}
|
{#each photos as photo}
|
||||||
<Photo {photo} />
|
<Photo {photo} on:sizechange={gallery.recompute()} />
|
||||||
{/each}
|
{/each}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
@ -1,38 +1,97 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
|
import { fetcher, request } from './http.js';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let photo;
|
export let photo;
|
||||||
|
$: raw = request('GET', photo).url;
|
||||||
|
|
||||||
async function ar() {
|
let title = '';
|
||||||
|
|
||||||
}
|
let size = { width: 1, height: 1 };
|
||||||
async function preview() {
|
$: 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>
|
</script>
|
||||||
|
|
||||||
<div class="gallery-item preloaded-thumbnail" data-ar="{ar(photo)}">
|
<div class="gallery-item preloaded-thumbnail" title="{title}" data-ar="{ar}">
|
||||||
<a href="{photo}" data-enlarge="{photo}">
|
|
||||||
<picture style="display: none;">
|
<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(320, 'image/webp', 70)} 2x, {preview(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(640, 'image/webp', 70)} 2x, {preview(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(320, 'image/jpeg', 70)} 2x, {preview(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">
|
<source srcset="{preview(640, 'image/jpeg', 70)} 2x, {preview(320, 'image/jpeg', 70)} 1x">
|
||||||
<img src="{preview(photo, 320, 'image/jpeg', 70)}"
|
<img src="{preview(320, 'image/jpeg', 70)}"
|
||||||
alt=""
|
alt=""
|
||||||
width="{ar(photo) * 320}"
|
width="{ar * 320}"
|
||||||
height="320"
|
height="320"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
data-enlarge-preload-for="{photo}">
|
data-enlarge-preload-for="{photo}">
|
||||||
</picture>
|
</picture>
|
||||||
<picture class="preloaded-thumbnail-image">
|
<picture class="preloaded-thumbnail-image">
|
||||||
<source srcset="{preview(photo, 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(photo, 60, 'image/webp', 34)} 1x" type="image/webp">
|
<source srcset="{preview(60, 'image/webp', 34)} 1x" type="image/webp">
|
||||||
<source srcset="{preview(photo, 30, 'image/jpeg', 34)} 1x" media="(max-width: 900px)">
|
<source srcset="{preview(30, 'image/jpeg', 34)} 1x" media="(max-width: 900px)">
|
||||||
<source srcset="{preview(photo, 60, 'image/jpeg', 34)} 1x">
|
<source srcset="{preview(60, 'image/jpeg', 34)} 1x">
|
||||||
<img src="{preview(photo, 30, 'image/jpeg', 34)}"
|
<img src="{preview(30, 'image/jpeg', 34)}"
|
||||||
alt=""
|
alt=""
|
||||||
width="{ar(photo) * 320}"
|
width="{ar * 320}"
|
||||||
height="320">
|
height="320">
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- vim: set ft=html: -->
|
<!-- 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({
|
const app = new App({
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: {
|
|
||||||
bucket: document.body.dataset.bucket,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
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;
|
font-size: 1.272em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dim {
|
||||||
|
opacity: 0.333;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Molecules
|
* Molecules
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,48 +1,90 @@
|
||||||
class LayoutEngine {
|
export default class Gallery {
|
||||||
/*
|
constructor(ele) {
|
||||||
rects = [];
|
this.ele = ele;
|
||||||
|
|
||||||
gap = 12;
|
|
||||||
baseHeight = 100;
|
|
||||||
// maxHeight = 320;
|
|
||||||
viewportWidth = 1024;
|
|
||||||
*/
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.rects = [];
|
|
||||||
this.gap = 12;
|
this.gap = 12;
|
||||||
this.baseHeight = 100;
|
this.baseHeight = 100;
|
||||||
this.viewportWidth = 1024;
|
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
|
// Extract gap values
|
||||||
insert(ar=1, index=-1) {
|
grabGap() {
|
||||||
const rect = {
|
// Simple implementation to guess from padding values
|
||||||
ar: ar,
|
const px = window.getComputedStyle(this.ele).getPropertyValue('padding-left').replace('px', '');
|
||||||
};
|
this.gap = parseInt(px) * 2 || 0;
|
||||||
if (index == -1) {
|
}
|
||||||
this.rects.push(rect);
|
|
||||||
} else {
|
// Extract baseHeight
|
||||||
this.rects.splice(index, 0, rect);
|
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
|
// Perform layout
|
||||||
pop(index=-1) {
|
layout(rects) {
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a list of dimensions for each rectangle
|
|
||||||
calculate() {
|
|
||||||
let dimensions = [];
|
let dimensions = [];
|
||||||
|
|
||||||
let currentWidth = this.gap;
|
let currentWidth = this.gap;
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
|
|
||||||
// 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 < rects.length; index++) {
|
||||||
// Add this rectangle width
|
// Add this rectangle width
|
||||||
const currentRectWidth = (this.baseHeight * this.rects[index].ar);
|
const currentRectWidth = (this.baseHeight * rects[index]);
|
||||||
currentWidth += currentRectWidth + this.gap;
|
currentWidth += currentRectWidth + this.gap;
|
||||||
|
|
||||||
if (currentWidth > this.viewportWidth) {
|
if (currentWidth > this.viewportWidth) {
|
||||||
|
@ -51,8 +93,8 @@ class LayoutEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the next width to add
|
// Get the next width to add
|
||||||
const hasNextRect = index + 1 < this.rects.length;
|
const hasNextRect = index + 1 < rects.length;
|
||||||
const nextRectWidth = hasNextRect ? (this.baseHeight * this.rects[index+1].ar) : 0;
|
const nextRectWidth = hasNextRect ? (this.baseHeight * rects[index+1]) : 0;
|
||||||
const nextRectOverflow = currentWidth + nextRectWidth + this.gap > this.viewportWidth;
|
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
|
||||||
|
@ -68,7 +110,7 @@ class LayoutEngine {
|
||||||
for (; startIndex <= index; startIndex++) {
|
for (; startIndex <= index; startIndex++) {
|
||||||
dimensions.push({
|
dimensions.push({
|
||||||
h: rectHeight,
|
h: rectHeight,
|
||||||
w: rectHeight * this.rects[startIndex].ar,
|
w: rectHeight * rects[startIndex],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
currentWidth = this.gap;
|
currentWidth = this.gap;
|
||||||
|
@ -78,104 +120,4 @@ class LayoutEngine {
|
||||||
return dimensions;
|
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>
|
</header>
|
||||||
<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 . }}" title="{{ photometadatatitle .Title }}" data-tags="{{ .Tags }}">
|
||||||
<a href="{{ photo . }}" data-enlarge="{{ photo . }}" title="{{ photometadatatitle .Title }}" data-tags="{{ .Tags }}">
|
<a href="{{ photo . }}" data-enlarge="{{ photo . }}">
|
||||||
<picture style="display: none;">
|
<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">
|
||||||
|
|
Loading…
Reference in New Issue