theme-store/app.py

188 lines
6.5 KiB
Python

from flask import Flask, render_template, Blueprint, abort, request, flash, url_for, redirect, send_file
from pathlib import Path
from datetime import datetime
from dateutil.relativedelta import relativedelta
from dotenv import load_dotenv
import json
import shutil
import requests
import click
import humanize
import os
import subprocess
import functions as func
import constants as const
load_dotenv()
app = Flask(const.appName)
app.secret_key = os.environ.get("APP_SECRET")
cfg = func.loadJSON(const.configFile)
api = Blueprint('api', const.appName)
themes_ = func.loadJSON(const.themesFile)
for theme in themes_:
theme['escaped_name'] = func.escapeThemeName(theme['name'])
theme['size'] = humanize.naturalsize(Path(theme['theme_file_url']).stat().st_size)
file_path = Path.cwd() / theme['theme_file_url']
with open(file_path) as f:
theme['file_contents'] = f.read()
# -- cli commands --
@app.cli.command("add-theme")
@click.option("--name", "-n", required=True, help="Theme name")
@click.option("--description", "-d", type=str, help="Theme description, if available")
@click.option("--version", "-v", type=str, default="1.0", help="Theme version, if available")
@click.option("--author", "-a", required=True, help="Theme author")
@click.option("--author-url", "-l", type=str, help="Theme author url, if available")
@click.option("--homepage", "-h", type=str, help="Theme homepage, if available")
@click.option("--created-at", "-c", type=str, required=True, help="Theme creation date")
@click.option("--updated-at", "-u", type=str, help="Theme update date, if available (defaults to current date)")
@click.option("--preview-file", "-p", type=str, required=True, help="Path to theme preview image")
@click.option("--theme-file", "-t", type=str, required=True, help="Path to theme file")
@click.option("--remote", "-r", type=bool, default=False, help="Whether the theme path is remote")
@click.option("--print-json", "-j", type=bool, default=False, help="Whether to print the result entry in JSON")
def add_theme(name, description, version, author, author_url, homepage, created_at, updated_at, preview_file, theme_file, remote, print_json):
theme_dir = os.path.join(const.themesDir, func.slugify(author), func.slugify(name))
os.makedirs(theme_dir, exist_ok=True)
theme_dir = Path(theme_dir)
preview_file = Path(preview_file).expanduser()
theme_file = Path(theme_file).expanduser()
preview_dest = Path(theme_dir / "preview.png")
theme_dest = Path(theme_dir / "theme.css")
if not remote:
shutil.copy(preview_file, preview_dest)
shutil.copy(theme_file, theme_dest)
theme_entry = {
"preview_url": f"/static/themes/{func.slugify(author)}/{func.slugify(name)}/preview.png",
"author": author,
"author_url": author_url or "",
"name": name,
"description": description or "",
"version": version,
"theme_file_url": f"static/themes/{func.slugify(author)}/{func.slugify(name)}/theme.css",
"homepage": homepage or "",
"created_at": created_at,
"updated_at": updated_at or datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
if os.path.exists(const.themesFile):
with open(const.themesFile, "r", encoding="utf-8") as f:
try:
themes = json.load(f)
except json.JSONDecodeError:
themes = []
else:
themes = []
themes.append(theme_entry)
with open(const.themesFile, "w", encoding="utf-8") as f:
json.dump(themes, f, indent=4)
if print_json:
click.echo("JSON entry:")
click.echo(theme_entry)
click.echo()
click.echo(f"Theme '{name}' by {author} added successfully.")
# -- context processor --
@app.context_processor
def inject_stuff():
git_hash = subprocess.check_output(['git', f'--git-dir={Path.cwd()}/.git', 'rev-parse', '--short', 'HEAD']).strip().decode('utf-8')
return dict(const=const, git_hash=git_hash, datetime=datetime, formatRelativeTime=func.formatRelativeTime, themes=themes_, len=len, cfg=cfg)
# -- frontend routes --
@app.route('/', methods=['GET'])
def index():
return render_template('index.html', themes=themes_)
@app.route('/search/', methods=['GET'])
def search():
query = request.args.get('q', '').lower()
if not query:
flash("Search query cannot be empty!", "danger")
return redirect(request.referrer)
results = [t for t in themes_ if query in t['name'].lower()]
return render_template('search.html', query=query, results=results)
@app.route('/themes/<esc_theme_name>/', methods=['GET'])
def viewTheme(esc_theme_name):
theme = next((t for t in themes_ if t['escaped_name'] == esc_theme_name), None)
return render_template('view.html', theme=theme)
@app.route('/explore/', methods=['GET'])
def explore():
return render_template('explore.html')
@app.route('/download/<author>-<theme>/', methods=['GET'])
def downloadTheme(author, theme):
return send_file(
Path.cwd() / 'static' / f"themes/{func.slugify(author)}/{func.slugify(theme)}/theme.css",
download_name=f"{ func.slugify(theme) }.css")
# -- api routes --
@api.route('/themes/', methods=['GET'])
def themes():
return themes_
@api.route('/themes/<int:theme_id>/', methods=['GET'])
def singleTheme(theme_id: int):
if theme_id < 1:
return abort(404)
else:
return themes_[theme_id - 1]
@api.route('/search/', methods=['GET'])
def search():
query = request.args.get('q', '').lower()
results = [t for t in themes_ if query in t['name'].lower()]
return results
@api.route('/hyper/search/', methods=['GET'])
def hyperSearch():
query = request.args.get('q', '').lower()
if not query.rstrip():
return ""
results = [t for t in themes_ if query in t['name'].lower()]
html = f"""
<span class="list-group-item">{ len(results) } results</span>
"""
if results:
for theme in results:
html += f"""
<a href="{ url_for('viewTheme', esc_theme_name=theme['escaped_name']) }" class="list-group-item list-group-item-action p-3">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{ theme['name'] }</h5>
<small class="text-body-secondary">{ func.formatRelativeTime(theme['updated_at']) }</small>
</div>
<p class="mb-1">{ theme['description'] }</p>
<small class="text-body-secondary">by { theme['author'] }</small>
</a>
"""
else:
html = f"""
<div class="list-group-item list-group-item-action p-3">
<h4>No themes found.</h4>
<span class="text-body-secondary">No themes found for query "{ query }".</span>
</div>
"""
return html
app.register_blueprint(api, url_prefix='/api/v1')
if __name__ == '__main__':
app.run()