Files
ERitors-Scribe-Desktop/electron/ipc/book.ipc.ts
natreex ceaecb19fc 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.
2026-03-05 16:31:56 -05:00

502 lines
17 KiB
TypeScript

import { ipcMain, dialog, BrowserWindow } from 'electron';
import { writeFile } from 'fs/promises';
import { createHandler } from '../database/LocalSystem.js';
import Book, {BookSyncCompare, CompleteBook, CompleteBookData, SyncedBook} from '../database/models/Book.js';
import type { BookProps } from '../database/models/Book.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";
import Download from "../database/models/Download.js";
import Upload from "../database/models/Upload.js";
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;
subTitle: string;
summary: string;
publicationDate: string;
wordCount: number;
bookId: string;
}
interface UpdateGuideLineData {
bookId: string;
tone: string | null;
atmosphere: string | null;
writingStyle: string | null;
themes: string | null;
symbolism: string | null;
motifs: string | null;
narrativeVoice: string | null;
pacing: string | null;
keyMessages: string | null;
intendedAudience: string | null;
}
interface StoryData {
acts: ActProps[];
issues: IssueProps[];
mainChapter: ChapterProps[];
}
interface UpdateStoryData {
bookId: string;
acts: ActProps[];
mainChapters: ChapterProps[];
}
interface CreateBookData {
title: string;
subTitle: string | null;
summary: string | null;
type: string;
serieId: number | null;
desiredReleaseDate: string | null;
desiredWordCount: number | null;
}
interface AddIncidentData {
bookId: string;
name: string;
incidentId?: string;
}
interface AddPlotPointData {
bookId: string;
name: string;
incidentId: string;
plotPointId?: string;
}
interface AddIssueData {
bookId: string;
name: string;
issueId?: string;
}
interface AddWorldData {
bookId: string;
worldName: string;
id?: string;
seriesWorldId?: string | null;
}
interface AddWorldElementData {
worldId: string;
elementName: string;
elementType: number;
id?: string;
}
interface SetAIGuideLineData {
bookId: string;
narrativeType: number;
dialogueType: number;
plotSummary: string;
toneAtmosphere: string;
verbTense: number;
language: number;
themes: string;
}
interface GetGuidelineData {
id: string;
}
interface UpdateWorldData {
bookId: string;
world: WorldProps;
}
interface UpdateBookToolData {
bookId: string;
toolName: 'characters' | 'worlds' | 'locations' | 'spells';
enabled: boolean;
}
// GET /books - Get all books
ipcMain.handle('db:book:books', createHandler<void, BookProps[]>(
async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise<BookProps[]> {
return await Book.getBooks(userId, lang);
}
)
);
// GET /books/synced - Get all synced books
ipcMain.handle('db:books:synced', createHandler<void, SyncedBook[]>(
async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise<SyncedBook[]> {
return await Sync.getSyncedBooks(userId, lang);
})
);
// POST /book/sync/save - Save complete book
ipcMain.handle('db:book:syncSave', createHandler<CompleteBook, boolean>(
async function(userId: string, data: CompleteBook, lang: 'fr' | 'en'):Promise<boolean> {
return await Download.saveCompleteBook(userId, data, lang);
})
);
// GET /book/:id - Get single book
ipcMain.handle('db:book:bookBasicInformation', createHandler<string, BookProps>(
async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise<BookProps> {
return await Book.getBook(userId, bookId, lang);
}
)
);
// GET
ipcMain.handle('db:book:uploadToServer', createHandler<string, CompleteBook>(
async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise<CompleteBook> {
return await Upload.uploadBookForSync(userId, bookId, lang);
}
)
);
// POST /book/basic-information - Update book basic info
ipcMain.handle('db:book:updateBasicInformation', createHandler<UpdateBookBasicData, boolean>(
function(userId: string, data: UpdateBookBasicData, lang: 'fr' | 'en') {
return Book.updateBookBasicInformation(userId, data.title, data.subTitle, data.summary, data.publicationDate, data.wordCount, data.bookId, lang);
}
)
);
// GET /book/sync/to-client - Get book data to sync to client
ipcMain.handle('db:book:sync:toServer', createHandler<BookSyncCompare, CompleteBook>(
async function(userId: string, data:BookSyncCompare, lang: 'fr' | 'en'):Promise<CompleteBook> {
return await Sync.getCompleteSyncBook(userId, data, lang);
}
)
);
// GET /book/sync/from-server - Get book data to sync from server
ipcMain.handle('db:book:sync:toClient', createHandler<CompleteBook, boolean>(
async function(userId: string, data:CompleteBook, lang: 'fr' | 'en'):Promise<boolean> {
return await Sync.syncBookFromServerToClient(userId, data, lang);
}
)
);
// GET /book/guide-line - Get guideline
ipcMain.handle('db:book:guideline:get',
createHandler<GetGuidelineData, GuideLine | null>(async function(userId: string, data: GetGuidelineData, lang: 'fr' | 'en') {
return await GuideLine.getGuideLine(userId, data.id, lang);
}
)
);
// POST /book/guide-line - Update guideline
ipcMain.handle('db:book:guideline:update', createHandler<UpdateGuideLineData, boolean>(
async function(userId: string, data: UpdateGuideLineData, lang: 'fr' | 'en') {
return await GuideLine.updateGuideLine(
userId,
data.bookId,
data.tone,
data.atmosphere,
data.writingStyle,
data.themes,
data.symbolism,
data.motifs,
data.narrativeVoice,
data.pacing,
data.keyMessages,
data.intendedAudience,
lang
);
}
)
);
// GET /book/story - Get story data (acts + issues + mainChapter)
interface GetStoryData {
bookid: string;
}
ipcMain.handle('db:book:story:get', createHandler<GetStoryData, StoryData>(
async function(userId: string, data: GetStoryData, lang: 'fr' | 'en'):Promise<StoryData> {
const acts:ActProps[] = await Act.getActsData(userId, data.bookid, lang);
const issues:IssueProps[] = await Issue.getIssuesFromBook(userId, data.bookid, lang);
const mainChapter:ChapterProps[] = Chapter.getAllChaptersFromABook(userId, data.bookid, lang);
return {
acts,
issues,
mainChapter
};
}
)
);
// POST /book/story - Update story (acts + mainChapters)
ipcMain.handle('db:book:story:update', createHandler<UpdateStoryData, boolean>(
function(userId: string, data: UpdateStoryData, lang: 'fr' | 'en'):boolean {
return Act.updateStory(userId, data.bookId, data.acts, data.mainChapters, lang);
}
)
);
// POST /book/add - Create new book
ipcMain.handle('db:book:create', createHandler<CreateBookData, string>(
function(userId: string, data: CreateBookData, lang: 'fr' | 'en') {
return Book.addBook(
null,
userId,
data.title,
data.subTitle || '',
data.summary || '',
data.type,
data.serieId || 0,
data.desiredReleaseDate || '',
data.desiredWordCount || 0,
lang
);
}
)
);
// POST /book/incident/new - Add incident
ipcMain.handle(
'db:book:incident:add',
createHandler<AddIncidentData, string>(
function(userId: string, data: AddIncidentData, lang: 'fr' | 'en') {
return Incident.addNewIncident(userId, data.bookId, data.name, lang, data.incidentId);
}
)
);
// DELETE /book/incident/remove - Remove incident
interface RemoveIncidentData {
bookId: string;
incidentId: string;
deletedAt: number;
}
ipcMain.handle('db:book:incident:remove', createHandler<RemoveIncidentData, boolean>(
function(userId: string, data: RemoveIncidentData, lang: 'fr' | 'en') {
return Incident.removeIncident(userId, data.bookId, data.incidentId, data.deletedAt, lang);
}
)
);
// POST /book/plot/new - Add plot point
ipcMain.handle('db:book:plot:add', createHandler<AddPlotPointData, string>(
function(userId: string, data: AddPlotPointData, lang: 'fr' | 'en'):string {
return PlotPoint.addNewPlotPoint(
userId,
data.bookId,
data.incidentId,
data.name,
lang,
data.plotPointId
);
}
)
);
// DELETE /book/plot/remove - Remove plot point
interface RemovePlotData {
plotId: string;
bookId: string;
deletedAt: number;
}
ipcMain.handle(
'db:book:plot:remove',
createHandler<RemovePlotData, boolean>(
function(userId: string, data: RemovePlotData, lang: 'fr' | 'en') {
return PlotPoint.removePlotPoint(userId, data.bookId, data.plotId, data.deletedAt, lang);
}
)
);
// POST /book/issue/add - Add issue
ipcMain.handle('db:book:issue:add', createHandler<AddIssueData, string>(
function(userId: string, data: AddIssueData, lang: 'fr' | 'en') {
return Issue.addNewIssue(userId, data.bookId, data.name, lang, data.issueId);
}
)
);
// DELETE /book/issue/remove - Remove issue
interface RemoveIssueData {
bookId: string;
issueId: string;
deletedAt: number;
}
ipcMain.handle('db:book:issue:remove', createHandler<RemoveIssueData, boolean>(
function(userId: string, data: RemoveIssueData, lang: 'fr' | 'en') {
return Issue.removeIssue(userId, data.bookId, data.issueId, data.deletedAt, lang);
}
)
);
// GET /book/worlds - Get worlds for book
interface GetWorldsData {
bookid: string;
}
ipcMain.handle('db:book:worlds:get', createHandler<GetWorldsData, WorldListResponse>(
function(userId: string, data: GetWorldsData, lang: 'fr' | 'en'): WorldListResponse {
return World.getWorlds(userId, data.bookid, lang);
}
)
);
// POST /book/world/add - Add world
ipcMain.handle('db:book:world:add', createHandler<AddWorldData, string>(
function(userId: string, data: AddWorldData, lang: 'fr' | 'en') {
return World.addNewWorld(userId, data.bookId, data.worldName, lang, data.id, data.seriesWorldId || null);
}
)
);
// POST /book/world/element/add - Add element to world
ipcMain.handle('db:book:world:element:add', createHandler<AddWorldElementData, string>(
function(userId: string, data: AddWorldElementData, lang: 'fr' | 'en') {
return World.addNewElementToWorld(
userId,
data.worldId,
data.elementName,
data.elementType.toString(),
lang,
data.id
);
}
)
);
// DELETE /book/world/element/delete - Remove element from world
interface RemoveWorldElementData {
elementId: string;
bookId: string;
deletedAt: number;
}
ipcMain.handle('db:book:world:element:remove', createHandler<RemoveWorldElementData, boolean>(
function(userId: string, data: RemoveWorldElementData, lang: 'fr' | 'en') {
return World.removeElementFromWorld(userId, data.bookId, data.elementId, data.deletedAt, lang);
}
)
);
// DELETE /book/delete - Delete book
interface DeleteBookData {
id: string;
deletedAt: number;
}
ipcMain.handle('db:book:delete', createHandler<DeleteBookData, boolean>(
function(userId: string, data: DeleteBookData, lang: 'fr' | 'en') {
return Book.removeBook(userId, data.id, data.deletedAt, lang);
}
)
);
// GET /book/ai/guideline - Get AI guideline
interface GetAIGuidelineData {
id: string;
}
ipcMain.handle('db:book:guideline:ai:get', createHandler<GetAIGuidelineData, GuideLineAI>(
function(userId: string, data: GetAIGuidelineData, lang: 'fr' | 'en') {
return GuideLine.getGuideLineAI(userId, data.id, lang);
}
)
);
// POST /book/ai/guideline (set) - Set AI guideline
ipcMain.handle('db:book:guideline:ai:set', createHandler<SetAIGuideLineData, boolean>(
function(userId: string, data: SetAIGuideLineData, lang: 'fr' | 'en') {
return GuideLine.setAIGuideLine(
userId,
data.bookId,
data.narrativeType,
data.dialogueType,
data.plotSummary,
data.toneAtmosphere,
data.verbTense,
data.language,
data.themes,
lang
);
}
)
);
// PUT /book/world/update - Update world
ipcMain.handle('db:book:world:update', createHandler<UpdateWorldData, boolean>(
function(userId: string, data: UpdateWorldData, lang: 'fr' | 'en') {
return World.updateWorld(userId, data.world, lang);
}
)
);
// PATCH /book/tool-setting - Update book tool setting
ipcMain.handle('db:book:tool:update', createHandler<UpdateBookToolData, boolean>(
function(userId: string, data: UpdateBookToolData, lang: 'fr' | 'en') {
return Book.updateBookToolSetting(userId, data.bookId, data.toolName, data.enabled, lang);
}
)
);
// 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;
}
)
);