diff --git a/app.py b/app.py index 91c9aa2..5761030 100644 --- a/app.py +++ b/app.py @@ -9,22 +9,43 @@ from flask import render_template from minify_html import minify from werkzeug.exceptions import HTTPException -from website.content import reload_content_items, get_articles, get_projects, get_tools, sanitize_input_tags +from website.content import get_articles, get_projects, get_tools, sanitize_input_tags, load_content_items, get_content, \ + get_applets from website.contributors import reload_contributors_data, get_contributors_data from website.domains import ALLOWED_DOMAINS from website.l10n.utils import get_user_lang, localize, reload_strings, l10n_url_abs, l10n_url_switch, L10N, \ DEFAULT_LANG +from website.renderers.applet import render_applet_scripts, render_applet_head from website.renderers.button import render_button -from website.renderers.headings import render_heading, render_h2, render_h1, render_h3 +from website.renderers.code import render_code_block +from website.renderers.headings import render_heading, render_h2, render_h1, render_h3, render_h4 from website.renderers.paragraph import render_paragraph +from website.renderers.lists import render_list_ul from website.renderers.splide import render_splide from website.sidebar import reload_sidebar_entries, get_sidebar_entries from website.sitemap import reload_sitemap_entries, get_sitemap_entries -# try: -# from rich import print -# except ImportError: -# pass +try: + from rich import print +except ImportError: + pass + + +if os.environ.get('NP_HTML_POST_PROCESS', "NONE") == "MINIFY": + print("Using 'minify' as HTML post-processor") + + def post_process_html(html: str) -> str: + return minify(html).replace("> <", "><") +elif os.environ.get('NP_HTML_POST_PROCESS', "NONE") == "BS4": + print("Using 'BeautifulSoup4' as HTML post-processor") + + def post_process_html(html: str) -> str: + return BeautifulSoup(html, features="html.parser").prettify() +else: + print("Using no HTML post-processor") + + def post_process_html(html: str) -> str: + return html app = Flask( @@ -72,24 +93,35 @@ def inject_processors(): domain_host=request.headers['Host'], domain_tld=request.headers['Host'].split('.')[-1] if request.headers['Host'] in ALLOWED_DOMAINS else "lu", domain_url_root=request.url_root, + # L10N l10n=localize, l10n_url_abs=l10n_url_abs, l10n_url_switch=l10n_url_switch, + # Sidebar get_sidebar_entries=get_sidebar_entries, + # Content + get_content=get_content, get_articles=get_articles, get_projects=get_projects, get_tools=get_tools, + # Renderers render_button=render_button, render_heading=render_heading, render_h1=render_h1, render_h2=render_h2, render_h3=render_h3, + render_h4=render_h4, render_paragraph=render_paragraph, + render_list_ul=render_list_ul, render_splide=render_splide, + render_applet_scripts=render_applet_scripts, + render_applet_head=render_applet_head, + render_code_block=render_code_block, + # Commons url_for=url_for, escape=escape, @@ -118,7 +150,7 @@ def route_sitemap(): @app.route('/fr/', defaults={'lang': "fr"}) def route_root(lang: Optional[str]): user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE")) - return minify(render_template( + return post_process_html(render_template( "pages/root.jinja", user_lang=user_lang, raw_lang=lang, @@ -132,7 +164,7 @@ def route_root(lang: Optional[str]): @app.route('/fr/contact/', defaults={'lang': "fr"}) def route_contact(lang: Optional[str]): user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE")) - return minify(render_template( + return post_process_html(render_template( "pages/contact.jinja", user_lang=user_lang, raw_lang=lang, @@ -152,7 +184,7 @@ def route_content(lang: Optional[str]): except ValueError: requested_tags = None - return minify(render_template( + return post_process_html(render_template( "pages/project_index.jinja", user_lang=user_lang, raw_lang=lang, @@ -178,7 +210,7 @@ def route_content_project(lang: Optional[str], project_id: str): error_code = 404 if error_key is not None: - return minify(render_template( + return post_process_html(render_template( "pages/error.jinja", user_lang=user_lang, raw_lang=lang, @@ -188,7 +220,7 @@ def route_content_project(lang: Optional[str], project_id: str): error_code=error_code, )).replace("> <", "><"), error_code else: - return minify(render_template( + return post_process_html(render_template( "projects/" + project_id + ".jinja", user_lang=user_lang, raw_lang=lang, @@ -210,7 +242,7 @@ def route_tools_index(lang: Optional[str]): except ValueError: requested_tags = None - return minify(render_template( + return post_process_html(render_template( "pages/tools_index.jinja", user_lang=user_lang, raw_lang=lang, @@ -236,7 +268,7 @@ def route_tools_page(lang: Optional[str], tool_id: str): error_code = 404 if error_key is not None: - return minify(render_template( + return post_process_html(render_template( "pages/error.jinja", user_lang=user_lang, raw_lang=lang, @@ -246,14 +278,15 @@ def route_tools_page(lang: Optional[str], tool_id: str): error_code=error_code, )).replace("> <", "><"), error_code else: - return minify(render_template( - "tools/" + tool_id + ".jinja", + return post_process_html(render_template( + "pages/tools_page.jinja", user_lang=user_lang, raw_lang=lang, request_path=request.path, standalone="standalone" in request.args, tool_data=get_tools().get(tool_id), tool_id=tool_id, + applet_data=get_applets().get(get_tools().get(tool_id).applet_id), )).replace("> <", "><") @@ -262,7 +295,7 @@ def route_tools_page(lang: Optional[str], tool_id: str): @app.route('/fr/about/', defaults={'lang': "fr"}) def route_about(lang: Optional[str]): user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE")) - return minify(render_template( + return post_process_html(render_template( "pages/about.jinja", user_lang=user_lang, raw_lang=lang, @@ -276,7 +309,7 @@ def route_about(lang: Optional[str]): @app.route('/fr/privacy/', defaults={'lang': "fr"}) def route_privacy(lang: Optional[str]): user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE")) - return minify(render_template( + return post_process_html(render_template( "pages/privacy.jinja", user_lang=user_lang, raw_lang=lang, @@ -290,7 +323,7 @@ def route_privacy(lang: Optional[str]): @app.route('/fr/links/', defaults={'lang': "fr"}) def route_links(lang: Optional[str]): user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE")) - return minify(render_template( + return post_process_html(render_template( "pages/links.jinja", user_lang=user_lang, raw_lang=lang, @@ -304,7 +337,7 @@ def route_links(lang: Optional[str]): @app.route('/fr/debug/', defaults={'lang': "fr"}) def route_debug(lang: Optional[str]): user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE")) - return minify(render_template( + return post_process_html(render_template( "pages/debug.jinja", user_lang=user_lang, raw_lang=lang, @@ -323,7 +356,7 @@ def handle_exception(e: Exception): if isinstance(e, HTTPException): error_code = e.code - return minify(render_template( + return post_process_html(render_template( "pages/error.jinja", user_lang=DEFAULT_LANG, raw_lang=DEFAULT_LANG, @@ -335,7 +368,7 @@ def handle_exception(e: Exception): if __name__ == '__main__': - reload_content_items() + load_content_items() reload_strings(os.path.join(os.getcwd(), "data/strings/")) reload_sidebar_entries(os.path.join(os.getcwd(), "data/sidebar.yml")) reload_contributors_data(os.path.join(os.getcwd(), "data/contributors.yml")) @@ -362,22 +395,3 @@ if __name__ == '__main__': #debug=False, load_dotenv=False ) - -# return BeautifulSoup(render_template( -# "pages/root.jinja", -# lang=user_lang, -# raw_lang=lang, -# request_path=request.path, -# standalone="standalone" in request.args, -# ), features="html.parser").prettify() - -# try: -# from minify_html import minify -# FORCE_NON_DEBUG = False -# except ImportError: -# from bs4 import BeautifulSoup -# FORCE_NON_DEBUG = True -# -# def minify(html): -# return BeautifulSoup(html, features="html.parser").prettify() -# debug=False if FORCE_NON_DEBUG else True, diff --git a/data/applets/uuid-generator.yml b/data/applets/uuid-generator.yml new file mode 100644 index 0000000..49d0333 --- /dev/null +++ b/data/applets/uuid-generator.yml @@ -0,0 +1,8 @@ + +applets: + - id: "uuid-generator" + resources: + scripts: + - "uuid-generator.mjs" + stylesheets: + - "uuid-generator.css" diff --git a/data/projects/.circuitpython-custom-fs.yml b/data/projects/.circuitpython-custom-fs.yml new file mode 100644 index 0000000..449fc8b --- /dev/null +++ b/data/projects/.circuitpython-custom-fs.yml @@ -0,0 +1,35 @@ + +projects: + - id: "circuitpython-custom-fs" + metadata: + head: + title_key: "meta.title" + description_key: "meta.description" + opengraph: + title_key: "meta.title" + description_key: "meta.description" + type: null + url: null + image_url: "/resources/NibblePoker/images/content/circuitpython-ebyte-e32/main.png" + image_type: null + twitter: + title_key: "meta.title" + description_key: "meta.description" + index: + priority: 105 + enable: true + title_key: "meta.title" + preamble_key: "meta.description" + image_url: "/resources/NibblePoker/images/content/circuitpython-ebyte-e32/main.png" + image_alt_key: "" + general: + icon: "fab fa-python" + title_key: "meta.title" + subtitle_key: "article.subtitle" + tags: + - "experiments" + - "electronic" + - "python" + - "circuitpython" + languages: + - "python" diff --git a/data/projects/circuitpython-ebyte-e32.yml b/data/projects/.circuitpython-ebyte-e32.yml similarity index 100% rename from data/projects/circuitpython-ebyte-e32.yml rename to data/projects/.circuitpython-ebyte-e32.yml diff --git a/data/projects/circuitpython-custom-fs.yml b/data/projects/circuitpython-custom-fs.yml deleted file mode 100644 index eb5ff87..0000000 --- a/data/projects/circuitpython-custom-fs.yml +++ /dev/null @@ -1,31 +0,0 @@ -head: - title_key: "meta.title" - description_key: "meta.description" -opengraph: - title_key: "meta.title" - description_key: "meta.description" - type: null - url: null - image_url: "/resources/NibblePoker/images/content/circuitpython-ebyte-e32/main.png" - image_type: null -twitter: - title_key: "meta.title" - description_key: "meta.description" -index: - priority: 105 - enable: true - title_key: "meta.title" - preamble_key: "meta.description" - image_url: "/resources/NibblePoker/images/content/circuitpython-ebyte-e32/main.png" - image_alt_key: "" -general: - icon: "fab fa-python" - title_key: "meta.title" - subtitle_key: "article.subtitle" - tags: - - "experiments" - - "electronic" - - "python" - - "circuitpython" - languages: - - "python" diff --git a/data/projects/docker-mini-cctv-nvr.yml b/data/projects/docker-mini-cctv-nvr.yml new file mode 100644 index 0000000..91f271c --- /dev/null +++ b/data/projects/docker-mini-cctv-nvr.yml @@ -0,0 +1,34 @@ + +projects: + - id: "docker-mini-cctv-nvr" + metadata: + head: + title_key: "meta.title" + description_key: "meta.description" + opengraph: + title_key: "meta.title" + description_key: "meta.description" + type: null + url: null + image_url: "/resources/NibblePoker/images/content/docker-mini-cctv-nvr/main.png" + image_type: null + twitter: + title_key: "meta.title" + description_key: "meta.description" + index: + priority: 105 + enable: true + title_key: "meta.title" + preamble_key: "meta.description" + image_url: "/resources/NibblePoker/images/content/docker-mini-cctv-nvr/main.png" + image_alt_key: "" + general: + icon: "fab fa-docker" + title_key: "meta.title" + subtitle_key: "article.subtitle" + tags: + - "docker" + languages: + - "docker" + - "php" + - "python" diff --git a/data/sidebar.yml b/data/sidebar.yml index 03318ba..b1bca51 100644 --- a/data/sidebar.yml +++ b/data/sidebar.yml @@ -42,13 +42,13 @@ abs_href: "/tools" icon: fad fa-toolbox active_id: tools - has_new_until_utc: 0 + has_new_until_utc: 1760986472 - title_key: text.downloads raw_href: "https://files.nibblepoker.lu/" icon: fad fa-download active_id: "" - has_new_until_utc: 1760986472 + has_new_until_utc: 0 - diff --git a/data/strings/en/docker-mini-cctv-nvr.yml b/data/strings/en/docker-mini-cctv-nvr.yml new file mode 100644 index 0000000..beb53bc --- /dev/null +++ b/data/strings/en/docker-mini-cctv-nvr.yml @@ -0,0 +1,72 @@ +# EN - Docker Mini CCTV NVR + +meta.title: "Mini Dockerized CCTV NVR" +meta.description: "Mini docker stack that allows you to easily record, clean and serve CCTV recordings made +over RSTP while using a minimal amount of system resources." + +intro.title: "Introduction" +intro.1: "A mini docker stack that allows you to easily record, clean and serve CCTV recordings made +over RSTP while using a minimal amount of system resources." +intro.2: "This stack is mainly intended to be used as a backup when other and more complete solutions crash or +need to be shutdown.This simple docker stack aims to provide you with a simple, +lightweight and robust NVR for all of your RTSP-based CCTV cameras." + +preamble.title: "Preamble" +preamble.1: "This stack records the camera's streams as-is and doesn't re-encode or compress it which uses more disk space. +See \"Usage statistics example\" for an example." +preamble.2: "If served out of your LAN, the web server should be behind a secure reverse-proxy that requires authentication." + +setup.title: "Setup" +setup.1: "All of the setup is done through environment variables in the docker-compose.yml file." +setup.2: "It should only take 2-3 minutes if you already have the RTSP URL on hand.
+If you don't have them, you should see your camera's user manual and test the URLs with VLC." + +setup.camera.title: "Cameras" +setup.camera.1: "Each recording container needs to be given a RSTP stream URL and a unique folder +into which the recordings will go." +setup.camera.2: "The URL must be given via the NP_CCTV_URL environment variable, +and the output folder via a mounted volume that is mounted as /data in the container." +setup.camera.3: "This example will use the rtsp://user:password@address:554/sub-path +URL and will put its recordings in ./recordings/cam1." + +setup.cleaner.title: "Cleaner" +setup.cleaner.1: "The cleaner script named cleaner.py only requires you to set 1 environment variable named +NP_MAX_FILE_AGE_HOURS to the max amount of hours any recording should be kept." +setup.cleaner.2: "If not set, the script will simply clean any recordings older than 72 hours." + +setup.web.title: "Web interface" +setup.web.1: "The web interface provides more customization options, but at its core, +it only requires the camera's environment variables to be set." +setup.web.2: "Each camera requires one of the following environment variable:
+  NP_CAM_<camId> = <Camera's name>" +setup.web.3: "Here is an example for cam1 if named as Camera #1:
+  NP_CAM_cam1 = Camera #1" +setup.web.vars.title: "Other variables" +setup.web.vars.description.title: "Page's title" +setup.web.vars.description.footer: "Page's footer HTML content" + +startup.title: "Startup" +startup.1: "Once you have finished setting up the stack, you can simply run the following command:" +#docker-compose up --build -d + +screenshots.title: "Screenshots" + +statistics.title: "Usage statistics example" +statistics.1: "NanoPi R4S 4GB" +statistics.1.1: "Uses 0.008 kWh / 8 Watts with other containers and USB HDD & USB SSD" +statistics.2: "4 IP Cameras" +statistics.2.1: "All H.256 4k RTSP TCP streams" +statistics.2.2: "Around 220 GB of data per day" +statistics.2.2.1: "Around 20.4 Mbit/s or 2.6 MB/s" +statistics.3: "Less than 200MB of RAM usage" +statistics.3.1: "~32 MB per recorder" +statistics.3.2: "4 MB for cleaner" +statistics.3.3: "4 MB for web server" +statistics.4: "Uses ~10% of CPU on average over 6 cores" +statistics.4.1: "Average of 15% per recorder" +statistics.4.2: "Average of 1-5% on cleaner and web server" + +license.title: "License" +license.1: "This software, as well as the Simplette CSS Stylesheet used for the web interface are both licensed under Unlicense." + +commons.example: "Example" diff --git a/data/strings/en/uuid-generator.yml b/data/strings/en/uuid-generator.yml index 2d199aa..a8e9688 100644 --- a/data/strings/en/uuid-generator.yml +++ b/data/strings/en/uuid-generator.yml @@ -1,4 +1,17 @@ # EN - UUID Generator -option.count: "UUID/GUID count" +meta.title: "UUID Generator" + +type.label: "UUID Type" + +type.uuid4: "UUID4 / GUID4" + +option.count: "UUID Count" option.hyphen: "Add hyphens" +option.guid_brackets: "Add GUID brackets" + +generate: "Generate" + +download.raw: "Raw" +download.json: "JSON" +download.yaml: "YAML" diff --git a/data/strings/fr/docker-mini-cctv-nvr.yml b/data/strings/fr/docker-mini-cctv-nvr.yml new file mode 100644 index 0000000..860f676 --- /dev/null +++ b/data/strings/fr/docker-mini-cctv-nvr.yml @@ -0,0 +1,5 @@ +# FR - Docker Mini CCTV NVR + +meta.title: "Mini Dockerized CCTV NVR" +meta.description: "Mini docker stack that allows you to easily record, clean and serve CCTV recordings made +over RSTP while using a minimal amount of system resources." diff --git a/data/strings/fr/uuid-generator.yml b/data/strings/fr/uuid-generator.yml index dcf7ed5..4764856 100644 --- a/data/strings/fr/uuid-generator.yml +++ b/data/strings/fr/uuid-generator.yml @@ -1,4 +1,17 @@ # FR - UUID Generator +meta.title: "Générateur d'UUID" + +type.label: "Type d'UUID" + +type.uuid4: "UUID4 / GUID4" + option.count: "Nombre d'UUID/GUID" option.hyphen: "Ajouter trait d'union" +option.guid_brackets: "Ajouter accolades pour GUID" + +generate: "Générer" + +download.raw: "Brut" +download.json: "JSON" +download.yaml: "YAML" diff --git a/data/tools/excel-password-remover.yml b/data/tools/.excel-password-remover.yml similarity index 98% rename from data/tools/excel-password-remover.yml rename to data/tools/.excel-password-remover.yml index 892cc5a..9f4d2e5 100644 --- a/data/tools/excel-password-remover.yml +++ b/data/tools/.excel-password-remover.yml @@ -25,6 +25,6 @@ metadata: subtitle_key: "article.subtitle" tags: - "undefined" -data: +resources: scripts: - "epr_main.js" diff --git a/data/tools/svg-to-png.yml b/data/tools/.svg-to-png.yml similarity index 98% rename from data/tools/svg-to-png.yml rename to data/tools/.svg-to-png.yml index db09100..653ad52 100644 --- a/data/tools/svg-to-png.yml +++ b/data/tools/.svg-to-png.yml @@ -25,7 +25,7 @@ metadata: subtitle_key: "article.subtitle" tags: - "undefined" -data: +resources: scripts: - "svg-to-png.mjs" stylesheets: diff --git a/data/tools/uuid-generator.yml b/data/tools/uuid-generator.yml index 4f39007..b1fa463 100644 --- a/data/tools/uuid-generator.yml +++ b/data/tools/uuid-generator.yml @@ -1,32 +1,31 @@ -metadata: - head: - title_key: "meta.title" - description_key: "meta.description" - opengraph: - title_key: "meta.title" - description_key: "meta.description" - type: null - url: null - image_url: "/resources/NibblePoker/images/tools/excel-password-remover/excel-password-remover.png" - image_type: null - twitter: - title_key: "meta.title" - description_key: "meta.description" - index: - priority: 100 - enable: true - title_key: "meta.title" - preamble_key: "meta.description" - image_url: "/resources/NibblePoker/images/tools/excel-password-remover/excel-password-remover.png" - image_alt_key: "" - general: - icon: "fab fa-python" - title_key: "meta.title" - subtitle_key: "article.subtitle" - tags: - - "undefined" -data: - scripts: - - "uuid-generator.mjs" - stylesheets: - - "uuid-generator.css" + +tools: + - id: "uuid-generator" + applet_id: "uuid-generator" + metadata: + head: + title_key: "meta.title" + description_key: "meta.description" + opengraph: + title_key: "meta.title" + description_key: "meta.description" + type: null + url: null + image_url: "/resources/NibblePoker/images/tools/excel-password-remover/excel-password-remover.png" + image_type: null + twitter: + title_key: "meta.title" + description_key: "meta.description" + index: + priority: 100 + enable: true + title_key: "meta.title" + preamble_key: "meta.description" + image_url: "/resources/NibblePoker/images/tools/excel-password-remover/excel-password-remover.png" + image_alt_key: "" + general: + icon: "fab fa-python" + title_key: "meta.title" + subtitle_key: "article.subtitle" + tags: + - "undefined" diff --git a/requirements.txt b/requirements.txt index fe9dfb9..503a550 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,4 @@ locked-dict Werkzeug~=3.0.4 #gunicorn -waitress~=3.0.0 +waitress~=3.0.2 diff --git a/scripts/compile-js-site.cmd b/scripts/compile-js-site.cmd index 1b0cda2..89446a6 100644 --- a/scripts/compile-js-site.cmd +++ b/scripts/compile-js-site.cmd @@ -39,8 +39,8 @@ popd :js-uuidgenerator-minify echo Minifying UUID Generator pushd %CD% -cd %~dp0\..\static\resources\NibblePoker\tools\uuid-generator\ -echo ^> static\resources\NibblePoker\tools\svg-to-png\svg-to-png.mjs +cd %~dp0\..\static\resources\NibblePoker\applets\uuid-generator\ +echo ^> static\resources\NibblePoker\applets\uuid-generator\uuid-generator.mjs call "%~dp0node_modules\.bin\rollup" uuid-generator.mjs --file uuid-generator.js call "%~dp0node_modules\.bin\terser" uuid-generator.js -c -m -o uuid-generator.min.js popd diff --git a/static/resources/NibblePoker/tools/uuid-generator/uuid-generator.css b/static/resources/NibblePoker/applets/uuid-generator/uuid-generator.css similarity index 100% rename from static/resources/NibblePoker/tools/uuid-generator/uuid-generator.css rename to static/resources/NibblePoker/applets/uuid-generator/uuid-generator.css diff --git a/static/resources/NibblePoker/applets/uuid-generator/uuid-generator.mjs b/static/resources/NibblePoker/applets/uuid-generator/uuid-generator.mjs new file mode 100644 index 0000000..1fd9aef --- /dev/null +++ b/static/resources/NibblePoker/applets/uuid-generator/uuid-generator.mjs @@ -0,0 +1,134 @@ +/** + * Generates a random UUID4 and returns its string representation + * @returns {`${string}-${string}-${string}-${string}-${string}`} + */ +export function generateUUID4(addHyphens, addGuidBrackets) { + let uuid4 = crypto.randomUUID(); + if(!addHyphens) { + uuid4 = uuid4.replace(/-/g, ""); + } + if(addGuidBrackets) { + uuid4 = "{" + uuid4 + "}"; + } + return uuid4; +} + +// Tool-centric stuff +{ + /** @type {HTMLSelectElement} */ + const eOptionTypeSelect = document.querySelector("select#uuid-generator-option-type"); + + /** @type {HTMLInputElement} */ + const eOptionCountInput = document.querySelector("input#uuid-generator-option-count"); + + /** @type {HTMLInputElement} */ + const eOptionHyphenInput = document.querySelector("input#uuid-generator-option-hyphens"); + /** @type {HTMLInputElement} */ + const eOptionGuidBracketsInput = document.querySelector("input#uuid-generator-option-guid-brackets"); + + /** @type {HTMLElement} */ + const eGenerateButton = document.querySelector("#uuid-generator-generate"); + /** @type {HTMLElement} */ + const eDownloadRawButton = document.querySelector("#uuid-generator-download-raw"); + /** @type {HTMLElement} */ + const eDownloadJsonButton = document.querySelector("#uuid-generator-download-json"); + /** @type {HTMLElement} */ + const eDownloadYamlButton = document.querySelector("#uuid-generator-download-yaml"); + + /** @type {HTMLTextAreaElement} */ + const ePreviewTextArea = document.querySelector("textarea#uuid-generator-preview"); + + let lastUUIDs = []; + + /** @returns {number} */ + function getDesiredCount() { + let desiredCount = null; + try { + desiredCount = parseInt(eOptionCountInput.value); + } catch (e) { + console.error(e); + } + if(desiredCount === null) { + desiredCount = 1; + } + if(desiredCount < 1) { + desiredCount = 1; + } + if(desiredCount > 1000) { + desiredCount = 1000; + } + return desiredCount; + } + + function changeDesiredCount(difference = 0) { + if(difference !== 0) { + eOptionCountInput.value = getDesiredCount() + difference; + } + eOptionCountInput.value = getDesiredCount(); + } + + + function downloadStringAsFile(content, filename, contentType) { + const blob = new Blob([content], { type: contentType }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + + link.href = url; + link.download = filename; + + document.body.appendChild(link); + link.click(); + + document.body.removeChild(link); + URL.revokeObjectURL(url); + } + + window.onload = function () { + eGenerateButton.addEventListener("click", function() { + ePreviewTextArea.value = ""; + + let desiredCount = getDesiredCount(); + let uuidGenerator = generateUUID4; + + let addHyphens = eOptionHyphenInput.checked; + let addGuidBrackets = eOptionGuidBracketsInput.checked; + + lastUUIDs = []; + for(let i= 0; i < desiredCount; i++) { + lastUUIDs.push(uuidGenerator(addHyphens, addGuidBrackets)); + ePreviewTextArea.value += uuidGenerator(addHyphens, addGuidBrackets) + "\n"; + } + ePreviewTextArea.value = lastUUIDs.join("\n"); + }); + eOptionCountInput.addEventListener("change", function() { + changeDesiredCount(0); + }); + eOptionCountInput.addEventListener("mousewheel", function(e) { + // Handling wheel scroll on count field. + if(e.wheelDelta < 0) { + changeDesiredCount(-1); + } else { + changeDesiredCount(1); + } + }); + + eDownloadRawButton.addEventListener("click", function() { + if (lastUUIDs.length <= 0) { + return; + } + downloadStringAsFile(lastUUIDs.join("\n"), "uuids.txt", "text/plain"); + }); + eDownloadJsonButton.addEventListener("click", function() { + if (lastUUIDs.length <= 0) { + return; + } + downloadStringAsFile(JSON.stringify(lastUUIDs, null, 4), "uuids.json", "application/json"); + }); + eDownloadYamlButton.addEventListener("click", function() { + if (lastUUIDs.length <= 0) { + return; + } + downloadStringAsFile("- \"" + lastUUIDs.join("\"\n- \"") + "\"", "uuids.yaml", "text/yaml"); + }); + } +} diff --git a/static/resources/NibblePoker/images/content/docker-mini-cctv-nvr/cam.png b/static/resources/NibblePoker/images/content/docker-mini-cctv-nvr/cam.png new file mode 100644 index 0000000..cf5bb0a Binary files /dev/null and b/static/resources/NibblePoker/images/content/docker-mini-cctv-nvr/cam.png differ diff --git a/static/resources/NibblePoker/images/content/docker-mini-cctv-nvr/home.png b/static/resources/NibblePoker/images/content/docker-mini-cctv-nvr/home.png new file mode 100644 index 0000000..c78e369 Binary files /dev/null and b/static/resources/NibblePoker/images/content/docker-mini-cctv-nvr/home.png differ diff --git a/static/resources/NibblePoker/images/projects/circuitpython-custom-fs/draft.md b/static/resources/NibblePoker/images/projects/circuitpython-custom-fs/draft.md new file mode 100644 index 0000000..94c9ca3 --- /dev/null +++ b/static/resources/NibblePoker/images/projects/circuitpython-custom-fs/draft.md @@ -0,0 +1,38 @@ +# CircuitPython - Custom File Systems + +The goal of this experiment was to try and give people a strong, clear, and documented starting point for +future experiments that may require virtual file systems and block-level devices on CircuitPython devices. + +For example, by using the blank examples, you can easily create a bootstrapping code and file system that connects +securely to a remote server and pulls code directly from it without ever having to touch the MCU's flash storage. \ + +The second main advantage is that this project can serve as a robust educational tool. \ +Due to the permissive nature of Python and CircuitPython's APIs, it lets people easily test out different designs and +mechanisms for their file systems without running the risk of corrupting unrelated data or bricking their device. \ +Additionally, it is possible to manipulate and add logging to many of the methods which allows you to see and understand +the inner workings of CircuitPython, filesystems and BLD devices themselves. + + + +Le but de cette expérience était de fournir un point de départ solide, clair et documenté pour +de futures expériences nécessitant des systèmes de fichiers virtuels, ou des périphériques de bloc sur +des appareils utilisant CircuitPython. + +Par exemple, en utilisant les exemples modèles, il est possible de créer facilement un code de démarrage et +un système de fichiers qui vont connecter de manière sécurisée à un serveur distant, et y récupérer directement du +code sans jamais avoir à toucher à la mémoire flash du MCU. + +Le deuxième avantage majeur est que ce projet peut servir d'outil éducatif. +En effet, les APIs extrêmement permissives de Python et CircuitPython permettent à leurs utilisateurs de tester +facilement différents designs et mécanismes pour leurs systèmes de fichiers sans risquer de corrompre des +données ou de rendre leur appareil inutilisable. \ +De plus, vous pouvez très facilement ajouter des messages de débogage et manipuler plusieurs méthodes, ce +qui permet de voir et de comprendre le fonctionnement interne de CircuitPython, des systèmes de fichiers et +des périphériques de bloc en eux-mêmes. + + + +## ??? + +## Media coverage +https://blog.adafruit.com/2023/02/22/icymi-python-on-microcontrollers-newsletter-new-raspberry-pi-debug-probe-circuitpython-8-0-2-and-much-more-circuitpython-python-micropython-icymi-raspberry_pi/ diff --git a/static/resources/NibblePoker/tools/uuid-generator/uuid-generator.mjs b/static/resources/NibblePoker/tools/uuid-generator/uuid-generator.mjs deleted file mode 100644 index b421b42..0000000 --- a/static/resources/NibblePoker/tools/uuid-generator/uuid-generator.mjs +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Generates a random UUID4 and returns its string representation - * @returns {`${string}-${string}-${string}-${string}-${string}`} - */ -export function generateUUID4() { - return crypto.randomUUID(); -} - -// Tool-centric stuff -{ - /** @type {HTMLSelectElement} */ - const eOptionTypeSelect = document.querySelector("select#uuid-generator-option-type"); - /** @type {HTMLInputElement} */ - const eOptionCountInput = document.querySelector("input#uuid-generator-option-count"); - /** @type {HTMLInputElement} */ - const eOptionHyphenInput = document.querySelector("input#uuid-generator-option-hyphens"); - - /** @type {HTMLElement} */ - const eGenerateButton = document.querySelector("#uuid-generator-generate"); - /** @type {HTMLElement} */ - const eDownloadButton = document.querySelector("#uuid-generator-download"); - - /** @type {HTMLTextAreaElement} */ - const ePreviewTextArea = document.querySelector("textarea#uuid-generator-preview"); - - /** @returns {number} */ - function getDesiredCount() { - let desiredCount = null; - try { - desiredCount = parseInt(eOptionCountInput.value); - } catch (e) { - console.error(e); - } - if(desiredCount === null) { - desiredCount = 1; - } - if(desiredCount < 1) { - desiredCount = 1; - } - if(desiredCount > 1000) { - desiredCount = 1000; - } - return desiredCount; - } - - window.onload = function () { - eGenerateButton.addEventListener("click", function() { - ePreviewTextArea.value = ""; - - let desiredCount = getDesiredCount(); - let uuidGenerator = generateUUID4; - - for(let i= 0; i < desiredCount; i++) { - ePreviewTextArea.value += uuidGenerator() + "\n"; - } - }); - eDownloadButton.addEventListener("click", function() { - //eFileDropInput.click(); - }); - } -} diff --git a/templates/applets/uuid-generator.jinja b/templates/applets/uuid-generator.jinja new file mode 100644 index 0000000..3189053 --- /dev/null +++ b/templates/applets/uuid-generator.jinja @@ -0,0 +1,44 @@ + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + + + + + +
+ + + diff --git a/templates/base_standalone.jinja b/templates/base_standalone.jinja index 5058f2f..f055e3e 100644 --- a/templates/base_standalone.jinja +++ b/templates/base_standalone.jinja @@ -10,7 +10,8 @@ - + + diff --git a/templates/base_www.jinja b/templates/base_www.jinja index adebbed..ad720e2 100644 --- a/templates/base_www.jinja +++ b/templates/base_www.jinja @@ -19,7 +19,8 @@ {% block extra_preloads %}{% endblock %} - + + {% block extra_stylesheets %}{% endblock %} @@ -114,4 +115,4 @@ {% block extra_scripts %}{% endblock %} - \ No newline at end of file + diff --git a/templates/elements/code.jinja b/templates/elements/code.jinja new file mode 100644 index 0000000..2bbe724 --- /dev/null +++ b/templates/elements/code.jinja @@ -0,0 +1,5 @@ + + {% for code_line in code_lines %} + {{ code_line }}
+ {% endfor %} +
diff --git a/templates/elements/list-ul.jinja b/templates/elements/list-ul.jinja new file mode 100644 index 0000000..7f49d12 --- /dev/null +++ b/templates/elements/list-ul.jinja @@ -0,0 +1,9 @@ + diff --git a/templates/elements/paragraph.jinja b/templates/elements/paragraph.jinja index 4bf8294..e05fae6 100644 --- a/templates/elements/paragraph.jinja +++ b/templates/elements/paragraph.jinja @@ -1,3 +1,3 @@ -

+

{{ paragraph_inner_html }}

\ No newline at end of file diff --git a/templates/pages/privacy.jinja b/templates/pages/privacy.jinja index cbf402b..6c4fe5c 100644 --- a/templates/pages/privacy.jinja +++ b/templates/pages/privacy.jinja @@ -17,22 +17,20 @@ {{ render_paragraph(l10n("introduction.text.1", "privacy", user_lang)) }} {{ render_paragraph(l10n("introduction.text.2", "privacy", user_lang) + '
- - https://gdpr.eu/ -
+ https://gdpr.eu/
- - https://eur-lex.europa.eu/ - ')}} + https://eur-lex.europa.eu/')}} {{ render_h1(l10n("v2.data.title", "privacy", user_lang), "fad fa-database") }} {{ render_paragraph(l10n("v2.data.intro.1", "privacy", user_lang) + '
' + l10n("v2.data.intro.2", "privacy", user_lang)) }} {{ render_paragraph(l10n('v2.data.private.1', "privacy", user_lang) + - '') }} + render_list_ul([ + l10n('v2.data.private_list.1', "privacy", user_lang), + l10n('v2.data.private_list.2', "privacy", user_lang), + ]) + ) }} {{ render_paragraph(l10n('v2.data.non_private.1', "privacy", user_lang) + '