'use client' import {useCallback, useContext, useEffect, useState} from 'react'; import {WorldListResponse, WorldProps} from '@/lib/models/World'; import {SeriesWorldProps} from '@/lib/models/Series'; import {SessionContext} from '@/context/SessionContext'; import {BookContext} from '@/context/BookContext'; import {AlertContext} from '@/context/AlertContext'; 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 {SeriesContext, SeriesContextProps} from '@/context/SeriesContext'; import {SeriesSyncContext, SeriesSyncContextProps} from '@/context/SeriesSyncContext'; import {SyncedSeries} from '@/lib/models/SyncedSeries'; import System from '@/lib/models/System'; import {useTranslations} from 'next-intl'; import {SelectBoxProps} from '@/shared/interface'; import {ViewMode} from '@/shared/interface'; import * as tauri from '@/lib/tauri'; 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: keyof WorldProps, value: string) => void; updateWorldArrayField: (field: keyof WorldProps, value: unknown) => 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} = useContext(LangContext); const {session} = useContext(SessionContext); const {book, setBook} = useContext(BookContext); const {errorMessage, successMessage} = useContext(AlertContext); const {isCurrentlyOffline} = useContext(OfflineContext); const {addToQueue} = useContext(LocalSyncQueueContext); const {localSyncedBooks} = useContext(BooksSyncContext); const {localSeries} = useContext(SeriesContext); const {localSyncedSeries} = useContext(SeriesSyncContext); 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 { let response: SeriesWorldProps[]; // Dual logic: offline ou livre local → IPC, sinon serveur if (isCurrentlyOffline() || book?.localBook) { response = await tauri.getSeriesWorldList(bookSeriesId); } else { response = await System.authGetQueryToServer( 'series/world/list', userToken, lang, {seriesid: bookSeriesId} ); } if (response) { setSeriesWorlds(response); } } catch (e: unknown) { if (e instanceof Error) { console.error('Error loading series worlds:', e.message); } } }, [bookSeriesId, userToken, lang, isCurrentlyOffline, book?.localBook]); const refreshWorlds = useCallback(async function (): Promise { setIsLoading(true); try { if (isSeriesMode) { // Series mode - dual logic let response: SeriesWorldProps[]; if (isCurrentlyOffline() || localSeries) { response = await tauri.getSeriesWorldList(entityId); } else { response = await System.authGetQueryToServer( '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 (isCurrentlyOffline()) { response = await tauri.getWorlds(entityId, true) as unknown as WorldListResponse; } else if (book?.localBook) { response = await tauri.getWorlds(entityId, true) as unknown as WorldListResponse; } else { response = await System.authGetQueryToServer( '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, isCurrentlyOffline]); 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: keyof WorldProps, value: string): void { setWorlds(function (prev: WorldProps[]): WorldProps[] { const updated: WorldProps[] = [...prev]; (updated[selectedWorldIndex][field] as string) = value; return updated; }); }, [selectedWorldIndex]); const updateWorldArrayField = useCallback(function (field: keyof WorldProps, value: unknown): void { setWorlds(function (prev: WorldProps[]): WorldProps[] { const updated: WorldProps[] = [...prev]; // @ts-ignore - Le type dépend du champ updated[selectedWorldIndex][field] = value; return updated; }); }, [selectedWorldIndex]); const toggleTool = useCallback(async function (enabled: boolean): Promise { if (isSeriesMode) return; try { const requestData = { bookId: book?.bookId, toolName: 'worlds', enabled: enabled }; let response: boolean; if (isCurrentlyOffline() || book?.localBook) { response = await tauri.updateBookToolSetting(requestData.bookId, requestData.toolName, requestData.enabled); } else { response = await System.authPatchToServer('book/tool-setting', requestData, userToken, lang); if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) { addToQueue('update_book_tool_setting', {data: requestData}); } } 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, isCurrentlyOffline, localSyncedBooks, addToQueue]); const addNewWorld = useCallback(async function (): Promise { if (newWorldName.trim() === '') { errorMessage(t("worldSetting.newWorldNameError")); return; } try { let newWorldId: string; if (isSeriesMode) { // Series mode - dual logic const addData = { seriesId: entityId, name: newWorldName, }; if (isCurrentlyOffline() || localSeries) { newWorldId = await tauri.addSeriesWorld(addData); } else { newWorldId = await System.authPostToServer( 'series/world/add', addData, userToken, lang ); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('add_series_world', {data: {...addData, id: newWorldId}}); } } if (!newWorldId) { errorMessage(t("worldSetting.addWorldError")); return; } } else { const requestData = { worldName: newWorldName, bookId: entityId, }; if (isCurrentlyOffline() || book?.localBook) { newWorldId = await tauri.addWorld(requestData.bookId || entityId, requestData.worldName, requestData.id, requestData.seriesWorldId); } else { newWorldId = await System.authPostToServer('book/world/add', requestData, userToken, lang); if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('add_world', {data: {...requestData, id: newWorldId}}); } } 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, isCurrentlyOffline, book?.localBook, localSyncedBooks, addToQueue]); const saveWorld = useCallback(async function (): Promise { if (worlds.length === 0) return false; try { const currentWorld: WorldProps = worlds[selectedWorldIndex]; if (isSeriesMode) { // Series mode - dual logic const updateData = { worldId: currentWorld.id, name: currentWorld.name, history: currentWorld.history, politics: currentWorld.politics, economy: currentWorld.economy, religion: currentWorld.religion, languages: currentWorld.languages, }; let response: boolean; if (isCurrentlyOffline() || localSeries) { response = await tauri.updateSeriesWorld(updateData); } else { response = await System.authPatchToServer('series/world/update', updateData, userToken, lang); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('update_series_world', {data: updateData}); } } if (!response) { errorMessage(t("worldSetting.updateWorldError")); return false; } } else { const requestData = { world: currentWorld, bookId: entityId, }; let response: boolean; if (isCurrentlyOffline() || book?.localBook) { response = await tauri.updateWorld(requestData.world || requestData); } else { response = await System.authPatchToServer('book/world/update', requestData, userToken, lang); if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('update_world', {data: requestData}); } } if (!response) { errorMessage(t("worldSetting.updateWorldError")); return false; } } successMessage(t("worldSetting.updateWorldSuccess")); return true; } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("worldSetting.unknownError")); } return false; } }, [worlds, selectedWorldIndex, isSeriesMode, entityId, userToken, lang, errorMessage, successMessage, t, isCurrentlyOffline, book?.localBook, localSyncedBooks, addToQueue]); const exportToSeries = useCallback(async function (): Promise { const selectedWorld: WorldProps | undefined = worlds[selectedWorldIndex]; if (!selectedWorld || !bookSeriesId) return; try { const seriesWorldData = { seriesId: bookSeriesId, name: selectedWorld.name, history: selectedWorld.history || null, politics: selectedWorld.politics || null, economy: selectedWorld.economy || null, religion: selectedWorld.religion || null, languages: selectedWorld.languages || null, }; let seriesWorldId: string; if (isCurrentlyOffline() || book?.localBook) { // Mode offline ou livre local → Tauri seriesWorldId = await tauri.addSeriesWorld(seriesWorldData); } else { // Mode online → Serveur seriesWorldId = 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, } }, userToken, lang); // Si la série a une copie locale → addToQueue if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === bookSeriesId)) { addToQueue('add_series_world', {data: {...seriesWorldData, id: seriesWorldId}}); } } if (seriesWorldId) { const updateData = { world: { ...selectedWorld, seriesWorldId: seriesWorldId }, bookId: entityId, }; let updateResponse: boolean; if (isCurrentlyOffline() || book?.localBook) { // Mode offline ou livre local → Tauri updateResponse = await tauri.updateWorld(updateData.world || updateData); } else { // Mode online → Serveur updateResponse = await System.authPostToServer('book/world/update', updateData, userToken, lang); // Si le livre a une copie locale → addToQueue if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('update_world', {data: updateData}); } } 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, isCurrentlyOffline, book, addToQueue, localSyncedBooks, localSyncedSeries, entityId]); 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 { const requestData = { worldName: seriesWorld.name, bookId: entityId, seriesWorldId: seriesWorldId, }; let worldId: string; if (isCurrentlyOffline() || book?.localBook) { // Mode offline ou livre local → Tauri worldId = await tauri.addWorld(requestData.bookId || entityId, requestData.worldName, requestData.id, requestData.seriesWorldId); } else { // Mode online → Serveur worldId = await System.authPostToServer('book/world/add', requestData, userToken, lang); // Si le livre a une copie locale → addToQueue if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('add_world', {data: {...requestData, id: worldId}}); } } 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, isCurrentlyOffline, book, addToQueue, localSyncedBooks]); 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) { const success: boolean = await saveWorld(); if (!success) { // Stay in edit mode on error return; } 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, }; }