refactor: scenes now folder-based with preview image support

This commit is contained in:
2026-07-27 11:36:25 +08:00
parent 30697e4a64
commit 7a03fd3ebd
2 changed files with 50 additions and 18 deletions
+47 -17
View File
@@ -84,34 +84,51 @@ class SceneManager:
def list_all(self, game_id: str | None = None) -> list[dict[str, Any]]: def list_all(self, game_id: str | None = None) -> list[dict[str, Any]]:
scenes = [] scenes = []
for f in SCENES_DIR.glob("*.json"): for item in SCENES_DIR.iterdir():
if not item.is_dir():
continue
f = item / "scene.json"
if not f.exists():
continue
try: try:
with open(f, "r", encoding="utf-8") as fp: with open(f, "r", encoding="utf-8") as fp:
data = json.load(fp) data = json.load(fp)
scene = Scene.from_dict(data) scene = Scene.from_dict(data)
if game_id is None or scene.game_id == game_id: if game_id is None or scene.game_id == game_id:
scenes.append(scene.to_dict()) d = scene.to_dict()
for ext in ["jpg","jpeg","png","webp","gif"]:
if (item / f"preview.{ext}").exists():
d["preview_ext"] = ext
break
scenes.append(d)
except Exception as e: except Exception as e:
logger.error("Failed to load scene %s: %s", f.name, e) logger.error("Failed to load scene %s: %s", item.name, e)
return scenes return scenes
def get(self, scene_id: str) -> dict[str, Any] | None: def get(self, scene_id: str) -> dict[str, Any] | None:
filepath = SCENES_DIR / f"{scene_id}.json" item = SCENES_DIR / scene_id
if not filepath.exists(): f = item / "scene.json"
if not f.exists():
return None return None
try: try:
with open(filepath, "r", encoding="utf-8") as f: with open(f, "r", encoding="utf-8") as fp:
data = json.load(f) data = json.load(fp)
return Scene.from_dict(data).to_dict() d = Scene.from_dict(data).to_dict()
for ext in ["jpg","jpeg","png","webp","gif"]:
if (item / f"preview.{ext}").exists():
d["preview_ext"] = ext
break
return d
except Exception as e: except Exception as e:
logger.error("Failed to read scene %s: %s", scene_id, e) logger.error("Failed to read scene %s: %s", scene_id, e)
return None return None
def save(self, scene: Scene) -> bool: def save(self, scene: Scene) -> bool:
scene.updated_at = datetime.now().isoformat() scene.updated_at = datetime.now().isoformat()
filepath = SCENES_DIR / f"{scene.id}.json" item = SCENES_DIR / scene.id
item.mkdir(parents=True, exist_ok=True)
try: try:
with open(filepath, "w", encoding="utf-8") as f: with open(item / "scene.json", "w", encoding="utf-8") as f:
json.dump(scene.to_dict(), f, indent=2, ensure_ascii=False) json.dump(scene.to_dict(), f, indent=2, ensure_ascii=False)
logger.info("Scene saved: %s", scene.id) logger.info("Scene saved: %s", scene.id)
return True return True
@@ -120,20 +137,23 @@ class SceneManager:
return False return False
def delete(self, scene_id: str) -> bool: def delete(self, scene_id: str) -> bool:
filepath = SCENES_DIR / f"{scene_id}.json" import shutil
if filepath.exists(): item = SCENES_DIR / scene_id
filepath.unlink() if item.exists():
shutil.rmtree(item)
logger.info("Scene deleted: %s", scene_id) logger.info("Scene deleted: %s", scene_id)
return True return True
return False return False
def export_scene_zip(self, scene_id: str) -> bytes | None: def export_scene_zip(self, scene_id: str) -> bytes | None:
filepath = SCENES_DIR / f"{scene_id}.json" item = SCENES_DIR / scene_id
if not filepath.exists(): if not item.exists():
return None return None
buf = io.BytesIO() buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf: with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
zf.write(filepath, "scene.json") for f in sorted(item.rglob("*")):
if f.is_file():
zf.write(f, f.relative_to(item))
logger.info("Scene exported as zip: %s", scene_id) logger.info("Scene exported as zip: %s", scene_id)
return buf.getvalue() return buf.getvalue()
@@ -145,7 +165,17 @@ class SceneManager:
return False return False
data = json.loads(zf.read("scene.json").decode('utf-8')) data = json.loads(zf.read("scene.json").decode('utf-8'))
scene = Scene.from_dict(data) scene = Scene.from_dict(data)
return self.save(scene) item = SCENES_DIR / scene.id
if item.exists():
import shutil; shutil.rmtree(item)
item.mkdir(parents=True)
for name in zf.namelist():
if name.endswith('/'): continue
dest = item / name
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(zf.read(name))
logger.info("Scene imported from zip: %s", scene.id)
return True
except Exception as e: except Exception as e:
logger.error("Failed to import scene zip: %s", e) logger.error("Failed to import scene zip: %s", e)
return False return False
+3 -1
View File
@@ -215,8 +215,10 @@ const PageScene = {
_sceneCardHtml(scene) { _sceneCardHtml(scene) {
const canvasCount = (scene.canvases || []).length; const canvasCount = (scene.canvases || []).length;
const preview = scene.preview_ext ? `/scene/${scene.id}/preview.${scene.preview_ext}` : '';
return `<div class="glass-card scene-card" data-scene-id="${scene.id}" data-action="render"> return `<div class="glass-card scene-card" data-scene-id="${scene.id}" data-action="render">
<div class="scene-card-name">${scene.name}</div> ${preview ? `<div style="height:120px;background:url(${preview}) center/cover;border-radius:8px 8px 0 0"></div>` : ''}
<div class="scene-card-name" style="${preview?'':'padding-top:18px'}">${scene.name}</div>
<div class="scene-card-game" style="margin-bottom:6px">${this._currentGameName || '未关联游戏'}</div> <div class="scene-card-game" style="margin-bottom:6px">${this._currentGameName || '未关联游戏'}</div>
<div class="scene-card-meta"><span>${canvasCount} 画布</span><span>${scene.updated_at ? new Date(scene.updated_at).toLocaleDateString() : ''}</span></div> <div class="scene-card-meta"><span>${canvasCount} 画布</span><span>${scene.updated_at ? new Date(scene.updated_at).toLocaleDateString() : ''}</span></div>
<div class="scene-canvas-list">${(scene.canvases||[]).map(c => `<span class="scene-canvas-badge">${c.label||c.width+'x'+c.height}</span>`).join('')}</div> <div class="scene-canvas-list">${(scene.canvases||[]).map(c => `<span class="scene-canvas-badge">${c.label||c.width+'x'+c.height}</span>`).join('')}</div>