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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,36 +523,55 @@ export default function BookList() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start justify-center w-full px-4 overflow-x-auto pb-4">
|
||||
{items.map((item, idx) => {
|
||||
if (item.type === 'book' && item.book) {
|
||||
return (
|
||||
<div key={item.book.bookId}
|
||||
{...(idx === 0 && {'data-guide': 'book-card'})}
|
||||
className={`flex-shrink-0 w-64 sm:w-52 md:w-48 lg:w-56 xl:w-64 p-2 box-border ${User.guideTourDone(session.user?.guideTour || [], 'new-first-book') && 'mb-[200px]'}`}>
|
||||
<BookCard
|
||||
book={item.book}
|
||||
syncStatus={detectBookSyncStatus(item.book.bookId)}
|
||||
onClickCallback={handleBookClick}
|
||||
index={idx}
|
||||
<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 (
|
||||
<div key={item.book.bookId}
|
||||
{...(idx === 0 && {'data-guide': 'book-card'})}
|
||||
className={`flex-shrink-0 w-64 sm:w-52 md:w-48 lg:w-56 xl:w-64 p-2 box-border ${User.guideTourDone(session.user?.guideTour || [], 'new-first-book') && 'mb-[200px]'}`}>
|
||||
<BookCard
|
||||
book={item.book}
|
||||
syncStatus={detectBookSyncStatus(item.book.bookId)}
|
||||
onClickCallback={handleBookClick}
|
||||
index={idx}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (item.type === 'series' && item.series) {
|
||||
return (
|
||||
<SeriesCard
|
||||
key={item.series.id}
|
||||
series={item.series}
|
||||
onBookClick={handleBookClick}
|
||||
onSettingsClick={handleSeriesSettingsClick}
|
||||
getSyncStatus={detectBookSyncStatus}
|
||||
seriesSyncStatus={detectSeriesSyncStatus(item.series.id)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (item.type === 'series' && item.series) {
|
||||
return (
|
||||
<SeriesCard
|
||||
key={item.series.id}
|
||||
series={item.series}
|
||||
onBookClick={handleBookClick}
|
||||
onSettingsClick={handleSeriesSettingsClick}
|
||||
getSyncStatus={detectBookSyncStatus}
|
||||
seriesSyncStatus={detectSeriesSyncStatus(item.series.id)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
);
|
||||
}
|
||||
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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user