diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..1585d14 Binary files /dev/null and b/__pycache__/main.cpython-313.pyc differ diff --git a/main.py b/main.py index 4a4f092..f4d1383 100644 --- a/main.py +++ b/main.py @@ -8,8 +8,13 @@ import shutil from datetime import datetime, timezone from pathlib import Path from typing import Any +import os +import uuid from flask import Flask, Response, abort, jsonify, render_template, request, send_from_directory +from werkzeug.utils import secure_filename + +from utils.media_manager import save_upload, list_media, delete_media app = Flask(__name__) @@ -132,6 +137,43 @@ def _copy_blank_template(dest: Path) -> None: ) +def build_page_tree(slug: str, max_depth: int = 3) -> dict[str, Any]: + """遞歸建構專案的頁面樹狀結構(根據資料夾結構)。""" + proj_dir = _project_dir(slug) + + def scan_folder(folder: Path, current_depth: int = 0): + items: list[dict[str, Any]] = [] + if current_depth > max_depth: + return items + try: + names = sorted([p.name for p in folder.iterdir()]) + except Exception: + return items + + for name in names: + if name.startswith("."): + continue + full = folder / name + if full.is_dir(): + children = scan_folder(full, current_depth + 1) + items.append({ + "type": "folder", + "name": name, + "title": name.replace("-", " ").title(), + "children": children, + }) + elif full.is_file() and full.suffix.lower() == ".html": + rel = str(full.relative_to(proj_dir)).replace("\\", "/") + items.append({ + "type": "file", + "name": rel, + "title": Path(name).stem.replace("-", " ").title(), + }) + return items + + return {"root": scan_folder(proj_dir, 0)} + + # ── 路由:一般頁面 ────────────────────────────────────────────────────────── @app.route("/") # type: ignore[untyped-decorator] @@ -195,6 +237,40 @@ def api_list_projects() -> Response: return jsonify(projects) +@app.route("/api/projects//settings", methods=["GET"]) # type: ignore[untyped-decorator] +def api_get_settings(slug: str) -> tuple[Response, int] | Response: + if not _project_dir(slug).exists(): + return jsonify({"error": "專案不存在"}), 404 + data = _load_project(slug) + settings = data.get("settings", {}) + # provide sensible defaults + defaults = { + "title": data.get("name", slug), + "description": data.get("description", ""), + "logo_url": None, + "favicon_url": None, + "primary_color": "#007bff", + "secondary_color": "#6c757d", + } + merged = {**defaults, **settings} + return jsonify(merged) + + +@app.route("/api/projects//settings", methods=["PUT"]) # type: ignore[untyped-decorator] +def api_put_settings(slug: str) -> tuple[Response, int] | Response: + if not _project_dir(slug).exists(): + return jsonify({"error": "專案不存在"}), 404 + body: dict[str, Any] = request.get_json(force=True) or {} + data = _load_project(slug) + settings = data.get("settings", {}) + settings.update(body) + data["settings"] = settings + data["updated_at"] = _now_iso() + _save_project(slug, data) + return jsonify({"ok": True, "settings": settings}) + + + @app.route("/api/projects", methods=["POST"]) # type: ignore[untyped-decorator] def api_create_project() -> tuple[Response, int]: body: dict[str, Any] = request.get_json(force=True) or {} @@ -261,6 +337,59 @@ def api_list_pages(slug: str) -> tuple[Response, int] | Response: return jsonify(_list_pages(slug)) +@app.route("/api/projects//pages-tree", methods=["GET"]) # type: ignore[untyped-decorator] +def api_pages_tree(slug: str) -> tuple[Response, int] | Response: + if not _project_dir(slug).exists(): + return jsonify({"error": "專案不存在"}), 404 + tree = build_page_tree(slug, max_depth=5) + return jsonify(tree) + + +@app.route("/api/projects//media/upload", methods=["POST"]) # type: ignore[untyped-decorator] +def api_media_upload(slug: str) -> tuple[Response, int] | Response: + proj_dir = _project_dir(slug) + if not proj_dir.exists(): + return jsonify({"error": "專案不存在"}), 404 + if "file" not in request.files: + return jsonify({"error": "缺少檔案 (file)"}), 400 + f = request.files["file"] + if f.filename == "": + return jsonify({"error": "檔名為空"}), 400 + # basic validation + filename = secure_filename(f.filename) + meta = save_upload(proj_dir, f) + # substitute slug in returned urls + if isinstance(meta.get("url"), str): + meta["url"] = meta["url"].replace("{slug}", slug) + if isinstance(meta.get("thumb"), str): + meta["thumb"] = meta["thumb"].replace("{slug}", slug) + return jsonify({"ok": True, "file": meta}) + + +@app.route("/api/projects//media/list", methods=["GET"]) # type: ignore[untyped-decorator] +def api_media_list(slug: str) -> tuple[Response, int] | Response: + proj_dir = _project_dir(slug) + if not proj_dir.exists(): + return jsonify({"error": "專案不存在"}), 404 + items = list_media(proj_dir) + # patch urls + for it in items: + if "filename" in it and "date" in it: + it["url"] = f"/sites/{slug}/media/images/{it['date']}/{it['filename']}" + return jsonify(items) + + +@app.route("/api/projects//media/", methods=["DELETE"]) # type: ignore[untyped-decorator] +def api_media_delete(slug: str, rel_path: str) -> tuple[Response, int] | Response: + proj_dir = _project_dir(slug) + if not proj_dir.exists(): + return jsonify({"error": "專案不存在"}), 404 + ok = delete_media(proj_dir, rel_path) + if not ok: + return jsonify({"error": "刪除失敗或檔案不存在"}), 400 + return jsonify({"ok": True}) + + @app.route("/api/projects//pages", methods=["POST"]) # type: ignore[untyped-decorator] def api_create_page(slug: str) -> tuple[Response, int]: if not _project_dir(slug).exists(): diff --git a/pyproject.toml b/pyproject.toml index aad68eb..857afac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,4 +6,10 @@ readme = "README.md" requires-python = ">=3.13" dependencies = [ "flask>=3.1", +] + +[tool.pip] +# Image processing for media manager +dependencies = [ + "Pillow>=10.0" ] \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html index acc3d10..8bdc7df 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,3 +1,136 @@ + + + + + + 管理器 - Dashboard + + + + +
+

網站管理器

+
+ +
+
概覽
+
網站設定
+
頁面管理
+
媒體庫
+
+ +
+
選擇一個專案在右側編輯。
+ + + + + + +
+ + + + diff --git a/templates/editor.html b/templates/editor.html index f4a09b3..742f65a 100644 --- a/templates/editor.html +++ b/templates/editor.html @@ -108,6 +108,9 @@ + + +