forked from akai/readest
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5fd37f40d | |||
| 2daee62b80 | |||
| ffd179bb06 | |||
| 4acc0d8e12 |
@@ -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.": "不支援此書籍格式。"
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -69,7 +69,12 @@ export const Toast = () => {
|
||||
messageClass.current,
|
||||
)}
|
||||
>
|
||||
{toastMessage}
|
||||
{toastMessage.split('\n').map((line, idx) => (
|
||||
<React.Fragment key={idx}>
|
||||
{line || <> </>}
|
||||
<br />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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)');
|
||||
};
|
||||
|
||||
@@ -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]!;
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: 605246a375...be37c6c03b
Reference in New Issue
Block a user