From e45a15225bd310efce83d9ef14556869813926ea Mon Sep 17 00:00:00 2001 From: natreex Date: Wed, 14 Jan 2026 17:42:59 -0500 Subject: [PATCH] Add enable/disable management for book tools (characters, worlds, and locations) - Introduced toggling functionality for managing `characters`, `worlds`, and `locations` tool availability per book. - Updated `CharacterComponent`, `WorldSetting`, and `LocationComponent` with toggle switches for tool enablement. - Added `book_tools` database table and related schema migration for storing tool settings. - Extended API calls, models, and IPC handlers to support tool enablement states. - Localized new strings for English with supporting descriptions and messages. - Adjusted conditional rendering logic across components to respect tool enablement. --- .../characters/CharacterComponent.tsx | 100 +++-- .../settings/locations/LocationComponent.tsx | 329 ++++++++++------- .../book/settings/world/WorldSetting.tsx | 348 ++++++++++-------- components/rightbar/ComposerRightBar.tsx | 18 +- electron/database/models/Book.ts | 25 +- electron/database/models/Character.ts | 19 +- electron/database/models/Download.ts | 9 +- electron/database/models/Location.ts | 20 +- electron/database/models/Sync.ts | 37 +- electron/database/models/Upload.ts | 16 +- electron/database/models/World.ts | 16 +- .../database/repositories/book.repository.ts | 80 ++++ electron/database/schema.ts | 29 +- electron/ipc/book.ipc.ts | 14 + lib/locales/en.json | 23 +- lib/locales/fr.json | 23 +- lib/models/Book.ts | 7 + lib/models/Character.ts | 5 + lib/models/World.ts | 5 + 19 files changed, 782 insertions(+), 341 deletions(-) diff --git a/components/book/settings/characters/CharacterComponent.tsx b/components/book/settings/characters/CharacterComponent.tsx index ffe730a..3b69b6a 100644 --- a/components/book/settings/characters/CharacterComponent.tsx +++ b/components/book/settings/characters/CharacterComponent.tsx @@ -1,6 +1,6 @@ 'use client'; import {Dispatch, forwardRef, SetStateAction, useContext, useEffect, useImperativeHandle, useState} from 'react'; -import {Attribute, CharacterProps} from "@/lib/models/Character"; +import {Attribute, CharacterProps, CharacterListResponse} from "@/lib/models/Character"; import {SessionContext} from "@/context/SessionContext"; import CharacterList from './CharacterList'; import System from '@/lib/models/System'; @@ -13,6 +13,7 @@ import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; import {SyncedBook} from "@/lib/models/SyncedBook"; +import ToggleSwitch from "@/components/form/ToggleSwitch"; interface CharacterDetailProps { selectedCharacter: CharacterProps | null; @@ -47,17 +48,18 @@ const initialCharacterState: CharacterProps = { motivations: [], }; -export function CharacterComponent(props: any, ref: any) { +export function CharacterComponent({showToggle = true}: {showToggle?: boolean}, ref: any) { const t = useTranslations(); const {lang} = useContext(LangContext) const {isCurrentlyOffline} = useContext(OfflineContext); const {addToQueue} = useContext(LocalSyncQueueContext); const {localSyncedBooks} = useContext(BooksSyncContext); const {session} = useContext(SessionContext); - const {book} = useContext(BookContext); + const {book, setBook} = useContext(BookContext); const {errorMessage, successMessage} = useContext(AlertContext); const [characters, setCharacters] = useState([]); const [selectedCharacter, setSelectedCharacter] = useState(null); + const [toolEnabled, setToolEnabled] = useState(book?.tools?.characters ?? false); useImperativeHandle(ref, function () { return { @@ -68,23 +70,61 @@ export function CharacterComponent(props: any, ref: any) { useEffect((): void => { getCharacters().then(); }, []); + + async function handleToggleTool(enabled: boolean): Promise { + try { + let response: boolean; + if (isCurrentlyOffline() || book?.localBook) { + response = await window.electron.invoke('db:book:tool:update', { + bookId: book?.bookId, + toolName: 'characters', + enabled: enabled + }); + } else { + response = await System.authPatchToServer('book/tool-setting', { + bookId: book?.bookId, + toolName: 'characters', + enabled: enabled + }, session.accessToken, lang); + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) { + addToQueue('db:book:tool:update', { + bookId: book?.bookId, + toolName: 'characters', + enabled: enabled + }); + } + } + if (response && setBook && book) { + setToolEnabled(enabled); + setBook({...book, tools: {...book.tools, characters: enabled}}); + } + } catch (e: unknown) { + if (e instanceof Error) { + errorMessage(e.message); + } + } + } async function getCharacters(): Promise { try { - let response: CharacterProps[]; + let response: CharacterListResponse; if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:character:list', {bookid: book?.bookId}); + response = await window.electron.invoke('db:character:list', {bookid: book?.bookId}); } else { if (book?.localBook) { - response = await window.electron.invoke('db:character:list', {bookid: book?.bookId}); + response = await window.electron.invoke('db:character:list', {bookid: book?.bookId}); } else { - response = await System.authGetQueryToServer(`character/list`, session.accessToken, lang, { + response = await System.authGetQueryToServer(`character/list`, session.accessToken, lang, { bookid: book?.bookId, }); } } if (response) { - setCharacters(response); + setCharacters(response.characters); + setToolEnabled(response.enabled); + if (setBook && book) { + setBook({...book, tools: {...book.tools, characters: response.enabled}}); + } } } catch (e: unknown) { if (e instanceof Error) { @@ -317,21 +357,35 @@ export function CharacterComponent(props: any, ref: any) { return (
- {selectedCharacter ? ( - - ) : ( - + {showToggle && ( +
+ +
+ )} + {toolEnabled && ( + <> + {selectedCharacter ? ( + + ) : ( + + )} + )}
); diff --git a/components/book/settings/locations/LocationComponent.tsx b/components/book/settings/locations/LocationComponent.tsx index 304f29e..4ac3b45 100644 --- a/components/book/settings/locations/LocationComponent.tsx +++ b/components/book/settings/locations/LocationComponent.tsx @@ -15,6 +15,7 @@ import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; import {SyncedBook} from "@/lib/models/SyncedBook"; +import ToggleSwitch from "@/components/form/ToggleSwitch"; interface SubElement { id: string; @@ -35,7 +36,12 @@ interface LocationProps { elements: Element[]; } -export function LocationComponent(props: any, ref: any) { +interface LocationListResponse { + locations: LocationProps[]; + enabled: boolean; +} + +export function LocationComponent({showToggle = true}: {showToggle?: boolean}, ref: any) { const t = useTranslations(); const {lang} = useContext(LangContext); const {isCurrentlyOffline} = useContext(OfflineContext); @@ -43,15 +49,16 @@ export function LocationComponent(props: any, ref: any) { const {localSyncedBooks} = useContext(BooksSyncContext); const {session} = useContext(SessionContext); const {successMessage, errorMessage} = useContext(AlertContext); - const {book} = useContext(BookContext); - + const {book, setBook} = useContext(BookContext); + const bookId: string | undefined = book?.bookId; const token: string = session.accessToken; - + const [sections, setSections] = useState([]); const [newSectionName, setNewSectionName] = useState(''); const [newElementNames, setNewElementNames] = useState<{ [key: string]: string }>({}); const [newSubElementNames, setNewSubElementNames] = useState<{ [key: string]: string }>({}); + const [toolEnabled, setToolEnabled] = useState(book?.tools?.locations ?? false); useImperativeHandle(ref, function () { return { @@ -62,23 +69,61 @@ export function LocationComponent(props: any, ref: any) { useEffect((): void => { getAllLocations().then(); }, []); - + + async function handleToggleTool(enabled: boolean): Promise { + try { + let response: boolean; + if (isCurrentlyOffline() || book?.localBook) { + response = await window.electron.invoke('db:book:tool:update', { + bookId: bookId, + toolName: 'locations', + enabled: enabled + }); + } else { + response = await System.authPatchToServer('book/tool-setting', { + bookId: bookId, + toolName: 'locations', + enabled: enabled + }, token, lang); + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:tool:update', { + bookId: bookId, + toolName: 'locations', + enabled: enabled + }); + } + } + if (response && setBook && book) { + setToolEnabled(enabled); + setBook({...book, tools: {...book.tools, locations: enabled}}); + } + } catch (e: unknown) { + if (e instanceof Error) { + errorMessage(e.message); + } + } + } + async function getAllLocations(): Promise { try { - let response: LocationProps[]; + let response: LocationListResponse; if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:location:all', {bookid: bookId}); + response = await window.electron.invoke('db:location:all', {bookid: bookId}); } else { if (book?.localBook) { - response = await window.electron.invoke('db:location:all', {bookid: bookId}); + response = await window.electron.invoke('db:location:all', {bookid: bookId}); } else { - response = await System.authGetQueryToServer(`location/all`, token, lang, { + response = await System.authGetQueryToServer(`location/all`, token, lang, { bookid: bookId, }); } } - if (response && response.length > 0) { - setSections(response); + if (response) { + setSections(response.locations); + setToolEnabled(response.enabled); + if (setBook && book) { + setBook({...book, tools: {...book.tools, locations: response.enabled}}); + } } } catch (e: unknown) { if (e instanceof Error) { @@ -423,140 +468,154 @@ export function LocationComponent(props: any, ref: any) { return (
-
-
- ) => setNewSectionName(e.target.value)} - placeholder={t("locationComponent.newSectionPlaceholder")} - /> - } - actionIcon={faPlus} - actionLabel={t("locationComponent.addSectionLabel")} - addButtonCallBack={handleAddSection} + {showToggle && ( +
+
-
- - {sections.length > 0 ? ( - sections.map((section: LocationProps) => ( -
-

- - {section.name} - - {section.elements.length || 0} - - -

-
- {section.elements.length > 0 ? ( - section.elements.map((element, elementIndex) => ( -
-
- ) => - handleElementChange(section.id, elementIndex, 'name', e.target.value) - } - placeholder={t("locationComponent.elementNamePlaceholder")} - /> - } - removeButtonCallBack={(): Promise => handleRemoveElement(section.id, elementIndex)} - /> -
- ): void => handleElementChange(section.id, elementIndex, 'description', e.target.value)} - placeholder={t("locationComponent.elementDescriptionPlaceholder")} - /> - -
- {element.subElements.length > 0 && ( -

{t("locationComponent.subElementsHeading")}

- )} - - {element.subElements.map((subElement: SubElement, subElementIndex: number) => ( -
-
- ): void => - handleSubElementChange(section.id, elementIndex, subElementIndex, 'name', e.target.value) - } - placeholder={t("locationComponent.subElementNamePlaceholder")} - /> - } - removeButtonCallBack={(): Promise => handleRemoveSubElement(section.id, elementIndex, subElementIndex)} - /> -
- - handleSubElementChange(section.id, elementIndex, subElementIndex, 'description', e.target.value) - } - placeholder={t("locationComponent.subElementDescriptionPlaceholder")} - /> -
- ))} - - ) => - setNewSubElementNames({ - ...newSubElementNames, - [elementIndex]: e.target.value - }) - } - placeholder={t("locationComponent.newSubElementPlaceholder")} - /> - } - addButtonCallBack={(): Promise => handleAddSubElement(section.id, elementIndex)} - /> -
-
- )) - ) : ( -
- {t("locationComponent.noElementAvailable")} -
- )} - + )} + {toolEnabled && ( + <> +
+
) => - setNewElementNames({...newElementNames, [section.id]: e.target.value}) - } - placeholder={t("locationComponent.newElementPlaceholder")} + value={newSectionName} + setValue={(e: ChangeEvent) => setNewSectionName(e.target.value)} + placeholder={t("locationComponent.newSectionPlaceholder")} /> } - addButtonCallBack={(): Promise => handleAddElement(section.id)} + actionIcon={faPlus} + actionLabel={t("locationComponent.addSectionLabel")} + addButtonCallBack={handleAddSection} />
- )) - ) : ( -
-

{t("locationComponent.noSectionAvailable")}

-
+ + {sections.length > 0 ? ( + sections.map((section: LocationProps) => ( +
+

+ + {section.name} + + {section.elements.length || 0} + + +

+
+ {section.elements.length > 0 ? ( + section.elements.map((element, elementIndex) => ( +
+
+ ) => + handleElementChange(section.id, elementIndex, 'name', e.target.value) + } + placeholder={t("locationComponent.elementNamePlaceholder")} + /> + } + removeButtonCallBack={(): Promise => handleRemoveElement(section.id, elementIndex)} + /> +
+ ): void => handleElementChange(section.id, elementIndex, 'description', e.target.value)} + placeholder={t("locationComponent.elementDescriptionPlaceholder")} + /> + +
+ {element.subElements.length > 0 && ( +

{t("locationComponent.subElementsHeading")}

+ )} + + {element.subElements.map((subElement: SubElement, subElementIndex: number) => ( +
+
+ ): void => + handleSubElementChange(section.id, elementIndex, subElementIndex, 'name', e.target.value) + } + placeholder={t("locationComponent.subElementNamePlaceholder")} + /> + } + removeButtonCallBack={(): Promise => handleRemoveSubElement(section.id, elementIndex, subElementIndex)} + /> +
+ + handleSubElementChange(section.id, elementIndex, subElementIndex, 'description', e.target.value) + } + placeholder={t("locationComponent.subElementDescriptionPlaceholder")} + /> +
+ ))} + + ) => + setNewSubElementNames({ + ...newSubElementNames, + [elementIndex]: e.target.value + }) + } + placeholder={t("locationComponent.newSubElementPlaceholder")} + /> + } + addButtonCallBack={(): Promise => handleAddSubElement(section.id, elementIndex)} + /> +
+
+ )) + ) : ( +
+ {t("locationComponent.noElementAvailable")} +
+ )} + + ) => + setNewElementNames({...newElementNames, [section.id]: e.target.value}) + } + placeholder={t("locationComponent.newElementPlaceholder")} + /> + } + addButtonCallBack={(): Promise => handleAddElement(section.id)} + /> +
+
+ )) + ) : ( +
+

{t("locationComponent.noSectionAvailable")}

+
+ )} + )}
); diff --git a/components/book/settings/world/WorldSetting.tsx b/components/book/settings/world/WorldSetting.tsx index 51c2a0c..45cbffb 100644 --- a/components/book/settings/world/WorldSetting.tsx +++ b/components/book/settings/world/WorldSetting.tsx @@ -7,7 +7,7 @@ import {BookContext} from "@/context/BookContext"; import {AlertContext} from "@/context/AlertContext"; import {SelectBoxProps} from "@/shared/interface"; import System from "@/lib/models/System"; -import {elementSections, WorldProps} from "@/lib/models/World"; +import {elementSections, WorldProps, WorldListResponse} from "@/lib/models/World"; import {SessionContext} from "@/context/SessionContext"; import InputField from "@/components/form/InputField"; import TextInput from '@/components/form/TextInput'; @@ -20,6 +20,7 @@ import OfflineContext, {OfflineContextType} from "@/context/OfflineContext"; import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext"; import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext"; import {SyncedBook} from "@/lib/models/SyncedBook"; +import ToggleSwitch from "@/components/form/ToggleSwitch"; export interface ElementSection { title: string; @@ -27,7 +28,7 @@ export interface ElementSection { icon: IconDefinition; } -export function WorldSetting(props: any, ref: any) { +export function WorldSetting({showToggle = true}: {showToggle?: boolean}, ref: any) { const t = useTranslations(); const {lang} = useContext(LangContext); const {isCurrentlyOffline} = useContext(OfflineContext); @@ -35,14 +36,15 @@ export function WorldSetting(props: any, ref: any) { const {localSyncedBooks} = useContext(BooksSyncContext); const {errorMessage, successMessage} = useContext(AlertContext); const {session} = useContext(SessionContext); - const {book} = useContext(BookContext); + const {book, setBook} = useContext(BookContext); const bookId: string = book?.bookId ? book.bookId.toString() : ''; - + const [worlds, setWorlds] = useState([]); const [newWorldName, setNewWorldName] = useState(''); const [selectedWorldIndex, setSelectedWorldIndex] = useState(0); const [worldsSelector, setWorldsSelector] = useState([]); const [showAddNewWorld, setShowAddNewWorld] = useState(false); + const [toolEnabled, setToolEnabled] = useState(book?.tools?.worlds ?? false); useImperativeHandle(ref, function () { return { @@ -53,24 +55,62 @@ export function WorldSetting(props: any, ref: any) { useEffect((): void => { getWorlds().then(); }, []); - + + async function handleToggleTool(enabled: boolean): Promise { + try { + let response: boolean; + if (isCurrentlyOffline() || book?.localBook) { + response = await window.electron.invoke('db:book:tool:update', { + bookId: bookId, + toolName: 'worlds', + enabled: enabled + }); + } else { + response = await System.authPatchToServer('book/tool-setting', { + bookId: bookId, + toolName: 'worlds', + enabled: enabled + }, session.accessToken, lang); + if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) { + addToQueue('db:book:tool:update', { + bookId: bookId, + toolName: 'worlds', + enabled: enabled + }); + } + } + if (response && setBook && book) { + setToolEnabled(enabled); + setBook({...book, tools: {...book.tools, worlds: enabled}}); + } + } catch (e: unknown) { + if (e instanceof Error) { + errorMessage(e.message); + } + } + } + async function getWorlds() { try { - let response: WorldProps[]; + let response: WorldListResponse; if (isCurrentlyOffline()) { - response = await window.electron.invoke('db:book:worlds:get', {bookid: bookId}); + response = await window.electron.invoke('db:book:worlds:get', {bookid: bookId}); } else { if (book?.localBook) { - response = await window.electron.invoke('db:book:worlds:get', {bookid: bookId}); + response = await window.electron.invoke('db:book:worlds:get', {bookid: bookId}); } else { - response = await System.authGetQueryToServer(`book/worlds`, session.accessToken, lang, { + response = await System.authGetQueryToServer(`book/worlds`, session.accessToken, lang, { bookid: bookId, }); } } if (response) { - setWorlds(response); - const formattedWorlds: SelectBoxProps[] = response.map( + setWorlds(response.worlds); + setToolEnabled(response.enabled); + if (setBook && book) { + setBook({...book, tools: {...book.tools, worlds: response.enabled}}); + } + const formattedWorlds: SelectBoxProps[] = response.worlds.map( (world: WorldProps): SelectBoxProps => ({ label: world.name, value: world.id.toString(), @@ -193,156 +233,170 @@ export function WorldSetting(props: any, ref: any) { return (
-
-
- { - const worldId = e.target.value; - const index = worlds.findIndex(world => world.id.toString() === worldId); - if (index !== -1) { - setSelectedWorldIndex(index); - } - }} - data={worldsSelector.length > 0 ? worldsSelector : [{ - label: t("worldSetting.noWorldAvailable"), - value: '0' - }]} - defaultValue={worlds[selectedWorldIndex]?.id.toString() || '0'} - placeholder={t("worldSetting.selectWorldPlaceholder")} - /> - } - actionIcon={faPlus} - actionLabel={t("worldSetting.addWorldLabel")} - action={async () => setShowAddNewWorld(!showAddNewWorld)} + {showToggle && ( +
+ - - {showAddNewWorld && ( - ) => setNewWorldName(e.target.value)} - placeholder={t("worldSetting.newWorldPlaceholder")} - /> - } - actionIcon={faPlus} - actionLabel={t("worldSetting.createWorldLabel")} - addButtonCallBack={handleAddNewWorld} - /> - )}
-
- - {worlds.length > 0 && worlds[selectedWorldIndex] ? ( - -
-
+ )} + {toolEnabled && ( + <> +
+
) => { - const updatedWorlds: WorldProps[] = [...worlds]; - updatedWorlds[selectedWorldIndex].name = e.target.value - setWorlds(updatedWorlds); + { + const worldId = e.target.value; + const index = worlds.findIndex(world => world.id.toString() === worldId); + if (index !== -1) { + setSelectedWorldIndex(index); + } }} - placeholder={t("worldSetting.worldNamePlaceholder")} + data={worldsSelector.length > 0 ? worldsSelector : [{ + label: t("worldSetting.noWorldAvailable"), + value: '0' + }]} + defaultValue={worlds[selectedWorldIndex]?.id.toString() || '0'} + placeholder={t("worldSetting.selectWorldPlaceholder")} /> } + actionIcon={faPlus} + actionLabel={t("worldSetting.addWorldLabel")} + action={async () => setShowAddNewWorld(!showAddNewWorld)} /> -
- handleInputChange(e.target.value, 'history')} - placeholder={t("worldSetting.worldHistoryPlaceholder")} + + {showAddNewWorld && ( + ) => setNewWorldName(e.target.value)} + placeholder={t("worldSetting.newWorldPlaceholder")} + /> + } + actionIcon={faPlus} + actionLabel={t("worldSetting.createWorldLabel")} + addButtonCallBack={handleAddNewWorld} /> - } - /> -
- -
-
- handleInputChange(e.target.value, 'politics')} - placeholder={t("worldSetting.politicsPlaceholder")} - /> - } - /> - handleInputChange(e.target.value, 'economy')} - placeholder={t("worldSetting.economyPlaceholder")} - /> - } - /> + )}
- -
-
- handleInputChange(e.target.value, 'religion')} - placeholder={t("worldSetting.religionPlaceholder")} + + {worlds.length > 0 && worlds[selectedWorldIndex] ? ( + +
+
+ ) => { + const updatedWorlds: WorldProps[] = [...worlds]; + updatedWorlds[selectedWorldIndex].name = e.target.value + setWorlds(updatedWorlds); + }} + placeholder={t("worldSetting.worldNamePlaceholder")} + /> + } /> - } - /> - handleInputChange(e.target.value, 'languages')} - placeholder={t("worldSetting.languagesPlaceholder")} +
+ handleInputChange(e.target.value, 'history')} + placeholder={t("worldSetting.worldHistoryPlaceholder")} + /> + } + /> +
+ +
+
+ handleInputChange(e.target.value, 'politics')} + placeholder={t("worldSetting.politicsPlaceholder")} + /> + } /> - } - /> + handleInputChange(e.target.value, 'economy')} + placeholder={t("worldSetting.economyPlaceholder")} + /> + } + /> +
+
+ +
+
+ handleInputChange(e.target.value, 'religion')} + placeholder={t("worldSetting.religionPlaceholder")} + /> + } + /> + handleInputChange(e.target.value, 'languages')} + placeholder={t("worldSetting.languagesPlaceholder")} + /> + } + /> +
+
+ + {elementSections.map((section, index) => ( +
+

+ + {section.title} + + {worlds[selectedWorldIndex][section.section]?.length || 0} + +

+ +
+ ))} +
+ ) : ( +
+

{t("worldSetting.noWorldAvailable")}

-
- - {elementSections.map((section, index) => ( -
-

- - {section.title} - - {worlds[selectedWorldIndex][section.section]?.length || 0} - -

- -
- ))} - - ) : ( -
-

{t("worldSetting.noWorldAvailable")}

-
+ )} + )}
); diff --git a/components/rightbar/ComposerRightBar.tsx b/components/rightbar/ComposerRightBar.tsx index 30bcee2..9b036d7 100644 --- a/components/rightbar/ComposerRightBar.tsx +++ b/components/rightbar/ComposerRightBar.tsx @@ -156,13 +156,13 @@ export default function ComposerRightBar() { )} {currentPanel?.id === 2 && ( - + )} {currentPanel?.id === 3 && ( - + )} {currentPanel?.id === 4 && ( - + )}
@@ -180,6 +180,18 @@ export default function ComposerRightBar() { return false; } } + // Filter Worlds if tools.worlds is disabled + if (component.id === 2 && !book?.tools?.worlds) { + return false; + } + // Filter Locations if tools.locations is disabled + if (component.id === 3 && !book?.tools?.locations) { + return false; + } + // Filter Characters if tools.characters is disabled + if (component.id === 4 && !book?.tools?.characters) { + return false; + } return true; }) .map((component: PanelComponent) => ( diff --git a/electron/database/models/Book.ts b/electron/database/models/Book.ts index d8e2343..45669ba 100644 --- a/electron/database/models/Book.ts +++ b/electron/database/models/Book.ts @@ -1,6 +1,6 @@ import System from '../System.js'; import { getUserEncryptionKey } from '../keyManager.js'; -import BookRepo, { BookQuery, EritBooksTable } from "../repositories/book.repository.js"; +import BookRepo, { BookQuery, BookToolsTable, BookToolsSettings, EritBooksTable } from "../repositories/book.repository.js"; import { BookActSummariesTable } from "../repositories/act.repository.js"; import { BookAIGuideLineTable, BookGuideLineTable } from "../repositories/guideline.repository.js"; import ChapterRepo, { @@ -34,6 +34,12 @@ import { SyncedAIGuideLine, SyncedGuideLine } from "./GuideLine.js"; import Cover from "./Cover.js"; import UserRepo from "../repositories/user.repository.js"; +export interface SyncedBookTools { + charactersEnabled: boolean; + worldsEnabled: boolean; + locationsEnabled: boolean; +} + export interface BookProps { id: string; type: string; @@ -47,6 +53,7 @@ export interface BookProps { wordCount?: number; coverImage?: string; bookMeta?: string; + tools?: BookToolsSettings; } export interface CompleteBook { @@ -67,6 +74,7 @@ export interface CompleteBook { worldElements: BookWorldElementsTable[]; locationElements: LocationElementTable[]; locationSubElements: LocationSubElementTable[]; + bookTools: BookToolsTable[]; } export interface SyncedBook { @@ -85,6 +93,7 @@ export interface SyncedBook { actSummaries: SyncedActSummary[]; guideLine: SyncedGuideLine | null; aiGuideLine: SyncedAIGuideLine | null; + bookTools: SyncedBookTools | null; } export interface BookSyncCompare { @@ -105,6 +114,7 @@ export interface BookSyncCompare { actSummaries: string[]; guideLine: boolean; aiGuideLine: boolean; + bookTools: boolean; } export interface CompleteBookData { @@ -242,6 +252,7 @@ export default class Book { public static async getBook(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { const book: Book = new Book(bookId); book.getBookInfos(userId); + const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang); return { id: book.getId(), type: book.getType(), @@ -253,7 +264,12 @@ export default class Book { desiredReleaseDate: book.getDesiredReleaseDate(), desiredWordCount: book.getDesiredWordCount(), wordCount: book.getWordCount(), - coverImage: book.getCover() + coverImage: book.getCover(), + tools: { + characters: bookTools ? bookTools.characters_enabled === 1 : false, + worlds: bookTools ? bookTools.worlds_enabled === 1 : false, + locations: bookTools ? bookTools.locations_enabled === 1 : false + } }; } @@ -290,6 +306,11 @@ 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'; + return BookRepo.updateBookToolSetting(userId, bookId, columnName, enabled, lang); + } + /** * Gets the book ID. * @returns The book's unique identifier diff --git a/electron/database/models/Character.ts b/electron/database/models/Character.ts index 626937c..7c51a68 100644 --- a/electron/database/models/Character.ts +++ b/electron/database/models/Character.ts @@ -3,6 +3,7 @@ import CharacterRepo, { CharacterResult, CompleteCharacterResult } from "../repositories/character.repository.js"; +import BookRepo, {BookToolsTable} from "../repositories/book.repository.js"; import System from "../System.js"; import {getUserEncryptionKey} from "../keyManager.js"; @@ -41,6 +42,11 @@ export interface CharacterProps { history: string; } +export interface CharacterListResponse { + characters: CharacterProps[]; + enabled: boolean; +} + export interface CompleteCharacterProps { id?: string; name: string; @@ -87,11 +93,15 @@ export default class Character { * @param lang - The language code for localization (defaults to 'fr') * @returns An array of decrypted character properties */ - public static getCharacterList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): CharacterProps[] { + public static getCharacterList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): CharacterListResponse { + const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang); + const enabled: boolean = bookTools ? bookTools.characters_enabled === 1 : false; + const userEncryptionKey: string = getUserEncryptionKey(userId); const encryptedCharacters: CharacterResult[] = CharacterRepo.fetchCharacters(userId, bookId, lang); - if (!encryptedCharacters) return []; - if (encryptedCharacters.length === 0) return []; + if (!encryptedCharacters || encryptedCharacters.length === 0) { + return { characters: [], enabled }; + } const decryptedCharacterList: CharacterProps[] = []; for (const encryptedCharacter of encryptedCharacters) { decryptedCharacterList.push({ @@ -106,7 +116,7 @@ export default class Character { history: encryptedCharacter.history ? System.decryptDataWithUserKey(encryptedCharacter.history, userEncryptionKey) : '', }) } - return decryptedCharacterList; + return { characters: decryptedCharacterList, enabled }; } /** @@ -358,4 +368,5 @@ export default class Character { }).join('\n\n'); return formattedCharactersDescription; } + } diff --git a/electron/database/models/Download.ts b/electron/database/models/Download.ts index 224601b..c7a054c 100644 --- a/electron/database/models/Download.ts +++ b/electron/database/models/Download.ts @@ -1,7 +1,7 @@ import {getUserEncryptionKey} from "../keyManager.js"; import System from "../System.js"; import {CompleteBook} from "./Book.js"; -import BookRepo, {EritBooksTable} from "../repositories/book.repository.js"; +import BookRepo, {EritBooksTable, BookToolsTable} from "../repositories/book.repository.js"; import ChapterRepo, { BookChapterInfosTable, BookChaptersTable @@ -192,9 +192,14 @@ export default class Download { }); if (!guidelinesInserted) return false; - return data.issues.every((issue: BookIssuesTable): boolean => { + const issuesInserted: boolean = data.issues.every((issue: BookIssuesTable): boolean => { const encryptedIssueName: string = System.encryptDataWithUserKey(issue.name, userEncryptionKey); return IssueRepository.insertSyncIssue(issue.issue_id, userId, issue.book_id, encryptedIssueName, issue.hashed_issue_name, issue.last_update, lang); }); + 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, lang); + }); } } diff --git a/electron/database/models/Location.ts b/electron/database/models/Location.ts index a0fe42a..e0634e5 100644 --- a/electron/database/models/Location.ts +++ b/electron/database/models/Location.ts @@ -5,6 +5,7 @@ import LocationRepo, { } from "../repositories/location.repository.js"; import System from "../System.js"; import {getUserEncryptionKey} from "../keyManager.js"; +import BookRepo, {BookToolsTable} from "../repositories/book.repository.js"; export interface SubElement { id: string; @@ -25,6 +26,11 @@ export interface LocationProps { elements: Element[]; } +export interface LocationListResponse { + locations: LocationProps[]; + enabled: boolean; +} + export interface SyncedLocation { id: string; name: string; @@ -51,11 +57,16 @@ export default class Location { * @param userId - The user's unique identifier. * @param bookId - The book's unique identifier. * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @returns An array of location properties with their elements and sub-elements. + * @returns LocationListResponse containing an array of locations and enabled flag. */ - static getAllLocations(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LocationProps[] { + static getAllLocations(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LocationListResponse { + const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang); + const enabled: boolean = bookTools ? bookTools.locations_enabled === 1 : false; + const locationRecords: LocationQueryResult[] = LocationRepo.getLocation(userId, bookId, lang); - if (!locationRecords || locationRecords.length === 0) return []; + if (!locationRecords || locationRecords.length === 0) { + return { locations: [], enabled }; + } const userKey: string = getUserEncryptionKey(userId); const locationArray: LocationProps[] = []; @@ -104,7 +115,7 @@ export default class Location { } } } - return locationArray; + return { locations: locationArray, enabled }; } /** @@ -325,4 +336,5 @@ export default class Location { return descriptionFields.join('\n'); }).join('\n\n'); } + } diff --git a/electron/database/models/Sync.ts b/electron/database/models/Sync.ts index 88625dc..ec44cab 100644 --- a/electron/database/models/Sync.ts +++ b/electron/database/models/Sync.ts @@ -1,7 +1,7 @@ import { getUserEncryptionKey } from "../keyManager.js"; import System from "../System.js"; -import { BookSyncCompare, CompleteBook, SyncedBook } from "./Book.js"; -import BookRepo, { EritBooksTable, SyncedBookResult } from "../repositories/book.repository.js"; +import { BookSyncCompare, CompleteBook, SyncedBook, SyncedBookTools } from "./Book.js"; +import BookRepo, { EritBooksTable, SyncedBookResult, BookToolsTable } from "../repositories/book.repository.js"; import ChapterRepo, { BookChapterInfosTable, BookChaptersTable, @@ -350,6 +350,9 @@ export default class Sync { }); } + const bookToolsResult: BookToolsTable | null = BookRepo.fetchBookTools(userId, syncCompareData.id, lang); + const bookTools: BookToolsTable[] = bookToolsResult ? [bookToolsResult] : []; + return { eritBooks: decryptedBooks, chapters: decryptedChapters, @@ -367,7 +370,8 @@ export default class Sync { actSummaries: decryptedActSummaries, guideLine: decryptedGuideLines, aiGuideLine: decryptedAIGuideLines, - issues: decryptedIssues + issues: decryptedIssues, + bookTools: bookTools }; } @@ -724,6 +728,23 @@ export default class Sync { } } + const serverBookTools: BookToolsTable[] = completeBook.bookTools; + if (serverBookTools && serverBookTools.length > 0) { + for (const serverBookTool of serverBookTools) { + const bookToolsExists: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang); + if (bookToolsExists) { + BookRepo.updateBookToolSetting(userId, bookId, 'characters_enabled', serverBookTool.characters_enabled === 1, lang); + BookRepo.updateBookToolSetting(userId, bookId, 'worlds_enabled', serverBookTool.worlds_enabled === 1, lang); + BookRepo.updateBookToolSetting(userId, bookId, 'locations_enabled', serverBookTool.locations_enabled === 1, lang); + } else { + const insertSuccessful: boolean = BookRepo.insertSyncBookTools(bookId, userId, serverBookTool.characters_enabled, serverBookTool.worlds_enabled, serverBookTool.locations_enabled, lang); + if (!insertSuccessful) { + return false; + } + } + } + } + return true; } @@ -940,6 +961,13 @@ export default class Sync { lastUpdate: aiGuidelineRecord.last_update } : null; + const bookToolsRecord: BookToolsTable | null = BookRepo.fetchBookTools(userId, currentBookId, lang); + const bookTools: SyncedBookTools | null = bookToolsRecord ? { + charactersEnabled: bookToolsRecord.characters_enabled === 1, + worldsEnabled: bookToolsRecord.worlds_enabled === 1, + locationsEnabled: bookToolsRecord.locations_enabled === 1 + } : null; + return { id: currentBookId, type: bookRecord.type, @@ -955,7 +983,8 @@ export default class Sync { issues: bookIssues, actSummaries: bookActSummaries, guideLine: bookGuideLine, - aiGuideLine: bookAIGuideLine + aiGuideLine: bookAIGuideLine, + bookTools: bookTools }; }); } diff --git a/electron/database/models/Upload.ts b/electron/database/models/Upload.ts index c62ec49..df80d6d 100644 --- a/electron/database/models/Upload.ts +++ b/electron/database/models/Upload.ts @@ -1,7 +1,7 @@ import { getUserEncryptionKey } from "../keyManager.js"; import System from "../System.js"; import { CompleteBook } from "./Book.js"; -import BookRepo, { EritBooksTable } from "../repositories/book.repository.js"; +import BookRepo, { EritBooksTable, BookToolsTable } from "../repositories/book.repository.js"; import ActRepository, { BookActSummariesTable } from "../repositories/act.repository.js"; import GuidelineRepo, { BookAIGuideLineTable, BookGuideLineTable } from "../repositories/guideline.repository.js"; import ChapterRepo, { @@ -51,7 +51,8 @@ export default class Upload { encryptedIssues, encryptedLocations, encryptedPlotPoints, - encryptedWorlds + encryptedWorlds, + bookToolsData ]: [ EritBooksTable[], BookActSummariesTable[], @@ -63,7 +64,8 @@ export default class Upload { BookIssuesTable[], BookLocationTable[], BookPlotPointsTable[], - BookWorldTable[] + BookWorldTable[], + BookToolsTable | null ] = await Promise.all([ BookRepo.fetchEritBooksTable(userId, bookId, lang), ActRepository.fetchBookActSummaries(userId, bookId, lang), @@ -75,7 +77,8 @@ export default class Upload { IssueRepository.fetchBookIssues(userId, bookId, lang), LocationRepo.fetchBookLocations(userId, bookId, lang), PlotPointRepository.fetchBookPlotPoints(userId, bookId, lang), - WorldRepository.fetchBookWorlds(userId, bookId, lang) + WorldRepository.fetchBookWorlds(userId, bookId, lang), + BookRepo.fetchBookTools(userId, bookId, lang) ]); const [ @@ -234,6 +237,8 @@ export default class Upload { sub_elem_description: locationSubElement.sub_elem_description ? System.decryptDataWithUserKey(locationSubElement.sub_elem_description, userEncryptionKey) : null })); + const bookTools: BookToolsTable[] = bookToolsData ? [bookToolsData] : []; + return { eritBooks, actSummaries, @@ -251,7 +256,8 @@ export default class Upload { worlds, worldElements, locationElements, - locationSubElements + locationSubElements, + bookTools }; } } diff --git a/electron/database/models/World.ts b/electron/database/models/World.ts index 96caf4b..06188b7 100644 --- a/electron/database/models/World.ts +++ b/electron/database/models/World.ts @@ -1,6 +1,7 @@ import { getUserEncryptionKey } from "../keyManager.js"; import System from "../System.js"; import WorldRepository, { WorldElementValue, WorldQuery } from "../repositories/world.repository.js"; +import BookRepo, {BookToolsTable} from "../repositories/book.repository.js"; export interface SyncedWorld { id: string; @@ -44,6 +45,11 @@ export interface WorldProps { importantCharacters: WorldElement[]; } +export interface WorldListResponse { + worlds: WorldProps[]; + enabled: boolean; +} + /** * Mapping of element type keys to their corresponding numeric type identifiers. */ @@ -107,9 +113,12 @@ export default class World { * @param userId - The unique identifier of the user * @param bookId - The unique identifier of the book * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr' - * @returns An array of WorldProps objects containing all world data and their elements + * @returns WorldListResponse containing an array of WorldProps and enabled flag */ - public static getWorlds(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): WorldProps[] { + public static getWorlds(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): WorldListResponse { + const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang); + const enabled: boolean = bookTools ? bookTools.worlds_enabled === 1 : false; + const worldQueryResults: WorldQuery[] = WorldRepository.fetchWorlds(userId, bookId, lang); const userEncryptionKey: string = getUserEncryptionKey(userId); const worlds: WorldProps[] = []; @@ -167,7 +176,7 @@ export default class World { } } } - return worlds; + return { worlds, enabled }; } /** @@ -265,4 +274,5 @@ export default class World { public static removeElementFromWorld(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { return WorldRepository.deleteElement(userId, elementId, lang); } + } diff --git a/electron/database/repositories/book.repository.ts b/electron/database/repositories/book.repository.ts index 7dcf4f3..51897b6 100644 --- a/electron/database/repositories/book.repository.ts +++ b/electron/database/repositories/book.repository.ts @@ -46,6 +46,20 @@ export interface BookCoverQuery extends Record { cover_image: string; } +export interface BookToolsTable extends Record { + book_id: string; + user_id: string; + characters_enabled: number; + worlds_enabled: number; + locations_enabled: number; +} + +export interface BookToolsSettings { + characters: boolean; + worlds: boolean; + locations: boolean; +} + export default class BookRepo { /** * Retrieves all books for a user. @@ -361,4 +375,70 @@ export default class BookRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } + + 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 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; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? 'Impossible de récupérer les paramètres des outils.' : 'Unable to fetch tools settings.'); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled', enabled: boolean, lang: 'fr' | 'en'): boolean { + const enabledValue: number = enabled ? 1 : 0; + try { + const db: Database = System.getDb(); + const updateQuery: string = `UPDATE book_tools SET ${toolName}=? WHERE user_id=? AND book_id=?`; + const updateResult: RunResult = db.run(updateQuery, [enabledValue, userId, bookId]); + if (updateResult.changes > 0) { + return true; + } + 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) VALUES (?, ?, ?, ?, ?)'; + const insertResult: RunResult = db.run(insertQuery, [bookId, userId, charactersValue, worldsValue, locationsValue]); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? 'Impossible de mettre à jour les paramètres des outils.' : 'Unable to update tools settings.'); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + /** + * Inserts book tools settings during sync. + * @param bookId - The book identifier + * @param userId - The user identifier + * @param charactersEnabled - Whether characters tool is enabled + * @param worldsEnabled - Whether worlds tool is enabled + * @param locationsEnabled - Whether locations tool is enabled + * @param lang - The language for error messages + * @returns true if the insertion was successful + */ + static insertSyncBookTools(bookId: string, userId: string, charactersEnabled: number, worldsEnabled: number, locationsEnabled: 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) VALUES (?, ?, ?, ?, ?)'; + const params: SQLiteValue[] = [bookId, userId, charactersEnabled, worldsEnabled, locationsEnabled]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? "Impossible d'insérer les paramètres des outils." : 'Unable to insert tools settings.'); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } } \ No newline at end of file diff --git a/electron/database/schema.ts b/electron/database/schema.ts index c27b68d..37675b2 100644 --- a/electron/database/schema.ts +++ b/electron/database/schema.ts @@ -8,7 +8,7 @@ type Database = sqlite3.Database; * Data is encrypted before storage and decrypted on retrieval */ -export const SCHEMA_VERSION = 2; +export const SCHEMA_VERSION = 3; /** * Initialize the local SQLite database with all required tables @@ -412,6 +412,19 @@ export function initializeSchema(db: Database): void { ); `); + // Book Tools + db.exec(` + CREATE TABLE IF NOT EXISTS book_tools ( + book_id TEXT NOT NULL, + user_id TEXT NOT NULL, + characters_enabled INTEGER NOT NULL DEFAULT 0, + worlds_enabled INTEGER NOT NULL DEFAULT 0, + locations_enabled INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (book_id, user_id), + FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE + ); + `); + // Create indexes for better performance createIndexes(db); @@ -574,6 +587,20 @@ export function runMigrations(db: Database): void { `, 'chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update'); } + if (currentVersion < 3) { + db.exec(` + CREATE TABLE IF NOT EXISTS book_tools ( + book_id TEXT NOT NULL, + user_id TEXT NOT NULL, + characters_enabled INTEGER NOT NULL DEFAULT 0, + worlds_enabled INTEGER NOT NULL DEFAULT 0, + locations_enabled INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (book_id, user_id), + FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE + ); + `); + } + // Update schema version setDbSchemaVersion(db, SCHEMA_VERSION); } diff --git a/electron/ipc/book.ipc.ts b/electron/ipc/book.ipc.ts index 54f6cbe..bafc4db 100644 --- a/electron/ipc/book.ipc.ts +++ b/electron/ipc/book.ipc.ts @@ -111,6 +111,12 @@ interface UpdateWorldData { world: WorldProps; } +interface UpdateBookToolData { + bookId: string; + toolName: 'characters' | 'worlds' | 'locations'; + enabled: boolean; +} + // GET /books - Get all books ipcMain.handle('db:book:books', createHandler( async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise { @@ -412,3 +418,11 @@ ipcMain.handle('db:book:world:update', createHandler( } ) ); + +// PATCH /book/tool-setting - Update book tool setting +ipcMain.handle('db:book:tool:update', createHandler( + function(userId: string, data: UpdateBookToolData, lang: 'fr' | 'en') { + return Book.updateBookToolSetting(userId, data.bookId, data.toolName, data.enabled, lang); + } + ) +); diff --git a/lib/locales/en.json b/lib/locales/en.json index 1fcd0ec..ac12577 100644 --- a/lib/locales/en.json +++ b/lib/locales/en.json @@ -354,7 +354,11 @@ "languagesPlaceholder": "Create your own language or simply mention those available.", "updateWorldError": "Failed to update:", "addWorldError": "Error adding world.", - "updateWorldSuccess": "World updated successfully." + "updateWorldSuccess": "World updated successfully.", + "enableTool": "Enable worlds", + "enableToolDescription": "Enable world management for this book.", + "toolEnabled": "World management enabled.", + "toolDisabled": "World management disabled." }, "locationComponent": { "newSectionPlaceholder": "New section name", @@ -387,7 +391,11 @@ "errorSave": "An error occurred while saving the locations.", "errorUnknownSave": "Unable to save changes. Please try again later.", "errorUnknownFetchLocations": "Unknown error fetching locations.", - "successSave": "Locations saved successfully." + "successSave": "Locations saved successfully.", + "enableTool": "Enable locations", + "enableToolDescription": "Enable location management for this book.", + "toolEnabled": "Location management enabled.", + "toolDisabled": "Location management disabled." }, "characterComponent": { "errorNameRequired": "Character name is required.", @@ -397,7 +405,11 @@ "errorAddCharacter": "Error adding character.", "errorUpdateCharacter": "Error updating character.", "errorAddAttribute": "Error adding attribute.", - "errorRemoveAttribute": "Error removing attribute." + "errorRemoveAttribute": "Error removing attribute.", + "enableTool": "Enable characters", + "enableToolDescription": "Enable character management for this book.", + "toolEnabled": "Character management enabled.", + "toolDisabled": "Character management disabled." }, "characterDetail": { "back": "Back", @@ -1013,6 +1025,9 @@ "errorSave": "Error saving settings.", "errorUnknown": "An unknown error occurred.", "successSave": "QuillSense settings saved successfully.", - "noBookSelected": "No book selected." + "noBookSelected": "No book selected.", + "enable_characters": "Enable character management for this book", + "enable_worlds": "Enable world management for this book", + "enable_locations": "Enable location management for this book" } } \ No newline at end of file diff --git a/lib/locales/fr.json b/lib/locales/fr.json index 45da365..786920d 100644 --- a/lib/locales/fr.json +++ b/lib/locales/fr.json @@ -354,7 +354,11 @@ "languagesPlaceholder": "Créez votre propre langue ou mentionnez simplement celles disponibles.", "updateWorldError": "Échec de la mise à jour :", "addWorldError": "Erreur lors de l'ajout du monde.", - "updateWorldSuccess": "Monde mis à jour avec succès." + "updateWorldSuccess": "Monde mis à jour avec succès.", + "enableTool": "Activer les mondes", + "enableToolDescription": "Activer la gestion des mondes pour ce livre.", + "toolEnabled": "Gestion des mondes activée.", + "toolDisabled": "Gestion des mondes désactivée." }, "locationComponent": { "newSectionPlaceholder": "Nom de la nouvelle section", @@ -387,7 +391,11 @@ "errorSave": "Une erreur est survenue lors de la sauvegarde des emplacements.", "errorUnknownSave": "Impossible de sauvegarder les modifications. Veuillez réessayer ultérieurement.", "errorUnknownFetchLocations": "Erreur inconnue lors de la récupération des emplacements.", - "successSave": "Emplacements sauvegardés avec succès." + "successSave": "Emplacements sauvegardés avec succès.", + "enableTool": "Activer les lieux", + "enableToolDescription": "Activer la gestion des lieux pour ce livre.", + "toolEnabled": "Gestion des lieux activée.", + "toolDisabled": "Gestion des lieux désactivée." }, "characterComponent": { "errorNameRequired": "Le nom du personnage est requis.", @@ -397,7 +405,11 @@ "errorAddCharacter": "Erreur lors de l'ajout du personnage.", "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." + "errorRemoveAttribute": "Erreur lors de la suppression de l'attribut.", + "enableTool": "Activer les personnages", + "enableToolDescription": "Activer la gestion des personnages pour ce livre.", + "toolEnabled": "Gestion des personnages activée.", + "toolDisabled": "Gestion des personnages désactivée." }, "characterDetail": { "back": "Retour", @@ -1014,6 +1026,9 @@ "errorSave": "Erreur lors de la sauvegarde des paramètres.", "errorUnknown": "Une erreur inconnue est survenue.", "successSave": "Paramètres QuillSense sauvegardés avec succès.", - "noBookSelected": "Aucun livre sélectionné." + "noBookSelected": "Aucun livre sélectionné.", + "enable_characters": "Activer la gestion des personnages pour ce livre", + "enable_worlds": "Activer la gestion des mondes pour ce livre", + "enable_locations": "Activer la gestion des lieux pour ce livre" } } \ No newline at end of file diff --git a/lib/models/Book.ts b/lib/models/Book.ts index 9522578..6e7bdfc 100644 --- a/lib/models/Book.ts +++ b/lib/models/Book.ts @@ -57,6 +57,12 @@ export interface SyncedBook { aiGuideLine: SyncedAIGuideLine | null; } +export interface BookToolsSettings { + characters: boolean; + worlds: boolean; + locations: boolean; +} + export interface BookProps { bookId: string; type: string; @@ -72,6 +78,7 @@ export interface BookProps { localBook?: boolean; chapters?: ChapterProps[]; quillsenseEnabled?: boolean; + tools?: BookToolsSettings; } export interface BookListProps { diff --git a/lib/models/Character.ts b/lib/models/Character.ts index 5438772..24625c8 100755 --- a/lib/models/Character.ts +++ b/lib/models/Character.ts @@ -163,6 +163,11 @@ export interface CharacterProps { history?: string; } +export interface CharacterListResponse { + characters: CharacterProps[]; + enabled: boolean; +} + export interface CharacterElement { title: string; section: keyof CharacterProps; diff --git a/lib/models/World.ts b/lib/models/World.ts index 4387024..d477a05 100755 --- a/lib/models/World.ts +++ b/lib/models/World.ts @@ -42,6 +42,11 @@ export interface WorldProps { importantCharacters: WorldElement[]; } +export interface WorldListResponse { + worlds: WorldProps[]; + enabled: boolean; +} + export const elementSections: ElementSection[] = [ { title: 'Lois',