- 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.
395 lines
19 KiB
TypeScript
395 lines
19 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 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 {faSliders} 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 CharacterEditorEditProps {
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* CharacterEditorEdit - Version sidebar édition
|
|
* Mêmes fonctionnalités que CharacterSettingsEdit, layout linéaire
|
|
*/
|
|
export default function CharacterEditorEdit({
|
|
character,
|
|
setCharacter,
|
|
onCharacterChange,
|
|
onAddAttribute,
|
|
onRemoveAttribute,
|
|
seriesCharacter,
|
|
onSyncComplete,
|
|
}: CharacterEditorEditProps): 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">
|
|
{/* Informations de base */}
|
|
<div className="border-b border-secondary/30 pb-3">
|
|
<h4 className="text-text-primary font-medium text-sm mb-3">{t('characterDetail.basicInfo')}</h4>
|
|
<div className="space-y-3">
|
|
<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.role')}
|
|
input={
|
|
<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}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<InputField
|
|
fieldName={t('characterDetail.gender')}
|
|
input={
|
|
<TextInput
|
|
value={character.gender || ''}
|
|
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
|
|
onCharacterChange('gender', e.target.value);
|
|
}}
|
|
placeholder={t('characterDetail.genderPlaceholder')}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<InputField
|
|
fieldName={t('characterDetail.age')}
|
|
input={
|
|
<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')}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Histoire */}
|
|
<div className="border-b border-secondary/30 pb-3">
|
|
<h4 className="text-text-primary font-medium text-sm mb-3">{t('characterDetail.historySection')}</h4>
|
|
<div className="space-y-3">
|
|
<InputField
|
|
fieldName={t('characterDetail.biography')}
|
|
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.roleFull')}
|
|
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>
|
|
</div>
|
|
|
|
{/* 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-3 bg-secondary/30 rounded-lg border border-secondary/50">
|
|
<div className="flex items-center gap-2">
|
|
<FontAwesomeIcon icon={faSliders} className="text-primary w-4 h-4"/>
|
|
<span className="text-text-primary font-medium text-sm">{t('characterDetail.advancedMode')}</span>
|
|
</div>
|
|
<button
|
|
onClick={function (): void { setShowAdvanced(!showAdvanced); }}
|
|
className={`px-3 py-1.5 rounded-lg text-sm 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 && (
|
|
<>
|
|
<div className="border-b border-secondary/30 pb-3">
|
|
<h4 className="text-text-primary font-medium text-sm mb-3">{t('characterDetail.identitySection')}</h4>
|
|
<div className="space-y-3">
|
|
<InputField
|
|
fieldName={t('characterDetail.species')}
|
|
input={
|
|
<TextInput
|
|
value={character.species || ''}
|
|
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
|
|
onCharacterChange('species', e.target.value);
|
|
}}
|
|
placeholder={t('characterDetail.speciesPlaceholder')}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<InputField
|
|
fieldName={t('characterDetail.status')}
|
|
input={
|
|
<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}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-b border-secondary/30 pb-3">
|
|
<h4 className="text-text-primary font-medium text-sm mb-3">{t('characterDetail.authorSection')}</h4>
|
|
<InputField
|
|
fieldName={t('characterDetail.notes')}
|
|
input={
|
|
<TexteAreaInput
|
|
value={character.notes || ''}
|
|
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
|
|
onCharacterChange('notes', e.target.value);
|
|
}}
|
|
placeholder={t('characterDetail.notesPlaceholder')}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{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>
|
|
);
|
|
}
|