refactor: scenes now folder-based with preview image support
This commit is contained in:
+47
-17
@@ -84,34 +84,51 @@ class SceneManager:
|
||||
|
||||
def list_all(self, game_id: str | None = None) -> list[dict[str, Any]]:
|
||||
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:
|
||||
with open(f, "r", encoding="utf-8") as fp:
|
||||
data = json.load(fp)
|
||||
scene = Scene.from_dict(data)
|
||||
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:
|
||||
logger.error("Failed to load scene %s: %s", f.name, e)
|
||||
logger.error("Failed to load scene %s: %s", item.name, e)
|
||||
return scenes
|
||||
|
||||
def get(self, scene_id: str) -> dict[str, Any] | None:
|
||||
filepath = SCENES_DIR / f"{scene_id}.json"
|
||||
if not filepath.exists():
|
||||
item = SCENES_DIR / scene_id
|
||||
f = item / "scene.json"
|
||||
if not f.exists():
|
||||
return None
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return Scene.from_dict(data).to_dict()
|
||||
with open(f, "r", encoding="utf-8") as fp:
|
||||
data = json.load(fp)
|
||||
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:
|
||||
logger.error("Failed to read scene %s: %s", scene_id, e)
|
||||
return None
|
||||
|
||||
def save(self, scene: Scene) -> bool:
|
||||
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:
|
||||
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)
|
||||
logger.info("Scene saved: %s", scene.id)
|
||||
return True
|
||||
@@ -120,20 +137,23 @@ class SceneManager:
|
||||
return False
|
||||
|
||||
def delete(self, scene_id: str) -> bool:
|
||||
filepath = SCENES_DIR / f"{scene_id}.json"
|
||||
if filepath.exists():
|
||||
filepath.unlink()
|
||||
import shutil
|
||||
item = SCENES_DIR / scene_id
|
||||
if item.exists():
|
||||
shutil.rmtree(item)
|
||||
logger.info("Scene deleted: %s", scene_id)
|
||||
return True
|
||||
return False
|
||||
|
||||
def export_scene_zip(self, scene_id: str) -> bytes | None:
|
||||
filepath = SCENES_DIR / f"{scene_id}.json"
|
||||
if not filepath.exists():
|
||||
item = SCENES_DIR / scene_id
|
||||
if not item.exists():
|
||||
return None
|
||||
buf = io.BytesIO()
|
||||
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)
|
||||
return buf.getvalue()
|
||||
|
||||
@@ -145,7 +165,17 @@ class SceneManager:
|
||||
return False
|
||||
data = json.loads(zf.read("scene.json").decode('utf-8'))
|
||||
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:
|
||||
logger.error("Failed to import scene zip: %s", e)
|
||||
return False
|
||||
|
||||
@@ -215,8 +215,10 @@ const PageScene = {
|
||||
|
||||
_sceneCardHtml(scene) {
|
||||
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">
|
||||
<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-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>
|
||||
|
||||
Reference in New Issue
Block a user