Add comprehensive spell management functionality
- Introduced spell management with creation, editing, deletion, and tagging capabilities. - Added `Spell`, `SpellList`, `SpellTagManager` models with corresponding IPC handlers for data operations. - Implemented `SpellList` and `SpellTagChip` components for UI interactions with spells and tags. - Localized spell-related strings for English (e.g., error messages, tooltips, and prompts). - Enhanced database models and repositories with encryption and decryption workflows for secure data handling. - Updated API to include filtering, searching, and tag-based spell management options.
This commit is contained in:
@@ -670,7 +670,7 @@ export default function Scribe() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<LangContext.Provider value={{lang: locale, setLang: setLocale}}>
|
<LangContext.Provider value={{lang: locale, setLang: setLocale}}>
|
||||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
<NextIntlClientProvider locale={locale} messages={messages} timeZone="America/Montreal">
|
||||||
<OfflineProvider>
|
<OfflineProvider>
|
||||||
<AlertProvider>
|
<AlertProvider>
|
||||||
<ScribeContent/>
|
<ScribeContent/>
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export function LocationComponent({showToggle = true}: {showToggle?: boolean}, r
|
|||||||
setBook({...book, tools: {
|
setBook({...book, tools: {
|
||||||
characters: book.tools?.characters ?? false,
|
characters: book.tools?.characters ?? false,
|
||||||
worlds: book.tools?.worlds ?? false,
|
worlds: book.tools?.worlds ?? false,
|
||||||
|
spells: book.tools?.spells ?? false,
|
||||||
locations: enabled
|
locations: enabled
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
@@ -129,6 +130,7 @@ export function LocationComponent({showToggle = true}: {showToggle?: boolean}, r
|
|||||||
setBook({...book, tools: {
|
setBook({...book, tools: {
|
||||||
characters: book.tools?.characters ?? false,
|
characters: book.tools?.characters ?? false,
|
||||||
worlds: book.tools?.worlds ?? false,
|
worlds: book.tools?.worlds ?? false,
|
||||||
|
spells: book.tools?.spells ?? false,
|
||||||
locations: response.enabled
|
locations: response.enabled
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
|
|||||||
563
components/spells/SpellComponent.tsx
Normal file
563
components/spells/SpellComponent.tsx
Normal file
@@ -0,0 +1,563 @@
|
|||||||
|
'use client';
|
||||||
|
import React, {forwardRef, useContext, useEffect, useImperativeHandle, useState} from 'react';
|
||||||
|
import {
|
||||||
|
initialSpellState,
|
||||||
|
SpellEditState,
|
||||||
|
SpellListItem,
|
||||||
|
SpellListResponse,
|
||||||
|
SpellProps,
|
||||||
|
SpellPropsPost,
|
||||||
|
SpellTagProps
|
||||||
|
} from "@/lib/models/Spell";
|
||||||
|
import {SessionContext} from "@/context/SessionContext";
|
||||||
|
import {AlertContext} from "@/context/AlertContext";
|
||||||
|
import {BookContext} from "@/context/BookContext";
|
||||||
|
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||||
|
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||||
|
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||||
|
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
||||||
|
import {SyncedBook} from "@/lib/models/SyncedBook";
|
||||||
|
import System from '@/lib/models/System';
|
||||||
|
import {useTranslations} from "next-intl";
|
||||||
|
import ToggleSwitch from "@/components/form/ToggleSwitch";
|
||||||
|
import InputField from "@/components/form/InputField";
|
||||||
|
import {faToggleOn} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import SpellList from "@/components/book/settings/spells/SpellList";
|
||||||
|
import SpellDetail from "@/components/book/settings/spells/SpellDetail";
|
||||||
|
import SpellTagManager from "@/components/book/settings/spells/SpellTagManager";
|
||||||
|
|
||||||
|
interface SpellComponentProps {
|
||||||
|
showToggle?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SpellComponent(props: SpellComponentProps, ref: React.Ref<{ handleSave: () => Promise<void> }>) {
|
||||||
|
const {showToggle = true} = props;
|
||||||
|
const t = useTranslations();
|
||||||
|
const {lang} = useContext<LangContextProps>(LangContext);
|
||||||
|
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
|
||||||
|
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||||
|
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||||
|
const {session} = useContext(SessionContext);
|
||||||
|
const {book, setBook} = useContext(BookContext);
|
||||||
|
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||||
|
|
||||||
|
const bookId: string | undefined = book?.bookId;
|
||||||
|
const token: string = session.accessToken;
|
||||||
|
|
||||||
|
const [spells, setSpells] = useState<SpellListItem[]>([]);
|
||||||
|
const [tags, setTags] = useState<SpellTagProps[]>([]);
|
||||||
|
const [selectedSpell, setSelectedSpell] = useState<SpellEditState | null>(null);
|
||||||
|
const [toolEnabled, setToolEnabled] = useState<boolean>(book?.tools?.spells ?? false);
|
||||||
|
const [showTagManager, setShowTagManager] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, function () {
|
||||||
|
return {
|
||||||
|
handleSave: handleSaveSpell,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect((): void => {
|
||||||
|
getSpells().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function handleToggleTool(enabled: boolean): Promise<void> {
|
||||||
|
try {
|
||||||
|
let response: boolean;
|
||||||
|
if (isCurrentlyOffline() || book?.localBook) {
|
||||||
|
response = await window.electron.invoke<boolean>('db:book:tool:update', {
|
||||||
|
bookId: bookId,
|
||||||
|
toolName: 'spells',
|
||||||
|
enabled: enabled
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
response = await System.authPatchToServer<boolean>('book/tool-setting', {
|
||||||
|
bookId: bookId,
|
||||||
|
toolName: 'spells',
|
||||||
|
enabled: enabled
|
||||||
|
}, token, lang);
|
||||||
|
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||||
|
addToQueue('db:book:tool:update', {
|
||||||
|
bookId: bookId,
|
||||||
|
toolName: 'spells',
|
||||||
|
enabled: enabled
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (response && setBook && book) {
|
||||||
|
setToolEnabled(enabled);
|
||||||
|
setBook({
|
||||||
|
...book, tools: {
|
||||||
|
characters: book.tools?.characters ?? false,
|
||||||
|
worlds: book.tools?.worlds ?? false,
|
||||||
|
locations: book.tools?.locations ?? false,
|
||||||
|
spells: enabled
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
errorMessage(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSpells(): Promise<void> {
|
||||||
|
try {
|
||||||
|
let response: SpellListResponse;
|
||||||
|
if (isCurrentlyOffline()) {
|
||||||
|
response = await window.electron.invoke<SpellListResponse>('db:spell:list', {bookid: bookId});
|
||||||
|
} else {
|
||||||
|
if (book?.localBook) {
|
||||||
|
response = await window.electron.invoke<SpellListResponse>('db:spell:list', {bookid: bookId});
|
||||||
|
} else {
|
||||||
|
response = await System.authGetQueryToServer<SpellListResponse>('spell/list', token, lang, {
|
||||||
|
bookid: bookId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (response) {
|
||||||
|
setSpells(response.spells);
|
||||||
|
setTags(response.tags);
|
||||||
|
setToolEnabled(response.enabled);
|
||||||
|
if (setBook && book) {
|
||||||
|
setBook({
|
||||||
|
...book, tools: {
|
||||||
|
characters: book.tools?.characters ?? false,
|
||||||
|
worlds: book.tools?.worlds ?? false,
|
||||||
|
locations: book.tools?.locations ?? false,
|
||||||
|
spells: response.enabled
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
errorMessage(e.message);
|
||||||
|
} else {
|
||||||
|
errorMessage(t("common.unknownError"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSpellClick(spell: SpellListItem): Promise<void> {
|
||||||
|
// Convertir les tags de SpellTagProps[] vers string[] (IDs)
|
||||||
|
const tagIds: string[] = spell.tags.map((tag: SpellTagProps): string => tag.id);
|
||||||
|
|
||||||
|
// D'abord afficher avec les données de la liste
|
||||||
|
setSelectedSpell({
|
||||||
|
id: spell.id,
|
||||||
|
name: spell.name,
|
||||||
|
description: spell.description,
|
||||||
|
appearance: '',
|
||||||
|
tags: tagIds,
|
||||||
|
powerLevel: null,
|
||||||
|
components: null,
|
||||||
|
limitations: null,
|
||||||
|
notes: null,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
let response: SpellProps;
|
||||||
|
if (isCurrentlyOffline()) {
|
||||||
|
response = await window.electron.invoke<SpellProps>('db:spell:detail', {spellid: spell.id});
|
||||||
|
} else {
|
||||||
|
if (book?.localBook) {
|
||||||
|
response = await window.electron.invoke<SpellProps>('db:spell:detail', {spellid: spell.id});
|
||||||
|
} else {
|
||||||
|
response = await System.authGetQueryToServer<SpellProps>('spell/detail', token, lang, {
|
||||||
|
spellid: spell.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (response) {
|
||||||
|
setSelectedSpell((prev: SpellEditState | null): SpellEditState | null => {
|
||||||
|
if (!prev) return null;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
appearance: response.appearance,
|
||||||
|
powerLevel: response.powerLevel,
|
||||||
|
components: response.components,
|
||||||
|
limitations: response.limitations,
|
||||||
|
notes: response.notes,
|
||||||
|
// Garder les tags de la liste, pas ceux de l'API
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
errorMessage(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddSpell(): void {
|
||||||
|
setSelectedSpell({...initialSpellState});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSpellChange(key: keyof SpellEditState, value: string | string[] | null): void {
|
||||||
|
if (selectedSpell) {
|
||||||
|
setSelectedSpell({...selectedSpell, [key]: value});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveSpell(): Promise<void> {
|
||||||
|
if (selectedSpell) {
|
||||||
|
if (selectedSpell.id === null) {
|
||||||
|
await addNewSpell(selectedSpell);
|
||||||
|
} else {
|
||||||
|
await updateSpell(selectedSpell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addNewSpell(spell: SpellEditState): Promise<void> {
|
||||||
|
if (!spell.name) {
|
||||||
|
errorMessage(t("spellComponent.errorNameRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!spell.description) {
|
||||||
|
errorMessage(t("spellComponent.errorDescriptionRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!spell.appearance) {
|
||||||
|
errorMessage(t("spellComponent.errorAppearanceRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const spellPost: SpellPropsPost = {
|
||||||
|
name: spell.name,
|
||||||
|
description: spell.description,
|
||||||
|
appearance: spell.appearance,
|
||||||
|
tags: spell.tags,
|
||||||
|
powerLevel: spell.powerLevel,
|
||||||
|
components: spell.components,
|
||||||
|
limitations: spell.limitations,
|
||||||
|
notes: spell.notes,
|
||||||
|
};
|
||||||
|
let spellId: string;
|
||||||
|
if (isCurrentlyOffline() || book?.localBook) {
|
||||||
|
spellId = await window.electron.invoke<string>('db:spell:create', {
|
||||||
|
bookId: bookId,
|
||||||
|
spell: spellPost,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const createdSpell: SpellProps = await System.authPostToServer<SpellProps>('spell/add', {
|
||||||
|
bookId: bookId,
|
||||||
|
spell: spellPost,
|
||||||
|
}, token, lang);
|
||||||
|
if (!createdSpell || !createdSpell.id) {
|
||||||
|
errorMessage(t("spellComponent.errorAddSpell"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
spellId = createdSpell.id;
|
||||||
|
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||||
|
addToQueue('db:spell:create', {
|
||||||
|
bookId: bookId,
|
||||||
|
spell: {...spellPost, id: spellId},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!spellId) {
|
||||||
|
errorMessage(t("spellComponent.errorAddSpell"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ajouter à la liste avec les tags résolus
|
||||||
|
const resolvedTags: SpellTagProps[] = tags.filter((tag: SpellTagProps) => spell.tags.includes(tag.id));
|
||||||
|
const newSpellListItem: SpellListItem = {
|
||||||
|
id: spellId,
|
||||||
|
name: spell.name,
|
||||||
|
description: spell.description.length > 150
|
||||||
|
? spell.description.substring(0, 150) + '...'
|
||||||
|
: spell.description,
|
||||||
|
tags: resolvedTags,
|
||||||
|
};
|
||||||
|
setSpells([...spells, newSpellListItem]);
|
||||||
|
setSelectedSpell(null);
|
||||||
|
successMessage(t("spellComponent.successAdd"));
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
errorMessage(e.message);
|
||||||
|
} else {
|
||||||
|
errorMessage(t("common.unknownError"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateSpell(spell: SpellEditState): Promise<void> {
|
||||||
|
if (!spell.id) return;
|
||||||
|
if (!spell.name) {
|
||||||
|
errorMessage(t("spellComponent.errorNameRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!spell.description) {
|
||||||
|
errorMessage(t("spellComponent.errorDescriptionRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!spell.appearance) {
|
||||||
|
errorMessage(t("spellComponent.errorAppearanceRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const spellPost: SpellPropsPost = {
|
||||||
|
name: spell.name,
|
||||||
|
description: spell.description,
|
||||||
|
appearance: spell.appearance,
|
||||||
|
tags: spell.tags,
|
||||||
|
powerLevel: spell.powerLevel,
|
||||||
|
components: spell.components,
|
||||||
|
limitations: spell.limitations,
|
||||||
|
notes: spell.notes,
|
||||||
|
};
|
||||||
|
let response: boolean;
|
||||||
|
if (isCurrentlyOffline() || book?.localBook) {
|
||||||
|
response = await window.electron.invoke<boolean>('db:spell:update', {
|
||||||
|
spellId: spell.id,
|
||||||
|
spell: spellPost,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
response = await System.authPutToServer<boolean>('spell/update', {
|
||||||
|
spellId: spell.id,
|
||||||
|
spell: spellPost,
|
||||||
|
}, token, lang);
|
||||||
|
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||||
|
addToQueue('db:spell:update', {
|
||||||
|
spellId: spell.id,
|
||||||
|
spell: spellPost,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!response) {
|
||||||
|
errorMessage(t("spellComponent.errorUpdateSpell"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Mettre à jour la liste avec les tags résolus
|
||||||
|
const resolvedTags: SpellTagProps[] = tags.filter((tag: SpellTagProps) => spell.tags.includes(tag.id));
|
||||||
|
setSpells(spells.map((s: SpellListItem): SpellListItem =>
|
||||||
|
s.id === spell.id ? {
|
||||||
|
id: spell.id,
|
||||||
|
name: spell.name,
|
||||||
|
description: spell.description.length > 150
|
||||||
|
? spell.description.substring(0, 150) + '...'
|
||||||
|
: spell.description,
|
||||||
|
tags: resolvedTags,
|
||||||
|
} : s
|
||||||
|
));
|
||||||
|
setSelectedSpell(null);
|
||||||
|
successMessage(t("spellComponent.successUpdate"));
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
errorMessage(e.message);
|
||||||
|
} else {
|
||||||
|
errorMessage(t("common.unknownError"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteSpell(spellId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
let response: boolean;
|
||||||
|
if (isCurrentlyOffline() || book?.localBook) {
|
||||||
|
response = await window.electron.invoke<boolean>('db:spell:delete', {
|
||||||
|
spellId: spellId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
response = await System.authDeleteToServer<boolean>('spell/delete', {
|
||||||
|
spellId: spellId,
|
||||||
|
}, token, lang);
|
||||||
|
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||||
|
addToQueue('db:spell:delete', {
|
||||||
|
spellId: spellId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!response) {
|
||||||
|
errorMessage(t("spellComponent.errorDeleteSpell"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSpells(spells.filter((s: SpellListItem) => s.id !== spellId));
|
||||||
|
setSelectedSpell(null);
|
||||||
|
successMessage(t("spellComponent.successDelete"));
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
errorMessage(e.message);
|
||||||
|
} else {
|
||||||
|
errorMessage(t("common.unknownError"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleManageTags(): void {
|
||||||
|
setShowTagManager(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBackFromTagManager(): void {
|
||||||
|
setShowTagManager(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateTag(name: string, color: string): Promise<SpellTagProps | null> {
|
||||||
|
try {
|
||||||
|
let tagId: string;
|
||||||
|
if (isCurrentlyOffline() || book?.localBook) {
|
||||||
|
tagId = await window.electron.invoke<string>('db:spell:tag:create', {
|
||||||
|
bookId: bookId,
|
||||||
|
name: name,
|
||||||
|
color: color,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const newTag: SpellTagProps = await System.authPostToServer<SpellTagProps>('spell/tag/add', {
|
||||||
|
bookId: bookId,
|
||||||
|
name: name,
|
||||||
|
color: color,
|
||||||
|
}, token, lang);
|
||||||
|
if (!newTag || !newTag.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
tagId = newTag.id;
|
||||||
|
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||||
|
addToQueue('db:spell:tag:create', {
|
||||||
|
bookId: bookId,
|
||||||
|
name: name,
|
||||||
|
color: color,
|
||||||
|
tagId: tagId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tagId) {
|
||||||
|
const createdTag: SpellTagProps = {id: tagId, name: name, color: color};
|
||||||
|
setTags([...tags, createdTag]);
|
||||||
|
return createdTag;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
errorMessage(e.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdateTag(tagId: string, name: string, color: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
let response: boolean;
|
||||||
|
if (isCurrentlyOffline() || book?.localBook) {
|
||||||
|
response = await window.electron.invoke<boolean>('db:spell:tag:update', {
|
||||||
|
tagId: tagId,
|
||||||
|
name: name,
|
||||||
|
color: color,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
response = await System.authPutToServer<boolean>('spell/tag/update', {
|
||||||
|
tagId: tagId,
|
||||||
|
name: name,
|
||||||
|
color: color,
|
||||||
|
}, token, lang);
|
||||||
|
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||||
|
addToQueue('db:spell:tag:update', {
|
||||||
|
tagId: tagId,
|
||||||
|
name: name,
|
||||||
|
color: color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (response) {
|
||||||
|
setTags(tags.map((tag: SpellTagProps): SpellTagProps =>
|
||||||
|
tag.id === tagId ? {id: tagId, name: name, color: color} : tag
|
||||||
|
));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
errorMessage(e.message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteTag(tagId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
let response: boolean;
|
||||||
|
if (isCurrentlyOffline() || book?.localBook) {
|
||||||
|
response = await window.electron.invoke<boolean>('db:spell:tag:delete', {
|
||||||
|
tagId: tagId,
|
||||||
|
bookId: bookId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
response = await System.authDeleteToServer<boolean>('spell/tag/delete', {
|
||||||
|
tagId: tagId,
|
||||||
|
bookId: bookId,
|
||||||
|
}, token, lang);
|
||||||
|
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||||
|
addToQueue('db:spell:tag:delete', {
|
||||||
|
tagId: tagId,
|
||||||
|
bookId: bookId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (response) {
|
||||||
|
setTags(tags.filter((tag: SpellTagProps): boolean => tag.id !== tagId));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
errorMessage(e.message);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-5">
|
||||||
|
{showToggle && (
|
||||||
|
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
|
||||||
|
<InputField
|
||||||
|
icon={faToggleOn}
|
||||||
|
fieldName={t('spellComponent.enableTool')}
|
||||||
|
input={
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={toolEnabled}
|
||||||
|
onChange={async (checked: boolean): Promise<void> => handleToggleTool(checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<p className="text-muted text-sm mt-2">
|
||||||
|
{t('spellComponent.enableToolDescription')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{toolEnabled && (
|
||||||
|
<>
|
||||||
|
{showTagManager ? (
|
||||||
|
<SpellTagManager
|
||||||
|
tags={tags}
|
||||||
|
onBack={handleBackFromTagManager}
|
||||||
|
onCreateTag={handleCreateTag}
|
||||||
|
onUpdateTag={handleUpdateTag}
|
||||||
|
onDeleteTag={handleDeleteTag}
|
||||||
|
/>
|
||||||
|
) : selectedSpell ? (
|
||||||
|
<SpellDetail
|
||||||
|
selectedSpell={selectedSpell}
|
||||||
|
setSelectedSpell={setSelectedSpell}
|
||||||
|
availableTags={tags}
|
||||||
|
handleSpellChange={handleSpellChange}
|
||||||
|
handleSaveSpell={handleSaveSpell}
|
||||||
|
handleDeleteSpell={handleDeleteSpell}
|
||||||
|
handleCreateTagInline={handleCreateTag}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SpellList
|
||||||
|
spells={spells}
|
||||||
|
tags={tags}
|
||||||
|
handleSpellClick={handleSpellClick}
|
||||||
|
handleAddSpell={handleAddSpell}
|
||||||
|
handleManageTags={handleManageTags}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef(SpellComponent);
|
||||||
325
components/spells/SpellDetail.tsx
Normal file
325
components/spells/SpellDetail.tsx
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
'use client';
|
||||||
|
import React, {Dispatch, SetStateAction, useState} from 'react';
|
||||||
|
import {defaultTagColors, SpellEditState, spellPowerLevels, SpellTagProps} from "@/lib/models/Spell";
|
||||||
|
import {SelectBoxProps} from "@/shared/interface";
|
||||||
|
import CollapsableArea from "@/components/CollapsableArea";
|
||||||
|
import InputField from "@/components/form/InputField";
|
||||||
|
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 {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {
|
||||||
|
faArrowLeft,
|
||||||
|
faBolt,
|
||||||
|
faBook,
|
||||||
|
faEye,
|
||||||
|
faHatWizard,
|
||||||
|
faPlus,
|
||||||
|
faPuzzlePiece,
|
||||||
|
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;
|
||||||
|
setSelectedSpell: Dispatch<SetStateAction<SpellEditState | null>>;
|
||||||
|
availableTags: SpellTagProps[];
|
||||||
|
handleSpellChange: (key: keyof SpellEditState, value: string | string[] | null) => void;
|
||||||
|
handleSaveSpell: () => Promise<void>;
|
||||||
|
handleDeleteSpell: (spellId: string) => Promise<void>;
|
||||||
|
handleCreateTagInline: (name: string, color: string) => Promise<SpellTagProps | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SpellDetail(
|
||||||
|
{
|
||||||
|
selectedSpell,
|
||||||
|
setSelectedSpell,
|
||||||
|
availableTags,
|
||||||
|
handleSpellChange,
|
||||||
|
handleSaveSpell,
|
||||||
|
handleDeleteSpell,
|
||||||
|
handleCreateTagInline,
|
||||||
|
}: SpellDetailProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const [tagSearchQuery, setTagSearchQuery] = useState<string>('');
|
||||||
|
const [showTagDropdown, setShowTagDropdown] = useState<boolean>(false);
|
||||||
|
const [isCreatingTag, setIsCreatingTag] = useState<boolean>(false);
|
||||||
|
const [newTagColor, setNewTagColor] = useState<string>(defaultTagColors[0]);
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
|
||||||
|
|
||||||
|
function handleAddTag(tagId: string): void {
|
||||||
|
if (!selectedSpell.tags.includes(tagId)) {
|
||||||
|
handleSpellChange('tags', [...selectedSpell.tags, tagId]);
|
||||||
|
}
|
||||||
|
setTagSearchQuery('');
|
||||||
|
setShowTagDropdown(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemoveTag(tagId: string): void {
|
||||||
|
handleSpellChange('tags', selectedSpell.tags.filter((id: string) => id !== tagId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilteredAvailableTags(): SpellTagProps[] {
|
||||||
|
return availableTags.filter((tag: SpellTagProps) => {
|
||||||
|
const notAlreadyAdded = !selectedSpell.tags.includes(tag.id);
|
||||||
|
const matchesSearch = tag.name.toLowerCase().includes(tagSearchQuery.toLowerCase());
|
||||||
|
return notAlreadyAdded && matchesSearch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedTags(): SpellTagProps[] {
|
||||||
|
return availableTags.filter((tag: SpellTagProps) => selectedSpell.tags.includes(tag.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCreateTag(): Promise<void> {
|
||||||
|
if (!tagSearchQuery.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newTag: SpellTagProps | null = await handleCreateTagInline(tagSearchQuery.trim(), newTagColor);
|
||||||
|
if (newTag) {
|
||||||
|
handleAddTag(newTag.id);
|
||||||
|
setIsCreatingTag(false);
|
||||||
|
setNewTagColor(defaultTagColors[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocalizedPowerLevels(): SelectBoxProps[] {
|
||||||
|
return spellPowerLevels.map((level: SelectBoxProps): SelectBoxProps => ({
|
||||||
|
value: level.value,
|
||||||
|
label: t(level.label),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredTags = getFilteredAvailableTags();
|
||||||
|
const selectedTags = getSelectedTags();
|
||||||
|
const showCreateOption = tagSearchQuery.trim() && !availableTags.some((tag: SpellTagProps) => tag.name.toLowerCase() === tagSearchQuery.toLowerCase());
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div
|
||||||
|
className="flex justify-between items-center p-4 border-b border-secondary/50 bg-tertiary/50 backdrop-blur-sm">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedSpell(null)}
|
||||||
|
className="flex items-center gap-2 bg-secondary/50 py-2 px-4 rounded-xl border border-secondary/50 hover:bg-secondary hover:border-secondary hover:shadow-md hover:scale-105 transition-all duration-200"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faArrowLeft} className="text-primary w-4 h-4"/>
|
||||||
|
<span className="text-text-primary font-medium">{t("spellDetail.back")}</span>
|
||||||
|
</button>
|
||||||
|
<span className="text-text-primary font-semibold text-lg">
|
||||||
|
{selectedSpell.name || t("spellDetail.newSpell")}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{selectedSpell.id && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} className="text-text-primary w-5 h-5"/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={handleSaveSpell}
|
||||||
|
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={selectedSpell.id ? faSave : faPlus}
|
||||||
|
className="text-text-primary w-5 h-5"/>
|
||||||
|
</button>
|
||||||
|
</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 className="overflow-y-auto max-h-[calc(100vh-350px)] space-y-4 px-2 pb-4">
|
||||||
|
<CollapsableArea title={t("spellDetail.basicInfo")} icon={faHatWizard}>
|
||||||
|
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||||
|
<InputField
|
||||||
|
fieldName={t("spellDetail.name")}
|
||||||
|
input={
|
||||||
|
<TextInput
|
||||||
|
value={selectedSpell.name}
|
||||||
|
setValue={(e) => handleSpellChange('name', e.target.value)}
|
||||||
|
placeholder={t("spellDetail.namePlaceholder")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputField
|
||||||
|
fieldName={t("spellDetail.description")}
|
||||||
|
icon={faBook}
|
||||||
|
input={
|
||||||
|
<TexteAreaInput
|
||||||
|
value={selectedSpell.description}
|
||||||
|
setValue={(e) => handleSpellChange('description', e.target.value)}
|
||||||
|
placeholder={t("spellDetail.descriptionPlaceholder")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputField
|
||||||
|
fieldName={t("spellDetail.appearance")}
|
||||||
|
icon={faEye}
|
||||||
|
input={
|
||||||
|
<TexteAreaInput
|
||||||
|
value={selectedSpell.appearance}
|
||||||
|
setValue={(e) => handleSpellChange('appearance', e.target.value)}
|
||||||
|
placeholder={t("spellDetail.appearancePlaceholder")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CollapsableArea>
|
||||||
|
|
||||||
|
<CollapsableArea title={t("spellDetail.tags")} icon={faTags}>
|
||||||
|
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||||
|
{selectedTags.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
{selectedTags.map((tag: SpellTagProps) => (
|
||||||
|
<SpellTagChip
|
||||||
|
key={tag.id}
|
||||||
|
tag={tag}
|
||||||
|
onRemove={() => handleRemoveTag(tag.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<TextInput
|
||||||
|
value={tagSearchQuery}
|
||||||
|
setValue={(e) => {
|
||||||
|
setTagSearchQuery(e.target.value);
|
||||||
|
setShowTagDropdown(true);
|
||||||
|
}}
|
||||||
|
placeholder={t("spellDetail.addTag")}
|
||||||
|
onFocus={() => setShowTagDropdown(true)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showTagDropdown && (tagSearchQuery || filteredTags.length > 0) && (
|
||||||
|
<div
|
||||||
|
className="absolute z-10 w-full mt-1 bg-tertiary border border-secondary/50 rounded-xl shadow-lg max-h-48 overflow-y-auto">
|
||||||
|
{filteredTags.map((tag: SpellTagProps) => (
|
||||||
|
<button
|
||||||
|
key={tag.id}
|
||||||
|
onClick={() => handleAddTag(tag.id)}
|
||||||
|
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-secondary/50 transition-colors text-left"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="w-3 h-3 rounded-full"
|
||||||
|
style={{backgroundColor: tag.color || '#3B82F6'}}
|
||||||
|
/>
|
||||||
|
<span className="text-text-primary">{tag.name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{showCreateOption && !isCreatingTag && (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsCreatingTag(true)}
|
||||||
|
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-secondary/50 transition-colors text-left border-t border-secondary/30"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} className="text-primary w-3 h-3"/>
|
||||||
|
<span className="text-primary font-medium">
|
||||||
|
{t("spellDetail.createTag", {name: tagSearchQuery})}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isCreatingTag && (
|
||||||
|
<div className="p-4 border-t border-secondary/30">
|
||||||
|
<p className="text-text-secondary text-sm mb-3">
|
||||||
|
{t("spellDetail.createTag", {name: tagSearchQuery})}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
{defaultTagColors.map((color: string) => (
|
||||||
|
<button
|
||||||
|
key={color}
|
||||||
|
onClick={() => setNewTagColor(color)}
|
||||||
|
className={`w-8 h-8 rounded-full transition-all duration-200 ${newTagColor === color ? 'ring-2 ring-offset-2 ring-primary scale-110' : 'hover:scale-110'}`}
|
||||||
|
style={{backgroundColor: color}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsCreatingTag(false)}
|
||||||
|
className="flex-1 py-2 px-3 bg-secondary/50 text-text-primary rounded-lg hover:bg-secondary transition-colors"
|
||||||
|
>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleCreateTag}
|
||||||
|
className="flex-1 py-2 px-3 bg-primary text-text-primary rounded-lg hover:bg-primary-dark transition-colors"
|
||||||
|
>
|
||||||
|
{t("common.confirm")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsableArea>
|
||||||
|
|
||||||
|
<CollapsableArea title={t("spellDetail.powerLevel")} icon={faBolt}>
|
||||||
|
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||||
|
<SelectBox
|
||||||
|
defaultValue={selectedSpell.powerLevel || 'none'}
|
||||||
|
onChangeCallBack={(e) => handleSpellChange('powerLevel', e.target.value === 'none' ? null : e.target.value)}
|
||||||
|
data={getLocalizedPowerLevels()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CollapsableArea>
|
||||||
|
|
||||||
|
<CollapsableArea title={t("spellDetail.components")} icon={faPuzzlePiece}>
|
||||||
|
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||||
|
<TexteAreaInput
|
||||||
|
value={selectedSpell.components || ''}
|
||||||
|
setValue={(e) => handleSpellChange('components', e.target.value || null)}
|
||||||
|
placeholder={t("spellDetail.componentsPlaceholder")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CollapsableArea>
|
||||||
|
|
||||||
|
<CollapsableArea title={t("spellDetail.limitations")} icon={faTriangleExclamation}>
|
||||||
|
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||||
|
<TexteAreaInput
|
||||||
|
value={selectedSpell.limitations || ''}
|
||||||
|
setValue={(e) => handleSpellChange('limitations', e.target.value || null)}
|
||||||
|
placeholder={t("spellDetail.limitationsPlaceholder")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CollapsableArea>
|
||||||
|
|
||||||
|
<CollapsableArea title={t("spellDetail.notes")} icon={faStickyNote}>
|
||||||
|
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||||
|
<TexteAreaInput
|
||||||
|
value={selectedSpell.notes || ''}
|
||||||
|
setValue={(e) => handleSpellChange('notes', e.target.value || null)}
|
||||||
|
placeholder={t("spellDetail.notesPlaceholder")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CollapsableArea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
141
components/spells/SpellList.tsx
Normal file
141
components/spells/SpellList.tsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
'use client';
|
||||||
|
import React, {useState} from 'react';
|
||||||
|
import {SpellListItem, SpellTagProps} from "@/lib/models/Spell";
|
||||||
|
import InputField from "@/components/form/InputField";
|
||||||
|
import TextInput from "@/components/form/TextInput";
|
||||||
|
import SpellTagChip from "@/components/book/settings/spells/SpellTagChip";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {faChevronRight, faCog, faHatWizard, faPlus} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import {useTranslations} from "next-intl";
|
||||||
|
|
||||||
|
interface SpellListProps {
|
||||||
|
spells: SpellListItem[];
|
||||||
|
tags: SpellTagProps[];
|
||||||
|
handleSpellClick: (spell: SpellListItem) => void;
|
||||||
|
handleAddSpell: () => void;
|
||||||
|
handleManageTags: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SpellList(
|
||||||
|
{
|
||||||
|
spells,
|
||||||
|
tags,
|
||||||
|
handleSpellClick,
|
||||||
|
handleAddSpell,
|
||||||
|
handleManageTags,
|
||||||
|
}: SpellListProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
|
const [filterTag, setFilterTag] = useState<string>('all');
|
||||||
|
|
||||||
|
function getFilteredSpells(): SpellListItem[] {
|
||||||
|
return spells.filter((spell: SpellListItem) => {
|
||||||
|
const matchesSearch = spell.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
const matchesTag = filterTag === 'all' || spell.tags.some((tag: SpellTagProps) => tag.id === filterTag);
|
||||||
|
return matchesSearch && matchesTag;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredSpells = getFilteredSpells();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="px-4 space-y-3">
|
||||||
|
<InputField
|
||||||
|
input={
|
||||||
|
<TextInput
|
||||||
|
value={searchQuery}
|
||||||
|
setValue={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder={t("spellList.search")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actionIcon={faPlus}
|
||||||
|
actionLabel={t("spellList.add")}
|
||||||
|
addButtonCallBack={async () => handleAddSpell()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-3 items-center">
|
||||||
|
<div className="flex-1 min-w-[150px]">
|
||||||
|
<select
|
||||||
|
value={filterTag}
|
||||||
|
onChange={(e) => setFilterTag(e.target.value)}
|
||||||
|
className="w-full text-text-primary bg-secondary/50 hover:bg-secondary px-4 py-2.5 rounded-xl border border-secondary/50 focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary hover:border-secondary outline-none transition-all duration-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="all"
|
||||||
|
className="bg-tertiary text-text-primary">{t("spellList.allTags")}</option>
|
||||||
|
{tags.map((tag: SpellTagProps) => (
|
||||||
|
<option key={tag.id} value={tag.id} className="bg-tertiary text-text-primary">
|
||||||
|
{tag.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleManageTags}
|
||||||
|
className="flex items-center gap-2 px-4 py-2.5 bg-secondary/50 rounded-xl border border-secondary/50 hover:bg-secondary hover:border-secondary hover:shadow-md hover:scale-105 transition-all duration-200"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCog} className="text-primary w-4 h-4"/>
|
||||||
|
<span className="text-text-primary text-sm font-medium">{t("spellList.manageTags")}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-y-auto max-h-[calc(100vh-450px)] px-2">
|
||||||
|
{filteredSpells.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<div className="w-20 h-20 rounded-full bg-primary/10 flex items-center justify-center mb-4">
|
||||||
|
<FontAwesomeIcon icon={faHatWizard} className="text-primary w-10 h-10"/>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-text-primary font-semibold text-lg mb-2">{t("spellList.noSpells")}</h3>
|
||||||
|
<p className="text-muted text-sm max-w-xs">{t("spellList.noSpellsDescription")}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2 p-2">
|
||||||
|
{filteredSpells.map((spell: SpellListItem) => (
|
||||||
|
<div
|
||||||
|
key={spell.id}
|
||||||
|
onClick={() => handleSpellClick(spell)}
|
||||||
|
className="group flex items-center p-4 bg-secondary/30 rounded-xl border-l-4 border-primary border border-secondary/50 cursor-pointer hover:bg-secondary hover:shadow-md hover:scale-102 transition-all duration-200 hover:border-primary/50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-12 h-12 rounded-full border-2 border-primary overflow-hidden bg-secondary shadow-md group-hover:scale-110 transition-transform flex items-center justify-center">
|
||||||
|
<FontAwesomeIcon icon={faHatWizard} className="text-primary w-6 h-6"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ml-4 flex-1 min-w-0">
|
||||||
|
<div
|
||||||
|
className="text-text-primary font-bold text-base group-hover:text-primary transition-colors">
|
||||||
|
{spell.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-text-secondary text-sm mt-0.5 truncate">
|
||||||
|
{spell.description}
|
||||||
|
</div>
|
||||||
|
{spell.tags.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
||||||
|
{spell.tags.slice(0, 3).map((tag: SpellTagProps) => (
|
||||||
|
<SpellTagChip key={tag.id} tag={tag} size="sm"/>
|
||||||
|
))}
|
||||||
|
{spell.tags.length > 3 && (
|
||||||
|
<span
|
||||||
|
className="text-muted text-xs px-2 py-0.5 bg-secondary/50 rounded-full">
|
||||||
|
+{spell.tags.length - 3}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-8 flex justify-center">
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faChevronRight}
|
||||||
|
className="text-muted group-hover:text-primary group-hover:translate-x-1 transition-all w-4 h-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
components/spells/SpellTagChip.tsx
Normal file
66
components/spells/SpellTagChip.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
'use client';
|
||||||
|
import React from 'react';
|
||||||
|
import {SpellTagProps} from "@/lib/models/Spell";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {faX} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
interface SpellTagChipProps {
|
||||||
|
tag: SpellTagProps;
|
||||||
|
onRemove?: () => void;
|
||||||
|
onClick?: () => void;
|
||||||
|
size?: 'sm' | 'md';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SpellTagChip(
|
||||||
|
{
|
||||||
|
tag,
|
||||||
|
onRemove,
|
||||||
|
onClick,
|
||||||
|
size = 'md'
|
||||||
|
}: SpellTagChipProps) {
|
||||||
|
|
||||||
|
function getContrastColor(hexColor: string | null): string {
|
||||||
|
if (!hexColor) return '#FFFFFF';
|
||||||
|
const hex = hexColor.replace('#', '');
|
||||||
|
const r = parseInt(hex.substring(0, 2), 16);
|
||||||
|
const g = parseInt(hex.substring(2, 4), 16);
|
||||||
|
const b = parseInt(hex.substring(4, 6), 16);
|
||||||
|
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||||
|
return luminance > 0.5 ? '#1F2023' : '#FFFFFF';
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizeClasses = size === 'sm'
|
||||||
|
? 'px-2 py-0.5 text-xs'
|
||||||
|
: 'px-3 py-1 text-sm';
|
||||||
|
|
||||||
|
const chipClasses = `
|
||||||
|
inline-flex items-center gap-1.5 rounded-full font-medium
|
||||||
|
transition-all duration-200
|
||||||
|
${sizeClasses}
|
||||||
|
${onClick ? 'cursor-pointer hover:scale-105 hover:shadow-md' : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={chipClasses}
|
||||||
|
style={{
|
||||||
|
backgroundColor: tag.color || '#3B82F6',
|
||||||
|
color: getContrastColor(tag.color)
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
{onRemove && (
|
||||||
|
<button
|
||||||
|
onClick={(e: React.MouseEvent): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemove();
|
||||||
|
}}
|
||||||
|
className="ml-0.5 hover:bg-white/20 rounded-full p-0.5 transition-all duration-200 hover:scale-110"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faX} className={size === 'sm' ? 'w-2 h-2' : 'w-2.5 h-2.5'}/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
250
components/spells/SpellTagManager.tsx
Normal file
250
components/spells/SpellTagManager.tsx
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
'use client';
|
||||||
|
import React, {useContext, useState} from 'react';
|
||||||
|
import {defaultTagColors, SpellTagProps} from "@/lib/models/Spell";
|
||||||
|
import SpellTagChip from "@/components/book/settings/spells/SpellTagChip";
|
||||||
|
import InputField from "@/components/form/InputField";
|
||||||
|
import TextInput from "@/components/form/TextInput";
|
||||||
|
import Modal from "@/components/Modal";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {faArrowLeft, faEdit, faPlus, faTags, faTrash} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import {useTranslations} from "next-intl";
|
||||||
|
import {AlertContext} from "@/context/AlertContext";
|
||||||
|
|
||||||
|
interface SpellTagManagerProps {
|
||||||
|
tags: SpellTagProps[];
|
||||||
|
onBack: () => void;
|
||||||
|
onCreateTag: (name: string, color: string) => Promise<SpellTagProps | null>;
|
||||||
|
onUpdateTag: (tagId: string, name: string, color: string) => Promise<boolean>;
|
||||||
|
onDeleteTag: (tagId: string) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SpellTagManager(
|
||||||
|
{
|
||||||
|
tags,
|
||||||
|
onBack,
|
||||||
|
onCreateTag,
|
||||||
|
onUpdateTag,
|
||||||
|
onDeleteTag,
|
||||||
|
}: SpellTagManagerProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const {successMessage} = useContext(AlertContext);
|
||||||
|
|
||||||
|
const [newTagName, setNewTagName] = useState<string>('');
|
||||||
|
const [newTagColor, setNewTagColor] = useState<string>(defaultTagColors[0]);
|
||||||
|
const [editingTag, setEditingTag] = useState<SpellTagProps | null>(null);
|
||||||
|
const [editTagName, setEditTagName] = useState<string>('');
|
||||||
|
const [editTagColor, setEditTagColor] = useState<string>('');
|
||||||
|
const [showEditModal, setShowEditModal] = useState<boolean>(false);
|
||||||
|
const [tagToDelete, setTagToDelete] = useState<SpellTagProps | null>(null);
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
|
||||||
|
|
||||||
|
async function handleAddTag(): Promise<void> {
|
||||||
|
if (!newTagName.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newTag: SpellTagProps | null = await onCreateTag(newTagName.trim(), newTagColor);
|
||||||
|
if (newTag) {
|
||||||
|
setNewTagName('');
|
||||||
|
setNewTagColor(defaultTagColors[0]);
|
||||||
|
successMessage(t("spellTagManager.successAdd"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditClick(tag: SpellTagProps): void {
|
||||||
|
setEditingTag(tag);
|
||||||
|
setEditTagName(tag.name);
|
||||||
|
setEditTagColor(tag.color || defaultTagColors[0]);
|
||||||
|
setShowEditModal(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpdateTag(): Promise<void> {
|
||||||
|
if (!editingTag || !editTagName.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const success: boolean = await onUpdateTag(editingTag.id, editTagName.trim(), editTagColor);
|
||||||
|
if (success) {
|
||||||
|
setShowEditModal(false);
|
||||||
|
setEditingTag(null);
|
||||||
|
successMessage(t("spellTagManager.successUpdate"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteClick(tag: SpellTagProps): void {
|
||||||
|
setTagToDelete(tag);
|
||||||
|
setShowDeleteConfirm(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeleteTag(): Promise<void> {
|
||||||
|
if (!tagToDelete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const success: boolean = await onDeleteTag(tagToDelete.id);
|
||||||
|
if (success) {
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
setTagToDelete(null);
|
||||||
|
successMessage(t("spellTagManager.successDelete"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div
|
||||||
|
className="flex justify-between items-center p-4 border-b border-secondary/50 bg-tertiary/50 backdrop-blur-sm">
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="flex items-center gap-2 bg-secondary/50 py-2 px-4 rounded-xl border border-secondary/50 hover:bg-secondary hover:border-secondary hover:shadow-md hover:scale-105 transition-all duration-200"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faArrowLeft} className="text-primary w-4 h-4"/>
|
||||||
|
<span className="text-text-primary font-medium">{t("spellTagManager.back")}</span>
|
||||||
|
</button>
|
||||||
|
<span className="text-text-primary font-semibold text-lg flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faTags} className="text-primary w-5 h-5"/>
|
||||||
|
{t("spellTagManager.title")}
|
||||||
|
</span>
|
||||||
|
<div className="w-24"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 space-y-4">
|
||||||
|
<div className="bg-secondary/20 rounded-xl p-4 border border-secondary/30">
|
||||||
|
<h3 className="text-text-primary font-semibold mb-3">{t("spellTagManager.addTag")}</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<InputField
|
||||||
|
fieldName={t("spellTagManager.tagName")}
|
||||||
|
input={
|
||||||
|
<TextInput
|
||||||
|
value={newTagName}
|
||||||
|
setValue={(e) => setNewTagName(e.target.value)}
|
||||||
|
placeholder={t("spellTagManager.tagNamePlaceholder")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className="text-text-primary text-sm font-medium mb-2 block">{t("spellTagManager.tagColor")}</span>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{defaultTagColors.map((color: string) => (
|
||||||
|
<button
|
||||||
|
key={color}
|
||||||
|
onClick={() => setNewTagColor(color)}
|
||||||
|
className={`w-10 h-10 rounded-full transition-all duration-200 ${newTagColor === color ? 'ring-2 ring-offset-2 ring-primary scale-110' : 'hover:scale-110'}`}
|
||||||
|
style={{backgroundColor: color}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleAddTag}
|
||||||
|
disabled={!newTagName.trim()}
|
||||||
|
className="w-full flex items-center justify-center gap-2 py-2.5 bg-primary text-text-primary rounded-xl hover:bg-primary-dark transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} className="w-4 h-4"/>
|
||||||
|
{t("spellTagManager.addTag")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-y-auto max-h-[calc(100vh-500px)]">
|
||||||
|
{tags.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||||
|
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mb-4">
|
||||||
|
<FontAwesomeIcon icon={faTags} className="text-primary w-8 h-8"/>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted text-sm">{t("spellTagManager.noTags")}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{tags.map((tag: SpellTagProps) => (
|
||||||
|
<div
|
||||||
|
key={tag.id}
|
||||||
|
className="flex items-center justify-between p-3 bg-secondary/30 rounded-xl border border-secondary/50"
|
||||||
|
>
|
||||||
|
<SpellTagChip tag={tag}/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => handleEditClick(tag)}
|
||||||
|
className="p-2 bg-secondary/50 rounded-lg hover:bg-secondary hover:scale-110 transition-all duration-200"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEdit} className="text-primary w-4 h-4"/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDeleteClick(tag)}
|
||||||
|
className="p-2 bg-error/10 rounded-lg hover:bg-error/20 hover:scale-110 transition-all duration-200"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} className="text-error w-4 h-4"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showEditModal && editingTag && (
|
||||||
|
<Modal
|
||||||
|
title={t("spellTagManager.editTag")}
|
||||||
|
size="small"
|
||||||
|
onClose={() => setShowEditModal(false)}
|
||||||
|
onConfirm={handleUpdateTag}
|
||||||
|
confirmText={t("common.confirm")}
|
||||||
|
cancelText={t("common.cancel")}
|
||||||
|
>
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<InputField
|
||||||
|
fieldName={t("spellTagManager.tagName")}
|
||||||
|
input={
|
||||||
|
<TextInput
|
||||||
|
value={editTagName}
|
||||||
|
setValue={(e) => setEditTagName(e.target.value)}
|
||||||
|
placeholder={t("spellTagManager.tagNamePlaceholder")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className="text-text-primary text-sm font-medium mb-2 block">{t("spellTagManager.tagColor")}</span>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{defaultTagColors.map((color: string) => (
|
||||||
|
<button
|
||||||
|
key={color}
|
||||||
|
onClick={() => setEditTagColor(color)}
|
||||||
|
className={`w-10 h-10 rounded-full transition-all duration-200 ${editTagColor === color ? 'ring-2 ring-offset-2 ring-primary scale-110' : 'hover:scale-110'}`}
|
||||||
|
style={{backgroundColor: color}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2">
|
||||||
|
<span className="text-text-secondary text-sm">{t("spellTagManager.preview")}</span>
|
||||||
|
<div className="mt-2">
|
||||||
|
<SpellTagChip tag={{
|
||||||
|
id: editingTag.id,
|
||||||
|
name: editTagName || editingTag.name,
|
||||||
|
color: editTagColor
|
||||||
|
}}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDeleteConfirm && tagToDelete && (
|
||||||
|
<Modal
|
||||||
|
title={t("spellTagManager.deleteTagTitle")}
|
||||||
|
size="small"
|
||||||
|
onClose={() => setShowDeleteConfirm(false)}
|
||||||
|
onConfirm={handleDeleteTag}
|
||||||
|
confirmText={t("spellTagManager.delete")}
|
||||||
|
cancelText={t("common.cancel")}
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
<p className="text-text-secondary">{t("spellTagManager.confirmDelete")}</p>
|
||||||
|
<div className="mt-4">
|
||||||
|
<SpellTagChip tag={tagToDelete}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
359
electron/database/models/Spell.ts
Normal file
359
electron/database/models/Spell.ts
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import SpellRepo, { SpellResult } from '../repositories/spell.repo.js';
|
||||||
|
import SpellTagRepo, { SpellTagResult } from '../repositories/spelltag.repo.js';
|
||||||
|
import BookRepo, { BookToolsTable } from '../repositories/book.repository.js';
|
||||||
|
import System from '../System.js';
|
||||||
|
import { getUserEncryptionKey } from '../keyManager.js';
|
||||||
|
|
||||||
|
export interface SpellTagProps {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpellProps {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
appearance: string;
|
||||||
|
tags: string[];
|
||||||
|
powerLevel: string | null;
|
||||||
|
components: string | null;
|
||||||
|
limitations: string | null;
|
||||||
|
notes: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpellListItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
tags: SpellTagProps[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpellListResponse {
|
||||||
|
enabled: boolean;
|
||||||
|
spells: SpellListItem[];
|
||||||
|
tags: SpellTagProps[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncedSpell {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
lastUpdate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncedSpellTag {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
lastUpdate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Spell {
|
||||||
|
/**
|
||||||
|
* Retrieves all spell tags for a specific book.
|
||||||
|
* @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')
|
||||||
|
* @returns An array of spell tag props
|
||||||
|
*/
|
||||||
|
static getSpellTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellTagProps[] {
|
||||||
|
const userKey: string = getUserEncryptionKey(userId);
|
||||||
|
const spellTags: SpellTagResult[] = SpellTagRepo.fetchSpellTags(userId, bookId, lang);
|
||||||
|
|
||||||
|
return spellTags.map((tag: SpellTagResult): SpellTagProps => ({
|
||||||
|
id: tag.tag_id,
|
||||||
|
name: System.decryptDataWithUserKey(tag.name, userKey),
|
||||||
|
color: tag.color,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new spell tag to a book.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param bookId - The unique identifier of the book
|
||||||
|
* @param name - The name of the tag
|
||||||
|
* @param color - The optional color hex code
|
||||||
|
* @param existingTagId - Optional existing tag ID for sync
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns The created spell tag props
|
||||||
|
*/
|
||||||
|
static addSpellTag(userId: string, bookId: string, name: string, color: string | null, existingTagId?: string, lang: 'fr' | 'en' = 'fr'): SpellTagProps {
|
||||||
|
const userKey: string = getUserEncryptionKey(userId);
|
||||||
|
const tagId: string = existingTagId || System.createUniqueId();
|
||||||
|
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||||
|
const nameHash: string = System.hashElement(name);
|
||||||
|
|
||||||
|
SpellTagRepo.insertSpellTag(tagId, bookId, userId, encryptedName, nameHash, color, lang);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: tagId,
|
||||||
|
name: name,
|
||||||
|
color: color,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an existing spell tag.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param tagId - The unique identifier of the tag
|
||||||
|
* @param name - The new name of the tag
|
||||||
|
* @param color - The new optional color hex code
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the update was successful
|
||||||
|
*/
|
||||||
|
static updateSpellTag(userId: string, tagId: string, name: string, color: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
const userKey: string = getUserEncryptionKey(userId);
|
||||||
|
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||||
|
const nameHash: string = System.hashElement(name);
|
||||||
|
|
||||||
|
return SpellTagRepo.updateSpellTag(userId, tagId, encryptedName, nameHash, color, lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a spell tag and removes its references from all spells in the book.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param tagId - The unique identifier of the tag to delete
|
||||||
|
* @param bookId - The unique identifier of the book
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the deletion was successful
|
||||||
|
*/
|
||||||
|
static deleteSpellTag(userId: string, tagId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
const userKey: string = getUserEncryptionKey(userId);
|
||||||
|
|
||||||
|
const spells: SpellResult[] = SpellRepo.fetchSpells(userId, bookId, lang);
|
||||||
|
|
||||||
|
for (const spell of spells) {
|
||||||
|
const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey);
|
||||||
|
let tagsArray: string[] = [];
|
||||||
|
try {
|
||||||
|
tagsArray = JSON.parse(decryptedTags) as string[];
|
||||||
|
} catch {
|
||||||
|
tagsArray = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagsArray.includes(tagId)) {
|
||||||
|
const updatedTags: string[] = tagsArray.filter((t: string): boolean => t !== tagId);
|
||||||
|
const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(updatedTags), userKey);
|
||||||
|
SpellRepo.updateSpellTags(userId, spell.spell_id, encryptedTags, lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then delete the tag
|
||||||
|
return SpellTagRepo.deleteSpellTag(userId, tagId, lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the spell list with tags for a specific book.
|
||||||
|
* @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')
|
||||||
|
* @returns The spell list response with enabled status, spells, and tags
|
||||||
|
*/
|
||||||
|
static getSpellList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellListResponse {
|
||||||
|
const userKey: string = getUserEncryptionKey(userId);
|
||||||
|
|
||||||
|
const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang);
|
||||||
|
const enabled: boolean = bookTools ? bookTools.spells_enabled === 1 : false;
|
||||||
|
|
||||||
|
const spellTags: SpellTagResult[] = SpellTagRepo.fetchSpellTags(userId, bookId, lang);
|
||||||
|
const tags: SpellTagProps[] = spellTags.map((tag: SpellTagResult): SpellTagProps => ({
|
||||||
|
id: tag.tag_id,
|
||||||
|
name: System.decryptDataWithUserKey(tag.name, userKey),
|
||||||
|
color: tag.color,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const tagMap: Map<string, SpellTagProps> = new Map();
|
||||||
|
for (const tag of tags) {
|
||||||
|
tagMap.set(tag.id, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
const spellResults: SpellResult[] = SpellRepo.fetchSpells(userId, bookId, lang);
|
||||||
|
|
||||||
|
const spells: SpellListItem[] = spellResults.map((spell: SpellResult): SpellListItem => {
|
||||||
|
const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey);
|
||||||
|
const decryptedDescription: string = System.decryptDataWithUserKey(spell.description, userKey);
|
||||||
|
const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey);
|
||||||
|
|
||||||
|
let tagIds: string[];
|
||||||
|
try {
|
||||||
|
tagIds = JSON.parse(decryptedTags) as string[];
|
||||||
|
} catch {
|
||||||
|
tagIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedTags: SpellTagProps[] = tagIds
|
||||||
|
.map((tagId: string): SpellTagProps | undefined => tagMap.get(tagId))
|
||||||
|
.filter((tag: SpellTagProps | undefined): tag is SpellTagProps => tag !== undefined);
|
||||||
|
|
||||||
|
const truncatedDescription: string = decryptedDescription.length > 150
|
||||||
|
? decryptedDescription.substring(0, 150) + '...'
|
||||||
|
: decryptedDescription;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: spell.spell_id,
|
||||||
|
name: decryptedName,
|
||||||
|
description: truncatedDescription,
|
||||||
|
tags: resolvedTags,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
spells,
|
||||||
|
tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the full details of a specific spell.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param spellId - The unique identifier of the spell
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns The spell props with all details
|
||||||
|
*/
|
||||||
|
static getSpellDetail(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SpellProps {
|
||||||
|
const userKey: string = getUserEncryptionKey(userId);
|
||||||
|
|
||||||
|
const spell: SpellResult | null = SpellRepo.fetchSpellById(userId, spellId, lang);
|
||||||
|
if (!spell) {
|
||||||
|
throw new Error(lang === 'fr' ? 'Sort non trouvé.' : 'Spell not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey);
|
||||||
|
const decryptedDescription: string = System.decryptDataWithUserKey(spell.description, userKey);
|
||||||
|
const decryptedAppearance: string = System.decryptDataWithUserKey(spell.appearance, userKey);
|
||||||
|
const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey);
|
||||||
|
|
||||||
|
let tagIds: string[];
|
||||||
|
try {
|
||||||
|
tagIds = JSON.parse(decryptedTags) as string[];
|
||||||
|
} catch {
|
||||||
|
tagIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: spell.spell_id,
|
||||||
|
name: decryptedName,
|
||||||
|
description: decryptedDescription,
|
||||||
|
appearance: decryptedAppearance,
|
||||||
|
tags: tagIds,
|
||||||
|
powerLevel: spell.power_level ? System.decryptDataWithUserKey(spell.power_level, userKey) : null,
|
||||||
|
components: spell.components ? System.decryptDataWithUserKey(spell.components, userKey) : null,
|
||||||
|
limitations: spell.limitations ? System.decryptDataWithUserKey(spell.limitations, userKey) : null,
|
||||||
|
notes: spell.notes ? System.decryptDataWithUserKey(spell.notes, userKey) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new spell to a book.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param bookId - The unique identifier of the book
|
||||||
|
* @param name - The name of the spell
|
||||||
|
* @param description - The description of the spell
|
||||||
|
* @param appearance - The appearance of the spell
|
||||||
|
* @param tags - The tag IDs array
|
||||||
|
* @param powerLevel - The optional power level
|
||||||
|
* @param components - The optional components
|
||||||
|
* @param limitations - The optional limitations
|
||||||
|
* @param notes - The optional notes
|
||||||
|
* @param existingSpellId - Optional existing spell ID for sync
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns The created spell props
|
||||||
|
*/
|
||||||
|
static addSpell(userId: string, bookId: string, name: string, description: string, appearance: string, tags: string[], powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, existingSpellId?: string, lang: 'fr' | 'en' = 'fr'): SpellProps {
|
||||||
|
const userKey: string = getUserEncryptionKey(userId);
|
||||||
|
const spellId: string = existingSpellId || System.createUniqueId();
|
||||||
|
|
||||||
|
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||||
|
const nameHash: string = System.hashElement(name);
|
||||||
|
const encryptedDescription: string = System.encryptDataWithUserKey(description, userKey);
|
||||||
|
const encryptedAppearance: string = System.encryptDataWithUserKey(appearance, userKey);
|
||||||
|
const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(tags), userKey);
|
||||||
|
const encryptedPowerLevel: string | null = powerLevel ? System.encryptDataWithUserKey(powerLevel, userKey) : null;
|
||||||
|
const encryptedComponents: string | null = components ? System.encryptDataWithUserKey(components, userKey) : null;
|
||||||
|
const encryptedLimitations: string | null = limitations ? System.encryptDataWithUserKey(limitations, userKey) : null;
|
||||||
|
const encryptedNotes: string | null = notes ? System.encryptDataWithUserKey(notes, userKey) : null;
|
||||||
|
|
||||||
|
SpellRepo.insertSpell(
|
||||||
|
spellId,
|
||||||
|
bookId,
|
||||||
|
userId,
|
||||||
|
encryptedName,
|
||||||
|
nameHash,
|
||||||
|
encryptedDescription,
|
||||||
|
encryptedAppearance,
|
||||||
|
encryptedTags,
|
||||||
|
encryptedPowerLevel,
|
||||||
|
encryptedComponents,
|
||||||
|
encryptedLimitations,
|
||||||
|
encryptedNotes,
|
||||||
|
lang,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: spellId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
appearance,
|
||||||
|
tags,
|
||||||
|
powerLevel,
|
||||||
|
components,
|
||||||
|
limitations,
|
||||||
|
notes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an existing spell.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param spellId - The unique identifier of the spell
|
||||||
|
* @param name - The name of the spell
|
||||||
|
* @param description - The description of the spell
|
||||||
|
* @param appearance - The appearance of the spell
|
||||||
|
* @param tags - The tag IDs array
|
||||||
|
* @param powerLevel - The optional power level
|
||||||
|
* @param components - The optional components
|
||||||
|
* @param limitations - The optional limitations
|
||||||
|
* @param notes - The optional notes
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the update was successful
|
||||||
|
*/
|
||||||
|
static updateSpell(userId: string, spellId: string, name: string, description: string, appearance: string, tags: string[], powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
const userKey: string = getUserEncryptionKey(userId);
|
||||||
|
|
||||||
|
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||||
|
const nameHash: string = System.hashElement(name);
|
||||||
|
const encryptedDescription: string = System.encryptDataWithUserKey(description, userKey);
|
||||||
|
const encryptedAppearance: string = System.encryptDataWithUserKey(appearance, userKey);
|
||||||
|
const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(tags), userKey);
|
||||||
|
const encryptedPowerLevel: string | null = powerLevel ? System.encryptDataWithUserKey(powerLevel, userKey) : null;
|
||||||
|
const encryptedComponents: string | null = components ? System.encryptDataWithUserKey(components, userKey) : null;
|
||||||
|
const encryptedLimitations: string | null = limitations ? System.encryptDataWithUserKey(limitations, userKey) : null;
|
||||||
|
const encryptedNotes: string | null = notes ? System.encryptDataWithUserKey(notes, userKey) : null;
|
||||||
|
|
||||||
|
return SpellRepo.updateSpell(
|
||||||
|
userId,
|
||||||
|
spellId,
|
||||||
|
encryptedName,
|
||||||
|
nameHash,
|
||||||
|
encryptedDescription,
|
||||||
|
encryptedAppearance,
|
||||||
|
encryptedTags,
|
||||||
|
encryptedPowerLevel,
|
||||||
|
encryptedComponents,
|
||||||
|
encryptedLimitations,
|
||||||
|
encryptedNotes,
|
||||||
|
lang,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a spell.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param spellId - The unique identifier of the spell
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the deletion was successful
|
||||||
|
*/
|
||||||
|
static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
return SpellRepo.deleteSpell(userId, spellId, lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
368
electron/database/repositories/spell.repo.ts
Normal file
368
electron/database/repositories/spell.repo.ts
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
||||||
|
import System from '../System.js';
|
||||||
|
|
||||||
|
export interface SpellResult extends Record<string, SQLiteValue> {
|
||||||
|
spell_id: string;
|
||||||
|
book_id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
appearance: string;
|
||||||
|
tags: string;
|
||||||
|
power_level: string | null;
|
||||||
|
components: string | null;
|
||||||
|
limitations: string | null;
|
||||||
|
notes: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookSpellsTable extends Record<string, SQLiteValue> {
|
||||||
|
spell_id: string;
|
||||||
|
book_id: string;
|
||||||
|
user_id: string;
|
||||||
|
name: string;
|
||||||
|
name_hash: string;
|
||||||
|
description: string;
|
||||||
|
appearance: string;
|
||||||
|
tags: string;
|
||||||
|
power_level: string | null;
|
||||||
|
components: string | null;
|
||||||
|
limitations: string | null;
|
||||||
|
notes: string | null;
|
||||||
|
last_update: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncedSpellResult extends Record<string, SQLiteValue> {
|
||||||
|
spell_id: string;
|
||||||
|
book_id: string;
|
||||||
|
name: string;
|
||||||
|
last_update: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SpellRepo {
|
||||||
|
/**
|
||||||
|
* Fetches all spells for a specific book owned by the user.
|
||||||
|
* @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')
|
||||||
|
* @returns An array of spell results
|
||||||
|
*/
|
||||||
|
static fetchSpells(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellResult[] {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes FROM book_spells WHERE user_id=? AND book_id=?';
|
||||||
|
const params: SQLiteValue[] = [userId, bookId];
|
||||||
|
return db.all(query, params) as SpellResult[];
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts.` : `Unable to retrieve spells.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a single spell by its ID.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param spellId - The unique identifier of the spell
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns The spell result or null if not found
|
||||||
|
*/
|
||||||
|
static fetchSpellById(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SpellResult | null {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes FROM book_spells WHERE user_id=? AND spell_id=?';
|
||||||
|
const params: SQLiteValue[] = [userId, spellId];
|
||||||
|
const spells: SpellResult[] = db.all(query, params) as SpellResult[];
|
||||||
|
return spells.length > 0 ? spells[0] : null;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de récupérer le sort.` : `Unable to retrieve spell.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a new spell.
|
||||||
|
* @param spellId - The unique identifier for the new spell
|
||||||
|
* @param bookId - The unique identifier of the book
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param name - The encrypted name
|
||||||
|
* @param nameHash - The hashed name for duplicate detection
|
||||||
|
* @param description - The encrypted description
|
||||||
|
* @param appearance - The encrypted appearance
|
||||||
|
* @param tags - The encrypted JSON tags array
|
||||||
|
* @param powerLevel - The encrypted power level (nullable)
|
||||||
|
* @param components - The encrypted components (nullable)
|
||||||
|
* @param limitations - The encrypted limitations (nullable)
|
||||||
|
* @param notes - The encrypted notes (nullable)
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns The spell ID if successful
|
||||||
|
*/
|
||||||
|
static insertSpell(spellId: string, bookId: string, userId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): string {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'INSERT INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)';
|
||||||
|
const params: SQLiteValue[] = [spellId, bookId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds()];
|
||||||
|
const result: RunResult = db.run(query, params);
|
||||||
|
if (!result || result.changes === 0) {
|
||||||
|
throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du sort.` : `Error adding spell.`);
|
||||||
|
}
|
||||||
|
return spellId;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible d'ajouter le sort.` : `Unable to add spell.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an existing spell.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param spellId - The unique identifier of the spell
|
||||||
|
* @param name - The encrypted name
|
||||||
|
* @param nameHash - The hashed name
|
||||||
|
* @param description - The encrypted description
|
||||||
|
* @param appearance - The encrypted appearance
|
||||||
|
* @param tags - The encrypted JSON tags array
|
||||||
|
* @param powerLevel - The encrypted power level (nullable)
|
||||||
|
* @param components - The encrypted components (nullable)
|
||||||
|
* @param limitations - The encrypted limitations (nullable)
|
||||||
|
* @param notes - The encrypted notes (nullable)
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the update was successful
|
||||||
|
*/
|
||||||
|
static updateSpell(userId: string, spellId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'UPDATE book_spells SET name=?, name_hash=?, description=?, appearance=?, tags=?, power_level=?, components=?, limitations=?, notes=?, last_update=? WHERE spell_id=? AND user_id=?';
|
||||||
|
const params: SQLiteValue[] = [name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds(), spellId, userId];
|
||||||
|
const result: RunResult = db.run(query, params);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort.` : `Unable to update spell.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a spell.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param spellId - The unique identifier of the spell
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the deletion was successful
|
||||||
|
*/
|
||||||
|
static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'DELETE FROM book_spells WHERE spell_id=? AND user_id=?';
|
||||||
|
const params: SQLiteValue[] = [spellId, userId];
|
||||||
|
const result: RunResult = db.run(query, params);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de supprimer le sort.` : `Unable to delete spell.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the tags field of a spell.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param spellId - The unique identifier of the spell
|
||||||
|
* @param tags - The new encrypted JSON tags array
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the update was successful
|
||||||
|
*/
|
||||||
|
static updateSpellTags(userId: string, spellId: string, tags: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'UPDATE book_spells SET tags=?, last_update=? WHERE spell_id=? AND user_id=?';
|
||||||
|
const params: SQLiteValue[] = [tags, System.timeStampInSeconds(), spellId, userId];
|
||||||
|
const result: RunResult = db.run(query, params);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de mettre à jour les tags du sort.` : `Unable to update spell tags.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all spells for a book with full table data for sync.
|
||||||
|
* @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')
|
||||||
|
* @returns An array of book spells table records
|
||||||
|
*/
|
||||||
|
static fetchBookSpellsTable(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): BookSpellsTable[] {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'SELECT spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM book_spells WHERE user_id=? AND book_id=?';
|
||||||
|
const params: SQLiteValue[] = [userId, bookId];
|
||||||
|
return db.all(query, params) as BookSpellsTable[];
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts.` : `Unable to retrieve spells.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a complete spell record by its ID.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param spellId - The unique identifier of the spell
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns The spell table record or null
|
||||||
|
*/
|
||||||
|
static fetchSpellTableById(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): BookSpellsTable | null {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'SELECT spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM book_spells WHERE user_id=? AND spell_id=?';
|
||||||
|
const params: SQLiteValue[] = [userId, spellId];
|
||||||
|
const spells: BookSpellsTable[] = db.all(query, params) as BookSpellsTable[];
|
||||||
|
return spells.length > 0 ? spells[0] : null;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de récupérer le sort.` : `Unable to retrieve spell.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all synced spells for a user.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns An array of synced spell results
|
||||||
|
*/
|
||||||
|
static fetchSyncedSpells(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSpellResult[] {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'SELECT spell_id, book_id, name, last_update FROM book_spells WHERE user_id=?';
|
||||||
|
const params: SQLiteValue[] = [userId];
|
||||||
|
return db.all(query, params) as SyncedSpellResult[];
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts synchronisés.` : `Unable to retrieve synced spells.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts or updates a spell from synchronization data.
|
||||||
|
* @param spellId - The unique identifier for the spell
|
||||||
|
* @param bookId - The unique identifier of the book
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param name - The encrypted name
|
||||||
|
* @param nameHash - The hashed name
|
||||||
|
* @param description - The encrypted description
|
||||||
|
* @param appearance - The encrypted appearance
|
||||||
|
* @param tags - The encrypted JSON tags array
|
||||||
|
* @param powerLevel - The encrypted power level (nullable)
|
||||||
|
* @param components - The encrypted components (nullable)
|
||||||
|
* @param limitations - The encrypted limitations (nullable)
|
||||||
|
* @param notes - The encrypted notes (nullable)
|
||||||
|
* @param lastUpdate - The timestamp of the last update
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the insertion was successful
|
||||||
|
*/
|
||||||
|
static insertSyncSpell(spellId: string, bookId: string, userId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'INSERT OR REPLACE INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)';
|
||||||
|
const params: SQLiteValue[] = [spellId, bookId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate];
|
||||||
|
const result: RunResult = db.run(query, params);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible d'insérer le sort.` : `Unable to insert spell.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a spell exists.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param spellId - The unique identifier of the spell
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the spell exists
|
||||||
|
*/
|
||||||
|
static isSpellExist(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'SELECT 1 FROM book_spells WHERE spell_id=? AND user_id=?';
|
||||||
|
const params: SQLiteValue[] = [spellId, userId];
|
||||||
|
const existenceCheck = db.all(query, params);
|
||||||
|
return existenceCheck.length > 0;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du sort.` : `Unable to check spell existence.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a spell with timestamp for sync.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param spellId - The unique identifier of the spell
|
||||||
|
* @param name - The encrypted name
|
||||||
|
* @param nameHash - The hashed name
|
||||||
|
* @param description - The encrypted description
|
||||||
|
* @param appearance - The encrypted appearance
|
||||||
|
* @param tags - The encrypted JSON tags array
|
||||||
|
* @param powerLevel - The encrypted power level (nullable)
|
||||||
|
* @param components - The encrypted components (nullable)
|
||||||
|
* @param limitations - The encrypted limitations (nullable)
|
||||||
|
* @param notes - The encrypted notes (nullable)
|
||||||
|
* @param lastUpdate - The timestamp of the last update
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the update was successful
|
||||||
|
*/
|
||||||
|
static updateSyncSpell(userId: string, spellId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'UPDATE book_spells SET name=?, name_hash=?, description=?, appearance=?, tags=?, power_level=?, components=?, limitations=?, notes=?, last_update=? WHERE spell_id=? AND user_id=?';
|
||||||
|
const params: SQLiteValue[] = [name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate, spellId, userId];
|
||||||
|
const result: RunResult = db.run(query, params);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort.` : `Unable to update spell.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
292
electron/database/repositories/spelltag.repo.ts
Normal file
292
electron/database/repositories/spelltag.repo.ts
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
||||||
|
import System from '../System.js';
|
||||||
|
|
||||||
|
export interface SpellTagResult extends Record<string, SQLiteValue> {
|
||||||
|
tag_id: string;
|
||||||
|
book_id: string;
|
||||||
|
name: string;
|
||||||
|
color: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookSpellTagsTable extends Record<string, SQLiteValue> {
|
||||||
|
tag_id: string;
|
||||||
|
book_id: string;
|
||||||
|
user_id: string;
|
||||||
|
name: string;
|
||||||
|
name_hash: string;
|
||||||
|
color: string | null;
|
||||||
|
last_update: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SyncedSpellTagResult extends Record<string, SQLiteValue> {
|
||||||
|
tag_id: string;
|
||||||
|
book_id: string;
|
||||||
|
name: string;
|
||||||
|
last_update: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SpellTagRepo {
|
||||||
|
/**
|
||||||
|
* Fetches all spell tags for a specific book owned by the user.
|
||||||
|
* @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')
|
||||||
|
* @returns An array of spell tag results
|
||||||
|
*/
|
||||||
|
static fetchSpellTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellTagResult[] {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'SELECT tag_id, book_id, name, color FROM book_spell_tags WHERE user_id=? AND book_id=?';
|
||||||
|
const params: SQLiteValue[] = [userId, bookId];
|
||||||
|
return db.all(query, params) as SpellTagResult[];
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sorts.` : `Unable to retrieve spell tags.`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||||
|
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a new spell tag.
|
||||||
|
* @param tagId - The unique identifier for the new tag
|
||||||
|
* @param bookId - The unique identifier of the book
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param name - The encrypted name of the tag
|
||||||
|
* @param nameHash - The hashed name for duplicate detection
|
||||||
|
* @param color - The optional color hex code
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns The tag ID if successful
|
||||||
|
*/
|
||||||
|
static insertSpellTag(tagId: string, bookId: string, userId: string, name: string, nameHash: string, color: string | null, lang: 'fr' | 'en' = 'fr'): string {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'INSERT INTO book_spell_tags (tag_id, book_id, user_id, name, name_hash, color, last_update) VALUES (?,?,?,?,?,?,?)';
|
||||||
|
const params: SQLiteValue[] = [tagId, bookId, userId, name, nameHash, color, System.timeStampInSeconds()];
|
||||||
|
const result: RunResult = db.run(query, params);
|
||||||
|
if (!result || result.changes === 0) {
|
||||||
|
throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du tag.` : `Error adding tag.`);
|
||||||
|
}
|
||||||
|
return tagId;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||||
|
}
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible d'ajouter le tag de sort.` : `Unable to add spell tag.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an existing spell tag.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param tagId - The unique identifier of the tag
|
||||||
|
* @param name - The encrypted name of the tag
|
||||||
|
* @param nameHash - The hashed name
|
||||||
|
* @param color - The optional color hex code
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the update was successful
|
||||||
|
*/
|
||||||
|
static updateSpellTag(userId: string, tagId: string, name: string, nameHash: string, color: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'UPDATE book_spell_tags SET name=?, name_hash=?, color=?, last_update=? WHERE tag_id=? AND user_id=?';
|
||||||
|
const params: SQLiteValue[] = [name, nameHash, color, System.timeStampInSeconds(), tagId, userId];
|
||||||
|
const result: RunResult = db.run(query, params);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le tag de sort.` : `Unable to update spell tag.`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||||
|
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a spell tag.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param tagId - The unique identifier of the tag
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the deletion was successful
|
||||||
|
*/
|
||||||
|
static deleteSpellTag(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'DELETE FROM book_spell_tags WHERE tag_id=? AND user_id=?';
|
||||||
|
const params: SQLiteValue[] = [tagId, userId];
|
||||||
|
const result: RunResult = db.run(query, params);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de supprimer le tag de sort.` : `Unable to delete spell tag.`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||||
|
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all spell tags for a book with full table data for sync.
|
||||||
|
* @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')
|
||||||
|
* @returns An array of book spell tags table records
|
||||||
|
*/
|
||||||
|
static fetchBookSpellTagsTable(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): BookSpellTagsTable[] {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'SELECT tag_id, book_id, user_id, name, name_hash, color, last_update FROM book_spell_tags WHERE user_id=? AND book_id=?';
|
||||||
|
const params: SQLiteValue[] = [userId, bookId];
|
||||||
|
return db.all(query, params) as BookSpellTagsTable[];
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sorts.` : `Unable to retrieve spell tags.`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||||
|
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a complete spell tag record by its ID.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param tagId - The unique identifier of the tag
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns The spell tag table record or null
|
||||||
|
*/
|
||||||
|
static fetchSpellTagTableById(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): BookSpellTagsTable | null {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'SELECT tag_id, book_id, user_id, name, name_hash, color, last_update FROM book_spell_tags WHERE user_id=? AND tag_id=?';
|
||||||
|
const params: SQLiteValue[] = [userId, tagId];
|
||||||
|
const spellTags: BookSpellTagsTable[] = db.all(query, params) as BookSpellTagsTable[];
|
||||||
|
return spellTags.length > 0 ? spellTags[0] : null;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de récupérer le tag de sort.` : `Unable to retrieve spell tag.`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||||
|
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all synced spell tags for a user.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns An array of synced spell tag results
|
||||||
|
*/
|
||||||
|
static fetchSyncedSpellTags(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSpellTagResult[] {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'SELECT tag_id, book_id, name, last_update FROM book_spell_tags WHERE user_id=?';
|
||||||
|
const params: SQLiteValue[] = [userId];
|
||||||
|
return db.all(query, params) as SyncedSpellTagResult[];
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sorts synchronisés.` : `Unable to retrieve synced spell tags.`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||||
|
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts or updates a spell tag from synchronization data.
|
||||||
|
* @param tagId - The unique identifier for the tag
|
||||||
|
* @param bookId - The unique identifier of the book
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param name - The encrypted name
|
||||||
|
* @param nameHash - The hashed name
|
||||||
|
* @param color - The optional color hex code
|
||||||
|
* @param lastUpdate - The timestamp of the last update
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the insertion was successful
|
||||||
|
*/
|
||||||
|
static insertSyncSpellTag(tagId: string, bookId: string, userId: string, name: string, nameHash: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'INSERT OR REPLACE INTO book_spell_tags (tag_id, book_id, user_id, name, name_hash, color, last_update) VALUES (?,?,?,?,?,?,?)';
|
||||||
|
const params: SQLiteValue[] = [tagId, bookId, userId, name, nameHash, color, lastUpdate];
|
||||||
|
const result: RunResult = db.run(query, params);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible d'insérer le tag de sort.` : `Unable to insert spell tag.`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||||
|
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a spell tag exists.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param tagId - The unique identifier of the tag
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the tag exists
|
||||||
|
*/
|
||||||
|
static isSpellTagExist(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'SELECT 1 FROM book_spell_tags WHERE tag_id=? AND user_id=?';
|
||||||
|
const params: SQLiteValue[] = [tagId, userId];
|
||||||
|
const existenceCheck = db.all(query, params);
|
||||||
|
return existenceCheck.length > 0;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du tag.` : `Unable to check tag existence.`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||||
|
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a spell tag with timestamp for sync.
|
||||||
|
* @param userId - The unique identifier of the user
|
||||||
|
* @param tagId - The unique identifier of the tag
|
||||||
|
* @param name - The encrypted name
|
||||||
|
* @param nameHash - The hashed name
|
||||||
|
* @param color - The optional color hex code
|
||||||
|
* @param lastUpdate - The timestamp of the last update
|
||||||
|
* @param lang - The language for error messages ('fr' or 'en')
|
||||||
|
* @returns True if the update was successful
|
||||||
|
*/
|
||||||
|
static updateSyncSpellTag(userId: string, tagId: string, name: string, nameHash: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||||
|
try {
|
||||||
|
const db: Database = System.getDb();
|
||||||
|
const query: string = 'UPDATE book_spell_tags SET name=?, name_hash=?, color=?, last_update=? WHERE tag_id=? AND user_id=?';
|
||||||
|
const params: SQLiteValue[] = [name, nameHash, color, lastUpdate, tagId, userId];
|
||||||
|
const result: RunResult = db.run(query, params);
|
||||||
|
return result.changes > 0;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||||
|
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le tag de sort.` : `Unable to update spell tag.`);
|
||||||
|
} else {
|
||||||
|
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||||
|
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
201
electron/ipc/spell.ipc.ts
Normal file
201
electron/ipc/spell.ipc.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import { ipcMain } from 'electron';
|
||||||
|
import { createHandler } from '../database/LocalSystem.js';
|
||||||
|
import Spell from '../database/models/Spell.js';
|
||||||
|
import type {
|
||||||
|
SpellProps,
|
||||||
|
SpellListResponse,
|
||||||
|
SpellTagProps,
|
||||||
|
} from '../database/models/Spell.js';
|
||||||
|
|
||||||
|
// ==================== INTERFACES ====================
|
||||||
|
|
||||||
|
interface SpellPost {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
appearance: string;
|
||||||
|
tags: string[];
|
||||||
|
powerLevel?: string | null;
|
||||||
|
components?: string | null;
|
||||||
|
limitations?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSpellListData {
|
||||||
|
bookid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSpellTagsData {
|
||||||
|
bookid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSpellDetailData {
|
||||||
|
spellid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSpellData {
|
||||||
|
bookId: string;
|
||||||
|
spell: SpellPost;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateSpellData {
|
||||||
|
spellId: string;
|
||||||
|
spell: SpellPost;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSpellData {
|
||||||
|
spellId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateTagData {
|
||||||
|
bookId: string;
|
||||||
|
name: string;
|
||||||
|
color?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateTagData {
|
||||||
|
tagId: string;
|
||||||
|
name: string;
|
||||||
|
color?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteTagData {
|
||||||
|
tagId: string;
|
||||||
|
bookId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== SPELL HANDLERS ====================
|
||||||
|
|
||||||
|
// GET /spell/list
|
||||||
|
ipcMain.handle(
|
||||||
|
'db:spell:list',
|
||||||
|
createHandler<GetSpellListData, SpellListResponse>(
|
||||||
|
function (userId: string, data: GetSpellListData, lang: 'fr' | 'en'): SpellListResponse {
|
||||||
|
return Spell.getSpellList(userId, data.bookid, lang);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /spell/tags
|
||||||
|
ipcMain.handle(
|
||||||
|
'db:spell:tags',
|
||||||
|
createHandler<GetSpellTagsData, SpellTagProps[]>(
|
||||||
|
function (userId: string, data: GetSpellTagsData, lang: 'fr' | 'en'): SpellTagProps[] {
|
||||||
|
return Spell.getSpellTags(userId, data.bookid, lang);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /spell/detail
|
||||||
|
ipcMain.handle(
|
||||||
|
'db:spell:detail',
|
||||||
|
createHandler<GetSpellDetailData, SpellProps>(
|
||||||
|
function (userId: string, data: GetSpellDetailData, lang: 'fr' | 'en'): SpellProps {
|
||||||
|
return Spell.getSpellDetail(userId, data.spellid, lang);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /spell/add
|
||||||
|
ipcMain.handle(
|
||||||
|
'db:spell:create',
|
||||||
|
createHandler<CreateSpellData, string>(
|
||||||
|
function (userId: string, data: CreateSpellData, lang: 'fr' | 'en'): string {
|
||||||
|
const spell: SpellPost = data.spell;
|
||||||
|
const result: SpellProps = Spell.addSpell(
|
||||||
|
userId,
|
||||||
|
data.bookId,
|
||||||
|
spell.name,
|
||||||
|
spell.description,
|
||||||
|
spell.appearance,
|
||||||
|
spell.tags || [],
|
||||||
|
spell.powerLevel || null,
|
||||||
|
spell.components || null,
|
||||||
|
spell.limitations || null,
|
||||||
|
spell.notes || null,
|
||||||
|
spell.id,
|
||||||
|
lang,
|
||||||
|
);
|
||||||
|
return result.id;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// PUT /spell/update
|
||||||
|
ipcMain.handle(
|
||||||
|
'db:spell:update',
|
||||||
|
createHandler<UpdateSpellData, boolean>(
|
||||||
|
function (userId: string, data: UpdateSpellData, lang: 'fr' | 'en'): boolean {
|
||||||
|
const spell: SpellPost = data.spell;
|
||||||
|
return Spell.updateSpell(
|
||||||
|
userId,
|
||||||
|
data.spellId,
|
||||||
|
spell.name,
|
||||||
|
spell.description,
|
||||||
|
spell.appearance,
|
||||||
|
spell.tags || [],
|
||||||
|
spell.powerLevel || null,
|
||||||
|
spell.components || null,
|
||||||
|
spell.limitations || null,
|
||||||
|
spell.notes || null,
|
||||||
|
lang,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// DELETE /spell/delete
|
||||||
|
ipcMain.handle(
|
||||||
|
'db:spell:delete',
|
||||||
|
createHandler<DeleteSpellData, boolean>(
|
||||||
|
function (userId: string, data: DeleteSpellData, lang: 'fr' | 'en'): boolean {
|
||||||
|
return Spell.deleteSpell(userId, data.spellId, lang);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==================== SPELL TAG HANDLERS ====================
|
||||||
|
|
||||||
|
// POST /spell/tag/add
|
||||||
|
ipcMain.handle(
|
||||||
|
'db:spell:tag:create',
|
||||||
|
createHandler<CreateTagData, string>(
|
||||||
|
function (userId: string, data: CreateTagData, lang: 'fr' | 'en'): string {
|
||||||
|
const result: SpellTagProps = Spell.addSpellTag(
|
||||||
|
userId,
|
||||||
|
data.bookId,
|
||||||
|
data.name,
|
||||||
|
data.color || null,
|
||||||
|
undefined,
|
||||||
|
lang,
|
||||||
|
);
|
||||||
|
return result.id;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// PUT /spell/tag/update
|
||||||
|
ipcMain.handle(
|
||||||
|
'db:spell:tag:update',
|
||||||
|
createHandler<UpdateTagData, boolean>(
|
||||||
|
function (userId: string, data: UpdateTagData, lang: 'fr' | 'en'): boolean {
|
||||||
|
return Spell.updateSpellTag(
|
||||||
|
userId,
|
||||||
|
data.tagId,
|
||||||
|
data.name,
|
||||||
|
data.color || null,
|
||||||
|
lang,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// DELETE /spell/tag/delete
|
||||||
|
ipcMain.handle(
|
||||||
|
'db:spell:tag:delete',
|
||||||
|
createHandler<DeleteTagData, boolean>(
|
||||||
|
function (userId: string, data: DeleteTagData, lang: 'fr' | 'en'): boolean {
|
||||||
|
return Spell.deleteSpellTag(userId, data.tagId, data.bookId, lang);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
@@ -456,6 +456,80 @@
|
|||||||
"characterSectionElement": {
|
"characterSectionElement": {
|
||||||
"newItem": "New {item}"
|
"newItem": "New {item}"
|
||||||
},
|
},
|
||||||
|
"spellComponent": {
|
||||||
|
"enableTool": "Enable spell book",
|
||||||
|
"enableToolDescription": "Manage the spells and magic of your universe.",
|
||||||
|
"errorNameRequired": "Spell name is required.",
|
||||||
|
"errorDescriptionRequired": "Spell description is required.",
|
||||||
|
"errorAppearanceRequired": "Spell appearance is required.",
|
||||||
|
"errorAddSpell": "Error adding spell.",
|
||||||
|
"errorUpdateSpell": "Error updating spell.",
|
||||||
|
"errorDeleteSpell": "Error deleting spell.",
|
||||||
|
"successAdd": "Spell added successfully.",
|
||||||
|
"successUpdate": "Spell updated successfully.",
|
||||||
|
"successDelete": "Spell deleted successfully."
|
||||||
|
},
|
||||||
|
"spellList": {
|
||||||
|
"search": "Search for a spell...",
|
||||||
|
"add": "Add a spell",
|
||||||
|
"manageTags": "Manage tags",
|
||||||
|
"filterByTag": "Filter by tag",
|
||||||
|
"filterByLevel": "Filter by level",
|
||||||
|
"allTags": "All tags",
|
||||||
|
"allLevels": "All levels",
|
||||||
|
"noSpells": "No spells created",
|
||||||
|
"noSpellsDescription": "Add your first spell to get started."
|
||||||
|
},
|
||||||
|
"spellDetail": {
|
||||||
|
"back": "Back",
|
||||||
|
"newSpell": "New spell",
|
||||||
|
"save": "Save",
|
||||||
|
"delete": "Delete",
|
||||||
|
"deleteTitle": "Delete spell",
|
||||||
|
"deleteMessage": "You are about to permanently delete the spell \"{name}\".",
|
||||||
|
"basicInfo": "Basic information",
|
||||||
|
"name": "Spell name",
|
||||||
|
"namePlaceholder": "Enter spell name",
|
||||||
|
"description": "Description",
|
||||||
|
"descriptionPlaceholder": "Describe the effects and nature of the spell",
|
||||||
|
"appearance": "Appearance",
|
||||||
|
"appearancePlaceholder": "Describe the visual appearance of the spell",
|
||||||
|
"tags": "Tags",
|
||||||
|
"addTag": "Add a tag...",
|
||||||
|
"createTag": "Create \"{name}\"",
|
||||||
|
"powerLevel": "Power level",
|
||||||
|
"components": "Components",
|
||||||
|
"componentsPlaceholder": "Ingredients, gestures, incantations required...",
|
||||||
|
"limitations": "Limitations",
|
||||||
|
"limitationsPlaceholder": "Restrictions, side effects, conditions of use...",
|
||||||
|
"notes": "Notes",
|
||||||
|
"notesPlaceholder": "Additional notes about the spell..."
|
||||||
|
},
|
||||||
|
"spellTagManager": {
|
||||||
|
"title": "Tag management",
|
||||||
|
"back": "Back",
|
||||||
|
"addTag": "Add a tag",
|
||||||
|
"tagName": "Tag name",
|
||||||
|
"tagNamePlaceholder": "Tag name...",
|
||||||
|
"tagColor": "Color",
|
||||||
|
"editTag": "Edit tag",
|
||||||
|
"preview": "Preview",
|
||||||
|
"deleteTagTitle": "Delete tag",
|
||||||
|
"delete": "Delete",
|
||||||
|
"confirmDelete": "Delete this tag? It will be removed from all spells.",
|
||||||
|
"noTags": "No tags created",
|
||||||
|
"successAdd": "Tag added successfully.",
|
||||||
|
"successUpdate": "Tag updated successfully.",
|
||||||
|
"successDelete": "Tag deleted successfully."
|
||||||
|
},
|
||||||
|
"spellPowerLevels": {
|
||||||
|
"none": "None",
|
||||||
|
"minor": "Minor",
|
||||||
|
"moderate": "Moderate",
|
||||||
|
"major": "Major",
|
||||||
|
"legendary": "Legendary",
|
||||||
|
"divine": "Divine"
|
||||||
|
},
|
||||||
"aboutEditors": {
|
"aboutEditors": {
|
||||||
"title": "About Scribe",
|
"title": "About Scribe",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
|
|||||||
@@ -456,6 +456,80 @@
|
|||||||
"characterSectionElement": {
|
"characterSectionElement": {
|
||||||
"newItem": "Nouveau {item}"
|
"newItem": "Nouveau {item}"
|
||||||
},
|
},
|
||||||
|
"spellComponent": {
|
||||||
|
"enableTool": "Activer le grimoire de sorts",
|
||||||
|
"enableToolDescription": "Gérez les sorts et la magie de votre univers.",
|
||||||
|
"errorNameRequired": "Le nom du sort est requis.",
|
||||||
|
"errorDescriptionRequired": "La description du sort est requise.",
|
||||||
|
"errorAppearanceRequired": "L'apparence du sort est requise.",
|
||||||
|
"errorAddSpell": "Erreur lors de l'ajout du sort.",
|
||||||
|
"errorUpdateSpell": "Erreur lors de la mise à jour du sort.",
|
||||||
|
"errorDeleteSpell": "Erreur lors de la suppression du sort.",
|
||||||
|
"successAdd": "Sort ajouté avec succès.",
|
||||||
|
"successUpdate": "Sort mis à jour avec succès.",
|
||||||
|
"successDelete": "Sort supprimé avec succès."
|
||||||
|
},
|
||||||
|
"spellList": {
|
||||||
|
"search": "Rechercher un sort...",
|
||||||
|
"add": "Ajouter un sort",
|
||||||
|
"manageTags": "Gérer les tags",
|
||||||
|
"filterByTag": "Filtrer par tag",
|
||||||
|
"filterByLevel": "Filtrer par niveau",
|
||||||
|
"allTags": "Tous les tags",
|
||||||
|
"allLevels": "Tous les niveaux",
|
||||||
|
"noSpells": "Aucun sort créé",
|
||||||
|
"noSpellsDescription": "Ajoutez votre premier sort pour commencer."
|
||||||
|
},
|
||||||
|
"spellDetail": {
|
||||||
|
"back": "Retour",
|
||||||
|
"newSpell": "Nouveau sort",
|
||||||
|
"save": "Enregistrer",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"deleteTitle": "Supprimer le sort",
|
||||||
|
"deleteMessage": "Vous êtes sur le point de supprimer le sort « {name} » définitivement.",
|
||||||
|
"basicInfo": "Informations de base",
|
||||||
|
"name": "Nom du sort",
|
||||||
|
"namePlaceholder": "Entrez le nom du sort",
|
||||||
|
"description": "Description",
|
||||||
|
"descriptionPlaceholder": "Décrivez les effets et la nature du sort",
|
||||||
|
"appearance": "Apparence",
|
||||||
|
"appearancePlaceholder": "Décrivez l'apparence visuelle du sort",
|
||||||
|
"tags": "Tags",
|
||||||
|
"addTag": "Ajouter un tag...",
|
||||||
|
"createTag": "Créer \"{name}\"",
|
||||||
|
"powerLevel": "Niveau de puissance",
|
||||||
|
"components": "Composantes",
|
||||||
|
"componentsPlaceholder": "Ingrédients, gestes, incantations nécessaires...",
|
||||||
|
"limitations": "Limitations",
|
||||||
|
"limitationsPlaceholder": "Restrictions, effets secondaires, conditions d'utilisation...",
|
||||||
|
"notes": "Notes",
|
||||||
|
"notesPlaceholder": "Notes supplémentaires sur le sort..."
|
||||||
|
},
|
||||||
|
"spellTagManager": {
|
||||||
|
"title": "Gestion des tags",
|
||||||
|
"back": "Retour",
|
||||||
|
"addTag": "Ajouter un tag",
|
||||||
|
"tagName": "Nom du tag",
|
||||||
|
"tagNamePlaceholder": "Nom du tag...",
|
||||||
|
"tagColor": "Couleur",
|
||||||
|
"editTag": "Modifier le tag",
|
||||||
|
"preview": "Aperçu",
|
||||||
|
"deleteTagTitle": "Supprimer le tag",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"confirmDelete": "Supprimer ce tag? Il sera retiré de tous les sorts.",
|
||||||
|
"noTags": "Aucun tag créé",
|
||||||
|
"successAdd": "Tag ajouté avec succès.",
|
||||||
|
"successUpdate": "Tag mis à jour avec succès.",
|
||||||
|
"successDelete": "Tag supprimé avec succès."
|
||||||
|
},
|
||||||
|
"spellPowerLevels": {
|
||||||
|
"none": "Aucun",
|
||||||
|
"minor": "Mineur",
|
||||||
|
"moderate": "Modéré",
|
||||||
|
"major": "Majeur",
|
||||||
|
"legendary": "Légendaire",
|
||||||
|
"divine": "Divin"
|
||||||
|
},
|
||||||
"aboutEditors": {
|
"aboutEditors": {
|
||||||
"title": "À propos de Scribe",
|
"title": "À propos de Scribe",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export interface BookToolsSettings {
|
|||||||
characters: boolean;
|
characters: boolean;
|
||||||
worlds: boolean;
|
worlds: boolean;
|
||||||
locations: boolean;
|
locations: boolean;
|
||||||
|
spells: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BookProps {
|
export interface BookProps {
|
||||||
|
|||||||
Reference in New Issue
Block a user