'use client' import {useCallback, useContext, useEffect, useState} from 'react'; import {Attribute, CharacterListResponse, CharacterProps} from '@/lib/models/Character'; import {SeriesCharacterProps} from '@/lib/models/Series'; import * as tauri from '@/lib/tauri'; 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 {ViewMode} from '@/shared/interface'; const initialCharacterState: CharacterProps = { id: null, name: '', lastName: '', nickname: '', age: null, gender: '', species: '', nationality: '', status: 'alive', category: 'none', title: '', role: '', image: 'https://via.placeholder.com/150', biography: '', history: '', speechPattern: '', catchphrase: '', residence: '', notes: '', color: '', physical: [], psychological: [], relations: [], skills: [], weaknesses: [], strengths: [], goals: [], motivations: [], arc: [], secrets: [], fears: [], flaws: [], beliefs: [], conflicts: [], quotes: [], distinguishingMarks: [], items: [], affiliations: [], }; export interface UseCharactersConfig { entityType: 'book' | 'series'; entityId: string; } export interface UseCharactersReturn { // State characters: CharacterProps[]; seriesCharacters: SeriesCharacterProps[]; selectedCharacter: CharacterProps | null; toolEnabled: boolean; isLoading: boolean; isSeriesMode: boolean; bookSeriesId: string | null; // Navigation state viewMode: ViewMode; characterBackup: CharacterProps | null; // Actions selectCharacter: (character: CharacterProps) => void; addNewCharacter: () => void; clearSelection: () => void; saveCharacter: () => Promise; deleteCharacter: (characterId: string) => Promise; updateCharacterField: (key: keyof CharacterProps, value: string | number | null) => void; addAttribute: (section: keyof CharacterProps, value: Attribute) => Promise; removeAttribute: (section: keyof CharacterProps, index: number, attrId: string) => Promise; toggleTool: (enabled: boolean) => Promise; importFromSeries: (seriesCharacterId: string) => Promise; exportToSeries: () => Promise; refreshCharacters: () => Promise; refreshSeriesCharacters: () => Promise; setSelectedCharacter: React.Dispatch>; // Navigation actions enterDetailMode: (character: CharacterProps) => void; enterEditMode: () => void; exitEditMode: (save: boolean) => Promise; backToList: () => void; } export function useCharacters(config: UseCharactersConfig): UseCharactersReturn { 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 [characters, setCharacters] = useState([]); const [seriesCharacters, setSeriesCharacters] = useState([]); const [selectedCharacter, setSelectedCharacter] = useState(null); const [toolEnabled, setToolEnabled] = useState(entityType === 'series' || (book?.tools?.characters ?? false)); const [isLoading, setIsLoading] = useState(true); // Navigation state const [viewMode, setViewMode] = useState('list'); const [characterBackup, setCharacterBackup] = useState(null); const isSeriesMode: boolean = entityType === 'series'; const bookSeriesId: string | null = book?.seriesId || null; const userToken: string = session?.accessToken || ''; // Load characters on mount useEffect(function (): void { if (entityId) { refreshCharacters(); } }, [entityId]); // Load series characters for book mode useEffect(function (): void { if (bookSeriesId && !isSeriesMode) { refreshSeriesCharacters(); } }, [bookSeriesId, isSeriesMode]); const refreshSeriesCharacters = useCallback(async function (): Promise { if (!bookSeriesId) return; try { let response: SeriesCharacterProps[]; // Dual logic: offline ou livre local → IPC, sinon serveur if (isCurrentlyOffline() || book?.localBook) { response = await tauri.getSeriesCharacterList(bookSeriesId); } else { response = await System.authGetQueryToServer( 'series/character/list', userToken, lang, {seriesid: bookSeriesId} ); } if (response) { setSeriesCharacters(response); } } catch (e: unknown) { if (e instanceof Error) { console.error('Error loading series characters:', e.message); } } }, [bookSeriesId, userToken, lang, isCurrentlyOffline, book?.localBook]); const refreshCharacters = useCallback(async function (): Promise { setIsLoading(true); try { if (isSeriesMode) { // Series mode - dual logic let response: SeriesCharacterProps[]; if (isCurrentlyOffline() || localSeries) { response = await tauri.getSeriesCharacterList(entityId); } else { response = await System.authGetQueryToServer( 'series/character/list', userToken, lang, {seriesid: entityId} ); } if (response) { const mappedCharacters: CharacterProps[] = response.map(function (char: SeriesCharacterProps): CharacterProps { return { id: char.id, name: char.name, lastName: char.lastName || '', nickname: '', age: char.age ?? null, gender: '', species: '', nationality: '', status: 'alive' as const, category: char.category as CharacterProps['category'], title: '', role: char.role || '', image: char.image || 'https://via.placeholder.com/150', color: char.color || '', physical: [], psychological: [], relations: [], skills: [], weaknesses: [], strengths: [], goals: [], motivations: [], arc: [], secrets: [], fears: [], flaws: [], beliefs: [], conflicts: [], quotes: [], distinguishingMarks: [], items: [], affiliations: [], }; }); setCharacters(mappedCharacters); } } else { // Pattern B: GET dans contexte livre let response: CharacterListResponse; if (isCurrentlyOffline()) { // Offline → Tauri response = await tauri.getCharacterList(entityId, true) as CharacterListResponse; } else if (book?.localBook) { // Online mais livre local → Tauri response = await tauri.getCharacterList(entityId, true) as CharacterListResponse; } else { // Online + livre serveur → Server response = await System.authGetQueryToServer( 'character/list', userToken, lang, {bookid: entityId} ); } if (response) { setCharacters(response.characters); setToolEnabled(response.enabled); if (setBook && book) { setBook({ ...book, tools: { characters: response.enabled, worlds: book.tools?.worlds ?? false, locations: book.tools?.locations ?? false, spells: book.tools?.spells ?? false } }); } } } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("common.unknownError")); } } finally { setIsLoading(false); } }, [entityId, isSeriesMode, userToken, lang, book, setBook, errorMessage, t, isCurrentlyOffline]); const selectCharacter = useCallback(function (character: CharacterProps): void { setSelectedCharacter({...character}); }, []); const addNewCharacter = useCallback(function (): void { setSelectedCharacter({...initialCharacterState}); setViewMode('edit'); setCharacterBackup(null); }, []); const clearSelection = useCallback(function (): void { setSelectedCharacter(null); setViewMode('list'); setCharacterBackup(null); }, []); const updateCharacterField = useCallback(function (key: keyof CharacterProps, value: string | number | null): void { setSelectedCharacter(function (prev: CharacterProps | null): CharacterProps | null { if (!prev) return null; return {...prev, [key]: value}; }); }, []); const toggleTool = useCallback(async function (enabled: boolean): Promise { if (isSeriesMode) return; try { const requestData = { bookId: book?.bookId, toolName: 'characters', enabled: enabled }; let response: boolean; if (isCurrentlyOffline() || book?.localBook) { // Offline OU livre local → Tauri response = await tauri.updateBookToolSetting(requestData.bookId, requestData.toolName, requestData.enabled); } else { // Online + livre serveur → Server response = await System.authPatchToServer('book/tool-setting', requestData, userToken, lang); // Si le livre a une copie locale → addToQueue pour sync if (book?.bookId && 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: enabled, worlds: book.tools?.worlds ?? false, 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, addToQueue, localSyncedBooks]); const saveCharacter = useCallback(async function (): Promise { if (!selectedCharacter) return false; if (selectedCharacter.id === null) { return await addCharacterInternal(selectedCharacter); } else { return await updateCharacterInternal(selectedCharacter); } }, [selectedCharacter]); async function addCharacterInternal(character: CharacterProps): Promise { if (!character.name) { errorMessage(t("characterComponent.errorNameRequired")); return false; } if (character.category === 'none') { errorMessage(t("characterComponent.errorCategoryRequired")); return false; } try { let characterId: string; if (isSeriesMode) { // Series mode - dual logic const seriesCharacterData = { seriesId: entityId, character: { name: character.name, lastName: character.lastName || null, nickname: character.nickname || null, age: character.age || null, gender: character.gender || null, species: character.species || null, nationality: character.nationality || null, status: character.status || null, category: character.category, title: character.title || null, image: character.image || null, role: character.role || null, biography: character.biography || null, history: character.history || null, speechPattern: character.speechPattern || null, catchphrase: character.catchphrase || null, residence: character.residence || null, notes: character.notes || null, color: character.color || null, } }; if (isCurrentlyOffline() || localSeries) { characterId = await tauri.addSeriesCharacter(seriesCharacterData); } else { characterId = await System.authPostToServer( 'series/character/add', seriesCharacterData, userToken, lang ); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('add_series_character', {data: {...seriesCharacterData, id: characterId}}); } } } else { // Pattern A: mutations const requestData = { bookId: entityId, character: character, }; if (isCurrentlyOffline() || book?.localBook) { // Offline OU livre local → Tauri characterId = await tauri.createCharacter(requestData.character, requestData.bookId, requestData.id); } else { // Online + livre serveur → Server characterId = await System.authPostToServer('character/add', requestData, userToken, lang); // Si le livre a une copie locale → addToQueue pour sync if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('create_character', {data: {...requestData, id: characterId}}); } } } if (!characterId) { errorMessage(t("characterComponent.errorAddCharacter")); return false; } const newCharacter: CharacterProps = {...character, id: characterId}; setCharacters(function (prev: CharacterProps[]): CharacterProps[] { return [...prev, newCharacter]; }); setSelectedCharacter(null); return true; } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("common.unknownError")); } return false; } } async function updateCharacterInternal(character: CharacterProps): Promise { try { let response: boolean; if (isSeriesMode) { // Series mode - dual logic const updateData = { characterId: character.id, character: { id: character.id, name: character.name, lastName: character.lastName || null, nickname: character.nickname || null, age: character.age || null, gender: character.gender || null, species: character.species || null, nationality: character.nationality || null, status: character.status || null, category: character.category, title: character.title || null, image: character.image || null, role: character.role || null, biography: character.biography || null, history: character.history || null, speechPattern: character.speechPattern || null, catchphrase: character.catchphrase || null, residence: character.residence || null, notes: character.notes || null, color: character.color || null, } }; if (isCurrentlyOffline() || localSeries) { response = await tauri.updateSeriesCharacter(updateData); } else { response = await System.authPatchToServer('series/character/update', updateData, userToken, lang); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('update_series_character', {data: updateData}); } } } else { // Pattern A: mutations const requestData = { character: character, }; if (isCurrentlyOffline() || book?.localBook) { // Offline OU livre local → Tauri response = await tauri.updateCharacter(requestData.character); } else { // Online + livre serveur → Server response = await System.authPostToServer('character/update', requestData, userToken, lang); // Si le livre a une copie locale → addToQueue pour sync if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('update_character', {data: requestData}); } } } if (!response) { errorMessage(t("characterComponent.errorUpdateCharacter")); return false; } setCharacters(function (prev: CharacterProps[]): CharacterProps[] { return prev.map(function (c: CharacterProps): CharacterProps { return c.id === character.id ? character : c; }); }); setSelectedCharacter(null); successMessage(t("characterComponent.successUpdate")); return true; } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("common.unknownError")); } return false; } } const deleteCharacter = useCallback(async function (characterId: string): Promise { try { let response: boolean; const deletedAt: number = System.timeStampInSeconds(); if (isSeriesMode) { // Series mode - dual logic const requestData = {characterId, deletedAt}; if (isCurrentlyOffline() || localSeries) { response = await tauri.deleteSeriesCharacter(requestData.characterId, requestData.deletedAt); } else { response = await System.authDeleteToServer('series/character/delete', requestData, userToken, lang); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('delete_series_character', {data: requestData}); } } } else { // Pattern A: mutations const requestData = {characterId, bookId: entityId, deletedAt}; if (isCurrentlyOffline() || book?.localBook) { // Offline OU livre local → Tauri response = await tauri.deleteCharacter(requestData.characterId, requestData.bookId, requestData.deletedAt); } else { // Online + livre serveur → Server response = await System.authDeleteToServer('character/delete', requestData, userToken, lang); // Si le livre a une copie locale → addToQueue pour sync if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('delete_character', {data: requestData}); } } } if (!response) { errorMessage(t("characterComponent.errorDeleteCharacter")); return; } setCharacters(function (prev: CharacterProps[]): CharacterProps[] { return prev.filter(function (c: CharacterProps): boolean { return c.id !== characterId; }); }); setSelectedCharacter(null); successMessage(t("characterComponent.successDelete")); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("common.unknownError")); } } }, [isSeriesMode, userToken, lang, errorMessage, successMessage, t, isCurrentlyOffline, book, addToQueue, localSyncedBooks, entityId]); const addAttribute = useCallback(async function (section: keyof CharacterProps, value: Attribute): Promise { if (!selectedCharacter) return; if (selectedCharacter.id === null) { const updatedSection: Attribute[] = [ ...(selectedCharacter[section] as Attribute[]), value, ]; setSelectedCharacter({...selectedCharacter, [section]: updatedSection}); } else { try { const requestData = { characterId: selectedCharacter.id, type: section, name: value.name, }; let attributeId: string; if (isSeriesMode) { // Series mode - dual logic if (isCurrentlyOffline() || localSeries) { attributeId = await tauri.addSeriesCharacterAttribute(requestData); } else { attributeId = await System.authPostToServer('series/character/attribute/add', requestData, userToken, lang); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('add_series_character_attribute', {data: {...requestData, id: attributeId}}); } } } else { // Pattern A: mutations if (isCurrentlyOffline() || book?.localBook) { // Offline OU livre local → Tauri attributeId = await tauri.addCharacterAttribute(requestData.characterId, requestData.type, requestData.name, requestData.id); } else { // Online + livre serveur → Server attributeId = await System.authPostToServer('character/attribute/add', requestData, userToken, lang); // Si le livre a une copie locale → addToQueue pour sync if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('add_character_attribute', {data: {...requestData, id: attributeId}}); } } } if (!attributeId) { errorMessage(t("characterComponent.errorAddAttribute")); return; } const newValue: Attribute = { name: value.name, id: attributeId, }; const updatedSection: Attribute[] = [...(selectedCharacter[section] as Attribute[]), newValue]; setSelectedCharacter({...selectedCharacter, [section]: updatedSection}); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("common.unknownError")); } } } }, [selectedCharacter, isSeriesMode, userToken, lang, errorMessage, t, isCurrentlyOffline, book, addToQueue, localSyncedBooks, entityId]); const removeAttribute = useCallback(async function (section: keyof CharacterProps, index: number, attrId: string): Promise { if (!selectedCharacter) return; if (selectedCharacter.id === null) { const updatedSection: Attribute[] = ( selectedCharacter[section] as Attribute[] ).filter(function (_: Attribute, i: number): boolean { return i !== index; }); setSelectedCharacter({...selectedCharacter, [section]: updatedSection}); } else { try { const deletedAt: number = System.timeStampInSeconds(); let response: boolean; if (isSeriesMode) { // Series mode - dual logic const requestData = {attributeId: attrId, deletedAt}; if (isCurrentlyOffline() || localSeries) { response = await tauri.deleteSeriesCharacterAttribute(requestData.attributeId, requestData.deletedAt); } else { response = await System.authDeleteToServer('series/character/attribute/delete', requestData, userToken, lang); if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) { addToQueue('delete_series_character_attribute', {data: requestData}); } } } else { // Pattern A: mutations const requestData = {attributeId: attrId, bookId: entityId, deletedAt}; if (isCurrentlyOffline() || book?.localBook) { // Offline OU livre local → Tauri response = await tauri.deleteCharacterAttribute(requestData.attributeId, requestData.bookId, requestData.deletedAt); } else { // Online + livre serveur → Server response = await System.authDeleteToServer('character/attribute/delete', requestData, userToken, lang); // Si le livre a une copie locale → addToQueue pour sync if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('delete_character_attribute', {data: requestData}); } } } if (!response) { errorMessage(t("characterComponent.errorRemoveAttribute")); return; } const updatedSection: Attribute[] = ( selectedCharacter[section] as Attribute[] ).filter(function (_: Attribute, i: number): boolean { return i !== index; }); setSelectedCharacter({...selectedCharacter, [section]: updatedSection}); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("common.unknownError")); } } } }, [selectedCharacter, isSeriesMode, userToken, lang, errorMessage, t, isCurrentlyOffline, book, addToQueue, localSyncedBooks, entityId]); const exportToSeries = useCallback(async function (): Promise { if (!selectedCharacter || !bookSeriesId) return; try { const seriesCharacterData = { seriesId: bookSeriesId, character: { name: selectedCharacter.name, lastName: selectedCharacter.lastName || null, nickname: selectedCharacter.nickname || null, age: selectedCharacter.age || null, gender: selectedCharacter.gender || null, species: selectedCharacter.species || null, nationality: selectedCharacter.nationality || null, status: selectedCharacter.status || null, category: selectedCharacter.category, title: selectedCharacter.title || null, image: selectedCharacter.image || null, role: selectedCharacter.role || null, biography: selectedCharacter.biography || null, history: selectedCharacter.history || null, speechPattern: selectedCharacter.speechPattern || null, catchphrase: selectedCharacter.catchphrase || null, residence: selectedCharacter.residence || null, notes: selectedCharacter.notes || null, color: selectedCharacter.color || null, } }; let seriesCharacterId: string; if (isCurrentlyOffline() || book?.localBook) { // Mode offline ou livre local → Tauri seriesCharacterId = await tauri.addSeriesCharacter(seriesCharacterData); } else { // Mode online → Serveur seriesCharacterId = await System.authPostToServer( 'series/character/add', seriesCharacterData, userToken, lang ); // Si la série a une copie locale → addToQueue if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === bookSeriesId)) { addToQueue('add_series_character', {data: {...seriesCharacterData, id: seriesCharacterId}}); } } if (seriesCharacterId) { const updateData = { character: { ...selectedCharacter, seriesCharacterId: seriesCharacterId }, }; let updateResponse: boolean; if (isCurrentlyOffline() || book?.localBook) { // Mode offline ou livre local → Tauri updateResponse = await tauri.updateCharacter(updateData.character); } else { // Mode online → Serveur updateResponse = await System.authPostToServer('character/update', updateData, userToken, lang); // Si le livre a une copie locale → addToQueue if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('update_character', {data: updateData}); } } if (updateResponse) { setSelectedCharacter({...selectedCharacter, seriesCharacterId: seriesCharacterId}); setCharacters(function (prev: CharacterProps[]): CharacterProps[] { return prev.map(function (c: CharacterProps): CharacterProps { return c.id === selectedCharacter.id ? {...c, seriesCharacterId: seriesCharacterId} : c; }); }); const newSeriesCharacter: SeriesCharacterProps = { id: seriesCharacterId, name: selectedCharacter.name, lastName: selectedCharacter.lastName || null, nickname: selectedCharacter.nickname || null, age: selectedCharacter.age || null, gender: selectedCharacter.gender || null, species: selectedCharacter.species || null, nationality: selectedCharacter.nationality || null, status: selectedCharacter.status || null, category: selectedCharacter.category, title: selectedCharacter.title || null, image: selectedCharacter.image || null, role: selectedCharacter.role || null, biography: selectedCharacter.biography || null, history: selectedCharacter.history || null, speechPattern: selectedCharacter.speechPattern || null, catchphrase: selectedCharacter.catchphrase || null, residence: selectedCharacter.residence || null, notes: selectedCharacter.notes || null, color: selectedCharacter.color || null, }; setSeriesCharacters(function (prev: SeriesCharacterProps[]): SeriesCharacterProps[] { return [...prev, newSeriesCharacter]; }); successMessage(t("characterComponent.exportSuccess")); } } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } }, [selectedCharacter, bookSeriesId, userToken, lang, successMessage, errorMessage, t, isCurrentlyOffline, book, addToQueue, localSyncedBooks, localSyncedSeries, entityId]); const importFromSeries = useCallback(async function (seriesCharacterId: string): Promise { const seriesChar = seriesCharacters.find(function (c: SeriesCharacterProps): boolean { return c.id === seriesCharacterId; }); if (!seriesChar) return; try { const characterToImport: CharacterProps = { id: null, name: seriesChar.name, lastName: seriesChar.lastName || '', nickname: seriesChar.nickname || '', age: seriesChar.age ?? null, gender: seriesChar.gender || '', species: seriesChar.species || '', nationality: seriesChar.nationality || '', status: (seriesChar.status as 'alive' | 'dead' | 'unknown') || 'alive', category: (seriesChar.category as CharacterProps['category']) || 'none', title: seriesChar.title || '', role: seriesChar.role || '', image: seriesChar.image || 'https://via.placeholder.com/150', biography: seriesChar.biography || '', history: seriesChar.history || '', speechPattern: seriesChar.speechPattern || '', catchphrase: seriesChar.catchphrase || '', residence: seriesChar.residence || '', notes: seriesChar.notes || '', color: seriesChar.color || '', physical: [], psychological: [], relations: [], skills: [], weaknesses: [], strengths: [], goals: [], motivations: [], arc: [], secrets: [], fears: [], flaws: [], beliefs: [], conflicts: [], quotes: [], distinguishingMarks: [], items: [], affiliations: [], seriesCharacterId: seriesCharacterId, }; const requestData = { bookId: entityId, character: characterToImport, }; let characterId: string; if (isCurrentlyOffline() || book?.localBook) { // Mode offline ou livre local → Tauri characterId = await tauri.createCharacter(requestData.character, requestData.bookId, requestData.id); } else { // Mode online → Serveur characterId = await System.authPostToServer('character/add', requestData, userToken, lang); // Si le livre a une copie locale → addToQueue if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) { addToQueue('create_character', {data: {...requestData, id: characterId}}); } } if (!characterId) { errorMessage(t("characterComponent.importError")); return; } characterToImport.id = characterId; setCharacters(function (prev: CharacterProps[]): CharacterProps[] { return [...prev, characterToImport]; }); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } } }, [seriesCharacters, entityId, userToken, lang, errorMessage, successMessage, t, isCurrentlyOffline, book, addToQueue, localSyncedBooks]); // Navigation functions const enterDetailMode = useCallback(function (character: CharacterProps): void { setSelectedCharacter({...character}); setViewMode('detail'); setCharacterBackup(null); }, []); const enterEditMode = useCallback(function (): void { if (selectedCharacter) { setCharacterBackup({...selectedCharacter}); } setViewMode('edit'); }, [selectedCharacter]); const exitEditMode = useCallback(async function (save: boolean): Promise { if (save) { const success: boolean = await saveCharacter(); if (!success) { // Stay in edit mode on error return; } if (characterBackup) { setViewMode('detail'); } else { setViewMode('list'); } } else { if (characterBackup) { setSelectedCharacter(characterBackup); setViewMode('detail'); } else { setSelectedCharacter(null); setViewMode('list'); } } setCharacterBackup(null); }, [saveCharacter, characterBackup]); const backToList = useCallback(function (): void { setSelectedCharacter(null); setCharacterBackup(null); setViewMode('list'); }, []); return { // State characters, seriesCharacters, selectedCharacter, toolEnabled, isLoading, isSeriesMode, bookSeriesId, // Navigation state viewMode, characterBackup, // Actions selectCharacter, addNewCharacter, clearSelection, saveCharacter, deleteCharacter, updateCharacterField, addAttribute, removeAttribute, toggleTool, importFromSeries, exportToSeries, refreshCharacters, refreshSeriesCharacters, setSelectedCharacter, // Navigation actions enterDetailMode, enterEditMode, exitEditMode, backToList, }; }