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//', 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/-/', 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//', 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""" { len(results) } results """ if results: for theme in results: html += f"""
{ theme['name'] }
{ func.formatRelativeTime(theme['updated_at']) }

{ theme['description'] }

by { theme['author'] }
""" else: html = f"""

No themes found.

No themes found for query "{ query }".
""" return html app.register_blueprint(api, url_prefix='/api/v1') if __name__ == '__main__': app.run()