diff --git a/scripts/template-generator.py b/scripts/template-generator.py
new file mode 100644
index 0000000..1c2895a
--- /dev/null
+++ b/scripts/template-generator.py
@@ -0,0 +1,79 @@
+#!/bin/python3
+
+import os
+import json
+import shutil
+import logging
+import argparse
+
+args_parser = argparse.ArgumentParser(description="Generator Template Generator")
+args_parser.add_argument("config", type=str, help="config json path", nargs="?")
+args = args_parser.parse_args()
+
+if args.config:
+ config = None
+ with open(args.config, "r") as f:
+ config = json.loads(f.read())
+
+ name = config["name"]
+ desc = config["desc"]
+ title = config["title"]
+ repo_name = config["repo_name"]
+
+else:
+ name = input("Generator name (like fortune, quote): ")
+ desc = input("Generator desc: ")
+ title = input("Generator title: ")
+ repo_name = input("Github repo name: ")
+
+folder_path = f"{name}_generator"
+
+if os.path.exists(folder_path):
+ logging.error(f"{folder_path} already exists. Please choose another name.")
+ exit(1)
+
+os.mkdir(folder_path)
+os.mkdir(f"{folder_path}/css")
+os.mkdir(f"{folder_path}/js")
+os.mkdir(f"{folder_path}/images")
+os.mkdir(f"{folder_path}/json")
+
+
+def write_file(src_path, dst_path, **kwargs):
+ content = None
+ with open(f"scripts/template/{src_path}", "r") as f:
+ content = f.read()
+
+ for key, val in kwargs.items():
+ assert (
+ content.find("{{ %s }}" % key) != -1
+ ), f"The key '{key}' does not appear in scripts/template/{src_path}"
+ content = content.replace("{{ %s }}" % key, val)
+
+ with open(dst_path, "w") as f:
+ f.write(content)
+
+
+write_file("css/styles.css", f"{folder_path}/css/styles.css")
+write_file("js/main.js", f"{folder_path}/js/{name}.js", name=name)
+write_file("js/matrix.js", f"{folder_path}/js/matrix.js")
+write_file("js/scripts.js", f"{folder_path}/js/scripts.js")
+write_file("js/theme.js", f"{folder_path}/js/theme.js")
+write_file(
+ "js/service-worker.js",
+ f"{folder_path}/js/service-worker.js",
+ name=name,
+ repo_name=repo_name,
+ folder_path=folder_path,
+)
+write_file(
+ "manifest.json",
+ f"{folder_path}/manifest.json",
+ title=title,
+ desc=desc,
+ repo_name=repo_name,
+ folder_path=folder_path,
+)
+write_file("json/themes.json", f"{folder_path}/json/themes.json")
+write_file("index.html", f"{folder_path}/index.html", name=name, desc=desc, title=title)
+shutil.copytree("scripts/template/images", f"{folder_path}/images", dirs_exist_ok=True)
diff --git a/scripts/template/css/styles.css b/scripts/template/css/styles.css
new file mode 100644
index 0000000..5491a66
--- /dev/null
+++ b/scripts/template/css/styles.css
@@ -0,0 +1,135 @@
+:root {
+ --button-color: #73a3eb;
+ --button-hover-color: #459aef;
+ --toggle-theme-button-color: #000000;
+ --copy-result-button-color: #000000;
+ --bg-color: #ffffff;
+ --title-color: #000000cc;
+}
+
+* {
+ overflow: hidden;
+ text-align: center;
+ white-space: nowrap;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ align-items: center;
+ justify-content: center;
+}
+
+.container {
+ top: 50%;
+ left: 50%;
+ width: 80%;
+ max-width: 800px;
+ position: absolute;
+ z-index: 1;
+ text-align: center;
+ transform: translate(-50%, -50%);
+ background-color: var(--bg-color);
+ border-radius: 40px;
+ padding: 10px;
+}
+
+
+button {
+ background-color: var(--button-color);
+ color: var(--bg-color);
+ z-index: 2;
+ font-size: 20px;
+ border: none;
+ padding: 20px 20px;
+ border-radius: 30px;
+ cursor: pointer;
+ transition: all 0.3s ease-in-out;
+}
+
+button:hover {
+ background-color: var(--button-hover-color);
+}
+
+#Matrix {
+ z-index: 0;
+}
+
+#toggle-theme-button {
+ margin-top: 15px;
+ font-size: 2.4rem;
+ color: var(--toggle-theme-button-color);
+ cursor: pointer;
+ opacity: 85%;
+}
+
+#copy-result-button {
+ margin-top: 20px;
+ font-size: 1.5rem;
+ color: var(--copy-result-button-color);
+}
+
+#themeModal {
+ .modal-content {
+ background-color: var(--bg-color) !important;
+ color: var(--title-color) !important;
+ }
+
+ .modal-header,
+ .modal-footer {
+ background-color: var(--bg-color) !important;
+ color: var(--bg-color) !important;
+ }
+
+ .modal-title,
+ .btn-close {
+ color: var(--title-color) !important;
+ }
+}
+
+#themeItem {
+ background-color: var(--bg-color);
+ color: var(--title-color);
+}
+
+::-webkit-scrollbar {
+ width: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg-color);
+ border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--button-color);
+ border-radius: 10px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--button-hover-color);
+}
+
+.color-preview-container {
+ display: flex;
+ align-items: center;
+ padding: 3px;
+ border-radius: 25px;
+}
+
+.color-preview {
+ display: flex; /* Use flex to align dots in a row */
+}
+
+.color-dot {
+ display: inline-block;
+ width: 12px; /* Dot size */
+ height: 12px; /* Dot size */
+ border-radius: 50%; /* Circular shape */
+ margin-left: 5px; /* Spacing between dots */
+}
+
+.color-preview .color-dot:first-child {
+ margin-left: 0; /* No margin on the left for the first dot */
+}
diff --git a/scripts/template/images/logo-180x180.png b/scripts/template/images/logo-180x180.png
new file mode 100644
index 0000000..d97340e
Binary files /dev/null and b/scripts/template/images/logo-180x180.png differ
diff --git a/scripts/template/images/logo-192x192.png b/scripts/template/images/logo-192x192.png
new file mode 100644
index 0000000..cce9ff4
Binary files /dev/null and b/scripts/template/images/logo-192x192.png differ
diff --git a/scripts/template/images/logo-270x270.png b/scripts/template/images/logo-270x270.png
new file mode 100644
index 0000000..b2dc9d1
Binary files /dev/null and b/scripts/template/images/logo-270x270.png differ
diff --git a/scripts/template/images/logo-512x512.png b/scripts/template/images/logo-512x512.png
new file mode 100644
index 0000000..b24d70e
Binary files /dev/null and b/scripts/template/images/logo-512x512.png differ
diff --git a/scripts/template/images/logo.png b/scripts/template/images/logo.png
new file mode 100644
index 0000000..6ffb041
Binary files /dev/null and b/scripts/template/images/logo.png differ
diff --git a/scripts/template/index.html b/scripts/template/index.html
new file mode 100644
index 0000000..f60ace0
--- /dev/null
+++ b/scripts/template/index.html
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/template/js/main.js b/scripts/template/js/main.js
new file mode 100644
index 0000000..3e8fc37
--- /dev/null
+++ b/scripts/template/js/main.js
@@ -0,0 +1,15 @@
+function InitPage() {
+ $("#init-page").show();
+ $("#result-page").hide();
+}
+
+function Appear() {
+ $("#init-page").hide();
+ $("#result-page").show();
+}
+
+function get{{ name }}() {
+ Update();
+}
+
+InitPage()
diff --git a/scripts/template/js/matrix.js b/scripts/template/js/matrix.js
new file mode 100644
index 0000000..0721d27
--- /dev/null
+++ b/scripts/template/js/matrix.js
@@ -0,0 +1,51 @@
+const canvas = document.getElementById("Matrix");
+const context = canvas.getContext("2d");
+
+canvas.height = globalThis.innerHeight + 100;
+canvas.width = globalThis.innerWidth + 5;
+
+const chars =
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./*-+#$%^@!~?><:;[]{}=_αβΓγΔδεζηΘθικΛλμΞξΠπρΣσςτυΦφχΨψΩω×≦≧≠∞≒≡~∩∠∪∟⊿∫∮∵∴$¥〒¢£℃€℉╩◢ⅩⅨⅧⅦⅥⅤⅣⅢⅡⅠあいうえおがぎぐげござじずぜぞだぢつでづどにぬのばひぴぶへぺぼみゃょァゐゎè";
+
+const fontSize = 16;
+const columns = canvas.width / fontSize;
+
+const charArr = [];
+for (let i = 0; i < columns; i++) {
+ charArr[i] = 1;
+}
+
+let frame = 0;
+let str;
+
+context.fillStyle = "rgba(0, 0, 0, 1)";
+context.fillRect(0, 0, canvas.width, canvas.height);
+
+function Update() {
+ context.fillStyle = "rgba(0, 0, 0, 0.05)";
+ context.fillRect(0, 0, canvas.width, canvas.height);
+
+ if (frame == 0) {
+ const a = parseInt(Math.random() * 255);
+ str = `rgba(${a}, ${Math.abs(a - 127)}, ${Math.abs(a - 255)}, 0.9)`;
+ }
+ context.fillStyle = str;
+ context.font = fontSize + "px monospace";
+
+ for (let i = 0; i < columns; i++) {
+ const text = chars[Math.floor(Math.random() * chars.length)];
+ context.fillText(text, i * fontSize, charArr[i] * fontSize);
+ if (charArr[i] * fontSize > canvas.height && Math.random() > 0.90) {
+ charArr[i] = 0;
+ }
+ charArr[i]++;
+ }
+ frame++;
+
+ if (frame <= 40 * (Math.floor(Math.random() * 10) + 3)) {
+ requestAnimationFrame(Update); // 40 frames a cycle
+ } else {
+ frame = 0;
+ Appear();
+ }
+}
diff --git a/scripts/template/js/scripts.js b/scripts/template/js/scripts.js
new file mode 100644
index 0000000..20038d4
--- /dev/null
+++ b/scripts/template/js/scripts.js
@@ -0,0 +1,45 @@
+function copyResultImageToClipboard() {
+ try {
+ const root = document.documentElement;
+ const backgroundColor = getComputedStyle(root).getPropertyValue('--bg-color');
+
+ htmlToImage.toBlob($("#result-page")[0], {
+ skipFonts: true,
+ preferredFontFormat: "woff2",
+ backgroundColor: backgroundColor, // Set background color dynamically
+ }).then((blob) => {
+ navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
+ showCopiedNotice();
+ $title.parent().remove();
+ }).catch((error) => {
+ console.error("Error converting result page to image:", error);
+ $title.parent().remove();
+ });
+ } catch (error) {
+ console.error("Error copying result image to clipboard:", error);
+ }
+}
+
+function showCopiedNotice() {
+ const notice = $("", {
+ text: "Copied to clipboard!",
+ css: {
+ position: "fixed",
+ bottom: "20px",
+ right: "20px",
+ padding: "10px 20px",
+ backgroundColor: "rgba(0, 0, 0, 0.7)",
+ color: "#fff",
+ borderRadius: "5px",
+ zIndex: 1000,
+ },
+ });
+
+ $("body").append(notice);
+
+ setTimeout(() => {
+ notice.fadeOut(300, () => {
+ notice.remove();
+ });
+ }, 3000);
+}
diff --git a/scripts/template/js/service-worker.js b/scripts/template/js/service-worker.js
new file mode 100644
index 0000000..5a63216
--- /dev/null
+++ b/scripts/template/js/service-worker.js
@@ -0,0 +1,102 @@
+const pre_cache_file_version = "pre-v1.0.0";
+const auto_cache_file_version = "auto-v1.0.0";
+
+const ASSETS = [
+ "/{{ repo_name }}/{{ folder_path }}/images/logo-192x192.png",
+ "/{{ repo_name }}/{{ folder_path }}/images/logo-512x512.png",
+ "/{{ repo_name }}/{{ folder_path }}/images/logo-180x180.png",
+ "/{{ repo_name }}/{{ folder_path }}/images/logo-270x270.png",
+ "/{{ repo_name }}/{{ folder_path }}/images/logo.jpg",
+
+ "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css",
+ "https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js",
+];
+
+const NEED_UPDATE = [
+ "/{{ repo_name }}/{{ folder_path }}/",
+ "/{{ repo_name }}/{{ folder_path }}/index.html",
+ "/{{ repo_name }}/{{ folder_path }}/css/styles.css",
+ "/{{ repo_name }}/{{ folder_path }}/js/{{ name }}.js",
+ "/{{ repo_name }}/{{ folder_path }}/js/matrix.js",
+ "/{{ repo_name }}/{{ folder_path }}/json/theme.json",
+ "/{{ repo_name }}/{{ folder_path }}/json/manifest.json",
+];
+
+const limit_cache_size = (name, size) => {
+ caches.open(name).then((cache) => {
+ cache.keys().then((keys) => {
+ if (keys.length > size) {
+ cache.delete(keys[0]).then(() => {
+ limit_cache_size(name, size);
+ });
+ }
+ });
+ });
+};
+
+const is_in_array = (str, array) => {
+ let path = "";
+
+ // Check the request's domain is the same as the current domain.
+ if (str.indexOf(self.origin) === 0) {
+ path = str.substring(self.origin.length); // Remove https://lifeadventurer.github.io
+ } else {
+ path = str; // outside request
+ }
+
+ return array.indexOf(path) > -1;
+};
+
+// install
+self.addEventListener("install", (event) => {
+ self.skipWaiting();
+
+ //pre-cache files
+ event.waitUntil(
+ caches.open(pre_cache_file_version).then((cache) => {
+ cache.addAll(ASSETS);
+ }),
+ );
+});
+
+// activate
+self.addEventListener("activate", (event) => {
+ event.waitUntil(
+ caches.keys().then((keys) => {
+ return Promise.all(keys.map((key) => {
+ if (
+ pre_cache_file_version.indexOf(key) === -1 &&
+ auto_cache_file_version.indexOf(key) === -1
+ ) {
+ return caches.delete(key);
+ }
+ }));
+ }),
+ );
+});
+
+// fetch event
+self.addEventListener("fetch", (event) => {
+ if (is_in_array(event.request.url, ASSETS)) {
+ // cache only strategy
+
+ event.respondWith(
+ caches.match(event.request.url),
+ );
+ } else if (is_in_array(event.request.url, NEED_UPDATE)) {
+ event.respondWith(
+ fetch(event.request.url).then(async (response) => {
+ if (response.ok) {
+ const cache = await caches.open(auto_cache_file_version);
+ cache.put(event.request.url, response.clone());
+ return response;
+ }
+
+ throw new Error("Network response was not ok.");
+ }).catch(async (_error) => {
+ const cache = await caches.open(auto_cache_file_version);
+ return cache.match(event.request.url);
+ }),
+ );
+ }
+});
diff --git a/scripts/template/js/theme.js b/scripts/template/js/theme.js
new file mode 100644
index 0000000..74d3a10
--- /dev/null
+++ b/scripts/template/js/theme.js
@@ -0,0 +1,91 @@
+document.addEventListener("DOMContentLoaded", () => {
+ const themeListContainer = document.querySelector("#themeList");
+ const root = document.documentElement;
+
+ // Apply the saved theme if it exists
+ applySavedTheme();
+
+ async function fetchThemes() {
+ try {
+ const response = await fetch("./json/themes.json");
+ const themes = await response.json();
+ populateThemeList(themes["themes"]);
+ } catch (error) {
+ console.error("Error fetching themes:", error);
+ }
+ }
+
+ // Populate theme list in modal
+ function populateThemeList(themes) {
+ themeListContainer.innerHTML = "";
+ themes.forEach((theme) => {
+ const themeItem = document.createElement("div");
+ themeItem.className =
+ "theme-item list-group-item d-flex justify-content-between align-items-center";
+ themeItem.style.cursor = "pointer";
+ themeItem.id = "themeItem";
+
+ // Add theme name
+ const themeName = document.createElement("span");
+ themeName.textContent = theme.name;
+ themeItem.appendChild(themeName);
+
+ const colorPreivewContainer = document.createElement("div");
+ colorPreivewContainer.className = "color-preview-container";
+
+ const propertyKeys = Object.keys(theme.properties);
+ colorPreivewContainer.style.backgroundColor =
+ theme.properties[propertyKeys[5]];
+
+ // Add color dots for visual preview
+ const colorPreview = document.createElement("div");
+ colorPreview.className = "color-preview";
+
+ Object.values(theme.properties).slice(0, 3).forEach((color) => {
+ const colorDot = document.createElement("span");
+ colorDot.style.backgroundColor = color;
+ colorDot.className = "color-dot";
+ colorPreview.appendChild(colorDot);
+ });
+
+ colorPreivewContainer.appendChild(colorPreview);
+ themeItem.appendChild(colorPreivewContainer);
+
+ // Apply theme on click
+ themeItem.addEventListener("click", () => {
+ applyTheme(theme.properties);
+ saveThemeToLocalStorage(theme.name);
+ });
+
+ themeListContainer.appendChild(themeItem);
+ });
+ }
+
+ // Apply theme by setting CSS variables
+ function applyTheme(properties) {
+ Object.entries(properties).forEach(([key, value]) => {
+ root.style.setProperty(`--${key}`, value);
+ });
+ }
+
+ function saveThemeToLocalStorage(themeName) {
+ localStorage.setItem("selectedTheme", themeName);
+ }
+
+ function applySavedTheme() {
+ const savedThemeName = localStorage.getItem("selectedTheme");
+ if (savedThemeName) {
+ fetch("./json/themes.json")
+ .then((response) => response.json())
+ .then((themes) => {
+ const theme = themes.themes.find((t) => t.name === savedThemeName);
+ if (theme) {
+ applyTheme(theme.properties);
+ }
+ })
+ .catch((error) => console.error("Error fetching themes:", error));
+ }
+ }
+
+ fetchThemes();
+});
diff --git a/scripts/template/json/themes.json b/scripts/template/json/themes.json
new file mode 100644
index 0000000..5b52d86
--- /dev/null
+++ b/scripts/template/json/themes.json
@@ -0,0 +1,26 @@
+{
+ "themes": [
+ {
+ "name": "Classic Light",
+ "properties": {
+ "title-color": "#000000cc",
+ "bg-color": "#ffffff",
+ "button-color": "#73a3eb",
+ "button-hover-color": "#459aef",
+ "toggle-theme-button-color": "#000000",
+ "copy-result-button-color": "#000000"
+ }
+ },
+ {
+ "name": "Classic Dark",
+ "properties": {
+ "title-color": "#cdcdcd",
+ "bg-color": "#1e1d24",
+ "button-color": "#5d99f4",
+ "button-hover-color": "#9ac6f1",
+ "toggle-theme-button-color": "#ffffff",
+ "copy-result-button-color": "#ffffff"
+ }
+ }
+ ]
+}
diff --git a/scripts/template/manifest.json b/scripts/template/manifest.json
new file mode 100644
index 0000000..de30844
--- /dev/null
+++ b/scripts/template/manifest.json
@@ -0,0 +1,32 @@
+{
+ "short_name": "{{ title }}",
+ "name": "{{ title }}",
+ "description": "{{ desc }}",
+ "background_color": "#1a1b1e",
+ "theme_color": "#1a1b1e",
+ "icons": [
+ {
+ "src": "./images/logo-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "./images/logo-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "./images/logo-180x180.png",
+ "sizes": "180x180",
+ "type": "image/png"
+ },
+ {
+ "src": "./images/logo-270x270.png",
+ "sizes": "270x270",
+ "type": "image/png"
+ }
+ ],
+ "start_url": "/{{ repo_name }}/{{ folder_path }}/index.html",
+ "display": "standalone",
+ "orientation": "portrait"
+}