2.0 - WebUI builder ("Cielight" merge) #9
@ -28,6 +28,30 @@ app.config["PHOTOS_DIR"] = PHOTOS_DIR
|
|||||||
# --- Register upload blueprint ---
|
# --- Register upload blueprint ---
|
||||||
app.register_blueprint(upload_bp)
|
app.register_blueprint(upload_bp)
|
||||||
|
|
||||||
|
# --- Helper functions for theme editor ---
|
||||||
|
def get_theme_name():
|
||||||
|
site_yaml_path = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
|
||||||
|
with open(site_yaml_path, "r") as f:
|
||||||
|
site_yaml = yaml.safe_load(f)
|
||||||
|
return site_yaml.get("build", {}).get("theme", "modern")
|
||||||
|
|
||||||
|
def get_theme_yaml(theme_name):
|
||||||
|
theme_yaml_path = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "theme.yaml"
|
||||||
|
with open(theme_yaml_path, "r") as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
def save_theme_yaml(theme_name, theme_yaml):
|
||||||
|
theme_yaml_path = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "theme.yaml"
|
||||||
|
with open(theme_yaml_path, "w") as f:
|
||||||
|
yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
|
||||||
|
|
||||||
|
def get_local_fonts(theme_name):
|
||||||
|
fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
|
||||||
|
if not fonts_dir.exists():
|
||||||
|
return []
|
||||||
|
# Return full filenames, not just stem
|
||||||
|
return [f.name for f in fonts_dir.glob("*") if f.is_file() and f.suffix in [".woff", ".woff2"]]
|
||||||
|
|
||||||
# --- Routes ---
|
# --- Routes ---
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
@ -133,26 +157,30 @@ def delete_all_hero_photos():
|
|||||||
|
|
||||||
@app.route("/photos/<section>/<path:filename>")
|
@app.route("/photos/<section>/<path:filename>")
|
||||||
def photos(section, filename):
|
def photos(section, filename):
|
||||||
"""Serve uploaded photos from disk."""
|
"""Serve uploaded photos from disk for a specific section."""
|
||||||
return send_from_directory(PHOTOS_DIR / section, filename)
|
return send_from_directory(PHOTOS_DIR / section, filename)
|
||||||
|
|
||||||
@app.route("/photos/<path:filename>")
|
@app.route("/photos/<path:filename>")
|
||||||
def serve_photo(filename):
|
def serve_photo(filename):
|
||||||
|
"""Serve uploaded photos from disk (generic)."""
|
||||||
photos_dir = Path(__file__).resolve().parents[3] / "config" / "photos"
|
photos_dir = Path(__file__).resolve().parents[3] / "config" / "photos"
|
||||||
return send_from_directory(photos_dir, filename)
|
return send_from_directory(photos_dir, filename)
|
||||||
|
|
||||||
@app.route("/site-info")
|
@app.route("/site-info")
|
||||||
def site_info():
|
def site_info():
|
||||||
|
"""Serve the site info editor page."""
|
||||||
return render_template("site-info/index.html")
|
return render_template("site-info/index.html")
|
||||||
|
|
||||||
@app.route("/api/site-info", methods=["GET"])
|
@app.route("/api/site-info", methods=["GET"])
|
||||||
def get_site_info():
|
def get_site_info():
|
||||||
|
"""Return the site info YAML as JSON."""
|
||||||
with open(SITE_YAML, "r") as f:
|
with open(SITE_YAML, "r") as f:
|
||||||
data = yaml.safe_load(f)
|
data = yaml.safe_load(f)
|
||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
||||||
@app.route("/api/site-info", methods=["POST"])
|
@app.route("/api/site-info", methods=["POST"])
|
||||||
def update_site_info():
|
def update_site_info():
|
||||||
|
"""Update the site info YAML from frontend JSON."""
|
||||||
data = request.json
|
data = request.json
|
||||||
with open(SITE_YAML, "w") as f:
|
with open(SITE_YAML, "w") as f:
|
||||||
yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
|
yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
|
||||||
@ -160,13 +188,14 @@ def update_site_info():
|
|||||||
|
|
||||||
@app.route("/api/themes")
|
@app.route("/api/themes")
|
||||||
def list_themes():
|
def list_themes():
|
||||||
|
"""List available themes (folders in config/themes)."""
|
||||||
themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes"
|
themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes"
|
||||||
themes = [d.name for d in themes_dir.iterdir() if d.is_dir()]
|
themes = [d.name for d in themes_dir.iterdir() if d.is_dir()]
|
||||||
return jsonify(themes)
|
return jsonify(themes)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/thumbnail/upload", methods=["POST"])
|
@app.route("/api/thumbnail/upload", methods=["POST"])
|
||||||
def upload_thumbnail():
|
def upload_thumbnail():
|
||||||
|
"""Upload a thumbnail image and update site.yaml."""
|
||||||
PHOTOS_DIR = app.config["PHOTOS_DIR"]
|
PHOTOS_DIR = app.config["PHOTOS_DIR"]
|
||||||
file = request.files.get("file")
|
file = request.files.get("file")
|
||||||
if not file:
|
if not file:
|
||||||
@ -183,6 +212,7 @@ def upload_thumbnail():
|
|||||||
|
|
||||||
@app.route("/api/thumbnail/remove", methods=["POST"])
|
@app.route("/api/thumbnail/remove", methods=["POST"])
|
||||||
def remove_thumbnail():
|
def remove_thumbnail():
|
||||||
|
"""Remove the thumbnail image and update site.yaml."""
|
||||||
PHOTOS_DIR = app.config["PHOTOS_DIR"]
|
PHOTOS_DIR = app.config["PHOTOS_DIR"]
|
||||||
thumbnail_path = PHOTOS_DIR / "thumbnail.png"
|
thumbnail_path = PHOTOS_DIR / "thumbnail.png"
|
||||||
# Remove thumbnail file if exists
|
# Remove thumbnail file if exists
|
||||||
@ -199,6 +229,7 @@ def remove_thumbnail():
|
|||||||
|
|
||||||
@app.route("/api/theme/upload", methods=["POST"])
|
@app.route("/api/theme/upload", methods=["POST"])
|
||||||
def upload_theme():
|
def upload_theme():
|
||||||
|
"""Upload a custom theme folder and save it in config/themes."""
|
||||||
themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes"
|
themes_dir = Path(__file__).resolve().parents[3] / "config" / "themes"
|
||||||
files = request.files.getlist("files")
|
files = request.files.getlist("files")
|
||||||
if not files:
|
if not files:
|
||||||
@ -215,6 +246,117 @@ def upload_theme():
|
|||||||
file.save(dest_path)
|
file.save(dest_path)
|
||||||
return jsonify({"status": "ok", "theme": folder_name})
|
return jsonify({"status": "ok", "theme": folder_name})
|
||||||
|
|
||||||
|
# --- Theme Editor API ---
|
||||||
|
@app.route("/theme-editor")
|
||||||
|
def theme_editor():
|
||||||
|
"""Serve the theme editor page."""
|
||||||
|
return render_template("theme-editor/index.html")
|
||||||
|
|
||||||
|
@app.route("/api/theme-info", methods=["GET", "POST"])
|
||||||
|
def api_theme_info():
|
||||||
|
theme_name = get_theme_name()
|
||||||
|
if request.method == "GET":
|
||||||
|
theme_yaml = get_theme_yaml(theme_name)
|
||||||
|
google_fonts = theme_yaml.get("google_fonts", [])
|
||||||
|
return jsonify({
|
||||||
|
"theme_name": theme_name,
|
||||||
|
"theme_yaml": theme_yaml,
|
||||||
|
"google_fonts": google_fonts
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
data = request.get_json()
|
||||||
|
theme_yaml = data.get("theme_yaml")
|
||||||
|
theme_name = data.get("theme_name", theme_name)
|
||||||
|
save_theme_yaml(theme_name, theme_yaml)
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
|
@app.route("/api/local-fonts")
|
||||||
|
def api_local_fonts():
|
||||||
|
theme_name = request.args.get("theme")
|
||||||
|
fonts = get_local_fonts(theme_name)
|
||||||
|
return jsonify(fonts)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/favicon/upload", methods=["POST"])
|
||||||
|
def upload_favicon():
|
||||||
|
"""Upload favicon to theme folder and update theme.yaml."""
|
||||||
|
theme_name = request.form.get("theme")
|
||||||
|
file = request.files.get("file")
|
||||||
|
if not file or not theme_name:
|
||||||
|
return jsonify({"error": "Missing file or theme"}), 400
|
||||||
|
ext = Path(file.filename).suffix.lower()
|
||||||
|
if ext not in [".png", ".jpg", ".jpeg", ".ico"]:
|
||||||
|
return jsonify({"error": "Invalid file type"}), 400
|
||||||
|
filename = "favicon" + ext
|
||||||
|
theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name
|
||||||
|
file.save(theme_dir / filename)
|
||||||
|
# Update theme.yaml
|
||||||
|
theme_yaml_path = theme_dir / "theme.yaml"
|
||||||
|
with open(theme_yaml_path, "r") as f:
|
||||||
|
theme_yaml = yaml.safe_load(f)
|
||||||
|
theme_yaml.setdefault("favicon", {})["path"] = filename
|
||||||
|
with open(theme_yaml_path, "w") as f:
|
||||||
|
yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
|
||||||
|
return jsonify({"status": "ok", "filename": filename})
|
||||||
|
|
||||||
|
@app.route("/api/favicon/remove", methods=["POST"])
|
||||||
|
def remove_favicon():
|
||||||
|
"""Remove favicon from theme folder and update theme.yaml."""
|
||||||
|
data = request.get_json()
|
||||||
|
theme_name = data.get("theme")
|
||||||
|
if not theme_name:
|
||||||
|
return jsonify({"error": "Missing theme"}), 400
|
||||||
|
theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name
|
||||||
|
# Remove favicon file
|
||||||
|
for ext in [".png", ".jpg", ".jpeg", ".ico"]:
|
||||||
|
favicon_path = theme_dir / f"favicon{ext}"
|
||||||
|
if favicon_path.exists():
|
||||||
|
favicon_path.unlink()
|
||||||
|
# Update theme.yaml
|
||||||
|
theme_yaml_path = theme_dir / "theme.yaml"
|
||||||
|
with open(theme_yaml_path, "r") as f:
|
||||||
|
theme_yaml = yaml.safe_load(f)
|
||||||
|
if "favicon" in theme_yaml:
|
||||||
|
theme_yaml["favicon"]["path"] = ""
|
||||||
|
with open(theme_yaml_path, "w") as f:
|
||||||
|
yaml.safe_dump(theme_yaml, f, sort_keys=False, allow_unicode=True)
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
|
@app.route("/themes/<theme>/<filename>")
|
||||||
|
def serve_theme_asset(theme, filename):
|
||||||
|
theme_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme
|
||||||
|
return send_from_directory(theme_dir, filename)
|
||||||
|
|
||||||
|
@app.route("/api/font/upload", methods=["POST"])
|
||||||
|
def upload_font():
|
||||||
|
"""Upload a font file to the theme's fonts folder (only .woff/.woff2 allowed)."""
|
||||||
|
theme_name = request.form.get("theme")
|
||||||
|
file = request.files.get("file")
|
||||||
|
if not file or not theme_name:
|
||||||
|
return jsonify({"error": "Missing file or theme"}), 400
|
||||||
|
ext = Path(file.filename).suffix.lower()
|
||||||
|
if ext not in [".woff", ".woff2"]:
|
||||||
|
return jsonify({"error": "Invalid font file type"}), 400
|
||||||
|
fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
|
||||||
|
fonts_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
file.save(fonts_dir / file.filename)
|
||||||
|
return jsonify({"status": "ok", "filename": file.filename})
|
||||||
|
|
||||||
|
@app.route("/api/font/remove", methods=["POST"])
|
||||||
|
def remove_font():
|
||||||
|
"""Remove a font file from the theme's fonts folder."""
|
||||||
|
data = request.get_json()
|
||||||
|
theme_name = data.get("theme")
|
||||||
|
font = data.get("font")
|
||||||
|
if not theme_name or not font:
|
||||||
|
return jsonify({"error": "Missing theme or font"}), 400
|
||||||
|
fonts_dir = Path(__file__).resolve().parents[3] / "config" / "themes" / theme_name / "fonts"
|
||||||
|
font_path = fonts_dir / font
|
||||||
|
if font_path.exists():
|
||||||
|
font_path.unlink()
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
return jsonify({"error": "Font not found"}), 404
|
||||||
|
|
||||||
# --- Run server ---
|
# --- Run server ---
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.info("Starting WebUI at http://127.0.0.1:5000")
|
logging.info("Starting WebUI at http://127.0.0.1:5000")
|
||||||
|
@ -29,8 +29,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
div.style.gap = "8px";
|
div.style.gap = "8px";
|
||||||
div.style.marginBottom = "6px";
|
div.style.marginBottom = "6px";
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label">
|
<input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label" required>
|
||||||
<input type="text" placeholder="?tag=tag1,tag2" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href">
|
<input type="text" placeholder="?tag=tag1,tag2" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href" required>
|
||||||
<button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
|
<button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
|
||||||
`;
|
`;
|
||||||
menuList.appendChild(div);
|
menuList.appendChild(div);
|
||||||
|
393
src/webui/js/theme-editor.js
Normal file
393
src/webui/js/theme-editor.js
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
async function fetchThemeInfo() {
|
||||||
|
const res = await fetch("/api/theme-info");
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLocalFonts(theme) {
|
||||||
|
const res = await fetch(`/api/local-fonts?theme=${encodeURIComponent(theme)}`);
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeFont(theme, font) {
|
||||||
|
const res = await fetch("/api/font/remove", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ theme, font })
|
||||||
|
});
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(message, type = "success", duration = 3000) {
|
||||||
|
const container = document.getElementById("toast-container");
|
||||||
|
if (!container) return;
|
||||||
|
const toast = document.createElement("div");
|
||||||
|
toast.className = `toast ${type}`;
|
||||||
|
toast.textContent = message;
|
||||||
|
container.appendChild(toast);
|
||||||
|
requestAnimationFrame(() => toast.classList.add("show"));
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove("show");
|
||||||
|
setTimeout(() => container.removeChild(toast), 300);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setColorInput(colorId, textId, value) {
|
||||||
|
const colorInput = document.getElementById(colorId);
|
||||||
|
const textInput = document.getElementById(textId);
|
||||||
|
if (colorInput) colorInput.value = value;
|
||||||
|
if (textInput) textInput.value = value;
|
||||||
|
if (colorInput && textInput) {
|
||||||
|
colorInput.addEventListener("input", () => {
|
||||||
|
textInput.value = colorInput.value;
|
||||||
|
});
|
||||||
|
textInput.addEventListener("input", () => {
|
||||||
|
colorInput.value = textInput.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFontDropdown(selectId, value, options) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
if (!select) return;
|
||||||
|
select.innerHTML = options.map(opt =>
|
||||||
|
`<option value="${opt}"${opt === value ? " selected" : ""}>${opt}</option>`
|
||||||
|
).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFallbackDropdown(selectId, value) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
if (!select) return;
|
||||||
|
select.value = (value === "serif" || value === "sans-serif") ? value : "sans-serif";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTextInput(inputId, value) {
|
||||||
|
const input = document.getElementById(inputId);
|
||||||
|
if (input) input.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGoogleFonts(googleFonts) {
|
||||||
|
const container = document.getElementById("google-fonts-fields");
|
||||||
|
container.innerHTML = "";
|
||||||
|
googleFonts.forEach((font, idx) => {
|
||||||
|
container.innerHTML += `
|
||||||
|
<div class="input-field" data-idx="${idx}">
|
||||||
|
<label>Family</label>
|
||||||
|
<input type="text" name="google_fonts[${idx}][family]" value="${font.family || ""}">
|
||||||
|
<label>Weights (comma separated)</label>
|
||||||
|
<input type="text" name="google_fonts[${idx}][weights]" value="${(font.weights || []).join(',')}">
|
||||||
|
<button type="button" class="remove-google-font" data-idx="${idx}">Remove</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLocalFonts(fonts) {
|
||||||
|
const listDiv = document.getElementById("local-fonts-list");
|
||||||
|
if (!listDiv) return;
|
||||||
|
listDiv.innerHTML = "";
|
||||||
|
fonts.forEach(font => {
|
||||||
|
listDiv.innerHTML += `
|
||||||
|
<div class="font-item">
|
||||||
|
<span>${font}</span>
|
||||||
|
<button type="button" class="remove-font-btn danger" data-font="${font}">Remove</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
const themeInfo = await fetchThemeInfo();
|
||||||
|
const themeNameSpan = document.getElementById("current-theme");
|
||||||
|
if (themeNameSpan) themeNameSpan.textContent = themeInfo.theme_name;
|
||||||
|
|
||||||
|
const themeYaml = themeInfo.theme_yaml;
|
||||||
|
const googleFonts = themeYaml.google_fonts ? JSON.parse(JSON.stringify(themeYaml.google_fonts)) : [];
|
||||||
|
let localFonts = await fetchLocalFonts(themeInfo.theme_name);
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
if (themeYaml.colors) {
|
||||||
|
setColorInput("color-primary", "color-primary-text", themeYaml.colors.primary || "#0065a1");
|
||||||
|
setColorInput("color-primary-dark", "color-primary-dark-text", themeYaml.colors.primary_dark || "#005384");
|
||||||
|
setColorInput("color-secondary", "color-secondary-text", themeYaml.colors.secondary || "#00b0f0");
|
||||||
|
setColorInput("color-accent", "color-accent-text", themeYaml.colors.accent || "#ffc700");
|
||||||
|
setColorInput("color-text-dark", "color-text-dark-text", themeYaml.colors.text_dark || "#616161");
|
||||||
|
setColorInput("color-background", "color-background-text", themeYaml.colors.background || "#fff");
|
||||||
|
setColorInput("color-browser-color", "color-browser-color-text", themeYaml.colors.browser_color || "#fff");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
function refreshFontDropdowns() {
|
||||||
|
setFontDropdown("font-primary", document.getElementById("font-primary").value, [
|
||||||
|
...googleFonts.map(f => f.family),
|
||||||
|
...localFonts
|
||||||
|
]);
|
||||||
|
setFontDropdown("font-secondary", document.getElementById("font-secondary").value, [
|
||||||
|
...googleFonts.map(f => f.family),
|
||||||
|
...localFonts
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (themeYaml.fonts) {
|
||||||
|
setFontDropdown("font-primary", themeYaml.fonts.primary?.name || "Lato", [
|
||||||
|
...googleFonts.map(f => f.family),
|
||||||
|
...localFonts
|
||||||
|
]);
|
||||||
|
setFallbackDropdown("font-primary-fallback", themeYaml.fonts.primary?.fallback || "sans-serif");
|
||||||
|
setFontDropdown("font-secondary", themeYaml.fonts.secondary?.name || "Montserrat", [
|
||||||
|
...googleFonts.map(f => f.family),
|
||||||
|
...localFonts
|
||||||
|
]);
|
||||||
|
setFallbackDropdown("font-secondary-fallback", themeYaml.fonts.secondary?.fallback || "serif");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font upload logic
|
||||||
|
const fontUploadInput = document.getElementById("font-upload");
|
||||||
|
const chooseFontBtn = document.getElementById("choose-font-btn");
|
||||||
|
const fontUploadStatus = document.getElementById("font-upload-status");
|
||||||
|
const localFontsList = document.getElementById("local-fonts-list");
|
||||||
|
|
||||||
|
// Modal logic for font deletion
|
||||||
|
const deleteFontModal = document.getElementById("delete-font-modal");
|
||||||
|
const deleteFontModalClose = document.getElementById("delete-font-modal-close");
|
||||||
|
const deleteFontModalConfirm = document.getElementById("delete-font-modal-confirm");
|
||||||
|
const deleteFontModalCancel = document.getElementById("delete-font-modal-cancel");
|
||||||
|
let fontToDelete = null;
|
||||||
|
|
||||||
|
function refreshLocalFonts() {
|
||||||
|
renderLocalFonts(localFonts);
|
||||||
|
refreshFontDropdowns();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chooseFontBtn && fontUploadInput) {
|
||||||
|
chooseFontBtn.addEventListener("click", () => fontUploadInput.click());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fontUploadInput) {
|
||||||
|
fontUploadInput.addEventListener("change", async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const ext = file.name.split('.').pop().toLowerCase();
|
||||||
|
if (!["woff", "woff2"].includes(ext)) {
|
||||||
|
fontUploadStatus.textContent = "Only .woff and .woff2 fonts are allowed.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
formData.append("theme", themeInfo.theme_name);
|
||||||
|
const res = await fetch("/api/font/upload", { method: "POST", body: formData });
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.status === "ok") {
|
||||||
|
fontUploadStatus.textContent = "Font uploaded!";
|
||||||
|
showToast("Font uploaded!", "success");
|
||||||
|
localFonts = await fetchLocalFonts(themeInfo.theme_name);
|
||||||
|
refreshLocalFonts();
|
||||||
|
} else {
|
||||||
|
fontUploadStatus.textContent = "Error uploading font.";
|
||||||
|
showToast("Error uploading font.", "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove font button triggers modal
|
||||||
|
if (localFontsList) {
|
||||||
|
localFontsList.addEventListener("click", (e) => {
|
||||||
|
if (e.target.classList.contains("remove-font-btn")) {
|
||||||
|
fontToDelete = e.target.dataset.font;
|
||||||
|
document.getElementById("delete-font-modal-text").textContent =
|
||||||
|
`Are you sure you want to remove the font "${fontToDelete}"?`;
|
||||||
|
deleteFontModal.style.display = "flex";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal logic for font deletion
|
||||||
|
if (deleteFontModal && deleteFontModalClose && deleteFontModalConfirm && deleteFontModalCancel) {
|
||||||
|
deleteFontModalClose.onclick = deleteFontModalCancel.onclick = () => {
|
||||||
|
deleteFontModal.style.display = "none";
|
||||||
|
fontToDelete = null;
|
||||||
|
};
|
||||||
|
window.onclick = function(event) {
|
||||||
|
if (event.target === deleteFontModal) {
|
||||||
|
deleteFontModal.style.display = "none";
|
||||||
|
fontToDelete = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
deleteFontModalConfirm.onclick = async () => {
|
||||||
|
if (!fontToDelete) return;
|
||||||
|
const result = await removeFont(themeInfo.theme_name, fontToDelete);
|
||||||
|
if (result.status === "ok") {
|
||||||
|
showToast("Font removed!", "success");
|
||||||
|
localFonts = await fetchLocalFonts(themeInfo.theme_name);
|
||||||
|
refreshLocalFonts();
|
||||||
|
} else {
|
||||||
|
showToast("Error removing font.", "error");
|
||||||
|
}
|
||||||
|
deleteFontModal.style.display = "none";
|
||||||
|
fontToDelete = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial render of local fonts
|
||||||
|
refreshLocalFonts();
|
||||||
|
|
||||||
|
// Favicon logic
|
||||||
|
const faviconInput = document.getElementById("favicon-path");
|
||||||
|
const faviconUpload = document.getElementById("favicon-upload");
|
||||||
|
const chooseFaviconBtn = document.getElementById("choose-favicon-btn");
|
||||||
|
const faviconPreview = document.getElementById("favicon-preview");
|
||||||
|
const removeFaviconBtn = document.getElementById("remove-favicon-btn");
|
||||||
|
const deleteFaviconModal = document.getElementById("delete-favicon-modal");
|
||||||
|
const deleteFaviconModalClose = document.getElementById("delete-favicon-modal-close");
|
||||||
|
const deleteFaviconModalConfirm = document.getElementById("delete-favicon-modal-confirm");
|
||||||
|
const deleteFaviconModalCancel = document.getElementById("delete-favicon-modal-cancel");
|
||||||
|
|
||||||
|
function updateFaviconPreview(src) {
|
||||||
|
if (faviconPreview) {
|
||||||
|
faviconPreview.src = src || "";
|
||||||
|
faviconPreview.style.display = src ? "inline-block" : "none";
|
||||||
|
}
|
||||||
|
if (removeFaviconBtn) {
|
||||||
|
removeFaviconBtn.style.display = src ? "inline-block" : "none";
|
||||||
|
}
|
||||||
|
if (chooseFaviconBtn) {
|
||||||
|
chooseFaviconBtn.style.display = src ? "none" : "inline-block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chooseFaviconBtn && faviconUpload) {
|
||||||
|
chooseFaviconBtn.addEventListener("click", () => faviconUpload.click());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faviconUpload) {
|
||||||
|
faviconUpload.addEventListener("change", async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const ext = file.name.split('.').pop().toLowerCase();
|
||||||
|
if (!["png", "jpg", "jpeg", "ico"].includes(ext)) {
|
||||||
|
showToast("Invalid file type for favicon.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
formData.append("theme", themeInfo.theme_name);
|
||||||
|
const res = await fetch("/api/favicon/upload", { method: "POST", body: formData });
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.status === "ok") {
|
||||||
|
faviconInput.value = result.filename;
|
||||||
|
updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`);
|
||||||
|
showToast("Favicon uploaded!", "success");
|
||||||
|
} else {
|
||||||
|
showToast("Error uploading favicon", "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removeFaviconBtn) {
|
||||||
|
removeFaviconBtn.addEventListener("click", () => {
|
||||||
|
deleteFaviconModal.style.display = "flex";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteFaviconModal && deleteFaviconModalClose && deleteFaviconModalConfirm && deleteFaviconModalCancel) {
|
||||||
|
deleteFaviconModalClose.onclick = deleteFaviconModalCancel.onclick = () => {
|
||||||
|
deleteFaviconModal.style.display = "none";
|
||||||
|
};
|
||||||
|
window.onclick = function(event) {
|
||||||
|
if (event.target === deleteFaviconModal) {
|
||||||
|
deleteFaviconModal.style.display = "none";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
deleteFaviconModalConfirm.onclick = async () => {
|
||||||
|
const res = await fetch("/api/favicon/remove", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ theme: themeInfo.theme_name })
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.status === "ok") {
|
||||||
|
faviconInput.value = "";
|
||||||
|
updateFaviconPreview("");
|
||||||
|
showToast("Favicon removed!", "success");
|
||||||
|
} else {
|
||||||
|
showToast("Error removing favicon", "error");
|
||||||
|
}
|
||||||
|
deleteFaviconModal.style.display = "none";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeYaml.favicon && themeYaml.favicon.path) {
|
||||||
|
faviconInput.value = themeYaml.favicon.path;
|
||||||
|
updateFaviconPreview(`/themes/${themeInfo.theme_name}/${themeYaml.favicon.path}?t=${Date.now()}`);
|
||||||
|
} else {
|
||||||
|
updateFaviconPreview("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Google Fonts
|
||||||
|
renderGoogleFonts(googleFonts);
|
||||||
|
|
||||||
|
// Add Google Font
|
||||||
|
const addGoogleFontBtn = document.getElementById("add-google-font");
|
||||||
|
if (addGoogleFontBtn) {
|
||||||
|
addGoogleFontBtn.addEventListener("click", () => {
|
||||||
|
googleFonts.push({ family: "", weights: [] });
|
||||||
|
renderGoogleFonts(googleFonts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove Google Font
|
||||||
|
const googleFontsFields = document.getElementById("google-fonts-fields");
|
||||||
|
if (googleFontsFields) {
|
||||||
|
googleFontsFields.addEventListener("click", (e) => {
|
||||||
|
if (e.target.classList.contains("remove-google-font")) {
|
||||||
|
const idx = parseInt(e.target.dataset.idx, 10);
|
||||||
|
googleFonts.splice(idx, 1);
|
||||||
|
renderGoogleFonts(googleFonts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form submit
|
||||||
|
document.getElementById("theme-editor-form").addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const data = {};
|
||||||
|
data.colors = {
|
||||||
|
primary: document.getElementById("color-primary-text").value,
|
||||||
|
primary_dark: document.getElementById("color-primary-dark-text").value,
|
||||||
|
secondary: document.getElementById("color-secondary-text").value,
|
||||||
|
accent: document.getElementById("color-accent-text").value,
|
||||||
|
text_dark: document.getElementById("color-text-dark-text").value,
|
||||||
|
background: document.getElementById("color-background-text").value,
|
||||||
|
browser_color: document.getElementById("color-browser-color-text").value
|
||||||
|
};
|
||||||
|
data.fonts = {
|
||||||
|
primary: {
|
||||||
|
name: document.getElementById("font-primary").value,
|
||||||
|
fallback: document.getElementById("font-primary-fallback").value
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
name: document.getElementById("font-secondary").value,
|
||||||
|
fallback: document.getElementById("font-secondary-fallback").value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
data.favicon = {
|
||||||
|
path: faviconInput.value
|
||||||
|
};
|
||||||
|
data.google_fonts = [];
|
||||||
|
document.querySelectorAll("#google-fonts-fields .input-field").forEach(field => {
|
||||||
|
const family = field.querySelector('input[name^="google_fonts"][name$="[family]"]').value;
|
||||||
|
const weights = field.querySelector('input[name^="google_fonts"][name$="[weights]"]').value
|
||||||
|
.split(",").map(w => w.trim()).filter(w => w);
|
||||||
|
if (family) data.google_fonts.push({ family, weights });
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch("/api/theme-info", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data })
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
showToast("Theme saved!", "success");
|
||||||
|
} else {
|
||||||
|
showToast("Error saving theme.", "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -49,27 +49,27 @@
|
|||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Title</label>
|
<label>Title</label>
|
||||||
<input type="text" name="info.title" placeholder="Your site title">
|
<input type="text" name="info.title" placeholder="Your site title" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Subtitle</label>
|
<label>Subtitle</label>
|
||||||
<input type="text" name="info.subtitle" placeholder="Your site subtitle">
|
<input type="text" name="info.subtitle" placeholder="Your site subtitle" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Description</label>
|
<label>Description</label>
|
||||||
<input type="text" name="info.description" placeholder="Your site description">
|
<input type="text" name="info.description" placeholder="Your site description" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Canonical URL</label>
|
<label>Canonical URL</label>
|
||||||
<input type="text" name="info.canonical" placeholder="https://yoursite.com">
|
<input type="text" name="info.canonical" placeholder="https://yoursite.com" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Keywords (comma separated)</label>
|
<label>Keywords (comma separated)</label>
|
||||||
<input type="text" name="info.keywords" placeholder="photo, gallery, photography">
|
<input type="text" name="info.keywords" placeholder="photo, gallery, photography" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Author</label>
|
<label>Author</label>
|
||||||
<input type="text" name="info.author" placeholder="Your Name">
|
<input type="text" name="info.author" placeholder="Your Name" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@ -79,9 +79,9 @@
|
|||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Instagram URL</label>
|
<label>Instagram URL</label>
|
||||||
<input type="text" name="social.instagram_url" placeholder="https://instagram.com/yourprofile">
|
<input type="text" name="social.instagram_url" placeholder="https://instagram.com/yourprofile" required>
|
||||||
<label class="thumbnail-form-label">Thumbnail</label>
|
<label class="thumbnail-form-label">Thumbnail</label>
|
||||||
<input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;">
|
<input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;" required>
|
||||||
<button type="button" id="choose-thumbnail-btn" class="up-btn">📸 Upload a photo</button>
|
<button type="button" id="choose-thumbnail-btn" class="up-btn">📸 Upload a photo</button>
|
||||||
<div class="thumbnail-form">
|
<div class="thumbnail-form">
|
||||||
<img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
|
<img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
|
||||||
@ -106,11 +106,11 @@
|
|||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Copyright</label>
|
<label>Copyright</label>
|
||||||
<input type="text" name="footer.copyright">
|
<input type="text" name="footer.copyright" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Legal Label</label>
|
<label>Legal Label</label>
|
||||||
<input type="text" name="footer.legal_label">
|
<input type="text" name="footer.legal_label" re>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@ -143,7 +143,7 @@
|
|||||||
<div class="fields">
|
<div class="fields">
|
||||||
<div class="input-field">
|
<div class="input-field">
|
||||||
<label>Theme</label>
|
<label>Theme</label>
|
||||||
<select name="build.theme" id="theme-select"></select>
|
<select name="build.theme" id="theme-select" required></select>
|
||||||
<input type="file" id="theme-upload" webkitdirectory directory multiple style="display:none;">
|
<input type="file" id="theme-upload" webkitdirectory directory multiple style="display:none;">
|
||||||
<button type="button" id="choose-theme-btn" class="up-btn">📂 Upload custom theme folder</button>
|
<button type="button" id="choose-theme-btn" class="up-btn">📂 Upload custom theme folder</button>
|
||||||
<label class="thumbnail-form-label">Images processing</label>
|
<label class="thumbnail-form-label">Images processing</label>
|
||||||
|
@ -48,7 +48,7 @@ h2 {
|
|||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
border: 1px solid #2f2e2e80;
|
border: 1px solid #2f2e2e80;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0px 20px;
|
padding: 0px 20px 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-section label {
|
.upload-section label {
|
||||||
@ -491,7 +491,7 @@ h2 {
|
|||||||
padding: 0 40px 40px 40px;
|
padding: 0 40px 40px 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form fieldset {
|
fieldset {
|
||||||
background-color: rgb(67 67 67 / 26%);
|
background-color: rgb(67 67 67 / 26%);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
@ -501,7 +501,7 @@ h2 {
|
|||||||
margin: 32px auto;
|
margin: 32px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form legend {
|
legend {
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #26c4ff;
|
color: #26c4ff;
|
||||||
@ -509,13 +509,13 @@ h2 {
|
|||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form .fields {
|
.fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form .input-field {
|
.input-field {
|
||||||
flex: 1 1 calc(33.333% - 18px);
|
flex: 1 1 calc(33.333% - 18px);
|
||||||
min-width: 220px;
|
min-width: 220px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@ -525,7 +525,7 @@ h2 {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form label {
|
label {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #e3e3e3;
|
color: #e3e3e3;
|
||||||
@ -533,9 +533,9 @@ h2 {
|
|||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form input,
|
#site-info-form input, #theme-editor-form input,
|
||||||
#site-info-form textarea,
|
#site-info-form textarea, #theme-editor-form textarea,
|
||||||
#site-info-form select {
|
#site-info-form select, #theme-editor-form select {
|
||||||
/* background: rgba(4, 44, 60, 0.55);*/
|
/* background: rgba(4, 44, 60, 0.55);*/
|
||||||
background: #1f2223;
|
background: #1f2223;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -551,23 +551,29 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form input::placeholder,
|
#site-info-form input::placeholder,
|
||||||
#site-info-form textarea::placeholder {
|
#theme-editor-form input::placeholder,
|
||||||
|
#site-info-form textarea::placeholder,
|
||||||
|
#theme-editor-form textarea::placeholder {
|
||||||
color: #585858;
|
color: #585858;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form input:focus,
|
#site-info-form input:focus,
|
||||||
|
#theme-editor-form input:focus,
|
||||||
#site-info-form textarea:focus,
|
#site-info-form textarea:focus,
|
||||||
#site-info-form select:focus {
|
#theme-editor-form textarea:focus,
|
||||||
|
#site-info-form select:focus,
|
||||||
|
#theme-editor-form select:focus {
|
||||||
border-color: #585858;
|
border-color: #585858;
|
||||||
background: #161616;
|
background: #161616;
|
||||||
}
|
}
|
||||||
#site-info-form textarea {
|
#site-info-form textarea,
|
||||||
|
#theme-editor-form textarea {
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form input[type="file"] {
|
#input[type="file"] {
|
||||||
background: none;
|
background: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
@ -575,13 +581,13 @@ h2 {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form img#thumbnail-preview {
|
img#thumbnail-preview {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid #585858;
|
border: 1px solid #585858;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button[type="submit"] {
|
#site-info-form button[type="submit"], #theme-editor-form button[type="submit"] {
|
||||||
background: linear-gradient(135deg, #26c4ff, #016074);
|
background: linear-gradient(135deg, #26c4ff, #016074);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -595,11 +601,11 @@ h2 {
|
|||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button[type="submit"]:hover {
|
#site-info-form button[type="submit"]:hover, #theme-editor-form button[type="submit"]:hover {
|
||||||
background: linear-gradient(135deg, #72d9ff, #26657e);
|
background: linear-gradient(135deg, #72d9ff, #26657e);
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button[type="button"] {
|
#site-info-form button[type="button"], #theme-editor-form button[type="button"] {
|
||||||
background: #00000000;
|
background: #00000000;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
@ -612,47 +618,47 @@ h2 {
|
|||||||
border: 1px solid #585858;
|
border: 1px solid #585858;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button[type="button"]:hover {
|
#site-info-form button[type="button"]:hover, #theme-editor-form button[type="button"]:hover {
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
#site-info-form {
|
#site-info-form, #theme-editor-form {
|
||||||
padding: 18px 8px;
|
padding: 18px 8px;
|
||||||
}
|
}
|
||||||
#site-info-form .fields,
|
.fields,
|
||||||
#site-info-form fieldset {
|
fieldset {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
#site-info-form .input-field {
|
.input-field {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button.remove-menu-item, #site-info-form button.remove-ip-paragraph {
|
#site-info-form button.remove-menu-item, #site-info-form button.remove-ip-paragraph, #theme-editor-form button.remove-menu-item, #theme-editor-form button.remove-ip-paragraph {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button.remove-menu-item:hover, #site-info-form button.remove-ip-paragraph:hover {
|
#site-info-form button.remove-menu-item:hover, #site-info-form button.remove-ip-paragraph:hover, #theme-editor-form button.remove-menu-item:hover, #theme-editor-form button.remove-ip-paragraph:hover {
|
||||||
background: rgb(121, 26, 19);
|
background: rgb(121, 26, 19);
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button.remove-btn {
|
#site-info-form button.remove-btn, #theme-editor-form button.remove-btn {
|
||||||
|
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
background: #2d2d2d;
|
background: #2d2d2d;
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form button.remove-btn:hover{
|
#site-info-form button.remove-btn:hover, #theme-editor-form button.remove-btn:hover {
|
||||||
background: rgb(121, 26, 19);
|
background: rgb(121, 26, 19);
|
||||||
}
|
}
|
||||||
|
|
||||||
#site-info-form .thumbnail-form-label {
|
#site-info-form .thumbnail-form-label, #theme-editor-form .thumbnail-form-label {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
178
src/webui/theme-editor/index.html
Normal file
178
src/webui/theme-editor/index.html
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Theme Editor</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Top bar -->
|
||||||
|
<div class="nav-bar">
|
||||||
|
<div class="content-inner nav">
|
||||||
|
<div class="nav-cta">
|
||||||
|
<div class="arrow">→</div>
|
||||||
|
<a class="button" href="/site-info">
|
||||||
|
<span id="step">← Back to Site Info</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" id="nav-check">
|
||||||
|
<div class="nav-header">
|
||||||
|
<div class="nav-title">
|
||||||
|
<img src="../img/logo.svg">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-btn">
|
||||||
|
<label for="nav-check">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="nav-links">
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li class="nav-item appear2"><a href="/site-info">Site info</a></li>
|
||||||
|
<li class="nav-item appear2"><a href="/theme-editor">Theme editor</a></li>
|
||||||
|
<li class="nav-item appear2"><a href="#">Gallery</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Toast container for notifications -->
|
||||||
|
<div id="theme-editor" class="content-inner">
|
||||||
|
<div id="toast-container"></div>
|
||||||
|
<h1>Edit Theme</h1>
|
||||||
|
<!-- Show current theme -->
|
||||||
|
<div class="theme-info">
|
||||||
|
<strong>Current theme:</strong> <span id="current-theme"></span>
|
||||||
|
</div>
|
||||||
|
<form id="theme-editor-form">
|
||||||
|
<!-- Colors Section -->
|
||||||
|
<fieldset>
|
||||||
|
<h2>Colors</h2>
|
||||||
|
<div class="fields">
|
||||||
|
<div class="input-field">
|
||||||
|
<label>Primary</label>
|
||||||
|
<input type="color" name="colors.primary" id="color-primary">
|
||||||
|
<input type="text" name="colors.primary_text" id="color-primary-text" style="width:100px;">
|
||||||
|
</div>
|
||||||
|
<div class="input-field">
|
||||||
|
<label>Primary Dark</label>
|
||||||
|
<input type="color" name="colors.primary_dark" id="color-primary-dark">
|
||||||
|
<input type="text" name="colors.primary_dark_text" id="color-primary-dark-text" style="width:100px;">
|
||||||
|
</div>
|
||||||
|
<div class="input-field">
|
||||||
|
<label>Secondary</label>
|
||||||
|
<input type="color" name="colors.secondary" id="color-secondary">
|
||||||
|
<input type="text" name="colors.secondary_text" id="color-secondary-text" style="width:100px;">
|
||||||
|
</div>
|
||||||
|
<div class="input-field">
|
||||||
|
<label>Accent</label>
|
||||||
|
<input type="color" name="colors.accent" id="color-accent">
|
||||||
|
<input type="text" name="colors.accent_text" id="color-accent-text" style="width:100px;">
|
||||||
|
</div>
|
||||||
|
<div class="input-field">
|
||||||
|
<label>Text Dark</label>
|
||||||
|
<input type="color" name="colors.text_dark" id="color-text-dark">
|
||||||
|
<input type="text" name="colors.text_dark_text" id="color-text-dark-text" style="width:100px;">
|
||||||
|
</div>
|
||||||
|
<div class="input-field">
|
||||||
|
<label>Background</label>
|
||||||
|
<input type="color" name="colors.background" id="color-background">
|
||||||
|
<input type="text" name="colors.background_text" id="color-background-text" style="width:100px;">
|
||||||
|
</div>
|
||||||
|
<div class="input-field">
|
||||||
|
<label>Browser Color</label>
|
||||||
|
<input type="color" name="colors.browser_color" id="color-browser-color">
|
||||||
|
<input type="text" name="colors.browser_color_text" id="color-browser-color-text" style="width:100px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<!-- Google Fonts Section -->
|
||||||
|
<fieldset>
|
||||||
|
<h2>Google Fonts</h2>
|
||||||
|
<div class="fields" id="google-fonts-fields">
|
||||||
|
<!-- JS will render font family and weights inputs here -->
|
||||||
|
</div>
|
||||||
|
<button type="button" id="add-google-font">Add Google Font</button>
|
||||||
|
</fieldset>
|
||||||
|
<!-- Custom Font Upload Section -->
|
||||||
|
<fieldset>
|
||||||
|
<h2>Upload Custom Font (.woff, .woff2)</h2>
|
||||||
|
<div class="fields">
|
||||||
|
<input type="file" id="font-upload" accept=".woff,.woff2" style="display:none;">
|
||||||
|
<button type="button" id="choose-font-btn" class="up-btn">🖋️ Upload font</button>
|
||||||
|
<div id="local-fonts-list" class="font-list"></div>
|
||||||
|
<span id="font-upload-status"></span>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<!-- Fonts Section -->
|
||||||
|
<fieldset>
|
||||||
|
<h2>Fonts</h2>
|
||||||
|
<div class="fields">
|
||||||
|
<div class="input-field">
|
||||||
|
<label>Primary Font</label>
|
||||||
|
<select name="fonts.primary.name" id="font-primary"></select>
|
||||||
|
<label>Fallback</label>
|
||||||
|
<select name="fonts.primary.fallback" id="font-primary-fallback">
|
||||||
|
<option value="sans-serif">sans-serif</option>
|
||||||
|
<option value="serif">serif</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="input-field">
|
||||||
|
<label>Secondary Font</label>
|
||||||
|
<select name="fonts.secondary.name" id="font-secondary"></select>
|
||||||
|
<label>Fallback</label>
|
||||||
|
<select name="fonts.secondary.fallback" id="font-secondary-fallback">
|
||||||
|
<option value="sans-serif">sans-serif</option>
|
||||||
|
<option value="serif">serif</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<!-- Favicon Section -->
|
||||||
|
<fieldset>
|
||||||
|
<h2>Favicon</h2>
|
||||||
|
<div class="fields">
|
||||||
|
<div class="input-field">
|
||||||
|
<label>Favicon Path</label>
|
||||||
|
<input type="text" name="favicon.path" id="favicon-path" readonly>
|
||||||
|
<input type="file" id="favicon-upload" accept=".png,.jpg,.jpeg,.ico" style="display:none;">
|
||||||
|
<button type="button" id="choose-favicon-btn" class="up-btn">🖼️ Upload favicon</button>
|
||||||
|
<div class="favicon-form">
|
||||||
|
<img id="favicon-preview" src="" alt="Favicon preview" style="max-width:48px;display:none;">
|
||||||
|
<button type="button" id="remove-favicon-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<button type="submit">Save Theme</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<!-- Delete confirmation modal for favicon -->
|
||||||
|
<div id="delete-favicon-modal" class="modal" style="display:none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span id="delete-favicon-modal-close" class="modal-close">×</span>
|
||||||
|
<h3>Confirm Deletion</h3>
|
||||||
|
<p id="delete-favicon-modal-text">Are you sure you want to remove this favicon?</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="delete-favicon-modal-confirm" class="modal-btn danger">Remove</button>
|
||||||
|
<button id="delete-favicon-modal-cancel" class="modal-btn">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Delete confirmation modal for font -->
|
||||||
|
<div id="delete-font-modal" class="modal" style="display:none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span id="delete-font-modal-close" class="modal-close">×</span>
|
||||||
|
<h3>Confirm Deletion</h3>
|
||||||
|
<p id="delete-font-modal-text">Are you sure you want to remove this font?</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="delete-font-modal-confirm" class="modal-btn danger">Remove</button>
|
||||||
|
<button id="delete-font-modal-cancel" class="modal-btn">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="{{ url_for('static', filename='js/theme-editor.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in New Issue
Block a user