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"))
|
||||
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
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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/"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -61,6 +61,8 @@ h4, h5, h6 {
|
|||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
|
||||
display: block !important;
|
||||
}
|
||||
.preloaded-thumbnail-image img {
|
||||
object-fit: cover;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: -->
|
||||
|
|
Loading…
Reference in New Issue