├── .gitignore ├── requirements.in ├── tests └── test_truth.py ├── screenshot.jpg ├── screenshot.png ├── dev_requirements.in ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── pyproject.toml ├── README.md ├── src └── masonry_viewer │ ├── __init__.py │ ├── templates │ └── index.html │ └── image_info.py ├── requirements.txt ├── LICENSE └── dev_requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | image_info.db 2 | *.pyc 3 | *.egg-info 4 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | flask 2 | Pillow 3 | sqlite_utils 4 | tqdm 5 | -------------------------------------------------------------------------------- /tests/test_truth.py: -------------------------------------------------------------------------------- 1 | def test_truth() -> None: 2 | assert True 3 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/masonry-viewer/main/screenshot.jpg -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwlchan/masonry-viewer/main/screenshot.png -------------------------------------------------------------------------------- /dev_requirements.in: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | mypy 4 | pytest-cov 5 | ruff 6 | types-tqdm 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "09:00" 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | time: "09:00" 13 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "masonry_viewer" 3 | version = "1.0" 4 | dynamic = ["dependencies"] 5 | 6 | [tool.setuptools.dynamic] 7 | dependencies = {file = ["requirements.txt"]} 8 | 9 | [tool.setuptools.packages.find] 10 | where = ["src"] 11 | 12 | [tool.setuptools.package-data] 13 | analytics = ["static/*", "templates/*"] 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # masonry-viewer 2 | 3 | This is a small Python web app that takes a local folder of images, and renders them in a "Masonry" layout. 4 | 5 |  6 | 7 | I made it after reading about the proposal for CSS Grid Level 3, aka "Masonry" layout, on [the WebKit blog](https://webkit.org/blog/15269/help-us-invent-masonry-layouts-for-css-grid-level-3/). 8 | I wanted to experiment with the new layout options. 9 | 10 | ## Installation 11 | 12 | ```console 13 | $ git clone https://github.com/alexwlchan/masonry-viewer.git 14 | $ cd masonry-viewer 15 | $ python -m venv .venv 16 | $ source .venv/bin/activate 17 | $ pip install -e . 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```console 23 | $ pip install -e . 24 | $ ROOT=/path/to/images/ python3 -m flask --app "masonry_viewer:app" run 25 | ``` 26 | 27 | ## License 28 | 29 | MIT. 30 | -------------------------------------------------------------------------------- /src/masonry_viewer/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | 4 | from flask import Flask, current_app, render_template, request, send_file 5 | 6 | from .image_info import get_image_info 7 | 8 | 9 | root = os.environ["ROOT"] 10 | 11 | app = Flask(__name__) 12 | app.config["ROOT"] = root 13 | 14 | get_image_info(root, show_progress=True) 15 | 16 | 17 | @app.route("/") 18 | def index() -> str: 19 | root = current_app.config["ROOT"] 20 | 21 | image_info = sorted(get_image_info(root), key=lambda info: random.random()) 22 | 23 | return render_template("index.html", image_info=image_info, root=root) 24 | 25 | 26 | @app.route("/image") 27 | def send_image(): 28 | path = request.args["path"] 29 | resp = send_file(os.path.abspath(path)) 30 | resp.cache_control.max_age = 3153600 31 | del resp.cache_control.no_cache 32 | resp.cache_control.public = True 33 | return resp 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.12" 23 | cache: 'pip' 24 | cache-dependency-path: 'dev_requirements.txt' 25 | 26 | - name: Install dependencies 27 | run: | 28 | pip install -r dev_requirements.txt 29 | pip install -e . 30 | 31 | - name: Run linting 32 | run: | 33 | ruff check . 34 | ruff format --check . 35 | 36 | - name: Check types 37 | run: mypy src tests 38 | 39 | - name: Run tests 40 | run: | 41 | coverage run -m pytest tests 42 | coverage report 43 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile requirements.in --output-file requirements.txt 3 | blinker==1.8.1 4 | # via flask 5 | click==8.1.7 6 | # via 7 | # click-default-group 8 | # flask 9 | # sqlite-utils 10 | click-default-group==1.2.4 11 | # via sqlite-utils 12 | flask==3.0.3 13 | # via -r requirements.in 14 | itsdangerous==2.2.0 15 | # via flask 16 | jinja2==3.1.4 17 | # via flask 18 | markupsafe==2.1.5 19 | # via 20 | # jinja2 21 | # werkzeug 22 | pillow==10.4.0 23 | # via -r requirements.in 24 | pluggy==1.5.0 25 | # via sqlite-utils 26 | python-dateutil==2.9.0.post0 27 | # via sqlite-utils 28 | six==1.16.0 29 | # via python-dateutil 30 | sqlite-fts4==1.0.3 31 | # via sqlite-utils 32 | sqlite-utils==3.37 33 | # via -r requirements.in 34 | tabulate==0.9.0 35 | # via sqlite-utils 36 | tqdm==4.66.5 37 | # via -r requirements.in 38 | werkzeug==3.0.3 39 | # via flask 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Alex Chan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/masonry_viewer/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 |