mirror of
https://gitlab.com/octospacc/Pignio.git
synced 2025-07-17 22:37:38 +02:00
.
This commit is contained in:
415
app.py
415
app.py
@ -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("/")
|
||||
|
@ -1,3 +1,6 @@
|
||||
flask
|
||||
flask-bcrypt
|
||||
flask-login
|
||||
flask-wtf
|
||||
requests
|
||||
snowflake-id
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
16
templates/login.html
Normal 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
10
templates/remove.html
Normal 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 %}
|
@ -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
11
templates/user.html
Normal 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 %}
|
Reference in New Issue
Block a user