Files
ERitors-Scribe-Desktop/components/book/settings/characters/CharacterComponent.tsx
natreex e45a15225b Add enable/disable management for book tools (characters, worlds, and locations)
- Introduced toggling functionality for managing `characters`, `worlds`, and `locations` tool availability per book.
- Updated `CharacterComponent`, `WorldSetting`, and `LocationComponent` with toggle switches for tool enablement.
- Added `book_tools` database table and related schema migration for storing tool settings.
- Extended API calls, models, and IPC handlers to support tool enablement states.
- Localized new strings for English with supporting descriptions and messages.
- Adjusted conditional rendering logic across components to respect tool enablement.
2026-01-14 17:42:59 -05:00

394 lines
16 KiB
TypeScript

'use client';
import {Dispatch, forwardRef, SetStateAction, useContext, useEffect, useImperativeHandle, useState} from 'react';
import {Attribute, CharacterProps, CharacterListResponse} from "@/lib/models/Character";
import {SessionContext} from "@/context/SessionContext";
import CharacterList from './CharacterList';
import System from '@/lib/models/System';
import {AlertContext} from "@/context/AlertContext";
import {BookContext} from "@/context/BookContext";
import CharacterDetail from "@/components/book/settings/characters/CharacterDetail";
import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook";
import ToggleSwitch from "@/components/form/ToggleSwitch";
interface CharacterDetailProps {
selectedCharacter: CharacterProps | null;
setSelectedCharacter: Dispatch<SetStateAction<CharacterProps | null>>;
handleCharacterChange: (key: keyof CharacterProps, value: string) => void;
handleAddElement: (section: keyof CharacterProps, element: any) => void;
handleRemoveElement: (
section: keyof CharacterProps,
index: number,
attrId: string,
) => void;
handleSaveCharacter: () => void;
}
const initialCharacterState: CharacterProps = {
id: null,
name: '',
lastName: '',
category: 'none',
title: '',
role: '',
image: 'https://via.placeholder.com/150',
biography: '',
history: '',
physical: [],
psychological: [],
relations: [],
skills: [],
weaknesses: [],
strengths: [],
goals: [],
motivations: [],
};
export function CharacterComponent({showToggle = true}: {showToggle?: boolean}, ref: any) {
const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext)
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {session} = useContext(SessionContext);
const {book, setBook} = useContext(BookContext);
const {errorMessage, successMessage} = useContext(AlertContext);
const [characters, setCharacters] = useState<CharacterProps[]>([]);
const [selectedCharacter, setSelectedCharacter] = useState<CharacterProps | null>(null);
const [toolEnabled, setToolEnabled] = useState<boolean>(book?.tools?.characters ?? false);
useImperativeHandle(ref, function () {
return {
handleSave: handleSaveCharacter,
};
});
useEffect((): void => {
getCharacters().then();
}, []);
async function handleToggleTool(enabled: boolean): Promise<void> {
try {
let response: boolean;
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:book:tool:update', {
bookId: book?.bookId,
toolName: 'characters',
enabled: enabled
});
} else {
response = await System.authPatchToServer<boolean>('book/tool-setting', {
bookId: book?.bookId,
toolName: 'characters',
enabled: enabled
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:book:tool:update', {
bookId: book?.bookId,
toolName: 'characters',
enabled: enabled
});
}
}
if (response && setBook && book) {
setToolEnabled(enabled);
setBook({...book, tools: {...book.tools, characters: enabled}});
}
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
}
}
}
async function getCharacters(): Promise<void> {
try {
let response: CharacterListResponse;
if (isCurrentlyOffline()) {
response = await window.electron.invoke<CharacterListResponse>('db:character:list', {bookid: book?.bookId});
} else {
if (book?.localBook) {
response = await window.electron.invoke<CharacterListResponse>('db:character:list', {bookid: book?.bookId});
} else {
response = await System.authGetQueryToServer<CharacterListResponse>(`character/list`, session.accessToken, lang, {
bookid: book?.bookId,
});
}
}
if (response) {
setCharacters(response.characters);
setToolEnabled(response.enabled);
if (setBook && book) {
setBook({...book, tools: {...book.tools, characters: response.enabled}});
}
}
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("common.unknownError"));
}
}
}
function handleCharacterClick(character: CharacterProps): void {
setSelectedCharacter({...character});
}
function handleAddCharacter(): void {
setSelectedCharacter({...initialCharacterState});
}
async function handleSaveCharacter(): Promise<void> {
if (selectedCharacter) {
const updatedCharacter: CharacterProps = {...selectedCharacter};
if (selectedCharacter.id === null) {
await addNewCharacter(updatedCharacter);
} else {
await updateCharacter(updatedCharacter);
}
}
}
async function addNewCharacter(updatedCharacter: CharacterProps): Promise<void> {
if (!updatedCharacter.name) {
errorMessage(t("characterComponent.errorNameRequired"));
return;
}
if (updatedCharacter.category === 'none') {
errorMessage(t("characterComponent.errorCategoryRequired"));
return;
}
try {
let characterId: string;
if (isCurrentlyOffline() || book?.localBook) {
characterId = await window.electron.invoke<string>('db:character:create', {
bookId: book?.bookId,
character: updatedCharacter,
});
} else {
characterId = await System.authPostToServer<string>(`character/add`, {
bookId: book?.bookId,
character: updatedCharacter,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:character:create', {
bookId: book?.bookId,
characterId,
character: updatedCharacter,
});
}
}
if (!characterId) {
errorMessage(t("characterComponent.errorAddCharacter"));
return;
}
updatedCharacter.id = characterId;
setCharacters([...characters, updatedCharacter]);
setSelectedCharacter(null);
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("common.unknownError"));
}
}
}
async function updateCharacter(updatedCharacter: CharacterProps,): Promise<void> {
try {
let response: boolean;
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:character:update', {
character: updatedCharacter,
});
} else {
response = await System.authPostToServer<boolean>(`character/update`, {
character: updatedCharacter,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:character:update', {
character: updatedCharacter,
});
}
}
if (!response) {
errorMessage(t("characterComponent.errorUpdateCharacter"));
return;
}
setCharacters(
characters.map((char: CharacterProps): CharacterProps =>
char.id === updatedCharacter.id ? updatedCharacter : char,
),
);
setSelectedCharacter(null);
successMessage(t("characterComponent.successUpdate"));
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("common.unknownError"));
}
}
}
function handleCharacterChange(
key: keyof CharacterProps,
value: string,
): void {
if (selectedCharacter) {
setSelectedCharacter({...selectedCharacter, [key]: value});
}
}
async function handleAddElement(
section: keyof CharacterProps,
value: Attribute,
): Promise<void> {
if (selectedCharacter) {
if (selectedCharacter.id === null) {
const updatedSection: any[] = [
...(selectedCharacter[section] as any[]),
value,
];
setSelectedCharacter({...selectedCharacter, [section]: updatedSection});
} else {
try {
let attributeId: string;
if (isCurrentlyOffline() || book?.localBook) {
attributeId = await window.electron.invoke<string>('db:character:attribute:add', {
characterId: selectedCharacter.id,
type: section,
name: value.name,
});
} else {
attributeId = await System.authPostToServer<string>(`character/attribute/add`, {
characterId: selectedCharacter.id,
type: section,
name: value.name,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:character:attribute:add', {
characterId: selectedCharacter.id,
attributeId,
type: section,
name: value.name,
});
}
}
if (!attributeId) {
errorMessage(t("characterComponent.errorAddAttribute"));
return;
}
const newValue: Attribute = {
name: value.name,
id: attributeId,
};
const updatedSection: Attribute[] = [...(selectedCharacter[section] as Attribute[]), newValue,];
setSelectedCharacter({...selectedCharacter, [section]: updatedSection});
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("common.unknownError"));
}
}
}
}
}
async function handleRemoveElement(
section: keyof CharacterProps,
index: number,
attrId: string,
): Promise<void> {
if (selectedCharacter) {
if (selectedCharacter.id === null) {
const updatedSection: Attribute[] = (
selectedCharacter[section] as Attribute[]
).filter((_, i: number): boolean => i !== index);
setSelectedCharacter({...selectedCharacter, [section]: updatedSection});
} else {
try {
let response: boolean;
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:character:attribute:delete', {
attributeId: attrId,
});
} else {
response = await System.authDeleteToServer<boolean>(`character/attribute/delete`, {
attributeId: attrId,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:character:attribute:delete', {
attributeId: attrId,
});
}
}
if (!response) {
errorMessage(t("characterComponent.errorRemoveAttribute"));
return;
}
const updatedSection: Attribute[] = (
selectedCharacter[section] as Attribute[]
).filter((_, i: number): boolean => i !== index);
setSelectedCharacter({
...selectedCharacter,
[section]: updatedSection,
});
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("common.unknownError"));
}
}
}
}
}
return (
<div className="space-y-5">
{showToggle && (
<div className="bg-secondary/20 rounded-xl p-4 shadow-inner border border-secondary/30">
<ToggleSwitch
enabled={toolEnabled}
setEnabled={handleToggleTool}
label={t('characterComponent.enableTool')}
description={t('characterComponent.enableToolDescription')}
/>
</div>
)}
{toolEnabled && (
<>
{selectedCharacter ? (
<CharacterDetail
selectedCharacter={selectedCharacter}
setSelectedCharacter={setSelectedCharacter}
handleAddElement={handleAddElement}
handleRemoveElement={handleRemoveElement}
handleCharacterChange={handleCharacterChange}
handleSaveCharacter={handleSaveCharacter}
/>
) : (
<CharacterList
characters={characters}
handleAddCharacter={handleAddCharacter}
handleCharacterClick={handleCharacterClick}
/>
)}
</>
)}
</div>
);
}
export default forwardRef(CharacterComponent);