'use client' import React, {ChangeEvent, forwardRef, useContext, useEffect, useImperativeHandle, useState} from 'react'; import {LucideIcon, Plus, Share2, ToggleRight} from 'lucide-react'; import {WorldContext} from '@/context/WorldContext'; import {BookContext, BookContextProps} from "@/context/BookContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import SelectBox, {SelectBoxProps} from "@/components/form/SelectBox"; import {apiGet, apiPatch, apiPost} from "@/lib/api/client"; import {isDesktop} from '@/lib/configs'; import * as tauri from '@/lib/tauri'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import {ElementSection, WorldListResponse, WorldProps, WorldTextField} from "@/lib/types/world"; import {SettingRef} from "@/lib/types/settings"; import {elementSections} from "@/lib/constants/world"; import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import InputField from "@/components/form/InputField"; import TextInput from '@/components/form/TextInput'; import TextAreaInput from "@/components/form/TextAreaInput"; import WorldElementComponent from './WorldElement'; import {useTranslations} from '@/lib/i18n'; import {LangContext, LangContextProps} from "@/context/LangContext"; import ToggleSwitch from "@/components/form/ToggleSwitch"; import {SeriesWorldListItem, SeriesWorldProps} from "@/lib/types/series"; import SeriesImportSelector from "@/components/form/SeriesImportSelector"; import SyncFieldWrapper from "@/components/form/SyncFieldWrapper"; import Button from "@/components/ui/Button"; interface WorldSettingProps { showToggle?: boolean; entityType?: 'book' | 'series'; entityId?: string; } export function WorldSetting(props: WorldSettingProps, ref: React.ForwardedRef): React.JSX.Element { const {showToggle = true, entityType = 'book', entityId} = props; const t = useTranslations(); const {lang}: LangContextProps = useContext(LangContext); const {errorMessage, successMessage}: AlertContextProps = useContext(AlertContext); const {session}: SessionContextProps = useContext(SessionContext); const {book, setBook}: BookContextProps = useContext(BookContext); const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); 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, (): SettingRef => ({ handleSave: handleUpdateWorld, })); useEffect((): void => { if (currentEntityId) { getWorlds().then(); } }, [currentEntityId]); useEffect((): void => { if (bookSeriesId && !isSeriesMode) { getSeriesWorlds().then(); } }, [bookSeriesId]); async function getSeriesWorlds(): Promise { if (!bookSeriesId) return; try { const response: SeriesWorldProps[] = await apiGet('series/world/list', session.accessToken, lang, { seriesid: bookSeriesId }); if (response) { setSeriesWorlds(response); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } } async function handleToggleTool(enabled: boolean): Promise { if (isSeriesMode) return; try { let response: boolean; if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.updateBookToolSetting(currentEntityId, 'worlds', enabled); } else { response = await apiPatch('book/tool-setting', { bookId: currentEntityId, toolName: 'worlds', enabled: enabled }, session.accessToken, lang); } 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) { const response: SeriesWorldProps[] = await apiGet('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 if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { const response: WorldListResponse = await tauri.getWorlds(currentEntityId, toolEnabled) as WorldListResponse; 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); } } else { const response: WorldListResponse = await apiGet('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) { newWorldId = await apiPost( 'series/world/add', { seriesId: currentEntityId, name: newWorldName, }, session.accessToken, lang ); if (!newWorldId) { errorMessage(t("worldSetting.addWorldError")); return; } } else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { newWorldId = await tauri.addWorld(currentEntityId, newWorldName); if (!newWorldId) { errorMessage(t("worldSetting.addWorldError")); return; } } else { const worldId: string = await apiPost('book/world/add', { worldName: newWorldName, bookId: currentEntityId, }, session.accessToken, lang); if (!worldId) { errorMessage(t("worldSetting.addWorldError")); return; } newWorldId = worldId; } 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]; if (isSeriesMode) { const response: boolean = await apiPatch('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); if (!response) { errorMessage(t("worldSetting.updateWorldError")); return; } } else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { const response: boolean = await tauri.updateWorld(currentWorld); if (!response) { errorMessage(t("worldSetting.updateWorldError")); return; } } else { const response: boolean = await apiPatch('book/world/update', { world: currentWorld, bookId: currentEntityId, }, session.accessToken, lang); 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: WorldTextField): void { const updatedWorlds: WorldProps[] = [...worlds]; updatedWorlds[selectedWorldIndex][field] = value; setWorlds(updatedWorlds); } async function handleExportToSeries(): Promise { const selectedWorld: WorldProps | undefined = worlds[selectedWorldIndex]; if (!selectedWorld || !bookSeriesId) return; try { const seriesWorldId: string = await apiPost('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 apiPost('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 { const seriesWorld: SeriesWorldListItem | undefined = seriesWorlds.find((w: SeriesWorldListItem): boolean => w.id === seriesWorldId); if (!seriesWorld) return; try { const worldId: string = await apiPost('book/world/add', { worldName: seriesWorld.name, bookId: currentEntityId, seriesWorldId: seriesWorldId, }, session.accessToken, lang); if (!worldId) { errorMessage(t("worldSetting.importError")); return; } 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 && 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: string; name: string } => ({ id: seriesWorld.id, name: seriesWorld.name }))} onImport={handleImportFromSeries} placeholder={t("seriesImport.selectElement")} label={t("seriesImport.importFromSeries")} /> )}
): void => 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={Plus} actionLabel={t("worldSetting.addWorldLabel")} action={async (): Promise => setShowAddNewWorld(!showAddNewWorld)} /> {showAddNewWorld && ( ): void => setNewWorldName(e.target.value)} placeholder={t("worldSetting.newWorldPlaceholder")} /> } actionIcon={Plus} actionLabel={t("worldSetting.createWorldLabel")} addButtonCallBack={handleAddNewWorld} /> )} {!isSeriesMode && bookSeriesId && worlds[selectedWorldIndex] && !worlds[selectedWorldIndex].seriesWorldId && ( )}
{worlds.length > 0 && worlds[selectedWorldIndex] ? (
{ const seriesWorld: SeriesWorldProps | null = getSeriesWorldForCurrentWorld(); if (seriesWorld) { const updatedWorlds: WorldProps[] = [...worlds]; updatedWorlds[selectedWorldIndex].name = seriesWorld.name; setWorlds(updatedWorlds); } }} onSyncComplete={getSeriesWorlds} > ): void => { const updatedWorlds: WorldProps[] = [...worlds]; updatedWorlds[selectedWorldIndex].name = e.target.value setWorlds(updatedWorlds); }} placeholder={t("worldSetting.worldNamePlaceholder")} /> } />
{ const seriesWorld: SeriesWorldProps | null = getSeriesWorldForCurrentWorld(); if (seriesWorld) handleInputChange(seriesWorld.history || '', 'history'); }} onSyncComplete={getSeriesWorlds} > ): void => handleInputChange(e.target.value, 'history')} placeholder={t("worldSetting.worldHistoryPlaceholder")} /> } />
{ const seriesWorld: SeriesWorldProps | null = getSeriesWorldForCurrentWorld(); if (seriesWorld) handleInputChange(seriesWorld.politics || '', 'politics'); }} onSyncComplete={getSeriesWorlds} > ): void => handleInputChange(e.target.value, 'politics')} placeholder={t("worldSetting.politicsPlaceholder")} /> } /> { const seriesWorld: SeriesWorldProps | null = getSeriesWorldForCurrentWorld(); if (seriesWorld) handleInputChange(seriesWorld.economy || '', 'economy'); }} onSyncComplete={getSeriesWorlds} > ): void => handleInputChange(e.target.value, 'economy')} placeholder={t("worldSetting.economyPlaceholder")} /> } />
{ const seriesWorld: SeriesWorldProps | null = getSeriesWorldForCurrentWorld(); if (seriesWorld) handleInputChange(seriesWorld.religion || '', 'religion'); }} onSyncComplete={getSeriesWorlds} > ): void => handleInputChange(e.target.value, 'religion')} placeholder={t("worldSetting.religionPlaceholder")} /> } /> { const seriesWorld: SeriesWorldProps | null = getSeriesWorldForCurrentWorld(); if (seriesWorld) handleInputChange(seriesWorld.languages || '', 'languages'); }} onSyncComplete={getSeriesWorlds} > ): void => handleInputChange(e.target.value, 'languages')} placeholder={t("worldSetting.languagesPlaceholder")} /> } />
{elementSections.map((section: ElementSection, index: number): React.JSX.Element => { const SectionIcon: LucideIcon = section.icon; return (

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

); })}
) : (

{t("worldSetting.noWorldAvailable")}

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