'use client' import {useCallback, useContext, useEffect, useState} from 'react'; import {WorldElement, WorldElementSection, WorldListResponse, WorldProps, WorldTextField} from '@/lib/types/world'; import {SeriesWorldProps} from '@/lib/types/series'; import {SessionContext, SessionContextProps} from '@/context/SessionContext'; import {BookContext, BookContextProps} from '@/context/BookContext'; import {AlertContext, AlertContextProps} from '@/context/AlertContext'; import {LangContext, LangContextProps} from '@/context/LangContext'; import {apiGet, apiPatch, apiPost} from '@/lib/api/client'; import {useTranslations} from '@/lib/i18n'; import {SelectBoxProps} from '@/components/form/SelectBox'; import {ViewMode} from '@/lib/types/settings'; import {isDesktop} from '@/lib/configs'; import * as tauri from '@/lib/tauri'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; const initialWorldState: WorldProps = { id: '', name: '', history: '', politics: '', economy: '', religion: '', languages: '', laws: [], biomes: [], issues: [], customs: [], kingdoms: [], climate: [], resources: [], wildlife: [], arts: [], ethnicGroups: [], socialClasses: [], importantCharacters: [], }; export interface UseWorldsConfig { entityType: 'book' | 'series'; entityId: string; } export interface UseWorldsReturn { // State worlds: WorldProps[]; seriesWorlds: SeriesWorldProps[]; selectedWorldIndex: number; worldsSelector: SelectBoxProps[]; toolEnabled: boolean; isLoading: boolean; isSeriesMode: boolean; bookSeriesId: string | null; showAddNewWorld: boolean; newWorldName: string; // Navigation state viewMode: ViewMode; worldBackup: WorldProps | null; // Actions selectWorld: (worldId: string) => void; addNewWorld: () => Promise; saveWorld: () => Promise; updateWorldField: (field: WorldTextField, value: string) => void; updateWorldArrayField: (field: WorldElementSection, value: WorldElement[]) => void; toggleTool: (enabled: boolean) => Promise; importFromSeries: (seriesWorldId: string) => Promise; exportToSeries: () => Promise; refreshWorlds: () => Promise; refreshSeriesWorlds: () => Promise; setShowAddNewWorld: (show: boolean) => void; setNewWorldName: (name: string) => void; setWorlds: React.Dispatch>; getSeriesWorldForCurrentWorld: () => SeriesWorldProps | null; // Navigation actions enterDetailMode: (worldId: string) => void; enterEditMode: () => void; exitEditMode: (save: boolean) => Promise; backToList: () => void; } export function useWorlds(config: UseWorldsConfig): UseWorldsReturn { const {entityType, entityId} = config; const t = useTranslations(); const {lang}: LangContextProps = useContext(LangContext); const {session}: SessionContextProps = useContext(SessionContext); const {book, setBook}: BookContextProps = useContext(BookContext); const {errorMessage, successMessage}: AlertContextProps = useContext(AlertContext); const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); const [worlds, setWorlds] = useState([]); const [seriesWorlds, setSeriesWorlds] = useState([]); const [selectedWorldIndex, setSelectedWorldIndex] = useState(0); const [worldsSelector, setWorldsSelector] = useState([]); const [toolEnabled, setToolEnabled] = useState(entityType === 'series' || (book?.tools?.worlds ?? false)); const [isLoading, setIsLoading] = useState(true); const [showAddNewWorld, setShowAddNewWorld] = useState(false); const [newWorldName, setNewWorldName] = useState(''); // Navigation state const [viewMode, setViewMode] = useState('list'); const [worldBackup, setWorldBackup] = useState(null); const isSeriesMode: boolean = entityType === 'series'; const bookSeriesId: string | null = book?.seriesId || null; const userToken: string = session?.accessToken || ''; // Load worlds on mount useEffect(function (): void { if (entityId) { refreshWorlds(); } }, [entityId]); // Load series worlds for book mode useEffect(function (): void { if (bookSeriesId && !isSeriesMode) { refreshSeriesWorlds(); } }, [bookSeriesId, isSeriesMode]); const refreshSeriesWorlds = useCallback(async function (): Promise { if (!bookSeriesId) return; try { const response: SeriesWorldProps[] = await apiGet( 'series/world/list', userToken, lang, {seriesid: bookSeriesId} ); if (response) { setSeriesWorlds(response); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } }, [bookSeriesId, userToken, lang]); const refreshWorlds = useCallback(async function (): Promise { setIsLoading(true); try { if (isSeriesMode) { const response: SeriesWorldProps[] = await apiGet( 'series/world/list', userToken, lang, {seriesid: entityId} ); if (response) { const mappedWorlds: WorldProps[] = response.map(function (world: SeriesWorldProps): WorldProps { return { 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(function (world: SeriesWorldProps): SelectBoxProps { return { label: world.name, value: world.id, }; }); setWorldsSelector(formattedWorlds); } } else { let response: WorldListResponse; if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.getWorlds(entityId, book?.tools?.worlds ?? false) as WorldListResponse; } else { response = await apiGet('book/worlds', userToken, lang, { bookid: entityId }); } 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(function (world: WorldProps): SelectBoxProps { return { label: world.name, value: world.id.toString(), }; }); setWorldsSelector(formattedWorlds); } } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("worldSetting.unknownError")); } } finally { setIsLoading(false); } }, [entityId, isSeriesMode, userToken, lang, book, setBook, errorMessage, t]); const selectWorld = useCallback(function (worldId: string): void { const index: number = worlds.findIndex(function (world: WorldProps): boolean { return world.id === worldId; }); if (index !== -1) { setSelectedWorldIndex(index); } }, [worlds]); const updateWorldField = useCallback(function (field: WorldTextField, value: string): void { setWorlds(function (prev: WorldProps[]): WorldProps[] { const updated: WorldProps[] = [...prev]; updated[selectedWorldIndex][field] = value; return updated; }); }, [selectedWorldIndex]); const updateWorldArrayField = useCallback(function (field: WorldElementSection, value: WorldElement[]): void { setWorlds(function (prev: WorldProps[]): WorldProps[] { const updated: WorldProps[] = [...prev]; updated[selectedWorldIndex][field] = value; return updated; }); }, [selectedWorldIndex]); const toggleTool = useCallback(async function (enabled: boolean): Promise { if (isSeriesMode) return; try { let response: boolean; if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.updateBookToolSetting(book?.bookId ?? '', 'worlds', enabled); } else { response = await apiPatch('book/tool-setting', { bookId: book?.bookId, toolName: 'worlds', enabled: enabled }, userToken, 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); } } }, [isSeriesMode, book, setBook, userToken, lang, errorMessage]); const addNewWorld = useCallback(async function (): Promise { if (newWorldName.trim() === '') { errorMessage(t("worldSetting.newWorldNameError")); return; } try { let newWorldId: string; if (isSeriesMode) { newWorldId = await apiPost( 'series/world/add', { seriesId: entityId, name: newWorldName, }, userToken, lang ); if (!newWorldId) { errorMessage(t("worldSetting.addWorldError")); return; } } else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { newWorldId = await tauri.addWorld(entityId, newWorldName); if (!newWorldId) { errorMessage(t("worldSetting.addWorldError")); return; } } else { newWorldId = await apiPost('book/world/add', { worldName: newWorldName, bookId: entityId, }, userToken, lang); if (!newWorldId) { errorMessage(t("worldSetting.addWorldError")); return; } } const newWorld: WorldProps = { ...initialWorldState, id: newWorldId, name: newWorldName, }; setWorlds(function (prev: WorldProps[]): WorldProps[] { return [...prev, newWorld]; }); setWorldsSelector(function (prev: SelectBoxProps[]): SelectBoxProps[] { return [...prev, {label: newWorldName, value: newWorldId}]; }); setNewWorldName(''); setShowAddNewWorld(false); // Sélectionner le nouveau monde et passer en mode edit setSelectedWorldIndex(worlds.length); // Le nouveau monde est à la fin setViewMode('edit'); setWorldBackup(null); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("worldSetting.unknownError")); } } }, [newWorldName, isSeriesMode, entityId, userToken, lang, errorMessage, t]); const saveWorld = useCallback(async function (): 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, }, userToken, 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: entityId, }, userToken, 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")); } } }, [worlds, selectedWorldIndex, isSeriesMode, entityId, userToken, lang, errorMessage, successMessage, t]); const exportToSeries = useCallback(async function (): 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, } }, userToken, lang); if (seriesWorldId) { const updateResponse: boolean = await apiPost('book/world/update', { world: { ...selectedWorld, seriesWorldId: seriesWorldId }, }, userToken, lang); if (updateResponse) { setWorlds(function (prev: WorldProps[]): WorldProps[] { const updated: WorldProps[] = [...prev]; updated[selectedWorldIndex] = {...selectedWorld, seriesWorldId: seriesWorldId}; return updated; }); const newSeriesWorld: SeriesWorldProps = { id: seriesWorldId, name: selectedWorld.name, history: selectedWorld.history || '', politics: selectedWorld.politics || '', economy: selectedWorld.economy || '', religion: selectedWorld.religion || '', languages: selectedWorld.languages || '', laws: [], biomes: [], issues: [], customs: [], kingdoms: [], climate: [], resources: [], wildlife: [], arts: [], ethnicGroups: [], socialClasses: [], importantCharacters: [], }; setSeriesWorlds(function (prev: SeriesWorldProps[]): SeriesWorldProps[] { return [...prev, newSeriesWorld]; }); successMessage(t("worldSetting.exportSuccess")); } } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } }, [worlds, selectedWorldIndex, bookSeriesId, userToken, lang, successMessage, errorMessage, t]); const importFromSeries = useCallback(async function (seriesWorldId: string): Promise { const seriesWorld: SeriesWorldProps | undefined = seriesWorlds.find(function (w: SeriesWorldProps): boolean { return w.id === seriesWorldId; }); if (!seriesWorld) return; try { let worldId: string; if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { worldId = await tauri.addWorld(entityId, seriesWorld.name, undefined, seriesWorldId); } else { worldId = await apiPost('book/world/add', { worldName: seriesWorld.name, bookId: entityId, seriesWorldId: seriesWorldId, }, userToken, 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(function (prev: WorldProps[]): WorldProps[] { return [...prev, newWorld]; }); setWorldsSelector(function (prev: SelectBoxProps[]): SelectBoxProps[] { return [...prev, {label: seriesWorld.name, value: worldId}]; }); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } }, [seriesWorlds, entityId, userToken, lang, errorMessage, t]); const getSeriesWorldForCurrentWorld = useCallback(function (): SeriesWorldProps | null { const currentWorld: WorldProps | undefined = worlds[selectedWorldIndex]; if (!currentWorld?.seriesWorldId) return null; return seriesWorlds.find(function (world: SeriesWorldProps): boolean { return world.id === currentWorld.seriesWorldId; }) || null; }, [worlds, selectedWorldIndex, seriesWorlds]); // Navigation functions const enterDetailMode = useCallback(function (worldId: string): void { const index: number = worlds.findIndex(function (world: WorldProps): boolean { return world.id === worldId; }); if (index !== -1) { setSelectedWorldIndex(index); setViewMode('detail'); setWorldBackup(null); } }, [worlds]); const enterEditMode = useCallback(function (): void { if (worlds.length > 0 && selectedWorldIndex >= 0) { setWorldBackup({...worlds[selectedWorldIndex]}); } setViewMode('edit'); }, [worlds, selectedWorldIndex]); const exitEditMode = useCallback(async function (save: boolean): Promise { if (save) { await saveWorld(); if (worldBackup) { setViewMode('detail'); } else { setViewMode('list'); } } else { if (worldBackup && selectedWorldIndex >= 0) { setWorlds(function (prev: WorldProps[]): WorldProps[] { const updated: WorldProps[] = [...prev]; updated[selectedWorldIndex] = worldBackup; return updated; }); setViewMode('detail'); } else { setViewMode('list'); } } setWorldBackup(null); }, [saveWorld, worldBackup, selectedWorldIndex]); const backToList = useCallback(function (): void { setSelectedWorldIndex(0); setWorldBackup(null); setViewMode('list'); }, []); return { // State worlds, seriesWorlds, selectedWorldIndex, worldsSelector, toolEnabled, isLoading, isSeriesMode, bookSeriesId, showAddNewWorld, newWorldName, // Navigation state viewMode, worldBackup, // Actions selectWorld, addNewWorld, saveWorld, updateWorldField, updateWorldArrayField, toggleTool, importFromSeries, exportToSeries, refreshWorlds, refreshSeriesWorlds, setShowAddNewWorld, setNewWorldName, setWorlds, getSeriesWorldForCurrentWorld, // Navigation actions enterDetailMode, enterEditMode, exitEditMode, backToList, }; }