commit
60ffa66fff
10 changed files with 247 additions and 0 deletions
@ -0,0 +1,9 @@ |
|||||||
|
tags |
||||||
|
*.swp |
||||||
|
instance/ |
||||||
|
*.egg-info/ |
||||||
|
__pycache__/ |
||||||
|
venv/ |
||||||
|
build/ |
||||||
|
dist/ |
||||||
|
|
||||||
@ -0,0 +1,4 @@ |
|||||||
|
|
||||||
|
recursive-include gallery/templates * |
||||||
|
recursive-include gallery/static * |
||||||
|
|
||||||
@ -0,0 +1,22 @@ |
|||||||
|
|
||||||
|
import os |
||||||
|
|
||||||
|
from flask import Flask |
||||||
|
|
||||||
|
from .views import default_routes |
||||||
|
|
||||||
|
|
||||||
|
def create_app(): |
||||||
|
app = Flask(__name__, instance_relative_config=True) |
||||||
|
|
||||||
|
# NOTE: ensure intance folders exist |
||||||
|
try: os.makedirs(app.instance_path) |
||||||
|
except OSError: pass |
||||||
|
try: os.makedirs(os.path.join(app.instance_path, 'images')) |
||||||
|
except OSError: pass |
||||||
|
try: os.makedirs(os.path.join(app.instance_path, 'thumbnails')) |
||||||
|
except OSError: pass |
||||||
|
|
||||||
|
app.register_blueprint(default_routes.bp) |
||||||
|
return app |
||||||
|
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@ -0,0 +1,27 @@ |
|||||||
|
|
||||||
|
.nav li { display: inline; } |
||||||
|
|
||||||
|
.flex_container { |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.infocard { |
||||||
|
margin: 0.5em; |
||||||
|
background-color: lightgrey; |
||||||
|
font-size: 0.7em; |
||||||
|
} |
||||||
|
.infocard img { border: 1px solid grey; } |
||||||
|
.infocard ul { |
||||||
|
list-style-type: none; |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
.infocard li { |
||||||
|
width: 160px; |
||||||
|
white-space: nowrap; |
||||||
|
overflow: hidden; |
||||||
|
text-overflow: ellipsis; |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,39 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<link rel="stylesheet" |
||||||
|
href="{{ url_for('static', filename='style.css') }}" |
||||||
|
type="text/css" |
||||||
|
/> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
|
||||||
|
<div class="nav"> |
||||||
|
<ul> |
||||||
|
<li><a href="/">Home</a> ></li> |
||||||
|
<li><a href="/">{{ album }}</a> ></li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="flex_container"> |
||||||
|
{% for image in images %} |
||||||
|
<div class="infocard"> |
||||||
|
<a href="{{ img_root + image.name }}"> |
||||||
|
<img src="{{ url_for( |
||||||
|
'default_routes.thumbs', |
||||||
|
path=album, |
||||||
|
img=image.name) |
||||||
|
}}" /> |
||||||
|
</a> |
||||||
|
<ul> |
||||||
|
<li>name: {{ image.name }}</li> |
||||||
|
<li>date: ...</li> |
||||||
|
<li>dimensions: ...</li> |
||||||
|
<li>filesize: ...</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
{% endfor %} |
||||||
|
</div> |
||||||
|
|
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html> |
||||||
|
<head> |
||||||
|
<link rel="stylesheet" |
||||||
|
href="{{ url_for('static', filename='style.css') }}" |
||||||
|
type="text/css" |
||||||
|
/> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
|
||||||
|
<div> |
||||||
|
<ul> |
||||||
|
{% for album in albums %} |
||||||
|
<li><a href="/album/{{ album|urlencode }}">{{ album }}</a></li> |
||||||
|
{% endfor %} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
|
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,110 @@ |
|||||||
|
|
||||||
|
import os |
||||||
|
import re |
||||||
|
|
||||||
|
from PIL import Image |
||||||
|
|
||||||
|
from flask import Blueprint, current_app, redirect, render_template, url_for |
||||||
|
from flask import send_from_directory, Response |
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('default_routes', __name__) |
||||||
|
VALID_CHARS = re.compile(r"^[a-zA-Z0-9_\-\.]+$") |
||||||
|
|
||||||
|
# |
||||||
|
# Routes |
||||||
|
# |
||||||
|
|
||||||
|
@bp.route('/') |
||||||
|
def index() -> Response: |
||||||
|
album_dir = os.path.join(current_app.instance_path, 'images') |
||||||
|
album_dirs = [] |
||||||
|
|
||||||
|
if os.path.isdir(album_dir): |
||||||
|
entries = os.listdir(album_dir) |
||||||
|
for e in entries: |
||||||
|
if os.path.isdir(os.path.join(album_dir, e)): |
||||||
|
album_dirs.append(e) |
||||||
|
|
||||||
|
return render_template('index.j2', albums = sorted(album_dirs)) |
||||||
|
|
||||||
|
@bp.route('/album/<path>') |
||||||
|
def album(path:str = '') -> Response: |
||||||
|
image_dir = joinPath(current_app.instance_path, 'images') |
||||||
|
album_dir = joinPath(image_dir, path) |
||||||
|
images = [] |
||||||
|
|
||||||
|
if not validateDir(image_dir, path): |
||||||
|
return redirect(url_for('.index')) |
||||||
|
|
||||||
|
for entry in os.scandir(album_dir): |
||||||
|
if entry.is_file(): |
||||||
|
createThumbnail(entry, path) |
||||||
|
images.append(getImageDetails(entry)) |
||||||
|
|
||||||
|
return render_template('album.j2', |
||||||
|
img_root = f'/image/{path}/', |
||||||
|
album = path, |
||||||
|
images = images |
||||||
|
) |
||||||
|
|
||||||
|
@bp.route('/image/<path>/<img>') |
||||||
|
def image(path:str, img:str) -> Response: |
||||||
|
base_dir = os.path.join(current_app.instance_path, 'images') |
||||||
|
album_dir = os.path.join(base_dir, os.path.basename(path)) |
||||||
|
return send_from_directory(album_dir, img) |
||||||
|
|
||||||
|
@bp.route('/thumbs/<path>/<img>') |
||||||
|
def thumbs(path:str, img:str) -> Response: |
||||||
|
base_dir = os.path.join(current_app.instance_path, 'thumbnails') |
||||||
|
album_dir = os.path.join(base_dir, os.path.basename(path)) |
||||||
|
return send_from_directory(album_dir, img) |
||||||
|
|
||||||
|
|
||||||
|
# |
||||||
|
# Helpers |
||||||
|
# |
||||||
|
|
||||||
|
def joinPath(base:os.PathLike, path:str) -> os.PathLike: |
||||||
|
return os.path.join(base, os.path.basename(path)) |
||||||
|
|
||||||
|
def validateDir(base:os.PathLike, path:str) -> bool: |
||||||
|
if VALID_CHARS.match(path): |
||||||
|
path = joinPath(base, path) |
||||||
|
|
||||||
|
if os.path.exists(path) and os.path.isdir(path): |
||||||
|
return True |
||||||
|
|
||||||
|
return False |
||||||
|
|
||||||
|
def validateFile(base:os.PathLike, path:str) -> bool: |
||||||
|
if VALID_CHARS.match(path) and os.path.exists(joinPath(base, path)): |
||||||
|
return True |
||||||
|
|
||||||
|
return False |
||||||
|
|
||||||
|
def getImageDetails(entry:os.PathLike) -> dict: |
||||||
|
return { |
||||||
|
'name': entry.name, |
||||||
|
'date': '...', |
||||||
|
'dims': '...', |
||||||
|
'size': '...' |
||||||
|
} |
||||||
|
|
||||||
|
def createThumbnail(image:os.DirEntry, album:str) -> None: |
||||||
|
thumb_path = joinPath(current_app.instance_path, 'thumbnails') |
||||||
|
thumb_path = joinPath(thumb_path, album) |
||||||
|
|
||||||
|
# NOTE: check for existing thumbnail |
||||||
|
if validateFile(thumb_path, image.name): |
||||||
|
return |
||||||
|
|
||||||
|
# NOTE: ensure thumbnail album directory exists |
||||||
|
try: os.makedirs(thumb_path) |
||||||
|
except OSError: pass |
||||||
|
|
||||||
|
with Image.open(image.path) as im: |
||||||
|
im.thumbnail([160, 160]) |
||||||
|
im.save(joinPath(thumb_path, image.name), "JPEG") |
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,11 @@ |
|||||||
|
|
||||||
|
from setuptools import setup |
||||||
|
|
||||||
|
setup( |
||||||
|
name='gallery', |
||||||
|
version='0.1', |
||||||
|
packages=['gallery'], |
||||||
|
include_package_data=True, |
||||||
|
install_requires=['flask', 'pillow'] |
||||||
|
) |
||||||
|
|
||||||
Loading…
Reference in new issue