countdowngen/app.py

196 lines
6.4 KiB
Python

from flask import Flask, render_template, request, redirect, url_for
from urllib.parse import urlencode
import datetime
import json
import os
app = Flask(__name__)
# Parameter mapping for URL compression
PARAM_MAP = {
# Long -> Short
'title': 't',
'target': 'dt',
'scheme': 's',
'accent': 'a',
'radius': 'r',
'shadow': 'sh',
'font': 'f',
'millis': 'm',
'rounded_unit': 'ru',
'bg': 'bg',
'fg': 'fg',
'lang': 'l'
}
# Short -> Long (reverse mapping)
REVERSE_PARAM_MAP = {v: k for k, v in PARAM_MAP.items()}
def normalize_params(params_dict):
"""Convert short parameter names to long ones for internal use"""
normalized = {}
for key, value in params_dict.items():
# Convert short to long if it exists in reverse map, otherwise keep original
long_key = REVERSE_PARAM_MAP.get(key, key)
normalized[long_key] = value
return normalized
def compress_params(params_dict):
"""Convert long parameter names to short ones for URLs"""
compressed = {}
for key, value in params_dict.items():
# Convert long to short if it exists in map, otherwise keep original
short_key = PARAM_MAP.get(key, key)
compressed[short_key] = value
return compressed
def get_param(args, key, default=None):
"""Get parameter value checking both long and short names"""
# Try long name first
value = args.get(key, None)
if value is not None:
return value
# Try short name
short_key = PARAM_MAP.get(key)
if short_key:
value = args.get(short_key, None)
if value is not None:
return value
return default
# Load translations
translations = {}
translations_dir = os.path.join(app.root_path, 'translations')
for filename in os.listdir(translations_dir):
if filename.endswith('.json'):
lang_code = filename[:-5] # Remove .json extension
with open(os.path.join(translations_dir, filename), 'r', encoding='utf-8') as f:
translations[lang_code] = json.load(f)
def get_translation(lang, key, fallback_lang='en'):
"""Get translation for a key in specified language, fallback to English if not found"""
keys = key.split('.')
# Try specified language first
current = translations.get(lang, {})
for k in keys:
if k in current:
current = current[k]
else:
break
else:
return current
# Fallback to English
current = translations.get(fallback_lang, {})
for k in keys:
if k in current:
current = current[k]
else:
return key # Return key if not found
return current
def coerce_bool(val, default=False):
if val is None:
return default
s = str(val).lower()
return s in ("1","true","yes","y","on")
@app.template_filter("default_if_none")
def default_if_none(value, default):
return default if value is None or value == "" else value
@app.template_filter("translate")
def translate_filter(key, lang='en'):
return get_translation(lang, key)
@app.template_filter("url_with_lang")
def url_with_lang_filter(query_string, new_lang):
"""Build clean URL with new language parameter using compressed format"""
from urllib.parse import parse_qs, urlencode
# Parse existing query string
if query_string:
params = parse_qs(query_string)
# Flatten the parameters (parse_qs creates lists)
flat_params = {k: v[0] if v else '' for k, v in params.items() if v}
# Normalize to long parameter names first
normalized = normalize_params(flat_params)
# Remove any existing lang parameters
normalized.pop('lang', None)
else:
normalized = {}
# Add new language
normalized['lang'] = new_lang
# Convert back to compressed format
compressed = compress_params(normalized)
# Build clean query string
return '?' + urlencode(compressed) if compressed else f'?{PARAM_MAP.get("lang", "lang")}={new_lang}'
@app.route("/")
def index():
# Prefill from query if present, so users can tweak and regenerate
q = request.args
lang = get_param(q, "lang", "en")
context = {
"title": get_param(q, "title", get_translation(lang, "form.countdown_title_placeholder")),
"target": get_param(q, "target", ""),
"scheme": get_param(q, "scheme", "dark"),
"accent": get_param(q, "accent", "#8b5cf6"), # violet-500
"radius": get_param(q, "radius", "16"),
"shadow": get_param(q, "shadow", "lg"),
"font": get_param(q, "font", "system-ui"),
"show_millis": coerce_bool(get_param(q, "millis"), False),
"rounded_unit": get_param(q, "rounded_unit", "none"), # none, minutes, hours, days
"bg": get_param(q, "bg", "#0b0b10"),
"fg": get_param(q, "fg", "#e5e7eb"),
"lang": lang,
"t": lambda key: get_translation(lang, key)
}
return render_template("index.html", **context)
@app.route("/preview", methods=["POST"])
def preview():
data = request.form.to_dict(flat=True)
# Convert to compressed parameters
compressed_data = compress_params(data)
# Build query string and redirect to /countdown
return redirect(url_for("countdown") + "?" + urlencode(compressed_data))
@app.route("/countdown")
def countdown():
q = request.args
lang = get_param(q, "lang", "en")
# Provide sane defaults if not supplied
params = {
"title": get_param(q, "title", get_translation(lang, "form.countdown_title_placeholder")),
"target": get_param(q, "target", ""),
"scheme": get_param(q, "scheme", "dark"),
"accent": get_param(q, "accent", "#8b5cf6"),
"radius": get_param(q, "radius", "16"),
"shadow": get_param(q, "shadow", "lg"),
"font": get_param(q, "font", "system-ui"),
"millis": get_param(q, "millis", "0"),
"rounded_unit": get_param(q, "rounded_unit", "none"),
"bg": get_param(q, "bg", "#0b0b10"),
"fg": get_param(q, "fg", "#e5e7eb"),
"lang": lang,
"t": lambda key: get_translation(lang, key)
}
# If no target specified, bounce user to setup screen
if not params["target"]:
# Pass lang as short parameter when redirecting
lang_param = PARAM_MAP.get("lang", "lang")
return redirect(url_for("index") + f"?{lang_param}={lang}")
return render_template("countdown.html", **params)
if __name__ == "__main__":
# Helpful for local dev
app.run(debug=True, host="0.0.0.0", port=5000)