Compare commits

...

4 Commits

36 changed files with 273 additions and 59 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.27",
"version": "0.9.28",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -261,5 +261,8 @@
"Export Annotations": "تصدير التعليقات",
"Auto Import on File Open": "استيراد تلقائي عند فتح الملف",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "تبديل منطقة التقليب بالنقر"
"Swap Click-to-Flip Area": "تبديل منطقة التقليب بالنقر",
"No chapters detected.": "لم يتم التعرف على فصول.",
"Failed to parse the EPUB file.": "فشل في تحليل ملف EPUB.",
"This book format is not supported.": "هذا تنسيق الكتاب غير مدعوم."
}
@@ -261,5 +261,8 @@
"Export Annotations": "Anmerkungen exportieren",
"Auto Import on File Open": "Automatischer Import beim Öffnen einer Datei",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "Klick-zu-Blättern-Bereich tauschen"
"Swap Click-to-Flip Area": "Klick-zu-Blättern-Bereich tauschen",
"No chapters detected.": "Keine Kapitel erkannt.",
"Failed to parse the EPUB file.": "Fehler beim Parsen der EPUB-Datei.",
"This book format is not supported.": "Dieses Buchformat wird nicht unterstützt."
}
@@ -261,5 +261,8 @@
"Export Annotations": "Εξαγωγή σχολιασμών",
"Auto Import on File Open": "Αυτόματη εισαγωγή κατά το άνοιγμα αρχείου",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "Ανταλλαγή περιοχής κλικ για γύρισμα σελίδας"
"Swap Click-to-Flip Area": "Ανταλλαγή περιοχής κλικ για γύρισμα σελίδας",
"No chapters detected.": "Δεν εντοπίστηκαν κεφάλαια.",
"Failed to parse the EPUB file.": "Αποτυχία ανάλυσης του αρχείου EPUB.",
"This book format is not supported.": "Αυτή η μορφή βιβλίου δεν υποστηρίζεται."
}
@@ -261,5 +261,8 @@
"Export Annotations": "Exportar anotaciones",
"Auto Import on File Open": "Importación automática al abrir archivo",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "Intercambiar área de clic para voltear"
"Swap Click-to-Flip Area": "Intercambiar área de clic para voltear",
"No chapters detected.": "No se detectaron capítulos.",
"Failed to parse the EPUB file.": "Error al analizar el archivo EPUB.",
"This book format is not supported.": "Este formato de libro no es compatible."
}
@@ -261,5 +261,8 @@
"Export Annotations": "Exporter les annotations",
"Auto Import on File Open": "Importation automatique à l'ouverture du fichier",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "Échanger la zone de clic pour tourner"
"Swap Click-to-Flip Area": "Échanger la zone de clic pour tourner",
"No chapters detected.": "Aucun chapitre détecté.",
"Failed to parse the EPUB file.": "Échec de l'analyse du fichier EPUB.",
"This book format is not supported.": "Ce format de livre n'est pas pris en charge."
}
@@ -261,5 +261,8 @@
"Export Annotations": "टिप्पणियाँ निर्यात करें",
"Auto Import on File Open": "फ़ाइल खोलने पर स्वचालित आयात",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "क्लिक-टू-फ्लिप क्षेत्र स्वैप करें"
"Swap Click-to-Flip Area": "क्लिक-टू-फ्लिप क्षेत्र स्वैप करें",
"No chapters detected.": "कोई अध्याय नहीं मिला।",
"Failed to parse the EPUB file.": "EPUB फ़ाइल को पार्स करने में विफल।",
"This book format is not supported.": "यह पुस्तक प्रारूप समर्थित नहीं है।"
}
@@ -261,5 +261,8 @@
"Export Annotations": "Ekspor Anotasi",
"Auto Import on File Open": "Impor Otomatis saat File Dibuka",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "Tukar Area Klik untuk Membalik"
"Swap Click-to-Flip Area": "Tukar Area Klik untuk Membalik",
"No chapters detected.": "Tidak ada bab yang terdeteksi.",
"Failed to parse the EPUB file.": "Gagal mengurai file EPUB.",
"This book format is not supported.": "Format buku ini tidak didukung."
}
@@ -261,5 +261,8 @@
"Export Annotations": "Esporta annotazioni",
"Auto Import on File Open": "Importazione automatica all'apertura del file",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "Scambia area click per voltare pagina"
"Swap Click-to-Flip Area": "Scambia area click per voltare pagina",
"No chapters detected.": "Nessun capitolo rilevato.",
"Failed to parse the EPUB file.": "Impossibile analizzare il file EPUB.",
"This book format is not supported.": "Questo formato di libro non è supportato."
}
@@ -261,5 +261,8 @@
"Export Annotations": "注釈をエクスポート",
"Auto Import on File Open": "ファイルを開くと自動インポート",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "クリックめくりエリアを入れ替え"
"Swap Click-to-Flip Area": "クリックめくりエリアを入れ替え",
"No chapters detected.": "章が検出されません。",
"Failed to parse the EPUB file.": "EPUBファイルの解析に失敗しました。",
"This book format is not supported.": "この書籍形式はサポートされていません。"
}
@@ -261,5 +261,8 @@
"Export Annotations": "주석 내보내기",
"Auto Import on File Open": "파일 열 때 자동 가져오기",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "클릭-뒤집기 영역 교환"
"Swap Click-to-Flip Area": "클릭-뒤집기 영역 교환",
"No chapters detected.": "장 발견되지 않음.",
"Failed to parse the EPUB file.": "EPUB 파일 구문 분석 실패.",
"This book format is not supported.": "이 책 형식은 지원되지 않습니다."
}
@@ -261,5 +261,8 @@
"Export Annotations": "Eksportuj adnotacje",
"Auto Import on File Open": "Automatyczne importowanie przy otwieraniu pliku",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "Zamień obszar kliknięcia na przewracanie"
"Swap Click-to-Flip Area": "Zamień obszar kliknięcia na przewracanie",
"No chapters detected.": "Nie wykryto rozdziałów.",
"Failed to parse the EPUB file.": "Nie udało się przeanalizować pliku EPUB.",
"This book format is not supported.": "Ten format książki nie jest obsługiwany."
}
@@ -261,5 +261,8 @@
"Export Annotations": "Exportar Anotações",
"Auto Import on File Open": "Importação Automática ao Abrir Arquivo",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "Trocar Área de Clique para Virar"
"Swap Click-to-Flip Area": "Trocar Área de Clique para Virar",
"No chapters detected.": "Nenhum capítulo detectado.",
"Failed to parse the EPUB file.": "Falha ao analisar o arquivo EPUB.",
"This book format is not supported.": "Este formato de livro não é suportado."
}
@@ -261,5 +261,8 @@
"Export Annotations": "Экспортировать аннотации",
"Auto Import on File Open": "Автоматический импорт при открытии файла",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "Переключить область клика для переворота"
"Swap Click-to-Flip Area": "Переключить область клика для переворота",
"No chapters detected.": "Не обнаружено глав.",
"Failed to parse the EPUB file.": "Не удалось разобрать файл EPUB.",
"This book format is not supported.": "Этот формат книги не поддерживается."
}
@@ -261,5 +261,8 @@
"Export Annotations": "Notları Dışa Aktar",
"Auto Import on File Open": "Dosya Açıldığında Otomatik İçe Aktar",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "Tıkla-Çevir Alanını Değiştir"
"Swap Click-to-Flip Area": "Tıkla-Çevir Alanını Değiştir",
"No chapters detected.": "Bölüm bulunamadı.",
"Failed to parse the EPUB file.": "EPUB dosyası ayrıştırılamadı.",
"This book format is not supported.": "Bu kitap formatı desteklenmiyor."
}
@@ -261,5 +261,8 @@
"Export Annotations": "Експортувати анотації",
"Auto Import on File Open": "Автоматичний імпорт при відкритті файлу",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "Поміняти місцями область кліку для перевороту"
"Swap Click-to-Flip Area": "Поміняти місцями область кліку для перевороту",
"No chapters detected.": "Не виявлено жодного розділу.",
"Failed to parse the EPUB file.": "Не вдалося розібрати файл EPUB.",
"This book format is not supported.": "Цей формат книги не підтримується."
}
@@ -261,5 +261,8 @@
"Export Annotations": "Xuất chú thích",
"Auto Import on File Open": "Tự động nhập khi mở tệp",
"LXGW WenKai GB Screen": "LXGW WenKai",
"Swap Click-to-Flip Area": "Hoán đổi khu vực nhấp để lật trang"
"Swap Click-to-Flip Area": "Hoán đổi khu vực nhấp để lật trang",
"No chapters detected.": "Không phát hiện thấy chương nào.",
"Failed to parse the EPUB file.": "Không thể phân tích tệp EPUB.",
"This book format is not supported.": "Định dạng sách này không được hỗ trợ."
}
@@ -261,5 +261,8 @@
"Export Annotations": "导出笔记",
"Auto Import on File Open": "打开文件时自动导入",
"LXGW WenKai GB Screen": "霞鹜文楷",
"Swap Click-to-Flip Area": "交换点击翻页区域"
"Swap Click-to-Flip Area": "交换点击翻页区域",
"No chapters detected.": "未检测到章节。",
"Failed to parse the EPUB file.": "无法解析 EPUB 文件。",
"This book format is not supported.": "不支持此书籍格式。"
}
@@ -261,5 +261,8 @@
"Export Annotations": "匯出筆記",
"Auto Import on File Open": "打開文件時自動導入",
"LXGW WenKai GB Screen": "霞鶩文楷",
"Swap Click-to-Flip Area": "交換點擊翻頁區域"
"Swap Click-to-Flip Area": "交換點擊翻頁區域",
"No chapters detected.": "未檢測到章節。",
"Failed to parse the EPUB file.": "無法解析 EPUB 文件。",
"This book format is not supported.": "不支援此書籍格式。"
}
+8
View File
@@ -1,5 +1,13 @@
{
"releases": {
"0.9.28": {
"date": "2025-03-30",
"notes": [
"Improved vertical reading mode with better support for Chinese punctuations",
"Fixed TXT file importing issue on desktop apps — TXT books now load reliably and display as expected",
"Improved annotation experience — highlighting tools are now properly dismissed when adding notes to avoid UI conflicts"
]
},
"0.9.27": {
"date": "2025-03-29",
"notes": [
+13 -3
View File
@@ -296,6 +296,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const importBooks = async (files: (string | File)[]) => {
setLoading(true);
const failedFiles = [];
const errorMap: [string, string][] = [
['No chapters detected.', _('No chapters detected.')],
['Failed to parse EPUB.', _('Failed to parse the EPUB file.')],
['Unsupported format.', _('This book format is not supported.')],
];
for (const file of files) {
try {
const book = await appService?.importBook(file, libraryBooks);
@@ -308,10 +313,15 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const filename = typeof file === 'string' ? file : file.name;
const baseFilename = getBaseFilename(filename);
failedFiles.push(baseFilename);
const errorMessage =
error instanceof Error
? errorMap.find(([substring]) => error.message.includes(substring))?.[1] || ''
: '';
eventDispatcher.dispatch('toast', {
message: _('Failed to import book(s): {{filenames}}', {
filenames: listFormater(false).format(failedFiles),
}),
message:
_('Failed to import book(s): {{filenames}}', {
filenames: listFormater(false).format(failedFiles),
}) + (errorMessage ? `\n${errorMessage}` : ''),
type: 'error',
});
console.error('Failed to import book:', filename, error);
@@ -9,7 +9,7 @@ import { useClickEvent, useTouchEvent } from '../hooks/useIframeEvents';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { useProgressSync } from '../hooks/useProgressSync';
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
import { getStyles, mountAdditionalFonts } from '@/utils/style';
import { getStyles, mountAdditionalFonts, transformStylesheet } from '@/utils/style';
import { getBookDirFromLanguage, getBookDirFromWritingMode } from '@/utils/book';
import { useUICSS } from '@/hooks/useUICSS';
import {
@@ -23,6 +23,7 @@ import {
handleTouchEnd,
} from '../utils/iframeEventHandlers';
import { getMaxInlineSize } from '@/utils/config';
import { transformContent } from '@/services/transformService';
const FoliateViewer: React.FC<{
bookKey: string;
@@ -53,6 +54,28 @@ const FoliateViewer: React.FC<{
setProgress(bookKey, detail.cfi, detail.tocItem, detail.section, detail.location, detail.range);
};
const docTransformHandler = (event: Event) => {
const { detail } = event as CustomEvent;
detail.data = Promise.resolve(detail.data)
.then((data) => {
const viewSettings = getViewSettings(bookKey)!;
if (detail.type === 'text/css') return transformStylesheet(data);
if (detail.type === 'application/xhtml+xml') {
const ctx = {
bookKey,
viewSettings,
content: data,
};
return Promise.resolve(transformContent(ctx));
}
return data;
})
.catch((e) => {
console.error(new Error(`Failed to load ${detail.name}`, { cause: e }));
return '';
});
};
const docLoadHandler = (event: Event) => {
const detail = (event as CustomEvent).detail;
console.log('doc loaded:', detail);
@@ -154,6 +177,9 @@ const FoliateViewer: React.FC<{
viewRef.current = view;
setFoliateView(bookKey, view);
const { book } = view;
book.transformTarget?.addEventListener('data', docTransformHandler);
view.renderer.setStyles?.(getStyles(viewSettings));
const isScrolled = viewSettings.scrolled!;
@@ -406,11 +406,10 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
if (!selection || !selection.text) return;
const { sectionHref: href } = progress;
selection.href = href;
setShowAnnotPopup(false);
setHighlightOptionsVisible(false);
handleHighlight(true);
setNotebookVisible(true);
setNotebookNewAnnotation(selection);
handleHighlight(true);
handleDismissPopup();
};
const handleSearch = () => {
@@ -122,7 +122,9 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
if (writingMode === viewSettings.writingMode) return;
// global settings are not supported for writing mode
const prevWritingMode = viewSettings.writingMode;
if (!writingMode.includes('vertical')) {
if (writingMode.includes('vertical')) {
viewSettings.vertical = true;
} else {
viewSettings.vertical = false;
}
saveViewSettings(envConfig, bookKey, 'writingMode', writingMode, true);
@@ -277,8 +277,8 @@ const TTSControl = () => {
className={clsx(
'fixed right-6 h-12 w-12',
appService?.hasSafeAreaInset
? 'bottom-[calc(env(safe-area-inset-bottom)+48px)]'
: 'bottom-12',
? 'bottom-[calc(env(safe-area-inset-bottom)+70px)]'
: 'bottom-[70px] sm:bottom-14',
)}
>
<TTSIcon isPlaying={isPlaying} onClick={togglePopup} />
+14 -14
View File
@@ -21,7 +21,20 @@ const Popup = ({
}) => (
<div>
<div
className={`triangle text-base-300 absolute z-40 ${triangleClassName}`}
id='popup-container'
className={`bg-base-300 absolute rounded-lg font-sans shadow-xl ${className}`}
style={{
width: `${width}px`,
height: `${height}px`,
left: `${position ? position.point.x : -999}px`,
top: `${position ? position.point.y : -999}px`,
...additionalStyle,
}}
>
{children}
</div>
<div
className={`triangle text-base-300 absolute ${triangleClassName}`}
style={{
left:
trianglePosition?.dir === 'left'
@@ -65,19 +78,6 @@ const Popup = ({
: 'translateX(-50%)',
}}
/>
<div
id='popup-container'
className={`bg-base-300 absolute z-30 rounded-lg font-sans shadow-xl ${className}`}
style={{
width: `${width}px`,
height: `${height}px`,
left: `${position ? position.point.x : -999}px`,
top: `${position ? position.point.y : -999}px`,
...additionalStyle,
}}
>
{children}
</div>
</div>
);
+6 -1
View File
@@ -69,7 +69,12 @@ export const Toast = () => {
messageClass.current,
)}
>
{toastMessage}
{toastMessage.split('\n').map((line, idx) => (
<React.Fragment key={idx}>
{line || <>&nbsp;</>}
<br />
</React.Fragment>
))}
</span>
</div>
</div>
+1
View File
@@ -66,6 +66,7 @@ export interface BookDoc {
dir: string;
toc?: Array<TOCItem>;
sections?: Array<SectionItem>;
transformTarget?: EventTarget;
splitTOCHref(href: string): Array<string | number>;
getCover(): Promise<Blob | null>;
}
@@ -0,0 +1,16 @@
import { activeTransformers } from './transformers';
import { TransformContext } from './transformers/types';
export const transformContent = async (ctx: TransformContext): Promise<string> => {
let transformed = ctx.content;
for (const transformer of activeTransformers) {
try {
transformed = await transformer.transform({ ...ctx, content: transformed });
} catch (error) {
console.warn(`Error in transformer ${transformer.name}:`, error);
}
}
return transformed;
};
@@ -0,0 +1,9 @@
import type { Transformer } from './types';
import { translateTransformer } from './translate';
import { punctuationTransformer } from './punctuation';
export const activeTransformers: Transformer[] = [
punctuationTransformer,
translateTransformer,
// Add more transformers here
];
@@ -0,0 +1,41 @@
import type { Transformer } from './types';
const punctuationMap: Record<string, string> = {
'': '︐', // full-width comma
'。': '︒', // full stop
'、': '︑', // enumeration comma
'': '︓', // colon
'': '︔', // semicolon
'': '︕', // exclamation
'': '︖', // question
'': '︵',
'': '︶',
'《': '︽',
'》': '︾',
'「': '﹁',
'」': '﹂',
'『': '﹃',
'』': '﹄',
'“': '﹁',
'”': '﹂',
'': '﹃',
'': '﹄',
};
export const punctuationTransformer: Transformer = {
name: 'punctuation',
transform: async (ctx) => {
if (!ctx.content.includes('<html')) return ctx.content;
const shouldTransform = ctx.viewSettings.vertical === true;
if (!shouldTransform) return ctx.content;
let result = ctx.content;
for (const [original, vertical] of Object.entries(punctuationMap)) {
result = result.replace(new RegExp(original, 'g'), vertical);
}
return result;
},
};
@@ -0,0 +1,9 @@
import type { Transformer } from './types';
export const translateTransformer: Transformer = {
name: 'translate',
transform: async (ctx) => {
return ctx.content;
},
};
@@ -0,0 +1,12 @@
import { ViewSettings } from '@/types/book';
export type TransformContext = {
bookKey: string;
viewSettings: ViewSettings;
content: string;
};
export type Transformer = {
name: string;
transform: (ctx: TransformContext) => Promise<string>;
};
+33 -3
View File
@@ -207,12 +207,11 @@ const getLayoutStyles = (
background-color: transparent !important;
}
p, li, blockquote, dd {
margin: ${vertical ? `0 ${paragraphMargin}em` : `${paragraphMargin}em 0`} ${overrideLayout ? '!important' : ''};
line-height: ${lineSpacing} ${overrideLayout ? '!important' : ''};
word-spacing: ${wordSpacing}px ${overrideLayout ? '!important' : ''};
letter-spacing: ${letterSpacing}px ${overrideLayout ? '!important' : ''};
text-indent: ${textIndent}em ${overrideLayout ? '!important' : ''};
text-align: ${overrideLayout ? 'var(--default-text-align) !important' : 'inherit'};
text-indent: ${vertical ? textIndent * 1.2 : textIndent}em;
text-align: ${overrideLayout ? 'var(--default-text-align)' : 'inherit'};
-webkit-hyphens: ${hyphenate ? 'auto' : 'manual'};
hyphens: ${hyphenate ? 'auto' : 'manual'};
-webkit-hyphenate-limit-before: 3;
@@ -221,6 +220,12 @@ const getLayoutStyles = (
hanging-punctuation: allow-end last;
widows: 2;
}
p {
${vertical ? `margin-left: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''}
${vertical ? `margin-right: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''}
${!vertical ? `margin-top: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''}
${!vertical ? `margin-bottom: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''}
}
li, p:has(> :is(img, video, font, h1, h2, h3, h4, h5, table)) {
text-indent: 0 !important;
}
@@ -240,6 +245,14 @@ const getLayoutStyles = (
display: none;
}
img.pi {
${vertical ? 'transform: rotate(90deg);' : ''}
${vertical ? 'transform-origin: center;' : ''}
${vertical ? 'height: 2em;' : ''}
${vertical ? `width: ${lineSpacing}em;` : ''}
${vertical ? `vertical-align: unset;` : ''}
}
.duokan-footnote-content,
.duokan-footnote-item {
display: none;
@@ -384,3 +397,20 @@ export const mountAdditionalFonts = (document: Document) => {
style.textContent = getAdditionalFontFaces();
document.head.appendChild(style);
};
export const transformStylesheet = (css: string) => {
// replace absolute font sizes with rem units
// replace hardcoded colors
return css
.replace(/font-size\s*:\s*xx-small/gi, 'font-size: 0.6rem')
.replace(/font-size\s*:\s*x-small/gi, 'font-size: 0.75rem')
.replace(/font-size\s*:\s*small/gi, 'font-size: 0.875rem')
.replace(/font-size\s*:\s*medium/gi, 'font-size: 1rem')
.replace(/font-size\s*:\s*large/gi, 'font-size: 1.2rem')
.replace(/font-size\s*:\s*x-large/gi, 'font-size: 1.5rem')
.replace(/font-size\s*:\s*xx-large/gi, 'font-size: 2rem')
.replace(/font-size\s*:\s*xxx-large/gi, 'font-size: 3rem')
.replace(/\scolor\s*:\s*#000000/gi, 'color: var(--theme-fg-color)')
.replace(/\scolor\s*:\s*#000/gi, 'color: var(--theme-fg-color)')
.replace(/\scolor\s*:\s*rgb\(0,\s*0,\s*0\)/gi, 'color: var(--theme-fg-color)');
};
+3 -11
View File
@@ -1,3 +1,4 @@
import { getBaseFilename } from './book';
import { partialMD5 } from './md5';
interface Metadata {
@@ -38,12 +39,12 @@ export class TxtToEpubConverter {
public async convert(options: Txt2EpubOptions): Promise<ConversionResult> {
const { file: txtFile, author: providedAuthor, language: providedLanguage } = options;
const fileContent = await this.readFileAsArrayBuffer(txtFile);
const fileContent = await txtFile.arrayBuffer();
const detectedEncoding = this.detectEncoding(fileContent) || 'utf-8';
const decoder = new TextDecoder(detectedEncoding);
const txtContent = decoder.decode(fileContent).trim();
const bookTitle = this.extractBookTitle(txtFile.name);
const bookTitle = this.extractBookTitle(getBaseFilename(txtFile.name));
const fileName = `${bookTitle}.epub`;
const fileHeader = txtContent.slice(0, 1024);
@@ -358,15 +359,6 @@ export class TxtToEpubConverter {
return 'en';
}
private readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as ArrayBuffer);
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
private extractBookTitle(filename: string): string {
const match = filename.match(/《([^》]+)》/);
return match ? match[1]! : filename.split('.')[0]!;