- Deleted redundant components (`AddActionButton`, `AlertBox`, `AlertStack`, `BackButton`, `CancelButton`, and `CollapsableArea`) and related files. - Removed unused models (`Book`, `BookSerie`, `BookTables`, `Character`, and `Chapter`) to reduce codebase clutter. - Updated project structure and references to reflect these removals.
802 lines
33 KiB
TypeScript
802 lines
33 KiB
TypeScript
'use client'
|
|
import {useCallback, useContext, useEffect, useState} from 'react';
|
|
import {
|
|
Attribute,
|
|
CharacterAttributeSection,
|
|
CharacterListResponse,
|
|
CharacterProps,
|
|
isCharacterCategory,
|
|
isCharacterStatus
|
|
} from '@/lib/types/character';
|
|
import {SeriesCharacterProps} from '@/lib/types/series';
|
|
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
|
import {BookContext, BookContextProps} from '@/context/BookContext';
|
|
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
|
import {LangContext, LangContextProps} from '@/context/LangContext';
|
|
import {apiDelete, apiGet, apiPatch, apiPost} from '@/lib/api/client';
|
|
import {useTranslations} from '@/lib/i18n';
|
|
import {ViewMode} from '@/lib/types/settings';
|
|
import {isDesktop} from '@/lib/configs';
|
|
import * as tauri from '@/lib/tauri';
|
|
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
|
|
|
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: CharacterAttributeSection, value: Attribute) => Promise<void>;
|
|
removeAttribute: (section: CharacterAttributeSection, 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}: LangContextProps = useContext<LangContextProps>(LangContext);
|
|
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
|
const {book, setBook}: BookContextProps = useContext<BookContextProps>(BookContext);
|
|
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
|
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
|
|
|
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 {
|
|
const response: SeriesCharacterProps[] = await apiGet<SeriesCharacterProps[]>('series/character/list', userToken, lang, {
|
|
seriesid: bookSeriesId
|
|
});
|
|
if (response) {
|
|
setSeriesCharacters(response);
|
|
}
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
}
|
|
}
|
|
}, [bookSeriesId, userToken, lang]);
|
|
|
|
const refreshCharacters = useCallback(async function (): Promise<void> {
|
|
setIsLoading(true);
|
|
try {
|
|
if (isSeriesMode) {
|
|
const response: SeriesCharacterProps[] = await apiGet<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: char.status && isCharacterStatus(char.status) ? char.status : 'alive',
|
|
category: isCharacterCategory(char.category) ? char.category : 'none',
|
|
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 {
|
|
let response: CharacterListResponse;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.getCharacterList(entityId, book?.tools?.characters ?? false) as CharacterListResponse;
|
|
} else {
|
|
response = await apiGet<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]);
|
|
|
|
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 {
|
|
let response: boolean;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.updateBookToolSetting(book?.bookId ?? '', 'characters', enabled);
|
|
} else {
|
|
response = await apiPatch<boolean>('book/tool-setting', {
|
|
bookId: book?.bookId,
|
|
toolName: 'characters',
|
|
enabled: enabled
|
|
}, userToken, lang);
|
|
}
|
|
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]);
|
|
|
|
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) {
|
|
characterId = await apiPost<string>(
|
|
'series/character/add',
|
|
{
|
|
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,
|
|
}
|
|
},
|
|
userToken,
|
|
lang
|
|
);
|
|
} else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
characterId = await tauri.createCharacter(character, entityId);
|
|
} else {
|
|
characterId = await apiPost<string>('character/add', {
|
|
bookId: entityId,
|
|
character: character,
|
|
}, userToken, lang);
|
|
}
|
|
|
|
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) {
|
|
response = await apiPatch<boolean>('series/character/update', {
|
|
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,
|
|
}
|
|
}, userToken, lang);
|
|
} else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.updateCharacter(character);
|
|
} else {
|
|
response = await apiPost<boolean>('character/update', {
|
|
character: character,
|
|
}, userToken, lang);
|
|
}
|
|
|
|
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;
|
|
if (isSeriesMode) {
|
|
response = await apiDelete<boolean>('series/character/delete', {
|
|
characterId: characterId,
|
|
}, userToken, lang);
|
|
} else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.deleteCharacter(characterId, book?.bookId ?? '', Date.now());
|
|
} else {
|
|
response = await apiDelete<boolean>('character/delete', {
|
|
characterId: characterId,
|
|
}, userToken, lang);
|
|
}
|
|
|
|
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]);
|
|
|
|
const addAttribute = useCallback(async function (section: CharacterAttributeSection, value: Attribute): Promise<void> {
|
|
if (!selectedCharacter) return;
|
|
|
|
if (selectedCharacter.id === null) {
|
|
const updatedSection: Attribute[] = [
|
|
...selectedCharacter[section],
|
|
value,
|
|
];
|
|
setSelectedCharacter({...selectedCharacter, [section]: updatedSection});
|
|
} else {
|
|
try {
|
|
let attributeId: string;
|
|
if (isSeriesMode) {
|
|
attributeId = await apiPost<string>('series/character/attribute/add', {
|
|
characterId: selectedCharacter.id,
|
|
type: section,
|
|
name: value.name,
|
|
}, userToken, lang);
|
|
} else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
attributeId = await tauri.addCharacterAttribute(selectedCharacter.id, section, value.name);
|
|
} else {
|
|
attributeId = await apiPost<string>('character/attribute/add', {
|
|
characterId: selectedCharacter.id,
|
|
type: section,
|
|
name: value.name,
|
|
}, userToken, lang);
|
|
}
|
|
|
|
if (!attributeId) {
|
|
errorMessage(t("characterComponent.errorAddAttribute"));
|
|
return;
|
|
}
|
|
|
|
const newValue: Attribute = {
|
|
name: value.name,
|
|
id: attributeId,
|
|
};
|
|
const updatedSection: Attribute[] = [...selectedCharacter[section], 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]);
|
|
|
|
const removeAttribute = useCallback(async function (section: CharacterAttributeSection, index: number, attrId: string): Promise<void> {
|
|
if (!selectedCharacter) return;
|
|
|
|
if (selectedCharacter.id === null) {
|
|
const updatedSection: Attribute[] = selectedCharacter[section].filter(function (_: Attribute, i: number): boolean {
|
|
return i !== index;
|
|
});
|
|
setSelectedCharacter({...selectedCharacter, [section]: updatedSection});
|
|
} else {
|
|
try {
|
|
let response: boolean;
|
|
if (isSeriesMode) {
|
|
response = await apiDelete<boolean>('series/character/attribute/delete', {
|
|
attributeId: attrId,
|
|
}, userToken, lang);
|
|
} else if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.deleteCharacterAttribute(attrId, book?.bookId ?? '', Date.now());
|
|
} else {
|
|
response = await apiDelete<boolean>('character/attribute/delete', {
|
|
attributeId: attrId,
|
|
}, userToken, lang);
|
|
}
|
|
|
|
if (!response) {
|
|
errorMessage(t("characterComponent.errorRemoveAttribute"));
|
|
return;
|
|
}
|
|
|
|
const updatedSection: Attribute[] = selectedCharacter[section].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]);
|
|
|
|
const exportToSeries = useCallback(async function (): Promise<void> {
|
|
if (!selectedCharacter || !bookSeriesId) return;
|
|
|
|
try {
|
|
const seriesCharacterId: string = await apiPost<string>(
|
|
'series/character/add',
|
|
{
|
|
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,
|
|
}
|
|
},
|
|
userToken,
|
|
lang
|
|
);
|
|
|
|
if (seriesCharacterId) {
|
|
const updateResponse: boolean = await apiPost<boolean>('character/update', {
|
|
character: {
|
|
...selectedCharacter,
|
|
seriesCharacterId: seriesCharacterId
|
|
},
|
|
}, userToken, lang);
|
|
|
|
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]);
|
|
|
|
const importFromSeries = useCallback(async function (seriesCharacterId: string): Promise<void> {
|
|
const seriesChar: SeriesCharacterProps | undefined = 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 && isCharacterStatus(seriesChar.status) ? seriesChar.status : 'alive',
|
|
category: isCharacterCategory(seriesChar.category) ? seriesChar.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,
|
|
};
|
|
|
|
let characterId: string;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
characterId = await tauri.createCharacter(characterToImport, entityId);
|
|
} else {
|
|
characterId = await apiPost<string>('character/add', {
|
|
bookId: entityId,
|
|
character: characterToImport,
|
|
}, userToken, lang);
|
|
}
|
|
|
|
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]);
|
|
|
|
// 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) 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,
|
|
};
|
|
}
|