Initial release version

This commit is contained in:
2025-07-13 01:20:07 +02:00
parent 4471ef647b
commit 0a8f5269e4
16 changed files with 310 additions and 249 deletions

3
.gitignore vendored
View File

@ -1,2 +1,5 @@
/_config.py
/data/ /data/
/node_modules/
/.mypy_cache/
*.pyc *.pyc

319
app.py
View File

@ -1,24 +1,36 @@
import os import os
import re
import requests import requests
import configparser
import urllib.parse import urllib.parse
import xml.etree.ElementTree as ElementTree from typing import Any
from io import StringIO from io import StringIO
from configparser import ConfigParser
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from flask import Flask, request, redirect, render_template, send_from_directory, abort, url_for, flash from flask import Flask, request, redirect, render_template, send_from_directory, abort, url_for, flash
from flask_bcrypt import Bcrypt from flask_bcrypt import Bcrypt # type: ignore[import-untyped]
from flask_login import LoginManager, UserMixin, current_user, login_user, logout_user, login_required from flask_login import LoginManager, UserMixin, current_user, login_user, logout_user, login_required # type: ignore[import-untyped]
from flask_wtf import FlaskForm from flask_wtf import FlaskForm # type: ignore[import-untyped]
from wtforms import StringField, PasswordField, SubmitField from wtforms import StringField, PasswordField, SubmitField # type: ignore[import-untyped]
from wtforms.validators import DataRequired from wtforms.validators import DataRequired # type: ignore[import-untyped]
from glob import glob from glob import glob
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from snowflake import Snowflake, SnowflakeGenerator from snowflake import Snowflake, SnowflakeGenerator # type: ignore[import-untyped]
SECRET_KEY = "SECRET_KEY" # import secrets; print(secrets.token_urlsafe())
DEVELOPMENT = True
HTTP_PORT = 5000
HTTP_THREADS = 32
LINKS_PREFIX = ""
from _config import *
app = Flask(__name__) app = Flask(__name__)
app.config["SECRET_KEY"] = "your_secret_key" # TODO: fix this for prod app.config["LINKS_PREFIX"] = LINKS_PREFIX
app.config["APP_NAME"] = "Pignio"
app.config["APP_ICON"] = "📌"
app.config["SECRET_KEY"] = SECRET_KEY
app.config["BCRYPT_HANDLE_LONG_PASSWORDS"] = True
login_manager = LoginManager() login_manager = LoginManager()
login_manager.login_view = "login" login_manager.login_view = "login"
login_manager.init_app(app) login_manager.init_app(app)
@ -30,19 +42,17 @@ snowflake = SnowflakeGenerator(1, epoch=snowflake_epoch)
DATA_ROOT = "data" DATA_ROOT = "data"
ITEMS_ROOT = f"{DATA_ROOT}/items" ITEMS_ROOT = f"{DATA_ROOT}/items"
USERS_ROOT = f"{DATA_ROOT}/users" USERS_ROOT = f"{DATA_ROOT}/users"
MEDIA_ROOT = f"{DATA_ROOT}/items"
EXTENSIONS = { EXTENSIONS = {
"images": ("jpg", "jpeg", "png", "gif", "webp", "avif"), "images": ("jpg", "jpeg", "png", "gif", "webp", "avif"),
"videos": ("mp4", "mov", "mpeg", "ogv", "webm", "mkv"), "videos": ("mp4", "mov", "mpeg", "ogv", "webm", "mkv"),
} }
ITEMS_EXT = ".pignio" ITEMS_EXT = ".ini"
class User(UserMixin): class User(UserMixin):
def __init__(self, username, filepath): def __init__(self, username, filepath):
self.username = username self.username = username
self.filepath = filepath self.filepath = filepath
with open(filepath, "r") as f: self.data = read_metadata(read_textual(filepath))
self.data = read_metadata(f.read())
def get_id(self): def get_id(self):
return self.username return self.username
@ -54,11 +64,15 @@ class LoginForm(FlaskForm):
@app.route("/") @app.route("/")
def index(): def index():
return render_template("index.html", media=walk_items()) return render_template("index.html", items=walk_items())
@app.route("/static/module/<path:module>/<path:filename>")
def serve_module(module:str, filename:str):
return send_from_directory(os.path.join("node_modules", module, "dist"), filename)
@app.route("/media/<path:filename>") @app.route("/media/<path:filename>")
def serve_media(filename:str): def serve_media(filename:str):
return send_from_directory(MEDIA_ROOT, filename) return send_from_directory(ITEMS_ROOT, filename)
@app.route("/item/<path:iid>") @app.route("/item/<path:iid>")
def view_item(iid:str): def view_item(iid:str):
@ -77,18 +91,18 @@ def view_user(username:str):
@app.route("/search") @app.route("/search")
def search(): def search():
query = request.args.get("query", "").lower() query = request.args.get("query", "").lower()
found = False
results = {} results = {}
for folder, items in walk_items().items(): for folder, items in walk_items().items():
results[folder] = [] results[folder] = []
for item in items: for item in items:
image = item["id"] if any([query in text.lower() for text in item.values()]):
meta = load_sider_metadata(image) or {} results[folder].append(item)
if any([query in text.lower() for text in [image, *meta.values()]]): found = True
results[folder].append(image)
return render_template("search.html", media=results, query=query) return render_template("search.html", items=(results if found else None), query=query)
@app.route("/add", methods=["GET", "POST"]) @app.route("/add", methods=["GET", "POST"])
@login_required @login_required
@ -102,15 +116,12 @@ def add_item():
elif request.method == "POST": elif request.method == "POST":
iid = request.form.get("id") or generate_iid() iid = request.form.get("id") or generate_iid()
data = {key: request.form[key] for key in ["link", "title", "description", "image", "text"]}
store_item(iid, { if store_item(iid, data, request.files):
"link": request.form.get("link"), return redirect(url_for("view_item", iid=iid))
"title": request.form.get("title"), else:
"description": request.form.get("description"), flash("Cannot save item", "danger")
"image": request.form.get("image"),
}, request.files)
return redirect(url_for("view_item", iid=iid))
return render_template("add.html", item=item) return render_template("add.html", item=item)
@ -130,19 +141,9 @@ def remove_item():
abort(404) abort(404)
# iid = request.args.get("item")
# item = load_item(iid)
# if not item:
# abort(404)
# if request.method == "GET":
# return render_template("remove.html", item=item)
# elif request.method == "POST":
# delete_item(item)
# return redirect(url_for("index"))
@app.route("/api/preview") @app.route("/api/preview")
@login_required @login_required
def preview(): def link_preview():
return fetch_url_data(request.args.get("url")) return fetch_url_data(request.args.get("url"))
@app.errorhandler(404) @app.errorhandler(404)
@ -164,8 +165,7 @@ def login():
if pass_equals or hash_equals: if pass_equals or hash_equals:
if pass_equals: if pass_equals:
user.data["password"] = bcrypt.generate_password_hash(user.data["password"]).decode("utf-8") user.data["password"] = bcrypt.generate_password_hash(user.data["password"]).decode("utf-8")
with open(user.filepath, "w") as f: write_textual(user.filepath, write_metadata(user.data))
f.write(write_metadata(user.data))
login_user(user) login_user(user)
# next_url = flask.request.args.get('next') # next_url = flask.request.args.get('next')
# if not url_has_allowed_host_and_scheme(next_url, request.host): return flask.abort(400) # if not url_has_allowed_host_and_scheme(next_url, request.host): return flask.abort(400)
@ -176,9 +176,9 @@ def login():
return render_template("login.html", form=form) return render_template("login.html", form=form)
@app.route("/logout") @app.route("/logout")
@login_required
def logout(): def logout():
logout_user() if current_user.is_authenticated:
logout_user()
return redirect(url_for("index")) return redirect(url_for("index"))
@login_manager.user_loader @login_manager.user_loader
@ -190,32 +190,13 @@ def load_user(username:str):
def walk_items(): def walk_items():
results, iids = {}, {} results, iids = {}, {}
for root, dirs, files in os.walk(MEDIA_ROOT): for root, dirs, files in os.walk(ITEMS_ROOT):
rel_path = os.path.relpath(root, MEDIA_ROOT).replace(os.sep, "/") rel_path = os.path.relpath(root, ITEMS_ROOT).replace(os.sep, "/")
if rel_path == ".": if rel_path == ".":
rel_path = "" rel_path = ""
results[rel_path], iids[rel_path] = [], [] results[rel_path], iids[rel_path] = [], []
# for file in files:
# if file.lower().endswith(tuple([f".{ext}" for ext in EXTENSIONS["images"]])):
# iid = strip_ext(os.path.join(rel_path, file).replace(os.sep, "/"))
# image = os.path.join(rel_path, file).replace(os.sep, "/")
# data = load_sider_metadata(image) or {}
# data["image"] = image
# data["id"] = iid
# results[rel_path].append(data)
# files.remove(file)
# for file in files:
# if file.lower().endswith(ITEMS_EXT):
# iid = strip_ext(os.path.join(rel_path, file).replace(os.sep, "/"))
# with open(os.path.join(MEDIA_ROOT, rel_path, file), "r") as f:
# data = read_metadata(f.read())
# data["id"] = iid
# results[rel_path].append(data)
# files.remove(file)
for file in files: for file in files:
#if file.lower().endswith(ITEMS_EXT) or file.lower().endswith(tuple([f".{ext}" for ext in EXTENSIONS["images"]])): #if file.lower().endswith(ITEMS_EXT) or file.lower().endswith(tuple([f".{ext}" for ext in EXTENSIONS["images"]])):
iid = strip_ext(os.path.join(rel_path, file).replace(os.sep, "/")) iid = strip_ext(os.path.join(rel_path, file).replace(os.sep, "/"))
@ -229,14 +210,14 @@ def walk_items():
return results return results
def walk_collections(username:str=None): def walk_collections(username:str):
results = {"": []} results: dict[str, list[str]] = {"": []}
filepath = USERS_ROOT filepath = USERS_ROOT
if username: # if username:
filepath = os.path.join(filepath, username) filepath = os.path.join(filepath, username)
results[""] = read_metadata(read_textual(filepath + ITEMS_EXT))["items"].strip().replace(" ", "\n").splitlines() data = read_metadata(read_textual(filepath + ITEMS_EXT))
results[""] = data["items"] if "items" in data else []
# for root, dirs, files in os.walk(filepath): # for root, dirs, files in os.walk(filepath):
# rel_path = os.path.relpath(root, filepath).replace(os.sep, "/") # rel_path = os.path.relpath(root, filepath).replace(os.sep, "/")
@ -266,7 +247,7 @@ def filename_to_iid(iid:str):
def load_item(iid:str): def load_item(iid:str):
iid = filename_to_iid(iid) iid = filename_to_iid(iid)
filename = iid_to_filename(iid) filename = iid_to_filename(iid)
filepath = os.path.join(MEDIA_ROOT, filename) filepath = os.path.join(ITEMS_ROOT, filename)
files = glob(f"{filepath}.*") files = glob(f"{filepath}.*")
if len(files): if len(files):
@ -274,89 +255,21 @@ 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):
# with open(file, "r", encoding="utf-8") as f:
# data = data | read_metadata(f.read())
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["images"]])): elif file.lower().endswith(tuple([f".{ext}" for ext in EXTENSIONS["images"]])):
data["image"] = file.replace(os.sep, "/").removeprefix(f"{MEDIA_ROOT}/") data["image"] = file.replace(os.sep, "/").removeprefix(f"{ITEMS_ROOT}/")
return data return data
def load_sider_metadata(filename:str): def store_item(iid:str, data:dict, files:dict):
filepath = os.path.join(MEDIA_ROOT, f"{strip_ext(filename)}{ITEMS_EXT}") iid = filename_to_iid(iid)
if os.path.exists(filepath): existing = load_item(iid)
with open(filepath, "r") as f: filename = split_iid(iid_to_filename(iid))
return read_metadata(f.read()) filepath = os.path.join(ITEMS_ROOT, *filename)
mkdirs(os.path.join(ITEMS_ROOT, filename[0]))
# def read_metadata(text:str):
# data = {}
# xml = "<root>" + re.sub(r'<(\w+)>(.*?)</>', r'<\1>\2</\1>', text) + "</root>"
# for elem in ElementTree.fromstring(xml, parser=ElementTree.XMLParser(encoding="utf-8")).findall('*'):
# data[elem.tag] = elem.text.strip()
# return data
def read_metadata(text:str) -> dict:
config = configparser.ConfigParser(allow_unnamed_section=True, interpolation=None)
config.read_string(text)
return config._sections[configparser.UNNAMED_SECTION] # tuple(config._sections.values())[0]
# def write_metadata(data:dict):
# text = ""
# for key in data:
# if key not in ("image",) and (value := data[key]):
# text += f'<{key}>{value}</>\n'
# return text
def write_metadata(data:dict) -> str:
output = StringIO()
config = configparser.ConfigParser(allow_unnamed_section=True, interpolation=None)
del data["image"]
config[configparser.UNNAMED_SECTION] = data
config.write(output)
return "\n".join(output.getvalue().splitlines()[1:]) # remove section header
def read_textual(filepath:str) -> str:
try:
with open(filepath, "r", encoding="utf-8") as f:
return f.read()
except UnicodeDecodeError:
with open(filepath, "r") as f:
return f.read()
def write_textual(filepath:str, content:bytes):
with open(filepath, "w", encoding="utf-8") as f:
return f.write(content)
def fetch_url_data(url:str):
response = requests.get(url, timeout=5)
soup = BeautifulSoup(response.text, "html.parser")
description = None
desc_tag = soup.find("meta", attrs={"name": "description"}) or \
soup.find("meta", attrs={"property": "og:description"})
if desc_tag and "content" in desc_tag.attrs:
description = desc_tag["content"]
image = None
img_tag = soup.find("meta", attrs={"property": "og:image"}) or \
soup.find("meta", attrs={"name": "twitter:image"})
if img_tag and "content" in img_tag.attrs:
image = img_tag["content"]
return {
"title": soup_or_default(soup, "meta", {"property": "og:title"}, "content", (soup.title.string if soup.title else None)),
"description": description,
"image": image,
"link": soup_or_default(soup, "link", {"rel": "canonical"}, "href", url),
}
def store_item(iid, data, files):
iid = iid_to_filename(iid)
iid = split_iid(strip_ext(iid))
filepath = os.path.join(MEDIA_ROOT, *iid)
Path(os.path.join(MEDIA_ROOT, iid[0])).mkdir(parents=True, exist_ok=True)
image = False image = False
if len(files): if len(files):
file = files["file"] file = files["file"]
if file.seek(0, os.SEEK_END): if file.seek(0, os.SEEK_END):
@ -369,39 +282,117 @@ def store_item(iid, data, files):
ext = response.headers["Content-Type"].split("/")[1] ext = response.headers["Content-Type"].split("/")[1]
with open(f"{filepath}.{ext}", "wb") as f: with open(f"{filepath}.{ext}", "wb") as f:
f.write(response.content) f.write(response.content)
# with open(filepath + ITEMS_EXT, "w", encoding="utf-8") as f: image = True
# f.write(write_metadata(data)) if not (existing or image or data["text"]):
return False
if existing:
if "creator" in existing:
data["creator"] = existing["creator"]
else:
data["creator"] = current_user.username
items = current_user.data["items"] if "items" in current_user.data else []
items.append(iid)
current_user.data["items"] = items
write_textual(current_user.filepath, write_metadata(current_user.data))
write_textual(filepath + ITEMS_EXT, write_metadata(data)) write_textual(filepath + ITEMS_EXT, write_metadata(data))
return True
def delete_item(item:dict): def delete_item(item:dict):
filepath = os.path.join(MEDIA_ROOT, iid_to_filename(item["id"])) filepath = os.path.join(ITEMS_ROOT, iid_to_filename(item["id"]))
files = glob(f"{filepath}.*") files = glob(f"{filepath}.*")
# for key in ("id", "image"):
# if key in item and (value := item[key]):
# filepath = os.path.join(MEDIA_ROOT, value)
# if os.path.exists(filepath):
# os.remove(filepath)
for file in files: for file in files:
os.remove(file) os.remove(file)
def prop_or_default(items:dict, prop:str, default): def read_metadata(text:str) -> dict:
config = ConfigParser(interpolation=None)
config.read_string(f"[DEFAULT]\n{text}")
data = config._defaults # type: ignore[attr-defined]
for key in ("items",):
if key in data:
data[key] = wsv_to_list(data[key])
return data
def write_metadata(data:dict) -> str:
output = StringIO()
config = ConfigParser(interpolation=None)
for key in ("image", "datetime"):
if key in data:
del data[key]
for key in data:
if type(data[key]) == list:
data[key] = list_to_wsv(data[key])
config["DEFAULT"] = data
config.write(output)
return "\n".join(output.getvalue().splitlines()[1:]) # remove section header
def read_textual(filepath:str) -> str:
try:
with open(filepath, "r", encoding="utf-8") as f:
return f.read()
except UnicodeDecodeError:
with open(filepath, "r") as f:
return f.read()
def write_textual(filepath:str, content:str):
with open(filepath, "w", encoding="utf-8") as f:
return f.write(content)
def fetch_url_data(url:str):
response = requests.get(url, timeout=5)
soup = BeautifulSoup(response.text, "html.parser")
description = None
desc_tag = soup.find("meta", attrs={"name": "description"}) or \
soup.find("meta", attrs={"property": "og:description"})
if desc_tag and "content" in desc_tag.attrs: # type: ignore[attr-defined]
description = desc_tag["content"] # type: ignore[index]
image = None
img_tag = soup.find("meta", attrs={"property": "og:image"}) or \
soup.find("meta", attrs={"name": "twitter:image"})
if img_tag and "content" in img_tag.attrs: # type: ignore[attr-defined]
image = img_tag["content"] # type: ignore[index]
return {
"title": soup_or_default(soup, "meta", {"property": "og:title"}, "content", (soup.title.string if soup.title else None)),
"description": description,
"image": image,
"link": soup_or_default(soup, "link", {"rel": "canonical"}, "href", url),
}
def prop_or_default(items:Any, prop:str, default):
return (items[prop] if (items and prop in items) else None) or default return (items[prop] if (items and prop in items) else None) or default
def soup_or_default(soup:BeautifulSoup, tag:str, attrs:dict, prop:str, default): def soup_or_default(soup:BeautifulSoup, tag:str, attrs:dict, prop:str, default):
return prop_or_default(soup.find(tag, attrs=attrs), prop, default) return prop_or_default(soup.find(tag, attrs=attrs), prop, default)
def generate_iid(): def generate_iid() -> str:
return str(next(snowflake)) return str(next(snowflake))
# iid = next(snowflake)
# date = Snowflake.parse(iid, snowflake_epoch).datetime
# return f"{date.year}/{date.month}/{next(snowflake)}"
def split_iid(iid:str): def split_iid(iid:str):
iid = iid.split("/") toks = iid.split("/")
return ["/".join(iid[:-1]), iid[-1]] return ["/".join(toks[:-1]), toks[-1]]
def strip_ext(filename:str): def strip_ext(filename:str):
return os.path.splitext(filename)[0] return os.path.splitext(filename)[0]
def list_to_wsv(data:list, sep="\n") -> str:
return sep.join(data)
def wsv_to_list(data:str) -> list:
return data.strip().replace(" ", "\n").replace("\t", "\n").splitlines()
def mkdirs(*paths:str):
for path in paths:
Path(path).mkdir(parents=True, exist_ok=True)
mkdirs(ITEMS_ROOT, USERS_ROOT)
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True) if DEVELOPMENT:
app.run(port=HTTP_PORT, debug=True)
else:
import waitress
waitress.serve(app, port=HTTP_PORT, threads=HTTP_THREADS)

18
package-lock.json generated Normal file
View File

@ -0,0 +1,18 @@
{
"name": "Pignio",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"uikit": "^3.23.11"
}
},
"node_modules/uikit": {
"version": "3.23.11",
"resolved": "https://registry.npmjs.org/uikit/-/uikit-3.23.11.tgz",
"integrity": "sha512-srUFBf5DfUxVpodcygibMQt1vgQjR9wlhIQo4GeWVpugk5+mKLPASJITDoY8wcwXQIHm7koELiPJ+FgNbzLv0A==",
"license": "MIT"
}
}
}

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"uikit": "^3.23.11"
}
}

View File

@ -2,5 +2,7 @@ flask
flask-bcrypt flask-bcrypt
flask-login flask-login
flask-wtf flask-wtf
beautifulsoup4
requests requests
snowflake-id snowflake-id
waitress

View File

@ -19,35 +19,35 @@
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
<label> <label>
<input type="checkbox" class="uk-checkbox" checked /> <input type="checkbox" class="uk-checkbox" {% if not item.id %} checked {% endif %} />
Fill Data from Link Fill data from link
</label> </label>
</div> </div>
{% if not item.id %} <div class="uk-margin">
<div class="uk-margin"> <div uk-grid>
<div uk-form-custom> <div uk-form-custom class="uk-width-1-1">
<input class="uk-input" type="file" name="file" aria-label="Custom controls" /> <input class="uk-input" type="file" name="file" />
<button class="uk-button uk-button-default" type="button" tabindex="-1">Select file</button> <button class="uk-button uk-width-1-1 uk-button-default" type="button" tabindex="-1">Select image</button>
</div> </div>
</div> </div>
{% endif %}
<div class="uk-margin">
<input type="color" class="uk-input" name="color" value="{{ item.text }}" />
</div> </div>
<!-- <div class="uk-margin">
<input type="color" class="uk-input" name="color" value="{{ item.text }}" />
</div> -->
<div class="uk-margin"> <div class="uk-margin">
<input class="uk-input" type="text" name="link" placeholder="URL..." value="{{ item.link }}" /> <input class="uk-input" type="text" name="link" placeholder="URL" value="{{ item.link }}" />
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
<input class="uk-input" type="text" name="title" placeholder="Title" value="{{ item.title }}" /> <input class="uk-input" type="text" name="title" placeholder="Title" value="{{ item.title }}" />
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
<textarea class="uk-textarea" name="description" placeholder="Description">{{ item.description }}</textarea> <textarea class="uk-textarea" rows="5" name="description" placeholder="Description">{{ item.description }}</textarea>
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
<textarea class="uk-textarea" name="text" placeholder="Text">{{ item.text }}</textarea> <textarea class="uk-textarea" rows="5" name="text" placeholder="Text">{{ item.text }}</textarea>
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
<button class="uk-input uk-button uk-button-default" 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> <script>
@ -55,8 +55,9 @@
var check = document.querySelector('form input[type="checkbox"]'); var check = document.querySelector('form input[type="checkbox"]');
['change', 'input', 'paste'].forEach(handler => { ['change', 'input', 'paste'].forEach(handler => {
link.addEventListener(handler, () => { link.addEventListener(handler, () => {
if (check.checked) { var url = link.value.trim();
fetch('http://localhost:5000/api/preview?url=' + encodeURIComponent(link.value)) if (check.checked && url) {
fetch('{{ url_for("link_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) {

View File

@ -1,27 +1,36 @@
<!DOCTYPE html> <!DOCTYPE html>
{% set title %}{% block title %}{% endblock %}{% endset %}
{% set canonical %}{% block canonical %}{% endblock %}{% endset %}
<html> <html>
<head> <head>
<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>{% block title %}My Media Gallery{% endblock %}</title> <title>{% if title %}{{ title }} | {% endif %}{{ config.APP_ICON }} {{ config.APP_NAME }}</title>
<link rel="canonical" href="{% block canonical %}{% endblock %}" /> <link rel="stylesheet" href="{{ url_for('serve_module', module='uikit', filename='css/uikit.min.css') }}" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.23.11/dist/css/uikit.min.css" /> <script src="{{ url_for('serve_module', module='uikit', filename='js/uikit.min.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.23.11/dist/js/uikit.min.js"></script> <script src="{{ url_for('serve_module', module='uikit', filename='js/uikit-icons.min.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.23.11/dist/js/uikit-icons.min.js"></script> {% if canonical %}
<link rel="canonical" href="{{ config.LINKS_PREFIX }}{{ canonical }}" />
{% endif %}
</head> </head>
<body> <body>
<nav class="uk-navbar-container" uk-navbar> <nav class="uk-navbar-container" uk-sticky="sel-target: .uk-navbar-container; cls-active: uk-navbar-sticky">
<div class="uk-navbar-left"> <div class="uk-container uk-container-expand" uk-navbar>
<a class="uk-button uk-button-primary" href="{{ url_for('index') }}">🏠 Home</a> <div class="uk-navbar-left uk-width-expand">
</div> <a class="uk-logo" href="{{ url_for('index') }}">
<div class="uk-navbar-center"> <span uk-icon="icon: home"></span>
<form class="uk-search uk-search-default" action="{{ url_for('search') }}"> {{ config.APP_NAME }}
<input class="uk-search-input" type="search" name="query" placeholder="Search or Add..." value="{{ query }}" required> </a>
<button class="uk-search-icon-flip" uk-search-icon></button> <form class="uk-search uk-search-navbar uk-width-auto uk-flex-1" action="{{ url_for('search') }}">
</form> <input class="uk-search-input" type="search" name="query" placeholder="Search..." value="{{ query }}" required>
</div> <button class="uk-search-icon-flip" uk-search-icon></button>
<div class="uk-navbar-right"> </form>
<a class="uk-button uk-button-default" href="{{ url_for('add_item') }}">🆕 Add</a> </div>
<div class="uk-navbar-right uk-margin-left">
<a class="uk-button uk-button-default uk-icon-link" uk-icon="plus" href="{{ url_for('add_item') }}">
<span class="uk-visible@s">Create</span>
</a>
</div>
</div> </div>
</nav> </nav>
<div class="uk-container uk-margin"> <div class="uk-container uk-margin">
@ -32,8 +41,6 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endwith %} {% endwith %}
</div>
<div class="uk-container uk-margin">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
</body> </body>

View File

@ -1,6 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}📁 Media Index{% endblock %} {% block canonical %}{{ url_for('index') }}{% endblock %}
<!-- {% block canonical %}{{ url_for('index') }}{% endblock %} -->
{% block content %} {% block content %}
{% include "results.html" %} {% include "results.html" %}
{% endblock %} {% endblock %}

6
templates/item-card.html Normal file
View File

@ -0,0 +1,6 @@
<div>
<a class="uk-link-text" href="{{ url_for('view_item', iid=item.id) }}">
{% include "item-content.html" %}
<span class="uk-text-break">{% include "item-title.html" %}</span>
</a>
</div>

View File

@ -0,0 +1,7 @@
{% if item.text %}
<div class="uk-background-cover uk-height-small uk-flex uk-flex-center uk-flex-middle uk-text-center uk-text-middle uk-overflow-hidden" style="background-image: url('https://getuikit.com/docs/images/dark.jpg');">
<p class="uk-text-stroke uk-text-primary">{{ item.text }}</p>
</div>
{% elif item.image %}
<img class="uk-width-expand" src="{{ url_for('serve_media', filename=item.image) }}" alt="{{ item.description }}" />
{% endif %}

View File

@ -1,28 +1,33 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}📌 Viewing {% include "item-title.html" %}{% endblock %} {% block title %}{% include "item-title.html" %}{% endblock %}
{% block canonical %}{{ url_for('view_item', iid=item.id) }}{% endblock %} {% block canonical %}{{ url_for('view_item', iid=item.id) }}{% 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">
{% if item.image %} {% include "item-content.html" %}
<img src="{{ url_for('serve_media', filename=item.image) }}" />
{% endif %}
</div>
<div class="uk-width-1-1 uk-width-1-2@s uk-padding-small">
<h3>{% include "item-title.html" %}</h3>
<div class="uk-text-truncate">
<a href="{{ item.link }}" target="_blank">{{ item.link }}</a>
</div> </div>
<span>{{ item.created }}</span> <div class="uk-width-1-1 uk-width-1-2@s uk-padding-small">
<p>{{ item.description }}</p> <h3>{% include "item-title.html" %}</h3>
<div class="uk-margin"> by <a href="{{ url_for('view_user', username=item.creator) }}">{{ item.creator }}</a>
<a class="uk-button uk-button-default" href="{{ url_for('add_item') }}?item={{ item.id }}">Edit</a> <!-- at <span>{{ item.datetime }}</span> -->
<a class="uk-button uk-button-danger" href="{{ url_for('remove_item') }}?item={{ item.id }}">Remove</a> <div class="uk-text-truncate">
<!-- <form class="uk-inline" method="POST" action="{{ url_for('remove_item') }}"> <a href="{{ item.link }}" target="_blank">{{ item.link }}</a>
<input type="hidden" name="item" value="{{ item.id }}" /> </div>
<button class="uk-input uk-button uk-button-danger" type="submit">Remove</button> <p>{{ item.description }}</p>
</form> --> <div class="uk-margin">
<a class="uk-button uk-button-default uk-icon-link" href="{{ url_for('add_item') }}?item={{ item.id }}">
Edit
<span uk-icon="icon: file-edit"></span>
</a>
<a class="uk-button uk-button-danger" href="{{ url_for('remove_item') }}?item={{ item.id }}">
Remove
<span uk-icon="icon: trash"></span>
</a>
<!-- <form class="uk-inline" method="POST" action="{{ url_for('remove_item') }}">
<input type="hidden" name="item" value="{{ item.id }}" />
<button class="uk-input uk-button uk-button-danger" type="submit">Remove</button>
</form> -->
</div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@ -4,13 +4,22 @@
<form method="POST"> <form method="POST">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
<div class="uk-margin"> <div class="uk-margin">
{{ form.username(class_="uk-input", placeholder="Username") }} <div class="uk-inline uk-width-1-1">
<span class="uk-form-icon" uk-icon="icon: user"></span>
{{ form.username(class_="uk-input", placeholder="Username") }}
</div>
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
{{ form.password(class_="uk-input", placeholder="Password") }} <div class="uk-inline uk-width-1-1">
<span class="uk-form-icon" uk-icon="icon: lock"></span>
{{ form.password(class_="uk-input", placeholder="Password") }}
</div>
</div> </div>
<div class="uk-margin"> <div class="uk-margin">
{{ form.submit(class_="uk-input uk-button uk-button-default") }} <div class="uk-inline uk-width-1-1">
{{ form.submit(class_="uk-input uk-button uk-button-default") }}
<span class="uk-form-icon uk-form-icon-flip" uk-icon="icon: lock"></span>
</div>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -3,8 +3,13 @@
{% block content %} {% block content %}
<form method="POST"> <form method="POST">
<input type="hidden" name="id" value="{{ item.id }}" /> <input type="hidden" name="id" value="{{ item.id }}" />
<div class="uk-margin"> <div class="uk-margin uk-grid-small" uk-grid>
<button class="uk-input uk-button uk-button-danger" type="submit">Remove {{ item.id }}</button> <div>
<a class="uk-button uk-button-default" href="{{ url_for('view_item', iid=item.id) }}">Back</a>
</div>
<div>
<button class="uk-input uk-button uk-button-danger" type="submit">Remove {{ item.id }}</button>
</div>
</div> </div>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -1,18 +1,7 @@
<div uk-grid="masonry: pack" class="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="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 media.items() %} {% for folder, items in items.items() %}
{% for item in items %} {% for item in items %}
<div margin="uk-grid-margin-small"> {% include "item-card.html" %}
<a class="uk-link-text" href="{{ url_for('view_item', iid=item.id) }}">
{% if item.image %}
<img src="{{ url_for('serve_media', filename=item.image) }}" />
{% elif item.description %}
<div class="uk-background-cover uk-height-small uk-flex uk-flex-center uk-flex-middle uk-text-center uk-text-middle uk-overflow-hidden" style="background-image: url('https://getuikit.com/docs/images/dark.jpg');">
<p class="uk-text-stroke uk-text-primary">{{ item.description }}</p>
</div>
{% endif %}
<span class="uk-text-break">{% include "item-title.html" %}</span>
</a>
</div>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</div> </div>

View File

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}Search Results for "{{ query }}"{% endblock %} {% block title %}Search Results for "{{ query }}"{% endblock %}
{% block content %} {% block content %}
{% if media_data %} {% if items %}
<p>🔍 Results for "{{ query }}"</p> <p>🔍 Results for "{{ query }}"</p>
{% include "results.html" %} {% include "results.html" %}
{% else %} {% else %}

View File

@ -1,11 +1,25 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}User {{ user.username }}{% endblock %} {% block title %}User {{ user.username }}{% endblock %}
{% block content %} {% block content %}
{{ user.username }} <div class="uk-text-center">
<img src="{{ url_for('serve_media', filename=load_item(user.data.propic).image) }}" /> <h2>{{ user.username }}</h2>
{% for folder, collection in collections.items() %} {% if user.data.propic %}
{% for item in collection %} {% with item=load_item(user.data.propic) %}
{{ item }} <div class="uk-width-medium uk-align-center">
{% include "item-content.html" %}
</div>
{% endwith %}
{% endif %}
<p>{{ user.data.description }}</p>
</div>
<hr />
<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, collection in collections.items() %}
{% for iid in collection %}
{% with item=load_item(iid) %}
{% include "item-card.html" %}
{% endwith %}
{% endfor %}
{% endfor %} {% endfor %}
{% endfor %} </div>
{% endblock %} {% endblock %}