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")) 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

View File

@ -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)
} }

View File

@ -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)

View File

@ -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/"

View File

@ -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}

View File

@ -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
}

View File

@ -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;

View File

@ -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 {

View File

@ -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);

View File

@ -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`;
} }
} }

View File

@ -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: -->