Add named layouts: save/load/delete scene+playlist profiles
This commit is contained in:
@@ -15,7 +15,21 @@ const MODELS_SEED = path.join(DATA, 'models.json');
|
|||||||
const CHARS_LIVE = path.join(LIVE_DIR, 'characters.live.json');
|
const CHARS_LIVE = path.join(LIVE_DIR, 'characters.live.json');
|
||||||
const LANDING_LIVE = path.join(LIVE_DIR, 'landing.live.json');
|
const LANDING_LIVE = path.join(LIVE_DIR, 'landing.live.json');
|
||||||
|
|
||||||
|
/* Named layouts: each is a self-contained { name, scene, playlist } saved under
|
||||||
|
* layouts/<slug>.json. layouts.index.json tracks the list + which slug is active.
|
||||||
|
* The active layout's scene/playlist are mirrored into scene.live.json /
|
||||||
|
* playlist.live.json so viewers and the playlist engine need no changes. */
|
||||||
|
const LAYOUTS_DIR = path.join(LIVE_DIR, 'layouts');
|
||||||
|
const LAYOUTS_INDEX = path.join(LIVE_DIR, 'layouts.index.json');
|
||||||
|
|
||||||
fs.mkdirSync(LIVE_DIR, { recursive: true });
|
fs.mkdirSync(LIVE_DIR, { recursive: true });
|
||||||
|
fs.mkdirSync(LAYOUTS_DIR, { recursive: true });
|
||||||
|
|
||||||
|
function slugify(name) {
|
||||||
|
const s = String(name || '').toLowerCase().trim()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60);
|
||||||
|
return s || 'layout';
|
||||||
|
}
|
||||||
|
|
||||||
function readJSON(p, fallback) {
|
function readJSON(p, fallback) {
|
||||||
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fallback; }
|
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fallback; }
|
||||||
@@ -53,6 +67,64 @@ let landing = Object.assign({}, LANDING_DEFAULTS, readJSON(LANDING_LIVE, null) |
|
|||||||
|
|
||||||
const listeners = [];
|
const listeners = [];
|
||||||
|
|
||||||
|
/* ---------- named layouts ---------- */
|
||||||
|
function readLayoutIndex() {
|
||||||
|
const idx = readJSON(LAYOUTS_INDEX, null);
|
||||||
|
if (idx && Array.isArray(idx.layouts)) return idx;
|
||||||
|
return { layouts: [], activeSlug: null };
|
||||||
|
}
|
||||||
|
function writeLayoutIndex(idx) { writeJSON(LAYOUTS_INDEX, idx); }
|
||||||
|
function layoutPath(slug) { return path.join(LAYOUTS_DIR, `${slug}.json`); }
|
||||||
|
|
||||||
|
function listLayouts() {
|
||||||
|
const idx = readLayoutIndex();
|
||||||
|
return { layouts: idx.layouts, activeSlug: idx.activeSlug };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Snapshot the CURRENT live scene + playlist under a name. Overwrites if the
|
||||||
|
* slug already exists (same name). Returns the updated index. */
|
||||||
|
function saveLayout(name, playlistCfg) {
|
||||||
|
const slug = slugify(name);
|
||||||
|
const record = {
|
||||||
|
name: String(name || slug),
|
||||||
|
slug,
|
||||||
|
scene,
|
||||||
|
playlist: playlistCfg || readJSON(PLAYLIST_LIVE, null) || readJSON(path.join(DATA, 'playlist.json'), {}),
|
||||||
|
savedAt: Date.now(),
|
||||||
|
};
|
||||||
|
writeJSON(layoutPath(slug), record);
|
||||||
|
const idx = readLayoutIndex();
|
||||||
|
if (!idx.layouts.some(l => l.slug === slug)) idx.layouts.push({ slug, name: record.name });
|
||||||
|
else idx.layouts = idx.layouts.map(l => l.slug === slug ? { slug, name: record.name } : l);
|
||||||
|
idx.activeSlug = slug;
|
||||||
|
writeLayoutIndex(idx);
|
||||||
|
return listLayouts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make a saved layout active: mirror its scene into the live scene (so viewers
|
||||||
|
* update) and return its playlist so the caller can reload the engine. */
|
||||||
|
function loadLayout(slug) {
|
||||||
|
const rec = readJSON(layoutPath(slug), null);
|
||||||
|
if (!rec) throw new Error('layout not found: ' + slug);
|
||||||
|
scene = validateScene(rec.scene);
|
||||||
|
writeJSON(LIVE, scene);
|
||||||
|
if (rec.playlist) writeJSON(PLAYLIST_LIVE, rec.playlist);
|
||||||
|
const idx = readLayoutIndex();
|
||||||
|
idx.activeSlug = slug;
|
||||||
|
writeLayoutIndex(idx);
|
||||||
|
listeners.forEach(f => f());
|
||||||
|
return { scene, playlist: rec.playlist || null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteLayout(slug) {
|
||||||
|
try { fs.unlinkSync(layoutPath(slug)); } catch {}
|
||||||
|
const idx = readLayoutIndex();
|
||||||
|
idx.layouts = idx.layouts.filter(l => l.slug !== slug);
|
||||||
|
if (idx.activeSlug === slug) idx.activeSlug = null;
|
||||||
|
writeLayoutIndex(idx);
|
||||||
|
return listLayouts();
|
||||||
|
}
|
||||||
|
|
||||||
function validateScene(s) {
|
function validateScene(s) {
|
||||||
if (!s || typeof s !== 'object') throw new Error('scene must be object');
|
if (!s || typeof s !== 'object') throw new Error('scene must be object');
|
||||||
for (const k of ['anchors', 'buildings', 'spawns']) {
|
for (const k of ['anchors', 'buildings', 'spawns']) {
|
||||||
@@ -122,5 +194,9 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
getPlaylistConfig: (fallback) => readJSON(PLAYLIST_LIVE, null) || readJSON(path.join(DATA, 'playlist.json'), fallback),
|
getPlaylistConfig: (fallback) => readJSON(PLAYLIST_LIVE, null) || readJSON(path.join(DATA, 'playlist.json'), fallback),
|
||||||
putPlaylistConfig(cfg) { writeJSON(PLAYLIST_LIVE, cfg); return cfg; },
|
putPlaylistConfig(cfg) { writeJSON(PLAYLIST_LIVE, cfg); return cfg; },
|
||||||
|
listLayouts,
|
||||||
|
saveLayout,
|
||||||
|
loadLayout,
|
||||||
|
deleteLayout,
|
||||||
onChange: (f) => listeners.push(f),
|
onChange: (f) => listeners.push(f),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user