Implemented tools as applets, Added Docker CCTV page, Fixed small issues

Update app.py, uuid-generator.yml, and 47 more files...
This commit is contained in:
2025-02-20 17:24:05 +01:00
parent bd96d85699
commit 0e91b5ed96
43 changed files with 889 additions and 337 deletions

94
app.py
View File

@@ -9,22 +9,43 @@ from flask import render_template
from minify_html import minify from minify_html import minify
from werkzeug.exceptions import HTTPException 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.contributors import reload_contributors_data, get_contributors_data
from website.domains import ALLOWED_DOMAINS from website.domains import ALLOWED_DOMAINS
from website.l10n.utils import get_user_lang, localize, reload_strings, l10n_url_abs, l10n_url_switch, L10N, \ from website.l10n.utils import get_user_lang, localize, reload_strings, l10n_url_abs, l10n_url_switch, L10N, \
DEFAULT_LANG DEFAULT_LANG
from website.renderers.applet import render_applet_scripts, render_applet_head
from website.renderers.button import render_button 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.paragraph import render_paragraph
from website.renderers.lists import render_list_ul
from website.renderers.splide import render_splide from website.renderers.splide import render_splide
from website.sidebar import reload_sidebar_entries, get_sidebar_entries from website.sidebar import reload_sidebar_entries, get_sidebar_entries
from website.sitemap import reload_sitemap_entries, get_sitemap_entries from website.sitemap import reload_sitemap_entries, get_sitemap_entries
# try: try:
# from rich import print from rich import print
# except ImportError: except ImportError:
# pass 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( app = Flask(
@@ -72,24 +93,35 @@ def inject_processors():
domain_host=request.headers['Host'], domain_host=request.headers['Host'],
domain_tld=request.headers['Host'].split('.')[-1] if request.headers['Host'] in ALLOWED_DOMAINS else "lu", domain_tld=request.headers['Host'].split('.')[-1] if request.headers['Host'] in ALLOWED_DOMAINS else "lu",
domain_url_root=request.url_root, domain_url_root=request.url_root,
# L10N # L10N
l10n=localize, l10n=localize,
l10n_url_abs=l10n_url_abs, l10n_url_abs=l10n_url_abs,
l10n_url_switch=l10n_url_switch, l10n_url_switch=l10n_url_switch,
# Sidebar # Sidebar
get_sidebar_entries=get_sidebar_entries, get_sidebar_entries=get_sidebar_entries,
# Content # Content
get_content=get_content,
get_articles=get_articles, get_articles=get_articles,
get_projects=get_projects, get_projects=get_projects,
get_tools=get_tools, get_tools=get_tools,
# Renderers # Renderers
render_button=render_button, render_button=render_button,
render_heading=render_heading, render_heading=render_heading,
render_h1=render_h1, render_h1=render_h1,
render_h2=render_h2, render_h2=render_h2,
render_h3=render_h3, render_h3=render_h3,
render_h4=render_h4,
render_paragraph=render_paragraph, render_paragraph=render_paragraph,
render_list_ul=render_list_ul,
render_splide=render_splide, render_splide=render_splide,
render_applet_scripts=render_applet_scripts,
render_applet_head=render_applet_head,
render_code_block=render_code_block,
# Commons # Commons
url_for=url_for, url_for=url_for,
escape=escape, escape=escape,
@@ -118,7 +150,7 @@ def route_sitemap():
@app.route('/fr/', defaults={'lang': "fr"}) @app.route('/fr/', defaults={'lang': "fr"})
def route_root(lang: Optional[str]): def route_root(lang: Optional[str]):
user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE")) 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", "pages/root.jinja",
user_lang=user_lang, user_lang=user_lang,
raw_lang=lang, raw_lang=lang,
@@ -132,7 +164,7 @@ def route_root(lang: Optional[str]):
@app.route('/fr/contact/', defaults={'lang': "fr"}) @app.route('/fr/contact/', defaults={'lang': "fr"})
def route_contact(lang: Optional[str]): def route_contact(lang: Optional[str]):
user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE")) 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", "pages/contact.jinja",
user_lang=user_lang, user_lang=user_lang,
raw_lang=lang, raw_lang=lang,
@@ -152,7 +184,7 @@ def route_content(lang: Optional[str]):
except ValueError: except ValueError:
requested_tags = None requested_tags = None
return minify(render_template( return post_process_html(render_template(
"pages/project_index.jinja", "pages/project_index.jinja",
user_lang=user_lang, user_lang=user_lang,
raw_lang=lang, raw_lang=lang,
@@ -178,7 +210,7 @@ def route_content_project(lang: Optional[str], project_id: str):
error_code = 404 error_code = 404
if error_key is not None: if error_key is not None:
return minify(render_template( return post_process_html(render_template(
"pages/error.jinja", "pages/error.jinja",
user_lang=user_lang, user_lang=user_lang,
raw_lang=lang, raw_lang=lang,
@@ -188,7 +220,7 @@ def route_content_project(lang: Optional[str], project_id: str):
error_code=error_code, error_code=error_code,
)).replace("> <", "><"), error_code )).replace("> <", "><"), error_code
else: else:
return minify(render_template( return post_process_html(render_template(
"projects/" + project_id + ".jinja", "projects/" + project_id + ".jinja",
user_lang=user_lang, user_lang=user_lang,
raw_lang=lang, raw_lang=lang,
@@ -210,7 +242,7 @@ def route_tools_index(lang: Optional[str]):
except ValueError: except ValueError:
requested_tags = None requested_tags = None
return minify(render_template( return post_process_html(render_template(
"pages/tools_index.jinja", "pages/tools_index.jinja",
user_lang=user_lang, user_lang=user_lang,
raw_lang=lang, raw_lang=lang,
@@ -236,7 +268,7 @@ def route_tools_page(lang: Optional[str], tool_id: str):
error_code = 404 error_code = 404
if error_key is not None: if error_key is not None:
return minify(render_template( return post_process_html(render_template(
"pages/error.jinja", "pages/error.jinja",
user_lang=user_lang, user_lang=user_lang,
raw_lang=lang, raw_lang=lang,
@@ -246,14 +278,15 @@ def route_tools_page(lang: Optional[str], tool_id: str):
error_code=error_code, error_code=error_code,
)).replace("> <", "><"), error_code )).replace("> <", "><"), error_code
else: else:
return minify(render_template( return post_process_html(render_template(
"tools/" + tool_id + ".jinja", "pages/tools_page.jinja",
user_lang=user_lang, user_lang=user_lang,
raw_lang=lang, raw_lang=lang,
request_path=request.path, request_path=request.path,
standalone="standalone" in request.args, standalone="standalone" in request.args,
tool_data=get_tools().get(tool_id), tool_data=get_tools().get(tool_id),
tool_id=tool_id, tool_id=tool_id,
applet_data=get_applets().get(get_tools().get(tool_id).applet_id),
)).replace("> <", "><") )).replace("> <", "><")
@@ -262,7 +295,7 @@ def route_tools_page(lang: Optional[str], tool_id: str):
@app.route('/fr/about/', defaults={'lang': "fr"}) @app.route('/fr/about/', defaults={'lang': "fr"})
def route_about(lang: Optional[str]): def route_about(lang: Optional[str]):
user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE")) 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", "pages/about.jinja",
user_lang=user_lang, user_lang=user_lang,
raw_lang=lang, raw_lang=lang,
@@ -276,7 +309,7 @@ def route_about(lang: Optional[str]):
@app.route('/fr/privacy/', defaults={'lang': "fr"}) @app.route('/fr/privacy/', defaults={'lang': "fr"})
def route_privacy(lang: Optional[str]): def route_privacy(lang: Optional[str]):
user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE")) 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", "pages/privacy.jinja",
user_lang=user_lang, user_lang=user_lang,
raw_lang=lang, raw_lang=lang,
@@ -290,7 +323,7 @@ def route_privacy(lang: Optional[str]):
@app.route('/fr/links/', defaults={'lang': "fr"}) @app.route('/fr/links/', defaults={'lang': "fr"})
def route_links(lang: Optional[str]): def route_links(lang: Optional[str]):
user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE")) 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", "pages/links.jinja",
user_lang=user_lang, user_lang=user_lang,
raw_lang=lang, raw_lang=lang,
@@ -304,7 +337,7 @@ def route_links(lang: Optional[str]):
@app.route('/fr/debug/', defaults={'lang': "fr"}) @app.route('/fr/debug/', defaults={'lang': "fr"})
def route_debug(lang: Optional[str]): def route_debug(lang: Optional[str]):
user_lang = get_user_lang(lang, request.headers.get("HTTP_ACCEPT_LANGUAGE")) 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", "pages/debug.jinja",
user_lang=user_lang, user_lang=user_lang,
raw_lang=lang, raw_lang=lang,
@@ -323,7 +356,7 @@ def handle_exception(e: Exception):
if isinstance(e, HTTPException): if isinstance(e, HTTPException):
error_code = e.code error_code = e.code
return minify(render_template( return post_process_html(render_template(
"pages/error.jinja", "pages/error.jinja",
user_lang=DEFAULT_LANG, user_lang=DEFAULT_LANG,
raw_lang=DEFAULT_LANG, raw_lang=DEFAULT_LANG,
@@ -335,7 +368,7 @@ def handle_exception(e: Exception):
if __name__ == '__main__': if __name__ == '__main__':
reload_content_items() load_content_items()
reload_strings(os.path.join(os.getcwd(), "data/strings/")) reload_strings(os.path.join(os.getcwd(), "data/strings/"))
reload_sidebar_entries(os.path.join(os.getcwd(), "data/sidebar.yml")) reload_sidebar_entries(os.path.join(os.getcwd(), "data/sidebar.yml"))
reload_contributors_data(os.path.join(os.getcwd(), "data/contributors.yml")) reload_contributors_data(os.path.join(os.getcwd(), "data/contributors.yml"))
@@ -362,22 +395,3 @@ if __name__ == '__main__':
#debug=False, #debug=False,
load_dotenv=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,

View File

@@ -0,0 +1,8 @@
applets:
- id: "uuid-generator"
resources:
scripts:
- "uuid-generator.mjs"
stylesheets:
- "uuid-generator.css"

View File

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

View File

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

View File

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

View File

@@ -42,13 +42,13 @@
abs_href: "/tools" abs_href: "/tools"
icon: fad fa-toolbox icon: fad fa-toolbox
active_id: tools active_id: tools
has_new_until_utc: 0 has_new_until_utc: 1760986472
- title_key: text.downloads - title_key: text.downloads
raw_href: "https://files.nibblepoker.lu/" raw_href: "https://files.nibblepoker.lu/"
icon: fad fa-download icon: fad fa-download
active_id: "" active_id: ""
has_new_until_utc: 1760986472 has_new_until_utc: 0
- -

View File

@@ -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.<br>
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 <span class=\"code\">NP_CCTV_URL</span> environment variable,
and the output folder via a mounted volume that is mounted as <span class=\"code\">/data</span> in the container."
setup.camera.3: "This example will use the <span class=\"code\">rtsp://user:password@address:554/sub-path</span>
URL and will put its recordings in <span class=\"code\">./recordings/cam1.</span>"
setup.cleaner.title: "Cleaner"
setup.cleaner.1: "The cleaner script named cleaner.py only requires you to set 1 environment variable named
<span class=\"code\">NP_MAX_FILE_AGE_HOURS</span> 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:<br>
&nbsp;&nbsp;<span class=\"code\">NP_CAM_&lt;camId&gt; = &lt;Camera's name&gt;</span>"
setup.web.3: "Here is an example for <span class=\"code\">cam1</span> if named as <span class=\"code\">Camera #1</span>:<br>
&nbsp;&nbsp;<span class=\"code\">NP_CAM_cam1 = Camera #1</span>"
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"

View File

@@ -1,4 +1,17 @@
# EN - UUID Generator # 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.hyphen: "Add hyphens"
option.guid_brackets: "Add GUID brackets"
generate: "Generate"
download.raw: "Raw"
download.json: "JSON"
download.yaml: "YAML"

View File

@@ -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."

View File

@@ -1,4 +1,17 @@
# FR - UUID Generator # 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.count: "Nombre d'UUID/GUID"
option.hyphen: "Ajouter trait d'union" 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"

View File

@@ -25,6 +25,6 @@ metadata:
subtitle_key: "article.subtitle" subtitle_key: "article.subtitle"
tags: tags:
- "undefined" - "undefined"
data: resources:
scripts: scripts:
- "epr_main.js" - "epr_main.js"

View File

@@ -25,7 +25,7 @@ metadata:
subtitle_key: "article.subtitle" subtitle_key: "article.subtitle"
tags: tags:
- "undefined" - "undefined"
data: resources:
scripts: scripts:
- "svg-to-png.mjs" - "svg-to-png.mjs"
stylesheets: stylesheets:

View File

@@ -1,3 +1,7 @@
tools:
- id: "uuid-generator"
applet_id: "uuid-generator"
metadata: metadata:
head: head:
title_key: "meta.title" title_key: "meta.title"
@@ -25,8 +29,3 @@ metadata:
subtitle_key: "article.subtitle" subtitle_key: "article.subtitle"
tags: tags:
- "undefined" - "undefined"
data:
scripts:
- "uuid-generator.mjs"
stylesheets:
- "uuid-generator.css"

View File

@@ -10,4 +10,4 @@ locked-dict
Werkzeug~=3.0.4 Werkzeug~=3.0.4
#gunicorn #gunicorn
waitress~=3.0.0 waitress~=3.0.2

View File

@@ -39,8 +39,8 @@ popd
:js-uuidgenerator-minify :js-uuidgenerator-minify
echo Minifying UUID Generator echo Minifying UUID Generator
pushd %CD% pushd %CD%
cd %~dp0\..\static\resources\NibblePoker\tools\uuid-generator\ cd %~dp0\..\static\resources\NibblePoker\applets\uuid-generator\
echo ^> static\resources\NibblePoker\tools\svg-to-png\svg-to-png.mjs 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\rollup" uuid-generator.mjs --file uuid-generator.js
call "%~dp0node_modules\.bin\terser" uuid-generator.js -c -m -o uuid-generator.min.js call "%~dp0node_modules\.bin\terser" uuid-generator.js -c -m -o uuid-generator.min.js
popd popd

View File

@@ -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");
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

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

View File

@@ -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();
});
}
}

View File

@@ -0,0 +1,44 @@
<label for="uuid-generator-option-type" class="mr-xs">{{ l10n("type.label", "uuid-generator", user_lang) }}:</label>
<select name="uuid-generator-option-type" id="uuid-generator-option-type" class="p-xxs border r-s">
<option value="type-uuid4" selected>{{ l10n("type.uuid4", "uuid-generator", user_lang) }}</option>
<!--<option value="type-guid">{{ l10n("type.guid", "uuid-generator", user_lang) }}</option>-->
</select>
<br>
<label for="uuid-generator-option-count" class="mr-xs">{{ l10n("option.count", "uuid-generator", user_lang) }}:</label>
<input id="uuid-generator-option-count" class="p-xxs border r-s" type="number" value="4" min="1" max="1000">
<br>
<label for="uuid-generator-option-hyphens" class="mr-xxs">{{ l10n("option.hyphen", "uuid-generator", user_lang) }}:</label>
<input id="uuid-generator-option-hyphens" class="r-m border" type="checkbox" checked>
<br>
<label for="uuid-generator-option-guid-brackets" class="mr-xxs">{{ l10n("option.guid_brackets", "uuid-generator", user_lang) }}:</label>
<input id="uuid-generator-option-guid-brackets" class="r-m border" type="checkbox">
<hr class="subtle">
<button id="uuid-generator-generate" class="p-xs r-s border b-light success">
<i class="fa-duotone fa-solid fa-gears mr-xs"></i>{{ l10n("generate", "uuid-generator", user_lang) }}
</button>
<button class="p-xs r-s border b-light primary rr-0 br-0">
<i class="fa-duotone fa-solid fa-download"></i>
</button>
<button id="uuid-generator-download-raw" class="p-xs r-s border b-light primary ml-0 r-0 br-0">
{{ l10n("download.raw", "uuid-generator", user_lang) }}
</button>
<button id="uuid-generator-download-json" class="p-xs r-s border b-light primary ml-0 r-0 br-0">
{{ l10n("download.json", "uuid-generator", user_lang) }}
</button>
<button id="uuid-generator-download-yaml" class="p-xs r-s border b-light primary ml-0 rl-0">
{{ l10n("download.yaml", "uuid-generator", user_lang) }}
</button>
<hr class="subtle">
<label for="uuid-generator-preview" class="d-none">{{ l10n("preview.label", "uuid-generator", user_lang) }}:</label>
<textarea name="uuid-generator-preview" id="uuid-generator-preview" rows="16" class="w-full border r-s"></textarea>

View File

@@ -10,7 +10,8 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="alternate icon" href="/favicon.ico"> <link rel="alternate icon" href="/favicon.ico">
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/FontAwesomePro/6.5.1/css/all.min.css"> <link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/FontAwesomePro/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/NibblePoker/StandardCSS/nibblepoker.min.css"> <!--<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/NibblePoker/StandardCSS/nibblepoker.min.css">-->
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/NibblePoker/IndevCSS/nibblepoker.min.css">
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/Quantum/Quantum.min.css"> <link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/Quantum/Quantum.min.css">
<meta charset="UTF-8"> <meta charset="UTF-8">

View File

@@ -19,7 +19,8 @@
{% block extra_preloads %}{% endblock %} {% block extra_preloads %}{% endblock %}
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/FontAwesomePro/6.5.1/css/all.min.css"> <link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/FontAwesomePro/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/NibblePoker/StandardCSS/nibblepoker.min.css"> <!--<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/NibblePoker/StandardCSS/nibblepoker.min.css">-->
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/NibblePoker/IndevCSS/nibblepoker.min.css">
<link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/Quantum/Quantum.min.css"> <link rel="stylesheet" href="https://cdn.nibblepoker.{{ domain_tld }}/Quantum/Quantum.min.css">
{% block extra_stylesheets %}{% endblock %} {% block extra_stylesheets %}{% endblock %}

View File

@@ -0,0 +1,5 @@
<code class="code ox-auto w-full d-inline-block position-relative language-{{ code_language }}">
{% for code_line in code_lines %}
<span class="code-line t-nowrap">{{ code_line }}</span><br>
{% endfor %}
</code>

View File

@@ -0,0 +1,9 @@
<ul class="l-bullets l-bullet-manual">
{% for list_item in list_items %}
{% if list_item.__class__.__name__ == 'list' %}
{{ render_list_ul(list_item) }}
{% else %}
<li>{{ list_item }}</li>
{% endif %}
{% endfor %}
</ul>

View File

@@ -1,3 +1,3 @@
<p class="mt-xs mx-s"> <p class="mt-xs">
{{ paragraph_inner_html }} {{ paragraph_inner_html }}
</p> </p>

View File

@@ -17,22 +17,20 @@
{{ render_paragraph(l10n("introduction.text.1", "privacy", user_lang)) }} {{ render_paragraph(l10n("introduction.text.1", "privacy", user_lang)) }}
{{ render_paragraph(l10n("introduction.text.2", "privacy", user_lang) + {{ render_paragraph(l10n("introduction.text.2", "privacy", user_lang) +
'<br><i class="fad fa-globe ml-s t-size-8"></i> '<br><i class="fad fa-globe ml-s t-size-8"></i>
<a href="https://gdpr.eu/privacy-notice/"> <a href="https://gdpr.eu/privacy-notice/" class="ml-xs">https://gdpr.eu/</a><br>
https://gdpr.eu/
</a><br>
<i class="fad fa-globe ml-s t-size-8"></i> <i class="fad fa-globe ml-s t-size-8"></i>
<a href="https://eur-lex.europa.eu/legal-content/ALL/?uri=CELEX%3A32016R0679"> <a href="https://eur-lex.europa.eu/legal-content/ALL/?uri=CELEX%3A32016R0679" class="ml-xs">https://eur-lex.europa.eu/</a>')}}
https://eur-lex.europa.eu/
</a>')}}
{{ render_h1(l10n("v2.data.title", "privacy", user_lang), "fad fa-database") }} {{ render_h1(l10n("v2.data.title", "privacy", user_lang), "fad fa-database") }}
{{ render_paragraph(l10n("v2.data.intro.1", "privacy", user_lang) + {{ render_paragraph(l10n("v2.data.intro.1", "privacy", user_lang) +
'<br>' + l10n("v2.data.intro.2", "privacy", user_lang)) }} '<br>' + l10n("v2.data.intro.2", "privacy", user_lang)) }}
{{ render_paragraph(l10n('v2.data.private.1', "privacy", user_lang) + {{ render_paragraph(l10n('v2.data.private.1', "privacy", user_lang) +
'<ul><li>' + l10n('v2.data.private_list.1', "privacy", user_lang) + render_list_ul([
'</li><li>' + l10n('v2.data.private_list.2', "privacy", user_lang) + l10n('v2.data.private_list.1', "privacy", user_lang),
'</li></ul>') }} l10n('v2.data.private_list.2', "privacy", user_lang),
])
) }}
{{ render_paragraph(l10n('v2.data.non_private.1', "privacy", user_lang) + {{ render_paragraph(l10n('v2.data.non_private.1', "privacy", user_lang) +
'<ul><li>' + l10n('v2.data.non_private_list.1', "privacy", user_lang) + '<ul><li>' + l10n('v2.data.non_private_list.1', "privacy", user_lang) +
'</li><li>' + l10n('v2.data.non_private_list.2', "privacy", user_lang) + '</li><li>' + l10n('v2.data.non_private_list.2', "privacy", user_lang) +
@@ -167,17 +165,17 @@
{{ render_h1(l10n("contact.title", "privacy", user_lang), "fad fa-mailbox") }} {{ render_h1(l10n("contact.title", "privacy", user_lang), "fad fa-mailbox") }}
{{ render_paragraph(l10n("contact.text.1", "privacy", user_lang) + {{ render_paragraph(l10n("contact.text.1", "privacy", user_lang) +
'<br><i class="fad fa-at t-size-8 ml-s"></i><a href="mailto:herwin.bozet@gmail.com">herwin.bozet@gmail.com</a>') }} '<br><i class="fad fa-at t-size-8 ml-s"></i><a class="ml-xs" href="mailto:herwin.bozet@gmail.com">herwin.bozet@gmail.com</a>') }}
{{ render_h1(l10n("complaint.title", "privacy", user_lang), "fad fa-gavel") }} {{ render_h1(l10n("complaint.title", "privacy", user_lang), "fad fa-gavel") }}
{{ render_paragraph(l10n("complaint.text.1", "privacy", user_lang)) }} {{ render_paragraph(l10n("complaint.text.1", "privacy", user_lang)) }}
{{ render_paragraph(l10n('complaint.text.2', "privacy", user_lang) + {{ render_paragraph(l10n('complaint.text.2', "privacy", user_lang) +
'<br><i class="fad fa-globe ml-s t-size-8"></i>' + '<br><i class="fad fa-globe ml-s t-size-8"></i>' +
'<a href="https://ec.europa.eu/info/law/law-topic/data-protection/reform/rights-citizens/redress/what-should-i-do-if-i-think-my-personal-data-protection-rights-havent-been-respected_en">https://ec.europa.eu/</a>' + '<a class="ml-xs" href="https://ec.europa.eu/info/law/law-topic/data-protection/reform/rights-citizens/redress/what-should-i-do-if-i-think-my-personal-data-protection-rights-havent-been-respected_en">https://ec.europa.eu/</a>' +
'<span class="ml-s">(' + l10n('english', "langs", user_lang) + ')</span>' + '<span class="ml-s">(' + l10n('english', "langs", user_lang) + ')</span>' +
'<br><i class="fad fa-globe ml-s t-size-8"></i>' + '<br><i class="fad fa-globe ml-s t-size-8"></i>' +
'<a href="https://gegevensbeschermingsautoriteit.be/citoyen/agir/introduire-une-plainte">https://gegevensbeschermingsautoriteit.be/</a>' + '<a class="ml-xs" href="https://gegevensbeschermingsautoriteit.be/citoyen/agir/introduire-une-plainte">https://gegevensbeschermingsautoriteit.be/</a>' +
'<span class="ml-s">(' + l10n('french', "langs", user_lang) + ')</span>' ) }} '<span class="ml-s">(' + l10n('french', "langs", user_lang) + ')</span>' ) }}
{% endblock %} {% endblock %}

View File

@@ -20,34 +20,35 @@
{{ render_h2(l10n("updates.title", "home", user_lang)) }} {{ render_h2(l10n("updates.title", "home", user_lang)) }}
<p><i class="fad fa-calendar-alt mr-xs"></i>{{ l10n("updates.4.date", "home", user_lang) }}</p> <p><i class="fad fa-calendar-alt mr-xs mt-s"></i>{{ l10n("updates.4.date", "home", user_lang) }}</p>
<ul> {{ render_list_ul([
<li>{{ l10n("updates.4.text.1", "home", user_lang) }}</li> l10n("updates.4.text.1", "home", user_lang),
<li>{{ l10n("updates.4.text.2", "home", user_lang) }}</li> l10n("updates.4.text.2", "home", user_lang),
<li>{{ l10n("updates.4.text.3", "home", user_lang) }}</li> l10n("updates.4.text.3", "home", user_lang),
<li>{{ l10n("updates.text.privacy", "home", user_lang) }}</li> l10n("updates.text.privacy", "home", user_lang)
</ul> ]) }}
<p><i class="fad fa-calendar-alt mr-xs"></i>{{ l10n("updates.3.date", "home", user_lang) }}</p> <p><i class="fad fa-calendar-alt mr-xs mt-s"></i>{{ l10n("updates.3.date", "home", user_lang) }}</p>
<ul> {{ render_list_ul([
<li>{{ l10n("updates.3.text.1", "home", user_lang) }}</li> l10n("updates.3.text.1", "home", user_lang),
<li>{{ l10n("updates.3.text.2", "home", user_lang) }}</li> l10n("updates.3.text.2", "home", user_lang),
<li>{{ l10n("updates.3.text.3", "home", user_lang) }}</li> l10n("updates.3.text.3", "home", user_lang),
<li>{{ l10n("updates.text.privacy", "home", user_lang) }}</li> l10n("updates.text.privacy", "home", user_lang)
</ul> ]) }}
<p><i class="fad fa-calendar-alt mr-xs"></i>{{ l10n("updates.2.date", "home", user_lang) }}</p> <p><i class="fad fa-calendar-alt mr-xs mt-s"></i>{{ l10n("updates.2.date", "home", user_lang) }}</p>
<ul> {{ render_list_ul([
<li>{{ l10n("updates.2.text.1", "home", user_lang) }}</li> l10n("updates.2.text.1", "home", user_lang),
<li>{{ l10n("updates.2.text.2", "home", user_lang) }}</li> l10n("updates.2.text.2", "home", user_lang),
<li>{{ l10n("updates.2.text.3", "home", user_lang) }}</li> l10n("updates.2.text.3", "home", user_lang),
<li>{{ l10n("updates.2.text.4", "home", user_lang) }}</li> l10n("updates.2.text.4", "home", user_lang),
<li>{{ l10n("updates.text.privacy", "home", user_lang) }}</li> l10n("updates.text.privacy", "home", user_lang)
</ul> ]) }}
<p><i class="fad fa-calendar-alt mr-xs mt-s"></i>{{ l10n("updates.1.date", "home", user_lang) }}</p>
{{ render_list_ul([
l10n("updates.1.text.1", "home", user_lang),
l10n("updates.text.privacy", "home", user_lang)
]) }}
<p><i class="fad fa-calendar-alt mr-xs"></i>{{ l10n("updates.1.date", "home", user_lang) }}</p>
<ul>
<li>{{ l10n("updates.1.text.1", "home", user_lang) }}</li>
<li>{{ l10n("updates.text.privacy", "home", user_lang) }}</li>
</ul>
{% endblock %} {% endblock %}

View File

@@ -4,9 +4,7 @@
{% block head_description %}{{ l10n(tool_data.metadata.head.description_key, tool_id, user_lang) }}{% endblock %} {% block head_description %}{{ l10n(tool_data.metadata.head.description_key, tool_id, user_lang) }}{% endblock %}
{% block extra_stylesheets %} {% block extra_stylesheets %}
{% for tool_stylesheet in tool_data.data.stylesheets %} {{ render_applet_head(applet_data) }}
<link rel="stylesheet" href="{{ url_for("static", filename="/resources/NibblePoker/tools/" + tool_id + "/" + tool_stylesheet) }}">
{% endfor %}
{% endblock %} {% endblock %}
{% block header_title %} {% block header_title %}
@@ -21,16 +19,10 @@
) }} ) }}
<div class="px-xxs"> <div class="px-xxs">
{% block tool_content %}{% endblock %} {% include 'applets/'+applet_data.id+'.jinja' %}
</div> </div>
{% endblock %} {% endblock %}
{% block extra_scripts %} {% block extra_scripts %}
{% for tool_script in tool_data.data.scripts %} {{ render_applet_scripts(applet_data) }}
{% if tool_script.endswith(".mjs") %}
<script src="{{ url_for("static", filename="/resources/NibblePoker/tools/" + tool_id + "/" + tool_script) }}" type="module"></script>
{% else %}
<script src="{{ url_for("static", filename="/resources/NibblePoker/tools/" + tool_id + "/" + tool_script) }}"></script>
{% endif %}
{% endfor %}
{% endblock %} {% endblock %}

View File

@@ -38,4 +38,5 @@
<script src="https://cdn.nibblepoker.lu/HighlightJS/11.9.0-custom/highlight.min.js"></script> <script src="https://cdn.nibblepoker.lu/HighlightJS/11.9.0-custom/highlight.min.js"></script>
<script src="{{ url_for("static", filename="/resources/NibblePoker/js/nibblepoker-splide.min.js") }}"></script> <script src="{{ url_for("static", filename="/resources/NibblePoker/js/nibblepoker-splide.min.js") }}"></script>
<script src="{{ url_for("static", filename="/resources/NibblePoker/js/nibblepoker-code.min.js") }}"></script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,131 @@
{% extends "projects/_project.jinja" %}
{% block project_content %}
{{ render_h2(l10n("intro.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("intro.1", project_id, user_lang)) }}
{{ render_paragraph(l10n("intro.2", project_id, user_lang)) }}
{{ render_h2(l10n("preamble.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("preamble.1", project_id, user_lang)) }}
{{ render_paragraph(l10n("preamble.2", project_id, user_lang)) }}
{{ render_h2(l10n("setup.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.1", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.2", project_id, user_lang)) }}
{{ render_h3(l10n("setup.camera.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.camera.1", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.camera.2", project_id, user_lang)) }}
{{ render_h4(l10n("commons.example", project_id, user_lang)) }}
{{
render_code_block([
"nvr_stack:",
" cctv_recorder_cam1:",
" container_name: cctv-recorder-cam1",
" build:",
" context: .",
" dockerfile: Dockerfile_recorder",
" environment:",
" - TZ=Europe/Brussels",
" - \"NP_CCTV_URL=rtsp://user:password@address:554/sub-path\"",
" volumes:",
" - ./recordings/cam1:/data",
" restart: unless-stopped",
], "yaml")
}}
{{ render_paragraph(l10n("setup.camera.3", project_id, user_lang)) }}
{{ render_h3(l10n("setup.cleaner.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.cleaner.1", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.cleaner.2", project_id, user_lang)) }}
{{ render_h4(l10n("commons.example", project_id, user_lang)) }}
{{
render_code_block([
"nvr_stack:",
" cctv_cleaner:",
" container_name: cctv-cleaner",
" build:",
" context: .",
" dockerfile: Dockerfile_cleaner",
" environment:",
" - TZ=Europe/Brussels",
" - \"NP_MAX_FILE_AGE_HOURS=72\"",
" volumes:",
" - ./recordings:/data",
" - ./cleaner.py:/app/app.py:ro",
" restart: unless-stopped",
], "yaml")
}}
{{ render_h3(l10n("setup.web.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.web.1", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.web.2", project_id, user_lang)) }}
{{ render_paragraph(l10n("setup.web.3", project_id, user_lang)) }}
{{ render_h4(l10n("commons.web.vars.title", project_id, user_lang)) }}
{{ render_h4(l10n("commons.example", project_id, user_lang)) }}
{{
render_code_block([
"nvr_stack:",
" cctv_web:",
" container_name: cctv-web",
" image: php:apache",
" ports:",
" - 26880:80",
" environment:",
" - TZ=Europe/Brussels",
" - \"NP_CAM_cam1=Camera #1\"",
" - \"NP_CAM_cam2=Camera #2\"",
" - \"NP_TITLE=NibblePoker's Mini CCTV NVR\"",
" - \"NP_FOOTER=Made by <i>BOZET Herwin</i>\"",
" volumes:",
" - ./htdocs:/var/www/html # Cannot be \":ro\" since the recordings are mounted into it.",
" - ./apache2.conf:/etc/apache2/apache2.conf:ro",
" - ./recordings:/var/www/html/data:ro",
" restart: unless-stopped",
], "yaml")
}}
{{ render_h2(l10n("startup.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("startup.1", project_id, user_lang)) }}
{{
render_code_block([
"docker-compose up --build -d",
], "bash")
}}
{{ render_h2(l10n("screenshots.title", project_id, user_lang)) }}
{{ render_splide([
'<img src="' + url_for("static", filename="/resources/NibblePoker/images/content/" + project_id + "/home.png") + '">',
'<img src="' + url_for("static", filename="/resources/NibblePoker/images/content/" + project_id + "/cam.png") + '">',
]) }}
{{ render_h2(l10n("statistics.title", project_id, user_lang)) }}
{{ render_list_ul([
l10n("statistics.1", project_id, user_lang),
[
l10n("statistics.1.1", project_id, user_lang),
],
l10n("statistics.2", project_id, user_lang),
[
l10n("statistics.2.1", project_id, user_lang),
l10n("statistics.2.2", project_id, user_lang),
[
l10n("statistics.2.2.1", project_id, user_lang),
],
],
l10n("statistics.3", project_id, user_lang),
[
l10n("statistics.3.1", project_id, user_lang),
l10n("statistics.3.2", project_id, user_lang),
l10n("statistics.3.3", project_id, user_lang),
],
l10n("statistics.4", project_id, user_lang),
[
l10n("statistics.4.1", project_id, user_lang),
l10n("statistics.4.2", project_id, user_lang),
],
]) }}
{{ render_h2(l10n("license.title", project_id, user_lang)) }}
{{ render_paragraph(l10n("license.1", project_id, user_lang)) }}
{% endblock %}

View File

@@ -1,38 +0,0 @@
{% extends "tools/_tool.jinja" %}
{% block tool_content %}
<div class="px-xs mt-s">
<label for="uuid-generator-option-type" class="mr-xs">{{ l10n("type.label", tool_id, user_lang) }}:</label>
<select name="uuid-generator-option-type" id="uuid-generator-option-type" class="p-xxs border r-s">
<option value="type-uuid4" selected>{{ l10n("type.uuid4", tool_id, user_lang) }}</option>
<option value="type-guid">{{ l10n("type.guid", tool_id, user_lang) }}</option>
</select>
<br>
<label for="uuid-generator-option-count" class="mr-xs">{{ l10n("option.count", tool_id, user_lang) }}:</label>
<input id="uuid-generator-option-count" class="p-xxs border r-s" type="number" value="4" min="1" max="1000">
<br>
<label for="uuid-generator-option-hyphens" class="mr-xxs">{{ l10n("option.hyphen", tool_id, user_lang) }}:</label>
<input id="uuid-generator-option-hyphens" type="checkbox" checked>
<hr class="subtle">
<button id="uuid-generator-generate" class="p-xs r-s border b-light success">
<i class="fa-duotone fa-solid fa-gears mr-xs"></i>{{ l10n("generate", tool_id, user_lang) }}
</button>
<button id="uuid-generator-download" class="p-xs r-s border b-light primary">
<i class="fa-duotone fa-solid fa-download mr-xs"></i>{{ l10n("download", tool_id, user_lang) }}
</button>
<hr class="subtle">
<label for="uuid-generator-preview" class="d-none">{{ l10n("preview.label", tool_id, user_lang) }}:</label>
<textarea name="uuid-generator-preview" id="uuid-generator-preview" rows="16" class="w-full border r-s"></textarea>
</div>
{% endblock %}

View File

@@ -5,31 +5,52 @@ from typing import Any
from locked_dict.locked_dict import LockedDict from locked_dict.locked_dict import LockedDict
import yaml import yaml
from .metadata import ContentMetadata from .dataclasses import *
from .project import ContentProject
from .tool import ContentTool, ContentToolData
__CONTENT: ContentRoot = ContentRoot()
__CONTENT_APPLETS: LockedDict[str, ContentApplet] = LockedDict()
__CONTENT_ARTICLES: LockedDict = LockedDict() __CONTENT_ARTICLES: LockedDict = LockedDict()
__CONTENT_PROJECTS: LockedDict[str, ContentProject] = LockedDict() __CONTENT_PROJECTS: LockedDict[str, ContentProject] = LockedDict()
__CONTENT_TOOLS: LockedDict[str, ContentTool] = LockedDict() __CONTENT_TOOLS: LockedDict[str, ContentTool] = LockedDict()
def get_content() -> ContentRoot:
return __CONTENT
def get_applets() -> LockedDict[str, ContentApplet]:
return __CONTENT.applets
def get_articles() -> LockedDict: def get_articles() -> LockedDict:
return __CONTENT_ARTICLES return __CONTENT_ARTICLES
def get_projects() -> LockedDict[str, ContentProject]: def get_projects() -> LockedDict[str, ContentProject]:
return __CONTENT_PROJECTS return __CONTENT.projects
def get_projects_by_tags(tags: list[str]) -> dict[Any, ContentProject]: def get_projects_by_tags(tags: list[str]) -> dict[Any, ContentProject]:
project_obj: ContentProject project_obj: ContentProject
return { return {
project_key: project_value for project_key, project_value in __CONTENT_PROJECTS.items() project_key: project_value for project_key, project_value in __CONTENT.projects.items()
if any(tag in project_value.metadata.general.tags for tag in tags) if any(tag in project_value.metadata.general.tags for tag in tags)
} }
def get_tools() -> LockedDict[str, ContentTool]:
return __CONTENT.tools
def get_tools_by_tags(tags: list[str]) -> dict[Any, ContentProject]:
tool_obj: ContentProject
return {
tool_key: tool_value for tool_key, tool_value in __CONTENT.tools.items()
if any(tag in tool_value.metadata.general.tags for tag in tags)
}
def sanitize_input_tags(input_tags: str) -> list[str]: def sanitize_input_tags(input_tags: str) -> list[str]:
tags: list[str] = input_tags.split(";") tags: list[str] = input_tags.split(";")
for tag in tags: for tag in tags:
@@ -38,34 +59,50 @@ def sanitize_input_tags(input_tags: str) -> list[str]:
return tags return tags
def get_tools() -> LockedDict: def load_content_items() -> None:
return __CONTENT_TOOLS global __CONTENT
__CONTENT = ContentRoot()
def get_tools_by_tags(tags: list[str]) -> dict[Any, ContentProject]: # Loading applets definition files
tool_obj: ContentProject for applets_file in os.listdir(os.path.join(os.getcwd(), "data/applets")):
return { applets_file_path = os.path.join(os.getcwd(), "data/applets", applets_file)
tool_key: tool_value for tool_key, tool_value in __CONTENT_TOOLS.items() if not os.path.isfile(applets_file_path) or applets_file.startswith("."):
if any(tag in tool_value.metadata.general.tags for tag in tags) continue
}
applets_data = yaml.safe_load(open(applets_file_path))
if "applets" not in applets_data:
print(f"Unable to load '{applets_file_path}' due to missing 'applets' field !")
continue
def reload_content_items() -> None: for applet_data in applets_data["applets"]:
global __CONTENT_ARTICLES _applet = ContentApplet(**applet_data)
global __CONTENT_PROJECTS __CONTENT.applets[_applet.id] = _applet
global __CONTENT_TOOLS
__CONTENT_ARTICLES = LockedDict() # Loading articles definition files
__CONTENT_PROJECTS = LockedDict() """for article_folder in os.listdir(os.path.join(os.getcwd(), "data/articles")):
__CONTENT_TOOLS = LockedDict()
for article_folder in os.listdir(os.path.join(os.getcwd(), "data/articles")):
article_folder_path = os.path.join(os.getcwd(), "data/articles", article_folder) article_folder_path = os.path.join(os.getcwd(), "data/articles", article_folder)
if not os.path.isdir(article_folder_path): if not os.path.isdir(article_folder_path):
continue continue
pass pass"""
for project_item in os.listdir(os.path.join(os.getcwd(), "data/projects")): # Loading projects definition files
for project_file in os.listdir(os.path.join(os.getcwd(), "data/projects")):
project_file_path = os.path.join(os.getcwd(), "data/projects", project_file)
if not os.path.isfile(project_file_path) or project_file.startswith("."):
continue
projects_data = yaml.safe_load(open(project_file_path))
if "projects" not in projects_data:
print(f"Unable to load '{project_file_path}' due to missing 'projects' field !")
continue
for project_data in projects_data["projects"]:
_project = ContentProject(**project_data)
__CONTENT.projects[_project.id] = _project
print(_project)
"""for project_item in os.listdir(os.path.join(os.getcwd(), "data/projects")):
project_item_path = os.path.join(os.getcwd(), "data/projects/", project_item) project_item_path = os.path.join(os.getcwd(), "data/projects/", project_item)
if not os.path.isfile(project_item_path) or project_item.startswith("."): if not os.path.isfile(project_item_path) or project_item.startswith("."):
continue continue
@@ -87,33 +124,25 @@ def reload_content_items() -> None:
print(f"Loaded project '{project_id}'") print(f"Loaded project '{project_id}'")
except Exception as e: except Exception as e:
print(f"Unable to load project '{project_id}' due to an exception !") print(f"Unable to load project '{project_id}' due to an exception !")
print(e) print(e)"""
for tool_item in os.listdir(os.path.join(os.getcwd(), "data/tools")): for tools_file in os.listdir(os.path.join(os.getcwd(), "data/tools")):
tool_item_path = os.path.join(os.getcwd(), "data/tools", tool_item) tools_file_path = os.path.join(os.getcwd(), "data/tools", tools_file)
if not os.path.isfile(tool_item_path) or tool_item_path.startswith("."): if not os.path.isfile(tools_file_path) or tools_file.startswith("."):
continue continue
tool_id = Path(tool_item_path).stem tools_data = yaml.safe_load(open(tools_file_path))
tool_page_path = os.path.join(os.getcwd(), f"templates/tools/{tool_id}.jinja") if "tools" not in tools_data:
print(f"Unable to load '{tools_file_path}' due to missing 'tools' field !")
if not all(os.path.isfile(project_file) for project_file in
[tool_item_path, tool_page_path]):
print(f"Unable to load tool '{tool_id}' due to missing files !")
continue continue
tool_data: ContentTool for tool_data in tools_data["tools"]:
try: _tool = ContentTool(**tool_data)
raw_tool_data = yaml.safe_load(open(tool_item_path)) __CONTENT.tools[_tool.id] = _tool
__CONTENT_TOOLS[tool_id] = ContentTool( #print(_tool)
id=tool_id,
metadata=ContentMetadata(**raw_tool_data["metadata"]),
data=ContentToolData(**raw_tool_data["data"]),
)
print(f"Loaded tool '{tool_id}'")
except Exception as e:
print(f"Unable to load tool '{tool_id}' due to an exception !")
print(e)
continue
# FIXME: Check if the required files exist too ! # FIXME: Check if the required files exist too !"""
def validate_content_items() -> bool:
pass

View File

@@ -1,6 +1,8 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional from typing import Optional
from locked_dict.locked_dict import LockedDict
@dataclass @dataclass
class ContentHeadMetadata: class ContentHeadMetadata:
@@ -66,3 +68,49 @@ class ContentMetadata:
self.general: dict self.general: dict
self.general = ContentGeneralMetadata(**self.general) self.general = ContentGeneralMetadata(**self.general)
@dataclass
class ContentResource:
scripts: list[str] = field(default_factory=list)
stylesheets: list[str] = field(default_factory=list)
@dataclass
class ContentApplet:
id: str
resources: ContentResource
def __post_init__(self):
self.resources: dict
self.resources = ContentResource(**self.resources)
@dataclass
class ContentTool:
# NOTE: could extend `ContentProject`
id: str
applet_id: str
metadata: ContentMetadata
def __post_init__(self):
self.metadata: dict
self.metadata = ContentMetadata(**self.metadata)
@dataclass
class ContentProject:
id: str
metadata: ContentMetadata
def __post_init__(self):
self.metadata: dict
self.metadata = ContentMetadata(**self.metadata)
@dataclass
class ContentRoot:
applets: LockedDict[str, ContentApplet] = field(default_factory=LockedDict)
# articles: list[Con] = field(default_factory=list)
projects: LockedDict[str, ContentProject] = field(default_factory=LockedDict)
tools: LockedDict[str, ContentTool] = field(default_factory=LockedDict)

View File

@@ -1,9 +0,0 @@
from dataclasses import dataclass
from .metadata import ContentMetadata
@dataclass
class ContentProject:
id: str
metadata: ContentMetadata

View File

@@ -1,16 +0,0 @@
from dataclasses import dataclass, field
from .metadata import ContentMetadata
@dataclass
class ContentToolData:
scripts: list[str] = field(default_factory=list)
stylesheets: list[str] = field(default_factory=list)
@dataclass
class ContentTool:
id: str
metadata: ContentMetadata
data: ContentToolData

View File

@@ -0,0 +1,29 @@
from flask import url_for
from website.content import ContentApplet
def render_applet_head(applet_data: ContentApplet) -> str:
applet_style_html = ""
for applet_style in applet_data.resources.stylesheets:
applet_style_html += ("<link rel='stylesheet' href='" +
url_for(
"static",
filename="/resources/NibblePoker/applets/" + applet_data.id + "/" + applet_style) +
"'>")
return applet_style_html
def render_applet_scripts(applet_data: ContentApplet):
applet_script_html = ""
for applet_script in applet_data.resources.scripts:
applet_script_html += ("<script src='" +
url_for(
"static",
filename="/resources/NibblePoker/applets/" + applet_data.id + "/" + applet_script) +
"'" + (" type='module'" if applet_script.endswith(".mjs") else "") + "></script>")
return applet_script_html

31
website/renderers/code.py Normal file
View File

@@ -0,0 +1,31 @@
import html
import re
from typing import Optional
from flask import render_template
def render_code_block(code_lines: list[str], language: Optional[str] = None):
_code_lines = list()
for code_line in code_lines:
code_line = html.escape(code_line)
code_line = code_line.replace('\t', '&nbsp;' * 4)
code_line = code_line.replace(' ', '&nbsp;')
_code_lines.append(code_line)
return render_template(
"elements/code.jinja",
code_lines=_code_lines,
code_language=language,
)
# return re.sub('>\s*<span', "><span",
# re.sub('<br>\s*</code>', "</code>",
# render_template(
# "elements/code.jinja",
# code_lines=_code_lines,
# code_language=language,
# )
# )
# )

View File

@@ -0,0 +1,3 @@
def render_file_input() -> str:
return ""

View File

@@ -35,3 +35,11 @@ def render_h3(inner_html: str, icon: Optional[str] = None, right_html: Optional[
return render_heading( return render_heading(
inner_html, 3, icon, right_html, anchor_id, background_class inner_html, 3, icon, right_html, anchor_id, background_class
) )
def render_h4(inner_html: str, icon: Optional[str] = None, right_html: Optional[str] = None,
anchor_id: Optional[str] = None, background_class: str = "bkgd-grid") -> str:
return render_heading(
inner_html, 4, icon, right_html, anchor_id, background_class
)

View File

@@ -0,0 +1,11 @@
from typing import Union
from flask import render_template
def render_list_ul(items: list[Union[str|list]]) -> str:
return render_template(
"elements/list-ul.jinja",
list_items=items,
render_list_ul=render_list_ul
)