Remove CharacterComponent and CharacterDetail components
- Deleted `CharacterComponent` and `CharacterDetail` files from the project. - Refactored related logic to improve code maintainability and reduce redundancy.
This commit is contained in:
975
hooks/settings/useCharacters.ts
Normal file
975
hooks/settings/useCharacters.ts
Normal file
@@ -0,0 +1,975 @@
|
||||
'use client'
|
||||
import {useCallback, useContext, useEffect, useState} from 'react';
|
||||
import {Attribute, CharacterListResponse, CharacterProps} from '@/lib/models/Character';
|
||||
import {SeriesCharacterProps} 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 {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<boolean>;
|
||||
deleteCharacter: (characterId: string) => Promise<void>;
|
||||
updateCharacterField: (key: keyof CharacterProps, value: string | number | null) => void;
|
||||
addAttribute: (section: keyof CharacterProps, value: Attribute) => Promise<void>;
|
||||
removeAttribute: (section: keyof CharacterProps, index: number, attrId: string) => Promise<void>;
|
||||
toggleTool: (enabled: boolean) => Promise<void>;
|
||||
importFromSeries: (seriesCharacterId: string) => Promise<void>;
|
||||
exportToSeries: () => Promise<void>;
|
||||
refreshCharacters: () => Promise<void>;
|
||||
refreshSeriesCharacters: () => Promise<void>;
|
||||
setSelectedCharacter: React.Dispatch<React.SetStateAction<CharacterProps | null>>;
|
||||
|
||||
// Navigation actions
|
||||
enterDetailMode: (character: CharacterProps) => void;
|
||||
enterEditMode: () => void;
|
||||
exitEditMode: (save: boolean) => Promise<void>;
|
||||
backToList: () => void;
|
||||
}
|
||||
|
||||
export function useCharacters(config: UseCharactersConfig): UseCharactersReturn {
|
||||
const {entityType, entityId} = config;
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {book, setBook} = useContext(BookContext);
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
const {localSeries} = useContext<SeriesContextProps>(SeriesContext);
|
||||
const {localSyncedSeries} = useContext<SeriesSyncContextProps>(SeriesSyncContext);
|
||||
|
||||
const [characters, setCharacters] = useState<CharacterProps[]>([]);
|
||||
const [seriesCharacters, setSeriesCharacters] = useState<SeriesCharacterProps[]>([]);
|
||||
const [selectedCharacter, setSelectedCharacter] = useState<CharacterProps | null>(null);
|
||||
const [toolEnabled, setToolEnabled] = useState<boolean>(entityType === 'series' || (book?.tools?.characters ?? false));
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
// Navigation state
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [characterBackup, setCharacterBackup] = useState<CharacterProps | null>(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<void> {
|
||||
if (!bookSeriesId) return;
|
||||
try {
|
||||
let response: SeriesCharacterProps[];
|
||||
// Dual logic: offline ou livre local → IPC, sinon serveur
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<SeriesCharacterProps[]>(
|
||||
'db:series:character:list',
|
||||
{seriesId: bookSeriesId}
|
||||
);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesCharacterProps[]>(
|
||||
'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<void> {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
let response: SeriesCharacterProps[];
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesCharacterProps[]>(
|
||||
'db:series:character:list',
|
||||
{seriesId: entityId}
|
||||
);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesCharacterProps[]>(
|
||||
'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 → IPC
|
||||
response = await window.electron.invoke<CharacterListResponse>('db:character:list', {bookid: entityId});
|
||||
} else if (book?.localBook) {
|
||||
// Online mais livre local → IPC
|
||||
response = await window.electron.invoke<CharacterListResponse>('db:character:list', {bookid: entityId});
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
response = await System.authGetQueryToServer<CharacterListResponse>(
|
||||
'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<void> {
|
||||
if (isSeriesMode) return;
|
||||
try {
|
||||
const requestData = {
|
||||
bookId: book?.bookId,
|
||||
toolName: 'characters',
|
||||
enabled: enabled
|
||||
};
|
||||
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
response = await window.electron.invoke<boolean>('db:book:tool:update', requestData);
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
response = await System.authPatchToServer<boolean>('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('db:book:tool:update', 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<boolean> {
|
||||
if (!selectedCharacter) return false;
|
||||
|
||||
if (selectedCharacter.id === null) {
|
||||
return await addCharacterInternal(selectedCharacter);
|
||||
} else {
|
||||
return await updateCharacterInternal(selectedCharacter);
|
||||
}
|
||||
}, [selectedCharacter]);
|
||||
|
||||
async function addCharacterInternal(character: CharacterProps): Promise<boolean> {
|
||||
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 window.electron.invoke<string>('db:series:character:add', seriesCharacterData);
|
||||
} else {
|
||||
characterId = await System.authPostToServer<string>(
|
||||
'series/character/add',
|
||||
seriesCharacterData,
|
||||
userToken,
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:character:add', {...seriesCharacterData, id: characterId});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pattern A: mutations
|
||||
const requestData = {
|
||||
bookId: entityId,
|
||||
character: character,
|
||||
};
|
||||
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
characterId = await window.electron.invoke<string>('db:character:create', requestData);
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
characterId = await System.authPostToServer<string>('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('db:character:create', {...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<boolean> {
|
||||
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 window.electron.invoke<boolean>('db:series:character:update', updateData);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('series/character/update', updateData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:character:update', updateData);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pattern A: mutations
|
||||
const requestData = {
|
||||
character: character,
|
||||
};
|
||||
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
response = await window.electron.invoke<boolean>('db:character:update', requestData);
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
response = await System.authPostToServer<boolean>('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('db:character:update', 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<void> {
|
||||
try {
|
||||
let response: boolean;
|
||||
const requestData = {characterId: characterId};
|
||||
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:character:delete', requestData);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('series/character/delete', requestData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:character:delete', requestData);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pattern A: mutations
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
response = await window.electron.invoke<boolean>('db:character:delete', requestData);
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
response = await System.authDeleteToServer<boolean>('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('db:character:delete', 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<void> {
|
||||
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 window.electron.invoke<string>('db:series:character:attribute:add', requestData);
|
||||
} else {
|
||||
attributeId = await System.authPostToServer<string>('series/character/attribute/add', requestData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:character:attribute:add', {...requestData, id: attributeId});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pattern A: mutations
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
attributeId = await window.electron.invoke<string>('db:character:attribute:add', requestData);
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
attributeId = await System.authPostToServer<string>('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('db:character:attribute:add', {...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<void> {
|
||||
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 requestData = {attributeId: attrId};
|
||||
|
||||
let response: boolean;
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:character:attribute:delete', requestData);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('series/character/attribute/delete', requestData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:character:attribute:delete', requestData);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pattern A: mutations
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
response = await window.electron.invoke<boolean>('db:character:attribute:delete', requestData);
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
response = await System.authDeleteToServer<boolean>('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('db:character:attribute:delete', 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<void> {
|
||||
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 → IPC
|
||||
seriesCharacterId = await window.electron.invoke<string>('db:series:character:add', seriesCharacterData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
seriesCharacterId = await System.authPostToServer<string>(
|
||||
'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('db:series:character:add', {...seriesCharacterData, id: seriesCharacterId});
|
||||
}
|
||||
}
|
||||
|
||||
if (seriesCharacterId) {
|
||||
const updateData = {
|
||||
character: {
|
||||
...selectedCharacter,
|
||||
seriesCharacterId: seriesCharacterId
|
||||
},
|
||||
};
|
||||
|
||||
let updateResponse: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
updateResponse = await window.electron.invoke<boolean>('db:character:update', updateData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
updateResponse = await System.authPostToServer<boolean>('character/update', updateData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:character:update', 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<void> {
|
||||
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 → IPC
|
||||
characterId = await window.electron.invoke<string>('db:character:create', requestData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
characterId = await System.authPostToServer<string>('character/add', requestData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:character:create', {...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<void> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user