# HG changeset patch # User paulo # Date 1616657622 25200 # Node ID 9b57b90aea31afee7fe2d2027815ff41a2333d80 # Parent b2aebd4994ea6ba43b416e036c8423fc950dd244 initial add for pics3 diff -r b2aebd4994ea -r 9b57b90aea31 pics3/flask_run_dev.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pics3/flask_run_dev.sh Thu Mar 25 00:33:42 2021 -0700 @@ -0,0 +1,8 @@ +#!/bin/sh + +export GOOGLE_APPLICATION_CREDENTIALS=dev.key.json +export GCS_BUCKET=dev.pauloang.com +export FLASK_APP=pics_flask_app.py +export FLASK_ENV=development + +./bin/flask run diff -r b2aebd4994ea -r 9b57b90aea31 pics3/pics_flask_app.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pics3/pics_flask_app.py Thu Mar 25 00:33:42 2021 -0700 @@ -0,0 +1,183 @@ +import csv +import datetime +import random +import os + +import flask +import google.cloud.storage +from html3.html3 import HTML + +app = flask.Flask(__name__) + +GCS_CLIENT = google.cloud.storage.Client() +GCS_BUCKET = GCS_CLIENT.get_bucket(os.environ.get("GCS_BUCKET")) + + +class PicsDialect(csv.Dialect): + delimiter = '\t' + quoting = csv.QUOTE_NONE + lineterminator = '\n' + +PICSDIALECT = PicsDialect() + + +def _parse_dt(dts): + return datetime.datetime.strptime(dts, "%Y%m%d") + + +def _format_dt(dt): + return dt.strftime("%Y-%m-%d") + + +def _numeric_pad_basename(path, maxdigits=20): + return os.path.basename(path).zfill(maxdigits) + + +def _get_images(d): + exts = (".jpg", ".webm") + thumb_dir = f"pics/{d}/thumbs" + browse_dir = f"pics/{d}/browse" + + thumb_fns = [i.name for i in GCS_CLIENT.list_blobs(GCS_BUCKET, prefix=thumb_dir) if i.name.endswith(exts)] + thumb_fns = sorted(thumb_fns, key=_numeric_pad_basename) + + browse_contents = set(i.name for i in GCS_CLIENT.list_blobs(GCS_BUCKET, prefix=browse_dir) if i.name.endswith(exts)) + browse_fns = [] + for i in thumb_fns: + i_basename = os.path.splitext(os.path.basename(i))[0] + try: + for j in exts: + browse_fn = browse_dir + "/" + i_basename + j + if browse_fn in browse_contents: + browse_fns.append(browse_fn) + raise StopIteration + except StopIteration: + pass + else: + raise RuntimeError(f"Cannot find browse image for {i}") + + return zip(thumb_fns, browse_fns) + + +def _get_standard_html_doc(title): + root = HTML("html") + + header = root.head + header.link(rel="stylesheet", type="text/css", href=flask.url_for("static", filename="index.css")) + header.title(title) + + body = root.body + body.h1(title) + + return (root, header, body) + + +def _go_thumbnail_links_to_browse_imgs_html_body(body, d, t, b, a_args={}, img_args={}, lazyload=False): + thumb_img_url = GCS_BUCKET.get_blob(t).public_url + browse_url = flask.url_for("browse", d=d, img=os.path.basename(b)) + + a = body.a(href=browse_url, **a_args) + if lazyload: + img_args = dict(img_args) + img_args["data-src"] = thumb_img_url + a.img(**img_args) + else: + a.img(src=thumb_img_url, **img_args) + + body.text(" ") + + +@app.route("/") +def index(): + n = 5 # number of thumbnails to display per dir + + (root, header, body) = _get_standard_html_doc("Pictures") + header.script('', type="text/javascript", src=flask.url_for("static", filename="lazyload.js")) + + #body.pre(str(list(GCS_CLIENT.list_blobs(GCS_BUCKET)))) + + pics_dirs = [] + pics_dirs_index_blob = GCS_BUCKET.get_blob("pics/index.tsv") + if pics_dirs_index_blob: + pics_dirs_index_strlist = pics_dirs_index_blob.download_as_text().splitlines() + pics_dirs_index_reader = csv.reader(pics_dirs_index_strlist, PICSDIALECT) + pics_dirs = sorted(pics_dirs_index_reader, key=lambda x: x[0]) + + for (dts, d) in pics_dirs: + dt = _parse_dt(dts) + body.h2(d) + body.h3(_format_dt(dt)) + + #body.pre(str(list(_get_images(d)))) + + imgs = _get_images(d) + imgs_idx = [(i, img) for (i, img) in enumerate(imgs)] + + sampled_imgs_idx = random.sample(imgs_idx, min(len(imgs_idx), n)) + sampled_imgs_idx.sort(key=lambda x: x[0]) + + p = body.p + for (i, (t, b)) in sampled_imgs_idx: + _go_thumbnail_links_to_browse_imgs_html_body(p, d, t, b, lazyload=True) + + return str(root).encode("utf-8") + + +@app.route("//browse/") +def browse(d, img): + browse_img_blob = GCS_BUCKET.get_blob(f"pics/{d}/browse/{img}") + if not browse_img_blob: + flask.abort(404) + + imgs = list(_get_images(d)) + + # thumbnail preview ribbon + w = 7 # must be odd + v = int(w/2) + imgs_circ = [None] * w + x = None + n = len(imgs) + for (i, (t, b)) in enumerate(imgs): + if os.path.basename(b) == img: + x = i + 1 + imgs_circ[v] = (t, b) + for j in range(1, v + 1): + if (i + j) < n: imgs_circ[v + j] = imgs[i + j] + if (i - j) >= 0: imgs_circ[v - j] = imgs[i - j] + break + + if x is None: + raise AssertionError + + (root, header, body) = _get_standard_html_doc(f"{d} \u2014 {x} of {n}") + header.script('', type="text/javascript", src=flask.url_for("static", filename="np_keys.js")) + + browse_img_url = browse_img_blob.public_url + ext = os.path.splitext(img)[1] + p = body.p + if ext == ".webm": + p.video(src=browse_img_url, autoplay="true", loop="true") + else: + p.img(src=browse_img_url) + + p = body.p + for (i, img_c) in enumerate(imgs_circ): + if img_c is not None: + (t, b) = img_c + a_args = {} + img_args = {} + # FIXME + #if b == img: + # a_args = {"id": "up"} + # img_args = {"klass": "sel"} + # b = f"{d}?from={img}#selected" + if False: + pass + elif i == v + 1: + a_args = {"id": "next"} + elif i == v - 1: + a_args = {"id": "prev"} + + _go_thumbnail_links_to_browse_imgs_html_body(p, d, t, b, a_args, img_args) + + return str(root).encode("utf-8") diff -r b2aebd4994ea -r 9b57b90aea31 pics3/static/index.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pics3/static/index.css Thu Mar 25 00:33:42 2021 -0700 @@ -0,0 +1,34 @@ +body +{ + background-color: #111; + color: #ccc; +} + +a:link +{ + color: #831; +} + +a:visited +{ + color: gray; +} + +img +{ + border-width: 0px; +} + +img.sel +{ + padding: 2px; + border-width: 4px; + border-style: solid; +} + +img.sel2 +{ + padding: 2px; + border-width: 2px; + border-style: dashed; +} diff -r b2aebd4994ea -r 9b57b90aea31 pics3/static/lazyload.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pics3/static/lazyload.js Thu Mar 25 00:33:42 2021 -0700 @@ -0,0 +1,42 @@ +window.addEventListener('load', lazyLoadImages); +window.addEventListener('DOMContentLoaded', lazyLoadImages); +window.addEventListener('resize', lazyLoadImages); +window.addEventListener('scroll', lazyLoadImages); + +function lazyLoadImages() { + var images = document.getElementsByTagName('img'); + var loaded = 0; + + for (var i = 0; i < images.length; i++) { + var img = images[i]; + if (img.hasAttribute('data-src')) { + if (isElementInViewport(img)) { + img.setAttribute('src', img.getAttribute('data-src')); + img.removeAttribute('data-src'); + } + } else { + loaded++; + } + } + + //console.log('Loaded images:' + loaded + '/' + images.length); + + if (loaded == images.length) { + //console.log('Loaded all images.'); + window.removeEventListener('load', lazyLoadImages); + window.removeEventListener('DOMContentLoaded', lazyLoadImages); + window.removeEventListener('resize', lazyLoadImages); + window.removeEventListener('scroll', lazyLoadImages); + } +} + +function isElementInViewport(el) { + var rect = el.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.top <= window.innerHeight && + rect.left <= window.innerWidth + ); +} diff -r b2aebd4994ea -r 9b57b90aea31 pics3/static/np_keys.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pics3/static/np_keys.js Thu Mar 25 00:33:42 2021 -0700 @@ -0,0 +1,23 @@ +function getKeypress(e) { + c = null + + if (e.which == null) + c = String.fromCharCode(e.keyCode); // IE + else if (e.which != 0 && e.charCode != 0) + c = String.fromCharCode(e.which); // All others + + if (c != null) { + if (c == 'n') + goHref('next'); + else if (c == 'p') + goHref('prev'); + else if (c == 'u') + goHref('up'); + } +} + +function goHref(id) { + window.location.href = document.getElementById(id).href; +} + +document.onkeypress = getKeypress