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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+