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
|
||||
/data/
|
||||
/node_modules/
|
||||
/.mypy_cache/
|
||||
.mypy_cache/
|
||||
*.pyc
|
57
app.py
57
app.py
@ -91,7 +91,11 @@ def noindex(view_func):
|
||||
|
||||
@app.route("/")
|
||||
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")
|
||||
@noindex
|
||||
@ -102,7 +106,12 @@ def serve_manifest():
|
||||
|
||||
@app.route("/static/module/<path:module>/<path:filename>")
|
||||
@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)
|
||||
|
||||
@app.route("/media/<path:filename>")
|
||||
@ -127,15 +136,17 @@ def view_user(username:str):
|
||||
def search():
|
||||
query = request.args.get("query", "").lower()
|
||||
found = False
|
||||
results = {}
|
||||
results = [] # {}
|
||||
|
||||
for folder, items in walk_items().items():
|
||||
results[folder] = []
|
||||
for item in walk_items():
|
||||
# for folder, items in walk_items().items():
|
||||
# results[folder] = []
|
||||
|
||||
for item in items:
|
||||
if any([query in text.lower() for text in item.values()]):
|
||||
results[folder].append(item)
|
||||
found = True
|
||||
#for item in items:
|
||||
if any([query in text.lower() for text in item.values()]):
|
||||
# results[folder].append(item)
|
||||
results.append(item)
|
||||
found = True
|
||||
|
||||
return render_template("search.html", items=(results if found else None), query=query)
|
||||
|
||||
@ -282,7 +293,7 @@ def walk_items():
|
||||
data = load_item(iid)
|
||||
results[rel_path].append(data)
|
||||
|
||||
return results
|
||||
return [value for values in results.values() for value in values] # results
|
||||
|
||||
def walk_collections(username:str):
|
||||
results: dict[str, list[str]] = {"": []}
|
||||
@ -330,9 +341,10 @@ def load_item(iid:str):
|
||||
for file in files:
|
||||
if file.lower().endswith(ITEMS_EXT):
|
||||
data = data | read_metadata(read_textual(file))
|
||||
|
||||
elif file.lower().endswith(tuple([f".{ext}" for ext in EXTENSIONS["image"]])):
|
||||
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
|
||||
|
||||
@ -343,6 +355,7 @@ def store_item(iid:str, data:dict, files:dict|None=None):
|
||||
filepath = os.path.join(ITEMS_ROOT, *filename)
|
||||
mkdirs(os.path.join(ITEMS_ROOT, filename[0]))
|
||||
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}
|
||||
|
||||
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)
|
||||
mime = file.content_type.split("/")
|
||||
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}")
|
||||
image = True
|
||||
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
|
||||
|
||||
if existing:
|
||||
if "creator" in existing:
|
||||
data["creator"] = existing["creator"]
|
||||
if (creator := safe_str_get(existing, "creator")):
|
||||
data["creator"] = creator
|
||||
else:
|
||||
data["creator"] = current_user.username
|
||||
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
|
||||
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))
|
||||
return True
|
||||
|
||||
@ -404,12 +419,15 @@ def read_metadata(text:str) -> dict:
|
||||
def write_metadata(data:dict) -> str:
|
||||
output = StringIO()
|
||||
config = ConfigParser(interpolation=None)
|
||||
for key in ("image", "datetime"):
|
||||
for key in ("image", "video", "datetime"):
|
||||
if key in data:
|
||||
del data[key]
|
||||
for key in data:
|
||||
if type(data[key]) == list:
|
||||
data[key] = list_to_wsv(data[key])
|
||||
for key in list(data.keys()):
|
||||
if (value := data[key]):
|
||||
if type(value) == list:
|
||||
data[key] = list_to_wsv(value)
|
||||
else:
|
||||
del data[key]
|
||||
config["DEFAULT"] = data
|
||||
config.write(output)
|
||||
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:
|
||||
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):
|
||||
for path in paths:
|
||||
Path(path).mkdir(parents=True, exist_ok=True)
|
||||
|
9
package-lock.json
generated
9
package-lock.json
generated
@ -5,7 +5,8 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"uikit": "^3.23.11"
|
||||
"uikit": "^3.23.11",
|
||||
"unpoly": "^3.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uikit": {
|
||||
@ -13,6 +14,12 @@
|
||||
"resolved": "https://registry.npmjs.org/uikit/-/uikit-3.23.11.tgz",
|
||||
"integrity": "sha512-srUFBf5DfUxVpodcygibMQt1vgQjR9wlhIQo4GeWVpugk5+mKLPASJITDoY8wcwXQIHm7koELiPJ+FgNbzLv0A==",
|
||||
"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": {
|
||||
"uikit": "^3.23.11"
|
||||
"uikit": "^3.23.11",
|
||||
"unpoly": "^3.11.0"
|
||||
}
|
||||
}
|
||||
|
120
static/add.js
120
static/add.js
@ -1,57 +1,69 @@
|
||||
var link = document.querySelector('form input[name="link"]');
|
||||
var check = document.querySelector('form input[type="checkbox"]');
|
||||
var image = document.querySelector('form img.image');
|
||||
var upload = document.querySelector('form input[name="file"]');
|
||||
|
||||
upload.addEventListener('change', function(ev) {
|
||||
const file = ev.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
image.src = e.target.result;
|
||||
image.parentElement.hidden = false;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
document.addEventListener('paste', function(ev) {
|
||||
const items = (ev.clipboardData || ev.originalEvent.clipboardData).items;
|
||||
for (let item of items) {
|
||||
if (item.type.indexOf('image') !== -1) {
|
||||
const file = item.getAsFile();
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
image.src = e.target.result;
|
||||
image.parentElement.hidden = false;
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
upload.files = dataTransfer.files;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
break;
|
||||
}
|
||||
if ('up' in window) {
|
||||
up.compiler('form.add', addHandler);
|
||||
} else {
|
||||
var form = document.querySelector('form.add');
|
||||
if (form) {
|
||||
addHandler(form);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
['change', 'input', 'paste'].forEach(handler => {
|
||||
link.addEventListener(handler, () => {
|
||||
var url = link.value.trim();
|
||||
if (check.checked && url) {
|
||||
fetch('../api/preview?url=' + encodeURIComponent(url))
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
for (var key in data) {
|
||||
var field = document.querySelector(`form [name="${key}"]`);
|
||||
if (field) {
|
||||
field.value = data[key];
|
||||
}
|
||||
var el = document.querySelector(`form [class="${key}"]`);
|
||||
if (el) {
|
||||
el.src = data[key];
|
||||
el.parentElement.hidden = false;
|
||||
}
|
||||
}
|
||||
})
|
||||
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) {
|
||||
const file = ev.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
image.src = e.target.result;
|
||||
image.parentElement.hidden = false;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
document.addEventListener('paste', function(ev) {
|
||||
const items = (ev.clipboardData || ev.originalEvent.clipboardData).items;
|
||||
for (let item of items) {
|
||||
if (item.type.indexOf('image') !== -1) {
|
||||
const file = item.getAsFile();
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
image.src = e.target.result;
|
||||
image.parentElement.hidden = false;
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
upload.files = dataTransfer.files;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
['change', 'input', 'paste'].forEach(handler => {
|
||||
link.addEventListener(handler, () => {
|
||||
var url = link.value.trim();
|
||||
if (checkLink.checked && url) {
|
||||
fetch('../api/preview?url=' + encodeURIComponent(url))
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
for (var key in data) {
|
||||
var field = form.querySelector(`[name="${key}"]`);
|
||||
if (field) {
|
||||
field.value = data[key];
|
||||
}
|
||||
var el = form.querySelector(`[class="${key}"]`);
|
||||
if (el) {
|
||||
el.src = data[key];
|
||||
el.parentElement.hidden = false;
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
@ -11,23 +11,34 @@
|
||||
<div>b</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="image" />
|
||||
<div class="uk-margin" {% if not item.image %} hidden {% endif %}>
|
||||
<img src="{{ url_for('serve_media', filename=item.image) }}" class="image" />
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
<div class="uk-margin uk-grid">
|
||||
<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
|
||||
</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 class="uk-margin">
|
||||
<div class="uk-grid-collapse" uk-grid>
|
||||
<div uk-form-custom class="uk-width-1-1">
|
||||
<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>
|
||||
@ -50,5 +61,4 @@
|
||||
<button class="uk-input uk-button uk-button-primary" type="submit">{{ ("Edit " + item.id) if item.id else "Add" }}</button>
|
||||
</div>
|
||||
</form>
|
||||
<script src="{{ url_for('static', filename='add.js') }}"></script>
|
||||
{% endblock %}
|
@ -6,9 +6,16 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<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') }}" />
|
||||
<script src="{{ url_for('serve_module', module='uikit', filename='js/uikit.min.js') }}"></script>
|
||||
<script src="{{ url_for('serve_module', module='uikit', filename='js/uikit-icons.min.js') }}"></script>
|
||||
<link rel="stylesheet" href="{{ url_for('serve_module_dist', module='uikit', filename='css/uikit.min.css') }}" />
|
||||
<script src="{{ url_for('serve_module_dist', module='uikit', filename='js/uikit.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 %}
|
||||
<link rel="canonical" href="{{ config.LINKS_PREFIX }}{{ canonical }}" />
|
||||
<meta name="og:url" content="{{ config.LINKS_PREFIX }}{{ canonical }}" />
|
||||
@ -43,8 +50,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="uk-flex uk-flex-center uk-margin uk-flex-auto">
|
||||
<div class="uk-container uk-flex-auto">
|
||||
<div class="uk-margin uk-flex-auto">
|
||||
<div class="uk-container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
|
@ -4,4 +4,6 @@
|
||||
</div>
|
||||
{% elif item.image %}
|
||||
<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 %}
|
@ -11,12 +11,16 @@
|
||||
<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:card" content="summary_large_image" />
|
||||
{% elif item.video %}
|
||||
<meta name="og:video" content="{{ url_for('serve_media', filename=item.video) }}" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="uk-flex uk-flex-wrap">
|
||||
<div class="uk-width-1-1 uk-width-1-2@s uk-padding-small">
|
||||
{% include "item-content.html" %}
|
||||
{% with full=true %}
|
||||
{% include "item-content.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="uk-width-1-1 uk-width-1-2@s uk-padding-small">
|
||||
<h3>{% include "item-title.html" %}</h3>
|
||||
@ -25,7 +29,7 @@
|
||||
<div class="uk-text-truncate">
|
||||
<a href="{{ item.link }}" target="_blank">{{ item.link }}</a>
|
||||
</div>
|
||||
<p>{{ item.description }}</p>
|
||||
<p class="uk-text-break" style="white-space: pre-line;">{{ item.description }}</p>
|
||||
<div class="uk-margin">
|
||||
<a class="uk-button uk-button-default uk-icon-link" href="{{ url_for('add_item') }}?item={{ item.id }}" rel="nofollow">
|
||||
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">
|
||||
{% for folder, items in items.items() %}
|
||||
<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 item in items %}
|
||||
{% include "item-card.html" %}
|
||||
{% 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