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:
@@ -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 = {
|
||||||
@@ -164,6 +165,40 @@ 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
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -139,12 +142,23 @@ 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>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{selectedCharacter?.id && (
|
||||||
|
<DeleteButton
|
||||||
|
onDelete={(): Promise<void> => handleDeleteCharacter(selectedCharacter.id as string)}
|
||||||
|
confirmTitle={t("characterDetail.deleteTitle")}
|
||||||
|
confirmMessage={t("characterDetail.deleteMessage", {name: selectedCharacter.name})}
|
||||||
|
confirmButtonText={t("common.delete")}
|
||||||
|
cancelButtonText={t("common.cancel")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<button onClick={handleSaveCharacter}
|
<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">
|
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}
|
<FontAwesomeIcon icon={selectedCharacter?.id ? faSave : faPlus}
|
||||||
className="text-text-primary w-5 h-5"/>
|
className="text-text-primary w-5 h-5"/>
|
||||||
</button>
|
</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">
|
||||||
<CollapsableArea title={t("characterDetail.basicInfo")} icon={faUser}>
|
<CollapsableArea title={t("characterDetail.basicInfo")} icon={faUser}>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }[];
|
||||||
|
|||||||
@@ -75,3 +75,14 @@ ipcMain.handle('db:character:update', createHandler<UpdateCharacterData, boolean
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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..."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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..."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user