WIP: 2024-refactor #1

Draft
c0de wants to merge 52 commits from 2024-refactor into master
20 changed files with 798 additions and 291 deletions

8
.gitignore vendored
View File

@ -1,6 +1,8 @@
pathlist*.txt
thumbs/*
GenThumb.log
*.log
index*.html
.vscode
gallery.html
**/example/**
**/*venv/**
test.html

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"license.author": "Code Fox <c0de@c0de.dev>",
"license.filename": "license",
"license.extension": ".txt",
"license.default": "bsd-3-clause"
}

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 David Todd (c0de@c0defox.es)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

28
license.txt Normal file
View File

@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2023, Code Fox <c0de@c0de.dev>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

13
templates/base.html Normal file
View File

@ -0,0 +1,13 @@
{% include 'header.html' %}
{% include 'body_top.html' %}
{% include 'navbar.html' %}
<main>
{% include 'call_to_action.html' %}
{% include 'gallery.html' %}
</main>
{% include 'footer.html' %}
{% include 'body_bottom.html' %}

View File

@ -0,0 +1,19 @@
{% if static.scripts | length >= 1 +%}
<!-- Scripts -->
{% for _, script in static.scripts | items %}
{# Load scripts at the bottom by default #}
{% if not script.place_in_head | default(False) %}
<script
src="{{ script.src }}"
{{- 'integrity="' + script.integrity + '"' if script.integrity else omit }}
{{- 'crossorigin="' + script.crossorigin + '"' if script.integrity else omit }}
></script>
{% endif %}
{% endfor %}
{% endif %}
</body>
</html>

53
templates/body_top.html Normal file
View File

@ -0,0 +1,53 @@
<body>
<!-- Bootstrap Color Picker -->
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
<symbol id="check2" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</symbol>
<symbol id="circle-half" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/>
</symbol>
<symbol id="moon-stars-fill" viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
<path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/>
</symbol>
<symbol id="sun-fill" viewBox="0 0 16 16">
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
</symbol>
</svg>
<div class="dropdown position-fixed bottom-0 end-0 mb-3 me-3 bd-mode-toggle">
<button class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center"
id="bd-theme"
type="button"
aria-expanded="false"
data-bs-toggle="dropdown"
aria-label="Toggle theme (auto)">
<svg class="bi my-1 theme-icon-active" width="1em" height="1em"><use href="#circle-half"></use></svg>
<span class="visually-hidden" id="bd-theme-text">Toggle theme</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="bd-theme-text">
<li>
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false">
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"><use href="#sun-fill"></use></svg>
Light
<svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
</button>
</li>
<li>
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false">
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"><use href="#moon-stars-fill"></use></svg>
Dark
<svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
</button>
</li>
<li>
<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto" aria-pressed="true">
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"><use href="#circle-half"></use></svg>
Auto
<svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
</button>
</li>
</ul>
</div>
<!-- End Bootstrap Color Picker -->

View File

@ -0,0 +1,39 @@
<!--{#
#~ Var | Required | Default ~#
active | no | false
header | yes | none
lead_text | yes | none
action_buttons (list, optional)
n. (list index)
- href | no | #
- theme | no | primary
- icon | no | none
- text | yes | none
... (etc)
#}-->
{% if call_to_action.active | default(false) %}
<section class="py-5 text-center container">
<div class="row py-lg-5">
<div class="col-lg-6 col-md-8 mx-auto">
<h1 class="fw-light">{{ call_to_action.header }}</h1>
<p class="lead text-body-secondary">{{ call_to_action.lead_text }}</p>
{% if call_to_action.buttons %}
<p>
{% for button in call_to_action.buttons %}
<a
href="{{ button.href | default('#') }}"
class="btn btn-{{ button.theme | default('primary') }} my-2"
>
{% if button.icon %}
<i class="bi-{{ button.icon }}"></i>
{% endif %}
{{ button.text }}
</a>
{% endfor %}
</p>
{% endif %}
</div>
</div>
</section>
{% endif %}

View File

@ -1,8 +1,58 @@
<div class="col-md-3">
<a href="{{FULLLINK}}" target="{{FULLLINK}}">
<div class="card mb-3 bg-dark text-white">
<div class="card-header">{{TITLE}}</div>
<img class="card-img" width="250" src="{{THUMBNAIL}}" />
</div>
</a>
<div class="card shadow-sm">
{% if this.headline and this.headline|length >= 1 and not this.title %}
<div class="card-header position-relative">
{{ this.headline }}
{% if gallery.reactions_enabled|default(False) %}
<a href="#" class="btn btn-link float-end">
<i class="bi-heart-fill position-absolute translate-middle top-50 end-1 text-danger"></i>
</a>
{% endif %}
</div>
{% endif %}
<img
class="card-image-top"
src="{{ this.src }}"
alt="{{ this.alt_text if this.alt_text else this.title }}"
/>
{% if this.description and this.description|length >= 1 %}
<div class="card-body position-relative">
{% if this.title and this.title|length >= 1 %}
<h5 class="card-title">{{ this.title }}</h5>
{% if this.subtitle and this.subtitle|length >= 1 %}
<h6 class="card-subtitle mb-2 text-body-secondary">{{ this.subtitle }}</h6>
{% endif %}
{% if gallery.reactions_enabled|default(False) %}
<a href="#" class="btn btn-link position-absolute bottom-1 end-0">
<i class="bi-heart-fill text-danger"></i>
</a>
{% endif %}
{% endif %}
<p class="card-text">{{ this.description }}</p>
{% if this.subtext and this.subtext|length >= 1 %}
<p class="card-text">
<small class="text-body-secondary">
{{ this.subtext }}
</small>
</p>
{% endif %}
</div>
{% endif %}
{# Don't render reaction section if disabled or max reactions are zero (or less) #}
{% if gallery.reactions_enabled|default(False) or gallery.max_reactions_per_card|int <= 0 %}
{% include 'card_reaction_section.html' %}
{% endif %}
</div>

View File

@ -0,0 +1,22 @@
<div class="card" aria-hidden="true">
<div class="card-header">
<div class="d-flex align-items-center">
<strong role="status">Loading...</strong>
<div class="spinner-border ms-auto" aria-hidden="true"></div>
</div>
</div>
<svg class="bd-placeholder-img card-img-top" width="100%" height="225" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Placeholder: Loading..." preserveAspectRatio="xMidYMid slice" focusable="false"><title>Placeholder</title><rect width="100%" height="100%" fill="#55595c"/><text x="50%" y="50%" fill="#eceeef" dy=".3em">Loading...</text></svg>
<div class="card-body">
<h5 class="card-title placeholder-glow">
<span class="placeholder col-6"></span>
</h5>
<p class="card-text placeholder-glow">
<span class="placeholder col-7"></span>
<span class="placeholder col-4"></span>
<span class="placeholder col-4"></span>
<span class="placeholder col-6"></span>
<span class="placeholder col-8"></span>
</p>
</div>
</div>

View File

@ -0,0 +1,35 @@
{% if this.reactions and gallery.max_reactions_per_card|int >= 1 %}
<div class="card-footer text-body-secondary text-end">
{% set reaction_iterations = namespace(value=0) %}
{% for reaction, reaction_count in this.reactions|dictsort(by="value") %}
{# Don't render reaction if we've reached the max number, or its counter is 0 (or lower) #}
{% if reaction_iterations.value|int != gallery.max_reactions_per_card|int and reaction_count|int >= 1 %}
<button class="btn btn-link btn-sm disabled text-body position-relative">
{# TODO: Enable the button, and make clicking it toggle adding/removing that reaction to the counter, also color reaction button #}
<i class="bi-{{ reaction }}"></i>
{# Only show counter if more than 2 people gave the same reaction #}
{% if reaction_count|int >= 2 %}
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-body-inverted text-body-inverted">
{# Don't render reaction counts larger than 1000 #}
{{ reaction_count if reaction_count <= 999 else '1K+' }}
<span class="visually-hidden">
{{ reaction_count }} people reacted with {{ reaction }}
</span>
</span>
{% endif %}
</button>
{% set reaction_iterations.value = reaction_iterations.value + 1 %}
{% endif %}
{% endfor %}
{% set reaction_iterations = none %}
</div>
{% endif %}

23
templates/footer.html Normal file
View File

@ -0,0 +1,23 @@
<!--{#
#~ Var | Required | Default ~#
main_line | no | none
extra_lines (list)
- (text to print) | no | none
#}-->
<footer class="text-body-secondary py-5">
<div class="container">
<p class="float-end mb-1">
<a href="#">Back to top</a>
</p>
<p class="mb-1">{{ footer.main_line | default('') }}</p>
{% if footer.extra_lines %}
{% for line in footer.extra_lines %}
{% if line %}
<p class="mb-0">{{ line }}</p>
{% endif %}
{% endfor %}
{% endif %}
</div>
</footer>

12
templates/gallery.html Normal file
View File

@ -0,0 +1,12 @@
<div class="album py-5 bg-body-tertiary">
<div class="container">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3" data-memory='{"PercentPossition": true}'>
{% for this in gallery_items %}
<div class="col-md">{% include 'card.html' %}</div>
<div class="col-md">{% include 'card_loading.html' %}</div>
{% endfor %}
</div>
</div>
</div>

122
templates/header.html Normal file
View File

@ -0,0 +1,122 @@
<!--{#
#~ Var | Required | Default ~#
language | no | en
css_theme_name | no | auto
canonical_url | yes | none
page_title | yes | none
static (dict)
- scripts (dict)
- bootstrap (used in footer.html)
- src | yes | none
- integrity | no | empty
- crossorigin | no | empty if no integrity, anonymous otherwise
- theme
- src | yes | none
- integrity | no | empty
- crossorigin | no | empty if no integrity, anonymous otherwise
- css (dict)
- bootstrap
- src | yes | none
- integrity | no | empty
- crossorigin | no | empty if no integrity, anonymous otherwise
- theme
- src | yes | none
- integrity | no | empty
- crossorigin | no | empty if no integrity, anonymous otherwise
- icons
- src | yes | none
- integrity | no | empty
- crossorigin | no | empty if no integrity, anonymous otherwise
meta_list
n. (list index)
- name | yes | none
- content | yes | none
... (etc)
favicon_list (list of dicts)
n. (list index)
- rel | no | icon
- src | yes | none
- sizes | no | none
- type | no | none
- color | no | none
... (etc)
#}-->
<!doctype html>
<html
lang="{{ language | default('en') }}"
data-bs-theme="{{ css_theme_name | default('auto') }}"
>
<head>
{# Scripts that should be loaded first to avoid flickering (theme, etc) #}
{% if static.scripts | length >= 1 %}
<!-- Scripts -->
{% for _, script in static.scripts | items %}
{# Only load script if it should be in the head #}
{% if script.place_in_head | default(False) %}
<script
src="{{ script.src }}"
{{- 'integrity="' + script.integrity + '"' if script.integrity else omit }}
{{- 'crossorigin="' + script.crossorigin + '"' if script.integrity else omit }}
></script>
{% endif %}
{% endfor %}
{% endif %}
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="{{ charset | default('utf-8') }}" />
<link rel="canonical" href="{{ canonical_url }}" />
<title>{{ page_title }}</title>
{% if meta_list %}
<!-- meta-tags -->
{% for meta in meta_list %}
<meta name="{{ meta.name }}" content="{{ meta.content }}" />
{% endfor %}
{% endif %}
{% if favicon_list %}
<!-- Favicons -->
{% for icon in favicon_list %}
<link
rel="{{ icon.rel | default('icon') }}"
href="{{ icon.src }}"
sizes="{{ icon.sizes | default('') }}"
type="{{ icon.type | default('') }}"
color="{{ icon.color | default('') }}"
/>
{% endfor %}
{% endif %}
<!--
#~ These are to be provided by the favicon_list above ~#
<link rel="apple-touch-icon" href="/docs/5.3/assets/img/favicons/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="/docs/5.3/assets/img/favicons/favicon-32x32.png" sizes="32x32" type="image/png">
<link rel="icon" href="/docs/5.3/assets/img/favicons/favicon-16x16.png" sizes="16x16" type="image/png">
<link rel="manifest" href="/docs/5.3/assets/img/favicons/manifest.json">
<link rel="mask-icon" href="/docs/5.3/assets/img/favicons/safari-pinned-tab.svg" color="#712cf9">
<link rel="icon" href="/docs/5.3/assets/img/favicons/favicon.ico">
-->
{% if static.css | length >= 1 +%}
<!-- CSS -->
{% for _, style in static.css | items %}
<link
rel="stylesheet"
href="{{ style.src }}"
{{- 'integrity="' + style.integrity + '"' if style.integrity else omit }}
{{- 'crossorigin="' + style.crossorigin + '"' if style.integrity else omit }}
/>
{% endfor %}
{% endif %}
</head>

57
templates/navbar.html Normal file
View File

@ -0,0 +1,57 @@
<!--{#
#~ Var | Required | Default ~#
theme | no | dark
text_theme | no | white
page_title | yes | none
left_header | no | About
left_text | no | none
right_header | no | none
link_list
n. (list index)
- href | yes | none
- text | yes | none
- icon | no | none
... (etc)
#}-->
<header data-bs-theme="{{ theme | default('dark') }}">
<div class="collapse text-bg-{{ theme | default('dark') }}" id="navbarHeader">
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-7 py-4">
<h4>{{ left_header | default('About') }}</h4>
<p class="text-body-secondary">{{ left_text | default('') }}</p>
</div>
<div class="col-sm-4 offset-md-1 py-4">
<h4>{{ right_header | default('') }}</h4>
{% if link_list %}
<ul class="list-unstyled">
{% for link in link_list %}
{% if link.href and link.text %}
<li><a href={{ link.href }} class="text-{{ text_theme | default('white') }}">
{% if link.icon %}
<i class="bi-{{ link.icon }}"></i>
{% endif %}
{{ link.text }}
</a></li>
{% endif %}
{% endfor %}
</ul>
{% endif %}
</div>
</div>
</div>
</div>
<div class="navbar navbar-{{ theme | default('dark') }} bg-{{ theme | default('dark') }} shadow-sm">
<div class="container">
<a href="#" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="me-2" viewBox="0 0 24 24"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
<strong>{{ page_title }}</strong>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
</header>

View File

@ -1,123 +0,0 @@
<html>
<head>
<title>Simple S3 Gallery</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="David Todd" />
<link rel="icon" type="image/png" href="https://secure.gravatar.com/avatar/1e346a54257cf0a9932fcfc1e61c015d" />
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<style>
.jumbotron {
padding-top: 3rem;
padding-bottom: 3rem;
margin-bottom: 0;
background-color: #fff;
}
@media (min-width: 768px) {
.jumbotron {
padding-top: 6rem;
padding-bottom: 6rem;
}
}
.jumbotron p:last-child {
margin-bottom: 0;
}
.jumbotron-heading {
font-weight: 300;
}
.jumbotron .container {
max-width: 40rem;
}
.card-header {
text-align: center;
padding: 0.25rem 0rem;
}
footer {
padding-top: 3rem;
padding-bottom: 3rem;
}
footer p {
margin-bottom: .25rem;
}
</style>
</head>
<body>
<header>
<div class="collapse bg-dark" id="navbarHeader">
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-7 py-4">
<h4 class="text-white">About</h4>
<p class="text-muted">This was a small gallery that I put together to make my photography available to the world. Images are grouped in folders for what I've done.</p>
</div>
<div class="col-sm-4 offset-md-1 py-4">
<h4 class="text-white">Contact</h4>
<ul class="list-unstyled">
<li><a href="https://c0defox.es" class="text-white">My contact site</a></li>
<li><a href="https://t.me/c0defox" class="text-white">Hit me up on Telegram</a></li>
<li><a href="c0de#0689" class="text-white">My Discord is c0de#0689</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container d-flex justify-content-between">
<a href="#" class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="mr-2" viewBox="0 0 24 24" focusable="false"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
<strong>Simple S3 Gallery</strong>
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
</header>
<main role="main">
<!-- Optional for creating a notification -->
<!--
<section class="jumbotron text-center">
<div class="container">
<h1 class="jumbotron-heading">Album example</h1>
<p class="lead text-muted">Something short and leading about the collection below—its contents, the creator, etc. Make it short and sweet, but not too short so folks dont simply skip over it entirely.</p>
<p>
<a href="#" class="btn btn-primary my-2">Main call to action</a>
<a href="#" class="btn btn-secondary my-2">Secondary action</a>
</p>
</div>
</section>
-->
<div class="album py-4 bg-dark">
<div class="container">
<div class="row">
{{THUMBROW}}
</div>
</div>
</div>
</main>
<footer class="text-muted">
<div class="container">
<p class="float-right"><a href="#">Back to top</a></p>
<p><a href="https://github.com/alopexc0de/simple-s3-gallery"></a>Simple S3 Gallery</a> &copy; 2019 <a href="https://c0defox.es">David Todd</a> - All photos are &copy; David Todd</p>
</div>
</footer>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -0,0 +1,80 @@
/*!
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
* Copyright 2011-2023 The Bootstrap Authors
* Licensed under the Creative Commons Attribution 3.0 Unported License.
*/
(() => {
'use strict'
const getStoredTheme = () => localStorage.getItem('theme')
const setStoredTheme = theme => localStorage.setItem('theme', theme)
const getPreferredTheme = () => {
const storedTheme = getStoredTheme()
if (storedTheme) {
return storedTheme
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
const setTheme = theme => {
if (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.setAttribute('data-bs-theme', 'dark')
} else {
document.documentElement.setAttribute('data-bs-theme', theme)
}
}
setTheme(getPreferredTheme())
const showActiveTheme = (theme, focus = false) => {
const themeSwitcher = document.querySelector('#bd-theme')
if (!themeSwitcher) {
return
}
const themeSwitcherText = document.querySelector('#bd-theme-text')
const activeThemeIcon = document.querySelector('.theme-icon-active use')
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
element.classList.remove('active')
element.setAttribute('aria-pressed', 'false')
})
btnToActive.classList.add('active')
btnToActive.setAttribute('aria-pressed', 'true')
activeThemeIcon.setAttribute('href', svgOfActiveBtn)
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
if (focus) {
themeSwitcher.focus()
}
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const storedTheme = getStoredTheme()
if (storedTheme !== 'light' && storedTheme !== 'dark') {
setTheme(getPreferredTheme())
}
})
window.addEventListener('DOMContentLoaded', () => {
showActiveTheme(getPreferredTheme())
document.querySelectorAll('[data-bs-theme-value]')
.forEach(toggle => {
toggle.addEventListener('click', () => {
const theme = toggle.getAttribute('data-bs-theme-value')
setStoredTheme(theme)
setTheme(theme)
showActiveTheme(theme, true)
})
})
})
})()

View File

@ -0,0 +1,88 @@
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
.b-example-divider {
width: 100%;
height: 3rem;
background-color: rgba(0, 0, 0, .1);
border: solid rgba(0, 0, 0, .15);
border-width: 1px 0;
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
}
.b-example-vr {
flex-shrink: 0;
width: 1.5rem;
height: 100vh;
}
.bi {
vertical-align: -.125em;
fill: currentColor;
}
.nav-scroller {
position: relative;
z-index: 2;
height: 2.75rem;
overflow-y: hidden;
}
.nav-scroller .nav {
display: flex;
flex-wrap: nowrap;
padding-bottom: 1rem;
margin-top: -1px;
overflow-x: auto;
text-align: center;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.btn-bd-primary {
--bd-violet-bg: #712cf9;
--bd-violet-rgb: 112.520718, 44.062154, 249.437846;
--bs-btn-font-weight: 600;
--bs-btn-color: var(--bs-white);
--bs-btn-bg: var(--bd-violet-bg);
--bs-btn-border-color: var(--bd-violet-bg);
--bs-btn-hover-color: var(--bs-white);
--bs-btn-hover-bg: #6528e0;
--bs-btn-hover-border-color: #6528e0;
--bs-btn-focus-shadow-rgb: var(--bd-violet-rgb);
--bs-btn-active-color: var(--bs-btn-hover-color);
--bs-btn-active-bg: #5a23c8;
--bs-btn-active-border-color: #5a23c8;
}
.bd-mode-toggle {
z-index: 1500;
}
.bd-mode-toggle .dropdown-menu .active .bi {
display: block !important;
}
/*
Surprised this doesn't exist by default.
Inverted colors for the current theme; works with the color switcher
*/
.bg-body-inverted {
background-color: var(--bs-body-color);
}
.text-body-inverted {
color: var(--bs-body-bg);
}

View File

@ -1,137 +0,0 @@
<html>
<head>
<title>Simple S3 Gallery</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="David Todd" />
<link rel="icon" type="image/png" href="https://secure.gravatar.com/avatar/1e346a54257cf0a9932fcfc1e61c015d" />
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<style>
.bg-dark {
background-color: #343a40cc !important;
}
nav.bcrumb {
margin-left: 1rem;
margin-top: 0.5rem;
}
.jumbotron {
padding-top: 3rem;
padding-bottom: 3rem;
margin-bottom: 0;
background-color: #fff;
}
@media (min-width: 768px) {
.jumbotron {
padding-top: 6rem;
padding-bottom: 6rem;
}
}
.jumbotron p:last-child {
margin-bottom: 0;
}
.jumbotron-heading {
font-weight: 300;
}
.jumbotron .container {
max-width: 40rem;
}
.card-header {
text-align: center;
padding: 0.25rem 0rem;
}
footer {
padding-top: 3rem;
padding-bottom: 3rem;
}
footer p {
margin-bottom: .25rem;
}
</style>
</head>
<body>
<header>
<div class="collapse bg-dark" id="navbarHeader">
<div class="container">
<div class="row">
<div class="col-sm-8 col-md-7 py-4">
<h4 class="text-white">About</h4>
<p class="text-white">This is a small gallery to represent images that I have taken over the past few years. Most of these are unsorted and unedited, and thus a lot are blurry or have lighting issues. I have taken, and thereby own all photos on this site.</p>
</div>
<div class="col-sm-4 offset-md-1 py-4">
<h4 class="text-white">Contact</h4>
<ul class="list-unstyled">
<li><a href="https://c0defox.es" class="text-white">My contact site</a></li>
<li><a href="https://t.me/c0defox" class="text-white">Hit me up on Telegram</a></li>
<li><a href="c0de#0689" class="text-white">My Discord is c0de#0689</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="navbar navbar-dark bg-dark shadow-sm">
<div class="container d-flex justify-content-between">
<span class="navbar-brand d-flex align-items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" aria-hidden="true" class="mr-2" viewBox="0 0 24 24" focusable="false"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
<strong>Simple S3 Gallery</strong>
<nav class="text-dark bcrumb" aria-label="breadcrumb">
<ol class="breadcrumb">
{{BREADCRUMBS}}
</ol>
</nav>
</span>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarHeader" aria-controls="navbarHeader" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
</header>
<main role="main">
<!-- Optional for creating a notification -->
<!--
<section class="jumbotron text-center">
<div class="container">
<h1 class="jumbotron-heading">Album example</h1>
<p class="lead text-muted">Something short and leading about the collection below—its contents, the creator, etc. Make it short and sweet, but not too short so folks dont simply skip over it entirely.</p>
<p>
<a href="#" class="btn btn-primary my-2">Main call to action</a>
<a href="#" class="btn btn-secondary my-2">Secondary action</a>
</p>
</div>
</section>
-->
<div class="album py-3 bg-dark">
<div class="container">
<div class="row">
{{THUMBROW}}
</div>
</div>
</div>
</main>
<footer class="text-muted">
<div class="container">
<p class="float-right"><a href="#">Back to top</a></p>
<p><a href="https://github.com/alopexc0de/simple-s3-gallery">Simple S3 Gallery</a> &copy; 2019 <a href="https://c0defox.es">David Todd</a> - All photos are &copy; David Todd</p>
</div>
</footer>
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>

139
test.py Normal file
View File

@ -0,0 +1,139 @@
#!/usr/bin/python3
"""
test template rendering
python3 test.py | tee test.html
python3 -m http.server
"""
from jinja2 import Environment, FileSystemLoader, select_autoescape
template_vars = {
"charset": "utf-8",
"language": "en",
"css_theme_name": "auto",
"canonical_url": "http://example.com",
"page_title": "This is a title",
"theme": "dark",
"text_theme": "white",
"static": {
"scripts": {
"bootstrap": {
"src": "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js",
},
"theme": {
"src": "./templates/static/color-modes.js",
"place_in_head": True,
},
"masonry": {
"src": "https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js"
},
},
"css": {
"bootstrap": {"src": "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"},
"theme": {"src": "./templates/static/gallery.css"},
"icons": {
"src": "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css"
},
},
},
"meta_list": [
{"name": "description", "content": "This is a description"},
{"name": "generator", "content": "simple-s3-gallery v.2023.12"},
{"name": "author", "content": "Code Fox"},
{"name": "theme-color", "content": "#712cf9"},
],
"left_header": "About the gallery",
"left_text": "This is an example gallery that is generated via a template",
"right_header": "Contact",
"link_list": [
{"href": "https://c0defox.es", "text": "Fox :3"},
{"href": "https://furry.engineer/c0de", "text": "Mastodon", "icon": "mastodon"},
],
"footer": {
"main_line": "Copyright 2024 Code Fox",
"extra_lines": ["This is another line", "This is yet another line"],
},
"call_to_action": {
"active": False,
"header": "This is a call to action",
"lead_text": "Some interesting text for the user",
"buttons": [
{"text": "something"},
{"theme": "danger", "text": "something dangerous"},
{"href": "3", "theme": "warning", "icon": "exclamation-diamond-fill"},
{
"href": "3",
"theme": "warning",
"icon": "exclamation-diamond-fill",
"text": "some text",
},
],
},
"gallery": {
"max_reactions_per_card": 9,
"reactions_enabled": True,
},
"gallery_items": [
{
"src": "https://files.c0defox.es/Pictures/arctic-fox.jpg",
"headline": "Alopex-Vulpes",
"description": "An arctic fox :3",
"reactions": {
"0-circle": 1,
"1-circle": 2,
"2-circle": 3,
"3-circle": 4,
"4-circle": 5,
"5-circle": 6,
"6-circle": 7,
"7-circle": 8,
"8-circle": 9,
"9-circle": 10,
"0-circle-fill": 11,
"1-circle-fill": 12,
"2-circle-fill": 13,
"3-circle-fill": 14,
"4-circle-fill": 15,
"5-circle-fill": 16,
"6-circle-fill": 17,
"7-circle-fill": 18,
"8-circle-fill": 19,
"9-circle-fill": 20,
},
},
{
"src": "https://files.c0defox.es/Pictures/20150825_114759.jpg",
"description": "The inside of a Data Center",
"title": "Data Center",
"reactions": {"heart-fill": 1024, "bag-x-fill": 50},
},
{
"src": "https://secure.gravatar.com/avatar/1e346a54257cf0a9932fcfc1e61c015d?s=200",
"description": "",
"title": "",
},
{
"src": "https://c0defox.es/paw-tail.svg",
"alt_text": "a fox tail next to a paw print",
"title": "",
},
],
}
def main():
"""main method"""
env = Environment(
loader=FileSystemLoader("templates"),
autoescape=select_autoescape(),
trim_blocks=True,
lstrip_blocks=True,
)
template = env.get_template("base.html")
print(template.render(**template_vars))
if __name__ == "__main__":
main()