1
0
Fork 0

Import/Export

This commit is contained in:
Pabloader 2026-04-07 08:01:22 +00:00
parent d93e3c812e
commit 62c71a02c3
2 changed files with 58 additions and 3 deletions

View File

@ -6,7 +6,7 @@ import { useBool } from "@common/hooks/useBool";
import { useInputState } from "@common/hooks/useInputState"; import { useInputState } from "@common/hooks/useInputState";
import type { World, Story } from "../contexts/state"; import type { World, Story } from "../contexts/state";
import styles from '../assets/menu.module.css'; 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 ────────────────────────────────────────────────────── // ─── Inline Rename Input ──────────────────────────────────────────────────────
@ -101,6 +101,7 @@ interface WorldItemProps {
onSelectWorld: () => void; onSelectWorld: () => void;
onRenameWorld: (title: string) => void; onRenameWorld: (title: string) => void;
onDeleteWorld: () => void; onDeleteWorld: () => void;
onExportWorld: () => void;
onCreateStory: () => void; onCreateStory: () => void;
onSelectStory: (storyId: string) => void; onSelectStory: (storyId: string) => void;
onRenameStory: (storyId: string, title: string) => void; onRenameStory: (storyId: string, title: string) => void;
@ -110,7 +111,7 @@ interface WorldItemProps {
const WorldItem = ({ const WorldItem = ({
world, activeWorldId, activeStoryId, world, activeWorldId, activeStoryId,
onSelectWorld, onRenameWorld, onDeleteWorld, onCreateStory, onSelectWorld, onRenameWorld, onDeleteWorld, onExportWorld, onCreateStory,
onSelectStory, onRenameStory, onDeleteStory, onDuplicateStory, onSelectStory, onRenameStory, onDeleteStory, onDuplicateStory,
}: WorldItemProps) => { }: WorldItemProps) => {
const isRenaming = useBool(false); const isRenaming = useBool(false);
@ -150,6 +151,9 @@ const WorldItem = ({
<button class={styles.actionButton} onClick={onCreateStory} title="New Story"> <button class={styles.actionButton} onClick={onCreateStory} title="New Story">
<Plus size={14} /> <Plus size={14} />
</button> </button>
<button class={styles.actionButton} onClick={onExportWorld} title="Export World">
<Download size={14} />
</button>
<button class={styles.actionButton} onClick={isRenaming.setTrue} title="Rename"> <button class={styles.actionButton} onClick={isRenaming.setTrue} title="Rename">
<Pencil size={14} /> <Pencil size={14} />
</button> </button>
@ -233,11 +237,48 @@ export const Menu = () => {
dispatch({ type: 'DUPLICATE_STORY', worldId, id: storyId }); 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 ( return (
<div class={styles.menu}> <div class={styles.menu}>
<button class={styles.newButton} onClick={handleCreateWorld}> <button class={styles.newButton} onClick={handleCreateWorld}>
<Plus size={16} /> New World <Plus size={16} /> New World
</button> </button>
<button class={styles.newButton} onClick={handleImportWorld}>
<Upload size={16} /> Import World
</button>
<div class={styles.list}> <div class={styles.list}>
{worlds.map(world => ( {worlds.map(world => (
<WorldItem <WorldItem
@ -248,6 +289,7 @@ export const Menu = () => {
onSelectWorld={() => handleSelectWorld(world.id)} onSelectWorld={() => handleSelectWorld(world.id)}
onRenameWorld={(title) => handleRenameWorld(world.id, title)} onRenameWorld={(title) => handleRenameWorld(world.id, title)}
onDeleteWorld={() => handleDeleteWorld(world.id)} onDeleteWorld={() => handleDeleteWorld(world.id)}
onExportWorld={() => handleExportWorld(world.id)}
onCreateStory={() => handleCreateStory(world.id)} onCreateStory={() => handleCreateStory(world.id)}
onSelectStory={(storyId) => handleSelectStory(world.id, storyId)} onSelectStory={(storyId) => handleSelectStory(world.id, storyId)}
onRenameStory={(storyId, title) => handleRenameStory(world.id, storyId, title)} onRenameStory={(storyId, title) => handleRenameStory(world.id, storyId, title)}

View File

@ -149,7 +149,9 @@ type Action =
| { type: 'DELETE_LOCATION'; worldId: string; storyId: string | null; locationId: string } | { type: 'DELETE_LOCATION'; worldId: string; storyId: string | null; locationId: string }
// Chapters // Chapters
| { type: 'STORE_CHAPTER_SUMMARY'; worldId: string; storyId: string; header: string; hash: Chapters.Hash; summary: string } | { 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<string, Chapters.Hash[]> }; | { type: 'CLEAN_CHAPTER_SUMMARIES'; worldId: string; storyId: string; validHashes: Record<string, Chapters.Hash[]> }
// Import/Export
| { type: 'IMPORT_WORLD'; world: World };
// ─── Helpers ───────────────────────────────────────────────────────────────── // ─── Helpers ─────────────────────────────────────────────────────────────────
@ -498,6 +500,17 @@ function reducer(state: IState, action: Action): IState {
return { ...s, chapters }; 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',
};
}
} }
} }