- Updated delete methods across hooks and components to include `deletedAt: System.timeStampInSeconds()`. - Refactored synchronized delete logic to pass `deletedAt` for both offline and online states. - Improved synchronization workflows to include `deletedAt` in server and IPC requests. - Enhanced destructuring patterns for cleaner and more consistent request data.
995 lines
44 KiB
TypeScript
995 lines
44 KiB
TypeScript
'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) {
|
|
let seriesSpellResponse: SeriesSpellDetailResponse;
|
|
if (isCurrentlyOffline() || book?.localBook) {
|
|
seriesSpellResponse = await window.electron.invoke<SeriesSpellDetailResponse>(
|
|
'db:series:spell:detail',
|
|
{spellId: response.seriesSpellId}
|
|
);
|
|
} else {
|
|
seriesSpellResponse = 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 deletedAt: number = System.timeStampInSeconds();
|
|
if (isSeriesMode) {
|
|
// Series mode - dual logic
|
|
const requestData = {spellId, deletedAt};
|
|
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 {
|
|
const requestData = {spellId, bookId: entityId, deletedAt};
|
|
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;
|
|
const deletedAt: number = System.timeStampInSeconds();
|
|
if (isSeriesMode) {
|
|
// Series mode - dual logic
|
|
const deleteData = {tagId, deletedAt};
|
|
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, deletedAt};
|
|
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) {
|
|
let seriesSpellResponse: SeriesSpellDetailResponse;
|
|
if (isCurrentlyOffline() || (isSeriesMode ? localSeries : book?.localBook)) {
|
|
seriesSpellResponse = await window.electron.invoke<SeriesSpellDetailResponse>(
|
|
'db:series:spell:detail',
|
|
{spellId: selectedSpell.seriesSpellId}
|
|
);
|
|
} else {
|
|
seriesSpellResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
|
|
'series/spell/detail',
|
|
userToken,
|
|
lang,
|
|
{spellid: selectedSpell.seriesSpellId}
|
|
);
|
|
}
|
|
if (seriesSpellResponse) {
|
|
setSelectedSeriesSpell(seriesSpellResponse);
|
|
}
|
|
}
|
|
}, [selectedSpell?.seriesSpellId, userToken, lang, isCurrentlyOffline, isSeriesMode, localSeries, book?.localBook]);
|
|
|
|
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,
|
|
};
|
|
}
|