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,
|
||||
};
|
||||
}
|
||||
982
hooks/settings/useLocations.ts
Normal file
982
hooks/settings/useLocations.ts
Normal file
@@ -0,0 +1,982 @@
|
||||
'use client'
|
||||
import {useCallback, useContext, useEffect, useState} from 'react';
|
||||
import {SeriesLocationItem, SeriesLocationElement, SeriesLocationSubElement} 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 System from '@/lib/models/System';
|
||||
import {useTranslations} from 'next-intl';
|
||||
import {ViewMode} from '@/shared/interface';
|
||||
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';
|
||||
|
||||
export interface SubElement {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Element {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
subElements: SubElement[];
|
||||
}
|
||||
|
||||
export interface LocationProps {
|
||||
id: string;
|
||||
name: string;
|
||||
elements: Element[];
|
||||
seriesLocationId?: string | null;
|
||||
}
|
||||
|
||||
interface LocationListResponse {
|
||||
locations: LocationProps[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface UseLocationsConfig {
|
||||
entityType: 'book' | 'series';
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
export interface UseLocationsReturn {
|
||||
// State
|
||||
sections: LocationProps[];
|
||||
seriesLocations: SeriesLocationItem[];
|
||||
toolEnabled: boolean;
|
||||
isLoading: boolean;
|
||||
isSeriesMode: boolean;
|
||||
bookSeriesId: string | null;
|
||||
newSectionName: string;
|
||||
newElementNames: { [key: string]: string };
|
||||
newSubElementNames: { [key: string]: string };
|
||||
|
||||
// Navigation state
|
||||
viewMode: ViewMode;
|
||||
selectedSectionIndex: number;
|
||||
sectionsBackup: LocationProps[] | null;
|
||||
|
||||
// Actions
|
||||
addSection: () => Promise<void>;
|
||||
addElement: (sectionId: string) => Promise<void>;
|
||||
addSubElement: (sectionId: string, elementIndex: number) => Promise<void>;
|
||||
removeSection: (sectionId: string) => Promise<void>;
|
||||
removeElement: (sectionId: string, elementIndex: number) => Promise<void>;
|
||||
removeSubElement: (sectionId: string, elementIndex: number, subElementIndex: number) => Promise<void>;
|
||||
updateElement: (sectionId: string, elementIndex: number, field: keyof Element, value: string) => void;
|
||||
updateSubElement: (sectionId: string, elementIndex: number, subElementIndex: number, field: keyof SubElement, value: string) => void;
|
||||
saveLocations: () => Promise<boolean>;
|
||||
toggleTool: (enabled: boolean) => Promise<void>;
|
||||
importFromSeries: (seriesLocationId: string) => Promise<void>;
|
||||
exportToSeries: (section: LocationProps) => Promise<void>;
|
||||
refreshLocations: () => Promise<void>;
|
||||
refreshSeriesLocations: () => Promise<void>;
|
||||
setNewSectionName: (name: string) => void;
|
||||
setNewElementNames: React.Dispatch<React.SetStateAction<{ [key: string]: string }>>;
|
||||
setNewSubElementNames: React.Dispatch<React.SetStateAction<{ [key: string]: string }>>;
|
||||
|
||||
// Navigation actions
|
||||
enterDetailMode: (sectionIndex: number) => void;
|
||||
enterEditMode: () => void;
|
||||
exitEditMode: (save: boolean) => Promise<void>;
|
||||
backToList: () => void;
|
||||
}
|
||||
|
||||
export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
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 [sections, setSections] = useState<LocationProps[]>([]);
|
||||
const [seriesLocations, setSeriesLocations] = useState<SeriesLocationItem[]>([]);
|
||||
const [toolEnabled, setToolEnabled] = useState<boolean>(entityType === 'series' || (book?.tools?.locations ?? false));
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [newSectionName, setNewSectionName] = useState<string>('');
|
||||
const [newElementNames, setNewElementNames] = useState<{ [key: string]: string }>({});
|
||||
const [newSubElementNames, setNewSubElementNames] = useState<{ [key: string]: string }>({});
|
||||
const [isAddingElement, setIsAddingElement] = useState<boolean>(false);
|
||||
const [isAddingSubElement, setIsAddingSubElement] = useState<boolean>(false);
|
||||
const [isAddingSection, setIsAddingSection] = useState<boolean>(false);
|
||||
|
||||
// Navigation state
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [selectedSectionIndex, setSelectedSectionIndex] = useState<number>(-1);
|
||||
const [sectionsBackup, setSectionsBackup] = useState<LocationProps[] | null>(null);
|
||||
|
||||
const isSeriesMode: boolean = entityType === 'series';
|
||||
const bookSeriesId: string | null = book?.seriesId || null;
|
||||
const userToken: string = session?.accessToken || '';
|
||||
|
||||
// Load locations on mount
|
||||
useEffect(function (): void {
|
||||
if (entityId) {
|
||||
refreshLocations();
|
||||
}
|
||||
}, [entityId]);
|
||||
|
||||
// Load series locations for book mode
|
||||
useEffect(function (): void {
|
||||
if (bookSeriesId && !isSeriesMode) {
|
||||
refreshSeriesLocations();
|
||||
}
|
||||
}, [bookSeriesId, isSeriesMode]);
|
||||
|
||||
const refreshSeriesLocations = useCallback(async function (): Promise<void> {
|
||||
if (!bookSeriesId) return;
|
||||
try {
|
||||
let response: SeriesLocationItem[];
|
||||
// Dual logic: offline ou livre local → IPC, sinon serveur
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<SeriesLocationItem[]>(
|
||||
'db:series:location:list',
|
||||
{seriesId: bookSeriesId}
|
||||
);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesLocationItem[]>(
|
||||
'series/location/list',
|
||||
userToken,
|
||||
lang,
|
||||
{seriesid: bookSeriesId}
|
||||
);
|
||||
}
|
||||
if (response) {
|
||||
setSeriesLocations(response);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
console.error('Error loading series locations:', e.message);
|
||||
}
|
||||
}
|
||||
}, [bookSeriesId, userToken, lang, isCurrentlyOffline, book?.localBook]);
|
||||
|
||||
const refreshLocations = useCallback(async function (): Promise<void> {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
let response: SeriesLocationItem[];
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesLocationItem[]>(
|
||||
'db:series:location:list',
|
||||
{seriesId: entityId}
|
||||
);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesLocationItem[]>(
|
||||
'series/location/list',
|
||||
userToken,
|
||||
lang,
|
||||
{seriesid: entityId}
|
||||
);
|
||||
}
|
||||
if (response) {
|
||||
const mappedLocations: LocationProps[] = response.map(function (loc: SeriesLocationItem): LocationProps {
|
||||
return {
|
||||
id: loc.id,
|
||||
name: loc.name,
|
||||
elements: loc.elements.map(function (elem: SeriesLocationElement): Element {
|
||||
return {
|
||||
id: elem.id,
|
||||
name: elem.name,
|
||||
description: elem.description,
|
||||
subElements: elem.subElements.map(function (sub: SeriesLocationSubElement): SubElement {
|
||||
return {
|
||||
id: sub.id,
|
||||
name: sub.name,
|
||||
description: sub.description,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
setSections(mappedLocations);
|
||||
}
|
||||
} else {
|
||||
let response: LocationListResponse;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<LocationListResponse>('db:location:all', {bookid: entityId});
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<LocationListResponse>('db:location:all', {bookid: entityId});
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<LocationListResponse>(
|
||||
'location/all',
|
||||
userToken,
|
||||
lang,
|
||||
{bookid: entityId}
|
||||
);
|
||||
}
|
||||
if (response) {
|
||||
setSections(response.locations);
|
||||
setToolEnabled(response.enabled);
|
||||
if (setBook && book) {
|
||||
setBook({
|
||||
...book,
|
||||
tools: {
|
||||
characters: book.tools?.characters ?? false,
|
||||
worlds: book.tools?.worlds ?? false,
|
||||
locations: response.enabled,
|
||||
spells: book.tools?.spells ?? false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownFetchLocations'));
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [entityId, isSeriesMode, userToken, lang, book, setBook, errorMessage, t, isCurrentlyOffline]);
|
||||
|
||||
const toggleTool = useCallback(async function (enabled: boolean): Promise<void> {
|
||||
if (isSeriesMode) return;
|
||||
try {
|
||||
const requestData = {
|
||||
bookId: book?.bookId,
|
||||
toolName: 'locations',
|
||||
enabled: enabled
|
||||
};
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:tool:update', requestData);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('book/tool-setting', requestData, userToken, lang);
|
||||
|
||||
if (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: book.tools?.characters ?? false,
|
||||
worlds: book.tools?.worlds ?? false,
|
||||
locations: enabled,
|
||||
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 addSection = useCallback(async function (): Promise<void> {
|
||||
if (isAddingSection) return;
|
||||
if (!newSectionName.trim()) {
|
||||
errorMessage(t('locationComponent.errorSectionNameEmpty'));
|
||||
return;
|
||||
}
|
||||
setIsAddingSection(true);
|
||||
try {
|
||||
let sectionId: string;
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
const addData = {
|
||||
seriesId: entityId,
|
||||
name: newSectionName,
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
sectionId = await window.electron.invoke<string>('db:series:location:section:add', addData);
|
||||
} else {
|
||||
sectionId = await System.authPostToServer<string>(
|
||||
'series/location/section/add',
|
||||
addData,
|
||||
userToken,
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:location:section:add', {...addData, id: sectionId});
|
||||
}
|
||||
}
|
||||
if (!sectionId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSection'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const requestData = {
|
||||
bookId: entityId,
|
||||
locationName: newSectionName,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
sectionId = await window.electron.invoke<string>('db:location:section:add', requestData);
|
||||
} else {
|
||||
sectionId = await System.authPostToServer<string>('location/section/add', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:section:add', {...requestData, id: sectionId});
|
||||
}
|
||||
}
|
||||
if (!sectionId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSection'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const newLocation: LocationProps = {
|
||||
id: sectionId,
|
||||
name: newSectionName,
|
||||
elements: [],
|
||||
};
|
||||
setSections(function (prev: LocationProps[]): LocationProps[] {
|
||||
return [...prev, newLocation];
|
||||
});
|
||||
setNewSectionName('');
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSection'));
|
||||
}
|
||||
} finally {
|
||||
setIsAddingSection(false);
|
||||
}
|
||||
}, [newSectionName, isSeriesMode, entityId, userToken, lang, errorMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, book, isAddingSection]);
|
||||
|
||||
const addElement = useCallback(async function (sectionId: string): Promise<void> {
|
||||
if (isAddingElement) return;
|
||||
if (!newElementNames[sectionId]?.trim()) {
|
||||
errorMessage(t('locationComponent.errorElementNameEmpty'));
|
||||
return;
|
||||
}
|
||||
setIsAddingElement(true);
|
||||
try {
|
||||
let elementId: string;
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
const addData = {
|
||||
locationId: sectionId,
|
||||
name: newElementNames[sectionId],
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
elementId = await window.electron.invoke<string>('db:series:location:element:add', addData);
|
||||
} else {
|
||||
elementId = await System.authPostToServer<string>(
|
||||
'series/location/element/add',
|
||||
addData,
|
||||
userToken,
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:location:element:add', {...addData, id: elementId});
|
||||
}
|
||||
}
|
||||
if (!elementId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddElement'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const requestData = {
|
||||
bookId: entityId,
|
||||
locationId: sectionId,
|
||||
elementName: newElementNames[sectionId],
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
elementId = await window.electron.invoke<string>('db:location:element:add', requestData);
|
||||
} else {
|
||||
elementId = await System.authPostToServer<string>('location/element/add', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:element:add', {...requestData, id: elementId});
|
||||
}
|
||||
}
|
||||
if (!elementId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddElement'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
setSections(function (prev: LocationProps[]): LocationProps[] {
|
||||
const updated: LocationProps[] = [...prev];
|
||||
const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean {
|
||||
return section.id === sectionId;
|
||||
});
|
||||
updated[sectionIndex].elements.push({
|
||||
id: elementId,
|
||||
name: newElementNames[sectionId],
|
||||
description: '',
|
||||
subElements: [],
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
setNewElementNames(function (prev: { [key: string]: string }): { [key: string]: string } {
|
||||
return {...prev, [sectionId]: ''};
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownAddElement'));
|
||||
}
|
||||
} finally {
|
||||
setIsAddingElement(false);
|
||||
}
|
||||
}, [newElementNames, isSeriesMode, entityId, userToken, lang, errorMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, book, isAddingElement]);
|
||||
|
||||
const addSubElement = useCallback(async function (sectionId: string, elementIndex: number): Promise<void> {
|
||||
if (isAddingSubElement) return;
|
||||
if (!newSubElementNames[elementIndex]?.trim()) {
|
||||
errorMessage(t('locationComponent.errorSubElementNameEmpty'));
|
||||
return;
|
||||
}
|
||||
setIsAddingSubElement(true);
|
||||
const sectionIndex: number = sections.findIndex(function (section: LocationProps): boolean {
|
||||
return section.id === sectionId;
|
||||
});
|
||||
try {
|
||||
let subElementId: string;
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
const addData = {
|
||||
elementId: sections[sectionIndex].elements[elementIndex].id,
|
||||
name: newSubElementNames[elementIndex],
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
subElementId = await window.electron.invoke<string>('db:series:location:subelement:add', addData);
|
||||
} else {
|
||||
subElementId = await System.authPostToServer<string>(
|
||||
'series/location/sub-element/add',
|
||||
addData,
|
||||
userToken,
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:location:subelement:add', {...addData, id: subElementId});
|
||||
}
|
||||
}
|
||||
if (!subElementId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSubElement'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const requestData = {
|
||||
elementId: sections[sectionIndex].elements[elementIndex].id,
|
||||
subElementName: newSubElementNames[elementIndex],
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
subElementId = await window.electron.invoke<string>('db:location:subelement:add', requestData);
|
||||
} else {
|
||||
subElementId = await System.authPostToServer<string>('location/sub-element/add', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:subelement:add', {...requestData, id: subElementId});
|
||||
}
|
||||
}
|
||||
if (!subElementId) {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSubElement'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
setSections(function (prev: LocationProps[]): LocationProps[] {
|
||||
const updated: LocationProps[] = [...prev];
|
||||
updated[sectionIndex].elements[elementIndex].subElements.push({
|
||||
id: subElementId,
|
||||
name: newSubElementNames[elementIndex],
|
||||
description: '',
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
setNewSubElementNames(function (prev: { [key: string]: string }): { [key: string]: string } {
|
||||
return {...prev, [elementIndex]: ''};
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownAddSubElement'));
|
||||
}
|
||||
} finally {
|
||||
setIsAddingSubElement(false);
|
||||
}
|
||||
}, [sections, newSubElementNames, isSeriesMode, userToken, lang, errorMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, entityId, book, isAddingSubElement]);
|
||||
|
||||
const removeSection = useCallback(async function (sectionId: string): Promise<void> {
|
||||
try {
|
||||
let success: boolean;
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
const deleteData = {locationId: sectionId};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:location:delete', deleteData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('series/location/delete', deleteData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:location:delete', deleteData);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const requestData = {
|
||||
locationId: sectionId,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:location:delete', requestData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('location/delete', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:delete', requestData);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteSection'));
|
||||
return;
|
||||
}
|
||||
setSections(function (prev: LocationProps[]): LocationProps[] {
|
||||
return prev.filter(function (section: LocationProps): boolean {
|
||||
return section.id !== sectionId;
|
||||
});
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteSection'));
|
||||
}
|
||||
}
|
||||
}, [isSeriesMode, userToken, lang, errorMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, entityId, book]);
|
||||
|
||||
const removeElement = useCallback(async function (sectionId: string, elementIndex: number): Promise<void> {
|
||||
try {
|
||||
const elementId: string | undefined = sections.find(function (section: LocationProps): boolean {
|
||||
return section.id === sectionId;
|
||||
})?.elements[elementIndex].id;
|
||||
let success: boolean;
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
const deleteData = {elementId: elementId};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:location:element:delete', deleteData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('series/location/element/delete', deleteData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:location:element:delete', deleteData);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const requestData = {
|
||||
elementId: elementId,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:location:element:delete', requestData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('location/element/delete', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:element:delete', requestData);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteElement'));
|
||||
return;
|
||||
}
|
||||
setSections(function (prev: LocationProps[]): LocationProps[] {
|
||||
const updated: LocationProps[] = [...prev];
|
||||
const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean {
|
||||
return section.id === sectionId;
|
||||
});
|
||||
updated[sectionIndex].elements.splice(elementIndex, 1);
|
||||
return updated;
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteElement'));
|
||||
}
|
||||
}
|
||||
}, [sections, isSeriesMode, userToken, lang, errorMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, entityId, book]);
|
||||
|
||||
const removeSubElement = useCallback(async function (sectionId: string, elementIndex: number, subElementIndex: number): Promise<void> {
|
||||
try {
|
||||
const subElementId: string | undefined = sections.find(function (section: LocationProps): boolean {
|
||||
return section.id === sectionId;
|
||||
})?.elements[elementIndex].subElements[subElementIndex].id;
|
||||
let success: boolean;
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
const deleteData = {subElementId: subElementId};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:location:subelement:delete', deleteData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('series/location/sub-element/delete', deleteData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:location:subelement:delete', deleteData);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const requestData = {
|
||||
subElementId: subElementId,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:location:subelement:delete', requestData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('location/sub-element/delete', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:subelement:delete', requestData);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteSubElement'));
|
||||
return;
|
||||
}
|
||||
setSections(function (prev: LocationProps[]): LocationProps[] {
|
||||
const updated: LocationProps[] = [...prev];
|
||||
const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean {
|
||||
return section.id === sectionId;
|
||||
});
|
||||
updated[sectionIndex].elements[elementIndex].subElements.splice(subElementIndex, 1);
|
||||
return updated;
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownDeleteSubElement'));
|
||||
}
|
||||
}
|
||||
}, [sections, isSeriesMode, userToken, lang, errorMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, entityId, book]);
|
||||
|
||||
const updateElement = useCallback(function (sectionId: string, elementIndex: number, field: keyof Element, value: string): void {
|
||||
setSections(function (prev: LocationProps[]): LocationProps[] {
|
||||
const updated: LocationProps[] = [...prev];
|
||||
const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean {
|
||||
return section.id === sectionId;
|
||||
});
|
||||
// @ts-ignore
|
||||
updated[sectionIndex].elements[elementIndex][field] = value;
|
||||
return updated;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const updateSubElement = useCallback(function (sectionId: string, elementIndex: number, subElementIndex: number, field: keyof SubElement, value: string): void {
|
||||
setSections(function (prev: LocationProps[]): LocationProps[] {
|
||||
const updated: LocationProps[] = [...prev];
|
||||
const sectionIndex: number = updated.findIndex(function (section: LocationProps): boolean {
|
||||
return section.id === sectionId;
|
||||
});
|
||||
updated[sectionIndex].elements[elementIndex].subElements[subElementIndex][field] = value;
|
||||
return updated;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const saveLocations = useCallback(async function (): Promise<boolean> {
|
||||
try {
|
||||
const requestData = {
|
||||
locations: sections,
|
||||
};
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:location:update', requestData);
|
||||
} else {
|
||||
response = await System.authPostToServer<boolean>('location/update', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:update', requestData);
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t('locationComponent.errorUnknownSave'));
|
||||
return false;
|
||||
}
|
||||
successMessage(t('locationComponent.successSave'));
|
||||
return true;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t('locationComponent.errorUnknownSave'));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, [sections, userToken, lang, errorMessage, successMessage, t, isCurrentlyOffline, addToQueue, localSyncedBooks, entityId, book]);
|
||||
|
||||
const exportToSeries = useCallback(async function (section: LocationProps): Promise<void> {
|
||||
if (!bookSeriesId) return;
|
||||
|
||||
try {
|
||||
const seriesLocationData = {
|
||||
seriesId: bookSeriesId,
|
||||
name: section.name,
|
||||
};
|
||||
|
||||
let seriesLocationId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
seriesLocationId = await window.electron.invoke<string>('db:series:location:section:add', seriesLocationData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
seriesLocationId = await System.authPostToServer<string>('series/location/section/add', seriesLocationData, userToken, lang);
|
||||
// Si la série a une copie locale → addToQueue avec l'ID du serveur
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === bookSeriesId)) {
|
||||
addToQueue('db:series:location:section:add', {...seriesLocationData, id: seriesLocationId});
|
||||
}
|
||||
}
|
||||
|
||||
if (seriesLocationId) {
|
||||
const updateData = {
|
||||
sectionId: section.id,
|
||||
sectionName: section.name,
|
||||
seriesLocationId: seriesLocationId,
|
||||
};
|
||||
|
||||
let updateResponse: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
updateResponse = await window.electron.invoke<boolean>('db:location:section:update', updateData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
updateResponse = await System.authPostToServer<boolean>('location/section/update', updateData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:section:update', updateData);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateResponse) {
|
||||
setSections(function (prev: LocationProps[]): LocationProps[] {
|
||||
return prev.map(function (s: LocationProps): LocationProps {
|
||||
return s.id === section.id ? {...s, seriesLocationId: seriesLocationId} : s;
|
||||
});
|
||||
});
|
||||
const newSeriesLocation: SeriesLocationItem = {
|
||||
id: seriesLocationId,
|
||||
name: section.name,
|
||||
elements: [],
|
||||
};
|
||||
setSeriesLocations(function (prev: SeriesLocationItem[]): SeriesLocationItem[] {
|
||||
return [...prev, newSeriesLocation];
|
||||
});
|
||||
successMessage(t("locationComponent.exportSuccess"));
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
}
|
||||
}, [bookSeriesId, userToken, lang, successMessage, errorMessage, t, isCurrentlyOffline, book, addToQueue, localSyncedBooks, localSyncedSeries, entityId]);
|
||||
|
||||
const importFromSeries = useCallback(async function (seriesLocationId: string): Promise<void> {
|
||||
const seriesLocation: SeriesLocationItem | undefined = seriesLocations.find(function (location: SeriesLocationItem): boolean {
|
||||
return location.id === seriesLocationId;
|
||||
});
|
||||
if (!seriesLocation) return;
|
||||
|
||||
try {
|
||||
const sectionData = {
|
||||
bookId: entityId,
|
||||
locationName: seriesLocation.name,
|
||||
seriesLocationId: seriesLocationId,
|
||||
};
|
||||
|
||||
let sectionId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
sectionId = await window.electron.invoke<string>('db:location:section:add', sectionData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
sectionId = await System.authPostToServer<string>('location/section/add', sectionData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue avec l'ID du serveur
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:section:add', {...sectionData, id: sectionId});
|
||||
}
|
||||
}
|
||||
|
||||
if (!sectionId) {
|
||||
errorMessage(t('locationComponent.importError'));
|
||||
return;
|
||||
}
|
||||
|
||||
const importedElements: Element[] = [];
|
||||
|
||||
for (const seriesElement of seriesLocation.elements) {
|
||||
const elementData = {
|
||||
bookId: entityId,
|
||||
locationId: sectionId,
|
||||
elementName: seriesElement.name,
|
||||
};
|
||||
|
||||
let elementId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
elementId = await window.electron.invoke<string>('db:location:element:add', elementData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
elementId = await System.authPostToServer<string>('location/element/add', elementData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue avec l'ID du serveur
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:element:add', {...elementData, id: elementId});
|
||||
}
|
||||
}
|
||||
|
||||
if (!elementId) continue;
|
||||
|
||||
const importedSubElements: SubElement[] = [];
|
||||
|
||||
for (const seriesSubElement of seriesElement.subElements) {
|
||||
const subElementData = {
|
||||
elementId: elementId,
|
||||
subElementName: seriesSubElement.name,
|
||||
};
|
||||
|
||||
let subElementId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
subElementId = await window.electron.invoke<string>('db:location:subelement:add', subElementData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
subElementId = await System.authPostToServer<string>('location/sub-element/add', subElementData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue avec l'ID du serveur
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:subelement:add', {...subElementData, id: subElementId});
|
||||
}
|
||||
}
|
||||
|
||||
if (subElementId) {
|
||||
importedSubElements.push({
|
||||
id: subElementId,
|
||||
name: seriesSubElement.name,
|
||||
description: seriesSubElement.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
importedElements.push({
|
||||
id: elementId,
|
||||
name: seriesElement.name,
|
||||
description: seriesElement.description,
|
||||
subElements: importedSubElements,
|
||||
});
|
||||
}
|
||||
|
||||
const newLocation: LocationProps = {
|
||||
id: sectionId,
|
||||
name: seriesLocation.name,
|
||||
elements: importedElements,
|
||||
seriesLocationId: seriesLocationId,
|
||||
};
|
||||
setSections(function (prev: LocationProps[]): LocationProps[] {
|
||||
return [...prev, newLocation];
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
}
|
||||
}, [seriesLocations, entityId, userToken, lang, errorMessage, successMessage, t, isCurrentlyOffline, book, addToQueue, localSyncedBooks]);
|
||||
|
||||
// Navigation functions
|
||||
const enterDetailMode = useCallback(function (sectionIndex: number): void {
|
||||
setSelectedSectionIndex(sectionIndex);
|
||||
setViewMode('detail');
|
||||
setSectionsBackup(null);
|
||||
}, []);
|
||||
|
||||
const enterEditMode = useCallback(function (): void {
|
||||
setSectionsBackup(sections.map(function (section: LocationProps): LocationProps {
|
||||
return {
|
||||
...section,
|
||||
elements: section.elements.map(function (element: Element): Element {
|
||||
return {
|
||||
...element,
|
||||
subElements: [...element.subElements]
|
||||
};
|
||||
})
|
||||
};
|
||||
}));
|
||||
setViewMode('edit');
|
||||
}, [sections]);
|
||||
|
||||
const exitEditMode = useCallback(async function (save: boolean): Promise<void> {
|
||||
if (save) {
|
||||
const success: boolean = await saveLocations();
|
||||
if (!success) {
|
||||
// Stay in edit mode on error
|
||||
return;
|
||||
}
|
||||
setViewMode('detail');
|
||||
} else {
|
||||
if (sectionsBackup) {
|
||||
setSections(sectionsBackup);
|
||||
setViewMode('detail');
|
||||
} else {
|
||||
setViewMode('list');
|
||||
}
|
||||
}
|
||||
setSectionsBackup(null);
|
||||
}, [saveLocations, sectionsBackup]);
|
||||
|
||||
const backToList = useCallback(function (): void {
|
||||
setSelectedSectionIndex(-1);
|
||||
setSectionsBackup(null);
|
||||
setViewMode('list');
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// State
|
||||
sections,
|
||||
seriesLocations,
|
||||
toolEnabled,
|
||||
isLoading,
|
||||
isSeriesMode,
|
||||
bookSeriesId,
|
||||
newSectionName,
|
||||
newElementNames,
|
||||
newSubElementNames,
|
||||
|
||||
// Navigation state
|
||||
viewMode,
|
||||
selectedSectionIndex,
|
||||
sectionsBackup,
|
||||
|
||||
// Actions
|
||||
addSection,
|
||||
addElement,
|
||||
addSubElement,
|
||||
removeSection,
|
||||
removeElement,
|
||||
removeSubElement,
|
||||
updateElement,
|
||||
updateSubElement,
|
||||
saveLocations,
|
||||
toggleTool,
|
||||
importFromSeries,
|
||||
exportToSeries,
|
||||
refreshLocations,
|
||||
refreshSeriesLocations,
|
||||
setNewSectionName,
|
||||
setNewElementNames,
|
||||
setNewSubElementNames,
|
||||
|
||||
// Navigation actions
|
||||
enterDetailMode,
|
||||
enterEditMode,
|
||||
exitEditMode,
|
||||
backToList,
|
||||
};
|
||||
}
|
||||
975
hooks/settings/useSpells.ts
Normal file
975
hooks/settings/useSpells.ts
Normal file
@@ -0,0 +1,975 @@
|
||||
'use client'
|
||||
import {useCallback, useContext, useEffect, useState} from 'react';
|
||||
import {
|
||||
initialSpellState,
|
||||
SpellEditState,
|
||||
SpellListItem,
|
||||
SpellListResponse,
|
||||
SpellProps,
|
||||
SpellTagProps
|
||||
} from '@/lib/models/Spell';
|
||||
import {SeriesSpellDetailResponse, SeriesSpellListItem, SeriesSpellListResponse} 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 System from '@/lib/models/System';
|
||||
import {useTranslations} from 'next-intl';
|
||||
import {ViewMode} from '@/shared/interface';
|
||||
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';
|
||||
|
||||
export interface UseSpellsConfig {
|
||||
entityType: 'book' | 'series';
|
||||
entityId: string;
|
||||
}
|
||||
|
||||
export interface UseSpellsReturn {
|
||||
spells: SpellListItem[];
|
||||
seriesSpells: SeriesSpellListItem[];
|
||||
tags: SpellTagProps[];
|
||||
selectedSpell: SpellEditState | null;
|
||||
selectedSeriesSpell: SeriesSpellDetailResponse | null;
|
||||
toolEnabled: boolean;
|
||||
isLoading: boolean;
|
||||
isSeriesMode: boolean;
|
||||
bookSeriesId: string | null;
|
||||
showTagManager: boolean;
|
||||
viewMode: ViewMode;
|
||||
spellBackup: SpellEditState | null;
|
||||
selectSpell: (spell: SpellListItem) => Promise<void>;
|
||||
addNewSpell: () => void;
|
||||
clearSelection: () => void;
|
||||
saveSpell: () => Promise<boolean>;
|
||||
deleteSpell: (spellId: string) => Promise<void>;
|
||||
updateSpellField: (key: keyof SpellEditState, value: string | string[] | null) => void;
|
||||
toggleTool: (enabled: boolean) => Promise<void>;
|
||||
importFromSeries: (seriesSpellId: string) => Promise<void>;
|
||||
exportToSeries: () => Promise<void>;
|
||||
refreshSeriesSpells: () => Promise<void>;
|
||||
setSelectedSpell: React.Dispatch<React.SetStateAction<SpellEditState | null>>;
|
||||
setShowTagManager: (show: boolean) => void;
|
||||
|
||||
enterDetailMode: (spell: SpellListItem) => Promise<void>;
|
||||
enterEditMode: () => void;
|
||||
exitEditMode: (save: boolean) => Promise<void>;
|
||||
backToList: () => void;
|
||||
|
||||
createTag: (name: string, color: string) => Promise<SpellTagProps | null>;
|
||||
updateTag: (tagId: string, name: string, color: string) => Promise<boolean>;
|
||||
deleteTag: (tagId: string) => Promise<boolean>;
|
||||
handleSyncComplete: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
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 [spells, setSpells] = useState<SpellListItem[]>([]);
|
||||
const [seriesSpells, setSeriesSpells] = useState<SeriesSpellListItem[]>([]);
|
||||
const [tags, setTags] = useState<SpellTagProps[]>([]);
|
||||
const [selectedSpell, setSelectedSpell] = useState<SpellEditState | null>(null);
|
||||
const [selectedSeriesSpell, setSelectedSeriesSpell] = useState<SeriesSpellDetailResponse | null>(null);
|
||||
const [toolEnabled, setToolEnabled] = useState<boolean>(entityType === 'series' || (book?.tools?.spells ?? false));
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [showTagManager, setShowTagManager] = useState<boolean>(false);
|
||||
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [spellBackup, setSpellBackup] = useState<SpellEditState | null>(null);
|
||||
|
||||
const isSeriesMode: boolean = entityType === 'series';
|
||||
const bookSeriesId: string | null = book?.seriesId || null;
|
||||
const userToken: string = session?.accessToken || '';
|
||||
|
||||
useEffect(function (): void {
|
||||
if (entityId) {
|
||||
refreshSpells().then();
|
||||
}
|
||||
}, [entityId]);
|
||||
|
||||
useEffect(function (): void {
|
||||
if (bookSeriesId && !isSeriesMode) {
|
||||
refreshSeriesSpells().then();
|
||||
}
|
||||
}, [bookSeriesId, isSeriesMode]);
|
||||
|
||||
const refreshSeriesSpells = useCallback(async function (): Promise<void> {
|
||||
if (!bookSeriesId) return;
|
||||
try {
|
||||
let response: SeriesSpellListResponse;
|
||||
// Dual logic: offline ou livre local → IPC, sinon serveur
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<SeriesSpellListResponse>(
|
||||
'db:series:spell:list',
|
||||
{seriesId: bookSeriesId}
|
||||
);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesSpellListResponse>(
|
||||
'series/spell/list',
|
||||
userToken,
|
||||
lang,
|
||||
{seriesid: bookSeriesId}
|
||||
);
|
||||
}
|
||||
if (response) {
|
||||
setSeriesSpells(response.spells);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
console.error('Error loading series spells:', e.message);
|
||||
}
|
||||
}
|
||||
}, [bookSeriesId, userToken, lang, isCurrentlyOffline, book?.localBook]);
|
||||
|
||||
const refreshSpells = useCallback(async function (): Promise<void> {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
let response: SeriesSpellListResponse;
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesSpellListResponse>(
|
||||
'db:series:spell:list',
|
||||
{seriesId: entityId}
|
||||
);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesSpellListResponse>(
|
||||
'series/spell/list',
|
||||
userToken,
|
||||
lang,
|
||||
{seriesid: entityId}
|
||||
);
|
||||
}
|
||||
if (response) {
|
||||
const mappedSpells: SpellListItem[] = response.spells.map(function (spell: SeriesSpellListItem): SpellListItem {
|
||||
return {
|
||||
id: spell.id,
|
||||
name: spell.name,
|
||||
description: spell.description,
|
||||
tags: spell.tags ? spell.tags.map(function (tagId: string): SpellTagProps {
|
||||
const foundTag: SpellTagProps | undefined = response.tags.find(function (t: SpellTagProps): boolean {
|
||||
return t.id === tagId;
|
||||
});
|
||||
return foundTag || {id: tagId, name: tagId, color: null};
|
||||
}) : [],
|
||||
};
|
||||
});
|
||||
setSpells(mappedSpells);
|
||||
setTags(response.tags || []);
|
||||
}
|
||||
} else {
|
||||
let response: SpellListResponse;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<SpellListResponse>('db:spell:list', {bookid: entityId});
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<SpellListResponse>('db:spell:list', {bookid: entityId});
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SpellListResponse>(
|
||||
'spell/list',
|
||||
userToken,
|
||||
lang,
|
||||
{bookid: entityId}
|
||||
);
|
||||
}
|
||||
if (response) {
|
||||
setSpells(response.spells.map(function (spell: SpellListItem): SpellListItem {
|
||||
return {
|
||||
...spell,
|
||||
tags: spell.tags || []
|
||||
};
|
||||
}));
|
||||
setTags(response.tags || []);
|
||||
setToolEnabled(response.enabled);
|
||||
if (setBook && book) {
|
||||
setBook({
|
||||
...book,
|
||||
tools: {
|
||||
characters: book.tools?.characters ?? false,
|
||||
worlds: book.tools?.worlds ?? false,
|
||||
locations: book.tools?.locations ?? false,
|
||||
spells: response.enabled
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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 selectSpell = useCallback(async function (spell: SpellListItem): Promise<void> {
|
||||
const tagIds: string[] = spell.tags ? spell.tags.map(function (tag: SpellTagProps): string {
|
||||
return tag.id;
|
||||
}) : [];
|
||||
|
||||
setSelectedSpell({
|
||||
id: spell.id,
|
||||
name: spell.name,
|
||||
description: spell.description,
|
||||
appearance: '',
|
||||
tags: tagIds,
|
||||
powerLevel: null,
|
||||
components: null,
|
||||
limitations: null,
|
||||
notes: null,
|
||||
seriesSpellId: spell.seriesSpellId || null,
|
||||
});
|
||||
setSelectedSeriesSpell(null);
|
||||
|
||||
try {
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
let response: SeriesSpellDetailResponse;
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesSpellDetailResponse>(
|
||||
'db:series:spell:detail',
|
||||
{spellId: spell.id}
|
||||
);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
|
||||
'series/spell/detail',
|
||||
userToken,
|
||||
lang,
|
||||
{spellid: spell.id}
|
||||
);
|
||||
}
|
||||
if (response) {
|
||||
setSelectedSpell(function (prev: SpellEditState | null): SpellEditState | null {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
appearance: response.appearance,
|
||||
powerLevel: response.powerLevel,
|
||||
components: response.components,
|
||||
limitations: response.limitations,
|
||||
notes: response.notes,
|
||||
};
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let response: SpellProps;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<SpellProps>('db:spell:detail', {spellid: spell.id});
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<SpellProps>('db:spell:detail', {spellid: spell.id});
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SpellProps>(
|
||||
'spell/detail',
|
||||
userToken,
|
||||
lang,
|
||||
{spellid: spell.id}
|
||||
);
|
||||
}
|
||||
if (response) {
|
||||
setSelectedSpell(function (prev: SpellEditState | null): SpellEditState | null {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
appearance: response.appearance,
|
||||
powerLevel: response.powerLevel,
|
||||
components: response.components,
|
||||
limitations: response.limitations,
|
||||
notes: response.notes,
|
||||
seriesSpellId: response.seriesSpellId || null,
|
||||
};
|
||||
});
|
||||
|
||||
if (response.seriesSpellId) {
|
||||
const seriesSpellResponse: SeriesSpellDetailResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
|
||||
'series/spell/detail',
|
||||
userToken,
|
||||
lang,
|
||||
{spellid: response.seriesSpellId}
|
||||
);
|
||||
if (seriesSpellResponse) {
|
||||
setSelectedSeriesSpell(seriesSpellResponse);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
}
|
||||
}, [isSeriesMode, userToken, lang, errorMessage, isCurrentlyOffline, book?.localBook]);
|
||||
|
||||
const addNewSpell = useCallback(function (): void {
|
||||
setSelectedSpell({...initialSpellState});
|
||||
setSelectedSeriesSpell(null);
|
||||
setViewMode('edit');
|
||||
setSpellBackup(null);
|
||||
}, []);
|
||||
|
||||
const clearSelection = useCallback(function (): void {
|
||||
setSelectedSpell(null);
|
||||
setSelectedSeriesSpell(null);
|
||||
setViewMode('list');
|
||||
setSpellBackup(null);
|
||||
}, []);
|
||||
|
||||
const updateSpellField = useCallback(function (key: keyof SpellEditState, value: string | string[] | null): void {
|
||||
if (selectedSpell) {
|
||||
setSelectedSpell({...selectedSpell, [key]: value});
|
||||
}
|
||||
}, [selectedSpell]);
|
||||
|
||||
const toggleTool = useCallback(async function (enabled: boolean): Promise<void> {
|
||||
if (isSeriesMode) return;
|
||||
try {
|
||||
const requestData = {
|
||||
bookId: book?.bookId,
|
||||
toolName: 'spells',
|
||||
enabled: enabled
|
||||
};
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:tool:update', requestData);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('book/tool-setting', requestData, userToken, lang);
|
||||
if (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: book.tools?.characters ?? false,
|
||||
worlds: book.tools?.worlds ?? false,
|
||||
locations: book.tools?.locations ?? false,
|
||||
spells: enabled
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
}
|
||||
}, [isSeriesMode, book, setBook, userToken, lang, errorMessage, isCurrentlyOffline, localSyncedBooks, addToQueue]);
|
||||
|
||||
const saveSpell = useCallback(async function (): Promise<boolean> {
|
||||
if (!selectedSpell) return false;
|
||||
|
||||
if (selectedSpell.id === null) {
|
||||
return await addSpellInternal(selectedSpell);
|
||||
} else {
|
||||
return await updateSpellInternal(selectedSpell);
|
||||
}
|
||||
}, [selectedSpell]);
|
||||
|
||||
async function addSpellInternal(spell: SpellEditState): Promise<boolean> {
|
||||
if (!spell.name) {
|
||||
errorMessage(t("spellComponent.errorNameRequired"));
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
let newSpellId: string;
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
const data = {
|
||||
seriesId: entityId,
|
||||
name: spell.name,
|
||||
description: spell.description,
|
||||
appearance: spell.appearance,
|
||||
tags: spell.tags,
|
||||
powerLevel: spell.powerLevel,
|
||||
components: spell.components,
|
||||
limitations: spell.limitations,
|
||||
notes: spell.notes,
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
newSpellId = await window.electron.invoke<string>('db:series:spell:add', data);
|
||||
} else {
|
||||
newSpellId = await System.authPostToServer<string>('series/spell/add', data, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:spell:add', {...data, id: newSpellId});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const data = {
|
||||
bookId: entityId,
|
||||
spell: {
|
||||
name: spell.name,
|
||||
description: spell.description,
|
||||
appearance: spell.appearance,
|
||||
tags: spell.tags,
|
||||
powerLevel: spell.powerLevel,
|
||||
components: spell.components,
|
||||
limitations: spell.limitations,
|
||||
notes: spell.notes,
|
||||
}
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
newSpellId = await window.electron.invoke<string>('db:spell:create', data);
|
||||
} else {
|
||||
newSpellId = await System.authPostToServer<string>('spell/add', data, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:create', {...data, id: newSpellId});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!newSpellId) {
|
||||
errorMessage(t("spellComponent.errorAddSpell"));
|
||||
return false;
|
||||
}
|
||||
const resolvedTags: SpellTagProps[] = tags.filter(function (tag: SpellTagProps): boolean {
|
||||
return spell.tags.includes(tag.id);
|
||||
});
|
||||
const newSpellListItem: SpellListItem = {
|
||||
id: newSpellId,
|
||||
name: spell.name,
|
||||
description: spell.description.length > 150
|
||||
? spell.description.substring(0, 150) + '...'
|
||||
: spell.description,
|
||||
tags: resolvedTags,
|
||||
};
|
||||
setSpells(function (prev: SpellListItem[]): SpellListItem[] {
|
||||
return [...prev, newSpellListItem];
|
||||
});
|
||||
setSelectedSpell(null);
|
||||
return true;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("common.unknownError"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSpellInternal(spellToUpdate: SpellEditState): Promise<boolean> {
|
||||
if (!spellToUpdate.id) return false;
|
||||
if (!spellToUpdate.name) {
|
||||
errorMessage(t("spellComponent.errorNameRequired"));
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
let success: boolean;
|
||||
const data = {
|
||||
id: spellToUpdate.id,
|
||||
name: spellToUpdate.name,
|
||||
description: spellToUpdate.description,
|
||||
appearance: spellToUpdate.appearance,
|
||||
tags: spellToUpdate.tags,
|
||||
powerLevel: spellToUpdate.powerLevel,
|
||||
components: spellToUpdate.components,
|
||||
limitations: spellToUpdate.limitations,
|
||||
notes: spellToUpdate.notes,
|
||||
};
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:spell:update', data);
|
||||
} else {
|
||||
success = await System.authPutToServer<boolean>('series/spell/update', data, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:spell:update', data);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:spell:update', data);
|
||||
} else {
|
||||
success = await System.authPutToServer<boolean>('spell/update', data, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:update', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
errorMessage(t("spellComponent.errorUpdateSpell"));
|
||||
return false;
|
||||
}
|
||||
const resolvedTags: SpellTagProps[] = tags.filter(function (tag: SpellTagProps): boolean {
|
||||
return spellToUpdate.tags.includes(tag.id);
|
||||
});
|
||||
setSpells(function (prev: SpellListItem[]): SpellListItem[] {
|
||||
return prev.map(function (spell: SpellListItem): SpellListItem {
|
||||
return spell.id === spellToUpdate.id ? {
|
||||
id: spellToUpdate.id,
|
||||
name: spellToUpdate.name,
|
||||
description: spellToUpdate.description.length > 150
|
||||
? spellToUpdate.description.substring(0, 150) + '...'
|
||||
: spellToUpdate.description,
|
||||
tags: resolvedTags,
|
||||
} : spell;
|
||||
});
|
||||
});
|
||||
setSelectedSpell(null);
|
||||
successMessage(t("spellComponent.successUpdate"));
|
||||
return true;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("common.unknownError"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSpell = useCallback(async function (spellId: string): Promise<void> {
|
||||
try {
|
||||
let success: boolean;
|
||||
const requestData = {spellId};
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:spell:delete', requestData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('series/spell/delete', requestData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:spell:delete', requestData);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:spell:delete', requestData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('spell/delete', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:delete', requestData);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
errorMessage(t("spellComponent.errorDeleteSpell"));
|
||||
return;
|
||||
}
|
||||
setSpells(function (prev: SpellListItem[]): SpellListItem[] {
|
||||
return prev.filter(function (s: SpellListItem): boolean {
|
||||
return s.id !== spellId;
|
||||
});
|
||||
});
|
||||
setSelectedSpell(null);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("common.unknownError"));
|
||||
}
|
||||
}
|
||||
}, [isSeriesMode, userToken, lang, errorMessage, t, isCurrentlyOffline, book?.localBook, entityId, localSyncedBooks, addToQueue]);
|
||||
|
||||
const exportToSeries = useCallback(async function (): Promise<void> {
|
||||
if (!selectedSpell || !selectedSpell.id || !bookSeriesId) return;
|
||||
|
||||
try {
|
||||
const seriesSpellData = {
|
||||
seriesId: bookSeriesId,
|
||||
name: selectedSpell.name,
|
||||
description: selectedSpell.description,
|
||||
appearance: selectedSpell.appearance || '',
|
||||
tags: [],
|
||||
powerLevel: selectedSpell.powerLevel || null,
|
||||
components: selectedSpell.components || null,
|
||||
limitations: selectedSpell.limitations || null,
|
||||
notes: selectedSpell.notes || null,
|
||||
};
|
||||
|
||||
let seriesSpellId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
seriesSpellId = await window.electron.invoke<string>('db:series:spell:add', seriesSpellData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
seriesSpellId = await System.authPostToServer<string>(
|
||||
'series/spell/add',
|
||||
seriesSpellData,
|
||||
userToken,
|
||||
lang
|
||||
);
|
||||
// Si la série a une copie locale → addToQueue
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === bookSeriesId)) {
|
||||
addToQueue('db:series:spell:add', {...seriesSpellData, id: seriesSpellId});
|
||||
}
|
||||
}
|
||||
|
||||
if (seriesSpellId) {
|
||||
const updateData = {
|
||||
id: selectedSpell.id,
|
||||
name: selectedSpell.name,
|
||||
description: selectedSpell.description,
|
||||
appearance: selectedSpell.appearance,
|
||||
tags: selectedSpell.tags,
|
||||
powerLevel: selectedSpell.powerLevel,
|
||||
components: selectedSpell.components,
|
||||
limitations: selectedSpell.limitations,
|
||||
notes: selectedSpell.notes,
|
||||
seriesSpellId: seriesSpellId
|
||||
};
|
||||
|
||||
let updateSuccess: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
updateSuccess = await window.electron.invoke<boolean>('db:spell:update', updateData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
updateSuccess = await System.authPutToServer<boolean>('spell/update', updateData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:update', updateData);
|
||||
}
|
||||
}
|
||||
|
||||
if (updateSuccess) {
|
||||
setSelectedSpell({...selectedSpell, seriesSpellId: seriesSpellId});
|
||||
setSpells(function (prev: SpellListItem[]): SpellListItem[] {
|
||||
return prev.map(function (s: SpellListItem): SpellListItem {
|
||||
return s.id === selectedSpell.id ? {...s, seriesSpellId: seriesSpellId} : s;
|
||||
});
|
||||
});
|
||||
const newSeriesSpell: SeriesSpellListItem = {
|
||||
id: seriesSpellId,
|
||||
name: selectedSpell.name,
|
||||
description: selectedSpell.description.length > 150
|
||||
? selectedSpell.description.substring(0, 150) + '...'
|
||||
: selectedSpell.description,
|
||||
tags: null,
|
||||
};
|
||||
setSeriesSpells(function (prev: SeriesSpellListItem[]): SeriesSpellListItem[] {
|
||||
return [...prev, newSeriesSpell];
|
||||
});
|
||||
successMessage(t("spellComponent.exportSuccess"));
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
}
|
||||
}, [selectedSpell, bookSeriesId, userToken, lang, successMessage, errorMessage, t, isCurrentlyOffline, book, addToQueue, localSyncedBooks, localSyncedSeries, entityId]);
|
||||
|
||||
const importFromSeries = useCallback(async function (seriesSpellId: string): Promise<void> {
|
||||
try {
|
||||
// 1. Récupérer les détails du sort de la série
|
||||
let seriesSpellDetail: SeriesSpellDetailResponse;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline → IPC pour récupérer les détails du sort de la série locale
|
||||
seriesSpellDetail = await window.electron.invoke<SeriesSpellDetailResponse>(
|
||||
'db:series:spell:detail',
|
||||
{spellId: seriesSpellId}
|
||||
);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
seriesSpellDetail = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
|
||||
'series/spell/detail',
|
||||
userToken,
|
||||
lang,
|
||||
{spellid: seriesSpellId}
|
||||
);
|
||||
}
|
||||
|
||||
if (!seriesSpellDetail) return;
|
||||
|
||||
// 2. Créer le sort dans le livre
|
||||
const spellData = {
|
||||
bookId: entityId,
|
||||
spell: {
|
||||
name: seriesSpellDetail.name,
|
||||
description: seriesSpellDetail.description,
|
||||
appearance: seriesSpellDetail.appearance || '',
|
||||
tags: [],
|
||||
powerLevel: seriesSpellDetail.powerLevel,
|
||||
components: seriesSpellDetail.components,
|
||||
limitations: seriesSpellDetail.limitations,
|
||||
notes: seriesSpellDetail.notes,
|
||||
seriesSpellId: seriesSpellId
|
||||
}
|
||||
};
|
||||
|
||||
let createdSpellId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
createdSpellId = await window.electron.invoke<string>('db:spell:create', spellData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
createdSpellId = await System.authPostToServer<string>('spell/add', spellData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:create', {...spellData, id: createdSpellId});
|
||||
}
|
||||
}
|
||||
|
||||
if (createdSpellId) {
|
||||
const newSpellListItem: SpellListItem = {
|
||||
id: createdSpellId,
|
||||
name: seriesSpellDetail.name,
|
||||
description: seriesSpellDetail.description.length > 150
|
||||
? seriesSpellDetail.description.substring(0, 150) + '...'
|
||||
: seriesSpellDetail.description,
|
||||
tags: [],
|
||||
seriesSpellId: seriesSpellId,
|
||||
};
|
||||
setSpells(function (prev: SpellListItem[]): SpellListItem[] {
|
||||
return [...prev, newSpellListItem];
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
}
|
||||
}, [entityId, userToken, lang, errorMessage, successMessage, t, isCurrentlyOffline, book, addToQueue, localSyncedBooks]);
|
||||
|
||||
const createTag = useCallback(async function (name: string, color: string): Promise<SpellTagProps | null> {
|
||||
try {
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
const addData = {
|
||||
seriesId: entityId,
|
||||
name: name,
|
||||
color: color,
|
||||
};
|
||||
let tagId: string;
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
tagId = await window.electron.invoke<string>('db:series:spell:tag:add', addData);
|
||||
} else {
|
||||
tagId = await System.authPostToServer<string>(
|
||||
'series/spell/tag/add',
|
||||
addData,
|
||||
userToken,
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:spell:tag:add', {...addData, id: tagId});
|
||||
}
|
||||
}
|
||||
if (tagId) {
|
||||
const newTag: SpellTagProps = {id: tagId, name: name, color: color};
|
||||
setTags(function (prev: SpellTagProps[]): SpellTagProps[] {
|
||||
return [...prev, newTag];
|
||||
});
|
||||
return newTag;
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
const requestData = {
|
||||
bookId: entityId,
|
||||
name: name,
|
||||
color: color,
|
||||
};
|
||||
let newTag: SpellTagProps;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
newTag = await window.electron.invoke<SpellTagProps>('db:spell:tag:create', requestData);
|
||||
} else {
|
||||
newTag = await System.authPostToServer<SpellTagProps>('spell/tag/add', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:tag:create', {...requestData, id: newTag?.id});
|
||||
}
|
||||
}
|
||||
if (newTag && newTag.id) {
|
||||
setTags(function (prev: SpellTagProps[]): SpellTagProps[] {
|
||||
return [...prev, newTag];
|
||||
});
|
||||
return newTag;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}, [isSeriesMode, entityId, userToken, lang, errorMessage, isCurrentlyOffline, book?.localBook, localSyncedBooks, addToQueue]);
|
||||
|
||||
const updateTag = useCallback(async function (tagId: string, name: string, color: string): Promise<boolean> {
|
||||
try {
|
||||
let success: boolean;
|
||||
const requestData = {tagId, name, color};
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:spell:tag:update', requestData);
|
||||
} else {
|
||||
success = await System.authPutToServer<boolean>('series/spell/tag/update', requestData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:spell:tag:update', requestData);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:spell:tag:update', requestData);
|
||||
} else {
|
||||
success = await System.authPutToServer<boolean>('spell/tag/update', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:tag:update', requestData);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
errorMessage(t("spellComponent.updateSuccess"));
|
||||
return false;
|
||||
}
|
||||
setTags(function (prev: SpellTagProps[]): SpellTagProps[] {
|
||||
return prev.map(function (tag: SpellTagProps): SpellTagProps {
|
||||
return tag.id === tagId ? {id: tagId, name: name, color: color} : tag;
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, [isSeriesMode, userToken, lang, errorMessage, t, isCurrentlyOffline, book?.localBook, entityId, localSyncedBooks, addToQueue]);
|
||||
|
||||
const deleteTag = useCallback(async function (tagId: string): Promise<boolean> {
|
||||
try {
|
||||
let success: boolean;
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
const deleteData = {tagId};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:spell:tag:delete', deleteData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('series/spell/tag/delete', deleteData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:spell:tag:delete', deleteData);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const requestData = {tagId, bookId: entityId};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:spell:tag:delete', requestData);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('spell/tag/delete', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:tag:delete', requestData);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (success) {
|
||||
setTags(function (prev: SpellTagProps[]): SpellTagProps[] {
|
||||
return prev.filter(function (tag: SpellTagProps): boolean {
|
||||
return tag.id !== tagId;
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, [isSeriesMode, entityId, userToken, lang, errorMessage, isCurrentlyOffline, book?.localBook, localSyncedBooks, addToQueue]);
|
||||
|
||||
const handleSyncComplete = useCallback(async function (): Promise<void> {
|
||||
if (selectedSpell?.seriesSpellId) {
|
||||
const seriesSpellResponse: SeriesSpellDetailResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
|
||||
'series/spell/detail',
|
||||
userToken,
|
||||
lang,
|
||||
{spellid: selectedSpell.seriesSpellId}
|
||||
);
|
||||
if (seriesSpellResponse) {
|
||||
setSelectedSeriesSpell(seriesSpellResponse);
|
||||
}
|
||||
}
|
||||
}, [selectedSpell?.seriesSpellId, userToken, lang]);
|
||||
|
||||
const enterDetailMode = useCallback(async function (spell: SpellListItem): Promise<void> {
|
||||
await selectSpell(spell);
|
||||
setViewMode('detail');
|
||||
setSpellBackup(null);
|
||||
}, [selectSpell]);
|
||||
|
||||
const enterEditMode = useCallback(function (): void {
|
||||
if (selectedSpell) {
|
||||
setSpellBackup({...selectedSpell});
|
||||
}
|
||||
setViewMode('edit');
|
||||
}, [selectedSpell]);
|
||||
|
||||
const exitEditMode = useCallback(async function (save: boolean): Promise<void> {
|
||||
if (save) {
|
||||
const success: boolean = await saveSpell();
|
||||
if (!success) return;
|
||||
if (spellBackup) {
|
||||
setViewMode('detail');
|
||||
} else {
|
||||
setViewMode('list');
|
||||
}
|
||||
} else {
|
||||
if (spellBackup) {
|
||||
setSelectedSpell(spellBackup);
|
||||
setViewMode('detail');
|
||||
} else {
|
||||
setSelectedSpell(null);
|
||||
setViewMode('list');
|
||||
}
|
||||
}
|
||||
setSpellBackup(null);
|
||||
}, [saveSpell, spellBackup]);
|
||||
|
||||
const backToList = useCallback(function (): void {
|
||||
setSelectedSpell(null);
|
||||
setSelectedSeriesSpell(null);
|
||||
setSpellBackup(null);
|
||||
setViewMode('list');
|
||||
}, []);
|
||||
|
||||
return {
|
||||
spells,
|
||||
seriesSpells,
|
||||
tags,
|
||||
selectedSpell,
|
||||
selectedSeriesSpell,
|
||||
toolEnabled,
|
||||
isLoading,
|
||||
isSeriesMode,
|
||||
bookSeriesId,
|
||||
showTagManager,
|
||||
viewMode,
|
||||
spellBackup,
|
||||
selectSpell,
|
||||
addNewSpell,
|
||||
clearSelection,
|
||||
saveSpell,
|
||||
deleteSpell,
|
||||
updateSpellField,
|
||||
toggleTool,
|
||||
importFromSeries,
|
||||
exportToSeries,
|
||||
refreshSeriesSpells,
|
||||
setSelectedSpell,
|
||||
setShowTagManager,
|
||||
enterDetailMode,
|
||||
enterEditMode,
|
||||
exitEditMode,
|
||||
backToList,
|
||||
createTag,
|
||||
updateTag,
|
||||
deleteTag,
|
||||
handleSyncComplete,
|
||||
};
|
||||
}
|
||||
720
hooks/settings/useWorlds.ts
Normal file
720
hooks/settings/useWorlds.ts
Normal file
@@ -0,0 +1,720 @@
|
||||
'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';
|
||||
|
||||
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<void>;
|
||||
saveWorld: () => Promise<boolean>;
|
||||
updateWorldField: (field: keyof WorldProps, value: string) => void;
|
||||
updateWorldArrayField: (field: keyof WorldProps, value: unknown) => void;
|
||||
toggleTool: (enabled: boolean) => Promise<void>;
|
||||
importFromSeries: (seriesWorldId: string) => Promise<void>;
|
||||
exportToSeries: () => Promise<void>;
|
||||
refreshWorlds: () => Promise<void>;
|
||||
refreshSeriesWorlds: () => Promise<void>;
|
||||
setShowAddNewWorld: (show: boolean) => void;
|
||||
setNewWorldName: (name: string) => void;
|
||||
setWorlds: React.Dispatch<React.SetStateAction<WorldProps[]>>;
|
||||
getSeriesWorldForCurrentWorld: () => SeriesWorldProps | null;
|
||||
|
||||
// Navigation actions
|
||||
enterDetailMode: (worldId: string) => void;
|
||||
enterEditMode: () => void;
|
||||
exitEditMode: (save: boolean) => Promise<void>;
|
||||
backToList: () => void;
|
||||
}
|
||||
|
||||
export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
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 [worlds, setWorlds] = useState<WorldProps[]>([]);
|
||||
const [seriesWorlds, setSeriesWorlds] = useState<SeriesWorldProps[]>([]);
|
||||
const [selectedWorldIndex, setSelectedWorldIndex] = useState<number>(0);
|
||||
const [worldsSelector, setWorldsSelector] = useState<SelectBoxProps[]>([]);
|
||||
const [toolEnabled, setToolEnabled] = useState<boolean>(entityType === 'series' || (book?.tools?.worlds ?? false));
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [showAddNewWorld, setShowAddNewWorld] = useState<boolean>(false);
|
||||
const [newWorldName, setNewWorldName] = useState<string>('');
|
||||
|
||||
// Navigation state
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
const [worldBackup, setWorldBackup] = useState<WorldProps | null>(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<void> {
|
||||
if (!bookSeriesId) return;
|
||||
try {
|
||||
let response: SeriesWorldProps[];
|
||||
// Dual logic: offline ou livre local → IPC, sinon serveur
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<SeriesWorldProps[]>(
|
||||
'db:series:world:list',
|
||||
{seriesId: bookSeriesId}
|
||||
);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesWorldProps[]>(
|
||||
'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<void> {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
let response: SeriesWorldProps[];
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesWorldProps[]>(
|
||||
'db:series:world:list',
|
||||
{seriesId: entityId}
|
||||
);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesWorldProps[]>(
|
||||
'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 window.electron.invoke<WorldListResponse>('db:book:worlds:get', {bookid: entityId});
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<WorldListResponse>('db:book:worlds:get', {bookid: entityId});
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<WorldListResponse>(
|
||||
'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<void> {
|
||||
if (isSeriesMode) return;
|
||||
try {
|
||||
const requestData = {
|
||||
bookId: book?.bookId,
|
||||
toolName: 'worlds',
|
||||
enabled: enabled
|
||||
};
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:tool:update', requestData);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('book/tool-setting', requestData, userToken, lang);
|
||||
if (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: 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<void> {
|
||||
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 window.electron.invoke<string>('db:series:world:add', addData);
|
||||
} else {
|
||||
newWorldId = await System.authPostToServer<string>(
|
||||
'series/world/add',
|
||||
addData,
|
||||
userToken,
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:world:add', {...addData, id: newWorldId});
|
||||
}
|
||||
}
|
||||
if (!newWorldId) {
|
||||
errorMessage(t("worldSetting.addWorldError"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const requestData = {
|
||||
worldName: newWorldName,
|
||||
bookId: entityId,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
newWorldId = await window.electron.invoke<string>('db:book:world:add', requestData);
|
||||
} else {
|
||||
newWorldId = await System.authPostToServer<string>('book/world/add', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:book:world:add', {...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<boolean> {
|
||||
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 window.electron.invoke<boolean>('db:series:world:update', updateData);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('series/world/update', updateData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:world:update', 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 window.electron.invoke<boolean>('db:book:world:update', requestData);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('book/world/update', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:book:world:update', 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<void> {
|
||||
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 → IPC
|
||||
seriesWorldId = await window.electron.invoke<string>('db:series:world:add', seriesWorldData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
seriesWorldId = await System.authPostToServer<string>('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('db:series:world:add', {...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 → IPC
|
||||
updateResponse = await window.electron.invoke<boolean>('db:book:world:update', updateData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
updateResponse = await System.authPostToServer<boolean>('book/world/update', updateData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:book:world:update', 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<void> {
|
||||
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 → IPC
|
||||
worldId = await window.electron.invoke<string>('db:book:world:add', requestData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
worldId = await System.authPostToServer<string>('book/world/add', requestData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:book:world:add', {...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<void> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user