From 49653cf0af0e7f4e05c474d4ccd5ced105e59f7c Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 24 Dec 2025 15:57:53 +0000 Subject: [PATCH 01/14] Repaired confirm dialog --- pyproject.toml | 2 +- src/flaskpp/app/static/js/base.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 22806ef..88d2514 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flaskpp" -version = "0.2.4" +version = "0.2.5" description = "A Flask based framework for fast and easy app creation. Experience the real power of Flask without boilerplate, but therefore a well balanced mix of magic and the default Flask framework." authors = [ { name = "Pierre", email = "pierre@growv-mail.org" } diff --git a/src/flaskpp/app/static/js/base.js b/src/flaskpp/app/static/js/base.js index 5d4cd10..4ccbd6a 100644 --- a/src/flaskpp/app/static/js/base.js +++ b/src/flaskpp/app/static/js/base.js @@ -63,7 +63,7 @@ const infoBody = document.getElementById('infoModalBody'); export async function confirmDialog(title, message, html, category) { - confirmText.innerHTML = message.replace(/\n/g, "
"); + confirmTitle.textContent = title; confirmBtn.className = `inline-flex items-center justify-center px-4 py-2 rounded-lg text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-primary/40 transition text-white @@ -76,7 +76,7 @@ export async function confirmDialog(title, message, html, category) { if (message) { confirmBody.classList.add('hidden'); confirmText.classList.remove('hidden'); - confirmText.textContent = message; + confirmText.textContent = message.replace(/\n/g, "
"); } else { confirmText.classList.add('hidden'); confirmBody.classList.remove('hidden'); @@ -112,7 +112,7 @@ export function showInfo(title, message, html) { if (message) { infoBody.classList.add('hidden'); infoText.classList.remove('hidden'); - infoText.textContent = message; + infoText.textContent = message.replace(/\n/g, "
"); } else { infoText.classList.add('hidden'); infoBody.classList.remove('hidden'); From a237a498c892f2ebbb7024428dca4b507ce60624 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 25 Dec 2025 11:08:00 +0000 Subject: [PATCH 02/14] Fixed passthrough cli --- pyproject.toml | 2 +- src/flaskpp/cli.py | 12 ++++++----- src/flaskpp/fpp_node/__init__.py | 8 ++++++-- src/flaskpp/fpp_node/cli.py | 34 ++++++++++++++------------------ src/flaskpp/fpp_node/vite.py | 6 +++--- src/flaskpp/tailwind/__init__.py | 12 ++++++++--- src/flaskpp/tailwind/cli.py | 23 +++++++++++---------- src/flaskpp/utils/__init__.py | 5 +++++ src/flaskpp/utils/run.py | 4 ++++ src/flaskpp/utils/setup.py | 4 ++++ 10 files changed, 65 insertions(+), 45 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 88d2514..7d5c681 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flaskpp" -version = "0.2.5" +version = "0.2.8" description = "A Flask based framework for fast and easy app creation. Experience the real power of Flask without boilerplate, but therefore a well balanced mix of magic and the default Flask framework." authors = [ { name = "Pierre", email = "pierre@growv-mail.org" } diff --git a/src/flaskpp/cli.py b/src/flaskpp/cli.py index 9bbf886..159cc30 100644 --- a/src/flaskpp/cli.py +++ b/src/flaskpp/cli.py @@ -3,8 +3,8 @@ import typer, os, subprocess, sys from flaskpp.modules.cli import modules_entry -from flaskpp.utils.setup import setup -from flaskpp.utils.run import run +from flaskpp.utils.setup import setup_entry +from flaskpp.utils.run import run_entry from flaskpp.utils.service_registry import registry_entry from flaskpp.tailwind import setup_tailwind from flaskpp.fpp_node import load_node @@ -195,15 +195,17 @@ def create_app(config_name: str = "default"): typer.echo(typer.style("Flask++ project successfully initialized.", fg=typer.colors.GREEN, bold=True)) -app.command()(setup) -app.command()(run) - def main(): + setup_entry(app) + run_entry(app) + modules_entry(app) registry_entry(app) + node_entry(app) tailwind_entry(app) + app() diff --git a/src/flaskpp/fpp_node/__init__.py b/src/flaskpp/fpp_node/__init__.py index 7878bc6..b656898 100644 --- a/src/flaskpp/fpp_node/__init__.py +++ b/src/flaskpp/fpp_node/__init__.py @@ -19,9 +19,13 @@ def _get_node_data(): def _node_cmd(cmd: str) -> str: + node = home / "node" + if not node.exists(): + raise NodeError("Missing node directory.") + if os.name == "nt": - return str(home / "node" / f"{cmd}.cmd") - return str(home / "node" / "bin" / cmd) + return str(node / f"{cmd}.cmd") + return str(node / "bin" / cmd) def _node_env() -> dict: diff --git a/src/flaskpp/fpp_node/cli.py b/src/flaskpp/fpp_node/cli.py index 612b993..a80258f 100644 --- a/src/flaskpp/fpp_node/cli.py +++ b/src/flaskpp/fpp_node/cli.py @@ -2,23 +2,14 @@ from flaskpp.fpp_node import _node_cmd, NodeError -node = typer.Typer( - help="Run node commands using the portable Flask++ node bundle. (Behaves like normal node.)" -) - - -@node.callback( - invoke_without_command=True, - context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, -) -def main( - ctx: typer.Context, - command: str = typer.Argument( - "npm", - help="The node command to execute", - ) -): - args = ctx.args + +def node(ctx: typer.Context): + if not ctx.args: + typer.echo(typer.style("Usage: fpp node [args]", bold=True, fg=typer.colors.YELLOW)) + raise typer.Exit(1) + + command = ctx.args[0] + args = ctx.args[1:] result = subprocess.run( [_node_cmd(command), *args], @@ -28,10 +19,15 @@ def main( ) if result.returncode != 0: - raise NodeError(result.stderr) + raise NodeError(result.stderr or result.stdout) typer.echo(result.stdout) def node_entry(app: typer.Typer): - app.add_typer(node, name="node") + app.command( + context_settings={ + "allow_extra_args": True, + "ignore_unknown_options": True, + } + )(node) diff --git a/src/flaskpp/fpp_node/vite.py b/src/flaskpp/fpp_node/vite.py index 5f7575f..6a58b7a 100644 --- a/src/flaskpp/fpp_node/vite.py +++ b/src/flaskpp/fpp_node/vite.py @@ -8,7 +8,7 @@ import typer, subprocess, json, re, requests, os from flaskpp.fpp_node import home, _node_cmd, _node_env -from flaskpp.utils import enabled, is_port_free +from flaskpp.utils import enabled, is_port_free, posix_path from flaskpp.utils.debugger import exception @@ -225,8 +225,8 @@ def __init__(self, parent: FlaskPP | Module): safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", parent.name) conf_name = f"vite.config.{safe_name}.js" (home / conf_name).write_text(vite_conf.format( - root=str(root), - entry_point=str(main) + root=posix_path(root), + entry_point=posix_path(main) )) conf_params = ["--config", conf_name] if not main.exists(): diff --git a/src/flaskpp/tailwind/__init__.py b/src/flaskpp/tailwind/__init__.py index 606b268..3c1108c 100644 --- a/src/flaskpp/tailwind/__init__.py +++ b/src/flaskpp/tailwind/__init__.py @@ -21,10 +21,16 @@ def _get_cli_data(): return tailwind_cli[selector], selector -def _tailwind_cmd(): +def _tailwind_cmd() -> str: + tw = "tailwind" if os.name == "nt": - return str(home / "tailwind.exe") - return str(home / "tailwind") + tw += ".exe" + + executable = home / tw + if not executable.exists(): + raise TailwindError("Missing tailwind cli executable.") + + return str(executable) def generate_tailwind_css(app: Flask): diff --git a/src/flaskpp/tailwind/cli.py b/src/flaskpp/tailwind/cli.py index dd24d51..3fa3be7 100644 --- a/src/flaskpp/tailwind/cli.py +++ b/src/flaskpp/tailwind/cli.py @@ -2,19 +2,13 @@ from flaskpp.tailwind import _tailwind_cmd, TailwindError -tailwind = typer.Typer( - help="Use the tailwind cli using the standalone Flask++ integration." -) +def tailwind(ctx: typer.Context): + if not ctx.args: + typer.echo(typer.style("Usage: fpp tailwind -- [args]", bold=True)) + raise typer.Exit(1) -@tailwind.callback( - invoke_without_command=True, - context_settings={"allow_extra_args": True, "ignore_unknown_options": True}, -) -def main( - ctx: typer.Context -): - args = ctx.args + args = ctx.args[0:] result = subprocess.run( [_tailwind_cmd(), *args], @@ -30,4 +24,9 @@ def main( def tailwind_entry(app: typer.Typer): - app.add_typer(tailwind, name="tailwind") + app.command( + context_settings={ + "allow_extra_args": True, + "ignore_unknown_options": True, + } + )(tailwind) diff --git a/src/flaskpp/utils/__init__.py b/src/flaskpp/utils/__init__.py index 1be1cc2..e4b303c 100644 --- a/src/flaskpp/utils/__init__.py +++ b/src/flaskpp/utils/__init__.py @@ -1,3 +1,4 @@ +from pathlib import Path import os, string, random, socket @@ -28,3 +29,7 @@ def is_port_free(port, host="127.0.0.1") -> bool: def sanitize_text(value: str) -> str: return value.encode("utf-8", "ignore").decode("utf-8") + + +def posix_path(path: Path) -> str: + return path.as_posix() diff --git a/src/flaskpp/utils/run.py b/src/flaskpp/utils/run.py index 4ded72f..294f751 100755 --- a/src/flaskpp/utils/run.py +++ b/src/flaskpp/utils/run.py @@ -245,3 +245,7 @@ def run( start_app(conf, args["port"]) while True: continue + + +def run_entry(app: typer.Typer): + app.command()(run) diff --git a/src/flaskpp/utils/setup.py b/src/flaskpp/utils/setup.py index 25ab3cd..bdde7c8 100644 --- a/src/flaskpp/utils/setup.py +++ b/src/flaskpp/utils/setup.py @@ -189,3 +189,7 @@ def setup(): typer.echo("----------------- " + typer.style("Happy coding!", fg=typer.colors.CYAN, bold=True) + " -----------------") + + +def setup_entry(app: typer.Typer): + app.command()(setup) From 6acba7f1167732863007174a30af18fe1c19c2ad Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 25 Dec 2025 18:16:52 +0100 Subject: [PATCH 03/14] Fixed FRONTEND_ENGINE on NT based systems --- src/flaskpp/__init__.py | 4 ++-- src/flaskpp/cli.py | 2 +- src/flaskpp/fpp_node/cli.py | 9 +++------ src/flaskpp/fpp_node/{vite.py => fpp_vite.py} | 13 ++++++++----- src/flaskpp/tailwind/cli.py | 6 ++---- src/flaskpp/utils/__init__.py | 4 ---- 6 files changed, 16 insertions(+), 22 deletions(-) rename src/flaskpp/fpp_node/{vite.py => fpp_vite.py} (96%) diff --git a/src/flaskpp/__init__.py b/src/flaskpp/__init__.py index d77c864..d261e35 100644 --- a/src/flaskpp/__init__.py +++ b/src/flaskpp/__init__.py @@ -152,7 +152,7 @@ def __init__(self, import_name: str, config_name: str): self.static_url_path = f"{self.url_prefix}/static" if enabled("FRONTEND_ENGINE"): - from flaskpp.fpp_node.vite import Frontend + from flaskpp.fpp_node.fpp_vite import Frontend engine = Frontend(self) self.context_processor(lambda: { "vite_main": engine.vite @@ -237,7 +237,7 @@ def _enable(self, app: FlaskPP, home: bool): log("warn", f"Failed to initialize models for {self.name}: {e}") if enabled("FRONTEND_ENGINE"): - from flaskpp.fpp_node.vite import Frontend + from flaskpp.fpp_node.fpp_vite import Frontend engine = Frontend(self) self.context["vite"] = engine.vite self.frontend_engine = engine diff --git a/src/flaskpp/cli.py b/src/flaskpp/cli.py index 159cc30..558919e 100644 --- a/src/flaskpp/cli.py +++ b/src/flaskpp/cli.py @@ -8,7 +8,7 @@ from flaskpp.utils.service_registry import registry_entry from flaskpp.tailwind import setup_tailwind from flaskpp.fpp_node import load_node -from flaskpp.fpp_node.vite import prepare_vite +from flaskpp.fpp_node.fpp_vite import prepare_vite from flaskpp.fpp_node.cli import node_entry from flaskpp.tailwind.cli import tailwind_entry diff --git a/src/flaskpp/fpp_node/cli.py b/src/flaskpp/fpp_node/cli.py index a80258f..fc52900 100644 --- a/src/flaskpp/fpp_node/cli.py +++ b/src/flaskpp/fpp_node/cli.py @@ -1,6 +1,6 @@ import typer, subprocess, os -from flaskpp.fpp_node import _node_cmd, NodeError +from flaskpp.fpp_node import _node_cmd, _node_env, NodeError def node(ctx: typer.Context): @@ -14,14 +14,11 @@ def node(ctx: typer.Context): result = subprocess.run( [_node_cmd(command), *args], cwd=os.getcwd(), - capture_output=True, - text=True, + env=_node_env(), ) if result.returncode != 0: - raise NodeError(result.stderr or result.stdout) - - typer.echo(result.stdout) + raise NodeError("Node command failed.") def node_entry(app: typer.Typer): diff --git a/src/flaskpp/fpp_node/vite.py b/src/flaskpp/fpp_node/fpp_vite.py similarity index 96% rename from src/flaskpp/fpp_node/vite.py rename to src/flaskpp/fpp_node/fpp_vite.py index 6a58b7a..8dcc6e7 100644 --- a/src/flaskpp/fpp_node/vite.py +++ b/src/flaskpp/fpp_node/fpp_vite.py @@ -8,7 +8,7 @@ import typer, subprocess, json, re, requests, os from flaskpp.fpp_node import home, _node_cmd, _node_env -from flaskpp.utils import enabled, is_port_free, posix_path +from flaskpp.utils import enabled, is_port_free from flaskpp.utils.debugger import exception @@ -30,7 +30,8 @@ class ManifestChunk: package_json = """ { "name": "fpp-vite", - "version": "0.0.2", + "version": "0.0.3", + "type": "module", "scripts": { "dev": "vite", "build": "vite build", @@ -114,7 +115,9 @@ class ManifestChunk: """ vite_tw = """ -@import "tailwindcss"; +@tailwind base; +@tailwind components; +@tailwind utilities; @theme { /* ... */ @@ -225,8 +228,8 @@ def __init__(self, parent: FlaskPP | Module): safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", parent.name) conf_name = f"vite.config.{safe_name}.js" (home / conf_name).write_text(vite_conf.format( - root=posix_path(root), - entry_point=posix_path(main) + root=Path(os.path.relpath(root, start=home)).as_posix(), + entry_point=Path(os.path.relpath(main, start=home)).as_posix() )) conf_params = ["--config", conf_name] if not main.exists(): diff --git a/src/flaskpp/tailwind/cli.py b/src/flaskpp/tailwind/cli.py index 3fa3be7..0d02d77 100644 --- a/src/flaskpp/tailwind/cli.py +++ b/src/flaskpp/tailwind/cli.py @@ -12,13 +12,11 @@ def tailwind(ctx: typer.Context): result = subprocess.run( [_tailwind_cmd(), *args], - cwd=os.getcwd(), - capture_output=True, - text=True, + cwd=os.getcwd() ) if result.returncode != 0: - raise TailwindError(result.stderr) + raise TailwindError("Tailwind command failed.") typer.echo(result.stdout) diff --git a/src/flaskpp/utils/__init__.py b/src/flaskpp/utils/__init__.py index e4b303c..320f080 100644 --- a/src/flaskpp/utils/__init__.py +++ b/src/flaskpp/utils/__init__.py @@ -29,7 +29,3 @@ def is_port_free(port, host="127.0.0.1") -> bool: def sanitize_text(value: str) -> str: return value.encode("utf-8", "ignore").decode("utf-8") - - -def posix_path(path: Path) -> str: - return path.as_posix() From 3d16b5350cea13027cf18c8c1ee1e4d73b58d9cc Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 30 Dec 2025 14:49:51 +0000 Subject: [PATCH 04/14] Further (potential) nt fixes --- pyproject.toml | 2 +- src/flaskpp/app/static/css/tailwind_raw.css | 2 -- src/flaskpp/fpp_node/fpp_vite.py | 2 +- src/flaskpp/modules/cli.py | 18 ++++++++++-------- src/flaskpp/modules/creator_templates.py | 9 --------- src/flaskpp/tailwind/__init__.py | 15 +++++++-------- 6 files changed, 19 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7d5c681..f7f68c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flaskpp" -version = "0.2.8" +version = "0.2.12" description = "A Flask based framework for fast and easy app creation. Experience the real power of Flask without boilerplate, but therefore a well balanced mix of magic and the default Flask framework." authors = [ { name = "Pierre", email = "pierre@growv-mail.org" } diff --git a/src/flaskpp/app/static/css/tailwind_raw.css b/src/flaskpp/app/static/css/tailwind_raw.css index 54eb724..2012beb 100644 --- a/src/flaskpp/app/static/css/tailwind_raw.css +++ b/src/flaskpp/app/static/css/tailwind_raw.css @@ -127,9 +127,7 @@ .footer-inner-div { @apply max-w-7xl mx-auto px-4 py-3 text-center opacity-80; } -} -@layer utilities { .wrap-center { @apply flex items-center justify-center; } diff --git a/src/flaskpp/fpp_node/fpp_vite.py b/src/flaskpp/fpp_node/fpp_vite.py index 8dcc6e7..309440c 100644 --- a/src/flaskpp/fpp_node/fpp_vite.py +++ b/src/flaskpp/fpp_node/fpp_vite.py @@ -138,7 +138,7 @@ def prepare_vite(): ) + "/**/vite/src" if not ts_conf_file.exists(): ts_conf_file.write_text(ts_conf_template.format( - include=tsc_path + include=tsc_path.replace("\\", "/") )) else: ts_conf = json.loads(ts_conf_file.read_text()) diff --git a/src/flaskpp/modules/cli.py b/src/flaskpp/modules/cli.py index cd60931..fd99742 100644 --- a/src/flaskpp/modules/cli.py +++ b/src/flaskpp/modules/cli.py @@ -1,6 +1,6 @@ from git import Repo, exc from pathlib import Path -import typer, shutil +import typer, shutil, json from flaskpp.utils import prompt_yes_no, sanitize_text from flaskpp.modules import module_home, creator_templates @@ -56,14 +56,16 @@ def create( module_dst.unlink() module_dst.mkdir(exist_ok=True) - manifest = creator_templates.module_manifest.format( - name=sanitize_text(input("Enter the name of your module: ")), - description=sanitize_text(input("Describe your module briefly: ")), - version=sanitize_text(input("Enter the version of your module: ")), - author=sanitize_text(input("Enter your name or nickname: ")) - ) + manifest = { + "name": sanitize_text(input("Enter the name of your module: ")), + "description": sanitize_text(input("Describe your module briefly: ")), + "version": sanitize_text(input("Enter the version of your module: ")), + "author": sanitize_text(input("Enter your name or nickname: ")) + } typer.echo(typer.style(f"Writing manifest...", bold=True)) - (module_dst / "manifest.json").write_text(manifest) + (module_dst / "manifest.json").write_text( + json.dumps(manifest, indent=2, ensure_ascii=False) + ) typer.echo(typer.style(f"Creating basic structure...", bold=True)) (module_dst / "handling").mkdir(exist_ok=True) diff --git a/src/flaskpp/modules/creator_templates.py b/src/flaskpp/modules/creator_templates.py index 80af0a1..5fa7a77 100644 --- a/src/flaskpp/modules/creator_templates.py +++ b/src/flaskpp/modules/creator_templates.py @@ -94,12 +94,3 @@ def init_models(): /* ... */ } """ - -module_manifest = """ -{{ - "name": "{name}", - "description": "{description}", - "version": "{version}", - "author": "{author}" -}} -""" diff --git a/src/flaskpp/tailwind/__init__.py b/src/flaskpp/tailwind/__init__.py index 3c1108c..66f99ed 100644 --- a/src/flaskpp/tailwind/__init__.py +++ b/src/flaskpp/tailwind/__init__.py @@ -37,11 +37,11 @@ def generate_tailwind_css(app: Flask): out = (home.parent / "app" / "static" / "css" / "tailwind.css") if not out.exists(): + in_file = out.parent / "tailwind_raw.css" result = subprocess.run( - [_tailwind_cmd(), - "-i", str(out.parent / "tailwind_raw.css"), - "-o", str(out), "--minify"], - cwd=home.parent + f'"{_tailwind_cmd()}" -i "{in_file}" -o "{out}" --minify', + cwd=home.parent, + shell=True ) if result.returncode != 0: raise TailwindError(f"Failed to generate {out}") @@ -54,10 +54,9 @@ def generate_tailwind_css(app: Flask): continue result = subprocess.run( - [_tailwind_cmd(), - "-i", str(in_file), - "-o", str(d / "tailwind.css"), "--minify"], - cwd=d + f'"{_tailwind_cmd()}" -i "{in_file}" -o "{out}" --minify', + cwd=d, + shell=True ) if result.returncode != 0: raise TailwindError(f"Failed to generate {d / 'tailwind.css'}") From ac9c45cf93271111379d38fc256fbe72ee9a8ae1 Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 30 Dec 2025 19:43:55 +0100 Subject: [PATCH 05/14] Final cross-compatability fixes, updated docs and improved home module functionality --- DOCS.md | 45 ++++++++--------- src/flaskpp/__init__.py | 13 ++++- src/flaskpp/app/static/css/tailwind_raw.css | 4 +- src/flaskpp/cli.py | 3 +- src/flaskpp/fpp_node/cli.py | 2 +- src/flaskpp/fpp_node/fpp_vite.py | 54 +++++++++++++-------- src/flaskpp/modules/creator_templates.py | 2 +- src/flaskpp/tailwind/__init__.py | 35 +++++++------ 8 files changed, 96 insertions(+), 62 deletions(-) diff --git a/DOCS.md b/DOCS.md index 0fef44e..1e29ffa 100644 --- a/DOCS.md +++ b/DOCS.md @@ -20,10 +20,10 @@ The FlaskPP class just extended Flask with basic factory tasks like setting up e ### Configuration -There are two ways of configuring your apps. The first and most important one are app configs, which you can find in project_root/app_configs. +There are two ways of configuring your apps. The first and most important one is app configs, which you can find in project_root/app_configs. They are named like that: **[app_name].conf** and used by the `fpp run` command to load your apps. (Config variables are passed as environment.) -With app configs you can control which extensions you want to use and pass secrets, defaults and every other data which you would usually write into your env files. +With app configs you can control which extensions you want to use and pass secrets, defaults and every other data that you would usually write into your env files. A basic Flask++ app.conf file looks like that: ``` @@ -80,9 +80,9 @@ HOME_MODULE = module_name And can be generated and configured automatically by running `fpp setup` inside your project root. -The second way of configuring your app, is by using config classes. You may have noticed, that the Flask++ app factory takes an +The second way of configuring your app, is by using config classes. You may have noticed that the Flask++ app factory takes a config name argument. There you can provide your own config name if you like to. We provide a registration function, so you can -plug in your own config files with ease. But of course we also provide a default config which you could just extend by your own config class: +plug in your own config files with ease. But of course we also provide a default config which you could extend by your own config class: ```python import os @@ -199,7 +199,7 @@ class DefaultConfig: ### Full-Stack Features & FPP Magic Inside our app.conf example you may have noticed that there are two feature switches, which are set to 1 by default. -The first one is **FPP_PROCESSING** which brings a slightly changed app processor registration to offer you some fueatures +The first one is **FPP_PROCESSING** which brings a slightly changed app processor registration to offer you some features like request logging and socket default events. If you have enabled this, you would have to use the Flask++ processing utils to overwrite our default processors: @@ -228,7 +228,7 @@ def handle(data): pass ``` -And of course we do also have some JavaScript utility that matches with our socket default handlers: +And of course, we do also have some JavaScript utility that matches with our socket default handlers: ```javascript /** @@ -269,19 +269,20 @@ export function emit(event, data=null, callback=null) { ``` Alright, before we talk about some further switch-less Flask++ magic... Let's talk about the second feature switch in our -app.conf file, which is **FRONTEND_ENGINE**. This switch enables you built in Vite engine. Your app and every module you may create has +app.conf file, which is **FRONTEND_ENGINE**. This switch enables you to use the built-in Vite engine. Your app and every module you may create has got a Vite folder inside it, which contains a main.js entrypoint. This is generated by default as a template you can use to integrate Vite into your Flask project. It will eiter run as `vite dev` if you run your app in debug mode or be built when starting your app -and then integrated using the .vite/manifest.json. If you want to integrate Vite files into your template, simply use: +and then integrated using the .vite/manifest.json. If you want to integrate Vite files into your template, use: `{{ vite_main("file.ending") }}` to integrate Vite files from your apps root and `{{ vite("file.ending") }}` inside module templates -to use the vite files of your modules. The framework does the configuration automatically for your. You can either write JavaScript or TypeScript. -And you can also work with Tailwind (Notice that there is a standalone Tailwind integration too. This is intended to -be fully seperated from your vite builds. We highly recommend to keep the autogenerated `@source not "../../vite"` part.), the default structure for +to use the vite files of your modules. The framework does the configuration automatically for you. You can either write JavaScript or TypeScript. +And you can also work with Tailwind (Notice that we are using the standalone Tailwind integration to generate your CSS. +This is done to reduce redundance. Anyway the CSS of your vite builds is generated seperately, to ensure easy export into standalone Node projects +using the Tailwindcss Vite plugin. That's why we highly recommend keeping the autogenerated `@source not "../../vite"` part.), the default structure for that is autogenerated as well. Okay, but now let's come to further Flask++ magic. The biggest switch-less feature is our module system. Modules look like little Flask apps which can simply be plugged into your app using the app.conf file. This process can be automated, if you install or create your modules before -running `fpp setup`. To work with modules, just use the modules sub-cli: +running `fpp setup`. To work with modules, use the modules sub-cli: ```bash # To install a module use: @@ -295,7 +296,7 @@ fpp modules create module_name ``` Our next feature is the i18n database (**EXT_BABEL**) with fallback to .po/.mo files for which we will offer an extra module to manage your translation keys -using a graphical web interface. ([FPP_i18n_module](https://github.com/GrowVolution/FPP_i18n_module) - coming soon.) But you can also manage your translations +using a graphical web interface. ([FPP_i18n_module](https://github.com/GrowVolution/FPP_i18n_module) – coming soon.) But you can also manage your translations by using our utility functions: ```python @@ -320,13 +321,13 @@ class MyUserMixin: # TODO: Add your own features and functionality ``` -Your mixin classes extend the user / role model, before the fsqla mixin extension is added. So be careful working with security features and utility. -In future, we'll add a priority feature, which will allow you to define the priority of your mixin when you decide to publish your own modules. +Your mixin classes extend the user / role model, before the fsqla mixin extension is added. So be careful working with security features and utilities. +In the future, we'll add a priority feature, which will allow you to define the priority of your mixin when you decide to publish your own modules. ### Running / Managing your apps -Attentive readers may have also noticed the `app.to_asgi()` wrapper. (This wrapper automatically wraps your app into the correct format - so it is sensitive to the **EXT_SOCKET** switch.) -This feature is required, if you want to execute your apps with our built-in executing utility, because Flask++ is running your apps using Uvicorn to offer +Attentive readers may have also noticed the `app.to_asgi()` wrapper. (This wrapper automatically wraps your app into the correct format – so it is sensitive to the **EXT_SOCKET** switch.) +This feature is required if you want to execute your apps with our built-in executing utility, because Flask++ is running your apps using Uvicorn to offer cross-platform compatibility. You've got two options to run your apps: ```bash @@ -339,7 +340,7 @@ fpp run [-i/--interactive] ### App Registry -If you are system administrator, you can also use our automated app registry (of course also cross-platform compatible): +If you are a system administrator, you can also use our automated app registry (of course also cross-platform compatible): ```bash fpp registry register app_name @@ -347,13 +348,13 @@ fpp registry [start/stop] app_name fpp registry remove app_name ``` -On NT based systems make sure you have pywin32 installed in your Python environment. +On NT-based systems make sure you have pywin32 installed in your Python environment.

Standalone Node & Tailwind implementation

Flask++ provides a native integration of Tailwind CSS and Node.js. -To use Tailwind, simply integrate: +To use Tailwind, integrate: ```html @@ -374,13 +375,13 @@ To use Tailwind, simply integrate: into your templates and work with its CSS utility. The app will (re-)generate all **tailwind.css** files based on your **tailwind_raw.css** files (auto generated by `fpp init` and `fpp modules create [mod_name]` in all **static/css** folders) when it is initialized. -And if you'd like to work with the native standalone node bundle, you can simply use the Flask++ Node CLI: +And if you'd like to work with the native standalone node bundle, you can use the Flask++ Node CLI: ```bash fpp node [npm/npx] [args] ``` -Of course, you can use the Tailwind CLI in a similar way: +Of course, you can use the Tailwind CLI similarly: ```bash fpp tailwind [args] diff --git a/src/flaskpp/__init__.py b/src/flaskpp/__init__.py index d261e35..1bcaf60 100644 --- a/src/flaskpp/__init__.py +++ b/src/flaskpp/__init__.py @@ -1,4 +1,4 @@ -from flask import Flask, Blueprint, render_template as _render_template, url_for +from flask import Flask, Blueprint, render_template as _render_template, url_for, send_from_directory from werkzeug.middleware.proxy_fix import ProxyFix from markupsafe import Markup from threading import Thread @@ -65,7 +65,11 @@ def set_default_handlers(app): class FlaskPP(Flask): def __init__(self, import_name: str, config_name: str): - super().__init__(import_name) + super().__init__( + import_name, + static_folder=None, + static_url_path=None + ) self.config.from_object(CONFIG_MAP.get(config_name, DefaultConfig)) start_session(enabled("DEBUG_MODE")) @@ -150,6 +154,11 @@ def __init__(self, import_name: str, config_name: str): self.url_prefix = "" register_modules(self) self.static_url_path = f"{self.url_prefix}/static" + self.add_url_rule( + f"{self.static_url_path}/", + endpoint="static", + view_func=lambda filename: send_from_directory(Path(self.root_path) / "static", filename) + ) if enabled("FRONTEND_ENGINE"): from flaskpp.fpp_node.fpp_vite import Frontend diff --git a/src/flaskpp/app/static/css/tailwind_raw.css b/src/flaskpp/app/static/css/tailwind_raw.css index 2012beb..62236cf 100644 --- a/src/flaskpp/app/static/css/tailwind_raw.css +++ b/src/flaskpp/app/static/css/tailwind_raw.css @@ -1,4 +1,4 @@ -@import "tailwindcss"; +@import "tailwindcss" source("../../"); @theme { /* ... */ @@ -127,7 +127,9 @@ .footer-inner-div { @apply max-w-7xl mx-auto px-4 py-3 text-center opacity-80; } +} +@layer utilities { .wrap-center { @apply flex items-center justify-center; } diff --git a/src/flaskpp/cli.py b/src/flaskpp/cli.py index 558919e..3789033 100644 --- a/src/flaskpp/cli.py +++ b/src/flaskpp/cli.py @@ -142,10 +142,11 @@ def create_app(config_name: str = "default"): """) (css / "tailwind_raw.css").write_text(""" -@import "tailwindcss"; +@import "tailwindcss" source("../../"); @source not "../../.venv"; @source not "../../venv"; + @source not "../../vite"; @source not "../../modules"; diff --git a/src/flaskpp/fpp_node/cli.py b/src/flaskpp/fpp_node/cli.py index fc52900..fb3a938 100644 --- a/src/flaskpp/fpp_node/cli.py +++ b/src/flaskpp/fpp_node/cli.py @@ -14,7 +14,7 @@ def node(ctx: typer.Context): result = subprocess.run( [_node_cmd(command), *args], cwd=os.getcwd(), - env=_node_env(), + env=_node_env() ) if result.returncode != 0: diff --git a/src/flaskpp/fpp_node/fpp_vite.py b/src/flaskpp/fpp_node/fpp_vite.py index 309440c..2b20346 100644 --- a/src/flaskpp/fpp_node/fpp_vite.py +++ b/src/flaskpp/fpp_node/fpp_vite.py @@ -8,6 +8,7 @@ import typer, subprocess, json, re, requests, os from flaskpp.fpp_node import home, _node_cmd, _node_env +from flaskpp.tailwind import generate_asset from flaskpp.utils import enabled, is_port_free from flaskpp.utils.debugger import exception @@ -35,34 +36,27 @@ class ManifestChunk: "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "ts-test": "tsc" }, "devDependencies": { "typescript": "~5.9.3", "vite": "^7.2.4" - }, - "dependencies": { - "@tailwindcss/vite": "^4.1.17", - "tailwindcss": "^4.1.17" } } """ vite_conf = """ import {{ defineConfig }} from "vite" -import tailwindcss from "@tailwindcss/vite" export default defineConfig({{ root: "{root}", build: {{ manifest: true, rollupOptions: {{ - input: "{entry_point}", - }}, - }}, - plugins: [ - tailwindcss(), - ], + input: "{entry_point}" + }} + }} }}) """ @@ -97,7 +91,8 @@ class ManifestChunk: """ vite_main = """ -import "./src/main.css"; +import "./src/tailwind.css"; // generated by Flask++ integrated tailwindcss-cli +// -> for external builds using the tailwindcss plugin import the raw css file const _ = window.FPP?._ ?? (async msg => msg) @@ -115,9 +110,7 @@ class ManifestChunk: """ vite_tw = """ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss" source("../"); @theme { /* ... */ @@ -224,7 +217,7 @@ def __init__(self, parent: FlaskPP | Module): src = root / "src" src.mkdir(exist_ok=True) main = root / "main.js" - main_css = src / "main.css" + tailwind = src / "tailwind_raw.css" safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", parent.name) conf_name = f"vite.config.{safe_name}.js" (home / conf_name).write_text(vite_conf.format( @@ -234,8 +227,16 @@ def __init__(self, parent: FlaskPP | Module): conf_params = ["--config", conf_name] if not main.exists(): main.write_text(vite_main) - if not main_css.exists(): - main_css.write_text(vite_tw) + if not tailwind.exists(): + tailwind.write_text(vite_tw) + + generate_tailwind_css = lambda: generate_asset( + tailwind, + tailwind.parent / "tailwind.css", + root + ) + + generate_tailwind_css() if enabled("DEBUG_MODE"): self.session = requests.Session() @@ -250,10 +251,20 @@ def __init__(self, parent: FlaskPP | Module): cwd=home, env=_node_env() ) + + self.raw_cached_css = tailwind.read_text() + + def conditional_regenerate(): + if tailwind.read_text() != self.raw_cached_css: + generate_tailwind_css() + self.raw_cached_css = tailwind.read_text() + + self.generate_css = conditional_regenerate + else: if any(root.rglob("*.ts")): result = subprocess.run( - [_node_cmd("npx"), "tsc"], + [_node_cmd("npm"), "run", "ts-test"], cwd=home, env=_node_env(), capture_output=True, @@ -315,6 +326,9 @@ def serve(self, path) -> Response: if not self.server or self.server.poll() is not None: raise ViteError("Frontend server is not running.") + + self.generate_css() + upstream = self.session.get(f"http://localhost:{self.port}/{path}") response = Response(upstream.content, upstream.status_code) response.headers = Headers(upstream.headers) diff --git a/src/flaskpp/modules/creator_templates.py b/src/flaskpp/modules/creator_templates.py index 5fa7a77..c3ebf41 100644 --- a/src/flaskpp/modules/creator_templates.py +++ b/src/flaskpp/modules/creator_templates.py @@ -86,7 +86,7 @@ def init_models(): """ tailwind_raw = """ -@import "tailwindcss"; +@import "tailwindcss" source("../../"); @source not "../../vite"; diff --git a/src/flaskpp/tailwind/__init__.py b/src/flaskpp/tailwind/__init__.py index 66f99ed..c3b59cb 100644 --- a/src/flaskpp/tailwind/__init__.py +++ b/src/flaskpp/tailwind/__init__.py @@ -33,33 +33,40 @@ def _tailwind_cmd() -> str: return str(executable) +def generate_asset(in_file: Path, out_file: Path, cwd: Path): + result = subprocess.run( + [_tailwind_cmd(), + "-i", str(in_file), + "-o", str(out_file), + "--cwd", str(cwd), + "--minify"], + cwd=cwd + ) + if result.returncode != 0: + raise TailwindError(f"Failed to generate {out_file}") + + def generate_tailwind_css(app: Flask): out = (home.parent / "app" / "static" / "css" / "tailwind.css") if not out.exists(): - in_file = out.parent / "tailwind_raw.css" - result = subprocess.run( - f'"{_tailwind_cmd()}" -i "{in_file}" -o "{out}" --minify', - cwd=home.parent, - shell=True + generate_asset( + out.parent / "tailwind_raw.css", + out, + home.parent ) - if result.returncode != 0: - raise TailwindError(f"Failed to generate {out}") root = Path(app.root_path).resolve() - for d in root.rglob("static/css"): in_file = d / "tailwind_raw.css" if not in_file.exists(): continue - result = subprocess.run( - f'"{_tailwind_cmd()}" -i "{in_file}" -o "{out}" --minify', - cwd=d, - shell=True + generate_asset( + in_file, + d / "tailwind.css", + d ) - if result.returncode != 0: - raise TailwindError(f"Failed to generate {d / 'tailwind.css'}") def setup_tailwind(): From e1b7b5d93cc9687fe2db405c4f9b5a81dff11a1b Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 2 Jan 2026 17:42:38 +0000 Subject: [PATCH 06/14] Fixed module loading and added MacOS support for Tailwind & Node --- pyproject.toml | 2 +- src/flaskpp/__init__.py | 28 +++++------------------ src/flaskpp/fpp_node/__init__.py | 28 ++++++++++++++++++----- src/flaskpp/fpp_node/cli.py | 2 +- src/flaskpp/fpp_node/fpp_vite.py | 5 ++++- src/flaskpp/modules/__init__.py | 38 ++++++++++++++++++++++++++++---- src/flaskpp/modules/cli.py | 18 ++++++++++++--- src/flaskpp/tailwind/__init__.py | 13 ++++++----- 8 files changed, 92 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f7f68c8..d853175 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flaskpp" -version = "0.2.12" +version = "0.2.13" description = "A Flask based framework for fast and easy app creation. Experience the real power of Flask without boilerplate, but therefore a well balanced mix of magic and the default Flask framework." authors = [ { name = "Pierre", email = "pierre@growv-mail.org" } diff --git a/src/flaskpp/__init__.py b/src/flaskpp/__init__.py index 1bcaf60..e05904a 100644 --- a/src/flaskpp/__init__.py +++ b/src/flaskpp/__init__.py @@ -13,7 +13,7 @@ from flaskpp.app.config.default import DefaultConfig from flaskpp.app.utils.processing import handlers from flaskpp.app.i18n import init_i18n -from flaskpp.modules import register_modules, ManifestError, ModuleError +from flaskpp.modules import register_modules, version_check, ManifestError, ModuleError from flaskpp.tailwind import generate_tailwind_css from flaskpp.utils import enabled from flaskpp.utils.debugger import start_session, log, exception @@ -198,7 +198,7 @@ def __init__(self, file: str, import_name: str, required_extensions: list = None self.root_path = Path(file).parent manifest = self.root_path / "manifest.json" self.info = self._load_manifest(manifest) - self.safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", self.name) + self.safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", self.name).lower() self.extensions = required_extensions or [] self.context = { "NAME": self.safe_name, @@ -284,27 +284,11 @@ def _load_manifest(self, manifest: Path) -> dict: @property def version(self) -> str: - version_str = self.info.get("version", "").lower().strip() - if not version_str: - raise ManifestError("Module version not defined.") - - if " " in version_str and not (version_str.endswith("alpha") or version_str.endswith("beta")): - raise ManifestError("Invalid version string format.") - - if version_str.startswith("v"): - version_str = version_str[1:] - - try: - v_numbers = version_str.split(" ")[0].split(".") - if len(v_numbers) > 3: - raise ManifestError("Too many version numbers.") - - for v_number in v_numbers: - int(v_number) - except ValueError: - raise ManifestError("Invalid version numbers.") + check = version_check(self.info.get("version", "")) + if not check[0]: + raise ManifestError(check[1]) - return version_str + return check[1] def render_template(self, template: str, **context) -> str: render_name = template if self.home else f"{self.safe_name}/{template}" diff --git a/src/flaskpp/fpp_node/__init__.py b/src/flaskpp/fpp_node/__init__.py index b656898..2eae39a 100644 --- a/src/flaskpp/fpp_node/__init__.py +++ b/src/flaskpp/fpp_node/__init__.py @@ -1,16 +1,26 @@ from pathlib import Path from tqdm import tqdm -import os, platform, requests, typer +import os, platform, requests, typer, subprocess home = Path(__file__).parent node_standalone = { "linux": "https://nodejs.org/dist/v24.11.1/node-v24.11.1-linux-{architecture}.tar.xz", - "win": "https://nodejs.org/dist/v24.11.1/node-v24.11.1-win-{architecture}.zip" + "windows": "https://nodejs.org/dist/v24.11.1/node-v24.11.1-win-{architecture}.zip", + "darwin": "https://nodejs.org/dist/v24.12.0/node-v24.12.0-darwin-{architecture}.tar.gz" } +def _sys_node() -> tuple[bool, str]: + result = subprocess.run( + ["npm", "--version"], + capture_output=True, + text=True + ) + return result.returncode == 0, result.stdout or "" + + def _get_node_data(): - selector = "win" if os.name == "nt" else "linux" + selector = platform.system().lower() machine = platform.machine().lower() arch = "x64" if machine == "x86_64" or machine == "amd64" else "arm64" @@ -21,7 +31,9 @@ def _get_node_data(): def _node_cmd(cmd: str) -> str: node = home / "node" if not node.exists(): - raise NodeError("Missing node directory.") + if not _sys_node()[0]: + raise NodeError("Missing node installation / integration... Try running 'fpp init' inside a project directory.") + return cmd if os.name == "nt": return str(node / f"{cmd}.cmd") @@ -40,8 +52,14 @@ def _node_env() -> dict: def load_node(): + sys_node = _sys_node() + if sys_node[0]: + typer.echo(f"Node.js version {sys_node[1]} is already installed... Skipping integration.") + return + data = _get_node_data() - file_type = "zip" if data[1] == "win" else "tar.xz" + file_type = "zip" if data[1] == "windows" else ( + "tar.xz" if data[1] == "linux" else "tar.gz") dest = home / f"node.{file_type}" bin_folder = home / "node" diff --git a/src/flaskpp/fpp_node/cli.py b/src/flaskpp/fpp_node/cli.py index fb3a938..5cf413e 100644 --- a/src/flaskpp/fpp_node/cli.py +++ b/src/flaskpp/fpp_node/cli.py @@ -18,7 +18,7 @@ def node(ctx: typer.Context): ) if result.returncode != 0: - raise NodeError("Node command failed.") + raise NodeError("Node command execution failed.") def node_entry(app: typer.Typer): diff --git a/src/flaskpp/fpp_node/fpp_vite.py b/src/flaskpp/fpp_node/fpp_vite.py index 2b20346..469ad53 100644 --- a/src/flaskpp/fpp_node/fpp_vite.py +++ b/src/flaskpp/fpp_node/fpp_vite.py @@ -219,7 +219,10 @@ def __init__(self, parent: FlaskPP | Module): main = root / "main.js" tailwind = src / "tailwind_raw.css" safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", parent.name) - conf_name = f"vite.config.{safe_name}.js" + app_name = os.getenv("APP_NAME") + if not app_name: + raise ViteError("Missing APP_NAME environment variable.") + conf_name = f"vite.config.{app_name}.{safe_name}.js" (home / conf_name).write_text(vite_conf.format( root=Path(os.path.relpath(root, start=home)).as_posix(), entry_point=Path(os.path.relpath(main, start=home)).as_posix() diff --git a/src/flaskpp/modules/__init__.py b/src/flaskpp/modules/__init__.py index e1aabb0..40042ce 100644 --- a/src/flaskpp/modules/__init__.py +++ b/src/flaskpp/modules/__init__.py @@ -113,12 +113,12 @@ def register_modules(app): try: home = os.getenv("HOME_MODULE", "").lower() == mod_name.lower() module.enable(app, home) - loader_context[mod_name] = FileSystemLoader(f"modules/{mod_name}/templates") + loader_context[module.safe_name] = FileSystemLoader(f"modules/{mod_name}/templates") if home: - primary_loader = loader_context[mod_name] - log("info", f"Registered module '{mod_name}' as {'home' if home else 'path'}.") + primary_loader = loader_context[module.safe_name] + log("info", f"Registered module '{module.name}' as {'home' if home else 'path'}.") except Exception as e: - exception(e, f"Failed registering module '{mod_name}'.") + exception(e, f"Failed registering module '{module.name}'.") loaders = [] if primary_loader: @@ -132,6 +132,36 @@ def register_modules(app): app.jinja_loader = ChoiceLoader(loaders) +def version_check(version: str) -> tuple[bool, str]: + version_str = version.lower().strip() + if not version_str: + return False, "Module version not defined." + + first_char_invalid = False + try: + if version_str.startswith("v"): + version_str = version_str[1:] + int(version_str[0]) + except ValueError: + first_char_invalid = True + + if first_char_invalid \ + or (" " in version_str and not (version_str.endswith("alpha") or version_str.endswith("beta"))): + return False, "Invalid version string format." + + try: + v_numbers = version_str.split(" ")[0].split(".") + if len(v_numbers) > 3: + return False, "Too many version numbers." + + for v_number in v_numbers: + int(v_number) + except ValueError: + return False, "Invalid version numbers." + + return True, version_str + + class ModuleError(Exception): pass diff --git a/src/flaskpp/modules/cli.py b/src/flaskpp/modules/cli.py index fd99742..d911645 100644 --- a/src/flaskpp/modules/cli.py +++ b/src/flaskpp/modules/cli.py @@ -3,7 +3,7 @@ import typer, shutil, json from flaskpp.utils import prompt_yes_no, sanitize_text -from flaskpp.modules import module_home, creator_templates +from flaskpp.modules import module_home, creator_templates, version_check modules = typer.Typer(help="Manage the modules of Flask++ apps.") @@ -57,11 +57,23 @@ def create( module_dst.mkdir(exist_ok=True) manifest = { - "name": sanitize_text(input("Enter the name of your module: ")), + "name": sanitize_text(input(f"Enter the name of your module ({module}): ")), "description": sanitize_text(input("Describe your module briefly: ")), - "version": sanitize_text(input("Enter the version of your module: ")), + "version": sanitize_text(input("Enter the version of your module [required]: ")), "author": sanitize_text(input("Enter your name or nickname: ")) } + if not manifest["name"].strip(): + manifest["name"] = module + check = version_check(manifest["version"]) + while not check[0]: + typer.echo(typer.style(check[1], fg=typer.colors.RED, bold=True)) + manifest["version"] = sanitize_text(input("Enter a correct version string: ")) + check = version_check(manifest["version"]) + for info in ["description", "author"]: + if not manifest[info].strip(): + typer.echo(typer.style(f"Missing {info}... Ignoring manifest entry.", fg=typer.colors.YELLOW, bold=True)) + manifest.pop(info) + typer.echo(typer.style(f"Writing manifest...", bold=True)) (module_dst / "manifest.json").write_text( json.dumps(manifest, indent=2, ensure_ascii=False) diff --git a/src/flaskpp/tailwind/__init__.py b/src/flaskpp/tailwind/__init__.py index c3b59cb..d593523 100644 --- a/src/flaskpp/tailwind/__init__.py +++ b/src/flaskpp/tailwind/__init__.py @@ -5,19 +5,22 @@ home = Path(__file__).parent.resolve() tailwind_cli = { - "linux": "https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.17/tailwindcss-linux-{architecture}", - "win": "https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.17/tailwindcss-windows-x64.exe" + "linux": "https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.18/tailwindcss-linux-{architecture}", + "windows": "https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.18/tailwindcss-windows-x64.exe", + "darwin": "https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.18/tailwindcss-macos-{architecture}" } def _get_cli_data(): - selector = "win" if os.name == "nt" else "linux" + selector = platform.system().lower() machine = platform.machine().lower() arch = "x64" if machine == "x86_64" or machine == "amd64" else "arm64" - if selector == "linux": + if selector != "windows": return tailwind_cli[selector].format(architecture=arch), selector + elif arch == "arm64": + raise TailwindError("ARM Architecture is not supported on Windows.") return tailwind_cli[selector], selector @@ -71,7 +74,7 @@ def generate_tailwind_css(app: Flask): def setup_tailwind(): data = _get_cli_data() - file_type = ".exe" if data[1] == "win" else "" + file_type = ".exe" if data[1] == "windows" else "" dest = home / f"tailwind{file_type}" if dest.exists(): From ac5990db3a2dac9d2f38898c545349c70bc462e5 Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 2 Jan 2026 19:15:28 +0000 Subject: [PATCH 07/14] Restructured framework and added basic event hooks --- pyproject.toml | 2 +- src/flaskpp/__init__.py | 60 ++++++++++++++++++++++++++++---- src/flaskpp/cli.py | 2 +- src/flaskpp/exceptions.py | 11 ++++++ src/flaskpp/fpp_node/__init__.py | 6 ++-- src/flaskpp/fpp_node/fpp_vite.py | 5 +-- src/flaskpp/modules/__init__.py | 9 +---- src/flaskpp/tailwind/__init__.py | 6 ++-- src/flaskpp/tailwind/cli.py | 3 +- src/flaskpp/utils/__init__.py | 23 ++++++++++-- src/flaskpp/utils/lifespan.py | 28 +++++++++++++++ 11 files changed, 123 insertions(+), 32 deletions(-) create mode 100644 src/flaskpp/exceptions.py create mode 100644 src/flaskpp/utils/lifespan.py diff --git a/pyproject.toml b/pyproject.toml index d853175..4fddd5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flaskpp" -version = "0.2.13" +version = "0.2.14" description = "A Flask based framework for fast and easy app creation. Experience the real power of Flask without boilerplate, but therefore a well balanced mix of magic and the default Flask framework." authors = [ { name = "Pierre", email = "pierre@growv-mail.org" } diff --git a/src/flaskpp/__init__.py b/src/flaskpp/__init__.py index e05904a..2693df2 100644 --- a/src/flaskpp/__init__.py +++ b/src/flaskpp/__init__.py @@ -7,16 +7,19 @@ from socketio import ASGIApp from pathlib import Path from importlib import import_module +from typing import Callable import os, json, re from flaskpp.app.config import CONFIG_MAP from flaskpp.app.config.default import DefaultConfig from flaskpp.app.utils.processing import handlers from flaskpp.app.i18n import init_i18n -from flaskpp.modules import register_modules, version_check, ManifestError, ModuleError +from flaskpp.modules import register_modules, version_check from flaskpp.tailwind import generate_tailwind_css -from flaskpp.utils import enabled +from flaskpp.utils import enabled, takes_arg, arg_count +from flaskpp.utils.lifespan import LifespanWrapper from flaskpp.utils.debugger import start_session, log, exception +from flaskpp.exceptions import ManifestError, ModuleError, EventHookException _fpp_default = Blueprint("fpp_default", __name__, static_folder=(Path(__file__).parent / "app" / "static").resolve(), @@ -174,18 +177,50 @@ def __init__(self, import_name: str, config_name: str): db_updater.start() self._asgi_app = None + self._startup_hooks = [] + self._shutdown_hooks = [] - def to_asgi(self) -> WsgiToAsgi | ASGIApp: + def to_asgi(self) -> LifespanWrapper | ASGIApp: if self._asgi_app is not None: return self._asgi_app - app = WsgiToAsgi(self) + wsgi = WsgiToAsgi(self) + + async def on_startup(): + self._startup() + + async def on_shutdown(): + self._shutdown() + + app = LifespanWrapper(wsgi, on_startup, on_shutdown) + if enabled("EXT_SOCKET"): from flaskpp.app.extensions import socket self._asgi_app = ASGIApp(socket, other_asgi_app=app) - return self._asgi_app - self._asgi_app = app - return app + else: + self._asgi_app = app + + return self._asgi_app + + def on_startup(self, fn: Callable) -> Callable: + if arg_count(fn) > 0: + raise EventHookException("Startup hooks must not receive any arguments.") + self._startup_hooks.append(fn) + return fn + + def on_shutdown(self, fn: Callable) -> Callable: + if arg_count(fn) > 0: + raise EventHookException("Shutdown hooks must not receive any arguments.") + self._shutdown_hooks.append(fn) + return fn + + def _startup(self): + for hook in self._startup_hooks: + hook() + + def _shutdown(self): + for hook in self._shutdown_hooks: + hook() class Module(Blueprint): @@ -207,6 +242,7 @@ def __init__(self, file: str, import_name: str, required_extensions: list = None from flaskpp.app.extensions import require_extensions self.enable = require_extensions(*self.extensions)(self._enable) + self._on_enable = None super().__init__( self.safe_name, @@ -255,6 +291,10 @@ def _enable(self, app: FlaskPP, home: bool): **self.context, tailwind=Markup(f"") )) + + if self._on_enable is not None: + self._on_enable(app) + app.register_blueprint(self) def _load_manifest(self, manifest: Path) -> dict: @@ -293,3 +333,9 @@ def version(self) -> str: def render_template(self, template: str, **context) -> str: render_name = template if self.home else f"{self.safe_name}/{template}" return _render_template(render_name, **context) + + def on_enable(self, fn: Callable) -> Callable: + if not takes_arg(fn, "app") or arg_count(fn) != 1: + raise EventHookException(f"{self.import_name}.on_enable must take exactly one argument: 'app'.") + self._on_enable = fn + return fn diff --git a/src/flaskpp/cli.py b/src/flaskpp/cli.py index 3789033..89b8109 100644 --- a/src/flaskpp/cli.py +++ b/src/flaskpp/cli.py @@ -166,7 +166,7 @@ def create_app(config_name: str = "default"): sys.executable, "-m", babel_cli, "extract", "-F", str(cli_home / "babel.cfg"), "-o", pot, - ".", str(cli_home.resolve()) + os.getcwd(), str(cli_home.resolve()) ]) if has_catalogs: diff --git a/src/flaskpp/exceptions.py b/src/flaskpp/exceptions.py new file mode 100644 index 0000000..9e2b354 --- /dev/null +++ b/src/flaskpp/exceptions.py @@ -0,0 +1,11 @@ + +class ModuleError(Exception): pass +class ManifestError(ModuleError): pass + +class NodeError(Exception): pass + +class ViteError(Exception): pass + +class TailwindError(Exception): pass + +class EventHookException(Exception): pass diff --git a/src/flaskpp/fpp_node/__init__.py b/src/flaskpp/fpp_node/__init__.py index 2eae39a..fceaafb 100644 --- a/src/flaskpp/fpp_node/__init__.py +++ b/src/flaskpp/fpp_node/__init__.py @@ -2,6 +2,8 @@ from tqdm import tqdm import os, platform, requests, typer, subprocess +from flaskpp.exceptions import NodeError + home = Path(__file__).parent node_standalone = { "linux": "https://nodejs.org/dist/v24.11.1/node-v24.11.1-linux-{architecture}.tar.xz", @@ -95,7 +97,3 @@ def load_node(): extracted_folder.rename(bin_folder) dest.unlink() - - -class NodeError(Exception): - pass diff --git a/src/flaskpp/fpp_node/fpp_vite.py b/src/flaskpp/fpp_node/fpp_vite.py index 469ad53..082df26 100644 --- a/src/flaskpp/fpp_node/fpp_vite.py +++ b/src/flaskpp/fpp_node/fpp_vite.py @@ -11,6 +11,7 @@ from flaskpp.tailwind import generate_asset from flaskpp.utils import enabled, is_port_free from flaskpp.utils.debugger import exception +from flaskpp.exceptions import ViteError @dataclass @@ -340,7 +341,3 @@ def serve(self, path) -> Response: @property def built(self) -> bool: return not enabled("DEBUG_MODE") and self.build.returncode == 0 - - -class ViteError(Exception): - pass diff --git a/src/flaskpp/modules/__init__.py b/src/flaskpp/modules/__init__.py index 40042ce..afeebb9 100644 --- a/src/flaskpp/modules/__init__.py +++ b/src/flaskpp/modules/__init__.py @@ -5,6 +5,7 @@ import os, typer from flaskpp.utils.debugger import log, exception +from flaskpp.exceptions import ManifestError home = Path.cwd() module_home = home / "modules" @@ -160,11 +161,3 @@ def version_check(version: str) -> tuple[bool, str]: return False, "Invalid version numbers." return True, version_str - - -class ModuleError(Exception): - pass - - -class ManifestError(ModuleError): - pass diff --git a/src/flaskpp/tailwind/__init__.py b/src/flaskpp/tailwind/__init__.py index d593523..8b55d66 100644 --- a/src/flaskpp/tailwind/__init__.py +++ b/src/flaskpp/tailwind/__init__.py @@ -3,6 +3,8 @@ from tqdm import tqdm import os, platform, typer, requests, subprocess +from flaskpp.exceptions import TailwindError + home = Path(__file__).parent.resolve() tailwind_cli = { "linux": "https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.18/tailwindcss-linux-{architecture}", @@ -98,7 +100,3 @@ def setup_tailwind(): os.system(f"chmod +x {str(dest)}") typer.echo(typer.style(f"Tailwind successfully setup.", fg=typer.colors.GREEN, bold=True)) - - -class TailwindError(Exception): - pass diff --git a/src/flaskpp/tailwind/cli.py b/src/flaskpp/tailwind/cli.py index 0d02d77..54dd969 100644 --- a/src/flaskpp/tailwind/cli.py +++ b/src/flaskpp/tailwind/cli.py @@ -1,6 +1,7 @@ import typer, subprocess, os -from flaskpp.tailwind import _tailwind_cmd, TailwindError +from flaskpp.tailwind import _tailwind_cmd +from flaskpp.exceptions import TailwindError def tailwind(ctx: typer.Context): diff --git a/src/flaskpp/utils/__init__.py b/src/flaskpp/utils/__init__.py index 320f080..0a71ce3 100644 --- a/src/flaskpp/utils/__init__.py +++ b/src/flaskpp/utils/__init__.py @@ -1,5 +1,5 @@ -from pathlib import Path -import os, string, random, socket +from typing import Callable +import os, string, random, socket, inspect def random_code(length: int = 6) -> str: @@ -29,3 +29,22 @@ def is_port_free(port, host="127.0.0.1") -> bool: def sanitize_text(value: str) -> str: return value.encode("utf-8", "ignore").decode("utf-8") + + +def takes_arg(fn: Callable, arg: str) -> bool: + sig = inspect.signature(fn) + return arg in sig.parameters + + +def arg_count(fn: Callable) -> int: + sig = inspect.signature(fn) + + params = [ + p for p in sig.parameters.values() + if p.kind in ( + p.POSITIONAL_ONLY, + p.POSITIONAL_OR_KEYWORD, + p.KEYWORD_ONLY, + ) + ] + return len(params) diff --git a/src/flaskpp/utils/lifespan.py b/src/flaskpp/utils/lifespan.py new file mode 100644 index 0000000..59fda13 --- /dev/null +++ b/src/flaskpp/utils/lifespan.py @@ -0,0 +1,28 @@ +from typing import Callable, Awaitable, Dict + +ASGIApp = Callable[[Dict, Callable, Callable], Awaitable[None]] + + +class LifespanWrapper: + def __init__(self, app: ASGIApp, on_shutdown: Callable, on_startup: Callable): + self.app = app + self.on_shutdown = on_shutdown + self.on_startup = on_startup + + async def __call__(self, scope, receive, send): + if scope["type"] == "lifespan": + while True: + message = await receive() + + if message["type"] == "lifespan.startup": + if self.on_startup: + await self.on_startup() + await send({"type": "lifespan.startup.complete"}) + + elif message["type"] == "lifespan.shutdown": + if self.on_shutdown: + await self.on_shutdown() + await send({"type": "lifespan.shutdown.complete"}) + return + else: + await self.app(scope, receive, send) From 5c99d25727433b638ee1614e9bbddf2e1e7aa8bd Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 6 Jan 2026 18:25:02 +0000 Subject: [PATCH 08/14] Updated socket and i18n ecosystem --- pyproject.toml | 3 +- src/flaskpp/__init__.py | 254 +++++++++------ src/flaskpp/app/config/default.py | 8 +- src/flaskpp/app/data/__init__.py | 2 +- src/flaskpp/app/data/babel.py | 16 +- src/flaskpp/app/data/locales.json | 210 ++++++++++++ src/flaskpp/app/data/noinit_translations.py | 106 +++++++ src/flaskpp/app/extensions.py | 28 +- src/flaskpp/app/socket.py | 13 - src/flaskpp/app/static/css/tailwind_raw.css | 29 +- src/flaskpp/app/static/js/base.js | 32 +- src/flaskpp/app/static/js/socket.js | 13 +- src/flaskpp/app/templates/404.html | 8 +- src/flaskpp/app/templates/base_example.html | 33 +- src/flaskpp/app/templates/error.html | 10 +- .../app/templates/modals/confirm_modal.html | 6 +- .../app/templates/modals/info_modal.html | 4 +- src/flaskpp/app/templates/nav_link.html | 2 +- src/flaskpp/app/utils/processing.py | 69 ++-- src/flaskpp/app/utils/translating.py | 119 +++++-- src/flaskpp/babel.py | 100 ++++++ src/flaskpp/cli.py | 72 +++-- src/flaskpp/exceptions.py | 2 + src/flaskpp/fpp_node/__init__.py | 13 +- src/flaskpp/fpp_node/fpp_vite.py | 6 +- src/flaskpp/{app => }/i18n.py | 43 ++- src/flaskpp/modules/__init__.py | 8 +- src/flaskpp/modules/creator_templates.py | 5 +- src/flaskpp/socket.py | 300 ++++++++++++++++++ src/flaskpp/utils/__init__.py | 51 ++- src/flaskpp/utils/lifespan.py | 28 -- src/flaskpp/utils/run.py | 13 +- src/flaskpp/utils/setup.py | 3 + 33 files changed, 1254 insertions(+), 355 deletions(-) create mode 100644 src/flaskpp/app/data/locales.json create mode 100644 src/flaskpp/app/data/noinit_translations.py delete mode 100644 src/flaskpp/app/socket.py create mode 100644 src/flaskpp/babel.py rename src/flaskpp/{app => }/i18n.py (50%) create mode 100644 src/flaskpp/socket.py delete mode 100644 src/flaskpp/utils/lifespan.py diff --git a/pyproject.toml b/pyproject.toml index 4fddd5b..d94ac01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flaskpp" -version = "0.2.14" +version = "0.2.16" description = "A Flask based framework for fast and easy app creation. Experience the real power of Flask without boilerplate, but therefore a well balanced mix of magic and the default Flask framework." authors = [ { name = "Pierre", email = "pierre@growv-mail.org" } @@ -19,6 +19,7 @@ dependencies = [ "flask-caching", "flask-smorest", "flask-jwt-extended", + "flask-authlib-client", "pymysql", "python-dotenv", diff --git a/src/flaskpp/__init__.py b/src/flaskpp/__init__.py index 2693df2..3323c99 100644 --- a/src/flaskpp/__init__.py +++ b/src/flaskpp/__init__.py @@ -1,23 +1,23 @@ from flask import Flask, Blueprint, render_template as _render_template, url_for, send_from_directory from werkzeug.middleware.proxy_fix import ProxyFix from markupsafe import Markup -from threading import Thread +from threading import Thread, Event from datetime import datetime from asgiref.wsgi import WsgiToAsgi from socketio import ASGIApp from pathlib import Path from importlib import import_module from typing import Callable -import os, json, re +from types import FrameType +import os, json, re, signal from flaskpp.app.config import CONFIG_MAP from flaskpp.app.config.default import DefaultConfig -from flaskpp.app.utils.processing import handlers -from flaskpp.app.i18n import init_i18n +from flaskpp.app.utils.processing import get_handler +from flaskpp.i18n import init_i18n from flaskpp.modules import register_modules, version_check from flaskpp.tailwind import generate_tailwind_css -from flaskpp.utils import enabled, takes_arg, arg_count -from flaskpp.utils.lifespan import LifespanWrapper +from flaskpp.utils import enabled, takes_arg, required_arg_count from flaskpp.utils.debugger import start_session, log, exception from flaskpp.exceptions import ManifestError, ModuleError, EventHookException @@ -60,10 +60,18 @@ def db_autoupdate(app): def set_default_handlers(app): - app.context_processor(handlers["context_processor"]) - app.before_request(handlers["before_request"]) - app.after_request(handlers["after_request"]) - app.errorhandler(Exception)(handlers["handle_app_error"]) + app.context_processor( + lambda: get_handler("context_processor")() + ) + app.before_request( + lambda : get_handler("before_request")() + ) + app.after_request( + lambda response: get_handler("after_request")(response) + ) + app.errorhandler(Exception)( + lambda error: get_handler("handle_app_error")(error) + ) class FlaskPP(Flask): @@ -73,25 +81,27 @@ def __init__(self, import_name: str, config_name: str): static_folder=None, static_url_path=None ) + self.name = re.sub(r"[^a-zA-Z0-9_-]", "_", os.getenv("APP_NAME", self.import_name)).lower() self.config.from_object(CONFIG_MAP.get(config_name, DefaultConfig)) - start_session(enabled("DEBUG_MODE")) + self._startup_hooks = [] + self._shutdown_hooks = [] if self.config["PROXY_FIX"]: count = self.config["PROXY_COUNT"] - self.wsgi_app = ProxyFix(self.wsgi_app, - x_for=count, - x_proto=count, - x_host=count, - x_port=count, - x_prefix=count) - - if self.config["RATELIMIT"]: - from flaskpp.app.extensions import limiter - limiter.init_app(self) - - fpp_processing = enabled("FPP_PROCESSING") - if fpp_processing: + self.wsgi_app = ProxyFix( + self.wsgi_app, + x_for=count, + x_proto=count, + x_host=count, + x_port=count, + x_prefix=count + ) + + from flaskpp.app.extensions import limiter + limiter.init_app(self) + + if enabled("FPP_PROCESSING"): set_default_handlers(self) ext_database = enabled("EXT_SQLALCHEMY") @@ -106,19 +116,20 @@ def __init__(self, import_name: str, config_name: str): if enabled("DB_AUTOUPDATE"): db_updater = Thread(target=db_autoupdate, args=(self,)) - if enabled("EXT_SOCKET") and fpp_processing: + if enabled("EXT_SOCKET"): from flaskpp.app.extensions import socket - socket.on("default_event")(handlers["socket_event_handler"]) + socket.init_app(self) if enabled("EXT_BABEL"): from flaskpp.app.extensions import babel - from flaskpp.app.i18n import DBDomain from flaskpp.app.utils.translating import set_locale - domain = DBDomain() - babel.init_app(self, default_domain=domain) - self.extensions["babel_domain"] = domain + babel.init_app(self) self.route("/lang/")(set_locale) + if enabled("FPP_I18N_FALLBACK") and ext_database: + from flaskpp.app.data.noinit_translations import setup_db + self.on_startup(setup_db) + if enabled("EXT_FST"): if not ext_database: raise RuntimeError("For EXT_FST EXT_SQLALCHEMY extension must be enabled.") @@ -151,25 +162,8 @@ def __init__(self, import_name: str, config_name: str): from flaskpp.app.extensions import jwt jwt.init_app(self) - generate_tailwind_css(self) - - self.register_blueprint(_fpp_default) - self.url_prefix = "" - register_modules(self) - self.static_url_path = f"{self.url_prefix}/static" - self.add_url_rule( - f"{self.static_url_path}/", - endpoint="static", - view_func=lambda filename: send_from_directory(Path(self.root_path) / "static", filename) - ) - - if enabled("FRONTEND_ENGINE"): - from flaskpp.fpp_node.fpp_vite import Frontend - engine = Frontend(self) - self.context_processor(lambda: { - "vite_main": engine.vite - }) - self.frontend_engine = engine + self.url_prefix = None + self.frontend_engine = None init_i18n(self) @@ -177,75 +171,120 @@ def __init__(self, import_name: str, config_name: str): db_updater.start() self._asgi_app = None - self._startup_hooks = [] - self._shutdown_hooks = [] + self._server = Thread(target=self._run_server, daemon=True) + self._shutdown_flag = Event() - def to_asgi(self) -> LifespanWrapper | ASGIApp: + def to_asgi(self) -> WsgiToAsgi | ASGIApp: if self._asgi_app is not None: return self._asgi_app wsgi = WsgiToAsgi(self) - async def on_startup(): - self._startup() - - async def on_shutdown(): - self._shutdown() - - app = LifespanWrapper(wsgi, on_startup, on_shutdown) - if enabled("EXT_SOCKET"): from flaskpp.app.extensions import socket - self._asgi_app = ASGIApp(socket, other_asgi_app=app) + app = ASGIApp(socket, other_asgi_app=wsgi) else: - self._asgi_app = app + app = wsgi + self._asgi_app = app return self._asgi_app def on_startup(self, fn: Callable) -> Callable: - if arg_count(fn) > 0: - raise EventHookException("Startup hooks must not receive any arguments.") + if required_arg_count(fn) > 0: + raise EventHookException("Startup hooks must not receive non optional arguments.") self._startup_hooks.append(fn) return fn def on_shutdown(self, fn: Callable) -> Callable: - if arg_count(fn) > 0: - raise EventHookException("Shutdown hooks must not receive any arguments.") + if required_arg_count(fn) > 0: + raise EventHookException("Shutdown hooks must not receive non optional arguments.") self._shutdown_hooks.append(fn) return fn def _startup(self): - for hook in self._startup_hooks: - hook() + with self.app_context(): + log("info", "Running startup hooks...") + [hook() for hook in self._startup_hooks] def _shutdown(self): - for hook in self._shutdown_hooks: - hook() + with self.app_context(): + log("info", "Running shutdown hooks...") + [hook() for hook in self._shutdown_hooks] + + def _run_server(self): + import uvicorn + uvicorn.run( + self.to_asgi(), + host="0.0.0.0", + port=int(os.getenv("SERVER_PORT", "5000")), + log_level="debug" if enabled("DEBUG_MODE") else "info", + ) + + def _handle_shutdown(self, signum: int, frame: FrameType): + log("info", f"Handling signal {'SIGINT' if signum == signal.SIGINT else 'SIGTERM'}: Shutting down...") + if self._shutdown_flag.is_set(): + return + self._shutdown_flag.set() + + def start(self): + signal.signal(signal.SIGTERM, self._handle_shutdown) + signal.signal(signal.SIGINT, self._handle_shutdown) + + start_session(enabled("DEBUG_MODE")) + + if enabled("AUTOGENERATE_TAILWIND_CSS"): + generate_tailwind_css(self) + + if enabled("FPP_MODULES"): + self.register_blueprint(_fpp_default) + self.url_prefix = "" + register_modules(self) + self.static_url_path = f"{self.url_prefix}/static" + self.add_url_rule( + f"{self.static_url_path}/", + endpoint="static", + view_func=lambda filename: send_from_directory(Path(self.root_path) / "static", filename) + ) + + if enabled("FRONTEND_ENGINE"): + from flaskpp.fpp_node.fpp_vite import Frontend + engine = Frontend(self) + self.context_processor(lambda: { + "vite_main": engine.vite + }) + self.frontend_engine = engine + + self._startup() + self._server.start() + self._shutdown_flag.wait() + self._shutdown() class Module(Blueprint): - def __init__(self, file: str, import_name: str, required_extensions: list = None): + def __init__(self, file: str, import_name: str, required_extensions: list = None, + init_routes_on_enable: bool = True): if not "modules." in import_name: raise ModuleError("Modules have to be created in the modules package.") - self.name = import_name.split(".")[-1] + self.module_name = import_name.split(".")[-1] self.import_name = import_name self.root_path = Path(file).parent manifest = self.root_path / "manifest.json" self.info = self._load_manifest(manifest) - self.safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", self.name).lower() - self.extensions = required_extensions or [] + safe_name = re.sub(r"[^a-zA-Z0-9_-]", "_", self.module_name).lower() + self.required_extensions = required_extensions or [] self.context = { - "NAME": self.safe_name, + "NAME": safe_name, } self.home = False - from flaskpp.app.extensions import require_extensions - self.enable = require_extensions(*self.extensions)(self._enable) + from flaskpp.utils import require_extensions + self.enable = require_extensions(*self.required_extensions)(self._enable) self._on_enable = None + self._init_routes = init_routes_on_enable super().__init__( - self.safe_name, + safe_name, import_name, static_folder=(Path(self.root_path) / "static") ) @@ -259,19 +298,13 @@ def _enable(self, app: FlaskPP, home: bool): app.url_prefix = "/app" self.home = True else: - self.url_prefix = f"/{self.safe_name}" - self.static_url_path = f"/{self.safe_name}/static" + self.url_prefix = f"/{self.name}" + self.static_url_path = f"/{self.name}/static" - try: - routes = import_module(f"{self.import_name}.routes") - init = getattr(routes, "init_routes", None) - if not init: - raise ImportError("Missing init function in routes.") - init(self) - except (ModuleNotFoundError, ImportError, TypeError) as e: - log("warn", f"Failed to register routes for {self.name}: {e}") + if self._init_routes: + self.init_routes() - if "sqlalchemy" in self.extensions: + if "sqlalchemy" in self.required_extensions: try: data = import_module(f"{self.import_name}.data") init = getattr(data, "init_models", None) @@ -279,7 +312,7 @@ def _enable(self, app: FlaskPP, home: bool): raise ImportError("Missing init function in data.") init() except (ModuleNotFoundError, ImportError, TypeError) as e: - log("warn", f"Failed to initialize models for {self.name}: {e}") + log("warn", f"Failed to initialize models for {self.module_name}: {e}") if enabled("FRONTEND_ENGINE"): from flaskpp.fpp_node.fpp_vite import Frontend @@ -289,7 +322,7 @@ def _enable(self, app: FlaskPP, home: bool): self.context_processor(lambda: dict( **self.context, - tailwind=Markup(f"") + tailwind=Markup(f"") )) if self._on_enable is not None: @@ -299,17 +332,17 @@ def _enable(self, app: FlaskPP, home: bool): def _load_manifest(self, manifest: Path) -> dict: if not manifest.exists(): - raise FileNotFoundError(f"Manifest file for {self.name} not found.") + raise FileNotFoundError(f"Manifest file for {self.module_name} not found.") try: module_data = json.loads(manifest.read_text()) except json.decoder.JSONDecodeError: - raise ManifestError(f"Invalid format for manifest of {self.name}.") + raise ManifestError(f"Invalid format for manifest of {self.module_name}.") if not "name" in module_data: - module_data["name"] = self.name + module_data["name"] = self.module_name else: - self.name = module_data["name"] + self.module_name = module_data["name"] if not "description" in module_data: log("warn", f"Missing description of {module_data['name']}.") @@ -330,12 +363,37 @@ def version(self) -> str: return check[1] + def init_routes(self): + try: + routes = import_module(f"{self.import_name}.routes") + init = getattr(routes, "init_routes", None) + if not init: + raise ImportError("Missing init function in routes.") + init(self) + except (ModuleNotFoundError, ImportError, TypeError) as e: + log("warn", f"Failed to register routes for {self.module_name}: {e}") + + def wrap_message(self, message: str) -> str: + domain = self.context.get("DOMAIN") + if not domain: + return message + return f"{message}@{domain}" + + def t(self, message: str) -> str: + from flaskpp.app.utils.translating import t + return t(self.wrap_message(message), False) + + def tn(self, singular: str, plural: str, n: int) -> str: + from flaskpp.app.utils.translating import tn + return tn(self.wrap_message(singular), plural, n, False) + def render_template(self, template: str, **context) -> str: - render_name = template if self.home else f"{self.safe_name}/{template}" + render_name = template if self.home else f"{self.name}/{template}" + return _render_template(render_name, **context) def on_enable(self, fn: Callable) -> Callable: - if not takes_arg(fn, "app") or arg_count(fn) != 1: - raise EventHookException(f"{self.import_name}.on_enable must take exactly one argument: 'app'.") + if not takes_arg(fn, "app") or required_arg_count(fn) != 1: + raise EventHookException(f"{self.import_name}.on_enable must take exactly one non optional argument: 'app'.") self._on_enable = fn return fn diff --git a/src/flaskpp/app/config/default.py b/src/flaskpp/app/config/default.py index 492e79c..8caf346 100644 --- a/src/flaskpp/app/config/default.py +++ b/src/flaskpp/app/config/default.py @@ -26,8 +26,8 @@ class DefaultConfig: # ------------------------------------------------- # Flask-Limiter (Rate Limiting) # ------------------------------------------------- - RATELIMIT = True - RATELIMIT_STORAGE_URL = f"{os.getenv('REDIS_URL', 'redis://localhost:6379')}/1" + RATELIMIT_ENABLED = True + RATELIMIT_STORAGE_URI = f"{os.getenv('REDIS_URL', 'redis://localhost:6379')}/1" RATELIMIT_DEFAULT = "500 per day; 100 per hour" RATELIMIT_STRATEGY = "fixed-window" @@ -40,9 +40,9 @@ class DefaultConfig: # ------------------------------------------------- # Flask-BabelPlus (i18n/l10n) # ------------------------------------------------- - BABEL_DEFAULT_LOCALE = "de" + BABEL_DEFAULT_LOCALE = "en" SUPPORTED_LOCALES = os.getenv("SUPPORTED_LOCALES", BABEL_DEFAULT_LOCALE) - BABEL_DEFAULT_TIMEZONE = "Europe/Berlin" + BABEL_DEFAULT_TIMEZONE = "UTC" BABEL_TRANSLATION_DIRECTORIES = "translations" # ------------------------------------------------- diff --git a/src/flaskpp/app/data/__init__.py b/src/flaskpp/app/data/__init__.py index c1d7502..f8cab60 100644 --- a/src/flaskpp/app/data/__init__.py +++ b/src/flaskpp/app/data/__init__.py @@ -6,7 +6,7 @@ def init_models(): for file in _package.rglob("*.py"): - if file.stem == "__init__": + if file.stem == "__init__" or file.stem.startswith("noinit"): continue import_module(f"flaskpp.app.data.{file.stem}") diff --git a/src/flaskpp/app/data/babel.py b/src/flaskpp/app/data/babel.py index fb3363d..e3cb2f9 100644 --- a/src/flaskpp/app/data/babel.py +++ b/src/flaskpp/app/data/babel.py @@ -19,17 +19,17 @@ def __init__(self, domain, locale, key, text): self.text = text -def add_entry(locale: str, key: str, text: str, domain: str = "messages"): +def add_entry(locale: str, key: str, text: str, domain: str = "messages", auto_commit: bool = True): entry = I18nMessage(domain, locale, key, text) - add_model(entry) + add_model(entry, auto_commit) -def get_entry(key: str, domain: str = "messages"): - return I18nMessage.query.filter_by(key=key, domain=domain).first() +def get_entry(key: str, locale: str, domain: str = "messages"): + return I18nMessage.query.filter_by(key=key, locale=locale, domain=domain).first() -def get_entries(**filters): - return I18nMessage.query.filter_by(**filters).all() +def get_entries(*filters, **filter_by): + return I18nMessage.query.filter(*filters).filter_by(**filter_by).all() def remove_entry(key: str, locale: str, domain: str = "messages"): @@ -38,8 +38,8 @@ def remove_entry(key: str, locale: str, domain: str = "messages"): delete_model(entry) -def remove_entries(key: str): - entries = I18nMessage.query.filter_by(key=key).all() +def remove_entries(key: str, domain: str = "messages"): + entries = I18nMessage.query.filter_by(key=key, domain=domain).all() for entry in entries: delete_model(entry, False) commit() diff --git a/src/flaskpp/app/data/locales.json b/src/flaskpp/app/data/locales.json new file mode 100644 index 0000000..294b09e --- /dev/null +++ b/src/flaskpp/app/data/locales.json @@ -0,0 +1,210 @@ +{ + "flags": { + "de": "🇩🇪", + "en": "🇬🇧", + "fr": "🇫🇷", + "es": "🇪🇸", + "it": "🇮🇹", + "pt": "🇧🇷", + "nl": "🇳🇱", + "sv": "🇸🇪", + "no": "🇳🇴", + "da": "🇩🇰", + "fi": "🇫🇮", + "is": "🇮🇸", + + "pl": "🇵🇱", + "cs": "🇨🇿", + "sk": "🇸🇰", + "sl": "🇸🇮", + "hr": "🇭🇷", + "sr": "🇷🇸", + "bs": "🇧🇦", + "mk": "🇲🇰", + "bg": "🇧🇬", + "ro": "🇷🇴", + "hu": "🇭🇺", + "el": "🇬🇷", + "sq": "🇦🇱", + + "ru": "🇷🇺", + "uk": "🇺🇦", + "be": "🇧🇾", + "lt": "🇱🇹", + "lv": "🇱🇻", + "et": "🇪🇪", + + "tr": "🇹🇷", + "az": "🇦🇿", + "ka": "🇬🇪", + "hy": "🇦🇲", + + "ar": "🇸🇦", + "he": "🇮🇱", + "fa": "🇮🇷", + "ur": "🇵🇰", + + "hi": "🇮🇳", + "bn": "🇧🇩", + "pa": "🇮🇳", + "ta": "🇮🇳", + "te": "🇮🇳", + "ml": "🇮🇳", + "kn": "🇮🇳", + "mr": "🇮🇳", + "gu": "🇮🇳", + + "zh": "🇨🇳", + "ja": "🇯🇵", + "ko": "🇰🇷", + + "th": "🇹🇭", + "vi": "🇻🇳", + "id": "🇮🇩", + "ms": "🇲🇾", + "tl": "🇵🇭", + + "sw": "🇹🇿", + "am": "🇪🇹", + "ha": "🇳🇬", + "yo": "🇳🇬", + "ig": "🇳🇬", + "zu": "🇿🇦", + "xh": "🇿🇦", + "af": "🇿🇦", + + "eo": "🇺🇳", + "la": "🇻🇦", + + "ga": "🇮🇪", + "cy": "🇬🇧", + "gd": "🇬🇧", + + "mt": "🇲🇹", + "lb": "🇱🇺", + "fo": "🇫🇴", + + "ne": "🇳🇵", + "si": "🇱🇰", + "km": "🇰🇭", + "lo": "🇱🇦", + "my": "🇲🇲", + + "mn": "🇲🇳", + "kk": "🇰🇿", + "uz": "🇺🇿", + "tk": "🇹🇲", + "ky": "🇰🇬", + + "ps": "🇦🇫", + "dv": "🇲🇻", + + "sm": "🇼🇸", + "mi": "🇳🇿", + "haw": "🇺🇸" + }, + "names": { + "de": "Deutsch", + "en": "English", + "fr": "Français", + "es": "Español", + "it": "Italiano", + "pt": "Português", + "nl": "Nederlands", + "sv": "Svenska", + "no": "Norsk", + "da": "Dansk", + "fi": "Suomi", + "is": "Íslenska", + + "pl": "Polski", + "cs": "Čeština", + "sk": "Slovenčina", + "sl": "Slovenščina", + "hr": "Hrvatski", + "sr": "Српски", + "bs": "Bosanski", + "mk": "Македонски", + "bg": "Български", + "ro": "Română", + "hu": "Magyar", + "el": "Ελληνικά", + "sq": "Shqip", + + "ru": "Русский", + "uk": "Українська", + "be": "Беларуская", + "lt": "Lietuvių", + "lv": "Latviešu", + "et": "Eesti", + + "tr": "Türkçe", + "az": "Azərbaycan dili", + "ka": "ქართული", + "hy": "Հայերեն", + + "ar": "العربية", + "he": "עברית", + "fa": "فارسی", + "ur": "اردو", + + "hi": "हिन्दी", + "bn": "বাংলা", + "pa": "ਪੰਜਾਬੀ", + "ta": "தமிழ்", + "te": "తెలుగు", + "ml": "മലയാളം", + "kn": "ಕನ್ನಡ", + "mr": "मराठी", + "gu": "ગુજરાતી", + + "zh": "中文", + "ja": "日本語", + "ko": "한국어", + + "th": "ไทย", + "vi": "Tiếng Việt", + "id": "Bahasa Indonesia", + "ms": "Bahasa Melayu", + "tl": "Filipino", + + "sw": "Kiswahili", + "am": "አማርኛ", + "ha": "Hausa", + "yo": "Yorùbá", + "ig": "Asụsụ Igbo", + "zu": "IsiZulu", + "xh": "IsiXhosa", + "af": "Afrikaans", + + "eo": "Esperanto", + "la": "Latina", + + "ga": "Gaeilge", + "cy": "Cymraeg", + "gd": "Gàidhlig", + + "mt": "Malti", + "lb": "Lëtzebuergesch", + "fo": "Føroyskt", + + "ne": "नेपाली", + "si": "සිංහල", + "km": "ភាសាខ្មែរ", + "lo": "ລາວ", + "my": "မြန်မာ", + + "mn": "Монгол", + "kk": "Қазақша", + "uz": "Oʻzbekcha", + "tk": "Türkmençe", + "ky": "Кыргызча", + + "ps": "پښتو", + "dv": "ދިވެހި", + + "sm": "Gagana Sāmoa", + "mi": "Te Reo Māori", + "haw": "ʻŌlelo Hawaiʻi" + } +} \ No newline at end of file diff --git a/src/flaskpp/app/data/noinit_translations.py b/src/flaskpp/app/data/noinit_translations.py new file mode 100644 index 0000000..c7878ab --- /dev/null +++ b/src/flaskpp/app/data/noinit_translations.py @@ -0,0 +1,106 @@ +import json + +from flaskpp.app.data import commit, _package +from flaskpp.app.data.babel import add_entry, get_entries +from flaskpp.babel import valid_state +from flaskpp.utils import enabled +from flaskpp.utils.debugger import log +from flaskpp.exceptions import I18nError + +_msg_keys = [ + "NAV_BRAND", + "NOT_FOUND_TITLE", + "NOT_FOUND_MSG", + "BACK_HOME", + "ERROR", + "ERROR_TITLE", + "ERROR_MSG", + "CONFIRM", + "YES", + "NO", + "HINT", + "UNDERSTOOD", + +] + +_translations_en = { + _msg_keys[0]: "My Flask++ App", + _msg_keys[1]: "Not Found", + _msg_keys[2]: "We are sorry, but the requested page doesn't exist.", + _msg_keys[3]: "Back Home", + _msg_keys[4]: "Error", + _msg_keys[5]: "An error occurred", + _msg_keys[6]: "Something went wrong, please try again later.", + _msg_keys[7]: "Confirm", + _msg_keys[8]: "Yes", + _msg_keys[9]: "No", + _msg_keys[10]: "Hint", + _msg_keys[11]: "Understood", + +} + +_translations_de = { + _msg_keys[0]: "Meine Flask++ App", + _msg_keys[1]: "Nicht Gefunden", + _msg_keys[2]: "Wir konnten die angefragte Seite leider nicht finden.", + _msg_keys[3]: "Zurück zur Startseite", + _msg_keys[4]: "Fehler", + _msg_keys[5]: "Ein Fehler ist aufgetreten", + _msg_keys[6]: "Leider ist etwas schief gelaufen, bitte versuche es später erneut.", + _msg_keys[7]: "Bestätigen", + _msg_keys[8]: "Ja", + _msg_keys[9]: "Nein", + _msg_keys[10]: "Hinweis", + _msg_keys[11]: "Verstanden", + +} + + +def _add_entries(key, domain): + add_entry("en", key, _translations_en[key], domain, False) + add_entry("de", key, _translations_de[key], domain, False) + + +def setup_db(domain: str = "flaskpp"): + if not (enabled("EXT_BABEL") and enabled("EXT_SQLALCHEMY")): + raise I18nError("To setup Flask++ base translations, you must enable EXT_BABEL and EXT_SQLALCHEMY.") + + state = valid_state() + state.fpp_fallback_domain = domain + entries = get_entries(domain=domain, locale="en") + + if entries: + log("info", f"Updating Flask++ base translations...") + + keys = [e.key for e in entries] + for key in _msg_keys: + if key not in keys: + _add_entries(key, domain) + + for entry in entries: + if _translations_en[entry.key] != entry.text: + entry.text = _translations_en[entry.key] + else: + log("info", f"Setting up Flask++ translations...") + + for key in _msg_keys: + _add_entries(key, domain) + + commit() + + +def get_locale_data(locale: str) -> tuple[str, str]: + if len(locale) != 2 and len(locale) != 5 or len(locale) == 5 and "_" not in locale: + raise I18nError(f"Invalid locale code: {locale}") + + if "_" in locale: + locale = locale.split("_")[0] + + try: + locale_data = json.loads((_package / "locales.json").read_text()) + except json.JSONDecodeError: + raise I18nError("Failed to parse locales.json") + + flags = locale_data.get("flags", {}) + names = locale_data.get("names", {}) + return flags.get(locale, "🇬🇧"), names.get(locale, "English") diff --git a/src/flaskpp/app/extensions.py b/src/flaskpp/app/extensions.py index 2d19ae9..8f02104 100644 --- a/src/flaskpp/app/extensions.py +++ b/src/flaskpp/app/extensions.py @@ -2,44 +2,24 @@ from flask_limiter.util import get_remote_address from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate -from socketio import AsyncServer -from flask_babelplus import Babel from flask_security import Security from authlib.integrations.flask_client import OAuth from flask_mailman import Mail from flask_caching import Cache from flask_smorest import Api from flask_jwt_extended import JWTManager -from functools import wraps -from flaskpp.utils import enabled -from flaskpp.utils.debugger import log +from flaskpp.socket import FppSocket +from flaskpp.babel import FppBabel limiter = Limiter(get_remote_address) db = SQLAlchemy() migrate = Migrate() -socket = AsyncServer(async_mode="asgi", cors_allowed_origins="*") -babel = Babel() +socket = FppSocket(async_mode="asgi") +babel = FppBabel() security = Security() oauth = OAuth() mailer = Mail() cache = Cache() api = Api() jwt = JWTManager() - - -def require_extensions(*extensions): - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - for ext in extensions: - if not isinstance(ext, str): - log("warn", f"Invalid extension '{ext}'.") - continue - - if not enabled(f"EXT_{ext.upper()}"): - raise RuntimeError(f"Extension '{ext}' is not enabled.") - return func(*args, **kwargs) - - return wrapper - return decorator diff --git a/src/flaskpp/app/socket.py b/src/flaskpp/app/socket.py deleted file mode 100644 index 3909cf0..0000000 --- a/src/flaskpp/app/socket.py +++ /dev/null @@ -1,13 +0,0 @@ - -default_handlers = {} - - -def default_event(name: str): - def decorator(func): - default_handlers[name] = func - return func - return decorator - - -def no_handler(_): - raise NotImplementedError("Socket event handler not found.") diff --git a/src/flaskpp/app/static/css/tailwind_raw.css b/src/flaskpp/app/static/css/tailwind_raw.css index 62236cf..3bf2735 100644 --- a/src/flaskpp/app/static/css/tailwind_raw.css +++ b/src/flaskpp/app/static/css/tailwind_raw.css @@ -6,22 +6,17 @@ @layer base { body { - @apply bg-slate-400 text-black min-h-screen w-screen; + @apply bg-slate-400 text-black min-h-screen; } header { - @apply sticky top-0 z-50; + @apply sticky top-0 z-[100]; } nav { @apply w-full bg-slate-800 text-white shadow-md; } - main { - @apply flex flex-col items-center justify-center; - min-height: 100dvh; - } - footer { @apply w-full mt-auto bg-slate-800 text-white text-sm shadow-md; } @@ -45,7 +40,7 @@ } .nav-collapse { - @apply absolute top-full left-0 w-full flex flex-col gap-1 bg-slate-800 px-4 py-3 shadow-md md:static md:flex md:w-auto md:flex-row md:bg-transparent md:p-0 md:shadow-none; + @apply absolute top-full left-0 w-full flex flex-col items-center gap-1 bg-slate-800 px-4 py-3 shadow-md md:static md:flex md:w-auto md:flex-row md:bg-transparent md:p-0 md:shadow-none; } .nav-link { @@ -60,8 +55,24 @@ @apply max-md:hover:bg-white/30 md:hover:font-semibold; } + .nav-lang-summary { + @apply flex items-center gap-2 cursor-pointer p-1 border-2 border-slate-500 text-sm font-semibold text-slate-600 rounded-lg bg-slate-200 hover:bg-slate-300 list-none select-none; + } + + .nav-lang-summary svg { + @apply h-4 w-4 text-slate-400 transition-transform duration-200 group-open:rotate-90; + } + + .nav-lang-dropdown { + @apply absolute right-0 mt-2 w-44 rounded-lg border border-slate-700 bg-slate-900 shadow-lg overflow-hidden z-50; + } + + .nav-lang-link { + @apply block px-4 py-2 text-sm text-slate-200 hover:bg-slate-800; + } + .flash-container { - @apply fixed top-20 right-4 z-50 w-full max-w-sm space-y-3; + @apply fixed top-20 right-4 w-full max-w-sm space-y-3; } .flash { diff --git a/src/flaskpp/app/static/js/base.js b/src/flaskpp/app/static/js/base.js index 4ccbd6a..8d4bf00 100644 --- a/src/flaskpp/app/static/js/base.js +++ b/src/flaskpp/app/static/js/base.js @@ -1,4 +1,4 @@ -import { socket, emit } from "/fpp-static/js/socket.js"; +import { socket, emit, emitAsync, namespace } from "/fpp-static/js/socket.js"; function getFocusable(elem) { @@ -9,7 +9,7 @@ function getFocusable(elem) { ); } -function showModal(elem) { +export function showModal(elem) { elem._trigger = document.activeElement; elem.classList.remove("hidden"); @@ -20,7 +20,7 @@ function showModal(elem) { focusable?.focus(); } -function hideModal(elem) { +export function hideModal(elem) { elem.classList.add("hidden"); elem.classList.remove("flex"); elem.setAttribute("inert", ""); @@ -76,7 +76,7 @@ export async function confirmDialog(title, message, html, category) { if (message) { confirmBody.classList.add('hidden'); confirmText.classList.remove('hidden'); - confirmText.textContent = message.replace(/\n/g, "
"); + confirmText.innerHTML = message.replace(/\n/g, "
"); } else { confirmText.classList.add('hidden'); confirmBody.classList.remove('hidden'); @@ -112,7 +112,7 @@ export function showInfo(title, message, html) { if (message) { infoBody.classList.add('hidden'); infoText.classList.remove('hidden'); - infoText.textContent = message.replace(/\n/g, "
"); + infoText.innerHTML = message.replace(/\n/g, "
"); } else { infoText.classList.add('hidden'); infoBody.classList.remove('hidden'); @@ -154,7 +154,10 @@ export function safe_(fn, rethrow=false) { } +const domain = document.querySelector('meta[name="i18n:domain"]')?.content; + export async function _(key) { + if (domain) key = `${key}@${domain}`; return new Promise((resolve) => { emit("_", key, (response) => { resolve(response); @@ -163,6 +166,7 @@ export async function _(key) { } export async function _n(singular, plural, count) { + if (domain) singular = `${singular}@${domain}`; return new Promise((resolve) => { emit("_n", { s: singular, @@ -175,7 +179,9 @@ export async function _n(singular, plural, count) { } -export function socketHtmlInject(key, dom_block) { +export async function socketHtmlInject(key, dom_block) { + if (namespace) key = `${key}@${namespace}`; + function handleHtml(html) { dom_block.innerHTML = html; @@ -190,7 +196,8 @@ export function socketHtmlInject(key, dom_block) { oldScript.remove(); }); } - emit("html", key, safe_(html => handleHtml(html))); + const html = await emitAsync("html", key); + safe_(handleHtml)(html); } @@ -209,15 +216,24 @@ socket.on('error', async (message) => { window.FPP = { + showModal: showModal, + hideModal: hideModal, + confirmDialog: confirmDialog, showInfo: showInfo, + flash: flash, + safe_: safe_, + _: _, _n: _n, + socketHtmlInject: socketHtmlInject, + socket: socket, - emit: emit + emit: emit, + emitAsync: emitAsync, } diff --git a/src/flaskpp/app/static/js/socket.js b/src/flaskpp/app/static/js/socket.js index 5c15971..14f073b 100644 --- a/src/flaskpp/app/static/js/socket.js +++ b/src/flaskpp/app/static/js/socket.js @@ -8,15 +8,26 @@ export function connectSocket() { reconnectionAttempts: 5, reconnectionDelay: 1000, reconnectionDelayMax: 5000, - timeout: 20000 + timeout: 20000, }) } + export let socket = connectSocket(); +export const namespace = document.querySelector('meta[name="sio:namespace"]')?.content; + export function emit(event, data=null, callback=null) { + if (namespace) event = `${event}@${namespace}`; + socket.emit('default_event', { event: event, payload: data }, callback); +} + +export function emitAsync(event, payload) { + return new Promise(resolve => { + emit(event, payload, resolve); + }); } \ No newline at end of file diff --git a/src/flaskpp/app/templates/404.html b/src/flaskpp/app/templates/404.html index 60d0697..d106d90 100644 --- a/src/flaskpp/app/templates/404.html +++ b/src/flaskpp/app/templates/404.html @@ -1,18 +1,18 @@ {% extends "base_example.html" %} -{% block title %}{{ _('Not found') }}{% endblock %} +{% block title %}{{ _('NOT_FOUND_TITLE') }}{% endblock %} {% block content %} -
+

404

- {{ _("We are sorry, but the requested page doesn't exist.") }} + {{ _("NOT_FOUND_MSG") }}

- {{ _("Back Home") }} + {{ _("BACK_HOME") }}
diff --git a/src/flaskpp/app/templates/base_example.html b/src/flaskpp/app/templates/base_example.html index 71bfd2e..006247a 100644 --- a/src/flaskpp/app/templates/base_example.html +++ b/src/flaskpp/app/templates/base_example.html @@ -19,10 +19,15 @@ src="{{ url_for('fpp_default.static', filename='js/socket.js') }}" type="module" data-socket-domain="{{ request.scheme.replace('http', 'ws') }}://{{ request.host }}"> + {% endif %} + {% if enabled("EXT_BABEL") and DOMAIN %} + + {% endif %} + {% block head %}{% endblock %} @@ -30,7 +35,7 @@
@@ -56,7 +83,7 @@
@@ -65,6 +92,8 @@ {% include "modals/info_modal.html" %} {% endif %} + {% block modals %}{% endblock %} +