from __future__ import annotations import json from pathlib import Path from flask import Flask from markupsafe import escape try: import pyphen except ModuleNotFoundError: # pragma: no cover - optional dependency in local dev pyphen = None from config import Config from .cli import register_cli, seed_badges from .extensions import csrf, db, login_manager from .forms import QuickTaskForm from .models import QuickWin from .routes import auth, main, scoreboard, settings, tasks from .routes.main import load_icon_svg from .services.app_settings import get_quick_task_config from .services.badges import sync_existing_badges from .services.bootstrap import ensure_schema_and_admins from .services.dates import MONTH_NAMES, local_now from .services.monthly import archive_months_missing_up_to_previous DE_HYPHENATOR = pyphen.Pyphen(lang="de_DE") if pyphen else None def _fallback_soft_hyphenate(word: str) -> str: return word def create_app(config_class: type[Config] = Config) -> Flask: app = Flask(__name__, static_folder="static", template_folder="templates") app.config.from_object(config_class) manifest_path = Path(app.root_path).parent / "CloudronManifest.json" try: app.config["APP_VERSION"] = json.loads(manifest_path.read_text(encoding="utf-8")).get("version", "0.0.0") except FileNotFoundError: app.config["APP_VERSION"] = "0.0.0" app.config["DATA_DIR"].mkdir(parents=True, exist_ok=True) app.config["UPLOAD_FOLDER"].mkdir(parents=True, exist_ok=True) db.init_app(app) login_manager.init_app(app) csrf.init_app(app) with app.app_context(): db.create_all() ensure_schema_and_admins() seed_badges() sync_existing_badges() register_cli(app) app.register_blueprint(main.bp) app.register_blueprint(auth.bp) app.register_blueprint(tasks.bp) app.register_blueprint(scoreboard.bp) app.register_blueprint(settings.bp) app.jinja_env.globals["icon_svg"] = lambda name: load_icon_svg(name, app.static_folder) @app.before_request def ensure_archives(): archive_months_missing_up_to_previous() @app.context_processor def inject_globals(): quick_task_form = QuickTaskForm(prefix="quick") quick_task_config = get_quick_task_config() quick_task_form.effort.choices = [ (key, values["label"]) for key, values in quick_task_config.items() ] quick_wins = QuickWin.query.filter_by(active=True).order_by(QuickWin.sort_order.asc(), QuickWin.id.asc()).all() def asset_version(filename: str) -> int: path = Path(app.static_folder) / filename try: return int(path.stat().st_mtime) except FileNotFoundError: return 1 return { "app_name": app.config["APP_NAME"], "app_version": app.config["APP_VERSION"], "nav_items": [ ("tasks.my_tasks", "Meine Aufgaben", "house"), ("tasks.all_tasks", "Alle", "list"), ("tasks.archive_view", "Archiv", "check-double"), ("tasks.create", "Neu", "plus"), ("tasks.calendar_view", "Kalender", "calendar"), ("scoreboard.index", "Highscore", "trophy"), ("settings.index", "Optionen", "gear"), ], "mobile_nav_items": [ ("tasks.my_tasks", "Meine Aufgaben", "house"), ("tasks.all_tasks", "Alle Aufgaben", "list"), ("tasks.calendar_view", "Kalender", "calendar"), ("scoreboard.index", "Highscore", "trophy"), ("settings.index", "Optionen", "gear"), ], "icon_svg": lambda name: load_icon_svg(name, app.static_folder), "asset_version": asset_version, "now_local": local_now(), "quick_task_form": quick_task_form, "quick_task_config": quick_task_config, "quick_wins": quick_wins, } @app.template_filter("date_de") def date_de(value): return value.strftime("%d.%m.%Y") if value else "—" @app.template_filter("datetime_de") def datetime_de(value): return value.strftime("%d.%m.%Y, %H:%M") if value else "—" @app.template_filter("month_name") def month_name(value): return MONTH_NAMES[value] @app.template_filter("hyphenate_de") def hyphenate_de(value): if not value: return "" text = str(value) parts: list[str] = [] current = [] def flush_word(): if not current: return word = "".join(current) if len(word) >= 6: if DE_HYPHENATOR: parts.append(DE_HYPHENATOR.inserted(word, "\u00AD")) else: parts.append(_fallback_soft_hyphenate(word)) else: parts.append(word) current.clear() for char in text: if char.isalpha() or char in "ÄÖÜäöüß": current.append(char) else: flush_word() parts.append(char) flush_word() return escape("".join(parts)) return app