Remove ExportBook component and integrate new export workflows

- Deleted `ExportBook` component and its usage in `BookCard.tsx`.
- Integrated improved book export workflows in `BookSettingOption` for better user experience.
- Updated database models and repositories to support export options with chapter/version selection.
- Added localization support for export-related messages and tooltips.
- Upgraded dependencies to include libraries required for export formats (e.g., DOCX, PDF, EPUB).
- Bumped app version to 0.4.1.
This commit is contained in:
natreex
2026-03-05 16:31:56 -05:00
parent 94cac463fb
commit ceaecb19fc
16 changed files with 780 additions and 245 deletions

View File

@@ -1,190 +0,0 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faDownload} from "@fortawesome/free-solid-svg-icons";
import {useContext, useRef, useState} from "react";
import {SessionContext} from "@/context/SessionContext";
import {AlertContext} from "@/context/AlertContext";
import {configs} from "@/lib/configs";
interface CreateEpubProps {
bookId: string;
bookTitle: string;
}
export default function ExportBook({bookId, bookTitle}: CreateEpubProps) {
const {session} = useContext(SessionContext);
const {successMessage, errorMessage} = useContext(AlertContext);
const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
function handleClickOutside(event: MouseEvent): void {
if (
menuRef.current &&
buttonRef.current &&
!menuRef.current.contains(event.target as Node) &&
!buttonRef.current.contains(event.target as Node)
) {
setShowMenu(false);
document.removeEventListener("mousedown", handleClickOutside);
}
}
function toggleMenu(): void {
if (!showMenu) {
setTimeout((): void => {
document.addEventListener("mousedown", handleClickOutside);
}, 0);
} else {
document.removeEventListener("mousedown", handleClickOutside);
}
setShowMenu(!showMenu);
}
async function handleDownloadEpub() {
try {
const response = await fetch(
`${configs.apiUrl}book/transform/epub?id=${bookId}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${session.accessToken}`,
},
}
);
if (!response.ok) {
errorMessage(`Échec du téléchargement du EPUB.`);
return;
}
const blob = await response.blob();
const virtualUrl = window.URL.createObjectURL(blob);
const aLink = document.createElement("a");
aLink.href = virtualUrl;
aLink.download = `${bookTitle}.epub`;
document.body.appendChild(aLink);
aLink.click();
aLink.remove();
window.URL.revokeObjectURL(virtualUrl);
setShowMenu(false);
successMessage(`Votre fichier EPUB a été téléchargé.`);
} catch (error) {
console.error(`Error downloading EPUB:`, error);
errorMessage(`Une erreur est survenue lors du téléchargement.`);
}
}
async function handleDownloadPdf() {
try {
const response = await fetch(
`${configs.apiUrl}book/transform/pdf?id=${bookId}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${session.accessToken}`,
},
}
);
if (!response.ok) {
errorMessage(`Échec du téléchargement du PDF.`);
return;
}
const blob = await response.blob();
const virtualUrl = window.URL.createObjectURL(blob);
const aLink = document.createElement("a");
aLink.href = virtualUrl;
aLink.download = `${bookTitle}.pdf`;
document.body.appendChild(aLink);
aLink.click();
aLink.remove();
window.URL.revokeObjectURL(virtualUrl);
setShowMenu(false);
successMessage(`Votre fichier PDF a été téléchargé.`);
} catch (error) {
console.error(`Error downloading PDF:`, error);
errorMessage(`Une erreur est survenue lors du téléchargement.`);
}
}
async function handleDownloadDocx() {
try {
const response = await fetch(
`${configs.apiUrl}book/transform/docx?id=${bookId}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${session.accessToken}`,
},
}
);
if (!response.ok) {
errorMessage(`Échec du téléchargement du DOCX.`);
return;
}
const blob = await response.blob();
const virtualUrl = window.URL.createObjectURL(blob);
const aLink = document.createElement("a");
aLink.href = virtualUrl;
aLink.download = `${bookTitle}.docx`;
document.body.appendChild(aLink);
aLink.click();
aLink.remove();
window.URL.revokeObjectURL(virtualUrl);
setShowMenu(false);
successMessage(`Votre fichier DOCX a été téléchargé.`);
} catch (error) {
console.error(`Error downloading DOCX:`, error);
errorMessage(`Une erreur est survenue lors du téléchargement.`);
}
}
return (
<div className="relative">
<button
ref={buttonRef}
onClick={toggleMenu}
className="text-muted hover:text-primary transition-all duration-200 p-1.5 rounded-lg hover:bg-secondary/50 hover:scale-110"
>
<FontAwesomeIcon icon={faDownload} className={'w-4 h-4'}/>
</button>
{showMenu && (
<div
ref={menuRef}
className="absolute z-50 bg-tertiary/90 backdrop-blur-sm shadow-2xl rounded-xl border border-secondary/50"
style={{
width: '110px',
right: '-30px',
top: '100%',
marginTop: '8px',
}}
>
<ul className="py-2">
<li
className="px-3 py-2 hover:bg-secondary cursor-pointer text-sm text-muted hover:text-text-primary transition-all duration-200 hover:scale-105 font-medium"
onClick={handleDownloadEpub}
>
EPUB
</li>
<li
className="px-3 py-2 hover:bg-secondary cursor-pointer text-sm text-muted hover:text-text-primary transition-all duration-200 hover:scale-105 font-medium"
onClick={handleDownloadPdf}
>
PDF
</li>
<li
className="px-3 py-2 hover:bg-secondary cursor-pointer text-sm text-muted hover:text-text-primary transition-all duration-200 hover:scale-105 font-medium"
onClick={handleDownloadDocx}
>
DOCX
</li>
</ul>
</div>
)}
</div>
);
}

View File

@@ -1,11 +1,9 @@
// Removed Next.js Link import for Electron
import {BookProps} from "@/lib/models/Book";
import DeleteBook from "@/components/book/settings/DeleteBook";
import ExportBook from "@/components/ExportBook";
import {useTranslations} from "next-intl";
import SyncBook from "@/components/SyncBook";
import {SyncType} from "@/context/BooksSyncContext";
import {useEffect} from "react";
interface BookCardProps {
book: BookProps;
@@ -68,7 +66,6 @@ export default function BookCard({book, onClickCallback, index, syncStatus}: Boo
<div className="flex justify-between items-center pt-3 border-t border-secondary/30">
<SyncBook status={syncStatus} bookId={book.bookId}/>
<div className="flex items-center gap-1" {...index === 0 && {'data-guide': 'bottom-book-card'}}>
<ExportBook bookTitle={book.title} bookId={book.bookId}/>
<DeleteBook bookId={book.bookId}/>
</div>
</div>

View File

@@ -1,10 +1,10 @@
import {useContext, useEffect, useState} from "react";
import {useContext, useEffect, useRef, useState} from "react";
import System from "@/lib/models/System";
import {AlertContext} from "@/context/AlertContext";
import {BookContext} from "@/context/BookContext";
import SearchBook from "./SearchBook";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faBook, faDownload, faGear, faTrash} from "@fortawesome/free-solid-svg-icons";
import {faBook, faChevronLeft, faChevronRight, faDownload, faGear, faTrash} from "@fortawesome/free-solid-svg-icons";
import {SessionContext} from "@/context/SessionContext";
import Book, {BookProps} from "@/lib/models/Book";
import BookCard from "@/components/book/BookCard";
@@ -55,6 +55,7 @@ export default function BookList() {
const [isLoadingBooks, setIsLoadingBooks] = useState<boolean>(true);
const [showSeriesSettingId, setShowSeriesSettingId] = useState<string | null>(null);
const [isLocalSeries, setIsLocalSeries] = useState<boolean>(false);
const carouselRefs = useRef<Record<string, HTMLDivElement | null>>({});
const [bookGuide, setBookGuide] = useState<boolean>(false);
@@ -447,6 +448,17 @@ export default function BookList() {
}
}
function scrollCarousel(category: string, direction: 'left' | 'right'): void {
const container: HTMLDivElement | null = carouselRefs.current[category];
if (!container) return;
const cardWidth: number = container.querySelector<HTMLDivElement>(':scope > div')?.offsetWidth || 250;
const scrollAmount: number = cardWidth * 2;
container.scrollBy({
left: direction === 'left' ? -scrollAmount : scrollAmount,
behavior: 'smooth'
});
}
function handleSeriesSettingsClick(seriesId: string): void {
const isLocal: boolean = isCurrentlyOffline() ||
Boolean(localOnlySeries.find((s: SyncedSeries): boolean => s.id === seriesId));
@@ -464,7 +476,7 @@ export default function BookList() {
<SearchBook searchQuery={searchQuery} setSearchQuery={setSearchQuery}/>
</div>
)}
<div className="flex flex-col w-full overflow-y-auto h-full min-h-0 flex-grow">
<div className="flex flex-col w-full overflow-y-auto overflow-x-hidden h-full min-h-0 flex-grow">
{
isLoadingBooks ? (
<>
@@ -511,7 +523,18 @@ export default function BookList() {
</span>
</div>
<div className="flex items-start justify-center w-full px-4 overflow-x-auto pb-4">
<div className="group relative w-full">
<button
onClick={() => scrollCarousel(category, 'left')}
className="absolute left-3 top-1/2 -translate-y-1/2 z-10 bg-primary/80 backdrop-blur-sm hover:bg-primary text-white rounded-2xl w-12 h-12 flex items-center justify-center shadow-xl border border-primary-light/30 transition-all duration-200 opacity-0 group-hover:opacity-100 hover:scale-110"
>
<FontAwesomeIcon icon={faChevronLeft} className="w-5 h-5"/>
</button>
<div
ref={(el: HTMLDivElement | null) => { carouselRefs.current[category] = el; }}
className="flex items-start w-full overflow-hidden px-4 gap-2 scroll-smooth"
>
{items.map((item, idx) => {
if (item.type === 'book' && item.book) {
return (
@@ -542,6 +565,14 @@ export default function BookList() {
return null;
})}
</div>
<button
onClick={() => scrollCarousel(category, 'right')}
className="absolute right-3 top-1/2 -translate-y-1/2 z-10 bg-primary/80 backdrop-blur-sm hover:bg-primary text-white rounded-2xl w-12 h-12 flex items-center justify-center shadow-xl border border-primary-light/30 transition-all duration-200 opacity-0 group-hover:opacity-100 hover:scale-110"
>
<FontAwesomeIcon icon={faChevronRight} className="w-5 h-5"/>
</button>
</div>
</div>
))}
</>

View File

@@ -33,6 +33,9 @@ const CharacterSettings = lazy(function () {
const SpellSettings = lazy(function () {
return import('./spells/settings/SpellSettings');
});
const ExportSetting = lazy(function () {
return import('./ExportSetting');
});
function LoadingSpinner(): React.JSX.Element {
return (
@@ -51,7 +54,7 @@ interface SettingRef {
}
// Settings qui gèrent leur propre save (pas de bouton save parent)
const selfManagedSettings: string[] = ['characters', 'spells', 'world', 'worlds', 'locations'];
const selfManagedSettings: string[] = ['characters', 'spells', 'world', 'worlds', 'locations', 'export'];
export default function BookSettingOption({setting}: BookSettingOptionProps): React.JSX.Element {
const t = useTranslations();
@@ -77,6 +80,8 @@ export default function BookSettingOption({setting}: BookSettingOptionProps): Re
return t("bookSettingOption.characters");
case 'spells':
return t("bookSettingOption.spells");
case 'export':
return t("bookSettingOption.export");
default:
return "";
}
@@ -119,7 +124,10 @@ export default function BookSettingOption({setting}: BookSettingOptionProps): Re
{setting === 'spells' && (
<SpellSettings entityType="book" showToggle={true}/>
)}
{!['basic-information', 'guide-line', 'story', 'world', 'worlds', 'locations', 'characters', 'spells', 'quillsense'].includes(setting) && (
{setting === 'export' && (
<ExportSetting/>
)}
{!['basic-information', 'guide-line', 'story', 'world', 'worlds', 'locations', 'characters', 'spells', 'quillsense', 'export'].includes(setting) && (
<div className="text-text-secondary py-4 text-center">
{t("bookSettingOption.notAvailable")}
</div>

View File

@@ -3,6 +3,7 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faBook,
faDownload,
faGlobe,
faHatWizard,
faListAlt,
@@ -74,6 +75,11 @@ export default function BookSettingSidebar(
name: 'bookSetting.quillsense',
icon: faWandMagicSparkles
},
{
id: 'export',
name: 'bookSetting.export',
icon: faDownload
},
// {
// id: 'objects',
// name: t('bookSetting.objects'),

View File

@@ -15,7 +15,7 @@ export default function ScribeEditor() {
return (
<SettingBookContext.Provider value={{bookSettingId, setBookSettingId}}>
<div className="flex-1 bg-darkest-background">
<div className="flex-1 min-w-0 bg-darkest-background">
{
chapter ? (
<TextEditor/>

View File

@@ -1,5 +1,5 @@
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faBookMedical, faBookOpen, faFeather, faLayerGroup} from "@fortawesome/free-solid-svg-icons";
import {faBookMedical, faBookOpen, faFeather, faFileImport, faLayerGroup} from "@fortawesome/free-solid-svg-icons";
import React, {useContext, useEffect, useState} from "react";
import {BookContext} from "@/context/BookContext";
import ScribeChapterComponent from "@/components/leftbar/ScribeChapterComponent";
@@ -8,6 +8,7 @@ import {PanelComponent} from "@/lib/models/Editor";
import AddNewBookForm from "@/components/book/AddNewBookForm";
import AddNewSeriesForm from "@/components/series/AddNewSeriesForm";
import ShortStoryGenerator from "@/components/ShortStoryGenerator";
import ImportBookForm from "@/components/book/ImportBookForm";
import {SessionContext} from "@/context/SessionContext";
import {useTranslations} from "next-intl";
import OfflineContext from "@/context/OfflineContext";
@@ -24,6 +25,7 @@ export default function ScribeLeftBar() {
const [showAddNewBook, setShowAddNewBook] = useState<boolean>(false);
const [showAddNewSeries, setShowAddNewSeries] = useState<boolean>(false);
const [showGenerateShortModal, setShowGenerateShortModal] = useState<boolean>(false)
const [showImportBook, setShowImportBook] = useState<boolean>(false)
const {isCurrentlyOffline} = useContext(OfflineContext)
const editorComponents: PanelComponent[] = [
@@ -69,6 +71,12 @@ export default function ScribeLeftBar() {
icon: faLayerGroup,
badge: t("scribeLeftBar.homeComponents.addSeries.badge"),
description: t("scribeLeftBar.homeComponents.addSeries.description")
}, {
id: 4,
title: t("scribeLeftBar.homeComponents.importBook.title"),
icon: faFileImport,
badge: t("scribeLeftBar.homeComponents.importBook.badge"),
description: t("scribeLeftBar.homeComponents.importBook.description")
},
]
@@ -116,9 +124,9 @@ export default function ScribeLeftBar() {
)) : (
homeComponents
.filter((component: PanelComponent): boolean => {
// Hide generate story (id: 2) in offline mode (requires AI server)
// Hide generate story (id: 2) and import book (id: 4) in offline mode (requires server)
// Series (id: 3) now has dual logic and works offline
if (isCurrentlyOffline() && component.id === 2) {
if (isCurrentlyOffline() && (component.id === 2 || component.id === 4)) {
return false;
}
return true;
@@ -126,7 +134,7 @@ export default function ScribeLeftBar() {
.map((component: PanelComponent) => (
<button
key={component.id}
onClick={() => component.id === 1 ? setShowAddNewBook(true) : component.id === 2 ? setShowGenerateShortModal(true) : component.id === 3 ? setShowAddNewSeries(true) : null}
onClick={() => component.id === 1 ? setShowAddNewBook(true) : component.id === 2 ? setShowGenerateShortModal(true) : component.id === 3 ? setShowAddNewSeries(true) : component.id === 4 ? setShowImportBook(true) : null}
title={component.title}
className={`group relative p-3 rounded-xl transition-all duration-200 ${panelHidden && currentPanel?.id === component.id
? 'bg-primary text-text-primary shadow-lg shadow-primary/30'
@@ -162,6 +170,10 @@ export default function ScribeLeftBar() {
showGenerateShortModal &&
<ShortStoryGenerator onClose={() => setShowGenerateShortModal(false)}/>
}
{
showImportBook &&
<ImportBookForm setCloseForm={setShowImportBook}/>
}
</div>
)
}

View File

@@ -3,9 +3,12 @@ import { getUserEncryptionKey } from "../keyManager.js";
import Book, { CompleteBookData } from "./Book.js";
import ChapterRepo, {
ActChapterQuery,
ChapterExportInfoResult,
ChapterQueryResult,
ChapterSelectionParam,
ChapterStoryQueryResult,
LastChapterResult
LastChapterResult,
SelectedChapterContentResult
} from "../repositories/chapter.repository.js";
import { ActChapter, ActStory } from "./Act.js";
import ChapterContentRepository, {
@@ -65,6 +68,13 @@ export interface CompleteChapterContent {
version?: number;
}
export interface ChapterExportInfo {
chapterId: string;
title: string;
chapterOrder: number;
availableVersions: number[];
}
interface TipTapNode {
type?: string;
text?: string;
@@ -602,4 +612,53 @@ export default class Chapter {
return processedChapters;
}
static getChaptersExportInfo(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterExportInfo[] {
const results: ChapterExportInfoResult[] = ChapterRepo.fetchChaptersExportInfo(userId, bookId, lang);
const userEncryptionKey: string = getUserEncryptionKey(userId);
const exportInfos: ChapterExportInfo[] = [];
for (const result of results) {
if (!result.available_versions) continue;
const versions: number[] = result.available_versions
.split(',')
.map((v: string): number => parseInt(v, 10))
.filter((v: number): boolean => !isNaN(v));
if (versions.length === 0) continue;
exportInfos.push({
chapterId: result.chapter_id,
title: result.title ? System.decryptDataWithUserKey(result.title, userEncryptionKey) : '',
chapterOrder: result.chapter_order,
availableVersions: versions.sort((a: number, b: number): number => a - b)
});
}
return exportInfos;
}
static getCompleteBookDataWithSelections(userId: string, bookId: string, selections: ChapterSelectionParam[] | null, lang: 'fr' | 'en' = 'fr'): CompleteBookData {
if (!selections || selections.length === 0) {
return Book.completeBookData(userId, bookId, lang);
}
const bookData: CompleteBookData = Book.completeBookData(userId, bookId, lang);
const selectedResults: SelectedChapterContentResult[] = ChapterRepo.fetchSelectedChaptersContent(bookId, selections, lang);
const userEncryptionKey: string = getUserEncryptionKey(userId);
const selectedChapters: CompleteChapterContent[] = [];
for (const result of selectedResults) {
selectedChapters.push({
id: result.chapter_id,
title: result.title ? System.decryptDataWithUserKey(result.title, userEncryptionKey) : '',
content: result.content ? System.decryptDataWithUserKey(result.content, userEncryptionKey) : '',
order: result.chapter_order,
version: result.version
});
}
return {
...bookData,
chapters: selectedChapters
};
}
}

View File

@@ -82,6 +82,26 @@ export interface ChapterBookResult extends Record<string, SQLiteValue> {
content: string | null;
}
export interface ChapterExportInfoResult extends Record<string, SQLiteValue> {
chapter_id: string;
title: string;
chapter_order: number;
available_versions: string;
}
export interface SelectedChapterContentResult extends Record<string, SQLiteValue> {
chapter_id: string;
title: string;
chapter_order: number;
content: string;
version: number;
}
export interface ChapterSelectionParam {
chapterId: string;
version: number;
}
export default class ChapterRepo {
/**
* Checks if a chapter name already exists for a book.
@@ -698,4 +718,38 @@ export default class ChapterRepo {
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
static fetchChaptersExportInfo(userId: string, bookId: string, lang: 'fr' | 'en'): ChapterExportInfoResult[] {
try {
const db: Database = System.getDb();
const query: string = `SELECT bc.chapter_id, bc.title, bc.chapter_order, GROUP_CONCAT(DISTINCT bcc.version) AS available_versions FROM book_chapters bc LEFT JOIN book_chapter_content bcc ON bc.chapter_id = bcc.chapter_id WHERE bc.author_id = ? AND bc.book_id = ? GROUP BY bc.chapter_id, bc.title, bc.chapter_order ORDER BY bc.chapter_order`;
const params: SQLiteValue[] = [userId, bookId];
return db.all(query, params) as ChapterExportInfoResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? "Impossible de récupérer les informations d'export des chapitres." : 'Unable to retrieve chapters export info.');
}
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
static fetchSelectedChaptersContent(bookId: string, selections: ChapterSelectionParam[], lang: 'fr' | 'en'): SelectedChapterContentResult[] {
try {
const db: Database = System.getDb();
const conditions: string[] = selections.map((): string => '(chapter.chapter_id = ? AND content.version = ?)');
const query: string = `SELECT chapter.chapter_id, chapter.title, chapter.chapter_order, content.content, content.version FROM book_chapters AS chapter INNER JOIN book_chapter_content AS content ON chapter.chapter_id = content.chapter_id WHERE chapter.book_id = ? AND (${conditions.join(' OR ')}) ORDER BY chapter.chapter_order`;
const params: SQLiteValue[] = [bookId];
for (const selection of selections) {
params.push(selection.chapterId, selection.version);
}
return db.all(query, params) as SelectedChapterContentResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? 'Impossible de récupérer le contenu des chapitres sélectionnés.' : 'Unable to retrieve selected chapters content.');
}
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}

View File

@@ -1,9 +1,11 @@
import { ipcMain } from 'electron';
import { ipcMain, dialog, BrowserWindow } from 'electron';
import { writeFile } from 'fs/promises';
import { createHandler } from '../database/LocalSystem.js';
import Book, {BookSyncCompare, CompleteBook, SyncedBook} from '../database/models/Book.js';
import Book, {BookSyncCompare, CompleteBook, CompleteBookData, SyncedBook} from '../database/models/Book.js';
import type { BookProps } from '../database/models/Book.js';
import Chapter from '../database/models/Chapter.js';
import Chapter, {ChapterExportInfo} from '../database/models/Chapter.js';
import type { ChapterProps } from '../database/models/Chapter.js';
import {ChapterSelectionParam} from "../database/repositories/chapter.repository.js";
import Act, {ActProps} from "../database/models/Act.js";
import Issue, {IssueProps} from "../database/models/Issue.js";
import Sync from "../database/models/Sync.js";
@@ -13,6 +15,7 @@ import GuideLine, {GuideLineAI} from "../database/models/GuideLine.js";
import Incident from "../database/models/Incident.js";
import PlotPoint from "../database/models/PlotPoint.js";
import World, {WorldListResponse, WorldProps} from "../database/models/World.js";
import Export, {ExportResult} from "../database/models/Export.js";
interface UpdateBookBasicData {
title: string;
@@ -434,3 +437,65 @@ ipcMain.handle('db:book:tool:update', createHandler<UpdateBookToolData, boolean>
}
)
);
// GET /book/export/info - Get chapters export info (available versions)
interface ExportInfoData {
bookId: string;
}
ipcMain.handle('db:book:export:info', createHandler<ExportInfoData, ChapterExportInfo[]>(
function(userId: string, data: ExportInfoData, lang: 'fr' | 'en'): ChapterExportInfo[] {
return Chapter.getChaptersExportInfo(userId, data.bookId, lang);
}
)
);
// POST /book/export - Export book to file (EPUB/PDF/DOCX)
type ExportFormat = 'epub' | 'pdf' | 'docx';
interface ExportRequestData {
bookId: string;
format: ExportFormat;
selections: ChapterSelectionParam[] | null;
}
const formatExtensions: Record<ExportFormat, {ext: string; filterName: string}> = {
epub: {ext: 'epub', filterName: 'EPUB'},
pdf: {ext: 'pdf', filterName: 'PDF'},
docx: {ext: 'docx', filterName: 'Word Document'}
};
ipcMain.handle('db:book:export', createHandler<ExportRequestData, boolean>(
async function(userId: string, data: ExportRequestData, lang: 'fr' | 'en'): Promise<boolean> {
const bookData: CompleteBookData = Chapter.getCompleteBookDataWithSelections(userId, data.bookId, data.selections, lang);
let result: ExportResult;
switch (data.format) {
case 'epub':
result = await Export.transformToEpub(bookData);
break;
case 'pdf':
result = await Export.transformToPDF(bookData);
break;
case 'docx':
result = await Export.transformToDOCX(bookData);
break;
default:
throw new Error(lang === 'fr' ? 'Format non supporté.' : 'Unsupported format.');
}
const formatInfo = formatExtensions[data.format];
const focusedWindow: BrowserWindow | null = BrowserWindow.getFocusedWindow();
const dialogResult = await dialog.showSaveDialog(focusedWindow!, {
defaultPath: result.fileName,
filters: [{name: formatInfo.filterName, extensions: [formatInfo.ext]}]
});
if (dialogResult.canceled || !dialogResult.filePath) {
return false;
}
await writeFile(dialogResult.filePath, result.buffer);
return true;
}
)
);

View File

@@ -207,6 +207,11 @@
"title": "Create a series",
"description": "Create a series to group multiple books.",
"badge": "SERIES"
},
"importBook": {
"title": "Import a book",
"description": "Import a book from a DOCX file.",
"badge": "IMPORT"
}
}
},
@@ -640,9 +645,27 @@
"objectsList": "Objects list",
"bookGoals": "Book goals",
"quillsense": "QuillSense Settings",
"export": "Export Book",
"save": "Save",
"notAvailable": "Option not available"
},
"exportOption": {
"title": "Export Your Book",
"description": "Choose the format and chapters to export.",
"format": "Format",
"selectFormat": "Select a format",
"chapters": "Chapters",
"selectAll": "Select all",
"deselectAll": "Deselect all",
"version": "Version",
"export": "Export",
"exporting": "Exporting...",
"noChapters": "No chapters available for export.",
"success": "Book exported successfully!",
"cancelled": "Export cancelled.",
"error": "Error exporting the book.",
"loadingChapters": "Loading chapters..."
},
"noBookHome": {
"title": "Your work is waiting for its first words",
"description": "This work does not have any chapters yet. To start writing, create your first chapter.",
@@ -987,6 +1010,7 @@
"characters": "Characters",
"spells": "Spell Book",
"quillsense": "QuillSense (AI)",
"export": "Export",
"objects": "Objects",
"goals": "Goals"
},
@@ -1333,5 +1357,51 @@
"enable_characters": "Enable character management for this book",
"enable_worlds": "Enable world management for this book",
"enable_locations": "Enable location management for this book"
},
"importBook": {
"header": {
"title": "Import a Book"
},
"pickFile": "Choose a DOCX file",
"parsing": "Analyzing file...",
"chaptersDetected": "{count} chapters detected",
"noChaptersDetected": "No chapters detected in the file",
"fields": {
"title": {
"label": "Book Title",
"placeholder": "Enter the title"
},
"subTitle": {
"label": "Subtitle",
"placeholder": "Enter the subtitle"
},
"summary": {
"label": "Summary",
"placeholder": "Enter a summary"
},
"type": {
"label": "Book Type"
},
"version": {
"label": "Chapter Version"
}
},
"chapters": {
"title": "Chapters to import",
"words": "{count} words",
"selectAll": "Select all",
"deselectAll": "Deselect all"
},
"submit": "Import",
"importing": "Importing...",
"success": "Book imported successfully",
"error": {
"titleRequired": "Book title is required",
"typeRequired": "Book type is required",
"noChaptersSelected": "Select at least one chapter",
"parseFailed": "Error analyzing the file",
"importFailed": "Error during import",
"invalidFormat": "Invalid format. Only DOCX files are accepted"
}
}
}

View File

@@ -207,6 +207,11 @@
"title": "Créer une série",
"description": "Créez une série pour regrouper plusieurs livres.",
"badge": "SÉRIE"
},
"importBook": {
"title": "Importer une oeuvre",
"description": "Importez un livre à partir d'un fichier DOCX.",
"badge": "IMPORT"
}
}
},
@@ -631,9 +636,27 @@
"objectsList": "Liste des objets",
"bookGoals": "Objectifs du livre",
"quillsense": "Parametres QuillSense",
"export": "Exporter le livre",
"save": "Sauvegarder",
"notAvailable": "Option non disponible"
},
"exportOption": {
"title": "Exporter votre livre",
"description": "Choisissez le format et les chapitres à exporter.",
"format": "Format",
"selectFormat": "Sélectionner un format",
"chapters": "Chapitres",
"selectAll": "Tout sélectionner",
"deselectAll": "Tout désélectionner",
"version": "Version",
"export": "Exporter",
"exporting": "Exportation en cours...",
"noChapters": "Aucun chapitre disponible pour l'export.",
"success": "Livre exporté avec succès !",
"cancelled": "Export annulé.",
"error": "Erreur lors de l'exportation du livre.",
"loadingChapters": "Chargement des chapitres..."
},
"noBookHome": {
"title": "Votre œuvre attend ses premiers mots",
"description": "Cette œuvre n'a pas encore de chapitres. Pour commencer à écrire, créez votre premier chapitre.",
@@ -953,7 +976,8 @@
"spells": "Sortilèges",
"objects": "Objets",
"goals": "Buts",
"quillsense": "QuillSense"
"quillsense": "QuillSense",
"export": "Export"
},
"basicInformationSetting": {
"error": {
@@ -1336,5 +1360,51 @@
"enable_characters": "Activer la gestion des personnages pour ce livre",
"enable_worlds": "Activer la gestion des mondes pour ce livre",
"enable_locations": "Activer la gestion des lieux pour ce livre"
},
"importBook": {
"header": {
"title": "Importer un livre"
},
"pickFile": "Choisir un fichier DOCX",
"parsing": "Analyse du fichier en cours...",
"chaptersDetected": "{count} chapitres détectés",
"noChaptersDetected": "Aucun chapitre détecté dans le fichier",
"fields": {
"title": {
"label": "Titre du livre",
"placeholder": "Entrez le titre"
},
"subTitle": {
"label": "Sous-titre",
"placeholder": "Entrez le sous-titre"
},
"summary": {
"label": "Résumé",
"placeholder": "Entrez un résumé"
},
"type": {
"label": "Type de livre"
},
"version": {
"label": "Version des chapitres"
}
},
"chapters": {
"title": "Chapitres à importer",
"words": "{count} mots",
"selectAll": "Tout sélectionner",
"deselectAll": "Tout désélectionner"
},
"submit": "Importer",
"importing": "Import en cours...",
"success": "Livre importé avec succès",
"error": {
"titleRequired": "Le titre du livre est requis",
"typeRequired": "Le type de livre est requis",
"noChaptersSelected": "Sélectionnez au moins un chapitre",
"parseFailed": "Erreur lors de l'analyse du fichier",
"importFailed": "Erreur lors de l'import",
"invalidFormat": "Format invalide. Seuls les fichiers DOCX sont acceptés"
}
}
}

View File

@@ -48,6 +48,21 @@ export type TiptapNode = {
};
export type ExportFormat = 'epub' | 'pdf' | 'docx';
export interface ChapterExportInfo {
chapterId: string;
title: string;
chapterOrder: number;
availableVersions: number[];
}
export interface ChapterExportSelection {
chapterId: string;
version: number;
selected: boolean;
}
export const chapterVersions: SelectBoxProps[] = [
{value: '1', label: 'chapterVersions.prompt'},
{value: '2', label: 'chapterVersions.draft'},

View File

@@ -212,6 +212,38 @@ export default class System{
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; ${domain} path=/; ${secure} ${sameSite}`;
}
public static async authUploadFileToServer<T>(url: string, file: File, auth: string, lang: string = "fr"): Promise<T> {
try {
const formData: FormData = new FormData();
formData.append('file', file);
formData.append('lang', lang);
formData.append('plateforme', window.electron.platform);
const response: AxiosResponse<T> = await axios({
method: 'POST',
headers: {
'Authorization': `Bearer ${auth}`,
},
url: configs.apiUrl + url,
params: {
lang: lang,
plateforme: window.electron.platform,
},
data: formData,
});
return response.data;
} catch (e: unknown) {
if (axios.isAxiosError(e)) {
const serverMessage: string = e.response?.data?.message || e.response?.data || e.message;
throw new Error(serverMessage as string);
} else if (e instanceof Error) {
throw new Error(e.message);
} else {
throw new Error('An unexpected error occurred');
}
}
}
public static async authDeleteToServer<T>(url: string, data: {}, auth: string, lang: string = "fr"): Promise<T> {
try {
const response: AxiosResponse<T> = await axios({

316
package-lock.json generated
View File

@@ -26,10 +26,13 @@
"autoprefixer": "^10.4.22",
"axios": "^1.13.2",
"bcrypt": "^6.0.0",
"docx": "^9.5.3",
"electron-updater": "^6.7.3",
"jszip": "^3.10.1",
"next": "^16.0.3",
"next-intl": "^4.5.3",
"node-sqlite3-wasm": "^0.8.51",
"pdfkit": "^0.17.2",
"postcss": "^8.5.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
@@ -39,6 +42,7 @@
"@electron/notarize": "^3.1.1",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.10.1",
"@types/pdfkit": "^0.17.5",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"concurrently": "^9.2.1",
@@ -2990,6 +2994,16 @@
"undici-types": "~7.16.0"
}
},
"node_modules/@types/pdfkit": {
"version": "0.17.5",
"resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.17.5.tgz",
"integrity": "sha512-T3ZHnvF91HsEco5ClhBCOuBwobZfPcI2jaiSHybkkKYq4KhVIIurod94JVKvDIG0JXT6o3KiERC0X0//m8dyrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/plist": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
@@ -3504,7 +3518,6 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -3576,6 +3589,15 @@
"concat-map": "0.0.1"
}
},
"node_modules/brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.1.2"
}
},
"node_modules/browserslist": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
@@ -4107,9 +4129,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"dev": true,
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/crc": {
"version": "3.8.0",
@@ -4175,6 +4195,12 @@
"node": ">= 8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -4320,6 +4346,12 @@
"license": "MIT",
"optional": true
},
"node_modules/dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
"license": "MIT"
},
"node_modules/dir-compare": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz",
@@ -4426,6 +4458,56 @@
"node": ">=8"
}
},
"node_modules/docx": {
"version": "9.5.3",
"resolved": "https://registry.npmjs.org/docx/-/docx-9.5.3.tgz",
"integrity": "sha512-uFVrYiN2WKx1an884SS6mRu4JuCO10fpnRGQLYmbYgMLVAqqjrKPc2qMXsGj9JuDVrzdntGRw+y84bSjBxXqew==",
"license": "MIT",
"dependencies": {
"@types/node": "^25.2.3",
"hash.js": "^1.1.7",
"jszip": "^3.10.1",
"nanoid": "^5.1.3",
"xml": "^1.0.1",
"xml-js": "^1.6.8"
},
"engines": {
"node": ">=10"
}
},
"node_modules/docx/node_modules/@types/node": {
"version": "25.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.18.0"
}
},
"node_modules/docx/node_modules/nanoid": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
"integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/docx/node_modules/undici-types": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/dotenv": {
"version": "17.2.4",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.4.tgz",
@@ -4967,7 +5049,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-equals": {
@@ -5067,6 +5148,32 @@
}
}
},
"node_modules/fontkit": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
"license": "MIT",
"dependencies": {
"@swc/helpers": "^0.5.12",
"brotli": "^1.3.2",
"clone": "^2.1.2",
"dfa": "^1.2.0",
"fast-deep-equal": "^3.1.3",
"restructure": "^3.0.0",
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.4.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/fontkit/node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -5414,6 +5521,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -5563,6 +5680,12 @@
],
"license": "BSD-3-Clause"
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -5589,7 +5712,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC"
},
"node_modules/intl-messageformat": {
@@ -5668,6 +5790,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/isbinaryfile": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz",
@@ -5753,6 +5881,13 @@
"node": ">= 20"
}
},
"node_modules/jpeg-exif": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
@@ -5810,6 +5945,48 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/jszip/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/jszip/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/jszip/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -5826,6 +6003,15 @@
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
"license": "MIT"
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
@@ -6075,6 +6261,25 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"license": "MIT",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/linebreak/node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
@@ -6282,6 +6487,12 @@
"node": ">=4"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/minimatch": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz",
@@ -6916,6 +7127,12 @@
"dev": true,
"license": "BlueOak-1.0.0"
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -6960,6 +7177,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/pdfkit": {
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz",
"integrity": "sha512-UnwF5fXy08f0dnp4jchFYAROKMNTaPqb/xgR8GtCzIcqoTnbOqtp3bwKvO4688oHI6vzEEs8Q6vqqEnC5IUELw==",
"license": "MIT",
"dependencies": {
"crypto-js": "^4.2.0",
"fontkit": "^2.0.4",
"jpeg-exif": "^1.1.4",
"linebreak": "^1.1.0",
"png-js": "^1.0.0"
}
},
"node_modules/pe-library": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz",
@@ -7015,6 +7245,11 @@
"node": ">=10.4.0"
}
},
"node_modules/png-js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
},
"node_modules/po-parser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
@@ -7095,6 +7330,12 @@
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -7486,6 +7727,12 @@
"node": ">=8"
}
},
"node_modules/restructure": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz",
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
"license": "MIT"
},
"node_modules/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
@@ -7634,6 +7881,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -8129,6 +8382,12 @@
"semver": "bin/semver"
}
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
"node_modules/tiny-typed-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
@@ -8238,6 +8497,32 @@
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT"
},
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unicode-trie/node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"license": "MIT"
},
"node_modules/unique-filename": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz",
@@ -8355,7 +8640,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/verror": {
@@ -8470,6 +8754,24 @@
"dev": true,
"license": "ISC"
},
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
"license": "MIT"
},
"node_modules/xml-js": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
"license": "MIT",
"dependencies": {
"sax": "^1.2.4"
},
"bin": {
"xml-js": "bin/cli.js"
}
},
"node_modules/xmlbuilder": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "eritorsscribe",
"productName": "ERitors Scribe",
"version": "0.3.1",
"version": "0.4.1",
"type": "module",
"main": "dist/electron/main.js",
"scripts": {
@@ -19,6 +19,7 @@
"@electron/notarize": "^3.1.1",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.10.1",
"@types/pdfkit": "^0.17.5",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"concurrently": "^9.2.1",
@@ -46,10 +47,13 @@
"autoprefixer": "^10.4.22",
"axios": "^1.13.2",
"bcrypt": "^6.0.0",
"docx": "^9.5.3",
"electron-updater": "^6.7.3",
"jszip": "^3.10.1",
"next": "^16.0.3",
"next-intl": "^4.5.3",
"node-sqlite3-wasm": "^0.8.51",
"pdfkit": "^0.17.2",
"postcss": "^8.5.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",