Files
ERitors-Scribe-Desktop/components/book/settings/characters/settings/CharacterSettingsEdit.tsx
natreex ee4438834c Migrate from window.electron to tauri IPC functions across components
- Replaced `window.electron.invoke` calls with equivalent `tauri` function calls for all IPC interactions.
- Removed `electron.d.ts` TypeScript definitions as they are no longer needed.
- Updated related logic for offline/online state synchronization.
- Added `types.rs` and `shared/mod.rs` modules to support Tauri IPC integration with Rust enums and shared logic.
- Refactored IPC request queues to use updated handler names for consistency with Tauri.
2026-03-21 09:34:13 -04:00

684 lines
38 KiB
TypeScript

'use client';
import React, {useContext, useEffect, useMemo, useState} from 'react';
import {
advancedCharacterElements,
Attribute,
basicCharacterElements,
CharacterAttribute,
characterCategories,
CharacterElement,
CharacterProps,
characterStatus
} from '@/lib/models/Character';
import {SeriesCharacterProps} from '@/lib/models/Series';
import CollapsableArea from '@/components/CollapsableArea';
import InputField from '@/components/form/InputField';
import TextInput from '@/components/form/TextInput';
import TexteAreaInput from '@/components/form/TexteAreaInput';
import NumberInput from '@/components/form/NumberInput';
import SelectBox from '@/components/form/SelectBox';
import CharacterSectionElement from '@/components/book/settings/characters/CharacterSectionElement';
import SyncFieldWrapper from '@/components/form/SyncFieldWrapper';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {
faBook,
faCommentDots,
faGlobe,
faScroll,
faSliders,
faStickyNote,
faUser
} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from 'next-intl';
import {SessionContext} from '@/context/SessionContext';
import {AlertContext} from '@/context/AlertContext';
import {LangContext} from '@/context/LangContext';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BookContext} from '@/context/BookContext';
import System from '@/lib/models/System';
import {Dispatch, SetStateAction} from 'react';
import * as tauri from '@/lib/tauri';
type AttributeResponse = { type: string; values: Attribute[] }[];
interface CharacterSettingsEditProps {
character: CharacterProps;
setCharacter: Dispatch<SetStateAction<CharacterProps | null>>;
onCharacterChange: (key: keyof CharacterProps, value: string | number | null) => void;
onAddAttribute: (section: keyof CharacterProps, attr: Attribute) => Promise<void>;
onRemoveAttribute: (section: keyof CharacterProps, idx: number, id: string) => Promise<void>;
seriesCharacter?: SeriesCharacterProps | null;
onSyncComplete?: () => void;
}
/**
* CharacterSettingsEdit - Vue édition des détails d'un personnage
* Pour BookSetting/SerieSetting - Tous les champs éditables avec SyncFieldWrapper
* PAS de scroll interne (géré par parent)
*/
export default function CharacterSettingsEdit({
character,
setCharacter,
onCharacterChange,
onAddAttribute,
onRemoveAttribute,
seriesCharacter,
onSyncComplete,
}: CharacterSettingsEditProps): React.JSX.Element {
const t = useTranslations();
const {lang} = useContext(LangContext);
const {session} = useContext(SessionContext);
const {errorMessage} = useContext(AlertContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {book} = useContext(BookContext);
const [showAdvanced, setShowAdvanced] = useState<boolean>(false);
// Traduire les données des SelectBox
const translatedCharacterCategories = useMemo(() =>
characterCategories.map((item) => ({
...item,
label: t(item.label)
})), [t]);
const translatedCharacterStatus = useMemo(() =>
characterStatus.map((item) => ({
...item,
label: t(item.label)
})), [t]);
useEffect(function (): void {
if (character?.id !== null) {
getAttributes().then();
}
}, [character?.id]);
async function getAttributes(): Promise<void> {
try {
let response: AttributeResponse;
if (isCurrentlyOffline() || book?.localBook) {
response = await tauri.getCharacterAttributes(character?.id!) as AttributeResponse;
} else {
response = await System.authGetQueryToServer<AttributeResponse>(
'character/attribute',
session.accessToken,
lang,
{characterId: character?.id}
);
}
if (response) {
const attributes: CharacterAttribute = {};
response.forEach(function (item: { type: string; values: Attribute[] }): void {
attributes[item.type] = item.values;
});
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
if (!prev) return null;
return {
...prev,
physical: attributes.physical ?? [],
psychological: attributes.psychological ?? [],
relations: attributes.relations ?? [],
skills: attributes.skills ?? [],
weaknesses: attributes.weaknesses ?? [],
strengths: attributes.strengths ?? [],
goals: attributes.goals ?? [],
motivations: attributes.motivations ?? [],
arc: attributes.arc ?? [],
secrets: attributes.secrets ?? [],
fears: attributes.fears ?? [],
flaws: attributes.flaws ?? [],
beliefs: attributes.beliefs ?? [],
conflicts: attributes.conflicts ?? [],
quotes: attributes.quotes ?? [],
distinguishingMarks: attributes.distinguishingMarks ?? [],
items: attributes.items ?? [],
affiliations: attributes.affiliations ?? [],
};
});
}
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
}
}
}
return (
<div className="space-y-4 px-2 pb-4">
{/* Informations de base */}
<CollapsableArea title={t('characterDetail.basicInfo')} icon={faUser}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<InputField
fieldName={t('characterDetail.name')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.name || ''}
currentValue={character.name || ''}
bookElementId={character.id || ''}
field="name"
elementType="character"
onDownload={function (): void { onCharacterChange('name', seriesCharacter?.name || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={character.name || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('name', e.target.value);
}}
placeholder={t('characterDetail.namePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.lastName')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.lastName || ''}
currentValue={character.lastName || ''}
bookElementId={character.id || ''}
field="lastName"
elementType="character"
onDownload={function (): void { onCharacterChange('lastName', seriesCharacter?.lastName || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={character.lastName || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('lastName', e.target.value);
}}
placeholder={t('characterDetail.lastNamePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.nickname')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.nickname || ''}
currentValue={character.nickname || ''}
bookElementId={character.id || ''}
field="nickname"
elementType="character"
onDownload={function (): void { onCharacterChange('nickname', seriesCharacter?.nickname || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={character.nickname || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('nickname', e.target.value);
}}
placeholder={t('characterDetail.nicknamePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.role')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.category || 'none'}
currentValue={character.category || 'none'}
bookElementId={character.id || ''}
field="category"
elementType="character"
onDownload={function (): void {
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
return prev ? {...prev, category: (seriesCharacter?.category || 'none') as CharacterProps['category']} : prev;
});
}}
onSyncComplete={onSyncComplete}
>
<SelectBox
defaultValue={character.category || 'none'}
onChangeCallBack={function (e: React.ChangeEvent<HTMLSelectElement>): void {
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
return prev ? {...prev, category: e.target.value as CharacterProps['category']} : prev;
});
}}
data={translatedCharacterCategories}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.title')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.title || ''}
currentValue={character.title || ''}
bookElementId={character.id || ''}
field="title"
elementType="character"
onDownload={function (): void { onCharacterChange('title', seriesCharacter?.title || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={character.title || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('title', e.target.value);
}}
placeholder={t('characterDetail.titlePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.gender')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.gender || ''}
currentValue={character.gender || ''}
bookElementId={character.id || ''}
field="gender"
elementType="character"
onDownload={function (): void { onCharacterChange('gender', seriesCharacter?.gender || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={character.gender || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('gender', e.target.value);
}}
placeholder={t('characterDetail.genderPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.age')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.age !== null && seriesCharacter?.age !== undefined ? String(seriesCharacter.age) : ''}
currentValue={character.age !== null && character.age !== undefined ? String(character.age) : ''}
bookElementId={character.id || ''}
field="age"
elementType="character"
onDownload={function (): void {
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
return prev ? {...prev, age: seriesCharacter?.age ?? null} : prev;
});
}}
onSyncComplete={onSyncComplete}
>
<NumberInput
value={character.age ?? null}
onValueChange={function (val: number | null): void {
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
return prev ? {...prev, age: val} : prev;
});
}}
placeholder={t('characterDetail.agePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
</div>
</CollapsableArea>
{/* Histoire */}
<CollapsableArea title={t('characterDetail.historySection')} icon={faBook}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<InputField
fieldName={t('characterDetail.biography')}
icon={faBook}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.biography || ''}
currentValue={character.biography || ''}
bookElementId={character.id || ''}
field="biography"
elementType="character"
onDownload={function (): void { onCharacterChange('biography', seriesCharacter?.biography || ''); }}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
value={character.biography || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('biography', e.target.value);
}}
placeholder={t('characterDetail.biographyPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.history')}
icon={faScroll}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.history || ''}
currentValue={character.history || ''}
bookElementId={character.id || ''}
field="history"
elementType="character"
onDownload={function (): void { onCharacterChange('history', seriesCharacter?.history || ''); }}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
value={character.history || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('history', e.target.value);
}}
placeholder={t('characterDetail.historyPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.roleFull')}
icon={faScroll}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.role || ''}
currentValue={character.role || ''}
bookElementId={character.id || ''}
field="role"
elementType="character"
onDownload={function (): void { onCharacterChange('role', seriesCharacter?.role || ''); }}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
value={character.role || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('role', e.target.value);
}}
placeholder={t('characterDetail.roleFullPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
</div>
</CollapsableArea>
{/* Attributs de base */}
{basicCharacterElements.map(function (item: CharacterElement, index: number): React.JSX.Element {
return (
<CharacterSectionElement
key={`basic-${index}`}
title={item.title}
section={item.section}
placeholder={item.placeholder}
icon={item.icon}
selectedCharacter={character}
setSelectedCharacter={setCharacter}
handleAddElement={onAddAttribute}
handleRemoveElement={onRemoveAttribute}
/>
);
})}
{/* Toggle Mode Avancé */}
<div className="flex items-center justify-between p-4 bg-secondary/30 rounded-xl border border-secondary/50">
<div className="flex items-center gap-3">
<FontAwesomeIcon icon={faSliders} className="text-primary w-5 h-5"/>
<span className="text-text-primary font-medium">{t('characterDetail.advancedMode')}</span>
</div>
<button
onClick={function (): void { setShowAdvanced(!showAdvanced); }}
className={`px-4 py-2 rounded-lg transition-all duration-200 ${
showAdvanced
? 'bg-primary text-white'
: 'bg-secondary/50 text-text-primary hover:bg-secondary'
}`}
>
{showAdvanced ? t('characterDetail.hideAdvanced') : t('characterDetail.showAdvanced')}
</button>
</div>
{/* Sections avancées */}
{showAdvanced && (
<>
{/* Identité étendue */}
<CollapsableArea title={t('characterDetail.identitySection')} icon={faGlobe}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<InputField
fieldName={t('characterDetail.species')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.species || ''}
currentValue={character.species || ''}
bookElementId={character.id || ''}
field="species"
elementType="character"
onDownload={function (): void { onCharacterChange('species', seriesCharacter?.species || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={character.species || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('species', e.target.value);
}}
placeholder={t('characterDetail.speciesPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.nationality')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.nationality || ''}
currentValue={character.nationality || ''}
bookElementId={character.id || ''}
field="nationality"
elementType="character"
onDownload={function (): void { onCharacterChange('nationality', seriesCharacter?.nationality || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={character.nationality || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('nationality', e.target.value);
}}
placeholder={t('characterDetail.nationalityPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.status')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.status || 'alive'}
currentValue={character.status || 'alive'}
bookElementId={character.id || ''}
field="status"
elementType="character"
onDownload={function (): void {
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
return prev ? {...prev, status: (seriesCharacter?.status || 'alive') as CharacterProps['status']} : prev;
});
}}
onSyncComplete={onSyncComplete}
>
<SelectBox
defaultValue={character.status || 'alive'}
onChangeCallBack={function (e: React.ChangeEvent<HTMLSelectElement>): void {
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
return prev ? {...prev, status: e.target.value as CharacterProps['status']} : prev;
});
}}
data={translatedCharacterStatus}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.residence')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.residence || ''}
currentValue={character.residence || ''}
bookElementId={character.id || ''}
field="residence"
elementType="character"
onDownload={function (): void { onCharacterChange('residence', seriesCharacter?.residence || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={character.residence || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('residence', e.target.value);
}}
placeholder={t('characterDetail.residencePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
</div>
</CollapsableArea>
{/* Voix du personnage */}
<CollapsableArea title={t('characterDetail.voiceSection')} icon={faCommentDots}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<InputField
fieldName={t('characterDetail.speechPattern')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.speechPattern || ''}
currentValue={character.speechPattern || ''}
bookElementId={character.id || ''}
field="speechPattern"
elementType="character"
onDownload={function (): void { onCharacterChange('speechPattern', seriesCharacter?.speechPattern || ''); }}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
value={character.speechPattern || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('speechPattern', e.target.value);
}}
placeholder={t('characterDetail.speechPatternPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.catchphrase')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.catchphrase || ''}
currentValue={character.catchphrase || ''}
bookElementId={character.id || ''}
field="catchphrase"
elementType="character"
onDownload={function (): void { onCharacterChange('catchphrase', seriesCharacter?.catchphrase || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={character.catchphrase || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('catchphrase', e.target.value);
}}
placeholder={t('characterDetail.catchphrasePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
</div>
</CollapsableArea>
{/* Notes de l'auteur */}
<CollapsableArea title={t('characterDetail.authorSection')} icon={faStickyNote}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<InputField
fieldName={t('characterDetail.notes')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.notes || ''}
currentValue={character.notes || ''}
bookElementId={character.id || ''}
field="notes"
elementType="character"
onDownload={function (): void { onCharacterChange('notes', seriesCharacter?.notes || ''); }}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
value={character.notes || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('notes', e.target.value);
}}
placeholder={t('characterDetail.notesPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.colorLabel')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.color || ''}
currentValue={character.color || ''}
bookElementId={character.id || ''}
field="color"
elementType="character"
onDownload={function (): void { onCharacterChange('color', seriesCharacter?.color || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={character.color || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('color', e.target.value);
}}
placeholder={t('characterDetail.colorPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
</div>
</CollapsableArea>
{/* Attributs avancés */}
{advancedCharacterElements.map(function (item: CharacterElement, index: number): React.JSX.Element {
return (
<CharacterSectionElement
key={`advanced-${index}`}
title={item.title}
section={item.section}
placeholder={item.placeholder}
icon={item.icon}
selectedCharacter={character}
setSelectedCharacter={setCharacter}
handleAddElement={onAddAttribute}
handleRemoveElement={onRemoveAttribute}
/>
);
})}
</>
)}
</div>
);
}