Feat: Add a script to generate Generator from a template

This commit is contained in:
2025-03-28 22:07:11 +08:00
parent 4eb9212a23
commit fe09f879b2
15 changed files with 687 additions and 0 deletions

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

111
scripts/template/index.html Normal file
View File

@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="{{ desc }}"/>
<title>{{ title }}</title>
<link rel="icon" href="./images/logo.png" />
<link rel="manifest" href="./manifest.json" />
<!-- bootstrap -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
/>
<script
src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"
></script>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"
rel="stylesheet"
/>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"
integrity="sha512-7tWCgq9tTYS/QkGVyKrtLpqAoMV9XIUxoou+sPUypsaZx56cYR/qio84fPK9EvJJtKvJEwt7vkn6je5UVzGevw=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<link rel="stylesheet" href="./css/styles.css" />
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("./js/service-worker.js");
}
</script>
</head>
<body>
<div class="container">
<div id="init-page">
</div>
<div id="result-page" style="display: none;">
</div>
<div class="row">
<i
class="col-2 fas fa-palette"
id="toggle-theme-button"
data-bs-toggle="modal"
data-bs-target="#themeModal"
></i>
<button class="col-4 offset-2 bi bi-files" id="btn" onclick="get{{ name }}()">
Generate
</button>
<i
class="col-2 offset-2 fas fa-clone d-none"
id="copy-result-button"
onclick="copyResultImageToClipboard()"
></i>
</div>
</div>
<!-- Theme Modal -->
<div
class="modal fade"
id="themeModal"
tabindex="-1"
aria-labelledby="themeModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="themeModalLabel">Choose Theme</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
>
</button>
</div>
<div class="modal-body" style="max-height: 70vh; overflow-y: auto">
<ul class="list-group" id="themeList">
<!-- Theme items will be dynamically populated here -->
</ul>
</div>
</div>
</div>
</div>
<canvas id="Matrix"></canvas>
<script src="./js/scripts.js"></script>
<script src="./js/{{ name }}.js"></script>
<script src="./js/matrix.js"></script>
<script src="./js/theme.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
crossorigin="anonymous"
></script>
</body>
</html>

View File

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

View File

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

View File

@@ -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 = $("<div>", {
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);
}

View File

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

View File

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

View File

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

View File

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