196 lines
6.4 KiB
Python
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)
|