1
0
Fork 0

Implement web interface generator

main
Ambrose Chua 2020-06-01 18:07:48 +08:00
parent 787e025a1f
commit c99274a0cc
Signed by: ambrose
GPG Key ID: BC367D33F140B5C2
12 changed files with 171 additions and 74 deletions

View File

@ -64,16 +64,31 @@ func read(w http.ResponseWriter, req *http.Request) {
}
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 err != nil {
httphelpers.ErrorResponse(w, err)
if photo.Validate() == nil {
url, err := signer.GetPhoto(bucket, photo)
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
}
w.Header().Add("Location", url)
w.WriteHeader(http.StatusTemporaryRedirect)
httphelpers.ErrorResponse(w, httphelpers.ErrorBadRequest)
}
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"))
photo := lib.Photo(req.FormValue("photo"))
photo := lib.Photo(req.FormValue("object"))
url, err := signer.PutPhoto(bucket, photo)
if err != nil {
httphelpers.ErrorResponse(w, err)
if photo.Validate() == nil {
url, err := signer.PutPhoto(bucket, photo)
if err != nil {
httphelpers.ErrorResponse(w, err)
return
}
w.Header().Add("Location", url)
w.WriteHeader(http.StatusTemporaryRedirect)
return
}
w.Header().Add("Location", url)
w.WriteHeader(http.StatusTemporaryRedirect)
httphelpers.ErrorResponse(w, httphelpers.ErrorBadRequest)
}

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,7 @@ package main
import (
"html/template"
"strings"
lib "git.makerforce.io/photos/photos/pkg/bucket"
)
@ -9,6 +10,7 @@ import (
var funcs = template.FuncMap{
"ar": func(p Photo) float64 { return float64(p.Width) / float64(p.Height) },
"mul": func(a, b float64) float64 { return a * b },
"photo": photo,
"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 {
preview := p.GetPreview(lib.PreviewOption{
Height: height,
Format: format,
Quality: quality,
})
return template.Srcset(preview.String())
path := strings.ReplaceAll(preview.Path(), ",", "%2C")
return template.Srcset(path)
}

View File

@ -14,6 +14,7 @@ import (
)
var client *lib.Client
var sharedFileServer http.Handler
var endpoint string
@ -28,12 +29,14 @@ func main() {
panic(err)
}
sharedFileServer = http.FileServer(AssetFile())
server := &http.Server{
Addr: ":8004",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
http.Handle("/shared/", http.FileServer(AssetFile()))
http.HandleFunc("/shared/", shared)
http.HandleFunc("/update", update)
err = server.ListenAndServe()
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) {
if req.Method != http.MethodPost {
err := fmt.Errorf("%w: %v", httphelpers.ErrorMethodNotAllowed, req.Method)

View File

@ -3,6 +3,7 @@ package bucket
import (
"errors"
"fmt"
"net/url"
"strings"
)
@ -24,7 +25,10 @@ func (p Photo) String() string {
}
func (p Photo) Path() string {
return "/" + string(p)
u := url.URL{
Path: string(p),
}
return u.String()
}
const photoMetadataPrefix = "photometa/"

View File

@ -4,6 +4,8 @@ import (
"errors"
"fmt"
"mime"
"net/url"
"sort"
"strings"
)
@ -24,6 +26,13 @@ func (p Preview) String() string {
return string(p)
}
func (p Preview) Path() string {
u := url.URL{
Path: string(p),
}
return u.String()
}
func (p Preview) Format() PhotoFormat {
extIndex := strings.LastIndex(string(p), ".")
return PhotoFormat(mime.TypeByExtension(string(p)[extIndex:]))
@ -88,10 +97,11 @@ func (p Photo) GetPreview(option PreviewOption) Preview {
if err != nil {
panic(err)
}
sort.Strings(exts)
if len(exts) < 1 {
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}

View File

@ -66,7 +66,7 @@ func (s *Signer) baseBucket(b Bucket) url.URL {
url := url.URL{
Scheme: "http",
Host: b.String(),
Path: "",
Path: "/",
}
if s.bucketSecure {
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))
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
}

View File

@ -61,6 +61,8 @@ h4, h5, h6 {
top: 0;
left: 0;
z-index: 1;
display: block !important;
}
.preloaded-thumbnail-image img {
object-fit: cover;

View File

@ -15,6 +15,9 @@
font-size: 0.9em;
letter-spacing: 0.07em;
text-transform: uppercase;
text-shadow: 1px 1px rgba(0, 0, 0, 0.5);
color: rgba(255, 255, 255, 0.5);
}
.enlarge-close {
@ -24,7 +27,6 @@
top: 0;
right: 0;
z-index: 100;
color: rgba(255, 255, 255, 0.5);
}
.enlarge-zoom-controls {
@ -39,7 +41,7 @@
padding: calc(var(--spacing) * 2);
display: inline-block;
color: rgba(255, 255, 255, 0.5);
color: inherit;
}
.enlarge-contents {

View File

@ -20,6 +20,7 @@ export default class Enlarge {
*/
constructor(ele, prefix='#view-') {
this.showing = false;
this.prefix = prefix;
this.zoom = 0;
this.x = 0;
@ -103,7 +104,11 @@ export default class Enlarge {
}
bindWindow() {
window.addEventListener('resize', this.updateSize.bind(this), false);
window.addEventListener('resize', () => {
if (this.showing) {
this.updateSize();
}
}, false);
}
bindTouch() {
@ -111,18 +116,20 @@ export default class Enlarge {
}
bindDrag() {
this.ele.addEventListener('pointerdown', (e) => {
this.contentsEle.addEventListener('pointerdown', (e) => {
e.preventDefault();
this.mouseDown = true;
this.contentsEle.style.cursor = 'grabbing';
}, false);
this.ele.addEventListener('pointermove', (e) => {
this.contentsEle.addEventListener('pointermove', (e) => {
e.preventDefault();
if (this.mouseDown) {
this.pan(e.movementX, e.movementY);
this.draw();
}
}, false);
this.ele.addEventListener('pointerup', (e) => {
this.contentsEle.addEventListener('pointerup', (e) => {
e.preventDefault();
this.mouseDown = false;
this.contentsEle.style.cursor = 'grab';
}, false);
@ -288,6 +295,8 @@ export default class Enlarge {
}
show() {
this.showing = true;
// Capture scroll position
this.preservedScroll.x = window.scrollX;
this.preservedScroll.y = window.scrollY;
@ -305,6 +314,8 @@ export default class Enlarge {
}
hide() {
this.showing = false;
// Restore scroll position
window.scrollTo(this.preservedScroll.x, this.preservedScroll.y);

View File

@ -37,42 +37,42 @@ class LayoutEngine {
let dimensions = [];
let currentWidth = this.gap;
let currentIndex = 0;
let lastHeight = this.baseHeight * 1.2;
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++) {
// 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
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 (currentWidth + rectWidth > this.viewportWidth) {
const gapTotal = this.gap * (index - currentIndex + 1);
const scale = (this.viewportWidth - gapTotal) / (currentWidth - gapTotal);
if (!hasNextRect || nextRectOverflow) {
const gapTotal = this.gap * (index - startIndex + 2);
let scale = (this.viewportWidth - gapTotal) / (currentWidth - gapTotal);
if (!hasNextRect) {
scale = Math.min(1.8, scale);
}
const rectHeight = this.baseHeight * scale;
lastHeight = rectHeight;
// Scale up every previous rectangle
for (; currentIndex < index; currentIndex++) {
for (; startIndex <= index; startIndex++) {
dimensions.push({
h: rectHeight,
w: rectHeight * this.rects[currentIndex].ar,
w: rectHeight * this.rects[startIndex].ar,
})
}
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;
@ -128,7 +128,7 @@ export default class Gallery {
// Extract viewport width
grabViewportWidth() {
this.layoutEngine.viewportWidth = window.innerWidth;
this.layoutEngine.viewportWidth = this.ele.clientWidth;
}
// Inform our engine of viewport width
@ -137,11 +137,20 @@ export default class Gallery {
if (this.layoutEngine.viewportWidth == window.innerWidth) {
return;
}
this.grabBaseHeight();
this.grabViewportWidth();
//this.layoutEngine.scheduleUpdate();
this.draw();
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
@ -164,7 +173,7 @@ export default class Gallery {
return;
}
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`;
}
}

View File

@ -7,12 +7,14 @@
<link rel="stylesheet" href="{{ .Assets }}css/base.css">
<link rel="stylesheet" href="{{ .Assets }}css/gallery.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>
<body>
<nav>
<ul>
<li><a href="#download">Download</a></li>
<li><a href="manage">Manage</a></li>
<li><a href="manage/">Manage</a></li>
</ul>
</nav>
<header>
@ -21,24 +23,24 @@
<main class="gallery">
{{ range .Photos }}
<div class="gallery-item preloaded-thumbnail" data-ar="{{ ar . }}">
<a href="{{ . }}" data-enlarge="{{ . }}">
<picture>
<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">
<source srcset="{{ preview . 320 "image/jpeg" 70 }} 2x, {{ preview . 160 "image/jpeg" 70 }} 1x" type="image/jpeg" media="(max-width: 900px)">
<source srcset="{{ preview . 640 "image/jpeg" 70 }} 2x, {{ preview . 320 "image/jpeg" 70 }} 1x" type="image/jpeg">
<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 . | mul 320 }}"
height="320"
loading="lazy"
data-enlarge-preload-for="{{ . }}">
data-enlarge-preload-for="{{ photo . }}">
</picture>
<picture class="preloaded-thumbnail-image">
<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" type="image/jpeg" media="(max-width: 900px)">
<source srcset="{{ preview . 60 "image/jpeg" 34 }} 1x" type="image/jpeg">
<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 . | mul 320 }}"
@ -48,8 +50,6 @@
</div>
{{ end }}
</main>
<!-- This gallery works without JavaScript! -->
<script type="module" src="{{ .Assets }}js/view.js" async></script>
</body>
</html>
<!-- vim: set ft=html: -->