This commit is contained in:
2025-07-12 01:46:41 +02:00
parent af882eece0
commit 4471ef647b
10 changed files with 428 additions and 198 deletions

415
app.py
View File

@ -1,41 +1,78 @@
import os
import re
import requests
import configparser
import urllib.parse
import xml.etree.ElementTree as ElementTree
from io import StringIO
from bs4 import BeautifulSoup
from flask import Flask, request, redirect, render_template, send_from_directory, abort, url_for
from flask import Flask, request, redirect, render_template, send_from_directory, abort, url_for, flash
from flask_bcrypt import Bcrypt
from flask_login import LoginManager, UserMixin, current_user, login_user, logout_user, login_required
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired
from glob import glob
from pathlib import Path
from datetime import datetime
from snowflake import SnowflakeGenerator
from snowflake import Snowflake, SnowflakeGenerator
app = Flask(__name__)
snowflake = SnowflakeGenerator(1, epoch=int(datetime(2025, 1, 1, 0, 0, 0).timestamp() * 1000))
app.config["SECRET_KEY"] = "your_secret_key" # TODO: fix this for prod
login_manager = LoginManager()
login_manager.login_view = "login"
login_manager.init_app(app)
bcrypt = Bcrypt(app)
snowflake_epoch = int(datetime(2025, 1, 1, 0, 0, 0).timestamp() * 1000)
snowflake = SnowflakeGenerator(1, epoch=snowflake_epoch)
DATA_ROOT = "data"
ITEMS_ROOT = f"{DATA_ROOT}/items"
USERS_ROOT = f"{DATA_ROOT}/users"
MEDIA_ROOT = f"{DATA_ROOT}/items"
EXTENSIONS = {
"images": ("jpg", "jpeg", "png", "gif", "webp", "avif"),
"videos": ("mp4", "mov", "mpeg", "ogv", "webm", "mkv"),
}
ITEMS_EXT = ".pignio"
class User(UserMixin):
def __init__(self, username, filepath):
self.username = username
self.filepath = filepath
with open(filepath, "r") as f:
self.data = read_metadata(f.read())
def get_id(self):
return self.username
class LoginForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
submit = SubmitField("Login")
@app.route("/")
def index():
return render_template("index.html", media=walk_items())
@app.route("/media/<path:filename>")
def serve_media(filename):
def serve_media(filename:str):
return send_from_directory(MEDIA_ROOT, filename)
@app.route("/item/<path:filename>")
def view_item(filename):
# full_path = os.path.join(MEDIA_ROOT, filename)
# if not os.path.exists(full_path):
# abort(404)
item = load_item(filename)
if not item:
@app.route("/item/<path:iid>")
def view_item(iid:str):
if (item := load_item(iid)):
return render_template("item.html", item=item)
else:
abort(404)
@app.route("/user/<path:username>")
def view_user(username:str):
if (user := load_user(username)):
return render_template("user.html", user=user, collections=walk_collections(username), load_item=load_item)
else:
abort(404)
return render_template("item.html", filename=filename, item=item)
@app.route("/search")
def search():
@ -54,39 +91,57 @@ def search():
return render_template("search.html", media=results, query=query)
@app.route("/add", methods=["GET", "POST"])
@login_required
def add_item():
item = {}
if request.method == "GET":
iid = request.args.get("item")
if iid:
item = load_item(iid)
if (iid := request.args.get("item")):
if not (item := load_item(iid)):
abort(404)
elif request.method == "POST":
iid = request.form.get("id") or generate_iid()
# title = request.form.get("title")
# description = request.form.get("description")
# if (url := request.form.get("url")):
# download_item(url)
# else:
# with open(os.path.join(MEDIA_ROOT, f"{iid[1]}.item"), "w") as f:
# f.write(write_metadata({
# "description": description
# }))
# return redirect(url_for("index"))
filename = store_item(iid, {
store_item(iid, {
"link": request.form.get("link"),
"title": request.form.get("title"),
"description": request.form.get("description"),
}, request.files['file'])
"image": request.form.get("image"),
}, request.files)
return redirect(url_for("view_item", filename=filename))
return redirect(url_for("view_item", iid=iid))
return render_template("add.html", item=item)
@app.route("/remove", methods=["GET", "POST"])
@login_required
def remove_item():
if request.method == "GET":
if (iid := request.args.get("item")):
if (item := load_item(iid)):
return render_template("remove.html", item=item)
elif request.method == "POST":
if (iid := request.form.get("id")):
if (item := load_item(iid)):
delete_item(item)
return redirect(url_for("index"))
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")
@login_required
def preview():
return fetch_url_data(request.args.get("url"))
@ -94,123 +149,240 @@ def preview():
def error_404(e):
return render_template("404.html"), 404
@app.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return redirect(url_for("index"))
form = LoginForm()
if form.validate_on_submit():
if (user := load_user(form.username.data)):
pass_equals = user.data["password"] == form.password.data
try:
hash_equals = bcrypt.check_password_hash(user.data["password"], form.password.data)
except ValueError as e:
hash_equals = False
if pass_equals or hash_equals:
if pass_equals:
user.data["password"] = bcrypt.generate_password_hash(user.data["password"]).decode("utf-8")
with open(user.filepath, "w") as f:
f.write(write_metadata(user.data))
login_user(user)
# next_url = flask.request.args.get('next')
# if not url_has_allowed_host_and_scheme(next_url, request.host): return flask.abort(400)
# return redirect(next_url or url_for("index"))
return redirect(url_for("index"))
if request.method == "POST":
flash("Invalid username or password", "danger")
return render_template("login.html", form=form)
@app.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("index"))
@login_manager.user_loader
def load_user(username:str):
filepath = os.path.join(USERS_ROOT, (username + ITEMS_EXT))
if os.path.exists(filepath):
return User(username, filepath)
def walk_items():
results = {}
results, iids = {}, {}
for root, dirs, files in os.walk(MEDIA_ROOT):
rel_path = os.path.relpath(root, MEDIA_ROOT).replace(os.sep, "/")
if rel_path == ".":
rel_path = ""
results[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:
filename = os.path.join(rel_path, file).replace(os.sep, "/")
if file.lower().endswith(".item"):
with open(os.path.join(MEDIA_ROOT, rel_path, file), "r") as f:
data = read_metadata(f.read())
data["id"] = filename
results[rel_path].append(data)
elif file.lower().endswith(tuple([f".{ext}" for ext in EXTENSIONS["images"]])):
# results[rel_path].append(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"] = filename
results[rel_path].append(data)
#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 = filename_to_iid(iid)
if iid not in iids[rel_path]:
iids[rel_path].append(iid)
for iid in iids[rel_path]:
data = load_item(iid)
results[rel_path].append(data)
return results
def load_item(iid):
data = None
filepath = os.path.join(MEDIA_ROOT, iid)
def walk_collections(username:str=None):
results = {"": []}
if os.path.exists(filepath):
if iid.lower().endswith(".item"):
with open(filepath, "r") as f:
data = read_metadata(f.read())
else:
data = load_sider_metadata(iid) or {}
data["image"] = iid
filepath = USERS_ROOT
if username:
filepath = os.path.join(filepath, username)
results[""] = read_metadata(read_textual(filepath + ITEMS_EXT))["items"].strip().replace(" ", "\n").splitlines()
# for root, dirs, files in os.walk(filepath):
# rel_path = os.path.relpath(root, filepath).replace(os.sep, "/")
# if rel_path == ".":
# rel_path = ""
# else:
# results[rel_path] = []
# for file in files:
# print(file, rel_path)
# results[rel_path] = read_metadata(read_textual(os.path.join(filepath, rel_path, file)))["items"].strip().replace(" ", "\n").splitlines()
return results
def iid_to_filename(iid:str):
if len(iid.split("/")) == 1:
date = Snowflake.parse(int(iid), snowflake_epoch).datetime
iid = f"{date.year}/{date.month}/{iid}"
return iid
def filename_to_iid(iid:str):
toks = iid.split("/")
if len(toks) == 3 and "".join(toks).isnumeric():
iid = toks[2]
return iid
def load_item(iid:str):
iid = filename_to_iid(iid)
filename = iid_to_filename(iid)
filepath = os.path.join(MEDIA_ROOT, filename)
files = glob(f"{filepath}.*")
if len(files):
data = {"id": iid}
for file in files:
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))
elif file.lower().endswith(tuple([f".{ext}" for ext in EXTENSIONS["images"]])):
data["image"] = file.replace(os.sep, "/").removeprefix(f"{MEDIA_ROOT}/")
if data:
data["id"] = iid
return data
def load_sider_metadata(filename):
filepath = os.path.join(MEDIA_ROOT, f"{strip_ext(filename)}.meta")
def load_sider_metadata(filename:str):
filepath = os.path.join(MEDIA_ROOT, f"{strip_ext(filename)}{ITEMS_EXT}")
if os.path.exists(filepath):
with open(filepath, "r") as f:
return read_metadata(f.read())
def read_metadata(text:str):
data = {}
for elem in BeautifulSoup(re.sub(r'<(\w+)>(.*?)</>', r'<\1>\2</\1>', text), "html.parser").find_all():
data[elem.name] = elem.text.strip()
return data
# 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 write_metadata(data:dict):
text = ""
for key in data:
if (value := data[key]):
text += f'<{key}>{value}</>\n'
return text
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):
try:
response = requests.get(url, timeout=5)
soup = BeautifulSoup(response.text, "html.parser")
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"]
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"]
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),
}
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),
}
except Exception as e:
# print("Metadata fetch failed:", e)
return {}
def download_item(url:str):
data = fetch_url_data(url)
url = urllib.parse.urlparse(data["link"])
slug = (url.path or "index").split("/")[-1]
domain = url.netloc
Path(os.path.join(MEDIA_ROOT, domain)).mkdir(parents=True, exist_ok=True)
path = os.path.join(MEDIA_ROOT, domain, slug)
with open(f"{path}.meta", "w") as f:
f.write(write_metadata(data))
def store_item(iid, data, file):
item = load_item(iid)
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)
if file:
with open(f"{filepath}.meta", "w") as f:
f.write(write_metadata(data))
# with open(f"{filepath}.{ext}", "wb") as f:
# f.write(write_metadata(data))
ext = file.content_type.split("/")[1]
file.save(f'{filepath}.{ext}')
return "/".join(iid) + f".{ext}"
else:
with open(f"{filepath}.item", "w") as f:
f.write(write_metadata(data))
return "/".join(iid) + ".item"
# return "/".join(iid)
image = False
if len(files):
file = files["file"]
if file.seek(0, os.SEEK_END):
file.seek(0, os.SEEK_SET)
ext = file.content_type.split("/")[1]
file.save(f"{filepath}.{ext}")
image = True
if not image and data["image"]:
response = requests.get(data["image"], timeout=5)
ext = response.headers["Content-Type"].split("/")[1]
with open(f"{filepath}.{ext}", "wb") as f:
f.write(response.content)
# with open(filepath + ITEMS_EXT, "w", encoding="utf-8") as f:
# f.write(write_metadata(data))
write_textual(filepath + ITEMS_EXT, write_metadata(data))
def delete_item(item:dict):
filepath = os.path.join(MEDIA_ROOT, iid_to_filename(item["id"]))
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:
os.remove(file)
def prop_or_default(items:dict, prop:str, default):
return (items[prop] if (items and prop in items) else None) or default
@ -219,9 +391,10 @@ def soup_or_default(soup:BeautifulSoup, tag:str, attrs:dict, prop:str, default):
return prop_or_default(soup.find(tag, attrs=attrs), prop, default)
def generate_iid():
date = datetime.now()
return f"{date.year}/{date.month}/{next(snowflake)}"
# return [f"{date.year}/{date.month}", 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):
iid = iid.split("/")

View File

@ -1,3 +1,6 @@
flask
flask-bcrypt
flask-login
flask-wtf
requests
snowflake-id

View File

@ -1,41 +1,77 @@
{% extends "base.html" %}
{% block title %}🆕 Add item{% endblock %}
{% block content %}
<!--
<ul uk-tab="connect: .my-class">
<li><a href="">New</a></li>
<li><a href="">Text</a></li>
</ul>
<div class="uk-switcher my-class">
<div>a</div>
<div>b</div>
</div>
-->
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="id" value="{{ item['id'] }}" />
<div class="uk-margin">
<div uk-form-custom>
<input type="file" name="file" aria-label="Custom controls" />
<button class="uk-button uk-button-default" type="button" tabindex="-1">Select file</button>
</div>
<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">
<input class="uk-input" type="text" name="url" placeholder="URL..." value="{{ item.url }}" />
<label>
<input type="checkbox" class="uk-checkbox" checked />
Fill Data from Link
</label>
</div>
{% if not item.id %}
<div class="uk-margin">
<div uk-form-custom>
<input class="uk-input" type="file" name="file" aria-label="Custom controls" />
<button class="uk-button uk-button-default" type="button" tabindex="-1">Select file</button>
</div>
</div>
{% endif %}
<div class="uk-margin">
<input type="color" class="uk-input" name="color" value="{{ item.text }}" />
</div>
<div class="uk-margin">
<input class="uk-input" type="text" name="link" placeholder="URL..." value="{{ item.link }}" />
</div>
<div class="uk-margin">
<input class="uk-input" type="text" name="title" placeholder="Title" value="{{ item.title }}" />
</div>
<div class="uk-margin">
<textarea class="uk-textarea" name="description" placeholder="Write anything...">{{ item.description }}</textarea>
<textarea class="uk-textarea" name="description" placeholder="Description">{{ item.description }}</textarea>
</div>
<div class="uk-margin">
<button class="uk-input" type="submit">{{ ("Edit " + item.id) if item.id else "Add" }}</button>
<textarea class="uk-textarea" name="text" placeholder="Text">{{ item.text }}</textarea>
</div>
<div class="uk-margin">
<button class="uk-input uk-button uk-button-default" type="submit">{{ ("Edit " + item.id) if item.id else "Add" }}</button>
</div>
</form>
<script>
var url = document.querySelector('form input[name="url"]');
var link = document.querySelector('form input[name="link"]');
var check = document.querySelector('form input[type="checkbox"]');
['change', 'input', 'paste'].forEach(handler => {
url.addEventListener(handler, () => {
fetch('http://localhost:5000/api/preview?url=' + url.value)
.then(res => res.json())
.then(data => {
for (var key in data) {
var field = document.querySelector(`form [name="${key}"]`);
if (field) {
field.value = data[key];
link.addEventListener(handler, () => {
if (check.checked) {
fetch('http://localhost:5000/api/preview?url=' + encodeURIComponent(link.value))
.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;
}
}
}
})
})
}
})
});
</script>

View File

@ -5,32 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}My Media Gallery{% endblock %}</title>
<link rel="canonical" href="{% block canonical %}{% endblock %}" />
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2.1.1/css/pico.min.css" /> -->
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css" /> -->
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script> -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.23.11/dist/css/uikit.min.css" />
<script src="https://cdn.jsdelivr.net/npm/uikit@3.23.11/dist/js/uikit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.23.11/dist/js/uikit-icons.min.js"></script>
<!-- <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css"> -->
<style>
/* body { font-family: sans-serif; margin: 1em; }
.container { margin: 1em; }
h2 { margin-top: 40px; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
.grid img { width: 100%; height: auto; border-radius: 8px; }
.search { display: flex; }
.search input { flex: 1; height: 2em; } */
/* img { max-width: 31%; } */
</style>
</head>
<body>
<nav class="uk-navbar-container" uk-navbar>
@ -44,9 +21,18 @@
</form>
</div>
<div class="uk-navbar-right">
<a class="uk-button uk-button-secondary" href="{{ url_for('add_item') }}">🆕 Add</a>
<a class="uk-button uk-button-default" href="{{ url_for('add_item') }}">🆕 Add</a>
</div>
</nav>
<div class="uk-container uk-margin">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p uk-alert class="uk-alert-{{ category }}">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<div class="uk-container uk-margin">
{% block content %}{% endblock %}
</div>

View File

View File

@ -1,30 +1,28 @@
{% extends "base.html" %}
{% block title %}📌 Viewing {{ filename }}{% endblock %}
{% block title %}📌 Viewing {% include "item-title.html" %}{% endblock %}
{% block canonical %}{{ url_for('view_item', iid=item.id) }}{% endblock %}
{% block content %}
{% raw %}
<!-- <style>
img { max-width: 49%; border-radius: 10px; }
.meta { display: inline-block; max-width: 49%; vertical-align: top; padding: 1em; box-sizing: border-box; }
</style> -->
<!-- <img src="{{ url_for('serve_media', filename=filename) }}" />
<div class="meta">
<p>{{ meta.description }}</p>
<p><strong>File:</strong> {{ filename }}</p>
<p><strong>Link:</strong> <a href="{{ url_for('view_item', filename=filename) }}">{{ request.url }}</a></p>
</div> -->
{% endraw %}
<div class="uk-flex uk-flex-wrap">
<div class="uk-width-1-1 uk-width-1-2@s uk-padding-small">
{% if item.image %}
<img src="{{ url_for('serve_media', filename=filename) }}" />
<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>
<span>{{ item.created }}</span>
<p>{{ item.description }}</p>
<a class="uk-button uk-button-default" href="{{ url_for('add_item') }}?item={{ item.id }}">Edit</a>
<a class="uk-button uk-button-danger" href="{{ url_for('add_item') }}?item={{ item.id }}">Remove</a>
<div class="uk-margin">
<a class="uk-button uk-button-default" href="{{ url_for('add_item') }}?item={{ item.id }}">Edit</a>
<a class="uk-button uk-button-danger" href="{{ url_for('remove_item') }}?item={{ item.id }}">Remove</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>
{% endblock %}

16
templates/login.html Normal file
View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<form method="POST">
{{ form.hidden_tag() }}
<div class="uk-margin">
{{ form.username(class_="uk-input", placeholder="Username") }}
</div>
<div class="uk-margin">
{{ form.password(class_="uk-input", placeholder="Password") }}
</div>
<div class="uk-margin">
{{ form.submit(class_="uk-input uk-button uk-button-default") }}
</div>
</form>
{% endblock %}

10
templates/remove.html Normal file
View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block title %}🗑 Remove item{% endblock %}
{% block content %}
<form method="POST">
<input type="hidden" name="id" value="{{ item.id }}" />
<div class="uk-margin">
<button class="uk-input uk-button uk-button-danger" type="submit">Remove {{ item.id }}</button>
</div>
</form>
{% endblock %}

View File

@ -1,21 +1,18 @@
<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">
{% for folder, items in media.items() %}
<!--<h2>{{ folder }}</h2>
<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">-->
{% for item in items %}
<div margin="uk-grid-margin-small">
<a class="uk-link-text" href="{{ url_for('view_item', filename=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" 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 %}
<!--</div>-->
{% for item in items %}
<div margin="uk-grid-margin-small">
<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 %}
</div>

11
templates/user.html Normal file
View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}User {{ user.username }}{% endblock %}
{% block content %}
{{ user.username }}
<img src="{{ url_for('serve_media', filename=load_item(user.data.propic).image) }}" />
{% for folder, collection in collections.items() %}
{% for item in collection %}
{{ item }}
{% endfor %}
{% endfor %}
{% endblock %}