Add character deletion functionality with confirmation workflow

- Added `handleDeleteCharacter` method to handle character deletion with confirmation prompts.
- Updated `CharacterComponent` and `CharacterDetail` to include delete button and related logic.
- Localized new strings for character deletion (e.g., confirmation prompts, success/error messages).
- Enhanced database repository methods (`deleteCharacter`) to handle character deletion securely.
- Improved synchronization workflows to accommodate character deletion.
This commit is contained in:
natreex
2026-01-22 15:09:04 -05:00
parent 9461eb6120
commit 4e462670a9
16 changed files with 383 additions and 59 deletions

View File

@@ -28,6 +28,7 @@ interface CharacterDetailProps {
attrId: string, attrId: string,
) => void; ) => void;
handleSaveCharacter: () => void; handleSaveCharacter: () => void;
handleDeleteCharacter: (characterId: string) => Promise<void>;
} }
const initialCharacterState: CharacterProps = { const initialCharacterState: CharacterProps = {
@@ -163,7 +164,41 @@ export function CharacterComponent({showToggle = true}: {showToggle?: boolean},
} }
} }
} }
async function handleDeleteCharacter(characterId: string): Promise<void> {
try {
let response: boolean;
if (isCurrentlyOffline() || book?.localBook) {
response = await window.electron.invoke<boolean>('db:character:delete', {
characterId: characterId,
});
} else {
response = await System.authDeleteToServer<boolean>('character/delete', {
characterId: characterId,
}, session.accessToken, lang);
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
addToQueue('db:character:delete', {
characterId: characterId,
});
}
}
if (!response) {
errorMessage(t("characterComponent.errorDeleteCharacter"));
return;
}
setCharacters(characters.filter((c: CharacterProps): boolean => c.id !== characterId));
setSelectedCharacter(null);
successMessage(t("characterComponent.successDelete"));
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t("common.unknownError"));
}
}
}
async function addNewCharacter(updatedCharacter: CharacterProps): Promise<void> { async function addNewCharacter(updatedCharacter: CharacterProps): Promise<void> {
if (!updatedCharacter.name) { if (!updatedCharacter.name) {
errorMessage(t("characterComponent.errorNameRequired")); errorMessage(t("characterComponent.errorNameRequired"));
@@ -394,6 +429,7 @@ export function CharacterComponent({showToggle = true}: {showToggle?: boolean},
handleRemoveElement={handleRemoveElement} handleRemoveElement={handleRemoveElement}
handleCharacterChange={handleCharacterChange} handleCharacterChange={handleCharacterChange}
handleSaveCharacter={handleSaveCharacter} handleSaveCharacter={handleSaveCharacter}
handleDeleteCharacter={handleDeleteCharacter}
/> />
) : ( ) : (
<CharacterList <CharacterList

View File

@@ -28,6 +28,7 @@ import {
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {Dispatch, SetStateAction, useContext, useEffect} from "react"; import {Dispatch, SetStateAction, useContext, useEffect} from "react";
import CharacterSectionElement from "@/components/book/settings/characters/CharacterSectionElement"; import CharacterSectionElement from "@/components/book/settings/characters/CharacterSectionElement";
import DeleteButton from "@/components/form/DeleteButton";
import {useTranslations} from "next-intl"; import {useTranslations} from "next-intl";
import {LangContext} from "@/context/LangContext"; import {LangContext} from "@/context/LangContext";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
@@ -46,6 +47,7 @@ interface CharacterDetailProps {
attrId: string, attrId: string,
) => void; ) => void;
handleSaveCharacter: () => void; handleSaveCharacter: () => void;
handleDeleteCharacter: (characterId: string) => Promise<void>;
} }
export default function CharacterDetail( export default function CharacterDetail(
@@ -56,6 +58,7 @@ export default function CharacterDetail(
handleRemoveElement, handleRemoveElement,
handleAddElement, handleAddElement,
handleSaveCharacter, handleSaveCharacter,
handleDeleteCharacter,
}: CharacterDetailProps }: CharacterDetailProps
) { ) {
const t = useTranslations(); const t = useTranslations();
@@ -64,7 +67,7 @@ export default function CharacterDetail(
const {book} = useContext(BookContext); const {book} = useContext(BookContext);
const {session} = useContext(SessionContext); const {session} = useContext(SessionContext);
const {errorMessage} = useContext(AlertContext); const {errorMessage} = useContext(AlertContext);
useEffect((): void => { useEffect((): void => {
if (selectedCharacter?.id !== null) { if (selectedCharacter?.id !== null) {
getAttributes().then(); getAttributes().then();
@@ -139,11 +142,22 @@ export default function CharacterDetail(
<span className="text-text-primary font-semibold text-lg"> <span className="text-text-primary font-semibold text-lg">
{selectedCharacter?.name || t("characterDetail.newCharacter")} {selectedCharacter?.name || t("characterDetail.newCharacter")}
</span> </span>
<button onClick={handleSaveCharacter} <div className="flex items-center gap-2">
className="flex items-center justify-center bg-primary w-10 h-10 rounded-xl border border-primary-dark shadow-md hover:shadow-lg hover:scale-110 transition-all duration-200"> {selectedCharacter?.id && (
<FontAwesomeIcon icon={selectedCharacter?.id ? faSave : faPlus} <DeleteButton
className="text-text-primary w-5 h-5"/> onDelete={(): Promise<void> => handleDeleteCharacter(selectedCharacter.id as string)}
</button> confirmTitle={t("characterDetail.deleteTitle")}
confirmMessage={t("characterDetail.deleteMessage", {name: selectedCharacter.name})}
confirmButtonText={t("common.delete")}
cancelButtonText={t("common.cancel")}
/>
)}
<button onClick={handleSaveCharacter}
className="flex items-center justify-center bg-primary w-10 h-10 rounded-xl border border-primary-dark shadow-md hover:shadow-lg hover:scale-110 transition-all duration-200">
<FontAwesomeIcon icon={selectedCharacter?.id ? faSave : faPlus}
className="text-text-primary w-5 h-5"/>
</button>
</div>
</div> </div>
<div className="overflow-y-auto max-h-[calc(100vh-350px)] space-y-4 px-2 pb-4"> <div className="overflow-y-auto max-h-[calc(100vh-350px)] space-y-4 px-2 pb-4">

View File

@@ -8,6 +8,7 @@ import TextInput from "@/components/form/TextInput";
import TexteAreaInput from "@/components/form/TexteAreaInput"; import TexteAreaInput from "@/components/form/TexteAreaInput";
import SelectBox from "@/components/form/SelectBox"; import SelectBox from "@/components/form/SelectBox";
import SpellTagChip from "@/components/book/settings/spells/SpellTagChip"; import SpellTagChip from "@/components/book/settings/spells/SpellTagChip";
import DeleteButton from "@/components/form/DeleteButton";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import { import {
faArrowLeft, faArrowLeft,
@@ -20,11 +21,9 @@ import {
faSave, faSave,
faStickyNote, faStickyNote,
faTags, faTags,
faTrash,
faTriangleExclamation faTriangleExclamation
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import {useTranslations} from "next-intl"; import {useTranslations} from "next-intl";
import AlertBox from "@/components/AlertBox";
interface SpellDetailProps { interface SpellDetailProps {
selectedSpell: SpellEditState; selectedSpell: SpellEditState;
@@ -52,7 +51,6 @@ export default function SpellDetail(
const [showTagDropdown, setShowTagDropdown] = useState<boolean>(false); const [showTagDropdown, setShowTagDropdown] = useState<boolean>(false);
const [isCreatingTag, setIsCreatingTag] = useState<boolean>(false); const [isCreatingTag, setIsCreatingTag] = useState<boolean>(false);
const [newTagColor, setNewTagColor] = useState<string>(defaultTagColors[0]); const [newTagColor, setNewTagColor] = useState<string>(defaultTagColors[0]);
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
function handleAddTag(tagId: string): void { function handleAddTag(tagId: string): void {
if (!selectedSpell.tags.includes(tagId)) { if (!selectedSpell.tags.includes(tagId)) {
@@ -118,12 +116,13 @@ export default function SpellDetail(
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{selectedSpell.id && ( {selectedSpell.id && (
<button <DeleteButton
onClick={() => setShowDeleteConfirm(true)} onDelete={(): Promise<void> => handleDeleteSpell(selectedSpell.id as string)}
className="flex items-center justify-center bg-error/90 hover:bg-error w-10 h-10 rounded-xl border border-error shadow-md hover:shadow-lg hover:scale-110 transition-all duration-200" confirmTitle={t("spellDetail.deleteTitle")}
> confirmMessage={t("spellDetail.deleteMessage", {name: selectedSpell.name})}
<FontAwesomeIcon icon={faTrash} className="text-text-primary w-5 h-5"/> confirmButtonText={t("common.delete")}
</button> cancelButtonText={t("common.cancel")}
/>
)} )}
<button <button
onClick={handleSaveSpell} onClick={handleSaveSpell}
@@ -133,20 +132,6 @@ export default function SpellDetail(
className="text-text-primary w-5 h-5"/> className="text-text-primary w-5 h-5"/>
</button> </button>
</div> </div>
{showDeleteConfirm && selectedSpell.id && (
<AlertBox
title={t("spellDetail.deleteTitle")}
message={t("spellDetail.deleteMessage", {name: selectedSpell.name})}
type="danger"
confirmText={t("common.delete")}
cancelText={t("common.cancel")}
onConfirm={async (): Promise<void> => {
await handleDeleteSpell(selectedSpell.id as string);
setShowDeleteConfirm(false);
}}
onCancel={(): void => setShowDeleteConfirm(false)}
/>
)}
</div> </div>
<div className="overflow-y-auto max-h-[calc(100vh-350px)] space-y-4 px-2 pb-4"> <div className="overflow-y-auto max-h-[calc(100vh-350px)] space-y-4 px-2 pb-4">

View File

@@ -22,6 +22,9 @@ import {
} from "../repositories/location.repository.js"; } from "../repositories/location.repository.js";
import { BookPlotPointsTable } from "../repositories/plotpoint.repository.js"; import { BookPlotPointsTable } from "../repositories/plotpoint.repository.js";
import { BookWorldElementsTable, BookWorldTable } from "../repositories/world.repository.js"; import { BookWorldElementsTable, BookWorldTable } from "../repositories/world.repository.js";
import { BookSpellsTable } from "../repositories/spell.repo.js";
import { BookSpellTagsTable } from "../repositories/spelltag.repo.js";
import { SyncedSpell, SyncedSpellTag } from "./Spell.js";
import { CompleteChapterContent, SyncedChapter } from "./Chapter.js"; import { CompleteChapterContent, SyncedChapter } from "./Chapter.js";
import { SyncedCharacter } from "./Character.js"; import { SyncedCharacter } from "./Character.js";
import { SyncedLocation } from "./Location.js"; import { SyncedLocation } from "./Location.js";
@@ -42,6 +45,7 @@ export interface BookToolsSettings {
characters: boolean; characters: boolean;
worlds: boolean; worlds: boolean;
locations: boolean; locations: boolean;
spells: boolean;
} }
export interface BookProps { export interface BookProps {
@@ -79,6 +83,8 @@ export interface CompleteBook {
locationElements: LocationElementTable[]; locationElements: LocationElementTable[];
locationSubElements: LocationSubElementTable[]; locationSubElements: LocationSubElementTable[];
bookTools: BookToolsTable[]; bookTools: BookToolsTable[];
spells: BookSpellsTable[];
spellTags: BookSpellTagsTable[];
} }
export interface SyncedBook { export interface SyncedBook {
@@ -98,6 +104,8 @@ export interface SyncedBook {
guideLine: SyncedGuideLine | null; guideLine: SyncedGuideLine | null;
aiGuideLine: SyncedAIGuideLine | null; aiGuideLine: SyncedAIGuideLine | null;
bookTools: SyncedBookTools | null; bookTools: SyncedBookTools | null;
spells: SyncedSpell[];
spellTags: SyncedSpellTag[];
} }
export interface BookSyncCompare { export interface BookSyncCompare {
@@ -119,6 +127,8 @@ export interface BookSyncCompare {
guideLine: boolean; guideLine: boolean;
aiGuideLine: boolean; aiGuideLine: boolean;
bookTools: boolean; bookTools: boolean;
spells: string[];
spellTags: string[];
} }
export interface CompleteBookData { export interface CompleteBookData {
@@ -272,7 +282,8 @@ export default class Book {
tools: { tools: {
characters: bookTools ? bookTools.characters_enabled === 1 : false, characters: bookTools ? bookTools.characters_enabled === 1 : false,
worlds: bookTools ? bookTools.worlds_enabled === 1 : false, worlds: bookTools ? bookTools.worlds_enabled === 1 : false,
locations: bookTools ? bookTools.locations_enabled === 1 : false locations: bookTools ? bookTools.locations_enabled === 1 : false,
spells: bookTools ? bookTools.spells_enabled === 1 : false
} }
}; };
} }
@@ -310,8 +321,8 @@ export default class Book {
return BookRepo.deleteBook(userId, bookId, lang); return BookRepo.deleteBook(userId, bookId, lang);
} }
public static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters' | 'worlds' | 'locations', enabled: boolean, lang: 'fr' | 'en' = 'fr'): boolean { public static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters' | 'worlds' | 'locations' | 'spells', enabled: boolean, lang: 'fr' | 'en' = 'fr'): boolean {
const columnName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' = `${toolName}_enabled` as 'characters_enabled' | 'worlds_enabled' | 'locations_enabled'; const columnName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' | 'spells_enabled' = `${toolName}_enabled` as 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' | 'spells_enabled';
return BookRepo.updateBookToolSetting(userId, bookId, columnName, enabled, System.timeStampInSeconds(), lang); return BookRepo.updateBookToolSetting(userId, bookId, columnName, enabled, System.timeStampInSeconds(), lang);
} }

View File

@@ -211,6 +211,17 @@ export default class Character {
return CharacterRepo.deleteAttribute(userId, attributeId, lang); return CharacterRepo.deleteAttribute(userId, attributeId, lang);
} }
/**
* Deletes a character and all its related data.
* @param userId - The unique identifier of the user
* @param characterId - The unique identifier of the character to delete
* @param lang - The language code for localization (defaults to 'fr')
* @returns True if the deletion was successful
*/
static deleteCharacter(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return CharacterRepo.deleteCharacter(userId, characterId, lang);
}
/** /**
* Retrieves all attributes for a specific character, grouped by type. * Retrieves all attributes for a specific character, grouped by type.
* Decrypts attribute data using the user's encryption key. * Decrypts attribute data using the user's encryption key.

View File

@@ -28,6 +28,8 @@ import GuidelineRepo, {
BookGuideLineTable BookGuideLineTable
} from "../repositories/guideline.repository.js"; } from "../repositories/guideline.repository.js";
import IssueRepository, {BookIssuesTable} from "../repositories/issue.repository.js"; import IssueRepository, {BookIssuesTable} from "../repositories/issue.repository.js";
import SpellRepo, {BookSpellsTable} from "../repositories/spell.repo.js";
import SpellTagRepo, {BookSpellTagsTable} from "../repositories/spelltag.repo.js";
export default class Download { export default class Download {
/** /**
@@ -198,8 +200,54 @@ export default class Download {
}); });
if (!issuesInserted) return false; if (!issuesInserted) return false;
return data.bookTools.every((bookTool: BookToolsTable): boolean => { const bookToolsInserted: boolean = data.bookTools.every((bookTool: BookToolsTable): boolean => {
return BookRepo.insertSyncBookTools(bookTool.book_id, userId, bookTool.characters_enabled, bookTool.worlds_enabled, bookTool.locations_enabled, bookTool.last_update, lang); return BookRepo.insertSyncBookTools(bookTool.book_id, userId, bookTool.characters_enabled, bookTool.worlds_enabled, bookTool.locations_enabled, bookTool.spells_enabled, bookTool.last_update, lang);
}); });
if (!bookToolsInserted) return false;
const spellTagsInserted: boolean = data.spellTags.every((spellTag: BookSpellTagsTable): boolean => {
const encryptedTagName: string = System.encryptDataWithUserKey(spellTag.name, userEncryptionKey);
return SpellTagRepo.insertSyncSpellTag(
spellTag.tag_id,
spellTag.book_id,
userId,
encryptedTagName,
spellTag.name_hash,
spellTag.color,
spellTag.last_update,
lang
);
});
if (!spellTagsInserted) return false;
const spellsInserted: boolean = data.spells.every((spell: BookSpellsTable): boolean => {
const encryptedName: string = System.encryptDataWithUserKey(spell.name, userEncryptionKey);
const encryptedDescription: string = System.encryptDataWithUserKey(spell.description, userEncryptionKey);
const encryptedAppearance: string = System.encryptDataWithUserKey(spell.appearance, userEncryptionKey);
const encryptedTags: string = System.encryptDataWithUserKey(spell.tags, userEncryptionKey);
const encryptedPowerLevel: string | null = spell.power_level ? System.encryptDataWithUserKey(spell.power_level, userEncryptionKey) : null;
const encryptedComponents: string | null = spell.components ? System.encryptDataWithUserKey(spell.components, userEncryptionKey) : null;
const encryptedLimitations: string | null = spell.limitations ? System.encryptDataWithUserKey(spell.limitations, userEncryptionKey) : null;
const encryptedNotes: string | null = spell.notes ? System.encryptDataWithUserKey(spell.notes, userEncryptionKey) : null;
return SpellRepo.insertSyncSpell(
spell.spell_id,
spell.book_id,
userId,
encryptedName,
spell.name_hash,
encryptedDescription,
encryptedAppearance,
encryptedTags,
encryptedPowerLevel,
encryptedComponents,
encryptedLimitations,
encryptedNotes,
spell.last_update,
lang
);
});
if (!spellsInserted) return false;
return true;
} }
} }

View File

@@ -1,6 +1,9 @@
import { getUserEncryptionKey } from "../keyManager.js"; import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js"; import System from "../System.js";
import { BookSyncCompare, CompleteBook, SyncedBook, SyncedBookTools } from "./Book.js"; import { BookSyncCompare, CompleteBook, SyncedBook, SyncedBookTools } from "./Book.js";
import { SyncedSpell, SyncedSpellTag } from "./Spell.js";
import SpellRepo, { BookSpellsTable, SyncedSpellResult } from "../repositories/spell.repo.js";
import SpellTagRepo, { BookSpellTagsTable, SyncedSpellTagResult } from "../repositories/spelltag.repo.js";
import BookRepo, { EritBooksTable, SyncedBookResult, BookToolsTable, SyncedBookToolsResult } from "../repositories/book.repository.js"; import BookRepo, { EritBooksTable, SyncedBookResult, BookToolsTable, SyncedBookToolsResult } from "../repositories/book.repository.js";
import ChapterRepo, { import ChapterRepo, {
BookChapterInfosTable, BookChapterInfosTable,
@@ -87,6 +90,8 @@ export default class Sync {
const decryptedGuideLines: BookGuideLineTable[] = []; const decryptedGuideLines: BookGuideLineTable[] = [];
const decryptedAIGuideLines: BookAIGuideLineTable[] = []; const decryptedAIGuideLines: BookAIGuideLineTable[] = [];
const decryptedIssues: BookIssuesTable[] = []; const decryptedIssues: BookIssuesTable[] = [];
const decryptedSpells: BookSpellsTable[] = [];
const decryptedSpellTags: BookSpellTagsTable[] = [];
const actSummaryIds: string[] = syncCompareData.actSummaries; const actSummaryIds: string[] = syncCompareData.actSummaries;
const chapterIds: string[] = syncCompareData.chapters; const chapterIds: string[] = syncCompareData.chapters;
@@ -102,6 +107,8 @@ export default class Sync {
const worldIds: string[] = syncCompareData.worlds; const worldIds: string[] = syncCompareData.worlds;
const worldElementIds: string[] = syncCompareData.worldElements; const worldElementIds: string[] = syncCompareData.worldElements;
const issueIds: string[] = syncCompareData.issues; const issueIds: string[] = syncCompareData.issues;
const spellIds: string[] = syncCompareData.spells;
const spellTagIds: string[] = syncCompareData.spellTags;
if (actSummaryIds && actSummaryIds.length > 0) { if (actSummaryIds && actSummaryIds.length > 0) {
for (const actSummaryId of actSummaryIds) { for (const actSummaryId of actSummaryIds) {
@@ -338,6 +345,37 @@ export default class Sync {
} }
} }
if (spellTagIds && spellTagIds.length > 0) {
for (const spellTagId of spellTagIds) {
const spellTagRecord: BookSpellTagsTable | null = SpellTagRepo.fetchSpellTagTableById(userId, spellTagId, lang);
if (spellTagRecord) {
decryptedSpellTags.push({
...spellTagRecord,
name: System.decryptDataWithUserKey(spellTagRecord.name, userEncryptionKey)
});
}
}
}
if (spellIds && spellIds.length > 0) {
for (const spellId of spellIds) {
const spellRecord: BookSpellsTable | null = SpellRepo.fetchSpellTableById(userId, spellId, lang);
if (spellRecord) {
decryptedSpells.push({
...spellRecord,
name: System.decryptDataWithUserKey(spellRecord.name, userEncryptionKey),
description: System.decryptDataWithUserKey(spellRecord.description, userEncryptionKey),
appearance: System.decryptDataWithUserKey(spellRecord.appearance, userEncryptionKey),
tags: System.decryptDataWithUserKey(spellRecord.tags, userEncryptionKey),
power_level: spellRecord.power_level ? System.decryptDataWithUserKey(spellRecord.power_level, userEncryptionKey) : null,
components: spellRecord.components ? System.decryptDataWithUserKey(spellRecord.components, userEncryptionKey) : null,
limitations: spellRecord.limitations ? System.decryptDataWithUserKey(spellRecord.limitations, userEncryptionKey) : null,
notes: spellRecord.notes ? System.decryptDataWithUserKey(spellRecord.notes, userEncryptionKey) : null
});
}
}
}
const bookResults: EritBooksTable[] = await BookRepo.fetchCompleteBookById(syncCompareData.id, lang); const bookResults: EritBooksTable[] = await BookRepo.fetchCompleteBookById(syncCompareData.id, lang);
if (bookResults.length > 0) { if (bookResults.length > 0) {
const bookRecord: EritBooksTable = bookResults[0]; const bookRecord: EritBooksTable = bookResults[0];
@@ -371,7 +409,9 @@ export default class Sync {
guideLine: decryptedGuideLines, guideLine: decryptedGuideLines,
aiGuideLine: decryptedAIGuideLines, aiGuideLine: decryptedAIGuideLines,
issues: decryptedIssues, issues: decryptedIssues,
bookTools: bookTools bookTools: bookTools,
spells: decryptedSpells,
spellTags: decryptedSpellTags
}; };
} }
@@ -730,13 +770,56 @@ export default class Sync {
if (completeBook.bookTools && completeBook.bookTools.length > 0) { if (completeBook.bookTools && completeBook.bookTools.length > 0) {
for (const serverBookTool of completeBook.bookTools) { for (const serverBookTool of completeBook.bookTools) {
const success: boolean = BookRepo.insertSyncBookTools(serverBookTool.book_id, userId, serverBookTool.characters_enabled, serverBookTool.worlds_enabled, serverBookTool.locations_enabled, serverBookTool.last_update, lang); const success: boolean = BookRepo.insertSyncBookTools(bookId, userId, serverBookTool.characters_enabled, serverBookTool.worlds_enabled, serverBookTool.locations_enabled, serverBookTool.spells_enabled, serverBookTool.last_update, lang);
if (!success) { if (!success) {
return false; return false;
} }
} }
} }
if (completeBook.spellTags && completeBook.spellTags.length > 0) {
for (const serverSpellTag of completeBook.spellTags) {
const spellTagExists: boolean = SpellTagRepo.isSpellTagExist(userId, serverSpellTag.tag_id, lang);
const encryptedName: string = System.encryptDataWithUserKey(serverSpellTag.name, userEncryptionKey);
if (spellTagExists) {
const updateSuccessful: boolean = SpellTagRepo.updateSyncSpellTag(userId, serverSpellTag.tag_id, encryptedName, serverSpellTag.name_hash, serverSpellTag.color, serverSpellTag.last_update, lang);
if (!updateSuccessful) {
return false;
}
} else {
const insertSuccessful: boolean = SpellTagRepo.insertSyncSpellTag(serverSpellTag.tag_id, serverSpellTag.book_id, userId, encryptedName, serverSpellTag.name_hash, serverSpellTag.color, serverSpellTag.last_update, lang);
if (!insertSuccessful) {
return false;
}
}
}
}
if (completeBook.spells && completeBook.spells.length > 0) {
for (const serverSpell of completeBook.spells) {
const spellExists: boolean = SpellRepo.isSpellExist(userId, serverSpell.spell_id, lang);
const encryptedName: string = System.encryptDataWithUserKey(serverSpell.name, userEncryptionKey);
const encryptedDescription: string = System.encryptDataWithUserKey(serverSpell.description, userEncryptionKey);
const encryptedAppearance: string = System.encryptDataWithUserKey(serverSpell.appearance, userEncryptionKey);
const encryptedTags: string = System.encryptDataWithUserKey(serverSpell.tags, userEncryptionKey);
const encryptedPowerLevel: string | null = serverSpell.power_level ? System.encryptDataWithUserKey(serverSpell.power_level, userEncryptionKey) : null;
const encryptedComponents: string | null = serverSpell.components ? System.encryptDataWithUserKey(serverSpell.components, userEncryptionKey) : null;
const encryptedLimitations: string | null = serverSpell.limitations ? System.encryptDataWithUserKey(serverSpell.limitations, userEncryptionKey) : null;
const encryptedNotes: string | null = serverSpell.notes ? System.encryptDataWithUserKey(serverSpell.notes, userEncryptionKey) : null;
if (spellExists) {
const updateSuccessful: boolean = SpellRepo.updateSyncSpell(userId, serverSpell.spell_id, encryptedName, serverSpell.name_hash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, serverSpell.last_update, lang);
if (!updateSuccessful) {
return false;
}
} else {
const insertSuccessful: boolean = SpellRepo.insertSyncSpell(serverSpell.spell_id, serverSpell.book_id, userId, encryptedName, serverSpell.name_hash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, serverSpell.last_update, lang);
if (!insertSuccessful) {
return false;
}
}
}
}
return true; return true;
} }
@@ -767,7 +850,9 @@ export default class Sync {
allIssues, allIssues,
allActSummaries, allActSummaries,
allGuidelines, allGuidelines,
allAIGuidelines allAIGuidelines,
allSpells,
allSpellTags
]: [ ]: [
SyncedBookResult[], SyncedBookResult[],
SyncedChapterResult[], SyncedChapterResult[],
@@ -785,7 +870,9 @@ export default class Sync {
SyncedIssueResult[], SyncedIssueResult[],
SyncedActSummaryResult[], SyncedActSummaryResult[],
SyncedGuideLineResult[], SyncedGuideLineResult[],
SyncedAIGuideLineResult[] SyncedAIGuideLineResult[],
SyncedSpellResult[],
SyncedSpellTagResult[]
] = await Promise.all([ ] = await Promise.all([
BookRepo.fetchSyncedBooks(userId, lang), BookRepo.fetchSyncedBooks(userId, lang),
ChapterRepo.fetchSyncedChapters(userId, lang), ChapterRepo.fetchSyncedChapters(userId, lang),
@@ -803,7 +890,9 @@ export default class Sync {
IssueRepository.fetchSyncedIssues(userId, lang), IssueRepository.fetchSyncedIssues(userId, lang),
ActRepository.fetchSyncedActSummaries(userId, lang), ActRepository.fetchSyncedActSummaries(userId, lang),
GuidelineRepo.fetchSyncedGuideLine(userId, lang), GuidelineRepo.fetchSyncedGuideLine(userId, lang),
GuidelineRepo.fetchSyncedAIGuideLine(userId, lang) GuidelineRepo.fetchSyncedAIGuideLine(userId, lang),
SpellRepo.fetchSyncedSpells(userId, lang),
SpellTagRepo.fetchSyncedSpellTags(userId, lang)
]); ]);
return allBooks.map((bookRecord: SyncedBookResult): SyncedBook => { return allBooks.map((bookRecord: SyncedBookResult): SyncedBook => {
@@ -958,6 +1047,22 @@ export default class Sync {
lastUpdate: bookToolsQuery.last_update lastUpdate: bookToolsQuery.last_update
} : null; } : null;
const bookSpells: SyncedSpell[] = allSpells
.filter((spellRecord: SyncedSpellResult): boolean => spellRecord.book_id === currentBookId)
.map((spellRecord: SyncedSpellResult): SyncedSpell => ({
id: spellRecord.spell_id,
name: System.decryptDataWithUserKey(spellRecord.name, userEncryptionKey),
lastUpdate: spellRecord.last_update
}));
const bookSpellTags: SyncedSpellTag[] = allSpellTags
.filter((spellTagRecord: SyncedSpellTagResult): boolean => spellTagRecord.book_id === currentBookId)
.map((spellTagRecord: SyncedSpellTagResult): SyncedSpellTag => ({
id: spellTagRecord.tag_id,
name: System.decryptDataWithUserKey(spellTagRecord.name, userEncryptionKey),
lastUpdate: spellTagRecord.last_update
}));
return { return {
id: currentBookId, id: currentBookId,
type: bookRecord.type, type: bookRecord.type,
@@ -974,7 +1079,9 @@ export default class Sync {
actSummaries: bookActSummaries, actSummaries: bookActSummaries,
guideLine: bookGuideLine, guideLine: bookGuideLine,
aiGuideLine: bookAIGuideLine, aiGuideLine: bookAIGuideLine,
bookTools: bookTools bookTools: bookTools,
spells: bookSpells,
spellTags: bookSpellTags
}; };
}); });
} }

View File

@@ -25,6 +25,8 @@ import WorldRepository, {
BookWorldTable BookWorldTable
} from "../repositories/world.repository.js"; } from "../repositories/world.repository.js";
import ChapterContentRepository, { BookChapterContentTable } from "../repositories/chaptercontent.repository.js"; import ChapterContentRepository, { BookChapterContentTable } from "../repositories/chaptercontent.repository.js";
import SpellRepo, { BookSpellsTable } from "../repositories/spell.repo.js";
import SpellTagRepo, { BookSpellTagsTable } from "../repositories/spelltag.repo.js";
export default class Upload { export default class Upload {
/** /**
@@ -52,7 +54,9 @@ export default class Upload {
encryptedLocations, encryptedLocations,
encryptedPlotPoints, encryptedPlotPoints,
encryptedWorlds, encryptedWorlds,
bookToolsData bookToolsData,
encryptedSpells,
encryptedSpellTags
]: [ ]: [
EritBooksTable[], EritBooksTable[],
BookActSummariesTable[], BookActSummariesTable[],
@@ -65,7 +69,9 @@ export default class Upload {
BookLocationTable[], BookLocationTable[],
BookPlotPointsTable[], BookPlotPointsTable[],
BookWorldTable[], BookWorldTable[],
BookToolsTable | null BookToolsTable | null,
BookSpellsTable[],
BookSpellTagsTable[]
] = await Promise.all([ ] = await Promise.all([
BookRepo.fetchEritBooksTable(userId, bookId, lang), BookRepo.fetchEritBooksTable(userId, bookId, lang),
ActRepository.fetchBookActSummaries(userId, bookId, lang), ActRepository.fetchBookActSummaries(userId, bookId, lang),
@@ -78,7 +84,9 @@ export default class Upload {
LocationRepo.fetchBookLocations(userId, bookId, lang), LocationRepo.fetchBookLocations(userId, bookId, lang),
PlotPointRepository.fetchBookPlotPoints(userId, bookId, lang), PlotPointRepository.fetchBookPlotPoints(userId, bookId, lang),
WorldRepository.fetchBookWorlds(userId, bookId, lang), WorldRepository.fetchBookWorlds(userId, bookId, lang),
BookRepo.fetchBookTools(userId, bookId, lang) BookRepo.fetchBookTools(userId, bookId, lang),
SpellRepo.fetchBookSpellsTable(userId, bookId, lang),
SpellTagRepo.fetchBookSpellTagsTable(userId, bookId, lang)
]); ]);
const [ const [
@@ -239,6 +247,23 @@ export default class Upload {
const bookTools: BookToolsTable[] = bookToolsData ? [bookToolsData] : []; const bookTools: BookToolsTable[] = bookToolsData ? [bookToolsData] : [];
const spells: BookSpellsTable[] = encryptedSpells.map((spell: BookSpellsTable): BookSpellsTable => ({
...spell,
name: System.decryptDataWithUserKey(spell.name, userEncryptionKey),
description: System.decryptDataWithUserKey(spell.description, userEncryptionKey),
appearance: System.decryptDataWithUserKey(spell.appearance, userEncryptionKey),
tags: System.decryptDataWithUserKey(spell.tags, userEncryptionKey),
power_level: spell.power_level ? System.decryptDataWithUserKey(spell.power_level, userEncryptionKey) : null,
components: spell.components ? System.decryptDataWithUserKey(spell.components, userEncryptionKey) : null,
limitations: spell.limitations ? System.decryptDataWithUserKey(spell.limitations, userEncryptionKey) : null,
notes: spell.notes ? System.decryptDataWithUserKey(spell.notes, userEncryptionKey) : null
}));
const spellTags: BookSpellTagsTable[] = encryptedSpellTags.map((spellTag: BookSpellTagsTable): BookSpellTagsTable => ({
...spellTag,
name: System.decryptDataWithUserKey(spellTag.name, userEncryptionKey)
}));
return { return {
eritBooks, eritBooks,
actSummaries, actSummaries,
@@ -257,7 +282,9 @@ export default class Upload {
worldElements, worldElements,
locationElements, locationElements,
locationSubElements, locationSubElements,
bookTools bookTools,
spells,
spellTags
}; };
} }
} }

View File

@@ -52,6 +52,7 @@ export interface BookToolsTable extends Record<string, SQLiteValue> {
characters_enabled: number; characters_enabled: number;
worlds_enabled: number; worlds_enabled: number;
locations_enabled: number; locations_enabled: number;
spells_enabled: number;
last_update: number; last_update: number;
} }
@@ -379,7 +380,7 @@ export default class BookRepo {
static fetchBookTools(userId: string, bookId: string, lang: 'fr' | 'en'): BookToolsTable | null { static fetchBookTools(userId: string, bookId: string, lang: 'fr' | 'en'): BookToolsTable | null {
try { try {
const db: Database = System.getDb(); const db: Database = System.getDb();
const query: string = 'SELECT book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, last_update FROM book_tools WHERE user_id=? AND book_id=?'; const query: string = 'SELECT book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, spells_enabled, last_update FROM book_tools WHERE user_id=? AND book_id=?';
const params: SQLiteValue[] = [userId, bookId]; const params: SQLiteValue[] = [userId, bookId];
const result = db.get(query, params) as BookToolsTable | undefined; const result = db.get(query, params) as BookToolsTable | undefined;
return result ?? null; return result ?? null;
@@ -392,7 +393,7 @@ export default class BookRepo {
} }
} }
static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled', enabled: boolean, lastUpdate: number, lang: 'fr' | 'en'): boolean { static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' | 'spells_enabled', enabled: boolean, lastUpdate: number, lang: 'fr' | 'en'): boolean {
const enabledValue: number = enabled ? 1 : 0; const enabledValue: number = enabled ? 1 : 0;
try { try {
const db: Database = System.getDb(); const db: Database = System.getDb();
@@ -404,8 +405,9 @@ export default class BookRepo {
const charactersValue: number = toolName === 'characters_enabled' ? enabledValue : 0; const charactersValue: number = toolName === 'characters_enabled' ? enabledValue : 0;
const worldsValue: number = toolName === 'worlds_enabled' ? enabledValue : 0; const worldsValue: number = toolName === 'worlds_enabled' ? enabledValue : 0;
const locationsValue: number = toolName === 'locations_enabled' ? enabledValue : 0; const locationsValue: number = toolName === 'locations_enabled' ? enabledValue : 0;
const insertQuery: string = 'INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, last_update) VALUES (?, ?, ?, ?, ?, ?)'; const spellsValue: number = toolName === 'spells_enabled' ? enabledValue : 0;
const insertResult: RunResult = db.run(insertQuery, [bookId, userId, charactersValue, worldsValue, locationsValue, lastUpdate]); const insertQuery: string = 'INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, spells_enabled, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)';
const insertResult: RunResult = db.run(insertQuery, [bookId, userId, charactersValue, worldsValue, locationsValue, spellsValue, lastUpdate]);
return insertResult.changes > 0; return insertResult.changes > 0;
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
@@ -420,11 +422,11 @@ export default class BookRepo {
* Upserts book tools settings during sync. * Upserts book tools settings during sync.
* Inserts if not exists, updates if exists. * Inserts if not exists, updates if exists.
*/ */
static insertSyncBookTools(bookId: string, userId: string, charactersEnabled: number, worldsEnabled: number, locationsEnabled: number, lastUpdate: number, lang: 'fr' | 'en'): boolean { static insertSyncBookTools(bookId: string, userId: string, charactersEnabled: number, worldsEnabled: number, locationsEnabled: number, spellsEnabled: number, lastUpdate: number, lang: 'fr' | 'en'): boolean {
try { try {
const db: Database = System.getDb(); const db: Database = System.getDb();
const query: string = 'INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, last_update) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (book_id, user_id) DO UPDATE SET characters_enabled = excluded.characters_enabled, worlds_enabled = excluded.worlds_enabled, locations_enabled = excluded.locations_enabled, last_update = excluded.last_update'; const query: string = 'INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, spells_enabled, last_update) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (book_id, user_id) DO UPDATE SET characters_enabled = excluded.characters_enabled, worlds_enabled = excluded.worlds_enabled, locations_enabled = excluded.locations_enabled, spells_enabled = excluded.spells_enabled, last_update = excluded.last_update';
const params: SQLiteValue[] = [bookId, userId, charactersEnabled, worldsEnabled, locationsEnabled, lastUpdate]; const params: SQLiteValue[] = [bookId, userId, charactersEnabled, worldsEnabled, locationsEnabled, spellsEnabled, lastUpdate];
db.run(query, params); db.run(query, params);
return true; return true;
} catch (error: unknown) { } catch (error: unknown) {

View File

@@ -198,6 +198,32 @@ export default class CharacterRepo {
} }
} }
/**
* Deletes a character and all its related data (attributes) from the database.
* @param userId - The unique identifier of the user
* @param characterId - The unique identifier of the character to delete
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the deletion was successful, false otherwise
*/
static deleteCharacter(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const deleteAttributesQuery: string = 'DELETE FROM `book_characters_attributes` WHERE `character_id`=? AND `user_id`=?';
db.run(deleteAttributesQuery, [characterId, userId]);
const deleteCharacterQuery: string = 'DELETE FROM `book_characters` WHERE `character_id`=? AND `user_id`=?';
const result: RunResult = db.run(deleteCharacterQuery, [characterId, userId]);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de supprimer le personnage.` : `Unable to delete character.`);
} else {
console.error("An unknown error occurred.");
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/** /**
* Deletes a character attribute from the database. * Deletes a character attribute from the database.
* @param userId - The unique identifier of the user * @param userId - The unique identifier of the user

View File

@@ -1,4 +1,5 @@
import sqlite3 from 'node-sqlite3-wasm'; import sqlite3 from 'node-sqlite3-wasm';
import { app } from 'electron';
type Database = sqlite3.Database; type Database = sqlite3.Database;
@@ -18,11 +19,9 @@ const schemaVersion = 1;
* DEV ONLY - S'exécute à chaque refresh, pas besoin de version * DEV ONLY - S'exécute à chaque refresh, pas besoin de version
* Mets ta query, test, efface après * Mets ta query, test, efface après
*/ */
const devQueries: string[] = [ const devQueries: string[] = [];
]; const isDev:boolean = !app.isPackaged;
const isDev = process.env.NODE_ENV === 'development';
function columnExists(db: Database, table: string, column: string): boolean { function columnExists(db: Database, table: string, column: string): boolean {
const result = db.all(`PRAGMA table_info(${table})`) as { name: string }[]; const result = db.all(`PRAGMA table_info(${table})`) as { name: string }[];

View File

@@ -74,4 +74,15 @@ ipcMain.handle('db:character:update', createHandler<UpdateCharacterData, boolean
return Character.updateCharacter(userId, data.character, lang); return Character.updateCharacter(userId, data.character, lang);
} }
) )
);
// DELETE /character/delete - Delete character
interface DeleteCharacterData {
characterId: string;
}
ipcMain.handle('db:character:delete', createHandler<DeleteCharacterData, boolean>(
function(userId: string, data: DeleteCharacterData, lang: 'fr' | 'en'): boolean {
return Character.deleteCharacter(userId, data.characterId, lang);
}
)
); );

View File

@@ -15,6 +15,7 @@ import './ipc/chapter.ipc.js';
import './ipc/character.ipc.js'; import './ipc/character.ipc.js';
import './ipc/location.ipc.js'; import './ipc/location.ipc.js';
import './ipc/offline.ipc.js'; import './ipc/offline.ipc.js';
import './ipc/spell.ipc.js';
// Fix pour __dirname en ES modules // Fix pour __dirname en ES modules
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);

View File

@@ -425,6 +425,8 @@
"errorUpdateCharacter": "Error updating character.", "errorUpdateCharacter": "Error updating character.",
"errorAddAttribute": "Error adding attribute.", "errorAddAttribute": "Error adding attribute.",
"errorRemoveAttribute": "Error removing attribute.", "errorRemoveAttribute": "Error removing attribute.",
"errorDeleteCharacter": "Error deleting character.",
"successDelete": "Character deleted successfully.",
"enableTool": "Enable characters", "enableTool": "Enable characters",
"enableToolDescription": "Enable character management for this book.", "enableToolDescription": "Enable character management for this book.",
"toolEnabled": "Character management enabled.", "toolEnabled": "Character management enabled.",
@@ -447,7 +449,9 @@
"historyPlaceholder": "Character history...", "historyPlaceholder": "Character history...",
"roleFull": "Role", "roleFull": "Role",
"roleFullPlaceholder": "Role of the character in the story", "roleFullPlaceholder": "Role of the character in the story",
"fetchAttributesError": "Error fetching attributes." "fetchAttributesError": "Error fetching attributes.",
"deleteTitle": "Delete character",
"deleteMessage": "Are you sure you want to delete {name}? This action cannot be undone."
}, },
"characterList": { "characterList": {
"search": "Search for a character...", "search": "Search for a character...",
@@ -939,6 +943,7 @@
"common": { "common": {
"cancel": "Cancel", "cancel": "Cancel",
"confirm": "Confirm", "confirm": "Confirm",
"delete": "Delete",
"unknownError": "An unknown error occurred", "unknownError": "An unknown error occurred",
"loading": "Loading..." "loading": "Loading..."
}, },

View File

@@ -425,6 +425,8 @@
"errorUpdateCharacter": "Erreur lors de la mise à jour du personnage.", "errorUpdateCharacter": "Erreur lors de la mise à jour du personnage.",
"errorAddAttribute": "Erreur lors de l'ajout de l'attribut.", "errorAddAttribute": "Erreur lors de l'ajout de l'attribut.",
"errorRemoveAttribute": "Erreur lors de la suppression de l'attribut.", "errorRemoveAttribute": "Erreur lors de la suppression de l'attribut.",
"errorDeleteCharacter": "Erreur lors de la suppression du personnage.",
"successDelete": "Personnage supprimé avec succès.",
"enableTool": "Activer les personnages", "enableTool": "Activer les personnages",
"enableToolDescription": "Activer la gestion des personnages pour ce livre.", "enableToolDescription": "Activer la gestion des personnages pour ce livre.",
"toolEnabled": "Gestion des personnages activée.", "toolEnabled": "Gestion des personnages activée.",
@@ -447,7 +449,9 @@
"historyPlaceholder": "Histoire du personnage...", "historyPlaceholder": "Histoire du personnage...",
"roleFull": "Rôle", "roleFull": "Rôle",
"roleFullPlaceholder": "Rôle du personnage dans l'histoire", "roleFullPlaceholder": "Rôle du personnage dans l'histoire",
"fetchAttributesError": "Erreur lors de la récupération des attributs." "fetchAttributesError": "Erreur lors de la récupération des attributs.",
"deleteTitle": "Supprimer le personnage",
"deleteMessage": "Êtes-vous sûr de vouloir supprimer {name} ? Cette action est irréversible."
}, },
"characterList": { "characterList": {
"search": "Rechercher un personnage...", "search": "Rechercher un personnage...",
@@ -940,6 +944,7 @@
"common": { "common": {
"cancel": "Annuler", "cancel": "Annuler",
"confirm": "Confirmer", "confirm": "Confirmer",
"delete": "Supprimer",
"unknownError": "Une erreur inconnue est survenue", "unknownError": "Une erreur inconnue est survenue",
"loading": "Chargement..." "loading": "Chargement..."
}, },

View File

@@ -19,6 +19,8 @@ export interface SyncedBook {
guideLine: SyncedGuideLine | null; guideLine: SyncedGuideLine | null;
aiGuideLine: SyncedAIGuideLine | null; aiGuideLine: SyncedAIGuideLine | null;
bookTools: SyncedBookTools | null; bookTools: SyncedBookTools | null;
spells: SyncedSpell[];
spellTags: SyncedSpellTag[];
} }
export interface SyncedChapter { export interface SyncedChapter {
@@ -116,6 +118,18 @@ export interface SyncedAIGuideLine {
lastUpdate: number; lastUpdate: number;
} }
export interface SyncedSpell {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedSpellTag {
id: string;
name: string;
lastUpdate: number;
}
export interface BookSyncCompare { export interface BookSyncCompare {
id: string; id: string;
chapters: string[]; chapters: string[];
@@ -135,6 +149,8 @@ export interface BookSyncCompare {
guideLine: boolean; guideLine: boolean;
aiGuideLine: boolean; aiGuideLine: boolean;
bookTools: boolean; bookTools: boolean;
spells: string[];
spellTags: string[];
} }
export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook): BookSyncCompare | null { export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook): BookSyncCompare | null {
@@ -152,6 +168,8 @@ export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook):
const changedPlotPointIds: string[] = []; const changedPlotPointIds: string[] = [];
const changedIssueIds: string[] = []; const changedIssueIds: string[] = [];
const changedActSummaryIds: string[] = []; const changedActSummaryIds: string[] = [];
const changedSpellIds: string[] = [];
const changedSpellTagIds: string[] = [];
let guideLineChanged: boolean = false; let guideLineChanged: boolean = false;
let aiGuideLineChanged: boolean = false; let aiGuideLineChanged: boolean = false;
let bookToolsChanged: boolean = false; let bookToolsChanged: boolean = false;
@@ -309,6 +327,20 @@ export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook):
bookToolsChanged = true; bookToolsChanged = true;
} }
newerBook.spellTags.forEach((newerSpellTag: SyncedSpellTag): void => {
const olderSpellTag: SyncedSpellTag | undefined = olderBook.spellTags.find((spellTag: SyncedSpellTag): boolean => spellTag.id === newerSpellTag.id);
if (!olderSpellTag || newerSpellTag.lastUpdate > olderSpellTag.lastUpdate) {
changedSpellTagIds.push(newerSpellTag.id);
}
});
newerBook.spells.forEach((newerSpell: SyncedSpell): void => {
const olderSpell: SyncedSpell | undefined = olderBook.spells.find((spell: SyncedSpell): boolean => spell.id === newerSpell.id);
if (!olderSpell || newerSpell.lastUpdate > olderSpell.lastUpdate) {
changedSpellIds.push(newerSpell.id);
}
});
const hasChanges: boolean = const hasChanges: boolean =
changedChapterIds.length > 0 || changedChapterIds.length > 0 ||
changedChapterContentIds.length > 0 || changedChapterContentIds.length > 0 ||
@@ -324,6 +356,8 @@ export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook):
changedPlotPointIds.length > 0 || changedPlotPointIds.length > 0 ||
changedIssueIds.length > 0 || changedIssueIds.length > 0 ||
changedActSummaryIds.length > 0 || changedActSummaryIds.length > 0 ||
changedSpellIds.length > 0 ||
changedSpellTagIds.length > 0 ||
guideLineChanged || guideLineChanged ||
aiGuideLineChanged || aiGuideLineChanged ||
bookToolsChanged; bookToolsChanged;
@@ -350,6 +384,8 @@ export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook):
actSummaries: changedActSummaryIds, actSummaries: changedActSummaryIds,
guideLine: guideLineChanged, guideLine: guideLineChanged,
aiGuideLine: aiGuideLineChanged, aiGuideLine: aiGuideLineChanged,
bookTools: bookToolsChanged bookTools: bookToolsChanged,
spells: changedSpellIds,
spellTags: changedSpellTagIds
}; };
} }