Compare commits

..

14 Commits

Author SHA1 Message Date
Huang Xin 8b80c52c54 release: version 0.9.10 (#324) 2025-02-08 18:53:50 +01:00
Huang Xin d3d496b583 mobile: fix TTS icon position on iOS (#322) 2025-02-08 18:21:47 +01:00
Huang Xin ad1365fa23 mobile: fix layout of book detail modal and refresh indicator (#321) 2025-02-08 17:57:49 +01:00
Huang Xin 0ba3b510ce fix: importing fb2.zip file and CSP for embedded cover image (#319)
* fix: don't set window shadow below Win 11

* fix: importing fb2.zip file and CSP for embedded cover image
2025-02-08 16:19:54 +01:00
Huang Xin 1da0817145 mobile: fix sidebar height on iOS (#318)
* mobile: fix sidebar height on iOS

* fix: tts should not resume playing when closing book
2025-02-08 13:32:15 +01:00
Huang Xin 151509b84c mobile: fix iOS safe area insets (#317) 2025-02-08 03:38:17 +01:00
Huang Xin 282b129bf5 mobile: save book config when progress changes on Android (#315) 2025-02-07 23:22:55 +01:00
Huang Xin 7dfd7b9ca2 mobile: fix hasSafeAreaInset as PWA is only for WebAppService (#314) 2025-02-07 22:35:24 +01:00
Huang Xin ce00c3e923 mobile: immersive UI for android (#313) 2025-02-07 22:23:42 +01:00
Huang Xin e2e3418412 mobile: support safe area insets for mobile platforms (#312) 2025-02-07 22:15:18 +01:00
Huang Xin c79657a1ee fix: don't autoscroll to current item in TOC when TTS is on (#311) 2025-02-07 19:08:50 +01:00
Huang Xin dee1e28a61 feat: add options to adjust paragraph margin, word spacing, letter spacing, text indent and font weight (#310) 2025-02-07 18:23:12 +01:00
Huang Xin adfdb10d8e fix: compatibility fix for some formerly imported corrupted metadata (#307)
* fix: compatibility fix for some formerly imported corrupted metadata

* fix: use font family names of sys fonts
2025-02-07 02:54:29 +01:00
Huang Xin 023ba663e3 fix: stop showing annotator popup when speaking with TTS on Android (#306)
* fix: stop showing annotator popup when speaking with TTS on Android

* chore: get rid of the warning complains of rustc
2025-02-07 01:20:25 +01:00
83 changed files with 619 additions and 150 deletions
+13 -2
View File
@@ -5,7 +5,7 @@
<h1>Readest</h1>
<br>
[Readest][link-website] is an open-source ebook reader designed for immersive and deep reading experiences. Built as a modern rewrite of [Foliate](https://github.com/johnfactotum/foliate), it leverages [Next.js 15](https://github.com/vercel/next.js) and [Tauri v2](https://github.com/tauri-apps/tauri) to offer a seamless cross-platform experience on macOS, Windows, Linux and Web, with support for mobile platforms coming soon.
[Readest][link-website] is an open-source ebook reader designed for immersive and deep reading experiences. Built as a modern rewrite of [Foliate](https://github.com/johnfactotum/foliate), it leverages [Next.js 15](https://github.com/vercel/next.js) and [Tauri v2](https://github.com/tauri-apps/tauri) to deliver a smooth, cross-platform experience across macOS, Windows, Linux, Android, iOS, and the Web.
[![Website][badge-website]][link-website]
[![Web App][badge-web-app]][link-web-readest]
@@ -62,7 +62,6 @@
| **Feature** | **Description** | **Priority** |
| ------------------------------- | ------------------------------------------------------------------------------------------ | ------------ |
| **Support iOS and Android** | Expand the APP to work on iOS and Android devices. | 🛠 |
| **Library Management** | Organize, sort, and manage your entire ebook library. | 🛠 |
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🔄 |
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🔄 |
@@ -153,9 +152,21 @@ For Windows targets, “Build Tools for Visual Studio 2022” (or a higher editi
pnpm tauri dev
```
For Android:
```bash
# Initialize the Android environment (run once)
pnpm tauri android init
pnpm tauri android dev
```
For iOS:
```bash
# Set up the iOS environment (run once)
pnpm tauri ios init
pnpm tauri ios dev
```
+13
View File
@@ -19,6 +19,19 @@ const nextConfig = {
// Configure assetPrefix or else the server won't properly resolve your assets.
assetPrefix: '',
reactStrictMode: true,
async headers() {
return [
{
source: '/.well-known/apple-app-site-association',
headers: [
{
key: 'Content-Type',
value: 'application/json',
},
],
},
];
},
};
const withPWA = withPWAInit({
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.9",
"version": "0.9.10",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -57,7 +57,6 @@
"Language:": "Sprache:",
"Layout": "Layout",
"Light Mode": "Hellmodus",
"Line Height": "Zeilenhöhe",
"Loading...": "Wird geladen...",
"Loc. {{currentPage}} / {{totalPage}}": "Pos. {{currentPage}} / {{totalPage}}",
"Logged in": "Angemeldet",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "Buch gelöscht: {{title}}",
"Failed to delete book: {{title}}": "Fehler beim Löschen des Buches: {{title}}",
"Check Updates on Start": "Aktualisierungen beim Start prüfen",
"Insufficient storage quota": "Unzureichendes Speicherkontingent"
"Insufficient storage quota": "Unzureichendes Speicherkontingent",
"Font Weight": "Schriftgewicht",
"Line Spacing": "Zeilenabstand",
"Word Spacing": "Wortabstand",
"Letter Spacing": "Buchstabenabstand",
"Text Indent": "Texteinzug",
"Paragraph Margin": "Absatzrand"
}
@@ -57,7 +57,6 @@
"Language:": "Γλώσσα:",
"Layout": "Διάταξη",
"Light Mode": "Φωτεινή λειτουργία",
"Line Height": "Ύψος γραμμής",
"Loading...": "Φόρτωση...",
"Loc. {{currentPage}} / {{totalPage}}": "Θέση {{currentPage}} / {{totalPage}}",
"Logged in": "Συνδεδεμένος",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "Το βιβλίο με τίτλο {{title}} διαγράφηκε",
"Failed to delete book: {{title}}": "Αποτυχία διαγραφής βιβλίου: {{title}}",
"Check Updates on Start": "Έλεγχος ενημερώσεων κατά την εκκίνηση",
"Insufficient storage quota": "Ανεπαρκές όριο αποθήκευσης"
"Insufficient storage quota": "Ανεπαρκές όριο αποθήκευσης",
"Font Weight": "Βάρος γραμματοσειράς",
"Line Spacing": "Διάκενο γραμμών",
"Word Spacing": "Διάκενο λέξεων",
"Letter Spacing": "Διάκενο γραμμάτων",
"Text Indent": "Εσοχή κειμένου",
"Paragraph Margin": "Περιθώριο παραγράφου"
}
@@ -57,7 +57,6 @@
"Language:": "Idioma:",
"Layout": "Diseño",
"Light Mode": "Modo claro",
"Line Height": "Altura de línea",
"Loading...": "Cargando...",
"Loc. {{currentPage}} / {{totalPage}}": "Pos. {{currentPage}} / {{totalPage}}",
"Logged in": "Sesión iniciada",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "Libro eliminado: {{title}}",
"Failed to delete book: {{title}}": "Error al eliminar libro: {{title}}",
"Check Updates on Start": "Comprobar actualizaciones al iniciar",
"Insufficient storage quota": "Cuota de almacenamiento insuficiente"
"Insufficient storage quota": "Cuota de almacenamiento insuficiente",
"Font Weight": "Peso de la fuente",
"Line Spacing": "Espaciado de línea",
"Word Spacing": "Espaciado de palabras",
"Letter Spacing": "Espaciado de letras",
"Text Indent": "Sangría de texto",
"Paragraph Margin": "Margen de párrafo"
}
@@ -57,7 +57,6 @@
"Language:": "Langue :",
"Layout": "Mise en page",
"Light Mode": "Mode clair",
"Line Height": "Hauteur de ligne",
"Loading...": "Chargement...",
"Loc. {{currentPage}} / {{totalPage}}": "Pos. {{currentPage}} / {{totalPage}}",
"Logged in": "Connecté",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "Livre supprimé : {{title}}",
"Failed to delete book: {{title}}": "Échec de la suppression du livre : {{title}}",
"Check Updates on Start": "Vérifier les mises à jour au démarrage",
"Insufficient storage quota": "Quota de stockage insuffisant"
"Insufficient storage quota": "Quota de stockage insuffisant",
"Font Weight": "Poids de la police",
"Line Spacing": "Interligne",
"Word Spacing": "Espacement des mots",
"Letter Spacing": "Espacement des lettres",
"Text Indent": "Retrait du texte",
"Paragraph Margin": "Marge de paragraphe"
}
@@ -57,7 +57,6 @@
"Language:": "भाषा:",
"Layout": "लेआउट",
"Light Mode": "लाइट मोड",
"Line Height": "लाइन हाइट",
"Loading...": "लोड हो रहा है...",
"Loc. {{currentPage}} / {{totalPage}}": "स्थान {{currentPage}} / {{totalPage}}",
"Logged in": "लॉग इन किया",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "पुस्तक हटाई गई: {{title}}",
"Failed to delete book: {{title}}": "पुस्तक हटाने में विफल: {{title}}",
"Check Updates on Start": "शुरू में अपडेट जांचें",
"Insufficient storage quota": "अपर्याप्त स्टोरेज कोटा"
"Insufficient storage quota": "अपर्याप्त स्टोरेज कोटा",
"Font Weight": "फ़ॉन्ट वजन",
"Line Spacing": "लाइन स्पेसिंग",
"Word Spacing": "शब्द स्पेसिंग",
"Letter Spacing": "अक्षर स्पेसिंग",
"Text Indent": "टेक्स्ट इंडेंट",
"Paragraph Margin": "पैराग्राफ मार्जिन"
}
@@ -57,7 +57,6 @@
"Language:": "Bahasa:",
"Layout": "Tata Letak",
"Light Mode": "Mode Terang",
"Line Height": "Tinggi Baris",
"Loading...": "Memuat...",
"Loc. {{currentPage}} / {{totalPage}}": "Lok. {{currentPage}} / {{totalPage}}",
"Logged in": "Sudah Masuk",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "Buku dihapus: {{title}}",
"Failed to delete book: {{title}}": "Gagal menghapus buku: {{title}}",
"Check Updates on Start": "Periksa Pembaruan saat Memulai",
"Insufficient storage quota": "Kuota penyimpanan tidak mencukupi"
"Insufficient storage quota": "Kuota penyimpanan tidak mencukupi",
"Font Weight": "Berat Font",
"Line Spacing": "Spasi Baris",
"Word Spacing": "Spasi Kata",
"Letter Spacing": "Spasi Huruf",
"Text Indent": "Indentasi Teks",
"Paragraph Margin": "Margin Paragraf"
}
@@ -57,7 +57,6 @@
"Language:": "Lingua:",
"Layout": "Layout",
"Light Mode": "Modalità chiara",
"Line Height": "Altezza riga",
"Loading...": "Caricamento...",
"Loc. {{currentPage}} / {{totalPage}}": "Pos. {{currentPage}} / {{totalPage}}",
"Logged in": "Accesso effettuato",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "Libro eliminato: {{title}}",
"Failed to delete book: {{title}}": "Eliminazione libro non riuscita: {{title}}",
"Check Updates on Start": "Controlla aggiornamenti all'avvio",
"Insufficient storage quota": "Quota di archiviazione insufficiente"
"Insufficient storage quota": "Quota di archiviazione insufficiente",
"Font Weight": "Peso font",
"Line Spacing": "Interlinea",
"Word Spacing": "Spaziatura parole",
"Letter Spacing": "Spaziatura lettere",
"Text Indent": "Rientro testo",
"Paragraph Margin": "Margine paragrafo"
}
@@ -57,7 +57,6 @@
"Language:": "言語:",
"Layout": "レイアウト",
"Light Mode": "ライトモード",
"Line Height": "行間",
"Loading...": "読み込み中...",
"Loc. {{currentPage}} / {{totalPage}}": "位置 {{currentPage}} / {{totalPage}}",
"Logged in": "ログイン済み",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "書籍が削除されました:{{title}}",
"Failed to delete book: {{title}}": "書籍の削除に失敗しました:{{title}}",
"Check Updates on Start": "開始時に更新を確認",
"Insufficient storage quota": "ストレージクォータが不足しています"
"Insufficient storage quota": "ストレージクォータが不足しています",
"Font Weight": "フォントウェイト",
"Line Spacing": "行間",
"Word Spacing": "単語間隔",
"Letter Spacing": "文字間隔",
"Text Indent": "テキストインデント",
"Paragraph Margin": "段落マージン"
}
@@ -57,7 +57,6 @@
"Language:": "언어:",
"Layout": "레이아웃",
"Light Mode": "라이트 모드",
"Line Height": "줄 간격",
"Loading...": "로딩 중...",
"Loc. {{currentPage}} / {{totalPage}}": "위치 {{currentPage}} / {{totalPage}}",
"Logged in": "로그인됨",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "책 삭제됨: {{title}}",
"Failed to delete book: {{title}}": "책 삭제 실패: {{title}}",
"Check Updates on Start": "시작 시 업데이트 확인",
"Insufficient storage quota": "저장소 할당량 부족"
"Insufficient storage quota": "저장소 할당량 부족",
"Font Weight": "글꼴 두께",
"Line Spacing": "줄 간격",
"Word Spacing": "단어 간격",
"Letter Spacing": "글자 간격",
"Text Indent": "들여쓰기",
"Paragraph Margin": "단락 여백"
}
@@ -57,7 +57,6 @@
"Language:": "Język:",
"Layout": "Układ",
"Light Mode": "Tryb jasny",
"Line Height": "Wysokość linii",
"Loading...": "Ładowanie...",
"Loc. {{currentPage}} / {{totalPage}}": "Poz. {{currentPage}} / {{totalPage}}",
"Logged in": "Zalogowano",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "Książka usunięta: {{title}}",
"Failed to delete book: {{title}}": "Nie udało się usunąć książki: {{title}}",
"Check Updates on Start": "Sprawdź aktualizacje przy starcie",
"Insufficient storage quota": "Niewystarczająca kwota przechowywania"
"Insufficient storage quota": "Niewystarczająca kwota przechowywania",
"Font Weight": "Grubość czcionki",
"Line Spacing": "Interlinia",
"Word Spacing": "Odstęp między wyrazami",
"Letter Spacing": "Odstęp między literami",
"Text Indent": "Wcięcie tekstu",
"Paragraph Margin": "Margines akapitu"
}
@@ -57,7 +57,6 @@
"Language:": "Idioma:",
"Layout": "Layout",
"Light Mode": "Modo Claro",
"Line Height": "Altura da Linha",
"Loading...": "Carregando...",
"Loc. {{currentPage}} / {{totalPage}}": "Loc. {{currentPage}} / {{totalPage}}",
"Logged in": "Conectado",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "Livro excluído: {{title}}",
"Failed to delete book: {{title}}": "Falha ao excluir livro: {{title}}",
"Check Updates on Start": "Verificar Atualizações ao Iniciar",
"Insufficient storage quota": "Cota de armazenamento insuficiente"
"Insufficient storage quota": "Cota de armazenamento insuficiente",
"Font Weight": "Peso da Fonte",
"Line Spacing": "Espaçamento de Linha",
"Word Spacing": "Espaçamento de Palavra",
"Letter Spacing": "Espaçamento de Letra",
"Text Indent": "Recuo de Texto",
"Paragraph Margin": "Margem de Parágrafo"
}
@@ -57,7 +57,6 @@
"Language:": "Язык:",
"Layout": "Макет",
"Light Mode": "Светлая тема",
"Line Height": "Межстрочный интервал",
"Loading...": "Загрузка...",
"Loc. {{currentPage}} / {{totalPage}}": "Поз. {{currentPage}} / {{totalPage}}",
"Logged in": "Выполнен вход",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "Книга удалена: {{title}}",
"Failed to delete book: {{title}}": "Не удалось удалить книгу: {{title}}",
"Check Updates on Start": "Проверять обновления при запуске",
"Insufficient storage quota": "Недостаточно квоты хранилища"
"Insufficient storage quota": "Недостаточно квоты хранилища",
"Font Weight": "Насыщенность шрифта",
"Line Spacing": "Межстрочный интервал",
"Word Spacing": "Межсловный интервал",
"Letter Spacing": "Межбуквенный интервал",
"Text Indent": "Отступ текста",
"Paragraph Margin": "Отступ абзаца"
}
@@ -57,7 +57,6 @@
"Language:": "Dil:",
"Layout": "Düzen",
"Light Mode": "Aydınlık Mod",
"Line Height": "Satır Yüksekliği",
"Loading...": "Yükleniyor...",
"Loc. {{currentPage}} / {{totalPage}}": "Konum {{currentPage}} / {{totalPage}}",
"Logged in": "Giriş Yapıldı",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "Kitap silindi: {{title}}",
"Failed to delete book: {{title}}": "Kitap silinemedi: {{title}}",
"Check Updates on Start": "Başlangıçta Güncellemeleri Kontrol Et",
"Insufficient storage quota": "Yetersiz depolama kotası"
"Insufficient storage quota": "Yetersiz depolama kotası",
"Font Weight": "Yazı Tipi Ağırlığı",
"Line Spacing": "Satır Aralığı",
"Word Spacing": "Kelime Aralığı",
"Letter Spacing": "Harf Aralığı",
"Text Indent": "Metin Girintisi",
"Paragraph Margin": "Paragraf Kenarı"
}
@@ -57,7 +57,6 @@
"Language:": "Мова:",
"Layout": "Макет",
"Light Mode": "Світлий режим",
"Line Height": "Висота рядка",
"Loading...": "Завантаження...",
"Loc. {{currentPage}} / {{totalPage}}": "Поз. {{currentPage}} / {{totalPage}}",
"Logged in": "Увійшли",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "Книгу видалено: {{title}}",
"Failed to delete book: {{title}}": "Не вдалося видалити книгу: {{title}}",
"Check Updates on Start": "Перевіряти оновлення при запуску",
"Insufficient storage quota": "Недостатньо квоти сховища"
"Insufficient storage quota": "Недостатньо квоти сховища",
"Font Weight": "Насиченість шрифту",
"Line Spacing": "Міжрядковий інтервал",
"Word Spacing": "Міжслівний інтервал",
"Letter Spacing": "Міжбуквений інтервал",
"Text Indent": "Відступ тексту",
"Paragraph Margin": "Відступ абзацу"
}
@@ -57,7 +57,6 @@
"Language:": "Ngôn ngữ:",
"Layout": "Bố cục",
"Light Mode": "Chế độ sáng",
"Line Height": "Độ cao dòng",
"Loading...": "Đang tải...",
"Loc. {{currentPage}} / {{totalPage}}": "Vị trí {{currentPage}} / {{totalPage}}",
"Logged in": "Đã đăng nhập",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "Sách đã xóa: {{title}}",
"Failed to delete book: {{title}}": "Không thể xóa sách: {{title}}",
"Check Updates on Start": "Kiểm tra cập nhật khi khởi động",
"Insufficient storage quota": "Không đủ lượng lưu trữ"
"Insufficient storage quota": "Không đủ lượng lưu trữ",
"Font Weight": "Độ đậm của phông chữ",
"Line Spacing": "Khoảng cách dòng",
"Word Spacing": "Khoảng cách từ",
"Letter Spacing": "Khoảng cách chữ",
"Text Indent": "Thụt lề",
"Paragraph Margin": "Lề đoạn"
}
@@ -57,7 +57,6 @@
"Language:": "语言",
"Layout": "布局",
"Light Mode": "浅色主题",
"Line Height": "行高",
"Loading...": "加载中...",
"Loc. {{currentPage}} / {{totalPage}}": "位置 {{currentPage}} / {{totalPage}}",
"Logged in": "已登录",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "书籍已删除:{{title}}",
"Failed to delete book: {{title}}": "删除书籍失败:{{title}}",
"Check Updates on Start": "启动时检查更新",
"Insufficient storage quota": "云存储空间不足"
"Insufficient storage quota": "云存储空间不足",
"Font Weight": "字重",
"Line Spacing": "行间距",
"Word Spacing": "词间距",
"Letter Spacing": "字间距",
"Text Indent": "首行缩进",
"Paragraph Margin": "段间距"
}
@@ -57,7 +57,6 @@
"Language:": "語言",
"Layout": "版面",
"Light Mode": "淺色主題",
"Line Height": "行高",
"Loading...": "載入中...",
"Loc. {{currentPage}} / {{totalPage}}": "位置 {{currentPage}} / {{totalPage}}",
"Logged in": "已登入",
@@ -150,5 +149,11 @@
"Book deleted: {{title}}": "書籍已刪除:{{title}}",
"Failed to delete book: {{title}}": "書籍刪除失敗:{{title}}",
"Check Updates on Start": "啟動時檢查更新",
"Insufficient storage quota": "雲存儲空間不足"
"Insufficient storage quota": "雲存儲空間不足",
"Font Weight": "字重",
"Line Spacing": "行間距",
"Word Spacing": "字間距",
"Letter Spacing": "字間距",
"Text Indent": "首行縮進",
"Paragraph Margin": "段間距"
}
+7
View File
@@ -1,5 +1,12 @@
{
"releases": {
"0.9.10": {
"date": "2025-02-08",
"notes": [
"Add options to adjust paragraph margin, word spacing, letter spacing, text indent and font weight",
"Alpha release for Android and iOS platforms"
]
},
"0.9.9": {
"date": "2025-02-06",
"notes": [
@@ -0,0 +1,80 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rust")
}
val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
android {
compileSdk = 34
namespace = "com.bilingify.readest"
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "com.bilingify.readest"
minSdk = 26
targetSdk = 34
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
signingConfigs {
create("release") {
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["password"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["password"] as String
}
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
signingConfig = signingConfigs.getByName("release")
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
}
rust {
rootDirRel = "../../../"
}
dependencies {
implementation("androidx.webkit:webkit:1.6.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.activity:activity-ktx:1.9.3")
implementation("com.google.android.material:material:1.8.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
apply(from = "tauri.build.gradle.kts")
@@ -0,0 +1,23 @@
package com.bilingify.readest
import android.os.Bundle
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import androidx.activity.enableEdgeToEdge
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
hideSystemUI()
}
private fun hideSystemUI() {
window.decorView.windowInsetsController?.let { controller ->
controller.hide(WindowInsets.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+32 -18
View File
@@ -11,21 +11,20 @@ mod menu;
#[cfg(target_os = "macos")]
mod traffic_light_plugin;
mod transfer_file;
use transfer_file::{download_file, upload_file};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use std::path::PathBuf;
use tauri::{command, Window};
use tauri::{AppHandle, Emitter, Manager, Url};
use tauri::{WebviewUrl, WebviewWindowBuilder};
use tauri_plugin_fs::FsExt;
use tauri_plugin_oauth::start;
#[cfg(desktop)]
use tauri::Listener;
use std::path::PathBuf;
#[cfg(desktop)]
use tauri::{AppHandle, Listener, Manager, Url};
#[cfg(desktop)]
use tauri_plugin_fs::FsExt;
mod transfer_file;
use tauri::{command, Emitter, WebviewUrl, WebviewWindowBuilder, Window};
use tauri_plugin_oauth::start;
use transfer_file::{download_file, upload_file};
#[cfg(desktop)]
fn allow_file_in_scopes(app: &AppHandle, files: Vec<PathBuf>) {
@@ -78,12 +77,13 @@ async fn list_fonts() -> Result<Vec<String>, String> {
let font_collection = font_enumeration::Collection::new().unwrap();
let mut fonts = Vec::new();
for font in font_collection.all() {
fonts.push(font.font_name.clone());
fonts.push(font.family_name.clone());
}
Ok(fonts)
}
#[derive(Clone, serde::Serialize)]
#[allow(dead_code)]
struct Payload {
args: Vec<String>,
cwd: String,
@@ -204,12 +204,26 @@ pub fn run() {
.title("");
#[cfg(all(not(target_os = "macos"), desktop))]
let win_builder = win_builder
.decorations(false)
.transparent(true)
.visible(false)
.shadow(true)
.title("Readest");
let win_builder = {
let mut win_builder = win_builder
.decorations(false)
.transparent(true)
.visible(false)
.title("Readest");
if cfg!(target_os = "windows") {
let version = tauri_plugin_os::version();
if version <= tauri_plugin_os::Version::from_string("10.0.19045") {
win_builder = win_builder.shadow(false);
} else {
win_builder = win_builder.shadow(true);
}
} else {
win_builder = win_builder.shadow(true);
}
win_builder
};
win_builder.build().unwrap();
// let win = win_builder.build().unwrap();
+5 -2
View File
@@ -14,8 +14,8 @@
"windows": [],
"security": {
"csp": {
"default-src": "'self' 'unsafe-inline' blob: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost",
"connect-src": "'self' blob: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com",
"default-src": "'self' 'unsafe-inline' blob: data: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost",
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com",
"img-src": "'self' blob: data: asset: http://asset.localhost https://*",
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com",
"font-src": "'self' data: asset: http://asset.localhost tauri: https://fonts.gstatic.com https://db.onlinewebfonts.com https://cdn.jsdelivr.net",
@@ -59,6 +59,9 @@
"android": {
"minSdkVersion": 26
},
"iOS": {
"developmentTeam": "J5W48D69VR"
},
"fileAssociations": [
{
"name": "epub",
@@ -7,6 +7,7 @@ import { LiaCloudUploadAltSolid, LiaCloudDownloadAltSolid } from 'react-icons/li
import { Book } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { formatAuthors, formatTitle } from '@/utils/book';
import ReadingProgress from './ReadingProgress';
interface BookItemProps {
@@ -61,11 +62,13 @@ const BookItem: React.FC<BookItemProps> = ({
)}
>
<div className='flex h-1/2 items-center justify-center'>
<span className='line-clamp-3 text-lg'>{book.title}</span>
<span className='line-clamp-3 text-lg'>{formatTitle(book.title)}</span>
</div>
<div className='h-1/6'></div>
<div className='flex h-1/3 items-center justify-center'>
<span className='text-neutral-content/50 line-clamp-1 text-base'>{book.author}</span>
<span className='text-neutral-content/50 line-clamp-1 text-base'>
{formatAuthors(book.author)}
</span>
</div>
</div>
{(selectedBooks.includes(book.hash) || clickedBookHash === book.hash) && (
@@ -18,7 +18,6 @@ import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
import Alert from '@/components/Alert';
import Spinner from '@/components/Spinner';
import BookDetailModal from '@/components/BookDetailModal';
import BookItem from './BookItem';
import GroupItem from './GroupItem';
@@ -56,6 +55,7 @@ interface BookshelfProps {
handleBookUpload: (book: Book) => void;
handleBookDownload: (book: Book) => void;
handleBookDelete: (book: Book) => void;
handleShowDetailsBook: (book: Book) => void;
booksTransferProgress: { [key: string]: number | null };
}
@@ -66,6 +66,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
handleBookUpload,
handleBookDownload,
handleBookDelete,
handleShowDetailsBook,
booksTransferProgress,
}) => {
const _ = useTranslation();
@@ -80,7 +81,6 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const [clickedImage, setClickedImage] = useState<string | null>(null);
const [importBookUrl] = useState(searchParams?.get('url') || '');
const isImportingBook = useRef(false);
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(null);
const makeBookAvailable = async (book: Book) => {
if (book.uploadedAt && !book.downloadedAt) {
@@ -98,14 +98,10 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const showBookDetailsModal = async (book: Book) => {
if (await makeBookAvailable(book)) {
setShowDetailsBook(book);
handleShowDetailsBook(book);
}
};
const dismissBookDetailsModal = () => {
setShowDetailsBook(null);
};
const { setLibrary } = useLibraryStore();
useEffect(() => {
@@ -287,13 +283,6 @@ const Bookshelf: React.FC<BookshelfProps> = ({
onClickConfirm={confirmDelete}
/>
)}
{showDetailsBook && (
<BookDetailModal
isOpen={!!showDetailsBook}
book={showDetailsBook}
onClose={dismissBookDetailsModal}
/>
)}
</div>
);
};
@@ -1,5 +1,6 @@
import Image from 'next/image';
import { BooksGroup } from '@/types/book';
import { formatAuthors, formatTitle } from '@/utils/book';
import ReadingProgress from './ReadingProgress';
interface GroupItemProps {
@@ -21,8 +22,10 @@ const GroupItem: React.FC<GroupItemProps> = ({ group }) => {
/>
</figure>
<div className='card-body p-4'>
<h3 className='card-title line-clamp-2 text-sm'>{book.title}</h3>
<p className='text-neutral-content line-clamp-1 text-xs'>{book.author}</p>
<h3 className='card-title line-clamp-2 text-sm'>{formatTitle(book.title)}</h3>
<p className='text-neutral-content line-clamp-1 text-xs'>
{formatAuthors(book.author)}
</p>
<ReadingProgress book={book} />
</div>
</div>
@@ -50,10 +50,11 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
ref={headerRef}
className={clsx(
'titlebar z-10 h-11 w-full py-2 pr-6',
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
isTrafficLightVisible ? 'pl-16' : 'pl-2',
)}
>
<div className='flex items-center justify-between space-x-12'>
<div className='flex items-center justify-between space-x-6 sm:space-x-12'>
<div className='exclude-title-bar-mousedown relative flex w-full items-center pl-4'>
<span className='absolute left-8 text-gray-500'>
<FaSearch className='h-4 w-4' />
@@ -68,7 +69,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
'border-none focus:outline-none focus:ring-0',
)}
/>
<div className='absolute right-4 flex items-center space-x-4 text-gray-500'>
<div className='absolute right-4 flex items-center space-x-2 text-gray-500 sm:space-x-4'>
<span className='mx-2 h-6 w-[1px] bg-gray-400'></span>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center'
@@ -98,7 +99,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
</button>
</div>
</div>
<div className='flex h-full items-center gap-x-4'>
<div className='flex h-full items-center gap-x-2 sm:gap-x-4'>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
+26 -5
View File
@@ -10,6 +10,8 @@ import { AppService } from '@/types/system';
import { navigateToLogin, navigateToReader } from '@/utils/nav';
import { getBaseFilename, listFormater } from '@/utils/book';
import { eventDispatcher } from '@/utils/event';
import { ProgressPayload } from '@/utils/transfer';
import { throttle } from '@/utils/throttle';
import { parseOpenWithFiles } from '@/helpers/cli';
import { isTauriAppPlatform, hasUpdater } from '@/services/environment';
import { checkForAppUpdates } from '@/helpers/updater';
@@ -30,8 +32,7 @@ import { Toast } from '@/components/Toast';
import Spinner from '@/components/Spinner';
import LibraryHeader from './components/LibraryHeader';
import Bookshelf from './components/Bookshelf';
import { ProgressPayload } from '@/utils/transfer';
import { throttle } from '@/utils/throttle';
import BookDetailModal from '@/components/BookDetailModal';
const LibraryPage = () => {
const router = useRouter();
@@ -51,6 +52,7 @@ const LibraryPage = () => {
const isInitiating = useRef(false);
const [libraryLoaded, setLibraryLoaded] = useState(false);
const [isSelectMode, setIsSelectMode] = useState(false);
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(null);
const [booksTransferProgress, setBooksTransferProgress] = useState<{
[key: string]: number | null;
}>({});
@@ -335,6 +337,10 @@ const LibraryPage = () => {
setIsSelectMode(!isSelectMode);
};
const handleShowDetailsBook = (book: Book) => {
setShowDetailsBook(book);
};
if (!appService) {
return null;
}
@@ -352,8 +358,9 @@ const LibraryPage = () => {
return (
<div
className={clsx(
'library-page bg-base-200 text-base-content flex h-dvh select-none flex-col overflow-hidden',
isTauriAppPlatform() && 'rounded-window',
'library-page bg-base-200 text-base-content flex select-none flex-col overflow-hidden',
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
appService?.hasRoundedWindow && 'rounded-window',
)}
>
<div className='fixed top-0 z-40 w-full'>
@@ -370,7 +377,13 @@ const LibraryPage = () => {
)}
{libraryLoaded &&
(libraryBooks.length > 0 ? (
<div ref={containerRef} className='mt-12 flex-grow overflow-auto px-2'>
<div
ref={containerRef}
className={clsx(
'mt-12 flex-grow overflow-auto px-2',
appService?.hasSafeAreaInset && 'mt-[calc(48px+env(safe-area-inset-top))]',
)}
>
<Suspense>
<Bookshelf
libraryBooks={libraryBooks}
@@ -379,6 +392,7 @@ const LibraryPage = () => {
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
handleBookDelete={handleBookDelete}
handleShowDetailsBook={handleShowDetailsBook}
booksTransferProgress={booksTransferProgress}
/>
</Suspense>
@@ -400,6 +414,13 @@ const LibraryPage = () => {
</div>
</div>
))}
{showDetailsBook && (
<BookDetailModal
isOpen={!!showDetailsBook}
book={showDetailsBook}
onClose={() => setShowDetailsBook(null)}
/>
)}
<AboutWindow />
<Toast />
</div>
@@ -1,10 +1,11 @@
import clsx from 'clsx';
import React, { useEffect } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { isTauriAppPlatform } from '@/services/environment';
import FoliateViewer from './FoliateViewer';
import getGridTemplate from '@/utils/grid';
import SectionInfo from './SectionInfo';
@@ -23,6 +24,7 @@ interface BooksGridProps {
}
const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const { appService } = useEnv();
const { getConfig, getBookData } = useBookDataStore();
const { getProgress, getViewState, getViewSettings } = useReaderStore();
const { sideBarBookKey } = useSidebarStore();
@@ -39,7 +41,10 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
return (
<div
className='grid h-full flex-grow'
className={clsx(
'grid h-full flex-grow',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
)}
style={{
gridTemplateColumns: gridTemplate.columns,
gridTemplateRows: gridTemplate.rows,
@@ -61,7 +66,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
<div
id={`gridcell-${bookKey}`}
key={bookKey}
className={`${isTauriAppPlatform() ? 'rounded-window' : ''} relative h-full w-full overflow-hidden`}
className={`${appService?.hasRoundedWindow ? 'rounded-window' : ''} relative h-full w-full overflow-hidden`}
>
{isBookmarked && <Ribbon width={marginGap} />}
<HeaderBar
@@ -7,6 +7,7 @@ import { useParallelViewStore } from '@/store/parallelViewStore';
import { useClickEvent, useTouchEvent } from '../hooks/useIframeEvents';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { useProgressSync } from '../hooks/useProgressSync';
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
import { useAutoHideScrollbar } from '../hooks/useAutoHideScrollbar';
import { getStyles, mountAdditionalFonts } from '@/utils/style';
import { getBookDirFromWritingMode } from '@/utils/book';
@@ -43,6 +44,7 @@ const FoliateViewer: React.FC<{
}, [toastMessage]);
useProgressSync(bookKey);
useProgressAutoSave(bookKey);
const progressRelocateHandler = (event: Event) => {
const detail = (event as CustomEvent).detail;
@@ -4,11 +4,11 @@ import { RiArrowLeftWideLine, RiArrowRightWideLine } from 'react-icons/ri';
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
import { FaHeadphones } from 'react-icons/fa6';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTranslation } from '@/hooks/useTranslation';
import { eventDispatcher } from '@/utils/event';
import { isPWA, isTauriAppPlatform } from '@/services/environment';
import Button from '@/components/Button';
interface FooterBarProps {
@@ -19,6 +19,7 @@ interface FooterBarProps {
const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { hoveredBookKey, setHoveredBookKey, getView, getProgress } = useReaderStore();
const { isSideBarVisible } = useSidebarStore();
const view = getView(bookKey);
@@ -62,9 +63,9 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
className={clsx(
'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center gap-x-4 px-4',
'shadow-xs bg-base-100 transition-opacity duration-300',
isPWA() ? 'pb-[env(safe-area-inset-bottom)]' : '',
isTauriAppPlatform() && 'rounded-window-bottom-right',
!isSideBarVisible && isTauriAppPlatform() && 'rounded-window-bottom-left',
appService?.hasSafeAreaInset && 'pb-[env(safe-area-inset-bottom)]',
appService?.hasRoundedWindow && 'rounded-window-bottom-right',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left',
isHoveredAnim && 'hover-bar-anim',
hoveredBookKey === bookKey ? `opacity-100` : `opacity-0`,
)}
@@ -5,7 +5,6 @@ import { PiDotsThreeVerticalBold } from 'react-icons/pi';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { isTauriAppPlatform } from '@/services/environment';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import useTrafficLight from '@/hooks/useTrafficLight';
import WindowButtons from '@/components/WindowButtons';
@@ -53,8 +52,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
`header-bar absolute top-0 z-10 flex h-11 w-full items-center pr-4`,
isTrafficLightVisible && isTopLeft && !isSideBarVisible ? 'pl-16' : 'pl-4',
`shadow-xs bg-base-100 transition-opacity duration-300`,
isTauriAppPlatform() && 'rounded-window-top-right',
!isSideBarVisible && isTauriAppPlatform() && 'rounded-window-top-left',
appService?.hasRoundedWindow && 'rounded-window-top-right',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-top-left',
isHoveredAnim && 'hover-bar-anim',
hoveredBookKey === bookKey || isDropdownOpen ? `visible` : `opacity-0`,
isDropdownOpen && 'header-bar-pinned',
@@ -8,13 +8,12 @@ import { useEnv } from '@/context/EnvContext';
import { useTheme } from '@/hooks/useTheme';
import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore';
import { isTauriAppPlatform } from '@/services/environment';
import { AboutWindow } from '@/components/AboutWindow';
import { Toast } from '@/components/Toast';
import ReaderContent from './ReaderContent';
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
const { envConfig } = useEnv();
const { envConfig, appService } = useEnv();
const { settings, setSettings } = useSettingsStore();
const { library, setLibrary } = useLibraryStore();
const isInitiating = useRef(false);
@@ -42,7 +41,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
<div
className={clsx(
`reader-page bg-base-100 text-base-content select-none`,
isTauriAppPlatform() && 'rounded-window',
appService?.hasRoundedWindow && 'rounded-window',
)}
>
<Suspense>
@@ -1,5 +1,6 @@
'use client';
import clsx from 'clsx';
import * as React from 'react';
import { useState, useRef, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
@@ -31,7 +32,7 @@ import TTSControl from './tts/TTSControl';
const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ ids, settings }) => {
const router = useRouter();
const searchParams = useSearchParams();
const { envConfig } = useEnv();
const { envConfig, appService } = useEnv();
const { bookKeys, dismissBook, getNextBookKey } = useBooksManager();
const { sideBarBookKey, setSideBarBookKey } = useSidebarStore();
const { saveSettings } = useSettingsStore();
@@ -148,7 +149,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
setTimeout(() => setLoading(true), 300);
return (
loading && (
<div className={'hero hero-content h-dvh'}>
<div className={clsx('hero hero-content', appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh')}>
<Spinner loading={true} />
</div>
)
@@ -156,7 +157,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
}
return (
<div className='flex h-dvh'>
<div className={clsx('flex', appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh')}>
<SideBar onGoToLibrary={handleCloseBooksToLibrary} />
<BooksGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
<TTSControl />
@@ -49,6 +49,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const isShowingPopup = useRef(false);
const isTextSelected = useRef(false);
const isUpToShowPopup = useRef(false);
const isTouchstarted = useRef(false);
const [selection, setSelection] = useState<TextSelection | null>();
const [showAnnotPopup, setShowAnnotPopup] = useState(false);
const [showWiktionaryPopup, setShowWiktionaryPopup] = useState(false);
@@ -94,14 +95,6 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}
setSelection({ key: bookKey, text: sel.toString(), range, index });
};
const handlePointerup = () => {
// Available on iOS and Desktop, fired when release the long press
// Note that on Android, pointerup event is fired for any tap but not for long press
const sel = doc.getSelection();
if (isValidSelection(sel)) {
makeSelection(sel, true);
}
};
const handleSelectionchange = () => {
// Available on iOS, Android and Desktop, fired when the selection is changed
// Ideally the popup only shows when the selection is done,
@@ -109,7 +102,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// we make the popup show when the selection is changed
const sel = doc.getSelection();
if (isValidSelection(sel)) {
if (osPlatform === 'android') {
if (osPlatform === 'android' && isTouchstarted.current) {
makeSelection(sel, false);
}
} else if (!isUpToShowPopup.current) {
@@ -120,14 +113,32 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
setShowDeepLPopup(false);
}
};
const handlePointerup = () => {
// Available on iOS and Desktop, fired when release the long press
// Note that on Android, pointerup event is fired after an additional touch event
const sel = doc.getSelection();
if (isValidSelection(sel)) {
makeSelection(sel, true);
}
};
const handleTouchstart = () => {
// Available on iOS and Android for the initial touch event
isTouchstarted.current = true;
};
const handleTouchmove = () => {
// Available on iOS
// Available on iOS, on Android not fired
// To make the popup not to follow the selection
setShowAnnotPopup(false);
};
const handleTouchend = () => {
// Available on iOS, on Android fired after an additional touch event
isTouchstarted.current = false;
};
if (bookData.book?.format !== 'PDF') {
detail.doc?.addEventListener('pointerup', handlePointerup);
detail.doc?.addEventListener('touchstart', handleTouchstart);
detail.doc?.addEventListener('touchmove', handleTouchmove);
detail.doc?.addEventListener('touchend', handleTouchend);
detail.doc?.addEventListener('selectionchange', handleSelectionchange);
// Disable the default context menu on mobile devices,
@@ -13,7 +13,6 @@ import { TextSelection } from '@/utils/sel';
import { BookNote } from '@/types/book';
import { uniqueId } from '@/utils/misc';
import { eventDispatcher } from '@/utils/event';
import { isTauriAppPlatform } from '@/services/environment';
import useDragBar from '../../hooks/useDragBar';
import BooknoteItem from '../sidebar/BooknoteItem';
import NotebookHeader from './Header';
@@ -25,7 +24,7 @@ const MAX_NOTEBOOK_WIDTH = 0.45;
const Notebook: React.FC = ({}) => {
const _ = useTranslation();
const { updateAppTheme } = useTheme();
const { envConfig } = useEnv();
const { envConfig, appService } = useEnv();
const { settings } = useSettingsStore();
const { sideBarBookKey } = useSidebarStore();
const { notebookWidth, isNotebookVisible, isNotebookPinned } = useNotebookStore();
@@ -152,7 +151,7 @@ const Notebook: React.FC = ({}) => {
<div
className={clsx(
'notebook-container bg-base-200 right-0 z-20 h-full min-w-60 select-none',
isTauriAppPlatform() && 'rounded-window-top-right rounded-window-bottom-right',
appService?.hasRoundedWindow && 'rounded-window-top-right rounded-window-bottom-right',
!isNotebookPinned && 'shadow-2xl',
)}
style={{
@@ -8,8 +8,8 @@ import { useDefaultIconSize, useResponsiveSize } from '@/hooks/useResponsiveSize
interface DropdownProps {
family?: string;
selected: string;
options: { option: string; label: string }[];
moreOptions?: string[];
options: { option: string; label?: string }[];
moreOptions?: { option: string; label?: string }[];
onSelect: (option: string) => void;
onGetFontFamily: (option: string, family: string) => string;
}
@@ -25,7 +25,8 @@ const FontDropdown: React.FC<DropdownProps> = ({
const _ = useTranslation();
const iconSize16 = useResponsiveSize(16);
const defaultIconSize = useDefaultIconSize();
const selectedOption = options.find((option) => option.option === selected)!;
const allOptions = [...options, ...(moreOptions ?? [])];
const selectedOption = allOptions.find((option) => option.option === selected) ?? allOptions[0]!;
return (
<div className='dropdown dropdown-top'>
<button
@@ -68,14 +69,14 @@ const FontDropdown: React.FC<DropdownProps> = ({
'!mr-5 mb-[-46px] inline max-h-80 w-[200px] overflow-y-scroll',
)}
>
{moreOptions.map((option) => (
<li key={option} onClick={() => onSelect(option)}>
{moreOptions.map((option, index) => (
<li key={`${index}-${option.option}`} onClick={() => onSelect(option.option)}>
<div className='flex items-center px-2'>
<span style={{ minWidth: `${defaultIconSize}px` }}>
{selected === option && <MdCheck className='text-base-content' />}
{selected === option.option && <MdCheck className='text-base-content' />}
</span>
<span style={{ fontFamily: onGetFontFamily(option, family ?? '') }}>
{option}
<span style={{ fontFamily: onGetFontFamily(option.option, family ?? '') }}>
{option.label || option.option}
</span>
</div>
</li>
@@ -50,7 +50,7 @@ const FontFace = ({
<FontDropdown
family={family}
options={options.map((option) => ({ option, label: option }))}
moreOptions={moreOptions}
moreOptions={moreOptions?.map((option) => ({ option, label: option })) ?? []}
selected={selected}
onSelect={onSelect}
onGetFontFamily={handleFontFaceFont}
@@ -106,6 +106,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [serifFont, setSerifFont] = useState(viewSettings.serifFont!);
const [sansSerifFont, setSansSerifFont] = useState(viewSettings.sansSerifFont!);
const [monospaceFont, setMonospaceFont] = useState(viewSettings.monospaceFont!);
const [fontWeight, setFontWeight] = useState(viewSettings.fontWeight!);
useEffect(() => {
if (isTauriAppPlatform() && FONT_ENUM_SUPPORTED_OS_PLATFORMS.includes(osPlatform)) {
@@ -149,6 +150,17 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [minFontSize]);
useEffect(() => {
viewSettings.fontWeight = fontWeight;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.fontWeight = fontWeight;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fontWeight]);
useEffect(() => {
viewSettings.serifFont = serifFont;
setViewSettings(bookKey, viewSettings);
@@ -232,6 +244,23 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
</div>
</div>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Font Weight')}</h2>
<div className='card border-base-200 border shadow'>
<div className='divide-base-200 divide-y'>
<NumberInput
className='config-item-top'
label={_('Font Weight')}
value={fontWeight}
onChange={setFontWeight}
min={100}
max={900}
step={100}
/>
</div>
</div>
</div>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Font Family')}</h2>
<div className='card border-base-200 border shadow'>
@@ -22,7 +22,11 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const viewSettings = getViewSettings(bookKey)!;
const { themeCode } = useTheme();
const [paragraphMargin, setParagraphMargin] = useState(viewSettings.paragraphMargin!);
const [lineHeight, setLineHeight] = useState(viewSettings.lineHeight!);
const [wordSpacing, setWordSpacing] = useState(viewSettings.wordSpacing!);
const [letterSpacing, setLetterSpacing] = useState(viewSettings.letterSpacing!);
const [textIndent, setTextIndent] = useState(viewSettings.textIndent!);
const [fullJustification, setFullJustification] = useState(viewSettings.fullJustification!);
const [hyphenation, setHyphenation] = useState(viewSettings.hyphenation!);
const [marginPx, setMarginPx] = useState(viewSettings.marginPx!);
@@ -32,6 +36,17 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [maxBlockSize, setMaxBlockSize] = useState(viewSettings.maxBlockSize!);
const [writingMode, setWritingMode] = useState(viewSettings.writingMode!);
useEffect(() => {
viewSettings.paragraphMargin = paragraphMargin;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.paragraphMargin = paragraphMargin;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paragraphMargin]);
useEffect(() => {
viewSettings.lineHeight = lineHeight;
setViewSettings(bookKey, viewSettings);
@@ -43,6 +58,39 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lineHeight]);
useEffect(() => {
viewSettings.wordSpacing = wordSpacing;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.wordSpacing = wordSpacing;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wordSpacing]);
useEffect(() => {
viewSettings.letterSpacing = letterSpacing;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.letterSpacing = letterSpacing;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [letterSpacing]);
useEffect(() => {
viewSettings.textIndent = textIndent;
setViewSettings(bookKey, viewSettings);
if (isFontLayoutSettingsGlobal) {
settings.globalViewSettings.textIndent = textIndent;
setSettings(settings);
}
view?.renderer.setStyles?.(getStyles(viewSettings, themeCode));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [textIndent]);
useEffect(() => {
viewSettings.fullJustification = fullJustification;
setViewSettings(bookKey, viewSettings);
@@ -188,13 +236,49 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='divide-base-200 divide-y'>
<NumberInput
className='config-item-top'
label={_('Line Height')}
label={_('Paragraph Margin')}
value={paragraphMargin}
onChange={setParagraphMargin}
min={0}
max={4}
step={0.5}
/>
<NumberInput
className='config-item-top'
label={_('Line Spacing')}
value={lineHeight}
onChange={setLineHeight}
min={1.0}
max={3.0}
step={0.1}
/>
<NumberInput
className='config-item-top'
label={_('Word Spacing')}
value={wordSpacing}
onChange={setWordSpacing}
min={-4}
max={8}
step={0.5}
/>
<NumberInput
className='config-item-top'
label={_('Letter Spacing')}
value={letterSpacing}
onChange={setLetterSpacing}
min={-2}
max={4}
step={0.1}
/>
<NumberInput
className='config-item-top'
label={_('Text Indent')}
value={textIndent}
onChange={setTextIndent}
min={-2}
max={4}
step={1}
/>
<div className='config-item config-item-bottom'>
<span className=''>{_('Full Justification')}</span>
<input
@@ -30,7 +30,7 @@ const BookCard = ({ book }: { book: Book }) => {
/>
<div className='min-w-0 flex-1'>
<h4 className='line-clamp-2 w-[90%] text-sm font-semibold'>{formatTitle(title)}</h4>
<p className='text-neutral-content truncate text-xs'>{formatAuthors(undefined, author)}</p>
<p className='text-neutral-content truncate text-xs'>{formatAuthors(author)}</p>
</div>
<button
className='btn btn-ghost hover:bg-base-300 h-6 min-h-6 w-6 rounded-full p-0 transition-colors'
@@ -2,16 +2,17 @@ import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import { BookDoc } from '@/libs/document';
import { useEnv } from '@/context/EnvContext';
import { useBookDataStore } from '@/store/bookDataStore';
import TOCView from './TOCView';
import BooknoteView from './BooknoteView';
import TabNavigation from './TabNavigation';
import { isPWA } from '@/services/environment';
const SidebarContent: React.FC<{
bookDoc: BookDoc;
sideBarBookKey: string;
}> = ({ bookDoc, sideBarBookKey }) => {
const { appService } = useEnv();
const scrollContainerRef = useRef<HTMLDivElement>(null);
const { getConfig, setConfig } = useBookDataStore();
const config = getConfig(sideBarBookKey);
@@ -92,7 +93,10 @@ const SidebarContent: React.FC<{
</div>
</div>
<div
className={clsx('flex-shrink-0', isPWA() ? 'pb-[calc(env(safe-area-inset-bottom)/2)]' : '')}
className={clsx(
'flex-shrink-0',
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom)/2)]',
)}
>
<TabNavigation activeTab={activeTab} onTabChange={handleTabChange} />
</div>
@@ -7,7 +7,7 @@ import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { BookSearchResult } from '@/types/book';
import { eventDispatcher } from '@/utils/event';
import { isTauriAppPlatform } from '@/services/environment';
import { useEnv } from '@/context/EnvContext';
import { useTheme } from '@/hooks/useTheme';
import SidebarHeader from './Header';
import SidebarContent from './Content';
@@ -24,6 +24,7 @@ const MAX_SIDEBAR_WIDTH = 0.45;
const SideBar: React.FC<{
onGoToLibrary: () => void;
}> = ({ onGoToLibrary }) => {
const { appService } = useEnv();
const { updateAppTheme } = useTheme();
const { settings } = useSettingsStore();
const { sideBarBookKey } = useSidebarStore();
@@ -117,8 +118,10 @@ const SideBar: React.FC<{
<>
<div
className={clsx(
'sidebar-container bg-base-200 z-20 flex h-full min-w-60 select-none flex-col',
isTauriAppPlatform() && 'rounded-window-top-left rounded-window-bottom-left',
'sidebar-container bg-base-200 z-20 flex min-w-60 select-none flex-col',
appService?.isIOSApp ? 'h-[100vh]' : 'h-full',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
appService?.hasRoundedWindow && 'rounded-window-top-left rounded-window-bottom-left',
!isSideBarPinned && 'shadow-2xl',
)}
style={{
@@ -120,7 +120,7 @@ const TOCView: React.FC<{
};
useEffect(() => {
if (!progress) return;
if (!progress || eventDispatcher.dispatchSync('tts-is-speaking')) return;
const { sectionHref: currentHref } = progress;
const hrefMd5 = currentHref ? getContentMd5(currentHref) : '';
const currentItem = viewRef.current?.querySelector(`[data-href="${hrefMd5}"]`);
@@ -1,5 +1,6 @@
import clsx from 'clsx';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useBookDataStore } from '@/store/bookDataStore';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
@@ -10,7 +11,6 @@ import { eventDispatcher } from '@/utils/event';
import { parseSSMLLang } from '@/utils/ssml';
import { getOSPlatform } from '@/utils/misc';
import { throttle } from '@/utils/throttle';
import { isPWA } from '@/services/environment';
import Popup from '@/components/Popup';
import TTSPanel from './TTSPanel';
import TTSIcon from './TTSIcon';
@@ -21,6 +21,7 @@ const POPUP_PADDING = 10;
const TTSControl = () => {
const _ = useTranslation();
const { appService } = useEnv();
const { getBookData } = useBookDataStore();
const { getView, getViewSettings } = useReaderStore();
const [bookKey, setBookKey] = useState<string>('');
@@ -273,8 +274,10 @@ const TTSControl = () => {
<div
ref={iconRef}
className={clsx(
'absolute right-6 h-12 w-12',
isPWA() ? 'bottom-[calc(env(safe-area-inset-bottom)+48px)]' : 'bottom-12',
'fixed right-6 h-12 w-12',
appService?.hasSafeAreaInset
? 'bottom-[calc(env(safe-area-inset-bottom)+48px)]'
: 'bottom-12',
)}
>
<TTSIcon isPlaying={isPlaying} onClick={togglePopup} />
@@ -0,0 +1,31 @@
import { useCallback, useEffect } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useBookDataStore } from '@/store/bookDataStore';
import { useReaderStore } from '@/store/readerStore';
import { useSettingsStore } from '@/store/settingsStore';
import { throttle } from '@/utils/throttle';
export const useProgressAutoSave = (bookKey: string) => {
const { envConfig, appService } = useEnv();
const { getConfig, saveConfig } = useBookDataStore();
const { getProgress } = useReaderStore();
const progress = getProgress(bookKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
const saveBookConfig = useCallback(
throttle(async () => {
const config = getConfig(bookKey)!;
const settings = useSettingsStore.getState().settings;
await saveConfig(envConfig, bookKey, config, settings);
}, 10000),
[],
);
useEffect(() => {
// FIXME: Save book config when progress changes on Android
// until we can hook into the lifecycle of the Android app
if (!appService?.isAndroidApp || !progress) return;
saveBookConfig();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [progress, bookKey]);
};
@@ -74,7 +74,7 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
);
return (
<>
<div className='fixed inset-0 z-50 flex items-center justify-center'>
<Dialog
title={_('Book Details')}
isOpen={isOpen}
@@ -83,7 +83,7 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
boxClassName='sm:min-w-[480px]'
contentClassName='!px-6 !py-2'
>
<div className='z-50 flex w-full select-text items-center justify-center'>
<div className='flex w-full select-text items-center justify-center'>
<div className='relative w-full rounded-lg'>
<div className='mb-10 flex h-40 items-start'>
<div className='book-cover relative mr-10 aspect-[28/41] h-40 items-end shadow-lg'>
@@ -115,7 +115,7 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
{formatTitle(book.title) || _('Untitled')}
</p>
<p className='text-neutral-content line-clamp-1'>
{formatAuthors(bookMeta.language, book.author) || _('Unknown')}
{formatAuthors(book.author, bookMeta.language) || _('Unknown')}
</p>
</div>
{window.innerWidth >= 400 && (
@@ -202,7 +202,7 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
onClickConfirm={confirmDelete}
/>
)}
</>
</div>
);
};
@@ -1,6 +1,7 @@
import clsx from 'clsx';
import React, { ReactNode, useEffect } from 'react';
import { MdArrowBackIosNew } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
interface DialogProps {
@@ -26,6 +27,7 @@ const Dialog: React.FC<DialogProps> = ({
contentClassName,
onClose,
}) => {
const { appService } = useEnv();
const iconSize22 = useResponsiveSize(22);
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
@@ -54,6 +56,7 @@ const Dialog: React.FC<DialogProps> = ({
className={clsx(
'modal-box settings-content flex h-full max-h-full w-full max-w-full flex-col rounded-none p-0 sm:rounded-2xl',
'sm:h-[60%] sm:w-[65%] sm:max-w-[600px]',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
boxClassName,
)}
>
+6 -1
View File
@@ -37,14 +37,19 @@ import { downloadFile, uploadFile, deleteFile, createProgressHandler } from '@/l
import { ProgressHandler } from '@/utils/transfer';
export abstract class BaseAppService implements AppService {
osPlatform: string = getOSPlatform();
isMobile: boolean = ['android', 'ios'].includes(getOSPlatform());
localBooksDir: string = '';
abstract fs: FileSystem;
abstract appPlatform: AppPlatform;
abstract isAppDataSandbox: boolean;
abstract isAndroidApp: boolean;
abstract isIOSApp: boolean;
abstract hasTrafficLight: boolean;
abstract hasWindowBar: boolean;
abstract hasContextMenu: boolean;
abstract hasRoundedWindow: boolean;
abstract hasSafeAreaInset: boolean;
abstract resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string };
abstract getCoverImageUrl(book: Book): string;
@@ -156,7 +161,7 @@ export abstract class BaseAppService implements AppService {
hash,
format,
title: formatTitle(loadedBook.metadata.title),
author: formatAuthors(loadedBook.metadata.language, loadedBook.metadata.author),
author: formatAuthors(loadedBook.metadata.author, loadedBook.metadata.language),
createdAt: existingBook ? existingBook.createdAt : Date.now(),
uploadedAt: existingBook ? existingBook.uploadedAt : null,
downloadedAt: Date.now(),
+5 -1
View File
@@ -13,7 +13,7 @@ import { UserStorageQuota } from '@/types/user';
export const LOCAL_BOOKS_SUBDIR = 'Readest/Books';
export const CLOUD_BOOKS_SUBDIR = 'Readest/Books';
export const SUPPORTED_FILE_EXTS = ['epub', 'mobi', 'azw', 'azw3', 'fb2', 'cbz', 'pdf'];
export const SUPPORTED_FILE_EXTS = ['epub', 'mobi', 'azw', 'azw3', 'fb2', 'zip', 'cbz', 'pdf'];
export const FILE_ACCEPT_FORMATS = SUPPORTED_FILE_EXTS.map((ext) => `.${ext}`).join(', ');
export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
@@ -67,7 +67,11 @@ export const DEFAULT_BOOK_LAYOUT: BookLayout = {
export const DEFAULT_BOOK_STYLE: BookStyle = {
zoomLevel: 100,
paragraphMargin: 1,
lineHeight: 1.6,
wordSpacing: 0,
letterSpacing: 0,
textIndent: 0,
fullJustification: true,
hyphenation: true,
invert: false,
@@ -23,7 +23,7 @@ import { isValidURL } from '@/utils/misc';
import { BaseAppService } from './appService';
import { LOCAL_BOOKS_SUBDIR } from './constants';
export const isMobile = ['android', 'ios'].includes(osType());
const OS_TYPE = osType();
const resolvePath = (fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } => {
switch (base) {
@@ -117,10 +117,14 @@ export const nativeFileSystem: FileSystem = {
export class NativeAppService extends BaseAppService {
fs = nativeFileSystem;
appPlatform = 'tauri' as AppPlatform;
isAppDataSandbox = isMobile;
hasTrafficLight = osType() === 'macos';
hasWindowBar = !(osType() === 'ios' || osType() === 'android');
hasContextMenu = !(osType() === 'ios' || osType() === 'android');
isAppDataSandbox = ['android', 'ios'].includes(OS_TYPE);
isAndroidApp = OS_TYPE === 'android';
isIOSApp = OS_TYPE === 'ios';
hasTrafficLight = OS_TYPE === 'macos';
hasWindowBar = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
hasContextMenu = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
hasRoundedWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android');
hasSafeAreaInset = OS_TYPE === 'ios' || OS_TYPE === 'android';
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
return resolvePath(fp, base);
@@ -242,7 +242,6 @@ export class TTSController extends EventTarget {
}
async kill() {
this.state = 'stopped';
await this.ttsClient.stop();
await this.stop();
}
}
@@ -3,6 +3,7 @@ import { ToastType, FileSystem, BaseDir, AppPlatform } from '@/types/system';
import { getCoverFilename } from '@/utils/book';
import { isValidURL } from '@/utils/misc';
import { isPWA } from './environment';
import { BaseAppService } from './appService';
import { LOCAL_BOOKS_SUBDIR } from './constants';
@@ -179,9 +180,13 @@ export class WebAppService extends BaseAppService {
fs = indexedDBFileSystem;
appPlatform = 'web' as AppPlatform;
isAppDataSandbox = false;
isAndroidApp = false;
isIOSApp = false;
hasTrafficLight = false;
hasWindowBar = false;
hasContextMenu = false;
hasRoundedWindow = false;
hasSafeAreaInset = isPWA();
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
return resolvePath(fp, base);
+1 -1
View File
@@ -212,7 +212,7 @@ foliate-view {
.pull-indicator {
position: fixed;
top: 64px;
top: calc(64px + env(safe-area-inset-top));
width: 100%;
background-color: transparent;
display: flex;
+4
View File
@@ -63,7 +63,11 @@ export interface BookLayout {
export interface BookStyle {
zoomLevel: number;
paragraphMargin: number;
lineHeight: number;
wordSpacing: number;
letterSpacing: number;
textIndent: number;
fullJustification: boolean;
hyphenation: boolean;
invert: boolean;
+5
View File
@@ -22,12 +22,17 @@ export interface FileSystem {
export interface AppService {
fs: FileSystem;
osPlatform: string;
appPlatform: AppPlatform;
hasTrafficLight: boolean;
hasWindowBar: boolean;
hasContextMenu: boolean;
hasRoundedWindow: boolean;
hasSafeAreaInset: boolean;
isMobile: boolean;
isAppDataSandbox: boolean;
isAndroidApp: boolean;
isIOSApp: boolean;
selectFiles(name: string, extensions: string[]): Promise<string[]>;
showMessage(msg: string, kind?: ToastType, title?: string, okLabel?: string): Promise<void>;
+1 -1
View File
@@ -64,8 +64,8 @@ export const getBookLangCode = (lang: string | string[] | undefined) => {
};
export const formatAuthors = (
bookLang: string | string[] | undefined,
contributors: string | Contributor | [string | Contributor],
bookLang?: string | string[],
) => {
const langCode = getBookLangCode(bookLang);
return Array.isArray(contributors)
+1 -1
View File
@@ -17,7 +17,7 @@ export const getSysFontsList = async (): Promise<string[]> => {
const osPlatform = getOSPlatform();
if (FONT_ENUM_SUPPORTED_OS_PLATFORMS.includes(osPlatform)) {
const fonts = await invoke<string[]>('list_fonts');
cachedSysFonts = fonts.filter((font) => !isSymbolicFontName(font)).sort();
cachedSysFonts = [...new Set(fonts.filter((font) => !isSymbolicFontName(font)))].sort();
console.log('Fetched font list:', cachedSysFonts);
return cachedSysFonts;
} else {
+17 -2
View File
@@ -16,6 +16,7 @@ const getFontStyles = (
monospace: string,
defaultFont: string,
fontSize: number,
fontWeight: number,
overrideFont: boolean,
) => {
const serifFonts = [serif, ...SERIF_FONTS.filter((font) => font !== serif), ...FALLBACK_FONTS];
@@ -34,6 +35,7 @@ const getFontStyles = (
html, body {
font-family: var(${defaultFont.toLowerCase() === 'serif' ? '--serif' : '--sans-serif'}) ${overrideFont ? '!important' : ''};
font-size: ${fontSize}px !important;
font-weight: ${fontWeight};
}
body * {
font-family: revert ${overrideFont ? '!important' : ''};
@@ -92,7 +94,11 @@ const getAdditionalFontFaces = () => `
`;
const getLayoutStyles = (
spacing: number,
paragraphMargin: number,
lineSpacing: number,
wordSpacing: number,
letterSpacing: number,
textIndent: number,
justify: boolean,
hyphenate: boolean,
zoomLevel: number,
@@ -144,6 +150,7 @@ const getLayoutStyles = (
html, body {
color: ${fg};
${writingMode === 'auto' ? '' : `writing-mode: ${writingMode};`}
text-indent: ${textIndent}em;
text-align: var(--default-text-align);
background-color: var(--theme-bg-color, transparent);
background: var(--background-set, none);
@@ -160,7 +167,10 @@ const getLayoutStyles = (
background-color: transparent !important;
}
p, li, blockquote, dd {
line-height: ${spacing} !important;
margin: ${paragraphMargin}em 0 !important;
line-height: ${lineSpacing} !important;
word-spacing: ${wordSpacing}px !important;
letter-spacing: ${letterSpacing}px !important;
text-align: inherit;
-webkit-hyphens: ${hyphenate ? 'auto' : 'manual'};
hyphens: ${hyphenate ? 'auto' : 'manual'};
@@ -201,7 +211,11 @@ export interface ThemeCode {
export const getStyles = (viewSettings: ViewSettings, themeCode: ThemeCode) => {
const layoutStyles = getLayoutStyles(
viewSettings.paragraphMargin!,
viewSettings.lineHeight!,
viewSettings.wordSpacing!,
viewSettings.letterSpacing!,
viewSettings.textIndent!,
viewSettings.fullJustification!,
viewSettings.hyphenation!,
viewSettings.zoomLevel! / 100.0,
@@ -219,6 +233,7 @@ export const getStyles = (viewSettings: ViewSettings, themeCode: ThemeCode) => {
viewSettings.monospaceFont!,
viewSettings.defaultFont!,
viewSettings.defaultFontSize! * fontScale,
viewSettings.fontWeight!,
viewSettings.overrideFont!,
);
const userStylesheet = viewSettings.userStylesheet!;