Remove unused components and models for improved maintainability

- Deleted redundant components (`AddActionButton`, `AlertBox`, `AlertStack`, `BackButton`, `CancelButton`, and `CollapsableArea`) and related files.
- Removed unused models (`Book`, `BookSerie`, `BookTables`, `Character`, and `Chapter`) to reduce codebase clutter.
- Updated project structure and references to reflect these removals.
This commit is contained in:
natreex
2026-03-22 22:37:31 -04:00
parent e8aaef108b
commit 64ed90d993
229 changed files with 15091 additions and 21289 deletions

View File

@@ -1,228 +1,288 @@
'use client'
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import {Check} from 'lucide-react';
import Button from '@/components/ui/Button';
import PulseLoader from '@/components/ui/PulseLoader';
import {useTranslations} from '@/lib/i18n';
import {BookContext, BookContextProps} from '@/context/BookContext';
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
import {configs, isDesktop} from '@/lib/configs';
import {apiGet} from '@/lib/api/client';
import * as tauri from '@/lib/tauri';
import React, {useCallback, useContext, useEffect, useState} from 'react';
import {useTranslations} from 'next-intl';
import {BookContext} from '@/context/BookContext';
import {AlertContext} from '@/context/AlertContext';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faDownload, faSpinner} from '@fortawesome/free-solid-svg-icons';
import {
ChapterExportInfo,
ChapterExportSelection,
chapterVersions,
ExportFormat
} from '@/lib/models/Chapter';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {LangContext, LangContextProps} from '@/context/LangContext';
import {ChapterExportInfo, ChapterExportSelection, ExportFormat,} from '@/lib/types/chapter';
import {chapterVersions} from '@/lib/constants/chapter';
import {SelectBoxProps} from '@/components/form/SelectBox';
const exportFormats: {value: ExportFormat; label: string}[] = [
{value: 'epub', label: 'EPUB'},
{value: 'pdf', label: 'PDF'},
{value: 'docx', label: 'DOCX'},
];
const formats: ExportFormat[] = ['epub', 'pdf', 'docx'];
const formatExtensions: Record<ExportFormat, string> = {
epub: '.epub',
pdf: '.pdf',
docx: '.docx',
};
export default function ExportSetting(): React.JSX.Element {
const t = useTranslations();
const {book} = useContext(BookContext);
const {errorMessage, successMessage} = useContext(AlertContext);
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {successMessage, errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const [format, setFormat] = useState<ExportFormat>('epub');
const [chapters, setChapters] = useState<ChapterExportInfo[]>([]);
const [selections, setSelections] = useState<ChapterExportSelection[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [selectedFormat, setSelectedFormat] = useState<ExportFormat>('epub');
const [chaptersExportInfo, setChaptersExportInfo] = useState<ChapterExportInfo[]>([]);
const [chapterSelections, setChapterSelections] = useState<ChapterExportSelection[]>([]);
const [isLoadingChapters, setIsLoadingChapters] = useState<boolean>(true);
const [isExporting, setIsExporting] = useState<boolean>(false);
const loadChapters = useCallback(async (): Promise<void> => {
if (!book) return;
setIsLoading(true);
try {
const chaptersInfo: ChapterExportInfo[] = await tauri.getBookExportInfo(book.bookId) as ChapterExportInfo[];
setChapters(chaptersInfo);
const initialSelections: ChapterExportSelection[] = chaptersInfo.map(
(ch: ChapterExportInfo): ChapterExportSelection => ({
chapterId: ch.chapterId,
version: ch.availableVersions[ch.availableVersions.length - 1],
selected: true
})
);
setSelections(initialSelections);
} catch {
errorMessage(t('exportOption.error'));
} finally {
setIsLoading(false);
const selectedCount: number = useMemo(
() => chapterSelections.filter((selection: ChapterExportSelection): boolean => selection.selected).length,
[chapterSelections],
);
const allSelected: boolean = useMemo(
() => chapterSelections.length > 0 && chapterSelections.every((selection: ChapterExportSelection): boolean => selection.selected),
[chapterSelections],
);
const canExport: boolean = useMemo(
() => !isExporting && selectedCount > 0,
[isExporting, selectedCount],
);
const fetchChaptersExportInfo = useCallback(async (): Promise<void> => {
if (!book?.bookId) {
setIsLoadingChapters(false);
return;
}
}, [book]);
useEffect((): void => {
loadChapters();
}, [loadChapters]);
function toggleChapter(chapterId: string): void {
setSelections((prev: ChapterExportSelection[]): ChapterExportSelection[] =>
prev.map((s: ChapterExportSelection): ChapterExportSelection =>
s.chapterId === chapterId ? {...s, selected: !s.selected} : s
)
);
}
function toggleAll(selectAll: boolean): void {
setSelections((prev: ChapterExportSelection[]): ChapterExportSelection[] =>
prev.map((s: ChapterExportSelection): ChapterExportSelection => ({...s, selected: selectAll}))
);
}
function updateVersion(chapterId: string, version: number): void {
setSelections((prev: ChapterExportSelection[]): ChapterExportSelection[] =>
prev.map((s: ChapterExportSelection): ChapterExportSelection =>
s.chapterId === chapterId ? {...s, version} : s
)
);
}
function getVersionLabel(version: number): string {
const found = chapterVersions.find((v) => v.value === String(version));
return found ? t(found.label) : `v${version}`;
}
async function handleExport(): Promise<void> {
if (!book) return;
setIsExporting(true);
setIsLoadingChapters(true);
try {
const selectedChapters = selections
.filter((s: ChapterExportSelection): boolean => s.selected)
.map((s: ChapterExportSelection) => ({chapterId: s.chapterId, version: s.version}));
const result: boolean = await tauri.exportBook({
bookId: book.bookId,
format,
selections: selectedChapters.length === chapters.length ? null : selectedChapters
});
if (result) {
successMessage(t('exportOption.success'));
let data: ChapterExportInfo[];
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
data = await tauri.getBookExportInfo(book.bookId) as ChapterExportInfo[];
} else {
data = await apiGet<ChapterExportInfo[]>(
'book/chapters/export-info', session.accessToken, lang, {id: book.bookId}
);
}
setChaptersExportInfo(data);
setChapterSelections(
data.map((chapter: ChapterExportInfo): ChapterExportSelection => ({
chapterId: chapter.chapterId,
version: chapter.availableVersions.length > 0
? chapter.availableVersions[chapter.availableVersions.length - 1]
: 1,
selected: true,
})),
);
} catch {
errorMessage(t('exportOption.error'));
errorMessage(t('exportOption.serverError'));
} finally {
setIsLoadingChapters(false);
}
}, [book?.bookId, session.accessToken, lang, errorMessage, t]);
useEffect((): void => {
fetchChaptersExportInfo();
}, [fetchChaptersExportInfo]);
function toggleChapterSelection(chapterId: string): void {
setChapterSelections((previous: ChapterExportSelection[]): ChapterExportSelection[] =>
previous.map((selection: ChapterExportSelection): ChapterExportSelection =>
selection.chapterId === chapterId
? {...selection, selected: !selection.selected}
: selection,
),
);
}
function toggleAllChapters(): void {
const newSelected: boolean = !allSelected;
setChapterSelections((previous: ChapterExportSelection[]): ChapterExportSelection[] =>
previous.map((selection: ChapterExportSelection): ChapterExportSelection => ({
...selection,
selected: newSelected,
})),
);
}
function setChapterVersion(chapterId: string, version: number): void {
setChapterSelections((previous: ChapterExportSelection[]): ChapterExportSelection[] =>
previous.map((selection: ChapterExportSelection): ChapterExportSelection =>
selection.chapterId === chapterId
? {...selection, version}
: selection,
),
);
}
function getVersionLabel(version: number): string {
const match: SelectBoxProps | undefined = chapterVersions.find(
(chapterVersion: SelectBoxProps): boolean => chapterVersion.value === String(version),
);
return match ? t(match.label) : `V${version}`;
}
async function handleExport(): Promise<void> {
if (!book?.bookId) {
errorMessage(t('exportOption.noBookSelected'));
return;
}
if (selectedCount === 0) {
errorMessage(t('exportOption.noChaptersSelected'));
return;
}
setIsExporting(true);
const selectedChapters: { chapterId: string; version: number }[] = chapterSelections
.filter((selection: ChapterExportSelection): boolean => selection.selected)
.map((selection: ChapterExportSelection): { chapterId: string; version: number } => ({
chapterId: selection.chapterId,
version: selection.version,
}));
try {
const response: Response = await fetch(`${configs.apiUrl}book/export`, {
method: 'POST',
headers: {
Authorization: `Bearer ${session.accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
bookId: book.bookId,
format: selectedFormat,
chapters: selectedChapters,
}),
});
if (!response.ok) {
errorMessage(t('exportOption.downloadError'));
return;
}
const blob: Blob = await response.blob();
const bookName: string = book.subTitle ? `${book.title} - ${book.subTitle}` : book.title;
const fileName: string = `${bookName}${formatExtensions[selectedFormat]}`;
const virtualUrl: string = window.URL.createObjectURL(blob);
const downloadLink: HTMLAnchorElement = document.createElement('a');
downloadLink.href = virtualUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
downloadLink.remove();
window.URL.revokeObjectURL(virtualUrl);
successMessage(t('exportOption.downloadSuccess', {format: selectedFormat.toUpperCase()}));
} catch {
errorMessage(t('exportOption.unknownError'));
} finally {
setIsExporting(false);
}
}
const allSelected: boolean = selections.every((s: ChapterExportSelection): boolean => s.selected);
const hasSelection: boolean = selections.some((s: ChapterExportSelection): boolean => s.selected);
return (
<div className="space-y-6 p-4">
<div className="p-4 space-y-6">
<div>
<h3 className="text-text-primary font-semibold mb-2">{t('exportOption.format')}</h3>
<h3 className="text-sm font-semibold text-text-primary mb-3">
{t('exportOption.formatLabel')}
</h3>
<div className="flex gap-3">
{exportFormats.map(({value, label}) => (
<button
key={value}
type="button"
onClick={(): void => setFormat(value)}
className={`px-4 py-2 rounded-lg font-medium transition-all duration-200 ${
format === value
? 'bg-primary text-white shadow-md'
: 'bg-secondary/30 text-text-secondary hover:bg-secondary/50'
}`}
{formats.map((format: ExportFormat) => (
<Button
key={format}
variant={selectedFormat === format ? 'primary' : 'secondary'}
size="sm"
onClick={(): void => setSelectedFormat(format)}
>
{label}
</button>
{format.toUpperCase()}
</Button>
))}
</div>
</div>
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-text-primary font-semibold">{t('exportOption.chapters')}</h3>
{chapters.length > 0 && (
<button
type="button"
onClick={(): void => toggleAll(!allSelected)}
className="text-sm text-primary hover:text-primary/80 transition-colors"
>
{allSelected ? t('exportOption.deselectAll') : t('exportOption.selectAll')}
</button>
)}
</div>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<FontAwesomeIcon icon={faSpinner} className="w-6 h-6 text-primary animate-spin mr-2"/>
<span className="text-text-secondary">{t('exportOption.loadingChapters')}</span>
</div>
) : chapters.length === 0 ? (
<p className="text-text-secondary text-center py-8">{t('exportOption.noChapters')}</p>
<h3 className="text-sm font-semibold text-text-primary mb-3">
{t('exportOption.chapters')}
</h3>
{isLoadingChapters ? (
<PulseLoader text={t('exportOption.loadingChapters')} size="sm"/>
) : chaptersExportInfo.length === 0 ? (
<p className="text-text-secondary text-sm text-center py-8">
{t('exportOption.noChaptersAvailable')}
</p>
) : (
<div className="space-y-2 max-h-96 overflow-y-auto pr-1">
{chapters.map((chapter: ChapterExportInfo) => {
const selection: ChapterExportSelection | undefined = selections.find(
(s: ChapterExportSelection): boolean => s.chapterId === chapter.chapterId
);
if (!selection) return null;
<>
<div className="mb-3">
<Button variant="ghost" size="sm" onClick={toggleAllChapters}>
{allSelected ? t('exportOption.deselectAll') : t('exportOption.selectAll')}
</Button>
</div>
return (
<div
key={chapter.chapterId}
className={`flex items-center justify-between p-3 rounded-lg transition-all duration-200 ${
selection.selected
? 'bg-primary/10 border border-primary/30'
: 'bg-secondary/20 border border-transparent'
}`}
>
<label className="flex items-center gap-3 cursor-pointer flex-1">
<input
type="checkbox"
checked={selection.selected}
onChange={(): void => toggleChapter(chapter.chapterId)}
className="w-4 h-4 rounded accent-primary cursor-pointer"
/>
<span className={`text-sm ${
selection.selected ? 'text-text-primary' : 'text-text-secondary'
}`}>
{chapter.title}
</span>
</label>
{chapter.availableVersions.length > 1 && selection.selected && (
<select
value={selection.version}
onChange={(e: React.ChangeEvent<HTMLSelectElement>): void =>
updateVersion(chapter.chapterId, parseInt(e.target.value, 10))
}
className="text-xs bg-secondary/50 text-text-primary rounded-md px-2 py-1 border border-secondary/30 focus:outline-none focus:border-primary"
<div className="space-y-1">
{chaptersExportInfo.map((chapter: ChapterExportInfo, index: number) => {
const selection: ChapterExportSelection = chapterSelections[index];
return (
<div key={chapter.chapterId} className="border-b border-secondary pb-2">
<button
onClick={(): void => toggleChapterSelection(chapter.chapterId)}
className="flex items-center gap-3 w-full py-2 text-left hover:bg-tertiary rounded-lg px-2 transition-all"
>
{chapter.availableVersions.map((v: number) => (
<option key={v} value={v}>
{getVersionLabel(v)}
</option>
))}
</select>
)}
</div>
);
})}
</div>
<div
className={`w-5 h-5 rounded flex items-center justify-center border transition-colors ${
selection.selected
? 'bg-primary/20 border-primary'
: 'border-secondary bg-tertiary'
}`}>
{selection.selected && (
<Check className="w-3 h-3 text-text-primary" strokeWidth={1.75}/>
)}
</div>
<span className={`text-sm font-medium truncate ${
selection.selected ? 'text-text-primary' : 'text-text-secondary'
}`}>
{chapter.chapterOrder !== -1 ? `${chapter.chapterOrder}. ` : ''}
{chapter.title}
</span>
</button>
{selection.selected && chapter.availableVersions.length > 1 && (
<div className="flex flex-wrap gap-2 ml-10 mt-1 mb-1">
{chapter.availableVersions.map((version: number) => (
<button
key={version}
onClick={(): void => setChapterVersion(chapter.chapterId, version)}
className={`px-3 py-1 rounded-full text-xs font-medium transition-all border ${
selection.version === version
? 'bg-primary/20 text-primary border-primary/50'
: 'bg-tertiary text-text-secondary border-secondary hover:bg-secondary'
}`}
>
{getVersionLabel(version)}
</button>
))}
</div>
)}
</div>
);
})}
</div>
</>
)}
</div>
<button
type="button"
onClick={handleExport}
disabled={isExporting || !hasSelection || isLoading}
className="w-full flex items-center justify-center gap-2 bg-primary hover:bg-primary/90 disabled:bg-primary/50 text-white font-semibold py-3 px-6 rounded-xl transition-all duration-200 shadow-md hover:shadow-lg disabled:cursor-not-allowed"
>
{isExporting ? (
<>
<FontAwesomeIcon icon={faSpinner} className="w-4 h-4 animate-spin"/>
{t('exportOption.exporting')}
</>
) : (
<>
<FontAwesomeIcon icon={faDownload} className="w-4 h-4"/>
{t('exportOption.export')}
</>
)}
</button>
<Button variant="primary" size="lg" onClick={handleExport} disabled={!canExport}
isLoading={isExporting} loadingText={t('exportOption.exporting')}
fullWidth>
{t('exportOption.exportButton')} {selectedFormat.toUpperCase()}
{selectedCount > 0 ? ` (${selectedCount})` : ''}
</Button>
</div>
);
}