188 lines
6.5 KiB
Python
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()
|