diff --git a/components/book/settings/characters/CharacterComponent.tsx b/components/book/settings/characters/CharacterComponent.tsx index e061e82..4eda4aa 100644 --- a/components/book/settings/characters/CharacterComponent.tsx +++ b/components/book/settings/characters/CharacterComponent.tsx @@ -28,6 +28,7 @@ interface CharacterDetailProps { attrId: string, ) => void; handleSaveCharacter: () => void; + handleDeleteCharacter: (characterId: string) => Promise; } const initialCharacterState: CharacterProps = { @@ -163,7 +164,41 @@ export function CharacterComponent({showToggle = true}: {showToggle?: boolean}, } } } - + + async function handleDeleteCharacter(characterId: string): Promise { + try { + let response: boolean; + if (isCurrentlyOffline() || book?.localBook) { + response = await window.electron.invoke('db:character:delete', { + characterId: characterId, + }); + } else { + response = await System.authDeleteToServer('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 { if (!updatedCharacter.name) { errorMessage(t("characterComponent.errorNameRequired")); @@ -394,6 +429,7 @@ export function CharacterComponent({showToggle = true}: {showToggle?: boolean}, handleRemoveElement={handleRemoveElement} handleCharacterChange={handleCharacterChange} handleSaveCharacter={handleSaveCharacter} + handleDeleteCharacter={handleDeleteCharacter} /> ) : ( void; handleSaveCharacter: () => void; + handleDeleteCharacter: (characterId: string) => Promise; } export default function CharacterDetail( @@ -56,6 +58,7 @@ export default function CharacterDetail( handleRemoveElement, handleAddElement, handleSaveCharacter, + handleDeleteCharacter, }: CharacterDetailProps ) { const t = useTranslations(); @@ -64,7 +67,7 @@ export default function CharacterDetail( const {book} = useContext(BookContext); const {session} = useContext(SessionContext); const {errorMessage} = useContext(AlertContext); - + useEffect((): void => { if (selectedCharacter?.id !== null) { getAttributes().then(); @@ -139,11 +142,22 @@ export default function CharacterDetail( {selectedCharacter?.name || t("characterDetail.newCharacter")} - +
+ {selectedCharacter?.id && ( + => handleDeleteCharacter(selectedCharacter.id as string)} + confirmTitle={t("characterDetail.deleteTitle")} + confirmMessage={t("characterDetail.deleteMessage", {name: selectedCharacter.name})} + confirmButtonText={t("common.delete")} + cancelButtonText={t("common.cancel")} + /> + )} + +
diff --git a/components/book/settings/spells/SpellDetail.tsx b/components/book/settings/spells/SpellDetail.tsx index 71dc118..f77fefb 100644 --- a/components/book/settings/spells/SpellDetail.tsx +++ b/components/book/settings/spells/SpellDetail.tsx @@ -8,6 +8,7 @@ import TextInput from "@/components/form/TextInput"; import TexteAreaInput from "@/components/form/TexteAreaInput"; import SelectBox from "@/components/form/SelectBox"; import SpellTagChip from "@/components/book/settings/spells/SpellTagChip"; +import DeleteButton from "@/components/form/DeleteButton"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import { faArrowLeft, @@ -20,11 +21,9 @@ import { faSave, faStickyNote, faTags, - faTrash, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; import {useTranslations} from "next-intl"; -import AlertBox from "@/components/AlertBox"; interface SpellDetailProps { selectedSpell: SpellEditState; @@ -52,7 +51,6 @@ export default function SpellDetail( const [showTagDropdown, setShowTagDropdown] = useState(false); const [isCreatingTag, setIsCreatingTag] = useState(false); const [newTagColor, setNewTagColor] = useState(defaultTagColors[0]); - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); function handleAddTag(tagId: string): void { if (!selectedSpell.tags.includes(tagId)) { @@ -118,12 +116,13 @@ export default function SpellDetail(
{selectedSpell.id && ( - + => handleDeleteSpell(selectedSpell.id as string)} + confirmTitle={t("spellDetail.deleteTitle")} + confirmMessage={t("spellDetail.deleteMessage", {name: selectedSpell.name})} + confirmButtonText={t("common.delete")} + cancelButtonText={t("common.cancel")} + /> )}
- {showDeleteConfirm && selectedSpell.id && ( - => { - await handleDeleteSpell(selectedSpell.id as string); - setShowDeleteConfirm(false); - }} - onCancel={(): void => setShowDeleteConfirm(false)} - /> - )}
diff --git a/electron/database/models/Book.ts b/electron/database/models/Book.ts index f2070d4..a0d72ed 100644 --- a/electron/database/models/Book.ts +++ b/electron/database/models/Book.ts @@ -22,6 +22,9 @@ import { } from "../repositories/location.repository.js"; import { BookPlotPointsTable } from "../repositories/plotpoint.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 { SyncedCharacter } from "./Character.js"; import { SyncedLocation } from "./Location.js"; @@ -42,6 +45,7 @@ export interface BookToolsSettings { characters: boolean; worlds: boolean; locations: boolean; + spells: boolean; } export interface BookProps { @@ -79,6 +83,8 @@ export interface CompleteBook { locationElements: LocationElementTable[]; locationSubElements: LocationSubElementTable[]; bookTools: BookToolsTable[]; + spells: BookSpellsTable[]; + spellTags: BookSpellTagsTable[]; } export interface SyncedBook { @@ -98,6 +104,8 @@ export interface SyncedBook { guideLine: SyncedGuideLine | null; aiGuideLine: SyncedAIGuideLine | null; bookTools: SyncedBookTools | null; + spells: SyncedSpell[]; + spellTags: SyncedSpellTag[]; } export interface BookSyncCompare { @@ -119,6 +127,8 @@ export interface BookSyncCompare { guideLine: boolean; aiGuideLine: boolean; bookTools: boolean; + spells: string[]; + spellTags: string[]; } export interface CompleteBookData { @@ -272,7 +282,8 @@ export default class Book { tools: { characters: bookTools ? bookTools.characters_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); } - public static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters' | 'worlds' | 'locations', enabled: boolean, lang: 'fr' | 'en' = 'fr'): boolean { - const columnName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' = `${toolName}_enabled` as 'characters_enabled' | 'worlds_enabled' | 'locations_enabled'; + 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' | 'spells_enabled' = `${toolName}_enabled` as 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' | 'spells_enabled'; return BookRepo.updateBookToolSetting(userId, bookId, columnName, enabled, System.timeStampInSeconds(), lang); } diff --git a/electron/database/models/Character.ts b/electron/database/models/Character.ts index 7c51a68..8a68e2c 100644 --- a/electron/database/models/Character.ts +++ b/electron/database/models/Character.ts @@ -211,6 +211,17 @@ export default class Character { 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. * Decrypts attribute data using the user's encryption key. diff --git a/electron/database/models/Download.ts b/electron/database/models/Download.ts index 24910f9..c617da7 100644 --- a/electron/database/models/Download.ts +++ b/electron/database/models/Download.ts @@ -28,6 +28,8 @@ import GuidelineRepo, { BookGuideLineTable } from "../repositories/guideline.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 { /** @@ -198,8 +200,54 @@ export default class Download { }); if (!issuesInserted) return false; - return 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); + 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.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; } } diff --git a/electron/database/models/Sync.ts b/electron/database/models/Sync.ts index 020b08f..3fd2f21 100644 --- a/electron/database/models/Sync.ts +++ b/electron/database/models/Sync.ts @@ -1,6 +1,9 @@ import { getUserEncryptionKey } from "../keyManager.js"; import System from "../System.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 ChapterRepo, { BookChapterInfosTable, @@ -87,6 +90,8 @@ export default class Sync { const decryptedGuideLines: BookGuideLineTable[] = []; const decryptedAIGuideLines: BookAIGuideLineTable[] = []; const decryptedIssues: BookIssuesTable[] = []; + const decryptedSpells: BookSpellsTable[] = []; + const decryptedSpellTags: BookSpellTagsTable[] = []; const actSummaryIds: string[] = syncCompareData.actSummaries; const chapterIds: string[] = syncCompareData.chapters; @@ -102,6 +107,8 @@ export default class Sync { const worldIds: string[] = syncCompareData.worlds; const worldElementIds: string[] = syncCompareData.worldElements; const issueIds: string[] = syncCompareData.issues; + const spellIds: string[] = syncCompareData.spells; + const spellTagIds: string[] = syncCompareData.spellTags; if (actSummaryIds && actSummaryIds.length > 0) { 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); if (bookResults.length > 0) { const bookRecord: EritBooksTable = bookResults[0]; @@ -371,7 +409,9 @@ export default class Sync { guideLine: decryptedGuideLines, aiGuideLine: decryptedAIGuideLines, 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) { 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) { 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; } @@ -767,7 +850,9 @@ export default class Sync { allIssues, allActSummaries, allGuidelines, - allAIGuidelines + allAIGuidelines, + allSpells, + allSpellTags ]: [ SyncedBookResult[], SyncedChapterResult[], @@ -785,7 +870,9 @@ export default class Sync { SyncedIssueResult[], SyncedActSummaryResult[], SyncedGuideLineResult[], - SyncedAIGuideLineResult[] + SyncedAIGuideLineResult[], + SyncedSpellResult[], + SyncedSpellTagResult[] ] = await Promise.all([ BookRepo.fetchSyncedBooks(userId, lang), ChapterRepo.fetchSyncedChapters(userId, lang), @@ -803,7 +890,9 @@ export default class Sync { IssueRepository.fetchSyncedIssues(userId, lang), ActRepository.fetchSyncedActSummaries(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 => { @@ -958,6 +1047,22 @@ export default class Sync { lastUpdate: bookToolsQuery.last_update } : 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 { id: currentBookId, type: bookRecord.type, @@ -974,7 +1079,9 @@ export default class Sync { actSummaries: bookActSummaries, guideLine: bookGuideLine, aiGuideLine: bookAIGuideLine, - bookTools: bookTools + bookTools: bookTools, + spells: bookSpells, + spellTags: bookSpellTags }; }); } diff --git a/electron/database/models/Upload.ts b/electron/database/models/Upload.ts index df80d6d..5a885d8 100644 --- a/electron/database/models/Upload.ts +++ b/electron/database/models/Upload.ts @@ -25,6 +25,8 @@ import WorldRepository, { BookWorldTable } from "../repositories/world.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 { /** @@ -52,7 +54,9 @@ export default class Upload { encryptedLocations, encryptedPlotPoints, encryptedWorlds, - bookToolsData + bookToolsData, + encryptedSpells, + encryptedSpellTags ]: [ EritBooksTable[], BookActSummariesTable[], @@ -65,7 +69,9 @@ export default class Upload { BookLocationTable[], BookPlotPointsTable[], BookWorldTable[], - BookToolsTable | null + BookToolsTable | null, + BookSpellsTable[], + BookSpellTagsTable[] ] = await Promise.all([ BookRepo.fetchEritBooksTable(userId, bookId, lang), ActRepository.fetchBookActSummaries(userId, bookId, lang), @@ -78,7 +84,9 @@ export default class Upload { LocationRepo.fetchBookLocations(userId, bookId, lang), PlotPointRepository.fetchBookPlotPoints(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 [ @@ -239,6 +247,23 @@ export default class Upload { 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 { eritBooks, actSummaries, @@ -257,7 +282,9 @@ export default class Upload { worldElements, locationElements, locationSubElements, - bookTools + bookTools, + spells, + spellTags }; } } diff --git a/electron/database/repositories/book.repository.ts b/electron/database/repositories/book.repository.ts index 04d512d..d857428 100644 --- a/electron/database/repositories/book.repository.ts +++ b/electron/database/repositories/book.repository.ts @@ -52,6 +52,7 @@ export interface BookToolsTable extends Record { characters_enabled: number; worlds_enabled: number; locations_enabled: number; + spells_enabled: number; last_update: number; } @@ -379,7 +380,7 @@ export default class BookRepo { static fetchBookTools(userId: string, bookId: string, lang: 'fr' | 'en'): BookToolsTable | null { try { 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 result = db.get(query, params) as BookToolsTable | undefined; 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; try { const db: Database = System.getDb(); @@ -404,8 +405,9 @@ export default class BookRepo { const charactersValue: number = toolName === 'characters_enabled' ? enabledValue : 0; const worldsValue: number = toolName === 'worlds_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 insertResult: RunResult = db.run(insertQuery, [bookId, userId, charactersValue, worldsValue, locationsValue, lastUpdate]); + const spellsValue: number = toolName === 'spells_enabled' ? enabledValue : 0; + 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; } catch (error: unknown) { if (error instanceof Error) { @@ -420,11 +422,11 @@ export default class BookRepo { * Upserts book tools settings during sync. * 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 { 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 params: SQLiteValue[] = [bookId, userId, charactersEnabled, worldsEnabled, locationsEnabled, lastUpdate]; + 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, spellsEnabled, lastUpdate]; db.run(query, params); return true; } catch (error: unknown) { diff --git a/electron/database/repositories/character.repository.ts b/electron/database/repositories/character.repository.ts index ed3ca49..7f83baf 100644 --- a/electron/database/repositories/character.repository.ts +++ b/electron/database/repositories/character.repository.ts @@ -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. * @param userId - The unique identifier of the user diff --git a/electron/database/schema.ts b/electron/database/schema.ts index 9db592a..d673e4b 100644 --- a/electron/database/schema.ts +++ b/electron/database/schema.ts @@ -1,4 +1,5 @@ import sqlite3 from 'node-sqlite3-wasm'; +import { app } from 'electron'; type Database = sqlite3.Database; @@ -18,11 +19,9 @@ const schemaVersion = 1; * DEV ONLY - S'exécute à chaque refresh, pas besoin de version * Mets ta query, test, efface après */ -const devQueries: string[] = [ +const devQueries: string[] = []; -]; - -const isDev = process.env.NODE_ENV === 'development'; +const isDev:boolean = !app.isPackaged; function columnExists(db: Database, table: string, column: string): boolean { const result = db.all(`PRAGMA table_info(${table})`) as { name: string }[]; diff --git a/electron/ipc/character.ipc.ts b/electron/ipc/character.ipc.ts index 865c142..deefa26 100644 --- a/electron/ipc/character.ipc.ts +++ b/electron/ipc/character.ipc.ts @@ -74,4 +74,15 @@ ipcMain.handle('db:character:update', createHandler( + function(userId: string, data: DeleteCharacterData, lang: 'fr' | 'en'): boolean { + return Character.deleteCharacter(userId, data.characterId, lang); + } + ) ); \ No newline at end of file diff --git a/electron/main.ts b/electron/main.ts index 44644a2..a44668e 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -15,6 +15,7 @@ import './ipc/chapter.ipc.js'; import './ipc/character.ipc.js'; import './ipc/location.ipc.js'; import './ipc/offline.ipc.js'; +import './ipc/spell.ipc.js'; // Fix pour __dirname en ES modules const __filename = fileURLToPath(import.meta.url); diff --git a/lib/locales/en.json b/lib/locales/en.json index 44f9dc5..81c27ad 100644 --- a/lib/locales/en.json +++ b/lib/locales/en.json @@ -425,6 +425,8 @@ "errorUpdateCharacter": "Error updating character.", "errorAddAttribute": "Error adding attribute.", "errorRemoveAttribute": "Error removing attribute.", + "errorDeleteCharacter": "Error deleting character.", + "successDelete": "Character deleted successfully.", "enableTool": "Enable characters", "enableToolDescription": "Enable character management for this book.", "toolEnabled": "Character management enabled.", @@ -447,7 +449,9 @@ "historyPlaceholder": "Character history...", "roleFull": "Role", "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": { "search": "Search for a character...", @@ -939,6 +943,7 @@ "common": { "cancel": "Cancel", "confirm": "Confirm", + "delete": "Delete", "unknownError": "An unknown error occurred", "loading": "Loading..." }, diff --git a/lib/locales/fr.json b/lib/locales/fr.json index 36390dc..e1f65e1 100644 --- a/lib/locales/fr.json +++ b/lib/locales/fr.json @@ -425,6 +425,8 @@ "errorUpdateCharacter": "Erreur lors de la mise à jour du personnage.", "errorAddAttribute": "Erreur lors de l'ajout 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", "enableToolDescription": "Activer la gestion des personnages pour ce livre.", "toolEnabled": "Gestion des personnages activée.", @@ -447,7 +449,9 @@ "historyPlaceholder": "Histoire du personnage...", "roleFull": "Rôle", "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": { "search": "Rechercher un personnage...", @@ -940,6 +944,7 @@ "common": { "cancel": "Annuler", "confirm": "Confirmer", + "delete": "Supprimer", "unknownError": "Une erreur inconnue est survenue", "loading": "Chargement..." }, diff --git a/lib/models/SyncedBook.ts b/lib/models/SyncedBook.ts index 73931fe..fc7a069 100644 --- a/lib/models/SyncedBook.ts +++ b/lib/models/SyncedBook.ts @@ -19,6 +19,8 @@ export interface SyncedBook { guideLine: SyncedGuideLine | null; aiGuideLine: SyncedAIGuideLine | null; bookTools: SyncedBookTools | null; + spells: SyncedSpell[]; + spellTags: SyncedSpellTag[]; } export interface SyncedChapter { @@ -116,6 +118,18 @@ export interface SyncedAIGuideLine { lastUpdate: number; } +export interface SyncedSpell { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedSpellTag { + id: string; + name: string; + lastUpdate: number; +} + export interface BookSyncCompare { id: string; chapters: string[]; @@ -135,6 +149,8 @@ export interface BookSyncCompare { guideLine: boolean; aiGuideLine: boolean; bookTools: boolean; + spells: string[]; + spellTags: string[]; } export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook): BookSyncCompare | null { @@ -152,6 +168,8 @@ export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook): const changedPlotPointIds: string[] = []; const changedIssueIds: string[] = []; const changedActSummaryIds: string[] = []; + const changedSpellIds: string[] = []; + const changedSpellTagIds: string[] = []; let guideLineChanged: boolean = false; let aiGuideLineChanged: boolean = false; let bookToolsChanged: boolean = false; @@ -309,6 +327,20 @@ export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook): 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 = changedChapterIds.length > 0 || changedChapterContentIds.length > 0 || @@ -324,6 +356,8 @@ export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook): changedPlotPointIds.length > 0 || changedIssueIds.length > 0 || changedActSummaryIds.length > 0 || + changedSpellIds.length > 0 || + changedSpellTagIds.length > 0 || guideLineChanged || aiGuideLineChanged || bookToolsChanged; @@ -350,6 +384,8 @@ export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook): actSummaries: changedActSummaryIds, guideLine: guideLineChanged, aiGuideLine: aiGuideLineChanged, - bookTools: bookToolsChanged + bookTools: bookToolsChanged, + spells: changedSpellIds, + spellTags: changedSpellTagIds }; } \ No newline at end of file