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:
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user