mirror of
https://gitlab.com/octospacc/Pignio.git
synced 2025-07-17 22:37:38 +02:00
AJAX loading of links, forms and home page items with Unpoly; Fix bad horizontal overflow introduced by flex; Initial support for videos; Initial support of systags and storing media provenance; More efficient and safe metadata storing (only non-void values are saved)
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
/_config.py
|
/_config.py
|
||||||
/data/
|
/data/
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/.mypy_cache/
|
.mypy_cache/
|
||||||
*.pyc
|
*.pyc
|
53
app.py
53
app.py
@ -91,7 +91,11 @@ def noindex(view_func):
|
|||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template("index.html", items=walk_items())
|
limit = 50
|
||||||
|
page = int(request.args.get("page", 1))
|
||||||
|
next_count = (limit * page)
|
||||||
|
items = walk_items()
|
||||||
|
return render_template("index.html", items=items[(limit * (page - 1)):next_count], next_page=(page + 1 if len(items) > next_count else None))
|
||||||
|
|
||||||
@app.route("/manifest.json")
|
@app.route("/manifest.json")
|
||||||
@noindex
|
@noindex
|
||||||
@ -102,7 +106,12 @@ def serve_manifest():
|
|||||||
|
|
||||||
@app.route("/static/module/<path:module>/<path:filename>")
|
@app.route("/static/module/<path:module>/<path:filename>")
|
||||||
@noindex
|
@noindex
|
||||||
def serve_module(module:str, filename:str):
|
def serve_module_main(module:str, filename:str):
|
||||||
|
return send_from_directory(os.path.join("node_modules", module), filename)
|
||||||
|
|
||||||
|
@app.route("/static/module/dist/<path:module>/<path:filename>")
|
||||||
|
@noindex
|
||||||
|
def serve_module_dist(module:str, filename:str):
|
||||||
return send_from_directory(os.path.join("node_modules", module, "dist"), filename)
|
return send_from_directory(os.path.join("node_modules", module, "dist"), filename)
|
||||||
|
|
||||||
@app.route("/media/<path:filename>")
|
@app.route("/media/<path:filename>")
|
||||||
@ -127,14 +136,16 @@ def view_user(username:str):
|
|||||||
def search():
|
def search():
|
||||||
query = request.args.get("query", "").lower()
|
query = request.args.get("query", "").lower()
|
||||||
found = False
|
found = False
|
||||||
results = {}
|
results = [] # {}
|
||||||
|
|
||||||
for folder, items in walk_items().items():
|
for item in walk_items():
|
||||||
results[folder] = []
|
# for folder, items in walk_items().items():
|
||||||
|
# results[folder] = []
|
||||||
|
|
||||||
for item in items:
|
#for item in items:
|
||||||
if any([query in text.lower() for text in item.values()]):
|
if any([query in text.lower() for text in item.values()]):
|
||||||
results[folder].append(item)
|
# results[folder].append(item)
|
||||||
|
results.append(item)
|
||||||
found = True
|
found = True
|
||||||
|
|
||||||
return render_template("search.html", items=(results if found else None), query=query)
|
return render_template("search.html", items=(results if found else None), query=query)
|
||||||
@ -282,7 +293,7 @@ def walk_items():
|
|||||||
data = load_item(iid)
|
data = load_item(iid)
|
||||||
results[rel_path].append(data)
|
results[rel_path].append(data)
|
||||||
|
|
||||||
return results
|
return [value for values in results.values() for value in values] # results
|
||||||
|
|
||||||
def walk_collections(username:str):
|
def walk_collections(username:str):
|
||||||
results: dict[str, list[str]] = {"": []}
|
results: dict[str, list[str]] = {"": []}
|
||||||
@ -330,9 +341,10 @@ def load_item(iid:str):
|
|||||||
for file in files:
|
for file in files:
|
||||||
if file.lower().endswith(ITEMS_EXT):
|
if file.lower().endswith(ITEMS_EXT):
|
||||||
data = data | read_metadata(read_textual(file))
|
data = data | read_metadata(read_textual(file))
|
||||||
|
|
||||||
elif file.lower().endswith(tuple([f".{ext}" for ext in EXTENSIONS["image"]])):
|
elif file.lower().endswith(tuple([f".{ext}" for ext in EXTENSIONS["image"]])):
|
||||||
data["image"] = file.replace(os.sep, "/").removeprefix(f"{ITEMS_ROOT}/")
|
data["image"] = file.replace(os.sep, "/").removeprefix(f"{ITEMS_ROOT}/")
|
||||||
|
elif file.lower().endswith(tuple([f".{ext}" for ext in EXTENSIONS["video"]])):
|
||||||
|
data["video"] = file.replace(os.sep, "/").removeprefix(f"{ITEMS_ROOT}/")
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -343,6 +355,7 @@ def store_item(iid:str, data:dict, files:dict|None=None):
|
|||||||
filepath = os.path.join(ITEMS_ROOT, *filename)
|
filepath = os.path.join(ITEMS_ROOT, *filename)
|
||||||
mkdirs(os.path.join(ITEMS_ROOT, filename[0]))
|
mkdirs(os.path.join(ITEMS_ROOT, filename[0]))
|
||||||
image = False
|
image = False
|
||||||
|
extra = {key: data[key] for key in ["provenance"] if key in data}
|
||||||
data = {key: data[key] for key in ["link", "title", "description", "image", "text"] if key in data}
|
data = {key: data[key] for key in ["link", "title", "description", "image", "text"] if key in data}
|
||||||
|
|
||||||
if files and len(files):
|
if files and len(files):
|
||||||
@ -351,7 +364,7 @@ def store_item(iid:str, data:dict, files:dict|None=None):
|
|||||||
file.seek(0, os.SEEK_SET)
|
file.seek(0, os.SEEK_SET)
|
||||||
mime = file.content_type.split("/")
|
mime = file.content_type.split("/")
|
||||||
ext = mime[1]
|
ext = mime[1]
|
||||||
if mime[0] == "image" and ext in EXTENSIONS["image"]:
|
if mime[0] == "image" and ext in EXTENSIONS["image"] or mime[0] == "video" and ext in EXTENSIONS["video"]:
|
||||||
file.save(f"{filepath}.{ext}")
|
file.save(f"{filepath}.{ext}")
|
||||||
image = True
|
image = True
|
||||||
if not image and "image" in data and data["image"]:
|
if not image and "image" in data and data["image"]:
|
||||||
@ -373,8 +386,8 @@ def store_item(iid:str, data:dict, files:dict|None=None):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
if "creator" in existing:
|
if (creator := safe_str_get(existing, "creator")):
|
||||||
data["creator"] = existing["creator"]
|
data["creator"] = creator
|
||||||
else:
|
else:
|
||||||
data["creator"] = current_user.username
|
data["creator"] = current_user.username
|
||||||
items = current_user.data["items"] if "items" in current_user.data else []
|
items = current_user.data["items"] if "items" in current_user.data else []
|
||||||
@ -382,6 +395,8 @@ def store_item(iid:str, data:dict, files:dict|None=None):
|
|||||||
current_user.data["items"] = items
|
current_user.data["items"] = items
|
||||||
write_textual(current_user.filepath, write_metadata(current_user.data))
|
write_textual(current_user.filepath, write_metadata(current_user.data))
|
||||||
|
|
||||||
|
if (provenance := safe_str_get(extra, "provenance")):
|
||||||
|
data["systags"] = provenance
|
||||||
write_textual(filepath + ITEMS_EXT, write_metadata(data))
|
write_textual(filepath + ITEMS_EXT, write_metadata(data))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -404,12 +419,15 @@ def read_metadata(text:str) -> dict:
|
|||||||
def write_metadata(data:dict) -> str:
|
def write_metadata(data:dict) -> str:
|
||||||
output = StringIO()
|
output = StringIO()
|
||||||
config = ConfigParser(interpolation=None)
|
config = ConfigParser(interpolation=None)
|
||||||
for key in ("image", "datetime"):
|
for key in ("image", "video", "datetime"):
|
||||||
if key in data:
|
if key in data:
|
||||||
del data[key]
|
del data[key]
|
||||||
for key in data:
|
for key in list(data.keys()):
|
||||||
if type(data[key]) == list:
|
if (value := data[key]):
|
||||||
data[key] = list_to_wsv(data[key])
|
if type(value) == list:
|
||||||
|
data[key] = list_to_wsv(value)
|
||||||
|
else:
|
||||||
|
del data[key]
|
||||||
config["DEFAULT"] = data
|
config["DEFAULT"] = data
|
||||||
config.write(output)
|
config.write(output)
|
||||||
return "\n".join(output.getvalue().splitlines()[1:]) # remove section header
|
return "\n".join(output.getvalue().splitlines()[1:]) # remove section header
|
||||||
@ -457,6 +475,9 @@ def list_to_wsv(data:list, sep="\n") -> str:
|
|||||||
def wsv_to_list(data:str) -> list:
|
def wsv_to_list(data:str) -> list:
|
||||||
return data.strip().replace(" ", "\n").replace("\t", "\n").splitlines()
|
return data.strip().replace(" ", "\n").replace("\t", "\n").splitlines()
|
||||||
|
|
||||||
|
def safe_str_get(dikt:dict[str, str|None], key:str) -> str:
|
||||||
|
return dikt.get(key) or ""
|
||||||
|
|
||||||
def mkdirs(*paths:str):
|
def mkdirs(*paths:str):
|
||||||
for path in paths:
|
for path in paths:
|
||||||
Path(path).mkdir(parents=True, exist_ok=True)
|
Path(path).mkdir(parents=True, exist_ok=True)
|
||||||
|
9
package-lock.json
generated
9
package-lock.json
generated
@ -5,7 +5,8 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"uikit": "^3.23.11"
|
"uikit": "^3.23.11",
|
||||||
|
"unpoly": "^3.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uikit": {
|
"node_modules/uikit": {
|
||||||
@ -13,6 +14,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/uikit/-/uikit-3.23.11.tgz",
|
"resolved": "https://registry.npmjs.org/uikit/-/uikit-3.23.11.tgz",
|
||||||
"integrity": "sha512-srUFBf5DfUxVpodcygibMQt1vgQjR9wlhIQo4GeWVpugk5+mKLPASJITDoY8wcwXQIHm7koELiPJ+FgNbzLv0A==",
|
"integrity": "sha512-srUFBf5DfUxVpodcygibMQt1vgQjR9wlhIQo4GeWVpugk5+mKLPASJITDoY8wcwXQIHm7koELiPJ+FgNbzLv0A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/unpoly": {
|
||||||
|
"version": "3.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpoly/-/unpoly-3.11.0.tgz",
|
||||||
|
"integrity": "sha512-9ozLaNvg17XAUd9c+gMCfDqZePDeb1fGpqQHAuQDMRF/Vf8X94rdrgo6A5oqt4PlxcywoJ8QGH4yS6J0vFZxhg==",
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"uikit": "^3.23.11"
|
"uikit": "^3.23.11",
|
||||||
|
"unpoly": "^3.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
var link = document.querySelector('form input[name="link"]');
|
if ('up' in window) {
|
||||||
var check = document.querySelector('form input[type="checkbox"]');
|
up.compiler('form.add', addHandler);
|
||||||
var image = document.querySelector('form img.image');
|
} else {
|
||||||
var upload = document.querySelector('form input[name="file"]');
|
var form = document.querySelector('form.add');
|
||||||
|
if (form) {
|
||||||
|
addHandler(form);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addHandler(form) {
|
||||||
|
var link = form.querySelector('input[name="link"]');
|
||||||
|
var checkLink = form.querySelector('input.from-link');
|
||||||
|
// var checkProxatore = form.querySelector('input.with-proxatore');
|
||||||
|
var image = form.querySelector('img.image');
|
||||||
|
var upload = form.querySelector('input[name="file"]');
|
||||||
|
|
||||||
upload.addEventListener('change', function(ev) {
|
upload.addEventListener('change', function(ev) {
|
||||||
const file = ev.target.files[0];
|
const file = ev.target.files[0];
|
||||||
@ -36,16 +47,16 @@ document.addEventListener('paste', function(ev) {
|
|||||||
['change', 'input', 'paste'].forEach(handler => {
|
['change', 'input', 'paste'].forEach(handler => {
|
||||||
link.addEventListener(handler, () => {
|
link.addEventListener(handler, () => {
|
||||||
var url = link.value.trim();
|
var url = link.value.trim();
|
||||||
if (check.checked && url) {
|
if (checkLink.checked && url) {
|
||||||
fetch('../api/preview?url=' + encodeURIComponent(url))
|
fetch('../api/preview?url=' + encodeURIComponent(url))
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
for (var key in data) {
|
for (var key in data) {
|
||||||
var field = document.querySelector(`form [name="${key}"]`);
|
var field = form.querySelector(`[name="${key}"]`);
|
||||||
if (field) {
|
if (field) {
|
||||||
field.value = data[key];
|
field.value = data[key];
|
||||||
}
|
}
|
||||||
var el = document.querySelector(`form [class="${key}"]`);
|
var el = form.querySelector(`[class="${key}"]`);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.src = data[key];
|
el.src = data[key];
|
||||||
el.parentElement.hidden = false;
|
el.parentElement.hidden = false;
|
||||||
@ -55,3 +66,4 @@ document.addEventListener('paste', function(ev) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
}
|
@ -11,23 +11,34 @@
|
|||||||
<div>b</div>
|
<div>b</div>
|
||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
<form method="POST" enctype="multipart/form-data">
|
<form method="POST" enctype="multipart/form-data" class="add">
|
||||||
<input type="hidden" name="id" value="{{ item.id }}" />
|
<input type="hidden" name="id" value="{{ item.id }}" />
|
||||||
<input type="hidden" name="image" />
|
<input type="hidden" name="image" />
|
||||||
<div class="uk-margin" {% if not item.image %} hidden {% endif %}>
|
<div class="uk-margin" {% if not item.image %} hidden {% endif %}>
|
||||||
<img src="{{ url_for('serve_media', filename=item.image) }}" class="image" />
|
<img src="{{ url_for('serve_media', filename=item.image) }}" class="image" />
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-margin">
|
<div class="uk-margin uk-grid">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" class="uk-checkbox" {% if not item.id %} checked {% endif %} />
|
<input type="checkbox" class="from-link uk-checkbox" {% if not item.id %} checked {% endif %} />
|
||||||
Fill data from link
|
Fill data from link
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<select class="uk-select" name="provenance">
|
||||||
|
<option value="">No provenance</option>
|
||||||
|
<option value="oc" {% if item.systags == 'oc' %} selected {% endif %}>Original content</option>
|
||||||
|
<option value="ai" {% if item.systags == 'ai' %} selected {% endif %}>AI-generated</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<!-- <label>
|
||||||
|
<input type="checkbox" class="with-proxatore uk-checkbox" />
|
||||||
|
Transparently use Proxatore
|
||||||
|
</label> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<div class="uk-grid-collapse" uk-grid>
|
<div class="uk-grid-collapse" uk-grid>
|
||||||
<div uk-form-custom class="uk-width-1-1">
|
<div uk-form-custom class="uk-width-1-1">
|
||||||
<input class="uk-input" type="file" name="file" />
|
<input class="uk-input" type="file" name="file" />
|
||||||
<button class="uk-button uk-width-1-1 uk-button-default" type="button" tabindex="-1">Select or drop an image file</button>
|
<button class="uk-button uk-width-1-1 uk-button-default" type="button" tabindex="-1">Select or drop a media file</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -50,5 +61,4 @@
|
|||||||
<button class="uk-input uk-button uk-button-primary" type="submit">{{ ("Edit " + item.id) if item.id else "Add" }}</button>
|
<button class="uk-input uk-button uk-button-primary" type="submit">{{ ("Edit " + item.id) if item.id else "Add" }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<script src="{{ url_for('static', filename='add.js') }}"></script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -6,9 +6,16 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{% if title %}{{ title }} | {% endif %}{{ config.APP_ICON }} {{ config.APP_NAME }}</title>
|
<title>{% if title %}{{ title }} | {% endif %}{{ config.APP_ICON }} {{ config.APP_NAME }}</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('serve_module', module='uikit', filename='css/uikit.min.css') }}" />
|
<link rel="stylesheet" href="{{ url_for('serve_module_dist', module='uikit', filename='css/uikit.min.css') }}" />
|
||||||
<script src="{{ url_for('serve_module', module='uikit', filename='js/uikit.min.js') }}"></script>
|
<script src="{{ url_for('serve_module_dist', module='uikit', filename='js/uikit.min.js') }}"></script>
|
||||||
<script src="{{ url_for('serve_module', module='uikit', filename='js/uikit-icons.min.js') }}"></script>
|
<script src="{{ url_for('serve_module_dist', module='uikit', filename='js/uikit-icons.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('serve_module_main', module='unpoly', filename='unpoly.min.js') }}" onload="(function(){
|
||||||
|
up.link.config.followSelectors.push('a[href]');
|
||||||
|
up.link.config.instantSelectors.push('a[href]');
|
||||||
|
up.form.config.submitSelectors.push(['form']);
|
||||||
|
})();"></script>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('serve_module_main', module='unpoly', filename='unpoly.min.css') }}" />
|
||||||
|
<script src="{{ url_for('static', filename='add.js') }}" defer></script>
|
||||||
{% if canonical %}
|
{% if canonical %}
|
||||||
<link rel="canonical" href="{{ config.LINKS_PREFIX }}{{ canonical }}" />
|
<link rel="canonical" href="{{ config.LINKS_PREFIX }}{{ canonical }}" />
|
||||||
<meta name="og:url" content="{{ config.LINKS_PREFIX }}{{ canonical }}" />
|
<meta name="og:url" content="{{ config.LINKS_PREFIX }}{{ canonical }}" />
|
||||||
@ -43,8 +50,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="uk-flex uk-flex-center uk-margin uk-flex-auto">
|
<div class="uk-margin uk-flex-auto">
|
||||||
<div class="uk-container uk-flex-auto">
|
<div class="uk-container">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
|
@ -4,4 +4,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{% elif item.image %}
|
{% elif item.image %}
|
||||||
<img class="uk-width-expand" src="{{ url_for('serve_media', filename=item.image) }}" alt="{{ item.description }}" />
|
<img class="uk-width-expand" src="{{ url_for('serve_media', filename=item.image) }}" alt="{{ item.description }}" />
|
||||||
|
{% elif item.video %}
|
||||||
|
<video src="{{ url_for('serve_media', filename=item.video) }}" {% if full %} controls {% else %} autoplay muted {% endif %} loop></video>
|
||||||
{% endif %}
|
{% endif %}
|
@ -11,12 +11,16 @@
|
|||||||
<meta name="og:image" content="{{ url_for('serve_media', filename=item.image) }}" />
|
<meta name="og:image" content="{{ url_for('serve_media', filename=item.image) }}" />
|
||||||
<meta name="twitter:image" content="{{ url_for('serve_media', filename=item.image) }}" />
|
<meta name="twitter:image" content="{{ url_for('serve_media', filename=item.image) }}" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
{% elif item.video %}
|
||||||
|
<meta name="og:video" content="{{ url_for('serve_media', filename=item.video) }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="uk-flex uk-flex-wrap">
|
<div class="uk-flex uk-flex-wrap">
|
||||||
<div class="uk-width-1-1 uk-width-1-2@s uk-padding-small">
|
<div class="uk-width-1-1 uk-width-1-2@s uk-padding-small">
|
||||||
|
{% with full=true %}
|
||||||
{% include "item-content.html" %}
|
{% include "item-content.html" %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-width-1-1 uk-width-1-2@s uk-padding-small">
|
<div class="uk-width-1-1 uk-width-1-2@s uk-padding-small">
|
||||||
<h3>{% include "item-title.html" %}</h3>
|
<h3>{% include "item-title.html" %}</h3>
|
||||||
@ -25,7 +29,7 @@
|
|||||||
<div class="uk-text-truncate">
|
<div class="uk-text-truncate">
|
||||||
<a href="{{ item.link }}" target="_blank">{{ item.link }}</a>
|
<a href="{{ item.link }}" target="_blank">{{ item.link }}</a>
|
||||||
</div>
|
</div>
|
||||||
<p>{{ item.description }}</p>
|
<p class="uk-text-break" style="white-space: pre-line;">{{ item.description }}</p>
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<a class="uk-button uk-button-default uk-icon-link" href="{{ url_for('add_item') }}?item={{ item.id }}" rel="nofollow">
|
<a class="uk-button uk-button-default uk-icon-link" href="{{ url_for('add_item') }}?item={{ item.id }}" rel="nofollow">
|
||||||
Edit
|
Edit
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
<div uk-grid="masonry: pack" class="uk-grid-small uk-child-width-1-2 uk-child-width-1-3@s uk-child-width-1-4@m uk-child-width-1-5@l uk-child-width-1-6@xl">
|
<div uk-grid="masonry: pack" class="results uk-grid-small uk-child-width-1-2 uk-child-width-1-3@s uk-child-width-1-4@m uk-child-width-1-5@l uk-child-width-1-6@xl">
|
||||||
{% for folder, items in items.items() %}
|
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
{% include "item-card.html" %}
|
{% include "item-card.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% if next_page %}
|
||||||
|
<div class="load-wrapper uk-margin">
|
||||||
|
<a href="/?page={{ next_page }}" class="uk-button uk-button-secondary uk-width-1-1"
|
||||||
|
up-target=".results:after, .load-wrapper" up-select=".results > *, .load-wrapper"
|
||||||
|
>Load More</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
Reference in New Issue
Block a user