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)