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]]:
|
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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user