From 62c71a02c34d5f9259fb3b555876137d05edb63b Mon Sep 17 00:00:00 2001 From: Pabloader Date: Tue, 7 Apr 2026 08:01:22 +0000 Subject: [PATCH] Import/Export --- src/games/storywriter/components/menu.tsx | 46 ++++++++++++++++++++++- src/games/storywriter/contexts/state.tsx | 15 +++++++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/games/storywriter/components/menu.tsx b/src/games/storywriter/components/menu.tsx index 04dbc17..23ebd93 100644 --- a/src/games/storywriter/components/menu.tsx +++ b/src/games/storywriter/components/menu.tsx @@ -6,7 +6,7 @@ import { useBool } from "@common/hooks/useBool"; import { useInputState } from "@common/hooks/useInputState"; import type { World, Story } from "../contexts/state"; import styles from '../assets/menu.module.css'; -import { Pencil, X, Plus, Plug, Settings, Copy, ChevronRight, ChevronDown, Globe } from "lucide-preact"; +import { Pencil, X, Plus, Plug, Settings, Copy, ChevronRight, ChevronDown, Globe, Download, Upload } from "lucide-preact"; // ─── Inline Rename Input ────────────────────────────────────────────────────── @@ -101,6 +101,7 @@ interface WorldItemProps { onSelectWorld: () => void; onRenameWorld: (title: string) => void; onDeleteWorld: () => void; + onExportWorld: () => void; onCreateStory: () => void; onSelectStory: (storyId: string) => void; onRenameStory: (storyId: string, title: string) => void; @@ -110,7 +111,7 @@ interface WorldItemProps { const WorldItem = ({ world, activeWorldId, activeStoryId, - onSelectWorld, onRenameWorld, onDeleteWorld, onCreateStory, + onSelectWorld, onRenameWorld, onDeleteWorld, onExportWorld, onCreateStory, onSelectStory, onRenameStory, onDeleteStory, onDuplicateStory, }: WorldItemProps) => { const isRenaming = useBool(false); @@ -150,6 +151,9 @@ const WorldItem = ({ + @@ -233,11 +237,48 @@ export const Menu = () => { dispatch({ type: 'DUPLICATE_STORY', worldId, id: storyId }); }; + const handleExportWorld = (worldId: string) => { + const world = worlds.find(w => w.id === worldId); + if (!world) return; + const json = JSON.stringify(world, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${world.title}.json`; + a.click(); + URL.revokeObjectURL(url); + }; + + const handleImportWorld = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json,application/json'; + input.onchange = () => { + const file = input.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { + try { + const world = JSON.parse(reader.result as string); + dispatch({ type: 'IMPORT_WORLD', world }); + } catch { + alert('Invalid world file.'); + } + }; + reader.readAsText(file); + }; + input.click(); + }; + return (
+
{worlds.map(world => ( { onSelectWorld={() => handleSelectWorld(world.id)} onRenameWorld={(title) => handleRenameWorld(world.id, title)} onDeleteWorld={() => handleDeleteWorld(world.id)} + onExportWorld={() => handleExportWorld(world.id)} onCreateStory={() => handleCreateStory(world.id)} onSelectStory={(storyId) => handleSelectStory(world.id, storyId)} onRenameStory={(storyId, title) => handleRenameStory(world.id, storyId, title)} diff --git a/src/games/storywriter/contexts/state.tsx b/src/games/storywriter/contexts/state.tsx index f71c9df..c9898e9 100644 --- a/src/games/storywriter/contexts/state.tsx +++ b/src/games/storywriter/contexts/state.tsx @@ -149,7 +149,9 @@ type Action = | { type: 'DELETE_LOCATION'; worldId: string; storyId: string | null; locationId: string } // Chapters | { type: 'STORE_CHAPTER_SUMMARY'; worldId: string; storyId: string; header: string; hash: Chapters.Hash; summary: string } - | { type: 'CLEAN_CHAPTER_SUMMARIES'; worldId: string; storyId: string; validHashes: Record }; + | { type: 'CLEAN_CHAPTER_SUMMARIES'; worldId: string; storyId: string; validHashes: Record } + // Import/Export + | { type: 'IMPORT_WORLD'; world: World }; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -498,6 +500,17 @@ function reducer(state: IState, action: Action): IState { return { ...s, chapters }; }); } + case 'IMPORT_WORLD': { + const exists = state.worlds.some(w => w.id === action.world.id); + const world = exists ? { ...action.world, id: crypto.randomUUID() } : action.world; + return { + ...state, + worlds: [...state.worlds, world], + currentWorldId: world.id, + currentStoryId: null, + currentTab: 'lore', + }; + } } }