'use client' import React, {ChangeEvent, forwardRef, useContext, useEffect, useImperativeHandle, useState} from 'react'; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faPlus, faShare, faToggleOn, IconDefinition} from "@fortawesome/free-solid-svg-icons"; import {WorldContext} from '@/context/WorldContext'; import {BookContext} from "@/context/BookContext"; import {AlertContext} from "@/context/AlertContext"; import {SelectBoxProps} from "@/shared/interface"; import System from "@/lib/models/System"; import {elementSections, WorldListResponse, WorldProps} from "@/lib/models/World"; import {SessionContext} from "@/context/SessionContext"; import InputField from "@/components/form/InputField"; import TextInput from '@/components/form/TextInput'; import TexteAreaInput from "@/components/form/TexteAreaInput"; import WorldElementComponent from './WorldElement'; import SelectBox from "@/components/form/SelectBox"; import {useTranslations} from "next-intl"; import {LangContext, LangContextProps} from "@/context/LangContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; import {SyncedBook} from "@/lib/models/SyncedBook"; import ToggleSwitch from "@/components/form/ToggleSwitch"; import {SeriesWorldProps, SeriesWorldListItem} from "@/lib/models/Series"; import SeriesImportSelector from "@/components/form/SeriesImportSelector"; import SyncFieldWrapper from "@/components/form/SyncFieldWrapper"; export interface ElementSection { title: string; section: keyof WorldProps; icon: IconDefinition; } interface WorldSettingProps { showToggle?: boolean; entityType?: 'book' | 'series'; entityId?: string; } export function WorldSetting(props: WorldSettingProps, ref: React.Ref<{ handleSave: () => Promise }>) { const {showToggle = true, entityType = 'book', entityId} = props; const t = useTranslations(); const {lang} = useContext(LangContext); const {isCurrentlyOffline} = useContext(OfflineContext); const {addToQueue} = useContext(LocalSyncQueueContext); const {localSyncedBooks} = useContext(BooksSyncContext); const {errorMessage, successMessage} = useContext(AlertContext); const {session} = useContext(SessionContext); const {book, setBook} = useContext(BookContext); const currentEntityId: string = entityId || book?.bookId || ''; const isSeriesMode: boolean = entityType === 'series'; const [worlds, setWorlds] = useState([]); const [seriesWorlds, setSeriesWorlds] = useState([]); const [newWorldName, setNewWorldName] = useState(''); const [selectedWorldIndex, setSelectedWorldIndex] = useState(0); const [worldsSelector, setWorldsSelector] = useState([]); const [showAddNewWorld, setShowAddNewWorld] = useState(false); const [toolEnabled, setToolEnabled] = useState(isSeriesMode ? true : (book?.tools?.worlds ?? false)); const bookSeriesId: string | null = book?.seriesId || null; useImperativeHandle(ref, function () { return { handleSave: handleUpdateWorld, }; }); useEffect((): void => { if (currentEntityId) { getWorlds().then(); } }, [currentEntityId]); useEffect((): void => { if (bookSeriesId && !isSeriesMode && !isCurrentlyOffline()) { getSeriesWorlds().then(); } }, [bookSeriesId]); async function getSeriesWorlds(): Promise { if (!bookSeriesId || isCurrentlyOffline()) return; try { const response: SeriesWorldProps[] = await System.authGetQueryToServer('series/world/list', session.accessToken, lang, { seriesid: bookSeriesId }); if (response) { setSeriesWorlds(response); } } catch (e: unknown) { if (e instanceof Error) { console.error('Error loading series worlds:', e.message); } } } async function handleToggleTool(enabled: boolean): Promise { if (isSeriesMode) return; try { let response: boolean; if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:book:tool:update', { bookId: currentEntityId, toolName: 'worlds', enabled: enabled }); } else { response = await System.authPatchToServer('book/tool-setting', { bookId: currentEntityId, toolName: 'worlds', enabled: enabled }, session.accessToken, lang); if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) { addToQueue('db:book:tool:update', { bookId: currentEntityId, toolName: 'worlds', enabled: enabled }); } } if (response && setBook && book) { setToolEnabled(enabled); setBook({ ...book, tools: { characters: book.tools?.characters ?? false, worlds: enabled, locations: book.tools?.locations ?? false, spells: book.tools?.spells ?? false } }); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } } async function getWorlds(): Promise { try { if (isSeriesMode) { // Series mode: server only (series are not local) const response: SeriesWorldProps[] = await System.authGetQueryToServer('series/world/list', session.accessToken, lang, { seriesid: currentEntityId }); if (response) { const mappedWorlds: WorldProps[] = response.map((world: SeriesWorldProps): WorldProps => ({ id: world.id, name: world.name, history: world.history || '', politics: world.politics || '', economy: world.economy || '', religion: world.religion || '', languages: world.languages || '', laws: world.laws || [], biomes: world.biomes || [], issues: world.issues || [], customs: world.customs || [], kingdoms: world.kingdoms || [], climate: world.climate || [], resources: world.resources || [], wildlife: world.wildlife || [], arts: world.arts || [], ethnicGroups: world.ethnicGroups || [], socialClasses: world.socialClasses || [], importantCharacters: world.importantCharacters || [], })); setWorlds(mappedWorlds); const formattedWorlds: SelectBoxProps[] = response.map((world: SeriesWorldProps): SelectBoxProps => ({ label: world.name, value: world.id, })); setWorldsSelector(formattedWorlds); } } else { // Book mode: dual offline/online logic let response: WorldListResponse; if (isCurrentlyOffline() || book?.localBook) { response = await window.electron.invoke('db:book:worlds:get', {bookid: currentEntityId}); } else { response = await System.authGetQueryToServer('book/worlds', session.accessToken, lang, { bookid: currentEntityId, }); } if (response) { setWorlds(response.worlds); setToolEnabled(response.enabled); if (setBook && book) { setBook({ ...book, tools: { characters: book.tools?.characters ?? false, worlds: response.enabled, locations: book.tools?.locations ?? false, spells: book.tools?.spells ?? false } }); } const formattedWorlds: SelectBoxProps[] = response.worlds.map( (world: WorldProps): SelectBoxProps => ({ label: world.name, value: world.id.toString(), }), ); setWorldsSelector(formattedWorlds); } } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("worldSetting.unknownError")) } } } async function handleAddNewWorld(): Promise { if (newWorldName.trim() === '') { errorMessage(t("worldSetting.newWorldNameError")); return; } try { let newWorldId: string; if (isSeriesMode) { // Series mode: server only newWorldId = await System.authPostToServer( 'series/world/add', { seriesId: currentEntityId, name: newWorldName, }, session.accessToken, lang ); if (!newWorldId) { errorMessage(t("worldSetting.addWorldError")); return; } } else if (isCurrentlyOffline() || book?.localBook) { // Book mode: offline/local newWorldId = await window.electron.invoke('db:book:world:add', { worldName: newWorldName, bookId: currentEntityId, }); if (!newWorldId) { errorMessage(t("worldSetting.addWorldError")); return; } } else { // Book mode: online newWorldId = await System.authPostToServer('book/world/add', { worldName: newWorldName, bookId: currentEntityId, }, session.accessToken, lang); if (!newWorldId) { errorMessage(t("worldSetting.addWorldError")); return; } if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) { addToQueue('db:book:world:add', { worldName: newWorldName, worldId: newWorldId, bookId: currentEntityId, }); } } const newWorld: WorldProps = { id: newWorldId, name: newWorldName, history: '', politics: '', economy: '', religion: '', languages: '', laws: [], biomes: [], issues: [], customs: [], kingdoms: [], climate: [], resources: [], wildlife: [], arts: [], ethnicGroups: [], socialClasses: [], importantCharacters: [], }; setWorlds([...worlds, newWorld]); setWorldsSelector([ ...worldsSelector, {label: newWorldName, value: newWorldId.toString()}, ]); setNewWorldName(''); setShowAddNewWorld(false); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("worldSetting.unknownError")) } } } async function handleUpdateWorld(): Promise { if (worlds.length === 0) return; try { const currentWorld: WorldProps = worlds[selectedWorldIndex]; let response: boolean; if (isSeriesMode) { // Series mode: server only response = await System.authPatchToServer('series/world/update', { worldId: currentWorld.id, name: currentWorld.name, history: currentWorld.history, politics: currentWorld.politics, economy: currentWorld.economy, religion: currentWorld.religion, languages: currentWorld.languages, }, session.accessToken, lang); } else if (isCurrentlyOffline() || book?.localBook) { // Book mode: offline/local response = await window.electron.invoke('db:book:world:update', { world: currentWorld, bookId: currentEntityId, }); } else { // Book mode: online response = await System.authPatchToServer('book/world/update', { world: currentWorld, bookId: currentEntityId, }, session.accessToken, lang); if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) { addToQueue('db:book:world:update', { world: currentWorld, bookId: currentEntityId, }); } } if (!response) { errorMessage(t("worldSetting.updateWorldError")); return; } successMessage(t("worldSetting.updateWorldSuccess")); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("worldSetting.unknownError")) } } } function handleWorldSelect(worldId: string): void { const index: number = worlds.findIndex((world: WorldProps): boolean => world.id === worldId); if (index !== -1) { setSelectedWorldIndex(index); } } function handleInputChange(value: string, field: keyof WorldProps) { const updatedWorlds = [...worlds] as WorldProps[]; (updatedWorlds[selectedWorldIndex][field] as string) = value; setWorlds(updatedWorlds); } async function handleExportToSeries(): Promise { if (isCurrentlyOffline()) return; const selectedWorld: WorldProps | undefined = worlds[selectedWorldIndex]; if (!selectedWorld || !bookSeriesId) return; try { const seriesWorldId: string = await System.authPostToServer('series/world/add', { seriesId: bookSeriesId, world: { name: selectedWorld.name, history: selectedWorld.history || null, politics: selectedWorld.politics || null, economy: selectedWorld.economy || null, religion: selectedWorld.religion || null, languages: selectedWorld.languages || null, } }, session.accessToken, lang); if (seriesWorldId) { const updateResponse: boolean = await System.authPostToServer('book/world/update', { world: { ...selectedWorld, seriesWorldId: seriesWorldId }, }, session.accessToken, lang); if (updateResponse) { const updatedWorlds: WorldProps[] = [...worlds]; updatedWorlds[selectedWorldIndex] = {...selectedWorld, seriesWorldId: seriesWorldId}; setWorlds(updatedWorlds); await getSeriesWorlds(); successMessage(t("worldSetting.exportSuccess")); } } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } } async function handleImportFromSeries(seriesWorldId: string): Promise { if (isCurrentlyOffline()) return; const seriesWorld: SeriesWorldListItem | undefined = seriesWorlds.find((w) => w.id === seriesWorldId); if (!seriesWorld) return; try { const worldId: string = await System.authPostToServer('book/world/add', { worldName: seriesWorld.name, bookId: currentEntityId, seriesWorldId: seriesWorldId, }, session.accessToken, lang); if (!worldId) { errorMessage(t("worldSetting.importError")); return; } // Sync to local if book is synced if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) { addToQueue('db:book:world:add', { worldName: seriesWorld.name, worldId: worldId, bookId: currentEntityId, }); } const newWorld: WorldProps = { id: worldId, name: seriesWorld.name, history: seriesWorld.history || '', politics: seriesWorld.politics || '', economy: seriesWorld.economy || '', religion: seriesWorld.religion || '', languages: seriesWorld.languages || '', laws: [], biomes: [], issues: [], customs: [], kingdoms: [], climate: [], resources: [], wildlife: [], arts: [], ethnicGroups: [], socialClasses: [], importantCharacters: [], seriesWorldId: seriesWorldId, }; setWorlds([...worlds, newWorld]); setWorldsSelector([ ...worldsSelector, {label: seriesWorld.name, value: worldId}, ]); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } } function getSeriesWorldForCurrentWorld(): SeriesWorldProps | null { const currentWorld: WorldProps = worlds[selectedWorldIndex]; if (!currentWorld?.seriesWorldId) return null; return seriesWorlds.find((world: SeriesWorldListItem): boolean => world.id === currentWorld.seriesWorldId) || null; } return (
{showToggle && !isSeriesMode && (
=> handleToggleTool(checked)} /> } />

{t('worldSetting.enableToolDescription')}

)} {(toolEnabled || isSeriesMode) && ( <> {!isSeriesMode && bookSeriesId && !isCurrentlyOffline() && seriesWorlds.filter((seriesWorld: SeriesWorldProps): boolean => !worlds.some((world: WorldProps): boolean => world.seriesWorldId === seriesWorld.id)).length > 0 && ( !worlds.some((world: WorldProps): boolean => world.seriesWorldId === seriesWorld.id)) .map((seriesWorld: SeriesWorldProps) => ({id: seriesWorld.id, name: seriesWorld.name}))} onImport={handleImportFromSeries} placeholder={t("seriesImport.selectElement")} label={t("seriesImport.importFromSeries")} /> )}
handleWorldSelect(e.target.value)} data={worldsSelector.length > 0 ? worldsSelector : [{ label: t("worldSetting.noWorldAvailable"), value: '0' }]} defaultValue={worlds[selectedWorldIndex]?.id.toString() || '0'} placeholder={t("worldSetting.selectWorldPlaceholder")} /> } actionIcon={faPlus} actionLabel={t("worldSetting.addWorldLabel")} action={async () => setShowAddNewWorld(!showAddNewWorld)} /> {showAddNewWorld && ( ) => setNewWorldName(e.target.value)} placeholder={t("worldSetting.newWorldPlaceholder")} /> } actionIcon={faPlus} actionLabel={t("worldSetting.createWorldLabel")} addButtonCallBack={handleAddNewWorld} /> )} {!isSeriesMode && bookSeriesId && !isCurrentlyOffline() && worlds[selectedWorldIndex] && !worlds[selectedWorldIndex].seriesWorldId && ( )}
{worlds.length > 0 && worlds[selectedWorldIndex] ? (
{ const seriesWorld = getSeriesWorldForCurrentWorld(); if (seriesWorld) { const updatedWorlds: WorldProps[] = [...worlds]; updatedWorlds[selectedWorldIndex].name = seriesWorld.name; setWorlds(updatedWorlds); } }} onSyncComplete={getSeriesWorlds} > ) => { const updatedWorlds: WorldProps[] = [...worlds]; updatedWorlds[selectedWorldIndex].name = e.target.value setWorlds(updatedWorlds); }} placeholder={t("worldSetting.worldNamePlaceholder")} /> } />
{ const seriesWorld = getSeriesWorldForCurrentWorld(); if (seriesWorld) handleInputChange(seriesWorld.history || '', 'history'); }} onSyncComplete={getSeriesWorlds} > handleInputChange(e.target.value, 'history')} placeholder={t("worldSetting.worldHistoryPlaceholder")} /> } />
{ const seriesWorld = getSeriesWorldForCurrentWorld(); if (seriesWorld) handleInputChange(seriesWorld.politics || '', 'politics'); }} onSyncComplete={getSeriesWorlds} > handleInputChange(e.target.value, 'politics')} placeholder={t("worldSetting.politicsPlaceholder")} /> } /> { const seriesWorld = getSeriesWorldForCurrentWorld(); if (seriesWorld) handleInputChange(seriesWorld.economy || '', 'economy'); }} onSyncComplete={getSeriesWorlds} > handleInputChange(e.target.value, 'economy')} placeholder={t("worldSetting.economyPlaceholder")} /> } />
{ const seriesWorld = getSeriesWorldForCurrentWorld(); if (seriesWorld) handleInputChange(seriesWorld.religion || '', 'religion'); }} onSyncComplete={getSeriesWorlds} > handleInputChange(e.target.value, 'religion')} placeholder={t("worldSetting.religionPlaceholder")} /> } /> { const seriesWorld = getSeriesWorldForCurrentWorld(); if (seriesWorld) handleInputChange(seriesWorld.languages || '', 'languages'); }} onSyncComplete={getSeriesWorlds} > handleInputChange(e.target.value, 'languages')} placeholder={t("worldSetting.languagesPlaceholder")} /> } />
{elementSections.map((section, index) => (

{section.title} {worlds[selectedWorldIndex][section.section]?.length || 0}

))}
) : (

{t("worldSetting.noWorldAvailable")}

)} )}
); } export default forwardRef(WorldSetting);