Compare commits

...

10 Commits

70 changed files with 861 additions and 209 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ yarn-error.log*
/private_keys
# plists
*.plist
Entitlements*.plist
# local confs
tauri.*.conf.json
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.35",
"version": "0.9.36",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -282,7 +282,6 @@
"Download finished": "اكتمل التنزيل",
"DOWNLOAD & INSTALL": "تنزيل وتثبيت",
"Changelog:": "سجل التغييرات:",
"Redownload": "إعادة التنزيل",
"Software Update": "تحديث البرنامج",
"Title": "العنوان",
"Date Read": "تاريخ القراءة",
@@ -290,5 +289,8 @@
"Format": "التنسيق",
"Ascending": "تصاعدي",
"Descending": "تنازلي",
"Sort by...": "ترتيب حسب..."
"Sort by...": "ترتيب حسب...",
"Added:": "تاريخ الإضافة:",
"Format:": "التنسيق:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "Download abgeschlossen",
"DOWNLOAD & INSTALL": "HERUNTERLADEN & INSTALLIEREN",
"Changelog:": "Änderungsprotokoll:",
"Redownload": "Neu herunterladen",
"Software Update": "Software-Update",
"Title": "Titel",
"Date Read": "Datum gelesen",
@@ -290,5 +289,8 @@
"Format": "Format",
"Ascending": "Aufsteigend",
"Descending": "Absteigend",
"Sort by...": "Sortieren nach..."
"Sort by...": "Sortieren nach...",
"Added:": "Hinzugefügt:",
"Format:": "Format:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "Η λήψη ολοκληρώθηκε",
"DOWNLOAD & INSTALL": "ΛΗΨΗ & ΕΓΚΑΤΑΣΤΑΣΗ",
"Changelog:": "Αλλαγές:",
"Redownload": "Επαναφόρτωση",
"Software Update": "Ενημέρωση λογισμικού",
"Title": "Τίτλος",
"Date Read": "Ημερομηνία ανάγνωσης",
@@ -290,5 +289,8 @@
"Format": "Μορφή",
"Ascending": "Αύξουσα",
"Descending": "Φθίνουσα",
"Sort by...": "Ταξινόμηση κατά..."
"Sort by...": "Ταξινόμηση κατά...",
"Added:": "Προστέθηκε:",
"Format:": "Μορφή:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "Descarga finalizada",
"DOWNLOAD & INSTALL": "DESCARGAR E INSTALAR",
"Changelog:": "Notas de la versión:",
"Redownload": "Volver a descargar",
"Software Update": "Actualización de software",
"Title": "Título",
"Date Read": "Fecha de lectura",
@@ -290,5 +289,8 @@
"Format": "Formato",
"Ascending": "Ascendente",
"Descending": "Descendente",
"Sort by...": "Ordenar por..."
"Sort by...": "Ordenar por...",
"Added:": "Agregado:",
"Format:": "Formato:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "Téléchargement terminé",
"DOWNLOAD & INSTALL": "TÉLÉCHARGER ET INSTALLER",
"Changelog:": "Journal des modifications :",
"Redownload": "Télécharger à nouveau",
"Software Update": "Mise à jour logicielle",
"Title": "Titre",
"Date Read": "Date de lecture",
@@ -290,5 +289,8 @@
"Format": "Format",
"Ascending": "Ascendant",
"Descending": "Descendant",
"Sort by...": "Trier par..."
"Sort by...": "Trier par...",
"Added:": "Ajouté :",
"Format:": "Format :",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "डाउनलोड पूरा हुआ",
"DOWNLOAD & INSTALL": "डाउनलोड और इंस्टॉल करें",
"Changelog:": "चेंजलॉग:",
"Redownload": "री-डाउनलोड करें",
"Software Update": "सॉफ़्टवेयर अपडेट",
"Title": "शीर्षक",
"Date Read": "पढ़ने की तारीख",
@@ -290,5 +289,8 @@
"Format": "फॉर्मेट",
"Ascending": "आरोही",
"Descending": "अवरोही",
"Sort by...": "द्वारा क्रमबद्ध करें..."
"Sort by...": "द्वारा क्रमबद्ध करें...",
"Added:": "जोड़ा गया:",
"Format:": "फॉर्मेट:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "Unduhan selesai",
"DOWNLOAD & INSTALL": "UNDUH & INSTAL",
"Changelog:": "Catatan Perubahan:",
"Redownload": "Unduh Ulang",
"Software Update": "Pembaruan Perangkat Lunak",
"Title": "Judul",
"Date Read": "Tanggal Dibaca",
@@ -290,5 +289,8 @@
"Format": "Format",
"Ascending": "Naik",
"Descending": "Turun",
"Sort by...": "Urutkan berdasarkan..."
"Sort by...": "Urutkan berdasarkan...",
"Added:": "Ditambahkan:",
"Format:": "Format:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "Download completato",
"DOWNLOAD & INSTALL": "SCARICA E INSTALLA",
"Changelog:": "Note di rilascio:",
"Redownload": "Scarica di nuovo",
"Software Update": "Aggiornamento software",
"Title": "Titolo",
"Date Read": "Data di lettura",
@@ -290,5 +289,8 @@
"Format": "Formato",
"Ascending": "Crescente",
"Descending": "Decrescente",
"Sort by...": "Ordina per..."
"Sort by...": "Ordina per...",
"Added:": "Aggiunto:",
"Format:": "Formato:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "ダウンロード完了",
"DOWNLOAD & INSTALL": "更新",
"Changelog:": "変更ログ:",
"Redownload": "再ダウンロード",
"Software Update": "ソフトウェアの更新",
"Title": "タイトル",
"Date Read": "読書日",
@@ -290,5 +289,8 @@
"Format": "形式",
"Ascending": "昇順",
"Descending": "降順",
"Sort by...": "並べ替え..."
"Sort by...": "並べ替え...",
"Added:": "追加日:",
"Format:": "形式:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "다운로드 완료",
"DOWNLOAD & INSTALL": "다운로드 및 설치",
"Changelog:": "변경 로그:",
"Redownload": "다시 다운로드",
"Software Update": "소프트웨어 업데이트",
"Title": "제목",
"Date Read": "읽은 날짜",
@@ -290,5 +289,8 @@
"Format": "형식",
"Ascending": "오름차순",
"Descending": "내림차순",
"Sort by...": "정렬 기준..."
"Sort by...": "정렬 기준...",
"Added:": "추가됨:",
"Format:": "형식:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "Pobieranie zakończone",
"DOWNLOAD & INSTALL": "POBIERZ I ZAINSTALUJ",
"Changelog:": "Dziennik zmian:",
"Redownload": "Pobierz ponownie",
"Software Update": "Aktualizacja oprogramowania",
"Title": "Tytuł",
"Date Read": "Data przeczytania",
@@ -290,5 +289,8 @@
"Format": "Format",
"Ascending": "Rosnąco",
"Descending": "Malejąco",
"Sort by...": "Sortuj według..."
"Sort by...": "Sortuj według...",
"Added:": "Data dodania:",
"Format:": "Format:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "Download concluído",
"DOWNLOAD & INSTALL": "BAIXAR E INSTALAR",
"Changelog:": "Notas de versão:",
"Redownload": "Baixar novamente",
"Software Update": "Atualização de Software",
"Title": "Título",
"Date Read": "Data de leitura",
@@ -290,5 +289,8 @@
"Format": "Formato",
"Ascending": "Crescente",
"Descending": "Decrescente",
"Sort by...": "Ordenar por..."
"Sort by...": "Ordenar por...",
"Added:": "Adicionado:",
"Format:": "Formato:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "Загрузка завершена",
"DOWNLOAD & INSTALL": "СКАЧАТЬ И УСТАНОВИТЬ",
"Changelog:": "Изменения:",
"Redownload": "Переустановить",
"Software Update": "Обновление программного обеспечения",
"Title": "Название",
"Date Read": "Дата прочтения",
@@ -290,5 +289,8 @@
"Format": "Формат",
"Ascending": "По возрастанию",
"Descending": "По убыванию",
"Sort by...": "Сортировать по..."
"Sort by...": "Сортировать по...",
"Added:": "Добавлено:",
"Format:": "Формат:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "İndirme tamamlandı",
"DOWNLOAD & INSTALL": "İNDİR & YÜKLE",
"Changelog:": "Değişiklik Günlüğü:",
"Redownload": "Yeniden İndir",
"Software Update": "Yazılım Güncellemesi",
"Title": "Başlık",
"Date Read": "Okunma Tarihi",
@@ -290,5 +289,8 @@
"Format": "Biçim",
"Ascending": "Artan",
"Descending": "Azalan",
"Sort by...": "Şuna göre sırala..."
"Sort by...": "Şuna göre sırala...",
"Added:": "Eklenme:",
"Format:": "Biçim:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "Завантаження завершено",
"DOWNLOAD & INSTALL": "ЗАВАНТАЖИТИ І ВСТАНОВИТИ",
"Changelog:": "Журнал змін:",
"Redownload": "Завантажити знову",
"Software Update": "Оновлення програмного забезпечення",
"Title": "Назва",
"Date Read": "Дата прочитання",
@@ -290,5 +289,8 @@
"Format": "Формат",
"Ascending": "За зростанням",
"Descending": "За спаданням",
"Sort by...": "Сортувати за..."
"Sort by...": "Сортувати за...",
"Added:": "Додано:",
"Format:": "Формат:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "Tải xuống hoàn tất",
"DOWNLOAD & INSTALL": "TẢI XUỐNG & CÀI ĐẶT",
"Changelog:": "Thay đổi:",
"Redownload": "Tải lại",
"Software Update": "Cập nhật phần mềm",
"Title": "Tiêu đề",
"Date Read": "Ngày đã đọc",
@@ -290,5 +289,8 @@
"Format": "Định dạng",
"Ascending": "Tăng dần",
"Descending": "Giảm dần",
"Sort by...": "Sắp xếp theo..."
"Sort by...": "Sắp xếp theo...",
"Added:": "Đã thêm:",
"Format:": "Định dạng:",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T"
}
@@ -282,7 +282,6 @@
"Download finished": "下载完成",
"DOWNLOAD & INSTALL": "下载并安装",
"Changelog:": "更新日志:",
"Redownload": "重新下载",
"Software Update": "软件更新",
"Title": "书名",
"Date Read": "阅读日期",
@@ -290,5 +289,8 @@
"Format": "格式",
"Ascending": "升序",
"Descending": "降序",
"Sort by...": "排序方式..."
"Sort by...": "排序方式...",
"Added:": "添加日期",
"Format:": "格式",
"GuanKiapTsingKhai-T": "原俠正楷-T"
}
@@ -282,7 +282,6 @@
"Download finished": "下載完成",
"DOWNLOAD & INSTALL": "下載並安裝",
"Changelog:": "更新日誌:",
"Redownload": "重新下載",
"Software Update": "軟體更新",
"Title": "書名",
"Date Read": "閱讀日期",
@@ -290,5 +289,8 @@
"Format": "格式",
"Ascending": "升序",
"Descending": "降序",
"Sort by...": "排序方式..."
"Sort by...": "排序方式...",
"Added:": "添加日期",
"Format:": "格式",
"GuanKiapTsingKhai-T": "原俠正楷-T"
}
+10
View File
@@ -1,5 +1,15 @@
{
"releases": {
"0.9.36": {
"date": "2025-04-20",
"notes": [
"📥 New download and upload buttons in the book detail dialog for easier file management",
"📂 You can now open files directly in Readest from your file manager on Android and iOS",
"🛠️ Fixed an issue where files with quotation marks in the filename could not be opened",
"📖 Improved the immersive reading experience on the reader page for Android and iOS",
"🌐 Added GuanKiapTsingKhai-T to the list of available CJK fonts"
]
},
"0.9.35": {
"date": "2025-04-15",
"notes": [
+201
View File
@@ -0,0 +1,201 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIHomeIndicatorAutoHidden</key>
<true/>
<key>UIRequiresFullScreen</key>
<true/>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>EPUB Document</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>org.idpf.epub-container</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>PDF Document</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>com.adobe.pdf</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>FB2 Document</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>com.readest.fb2</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>CBZ Archive</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>com.readest.cbz</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>MOBI Document</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>org.mobipocket.mobi</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>AZW Document</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>com.amazon.azw</string>
<string>com.amazon.azw3</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>Text File</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>public.plain-text</string>
</array>
</dict>
</array>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeIdentifier</key>
<string>org.idpf.epub-container</string>
<key>UTTypeDescription</key>
<string>EPUB Document</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.content</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>epub</string>
</array>
<key>public.mime-type</key>
<string>application/epub+zip</string>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>com.readest.fb2</string>
<key>UTTypeDescription</key>
<string>FB2 Document</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.xml</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>fb2</string>
</array>
<key>public.mime-type</key>
<string>application/xml</string>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>com.readest.cbz</string>
<key>UTTypeDescription</key>
<string>CBZ Archive</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.archive</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>cbz</string>
</array>
<key>public.mime-type</key>
<string>application/x-cbz</string>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>org.mobipocket.mobi</string>
<key>UTTypeDescription</key>
<string>MOBI Document</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>mobi</string>
</array>
<key>public.mime-type</key>
<string>application/x-mobipocket-ebook</string>
</dict>
</dict>
<dict>
<key>UTTypeIdentifier</key>
<string>com.amazon.azw</string>
<key>UTTypeDescription</key>
<string>AZW Document</string>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
</array>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>azw</string>
<string>azw3</string>
</array>
<key>public.mime-type</key>
<string>application/vnd.amazon.ebook</string>
</dict>
</dict>
</array>
</dict>
</plist>
@@ -39,6 +39,17 @@
}
]
},
{
"identifier": "fs:allow-cache-read",
"allow": [
{
"path": "$CACHEDIR/**/*"
},
{
"path": "/private/var/mobile/Containers/Data/Application/**/*"
}
]
},
{
"identifier": "http:default",
"allow": [
@@ -20,12 +20,43 @@
android:label="@string/main_activity_title"
android:name=".MainActivity"
android:exported="true">
<intent-filter android:label="oauth">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="readest" android:host="auth-callback" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" />
<data android:scheme="file" />
<data android:mimeType="application/epub+zip" />
<data android:mimeType="application/pdf" />
<data android:mimeType="application/fb2" />
<data android:mimeType="application/x-fb2" />
<data android:mimeType="application/vnd.amazon.mobi8-ebook" />
<data android:mimeType="application/vnd.comicbook+zip" />
<data android:mimeType="application/x-cbz" />
<data android:mimeType="application/x-mobipocket-ebook" />
<data android:mimeType="application/zip" />
<data android:mimeType="text/plain" />
<data android:pathPattern=".*\\.epub" />
<data android:pathPattern=".*\\.pdf" />
<data android:pathPattern=".*\\.fb2" />
<data android:pathPattern=".*\\.fb2.zip" />
<data android:pathPattern=".*\\.mobi" />
<data android:pathPattern=".*\\.azw" />
<data android:pathPattern=".*\\.cbz" />
<data android:pathPattern=".*\\.zip" />
<data android:pathPattern=".*\\.txt" />
</intent-filter>
<intent-filter android:label="oauth">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="readest" android:host="auth-callback" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -1,51 +1,11 @@
package com.bilingify.readest
import android.os.Build
import android.os.Bundle
import android.content.Intent
import android.net.Uri
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import androidx.activity.enableEdgeToEdge
import app.tauri.plugin.JSObject
import com.readest.native_bridge.NativeBridgePlugin
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
hideSystemUI()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val uri = intent.data ?: return
if (uri.scheme == "readest" && uri.host == "auth-callback") {
val result = JSObject().apply {
put("redirectUrl", uri.toString())
}
NativeBridgePlugin.pendingInvoke?.resolve(result)
NativeBridgePlugin.pendingInvoke = null
}
}
private fun hideSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.decorView.windowInsetsController?.let { controller ->
controller.hide(WindowInsets.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
} else {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
)
}
}
}
@@ -2,8 +2,12 @@
<!-- Base application theme. -->
<style name="Theme.readest" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:fitsSystemWindows">false</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
</resources>
@@ -4,6 +4,13 @@ import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.util.Log
import android.os.Build
import android.view.View
import android.view.WindowInsets
import android.view.WindowManager
import android.view.WindowInsetsController
import android.graphics.Color
import android.webkit.WebView
import androidx.core.content.FileProvider
import androidx.browser.customtabs.CustomTabsIntent
import app.tauri.annotation.Command
@@ -30,6 +37,12 @@ class InstallPackageRequestArgs {
var path: String? = null
}
@InvokeArg
class SetSystemUIVisibilityRequestArgs {
var visible: Boolean? = false
var darkMode: Boolean? = false
}
@TauriPlugin
class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
private val implementation = NativeBridge()
@@ -40,6 +53,40 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
var pendingInvoke: Invoke? = null
}
override fun load(webView: WebView) {
super.load(webView)
handleIntent(activity.intent)
}
override fun onNewIntent(intent: Intent) {
handleIntent(intent)
}
private fun handleIntent(intent: Intent?) {
val uri = intent?.data ?: return
Log.e("NativeBridgePlugin", "Received intent: $uri")
when {
uri.scheme == "readest" && uri.host == "auth-callback" -> {
val result = JSObject().apply {
put("redirectUrl", uri.toString())
}
pendingInvoke?.resolve(result)
pendingInvoke = null
}
intent.action == Intent.ACTION_VIEW -> {
try {
activity.contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} catch (e: SecurityException) {
Log.e("NativeBridgePlugin", "Failed to take persistable URI permission: ${e.message}")
}
}
}
}
@Command
fun auth_with_custom_tab(invoke: Invoke) {
val args = invoke.parseArgs(AuthRequestArgs::class.java)
@@ -110,4 +157,71 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
}
invoke.resolve(ret)
}
@Command
fun set_system_ui_visibility(invoke: Invoke) {
val args = invoke.parseArgs(SetSystemUIVisibilityRequestArgs::class.java)
val visible = args.visible ?: false
var isDarkMode = args.darkMode ?: false
val ret = JSObject()
try {
val window = activity.window
val decorView = window.decorView
if (!visible) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false)
val controller = window.insetsController
if (controller != null) {
controller.systemBarsBehavior =
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
if (isDarkMode) {
controller.setSystemBarsAppearance(
0,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
)
} else {
controller.setSystemBarsAppearance(
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
)
}
if (visible) {
controller.show(WindowInsets.Type.statusBars())
} else {
controller.hide(WindowInsets.Type.systemBars())
}
}
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
} else {
@Suppress("DEPRECATION")
decorView.systemUiVisibility = when {
visible && !isDarkMode -> View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
visible -> View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
else -> View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_FULLSCREEN
}
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
}
ret.put("success", true)
} catch (e: Exception) {
ret.put("success", false)
ret.put("error", e.message)
}
invoke.resolve(ret)
}
}
@@ -4,6 +4,7 @@ const COMMANDS: &[&str] = &[
"copy_uri_to_path",
"use_background_audio",
"install_package",
"set_system_ui_visibility",
];
fn main() {
@@ -14,6 +14,11 @@ class UseBackgroundAudioRequestArgs: Decodable {
let enabled: Bool
}
class SetSystemUIVisibilityRequestArgs: Decodable {
let visible: Bool
let darkMode: Bool
}
class NativeBridgePlugin: Plugin {
private var authSession: ASWebAuthenticationSession?
@@ -66,6 +71,30 @@ class NativeBridgePlugin: Plugin {
let started = authSession?.start() ?? false
Logger.info("Auth session start result: \(started)")
}
@objc public func set_system_ui_visibility(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(SetSystemUIVisibilityRequestArgs.self)
let visible = args.visible
let darkMode = args.darkMode
DispatchQueue.main.async {
UIApplication.shared.isIdleTimerDisabled = !visible
UIApplication.shared.setStatusBarHidden(!visible, with: .none)
let windows = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
let keyWindow = windows.first(where: { $0.isKeyWindow }) ?? windows.first
if let keyWindow = keyWindow {
keyWindow.overrideUserInterfaceStyle = darkMode ? .dark : .light
keyWindow.layoutIfNeeded()
} else {
print("No key window found")
}
}
invoke.resolve(["success": true])
}
}
@_cdecl("init_plugin_native_bridge")
@@ -78,4 +107,4 @@ extension NativeBridgePlugin: ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
return UIApplication.shared.windows.first ?? UIWindow()
}
}
}
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-set-system-ui-visibility"
description = "Enables the set_system_ui_visibility command without any pre-configured scope."
commands.allow = ["set_system_ui_visibility"]
[[permission]]
identifier = "deny-set-system-ui-visibility"
description = "Denies the set_system_ui_visibility command without any pre-configured scope."
commands.deny = ["set_system_ui_visibility"]
@@ -9,6 +9,7 @@ Default permissions for the plugin
- `allow-copy-uri-to-path`
- `allow-use-background-audio`
- `allow-install-package`
- `allow-set-system-ui-visibility`
## Permission Table
@@ -126,6 +127,32 @@ Denies the install_package command without any pre-configured scope.
<tr>
<td>
`native-bridge:allow-set-system-ui-visibility`
</td>
<td>
Enables the set_system_ui_visibility command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-set-system-ui-visibility`
</td>
<td>
Denies the set_system_ui_visibility command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-use-background-audio`
</td>
@@ -6,4 +6,5 @@ permissions = [
"allow-copy-uri-to-path",
"allow-use-background-audio",
"allow-install-package",
"allow-set-system-ui-visibility",
]
@@ -342,6 +342,18 @@
"const": "deny-install-package",
"markdownDescription": "Denies the install_package command without any pre-configured scope."
},
{
"description": "Enables the set_system_ui_visibility command without any pre-configured scope.",
"type": "string",
"const": "allow-set-system-ui-visibility",
"markdownDescription": "Enables the set_system_ui_visibility command without any pre-configured scope."
},
{
"description": "Denies the set_system_ui_visibility command without any pre-configured scope.",
"type": "string",
"const": "deny-set-system-ui-visibility",
"markdownDescription": "Denies the set_system_ui_visibility command without any pre-configured scope."
},
{
"description": "Enables the use_background_audio command without any pre-configured scope.",
"type": "string",
@@ -355,10 +367,10 @@
"markdownDescription": "Denies the use_background_audio command without any pre-configured scope."
},
{
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`",
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`",
"type": "string",
"const": "default",
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`"
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`"
}
]
}
@@ -43,3 +43,11 @@ pub(crate) async fn install_package<R: Runtime>(
) -> Result<InstallPackageResponse> {
app.native_bridge().install_package(payload)
}
#[command]
pub(crate) async fn set_system_ui_visibility<R: Runtime>(
app: AppHandle<R>,
payload: SetSystemUIVisibilityRequest,
) -> Result<SetSystemUIVisibilityResponse> {
app.native_bridge().set_system_ui_visibility(payload)
}
@@ -36,4 +36,11 @@ impl<R: Runtime> NativeBridge<R> {
) -> crate::Result<InstallPackageResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn set_system_ui_visibility(
&self,
_payload: SetSystemUIVisibilityRequest,
) -> crate::Result<SetSystemUIVisibilityResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
}
@@ -42,6 +42,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::copy_uri_to_path,
commands::use_background_audio,
commands::install_package,
commands::set_system_ui_visibility,
])
.setup(|app, api| {
#[cfg(mobile)]
@@ -66,3 +66,14 @@ impl<R: Runtime> NativeBridge<R> {
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn set_system_ui_visibility(
&self,
payload: SetSystemUIVisibilityRequest,
) -> crate::Result<SetSystemUIVisibilityResponse> {
self.0
.run_mobile_plugin("set_system_ui_visibility", payload)
.map_err(Into::into)
}
}
@@ -44,3 +44,17 @@ pub struct InstallPackageResponse {
pub success: bool,
pub error: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetSystemUIVisibilityRequest {
pub visible: bool,
pub dark_mode: bool,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetSystemUIVisibilityResponse {
pub success: bool,
pub error: Option<String>,
}
+4 -1
View File
@@ -71,7 +71,10 @@ fn set_window_open_with_files(app: &AppHandle, files: Vec<PathBuf>) {
let files = files
.into_iter()
.map(|f| {
let file = f.to_string_lossy().replace("\\", "\\\\");
let file = f
.to_string_lossy()
.replace("\\", "\\\\")
.replace("\"", "\\\"");
format!("\"{file}\"",)
})
.collect::<Vec<_>>()
+8 -3
View File
@@ -17,15 +17,20 @@
"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' blob: data: asset: http://asset.localhost tauri: https://fonts.gstatic.com https://db.onlinewebfonts.com https://cdn.jsdelivr.net",
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://fontsapi.zeoseven.com",
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://fontsapi.zeoseven.com",
"frame-src": "'self' blob: asset: http://asset.localhost",
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com"
},
"assetProtocol": {
"enable": true,
"scope": {
"allow": ["$RESOURCE/**", "$APPDATA/**/*", "$TEMP/**/*"],
"allow": [
"$RESOURCE/**",
"$APPDATA/**/*",
"$TEMP/**/*",
"/private/var/mobile/Containers/Data/Application/**/*"
],
"deny": []
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { useTheme } from '@/hooks/useTheme';
export default function AuthErrorPage() {
const router = useRouter();
useTheme();
useTheme({ systemUIVisible: false });
useEffect(() => {
const timer = setTimeout(() => {
+3
View File
@@ -13,6 +13,7 @@ import { IoArrowBack } from 'react-icons/io5';
import { useAuth } from '@/context/AuthContext';
import { supabase } from '@/utils/supabase';
import { useEnv } from '@/context/EnvContext';
import { useTheme } from '@/hooks/useTheme';
import { useThemeStore } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
@@ -74,6 +75,8 @@ export default function AuthPage() {
const headerRef = useRef<HTMLDivElement>(null);
useTheme({ systemUIVisible: false });
const getTauriRedirectTo = (isOAuth: boolean) => {
if (process.env.NODE_ENV === 'production' || appService?.isMobile || USE_APPLE_SIGN_IN) {
if (appService?.isMobile) {
@@ -92,10 +92,12 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
<div
ref={headerRef}
className={clsx(
'titlebar z-10 flex h-[52px] w-full items-center py-2 pr-4 sm:h-[48px] sm:pr-6',
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
'titlebar bg-base-200 z-10 flex h-[52px] w-full items-center py-2 pr-4 sm:h-[48px] sm:pr-6',
isTrafficLightVisible ? 'pl-16' : 'pl-0 sm:pl-2',
)}
style={{
marginTop: appService?.hasSafeAreaInset ? 'max(env(safe-area-inset-top), 24px)' : '',
}}
>
<div className='flex w-full 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'>
@@ -175,16 +177,16 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<MdOutlineMenu size={iconSize18} />}
toggleButton={<PiDotsThreeCircle size={iconSize18} />}
>
<SettingsMenu />
<SortMenu />
</Dropdown>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeCircle size={iconSize18} />}
toggleButton={<MdOutlineMenu size={iconSize18} />}
>
<SortMenu />
<SettingsMenu />
</Dropdown>
{appService?.hasWindowBar && (
<WindowButtons
+13 -12
View File
@@ -12,7 +12,7 @@ import { getFilename, 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 { parseOpenWithFiles } from '@/helpers/openWith';
import { isTauriAppPlatform } from '@/services/environment';
import { checkForAppUpdates } from '@/helpers/updater';
import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants';
@@ -21,7 +21,6 @@ import { getCurrentWebview } from '@tauri-apps/api/webview';
import { useEnv } from '@/context/EnvContext';
import { useAuth } from '@/context/AuthContext';
import { useThemeStore } from '@/store/themeStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore';
@@ -60,8 +59,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
setCheckOpenWithBooks,
} = useLibraryStore();
const _ = useTranslation();
useTheme();
const { updateAppTheme } = useThemeStore();
useTheme({ systemUIVisible: true, appThemeColor: 'base-200' });
const { settings, setSettings, saveSettings } = useSettingsStore();
const [loading, setLoading] = useState(false);
const isInitiating = useRef(false);
@@ -94,11 +92,6 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
},
});
useEffect(() => {
updateAppTheme('base-200');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const doCheckAppUpdates = async () => {
if (appService?.hasUpdater && settings.autoCheckUpdates) {
@@ -204,7 +197,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
for (const file of openWithFiles) {
console.log('Open with book:', file);
try {
const temp = !settings.autoImportBooksOnOpen;
const temp = appService.isMobile ? false : !settings.autoImportBooksOnOpen;
const book = await appService.importBook(file, libraryBooks, true, true, false, temp);
if (book) {
bookIds.push(book.hash);
@@ -533,11 +526,16 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
<div
ref={containerRef}
className={clsx(
'scroll-container drop-zone mt-[48px] flex-grow overflow-y-auto px-4 sm:px-2',
appService?.hasSafeAreaInset && 'mt-[calc(52px+env(safe-area-inset-top))]',
'scroll-container drop-zone flex-grow overflow-y-auto px-4 sm:px-2',
appService?.hasSafeAreaInset && 'pt-[52px]',
appService?.hasSafeAreaInset && 'pb-[calc(env(safe-area-inset-bottom))]',
isDragging && 'drag-over',
)}
style={{
marginTop: appService?.hasSafeAreaInset
? 'max(env(safe-area-inset-top), 24px)'
: '48px',
}}
>
<DropIndicator />
<Bookshelf
@@ -575,6 +573,9 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
isOpen={!!showDetailsBook}
book={showDetailsBook}
onClose={() => setShowDetailsBook(null)}
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
handleBookDelete={handleBookDelete}
/>
)}
<AboutWindow />
@@ -42,7 +42,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
cleanupTrafficLightListeners,
} = useTrafficLightStore();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { bookKeys, hoveredBookKey, setHoveredBookKey } = useReaderStore();
const { bookKeys, hoveredBookKey, systemUIVisible, setHoveredBookKey } = useReaderStore();
const { isSideBarVisible } = useSidebarStore();
const iconSize16 = useResponsiveSize(16);
@@ -89,20 +89,27 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
'bg-base-100 absolute left-0 right-0 top-0 z-10 h-[env(safe-area-inset-top)]',
isVisible ? 'visible' : 'hidden',
)}
style={{
height: systemUIVisible ? 'max(env(safe-area-inset-top), 24px)' : '',
}}
/>
<div
ref={headerRef}
className={clsx(
`header-bar bg-base-100 absolute top-0 z-10 flex h-11 w-full items-center pr-4`,
`shadow-xs transition-opacity duration-300`,
`shadow-xs transition-[opacity,margin-top] duration-300`,
isTrafficLightVisible && isTopLeft && !isSideBarVisible ? 'pl-16' : 'pl-4',
appService?.hasRoundedWindow && 'rounded-window-top-right',
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-top-left',
isHoveredAnim && 'hover-bar-anim',
isVisible ? 'pointer-events-auto visible' : 'pointer-events-none opacity-0',
isDropdownOpen && 'header-bar-pinned',
)}
style={{
marginTop: systemUIVisible
? 'max(env(safe-area-inset-top), 24px)'
: 'env(safe-area-inset-top)',
}}
onMouseLeave={() => !appService?.isMobile && setHoveredBookKey('')}
>
<div className='sidebar-bookmark-toggler z-20 flex h-full items-center gap-x-4'>
@@ -139,7 +146,10 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
showMaximize={
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
}
onClose={() => onCloseBook(bookKey)}
onClose={() => {
setHoveredBookKey(null);
onCloseBook(bookKey);
}}
/>
</div>
</div>
@@ -7,10 +7,12 @@ import { useEffect, Suspense, useRef } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useTheme } from '@/hooks/useTheme';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
import { useLibraryStore } from '@/store/libraryStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
import { setSystemUIVisibility } from '@/utils/bridge';
import { AboutWindow } from '@/components/AboutWindow';
import { UpdaterWindow } from '@/components/UpdaterWindow';
import { Toast } from '@/components/Toast';
@@ -19,16 +21,16 @@ import ReaderContent from './ReaderContent';
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
const { envConfig, appService } = useEnv();
const { settings, setSettings } = useSettingsStore();
const { isDarkMode } = useThemeStore();
const { hoveredBookKey, showSystemUI, dismissSystemUI } = useReaderStore();
const { isSideBarVisible } = useSidebarStore();
const { getVisibleLibrary, setLibrary } = useLibraryStore();
const isInitiating = useRef(false);
const { updateAppTheme } = useThemeStore();
useTheme();
useTheme({ systemUIVisible: false, appThemeColor: 'base-100' });
useScreenWakeLock(settings.screenWakeLock);
useEffect(() => {
updateAppTheme('base-100');
if (isInitiating.current) return;
isInitiating.current = true;
const initLibrary = async () => {
@@ -42,6 +44,18 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!appService?.isMobile) return;
const systemUIVisible = !!hoveredBookKey;
setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode });
if (systemUIVisible) {
showSystemUI();
} else {
dismissSystemUI();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hoveredBookKey]);
return (
getVisibleLibrary().length > 0 &&
settings.globalReadSettings && (
@@ -12,7 +12,7 @@ import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { Book } from '@/types/book';
import { SystemSettings } from '@/types/settings';
import { parseOpenWithFiles } from '@/helpers/cli';
import { parseOpenWithFiles } from '@/helpers/openWith';
import { tauriHandleClose, tauriHandleOnCloseWindow } from '@/utils/window';
import { isTauriAppPlatform } from '@/services/environment';
import { uniqueId } from '@/utils/misc';
@@ -135,7 +135,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
dismissBook(bookKey);
if (bookKeys.filter((key) => key !== bookKey).length == 0) {
const openWithFiles = (await parseOpenWithFiles()) || [];
if (openWithFiles.length > 0) {
if (openWithFiles.length > 0 && !appService?.isMobile) {
tauriHandleClose();
} else {
saveSettingsAndGoToLibrary();
@@ -200,8 +200,10 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const lineHeightValue =
parseFloat(lineHeight) || viewSettings.lineHeight * viewSettings.defaultFontSize;
const fontSizeValue = parseFloat(fontSize) || viewSettings.defaultFontSize;
const strokeWidth = style === 'underline' ? 2 : 4;
const padding = (lineHeightValue - fontSizeValue - strokeWidth) / 2;
const strokeWidth = 2;
const padding = viewSettings.vertical
? (lineHeightValue - fontSizeValue - strokeWidth) / 2
: strokeWidth;
draw(Overlayer[style as keyof typeof Overlayer], { writingMode, color: hexColor, padding });
}
};
@@ -38,9 +38,10 @@ const TTSControl = () => {
const [timeoutTimestamp, setTimeoutTimestamp] = useState(0);
const [timeoutFunc, setTimeoutFunc] = useState<ReturnType<typeof setTimeout> | null>(null);
const popupWidth = useResponsiveSize(POPUP_WIDTH);
const popupHeight = useResponsiveSize(POPUP_HEIGHT);
const popupPadding = useResponsiveSize(POPUP_PADDING);
const maxWidth = window.innerWidth - 2 * popupPadding;
const popupWidth = Math.min(maxWidth, useResponsiveSize(POPUP_WIDTH));
const popupHeight = useResponsiveSize(POPUP_HEIGHT);
const iconRef = useRef<HTMLDivElement>(null);
const ttsControllerRef = useRef<TTSController | null>(null);
+4 -1
View File
@@ -1,10 +1,13 @@
'use client';
import { Suspense } from 'react';
import Spinner from '@/components/Spinner';
import { UpdaterContent } from '@/components/UpdaterWindow';
import { useTheme } from '@/hooks/useTheme';
import Spinner from '@/components/Spinner';
const UpdaterPage = () => {
useTheme();
return (
<Suspense
fallback={
+1 -1
View File
@@ -34,7 +34,7 @@ const ProfilePage = () => {
const headerRef = useRef<HTMLDivElement>(null);
useTheme();
useTheme({ systemUIVisible: false });
useEffect(() => {
if (!user || !token) return;
@@ -6,6 +6,7 @@ import { useTranslation } from '@/hooks/useTranslation';
import { checkForAppUpdates } from '@/helpers/updater';
import { parseWebViewVersion } from '@/utils/ua';
import Dialog from './Dialog';
import Link from './Link';
export const setAboutDialogVisible = (visible: boolean) => {
const dialog = document.getElementById('about_window');
@@ -75,27 +76,20 @@ export const AboutWindow = () => {
</p>
<p className='text-neutral-content mt-2 text-xs'>
This software is licensed under the{' '}
<a
<Link
href='https://www.gnu.org/licenses/agpl-3.0.html'
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
GNU Affero General Public License v3.0
</a>
</Link>
. You are free to use, modify, and distribute this software under the terms of the AGPL
v3 license. Please see the license for more details.
</p>
<p className='text-neutral-content my-2 text-xs'>
Source code is available at{' '}
<a
href='https://github.com/readest/readest'
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
<Link href='https://github.com/readest/readest' className='text-blue-500 underline'>
GitHub
</a>
</Link>
.
</p>
</div>
+1 -1
View File
@@ -38,7 +38,7 @@ const Alert: React.FC<{
<div className='text-xs'>{message}</div>
</div>
</div>
<div className='flex flex-wrap items-center justify-center gap-2'>
<div className='flex max-w-[20vw] flex-wrap items-center justify-center gap-2'>
<button className='btn btn-sm' onClick={onCancel}>
{_('Cancel')}
</button>
@@ -1,13 +1,13 @@
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import { MdDelete, MdCloudDownload, MdCloudUpload } from 'react-icons/md';
import { Book } from '@/types/book';
import { BookDoc } from '@/libs/document';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useLibraryStore } from '@/store/libraryStore';
import {
formatAuthors,
formatDate,
@@ -24,16 +24,25 @@ interface BookDetailModalProps {
book: Book;
isOpen: boolean;
onClose: () => void;
handleBookDownload?: (book: Book) => void;
handleBookUpload?: (book: Book) => void;
handleBookDelete?: (book: Book) => void;
}
const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
const BookDetailModal = ({
book,
isOpen,
onClose,
handleBookDownload,
handleBookUpload,
handleBookDelete,
}: BookDetailModalProps) => {
const _ = useTranslation();
const [loading, setLoading] = useState(false);
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
const [bookMeta, setBookMeta] = useState<BookDoc['metadata'] | null>(null);
const { envConfig, appService } = useEnv();
const { envConfig } = useEnv();
const { settings } = useSettingsStore();
const { updateBook } = useLibraryStore();
useEffect(() => {
const loadingTimeout = setTimeout(() => setLoading(true), 300);
@@ -61,10 +70,25 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
};
const confirmDelete = async () => {
await appService?.deleteBook(book, !!book.uploadedAt);
await updateBook(envConfig, book);
handleClose();
setShowDeleteAlert(false);
if (handleBookDelete) {
handleBookDelete(book);
}
};
const handleRedownload = async () => {
handleClose();
if (handleBookDownload) {
handleBookDownload(book);
}
};
const handleReupload = async () => {
handleClose();
if (handleBookUpload) {
handleBookUpload(book);
}
};
if (!bookMeta)
@@ -83,13 +107,13 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
isOpen={isOpen}
onClose={handleClose}
bgClassName='sm:bg-black/50'
boxClassName='sm:min-w-[480px] sm:h-auto'
boxClassName='sm:min-w-[480px] sm:max-w-[480px] sm:h-auto'
contentClassName='!px-6 !py-2'
>
<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'>
<div className='mb-10 me-4 flex h-32 items-start'>
<div className='book-cover relative mr-10 aspect-[28/41] h-32 items-end shadow-lg'>
<Image
src={book.coverImageUrl!}
alt={formatTitle(book.title)}
@@ -112,49 +136,35 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
</div>
</div>
<div className='title-author flex h-40 flex-col justify-between'>
<div className='title-author flex h-32 flex-col justify-between'>
<div>
<p className='text-base-content mb-2 line-clamp-2 break-all text-2xl font-bold'>
<p className='text-base-content mb-2 line-clamp-2 break-all text-lg font-bold'>
{formatTitle(book.title) || _('Untitled')}
</p>
<p className='text-neutral-content line-clamp-1'>
{formatAuthors(book.author, bookMeta.language) || _('Unknown')}
</p>
</div>
{window.innerWidth >= 400 && (
<div className='flex flex-wrap items-center gap-x-4 gap-y-2 py-2'>
<button
className='btn rounded-xl bg-red-600 px-4 text-white hover:bg-red-700'
onClick={handleDelete}
>
{_('Delete')}
<div className='flex flex-wrap items-center gap-x-4'>
{handleBookDelete && (
<button onClick={handleDelete}>
<MdDelete className='fill-red-500' />
</button>
{book.uploadedAt && (
<button className='btn rounded-xl bg-orange-500 text-white hover:bg-orange-600'>
{_('Redownload')}
</button>
)}
</div>
)}
)}
{book.uploadedAt && handleBookDownload && (
<button onClick={handleRedownload}>
<MdCloudDownload className='fill-base-content' />
</button>
)}
{book.downloadedAt && handleBookUpload && (
<button onClick={handleReupload}>
<MdCloudUpload className='fill-base-content' />
</button>
)}
</div>
</div>
</div>
{window.innerWidth < 400 && (
<div className='flex flex-wrap items-center gap-x-4 gap-y-2 py-2'>
<button
className='btn rounded bg-red-600 text-white hover:bg-red-700'
onClick={handleDelete}
>
{_('Delete')}
</button>
{book.uploadedAt && (
<button className='btn rounded bg-orange-500 text-white hover:bg-orange-600'>
{_('Redownload')}
</button>
)}
</div>
)}
<div className='text-base-content my-4'>
<div className='mb-4 grid grid-cols-2 gap-4 sm:grid-cols-3'>
<div className='overflow-hidden'>
@@ -171,11 +181,12 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Updated:')}</span>
<p className='text-neutral-content text-sm'>
{formatDate(book.lastUpdated) || ''}
</p>
<p className='text-neutral-content text-sm'>{formatDate(book.updatedAt) || ''}</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Added:')}</span>
<p className='text-neutral-content text-sm'>{formatDate(book.createdAt) || ''}</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Language:')}</span>
<p className='text-neutral-content text-sm'>
@@ -194,6 +205,12 @@ const BookDetailModal = ({ book, isOpen, onClose }: BookDetailModalProps) => {
{formatSubject(bookMeta.subject) || _('Unknown')}
</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Format:')}</span>
<p className='text-neutral-content line-clamp-1 text-sm'>
{book.format || _('Unknown')}
</p>
</div>
</div>
</div>
</div>
+23
View File
@@ -0,0 +1,23 @@
import { isTauriAppPlatform } from '@/services/environment';
import { openUrl } from '@tauri-apps/plugin-opener';
interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
href: string;
}
const Link: React.FC<LinkProps> = ({ href, children, ...props }) => {
const handleClick = async (e: React.MouseEvent<HTMLAnchorElement>) => {
if (isTauriAppPlatform()) {
e.preventDefault();
await openUrl(href);
}
};
return (
<a href={href} target='_blank' rel='noopener noreferrer' onClick={handleClick} {...props}>
{children}
</a>
);
};
export default Link;
+10 -7
View File
@@ -17,10 +17,10 @@ export const Toast = () => {
error: 'toast-error toast-top toast-end',
};
const alertClassMap = {
info: 'alert-primary',
success: 'alert-success',
warning: 'alert-warning',
error: 'alert-error',
info: 'alert-primary border-base-300',
success: 'alert-success border-0',
warning: 'alert-warning border-0',
error: 'alert-error border-0',
};
useEffect(() => {
@@ -58,14 +58,17 @@ export const Toast = () => {
>
<div
className={clsx(
'alert flex max-w-80 items-center justify-center border-0',
'alert flex items-center justify-center',
alertClassMap[toastType.current],
)}
>
<span
className={clsx(
'max-h-[50vh] min-w-32 max-w-80',
'overflow-y-auto whitespace-normal break-words text-center',
'max-h-[50vh] min-w-32',
'overflow-y-auto text-center',
toastType.current === 'info'
? 'max-w-[80vw]'
: 'max-w-80 whitespace-normal break-words',
messageClass.current,
)}
>
@@ -9,7 +9,6 @@ import { relaunch } from '@tauri-apps/plugin-process';
import { fetch } from '@tauri-apps/plugin-http';
import { useTranslation } from '@/hooks/useTranslation';
import { useSearchParams } from 'next/navigation';
import { useTheme } from '@/hooks/useTheme';
import { tauriDownload } from '@/utils/transfer';
import { READEST_UPDATER_FILE, READEST_CHANGELOG_FILE } from '@/services/constants';
import packageJson from '../../package.json';
@@ -201,8 +200,6 @@ export const UpdaterContent = ({ version }: { version?: string }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [update]);
useTheme();
useEffect(() => {
setIsMounted(true);
}, []);
@@ -1,8 +1,9 @@
import { isWebAppPlatform, hasCli } from '@/services/environment';
import { getCurrent } from '@tauri-apps/plugin-deep-link';
declare global {
interface Window {
OPEN_WITH_FILES?: string[];
OPEN_WITH_FILES?: string[] | null;
}
}
@@ -32,12 +33,35 @@ const parseCLIOpenWithFiles = async () => {
return files;
};
const parseIntentOpenWithFiles = async () => {
const urls = await getCurrent();
if (urls && urls.length > 0) {
console.log('Intent Open with URL:', urls);
return urls
.map((url) => {
if (url.startsWith('file://')) {
return decodeURI(url.replace('file://', ''));
} else if (url.startsWith('content://')) {
return url;
} else {
console.info('Skip non-file URL:', url);
return null;
}
})
.filter((url) => url !== null) as string[];
}
return null;
};
export const parseOpenWithFiles = async () => {
if (isWebAppPlatform()) return [];
let files = parseWindowOpenWithFiles();
if (!files && hasCli()) {
if ((!files || files.length === 0) && hasCli()) {
files = await parseCLIOpenWithFiles();
}
if (!files || files.length === 0) {
files = await parseIntentOpenWithFiles();
}
return files;
};
@@ -62,7 +62,10 @@ export const usePullToRefresh = (ref: React.RefObject<HTMLDivElement>, onTrigger
return;
}
const headerbar = document.querySelector('.titlebar');
const pullIndicator = document.createElement('div');
const headerBottom = headerbar?.getBoundingClientRect().bottom || 0;
pullIndicator.style.top = `${headerBottom + 20}px`;
pullIndicator.className = 'pull-indicator text-gray-500';
pullIndicator.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
+23 -3
View File
@@ -1,11 +1,31 @@
import { useEffect } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useSettingsStore } from '@/store/settingsStore';
import { applyCustomTheme } from '@/styles/themes';
import { applyCustomTheme, Palette } from '@/styles/themes';
import { setSystemUIVisibility } from '@/utils/bridge';
export const useTheme = () => {
type UseThemeProps = {
systemUIVisible?: boolean;
appThemeColor?: keyof Palette;
};
export const useTheme = ({
systemUIVisible = true,
appThemeColor = 'base-100',
}: UseThemeProps = {}) => {
const { appService } = useEnv();
const { settings } = useSettingsStore();
const { themeColor, isDarkMode } = useThemeStore();
const { themeColor, isDarkMode, updateAppTheme } = useThemeStore();
useEffect(() => {
updateAppTheme(appThemeColor);
if (appService?.isMobile) {
setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDarkMode]);
useEffect(() => {
const customThemes = settings.globalReadSettings?.customThemes ?? [];
customThemes.forEach((customTheme) => {
+2 -1
View File
@@ -137,7 +137,8 @@ export abstract class BaseAppService implements AppService {
async importBook(
// file might be:
// 1. absolute path for local file
// 1.1 absolute path for local file on Desktop
// 1.2 /private/var inbox file path on iOS
// 2. remote url
// 3. content provider uri
// 4. File object from browsers
+6 -1
View File
@@ -150,7 +150,11 @@ export const SERIF_FONTS = [
'Times New Roman',
];
export const CJK_SERIF_FONTS = [_('LXGW WenKai GB Screen'), _('LXGW WenKai TC')];
export const CJK_SERIF_FONTS = [
_('LXGW WenKai GB Screen'),
_('LXGW WenKai TC'),
_('GuanKiapTsingKhai-T'),
];
export const CJK_SANS_SERIF_FONTS = ['Noto Sans SC', 'Noto Sans TC'];
@@ -437,6 +441,7 @@ export const CJK_FONTS_PATTENS = new RegExp(
'Hei',
'Yan',
'Min',
'Khai',
'Yuan',
'Song',
'Ming',
@@ -99,12 +99,12 @@ export const nativeFileSystem: FileSystem = {
}
} else {
const prefix = this.getPrefix(base);
if (prefix && OS_TYPE !== 'android') {
const absolutePath = path.startsWith('/') ? path : prefix ? await join(prefix, path) : null;
if (absolutePath && OS_TYPE !== 'android') {
// NOTE: RemoteFile currently performs about 2× faster than NativeFile
// due to an unresolved performance issue in Tauri (see tauri-apps/tauri#9190).
// Once the bug is resolved, we should switch back to using NativeFile.
// RemoteFile is not usable on Android due to unknown issues of range fetch with Android WebView.
const absolutePath = await join(prefix, path);
return await new RemoteFile(this.getURL(absolutePath), fname).open();
} else {
return await new NativeFile(fp, fname, base ? baseDir : null).open();
@@ -29,9 +29,12 @@ interface ReaderStore {
viewStates: { [key: string]: ViewState };
bookKeys: string[];
hoveredBookKey: string | null;
systemUIVisible: boolean;
setBookKeys: (keys: string[]) => void;
setHoveredBookKey: (key: string | null) => void;
setBookmarkRibbonVisibility: (key: string, visible: boolean) => void;
showSystemUI: () => void;
dismissSystemUI: () => void;
setProgress: (
key: string,
@@ -63,8 +66,11 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
viewStates: {},
bookKeys: [],
hoveredBookKey: null,
systemUIVisible: false,
setBookKeys: (keys: string[]) => set({ bookKeys: keys }),
setHoveredBookKey: (key: string | null) => set({ hoveredBookKey: key }),
showSystemUI: () => set({ systemUIVisible: true }),
dismissSystemUI: () => set({ systemUIVisible: false }),
getView: (key: string | null) => (key && get().viewStates[key]?.view) || null,
setView: (key: string, view) =>
-1
View File
@@ -220,7 +220,6 @@ foliate-view {
.pull-indicator {
position: fixed;
top: calc(64px + env(safe-area-inset-top));
width: 100%;
background-color: transparent;
display: flex;
+1 -1
View File
@@ -115,7 +115,7 @@ export const primaryLanguage = (lang: string | string[] | undefined) => {
return Array.isArray(lang) ? lang[0] : lang;
};
export const formatDate = (date: string | number | Date | undefined) => {
export const formatDate = (date: string | number | Date | null | undefined) => {
if (!date) return;
const userLang = getUserLang();
try {
+22
View File
@@ -23,6 +23,16 @@ export interface InstallPackageResponse {
error?: string;
}
export interface SetSystemUIVisibilityRequest {
visible: boolean;
darkMode: boolean;
}
export interface SetSystemUIVisibilityResponse {
success: boolean;
error?: string;
}
export async function copyURIToPath(request: CopyURIRequest): Promise<CopyURIResponse> {
const result = await invoke<CopyURIResponse>('plugin:native-bridge|copy_uri_to_path', {
payload: request,
@@ -45,3 +55,15 @@ export async function installPackage(
});
return result;
}
export async function setSystemUIVisibility(
request: SetSystemUIVisibilityRequest,
): Promise<SetSystemUIVisibilityResponse> {
const result = await invoke<SetSystemUIVisibilityResponse>(
'plugin:native-bridge|set_system_ui_visibility',
{
payload: request,
},
);
return result;
}
+5 -2
View File
@@ -110,6 +110,7 @@ const getFontStyles = (
const getAdditionalFontLinks = () => `
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/misans-webfont@1.0.4/misans-l3/misans-l3/result.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cn-fontsource-lxgw-wen-kai-gb-screen@1.0.6/font.min.css" crossorigin="anonymous">
<link rel='stylesheet' href='https://fontsapi.zeoseven.com/431/main/result.css' crossorigin="anonymous"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=LXGW+WenKai+TC&display=swap" crossorigin="anonymous">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC&family=Noto+Sans+TC&display=swap" crossorigin="anonymous">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Serif+JP&display=swap" crossorigin="anonymous">
@@ -419,6 +420,8 @@ export const mountAdditionalFonts = (document: Document) => {
};
export const transformStylesheet = (css: string) => {
const isMobile = ['ios', 'android'].includes(getOSPlatform());
const fontScale = isMobile ? 1.25 : 1;
// replace absolute font sizes with rem units
// replace hardcoded colors
return css
@@ -431,11 +434,11 @@ export const transformStylesheet = (css: string) => {
.replace(/font-size\s*:\s*xx-large/gi, 'font-size: 2rem')
.replace(/font-size\s*:\s*xxx-large/gi, 'font-size: 3rem')
.replace(/font-size\s*:\s*(\d+(?:\.\d+)?)px/gi, (_, px) => {
const rem = parseFloat(px) / 16;
const rem = parseFloat(px) / fontScale / 16;
return `font-size: ${rem}rem`;
})
.replace(/font-size\s*:\s*(\d+(?:\.\d+)?)pt/gi, (_, pt) => {
const rem = parseFloat(pt) / 12;
const rem = parseFloat(pt) / fontScale / 12;
return `font-size: ${rem}rem`;
})
.replace(/[\s;]color\s*:\s*#000000/gi, 'color: var(--theme-fg-color)')