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/
/node_modules/
/.mypy_cache/
*.pyc

313
app.py
View File

@ -1,24 +1,36 @@
import os
import re
import requests
import configparser
import urllib.parse
import xml.etree.ElementTree as ElementTree
from typing import Any
from io import StringIO
from configparser import ConfigParser
from bs4 import BeautifulSoup
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 flask_bcrypt import Bcrypt # type: ignore[import-untyped]
from flask_login import LoginManager, UserMixin, current_user, login_user, logout_user, login_required # type: ignore[import-untyped]
from flask_wtf import FlaskForm # type: ignore[import-untyped]
from wtforms import StringField, PasswordField, SubmitField # type: ignore[import-untyped]
from wtforms.validators import DataRequired # type: ignore[import-untyped]
from glob import glob
from pathlib import Path
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.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.login_view = "login"
login_manager.init_app(app)
@ -30,19 +42,17 @@ 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"
ITEMS_EXT = ".ini"
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())
self.data = read_metadata(read_textual(filepath))
def get_id(self):
return self.username
@ -54,11 +64,15 @@ class LoginForm(FlaskForm):
@app.route("/")
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>")
def serve_media(filename:str):
return send_from_directory(MEDIA_ROOT, filename)
return send_from_directory(ITEMS_ROOT, filename)
@app.route("/item/<path:iid>")
def view_item(iid:str):
@ -77,18 +91,18 @@ def view_user(username:str):
@app.route("/search")
def search():
query = request.args.get("query", "").lower()
found = False
results = {}
for folder, items in walk_items().items():
results[folder] = []
for item in items:
image = item["id"]
meta = load_sider_metadata(image) or {}
if any([query in text.lower() for text in [image, *meta.values()]]):
results[folder].append(image)
if any([query in text.lower() for text in item.values()]):
results[folder].append(item)
found = True
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"])
@login_required
@ -102,15 +116,12 @@ def add_item():
elif request.method == "POST":
iid = request.form.get("id") or generate_iid()
data = {key: request.form[key] for key in ["link", "title", "description", "image", "text"]}
store_item(iid, {
"link": request.form.get("link"),
"title": request.form.get("title"),
"description": request.form.get("description"),
"image": request.form.get("image"),
}, request.files)
if store_item(iid, data, request.files):
return redirect(url_for("view_item", iid=iid))
else:
flash("Cannot save item", "danger")
return render_template("add.html", item=item)
@ -130,19 +141,9 @@ def remove_item():
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():
def link_preview():
return fetch_url_data(request.args.get("url"))
@app.errorhandler(404)
@ -164,8 +165,7 @@ def login():
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))
write_textual(user.filepath, 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)
@ -176,8 +176,8 @@ def login():
return render_template("login.html", form=form)
@app.route("/logout")
@login_required
def logout():
if current_user.is_authenticated:
logout_user()
return redirect(url_for("index"))
@ -190,32 +190,13 @@ def load_user(username:str):
def walk_items():
results, iids = {}, {}
for root, dirs, files in os.walk(MEDIA_ROOT):
rel_path = os.path.relpath(root, MEDIA_ROOT).replace(os.sep, "/")
for root, dirs, files in os.walk(ITEMS_ROOT):
rel_path = os.path.relpath(root, ITEMS_ROOT).replace(os.sep, "/")
if rel_path == ".":
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:
#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, "/"))
@ -229,14 +210,14 @@ def walk_items():
return results
def walk_collections(username:str=None):
results = {"": []}
def walk_collections(username:str):
results: dict[str, list[str]] = {"": []}
filepath = USERS_ROOT
if username:
# if 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):
# 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):
iid = filename_to_iid(iid)
filename = iid_to_filename(iid)
filepath = os.path.join(MEDIA_ROOT, filename)
filepath = os.path.join(ITEMS_ROOT, filename)
files = glob(f"{filepath}.*")
if len(files):
@ -274,89 +255,21 @@ def load_item(iid:str):
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}/")
data["image"] = file.replace(os.sep, "/").removeprefix(f"{ITEMS_ROOT}/")
return data
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 = {}
# 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)
def store_item(iid:str, data:dict, files:dict):
iid = filename_to_iid(iid)
existing = load_item(iid)
filename = split_iid(iid_to_filename(iid))
filepath = os.path.join(ITEMS_ROOT, *filename)
mkdirs(os.path.join(ITEMS_ROOT, filename[0]))
image = False
if len(files):
file = files["file"]
if file.seek(0, os.SEEK_END):
@ -369,39 +282,117 @@ def store_item(iid, data, files):
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))
image = True
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))
return True
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}.*")
# 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):
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
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():
def generate_iid() -> str:
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("/")
return ["/".join(iid[:-1]), iid[-1]]
toks = iid.split("/")
return ["/".join(toks[:-1]), toks[-1]]
def strip_ext(filename:str):
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__":
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-login
flask-wtf
beautifulsoup4
requests
snowflake-id
waitress

View File

@ -19,35 +19,35 @@
</div>
<div class="uk-margin">
<label>
<input type="checkbox" class="uk-checkbox" checked />
Fill Data from Link
<input type="checkbox" class="uk-checkbox" {% if not item.id %} checked {% endif %} />
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 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 image</button>
</div>
</div>
{% endif %}
<div class="uk-margin">
</div>
<!-- <div class="uk-margin">
<input type="color" class="uk-input" name="color" value="{{ item.text }}" />
</div>
</div> -->
<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 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="Description">{{ item.description }}</textarea>
<textarea class="uk-textarea" rows="5" name="description" placeholder="Description">{{ item.description }}</textarea>
</div>
<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 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>
</form>
<script>
@ -55,8 +55,9 @@
var check = document.querySelector('form input[type="checkbox"]');
['change', 'input', 'paste'].forEach(handler => {
link.addEventListener(handler, () => {
if (check.checked) {
fetch('http://localhost:5000/api/preview?url=' + encodeURIComponent(link.value))
var url = link.value.trim();
if (check.checked && url) {
fetch('{{ url_for("link_preview") }}?url=' + encodeURIComponent(url))
.then(res => res.json())
.then(data => {
for (var key in data) {

View File

@ -1,27 +1,36 @@
<!DOCTYPE html>
{% set title %}{% block title %}{% endblock %}{% endset %}
{% set canonical %}{% block canonical %}{% endblock %}{% endset %}
<html>
<head>
<meta charset="utf-8" />
<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/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>
<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>
{% if canonical %}
<link rel="canonical" href="{{ config.LINKS_PREFIX }}{{ canonical }}" />
{% endif %}
</head>
<body>
<nav class="uk-navbar-container" uk-navbar>
<div class="uk-navbar-left">
<a class="uk-button uk-button-primary" href="{{ url_for('index') }}">🏠 Home</a>
</div>
<div class="uk-navbar-center">
<form class="uk-search uk-search-default" action="{{ url_for('search') }}">
<input class="uk-search-input" type="search" name="query" placeholder="Search or Add..." value="{{ query }}" required>
<nav class="uk-navbar-container" uk-sticky="sel-target: .uk-navbar-container; cls-active: uk-navbar-sticky">
<div class="uk-container uk-container-expand" uk-navbar>
<div class="uk-navbar-left uk-width-expand">
<a class="uk-logo" href="{{ url_for('index') }}">
<span uk-icon="icon: home"></span>
{{ config.APP_NAME }}
</a>
<form class="uk-search uk-search-navbar uk-width-auto uk-flex-1" action="{{ url_for('search') }}">
<input class="uk-search-input" type="search" name="query" placeholder="Search..." value="{{ query }}" required>
<button class="uk-search-icon-flip" uk-search-icon></button>
</form>
</div>
<div class="uk-navbar-right">
<a class="uk-button uk-button-default" href="{{ url_for('add_item') }}">🆕 Add</a>
<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>
</nav>
<div class="uk-container uk-margin">
@ -32,8 +41,6 @@
{% endfor %}
{% endif %}
{% endwith %}
</div>
<div class="uk-container uk-margin">
{% block content %}{% endblock %}
</div>
</body>

View File

@ -1,6 +1,5 @@
{% extends "base.html" %}
{% block title %}📁 Media Index{% endblock %}
<!-- {% block canonical %}{{ url_for('index') }}{% endblock %} -->
{% block canonical %}{{ url_for('index') }}{% endblock %}
{% block content %}
{% include "results.html" %}
{% 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,23 +1,28 @@
{% 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 content %}
<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=item.image) }}" />
{% endif %}
{% include "item-content.html" %}
</div>
<div class="uk-width-1-1 uk-width-1-2@s uk-padding-small">
<h3>{% include "item-title.html" %}</h3>
by <a href="{{ url_for('view_user', username=item.creator) }}">{{ item.creator }}</a>
<!-- at <span>{{ item.datetime }}</span> -->
<div class="uk-text-truncate">
<a href="{{ item.link }}" target="_blank">{{ item.link }}</a>
</div>
<span>{{ item.created }}</span>
<p>{{ item.description }}</p>
<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>
<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>

View File

@ -4,13 +4,22 @@
<form method="POST">
{{ form.hidden_tag() }}
<div class="uk-margin">
<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 class="uk-margin">
{{ form.password(class_="uk-input", placeholder="Password") }}
</div>
<div class="uk-margin">
<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 class="uk-margin">
<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>
</form>
{% endblock %}

View File

@ -3,8 +3,13 @@
{% block content %}
<form method="POST">
<input type="hidden" name="id" value="{{ item.id }}" />
<div class="uk-margin">
<div class="uk-margin uk-grid-small" uk-grid>
<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>
</form>
{% 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">
{% for folder, items in media.items() %}
<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() %}
{% 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>
{% include "item-card.html" %}
{% endfor %}
{% endfor %}
</div>

View File

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

View File

@ -1,11 +1,25 @@
{% 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) }}" />
<div class="uk-text-center">
<h2>{{ user.username }}</h2>
{% if user.data.propic %}
{% with item=load_item(user.data.propic) %}
<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 item in collection %}
{{ item }}
{% for iid in collection %}
{% with item=load_item(iid) %}
{% include "item-card.html" %}
{% endwith %}
{% endfor %}
{% endfor %}
</div>
{% endblock %}