Import/Export
This commit is contained in:
parent
d93e3c812e
commit
62c71a02c3
|
|
@ -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 = ({
|
|||
<button class={styles.actionButton} onClick={onCreateStory} title="New Story">
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
<button class={styles.actionButton} onClick={onExportWorld} title="Export World">
|
||||
<Download size={14} />
|
||||
</button>
|
||||
<button class={styles.actionButton} onClick={isRenaming.setTrue} title="Rename">
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
|
|
@ -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 (
|
||||
<div class={styles.menu}>
|
||||
<button class={styles.newButton} onClick={handleCreateWorld}>
|
||||
<Plus size={16} /> New World
|
||||
</button>
|
||||
<button class={styles.newButton} onClick={handleImportWorld}>
|
||||
<Upload size={16} /> Import World
|
||||
</button>
|
||||
<div class={styles.list}>
|
||||
{worlds.map(world => (
|
||||
<WorldItem
|
||||
|
|
@ -248,6 +289,7 @@ export const Menu = () => {
|
|||
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)}
|
||||
|
|
|
|||
|
|
@ -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<string, Chapters.Hash[]> };
|
||||
| { type: 'CLEAN_CHAPTER_SUMMARIES'; worldId: string; storyId: string; validHashes: Record<string, Chapters.Hash[]> }
|
||||
// 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',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue