forked from akai/readest
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b91d7b7a61 | |||
| 7c0d9bf375 | |||
| ea8ab91ed3 | |||
| efd415ece2 | |||
| ec86d68b80 | |||
| 172ab382bc | |||
| ad551a04a1 | |||
| 4dc943d23e | |||
| ee93885c04 | |||
| 14c032aaff | |||
| 2f619ca745 | |||
| 8979827cf5 | |||
| ae16eeffa7 | |||
| 8e96ef3e5d | |||
| 38e24da968 | |||
| 7371efe1d1 | |||
| 3cb37bca5d | |||
| b9fa204eda | |||
| cf66665096 | |||
| 759779a98d | |||
| 68409db8a0 | |||
| 46f0e8e7f6 | |||
| 6260168caa | |||
| a8430a7be1 | |||
| 7bfaa38746 |
@@ -32,7 +32,7 @@ yarn-error.log*
|
||||
/private_keys
|
||||
|
||||
# plists
|
||||
*.plist
|
||||
Entitlements*.plist
|
||||
|
||||
# local confs
|
||||
tauri.*.conf.json
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.35",
|
||||
"version": "0.9.37",
|
||||
"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,12 @@
|
||||
"Format": "التنسيق",
|
||||
"Ascending": "تصاعدي",
|
||||
"Descending": "تنازلي",
|
||||
"Sort by...": "ترتيب حسب..."
|
||||
"Sort by...": "ترتيب حسب...",
|
||||
"Added:": "تاريخ الإضافة:",
|
||||
"Format:": "التنسيق:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "الوصف:",
|
||||
"No description available": "لا يوجد وصف متاح",
|
||||
"List": "قائمة",
|
||||
"Grid": "شبكة"
|
||||
}
|
||||
|
||||
@@ -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,12 @@
|
||||
"Format": "Format",
|
||||
"Ascending": "Aufsteigend",
|
||||
"Descending": "Absteigend",
|
||||
"Sort by...": "Sortieren nach..."
|
||||
"Sort by...": "Sortieren nach...",
|
||||
"Added:": "Hinzugefügt:",
|
||||
"Format:": "Format:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Beschreibung:",
|
||||
"No description available": "Keine Beschreibung verfügbar",
|
||||
"List": "Liste",
|
||||
"Grid": "Raster"
|
||||
}
|
||||
|
||||
@@ -282,7 +282,6 @@
|
||||
"Download finished": "Η λήψη ολοκληρώθηκε",
|
||||
"DOWNLOAD & INSTALL": "ΛΗΨΗ & ΕΓΚΑΤΑΣΤΑΣΗ",
|
||||
"Changelog:": "Αλλαγές:",
|
||||
"Redownload": "Επαναφόρτωση",
|
||||
"Software Update": "Ενημέρωση λογισμικού",
|
||||
"Title": "Τίτλος",
|
||||
"Date Read": "Ημερομηνία ανάγνωσης",
|
||||
@@ -290,5 +289,12 @@
|
||||
"Format": "Μορφή",
|
||||
"Ascending": "Αύξουσα",
|
||||
"Descending": "Φθίνουσα",
|
||||
"Sort by...": "Ταξινόμηση κατά..."
|
||||
"Sort by...": "Ταξινόμηση κατά...",
|
||||
"Added:": "Προστέθηκε:",
|
||||
"Format:": "Μορφή:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Περιγραφή:",
|
||||
"No description available": "Δεν είναι διαθέσιμη καμία περιγραφή",
|
||||
"List": "Λίστα",
|
||||
"Grid": "Πλέγμα"
|
||||
}
|
||||
|
||||
@@ -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,12 @@
|
||||
"Format": "Formato",
|
||||
"Ascending": "Ascendente",
|
||||
"Descending": "Descendente",
|
||||
"Sort by...": "Ordenar por..."
|
||||
"Sort by...": "Ordenar por...",
|
||||
"Added:": "Agregado:",
|
||||
"Format:": "Formato:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Descripción:",
|
||||
"No description available": "No hay descripción disponible",
|
||||
"List": "Lista",
|
||||
"Grid": "Cuadrícula"
|
||||
}
|
||||
|
||||
@@ -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,12 @@
|
||||
"Format": "Format",
|
||||
"Ascending": "Ascendant",
|
||||
"Descending": "Descendant",
|
||||
"Sort by...": "Trier par..."
|
||||
"Sort by...": "Trier par...",
|
||||
"Added:": "Ajouté :",
|
||||
"Format:": "Format :",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Description :",
|
||||
"No description available": "Aucune description disponible",
|
||||
"List": "Liste",
|
||||
"Grid": "Grille"
|
||||
}
|
||||
|
||||
@@ -282,7 +282,6 @@
|
||||
"Download finished": "डाउनलोड पूरा हुआ",
|
||||
"DOWNLOAD & INSTALL": "डाउनलोड और इंस्टॉल करें",
|
||||
"Changelog:": "चेंजलॉग:",
|
||||
"Redownload": "री-डाउनलोड करें",
|
||||
"Software Update": "सॉफ़्टवेयर अपडेट",
|
||||
"Title": "शीर्षक",
|
||||
"Date Read": "पढ़ने की तारीख",
|
||||
@@ -290,5 +289,12 @@
|
||||
"Format": "फॉर्मेट",
|
||||
"Ascending": "आरोही",
|
||||
"Descending": "अवरोही",
|
||||
"Sort by...": "द्वारा क्रमबद्ध करें..."
|
||||
"Sort by...": "द्वारा क्रमबद्ध करें...",
|
||||
"Added:": "जोड़ा गया:",
|
||||
"Format:": "फॉर्मेट:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "विवरण:",
|
||||
"No description available": "कोई विवरण उपलब्ध नहीं है",
|
||||
"List": "सूची",
|
||||
"Grid": "ग्रिड"
|
||||
}
|
||||
|
||||
@@ -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,12 @@
|
||||
"Format": "Format",
|
||||
"Ascending": "Naik",
|
||||
"Descending": "Turun",
|
||||
"Sort by...": "Urutkan berdasarkan..."
|
||||
"Sort by...": "Urutkan berdasarkan...",
|
||||
"Added:": "Ditambahkan:",
|
||||
"Format:": "Format:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Deskripsi:",
|
||||
"No description available": "Tidak ada deskripsi yang tersedia",
|
||||
"List": "Daftar",
|
||||
"Grid": "Jala"
|
||||
}
|
||||
|
||||
@@ -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,12 @@
|
||||
"Format": "Formato",
|
||||
"Ascending": "Crescente",
|
||||
"Descending": "Decrescente",
|
||||
"Sort by...": "Ordina per..."
|
||||
"Sort by...": "Ordina per...",
|
||||
"Added:": "Aggiunto:",
|
||||
"Format:": "Formato:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Descrizione:",
|
||||
"No description available": "Nessuna descrizione disponibile",
|
||||
"List": "Elenco",
|
||||
"Grid": "Griglia"
|
||||
}
|
||||
|
||||
@@ -282,7 +282,6 @@
|
||||
"Download finished": "ダウンロード完了",
|
||||
"DOWNLOAD & INSTALL": "更新",
|
||||
"Changelog:": "変更ログ:",
|
||||
"Redownload": "再ダウンロード",
|
||||
"Software Update": "ソフトウェアの更新",
|
||||
"Title": "タイトル",
|
||||
"Date Read": "読書日",
|
||||
@@ -290,5 +289,12 @@
|
||||
"Format": "形式",
|
||||
"Ascending": "昇順",
|
||||
"Descending": "降順",
|
||||
"Sort by...": "並べ替え..."
|
||||
"Sort by...": "並べ替え...",
|
||||
"Added:": "追加日:",
|
||||
"Format:": "形式:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "説明:",
|
||||
"No description available": "説明はありません",
|
||||
"List": "リスト",
|
||||
"Grid": "グリッド"
|
||||
}
|
||||
|
||||
@@ -282,7 +282,6 @@
|
||||
"Download finished": "다운로드 완료",
|
||||
"DOWNLOAD & INSTALL": "다운로드 및 설치",
|
||||
"Changelog:": "변경 로그:",
|
||||
"Redownload": "다시 다운로드",
|
||||
"Software Update": "소프트웨어 업데이트",
|
||||
"Title": "제목",
|
||||
"Date Read": "읽은 날짜",
|
||||
@@ -290,5 +289,12 @@
|
||||
"Format": "형식",
|
||||
"Ascending": "오름차순",
|
||||
"Descending": "내림차순",
|
||||
"Sort by...": "정렬 기준..."
|
||||
"Sort by...": "정렬 기준...",
|
||||
"Added:": "추가됨:",
|
||||
"Format:": "형식:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "설명:",
|
||||
"No description available": "설명 없음",
|
||||
"List": "목록",
|
||||
"Grid": "격자"
|
||||
}
|
||||
|
||||
@@ -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,12 @@
|
||||
"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",
|
||||
"Description:": "Opis:",
|
||||
"No description available": "Brak opisu",
|
||||
"List": "Lista",
|
||||
"Grid": "Siatka"
|
||||
}
|
||||
|
||||
@@ -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,12 @@
|
||||
"Format": "Formato",
|
||||
"Ascending": "Crescente",
|
||||
"Descending": "Decrescente",
|
||||
"Sort by...": "Ordenar por..."
|
||||
"Sort by...": "Ordenar por...",
|
||||
"Added:": "Adicionado:",
|
||||
"Format:": "Formato:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Descrição:",
|
||||
"No description available": "Nenhuma descrição disponível",
|
||||
"List": "Lista",
|
||||
"Grid": "Grade"
|
||||
}
|
||||
|
||||
@@ -282,7 +282,6 @@
|
||||
"Download finished": "Загрузка завершена",
|
||||
"DOWNLOAD & INSTALL": "СКАЧАТЬ И УСТАНОВИТЬ",
|
||||
"Changelog:": "Изменения:",
|
||||
"Redownload": "Переустановить",
|
||||
"Software Update": "Обновление программного обеспечения",
|
||||
"Title": "Название",
|
||||
"Date Read": "Дата прочтения",
|
||||
@@ -290,5 +289,12 @@
|
||||
"Format": "Формат",
|
||||
"Ascending": "По возрастанию",
|
||||
"Descending": "По убыванию",
|
||||
"Sort by...": "Сортировать по..."
|
||||
"Sort by...": "Сортировать по...",
|
||||
"Added:": "Добавлено:",
|
||||
"Format:": "Формат:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Описание:",
|
||||
"No description available": "Нет доступного описания",
|
||||
"List": "Список",
|
||||
"Grid": "Сетка"
|
||||
}
|
||||
|
||||
@@ -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,12 @@
|
||||
"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",
|
||||
"Description:": "Tanım:",
|
||||
"No description available": "Tanım mevcut değil",
|
||||
"List": "Liste",
|
||||
"Grid": "Izgara"
|
||||
}
|
||||
|
||||
@@ -282,7 +282,6 @@
|
||||
"Download finished": "Завантаження завершено",
|
||||
"DOWNLOAD & INSTALL": "ЗАВАНТАЖИТИ І ВСТАНОВИТИ",
|
||||
"Changelog:": "Журнал змін:",
|
||||
"Redownload": "Завантажити знову",
|
||||
"Software Update": "Оновлення програмного забезпечення",
|
||||
"Title": "Назва",
|
||||
"Date Read": "Дата прочитання",
|
||||
@@ -290,5 +289,12 @@
|
||||
"Format": "Формат",
|
||||
"Ascending": "За зростанням",
|
||||
"Descending": "За спаданням",
|
||||
"Sort by...": "Сортувати за..."
|
||||
"Sort by...": "Сортувати за...",
|
||||
"Added:": "Додано:",
|
||||
"Format:": "Формат:",
|
||||
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
|
||||
"Description:": "Опис:",
|
||||
"No description available": "Опис недоступний",
|
||||
"List": "Список",
|
||||
"Grid": "Сітка"
|
||||
}
|
||||
|
||||
@@ -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,12 @@
|
||||
"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",
|
||||
"Description:": "Miêu tả:",
|
||||
"No description available": "Không có mô tả nào",
|
||||
"List": "Danh sách",
|
||||
"Grid": "Lưới"
|
||||
}
|
||||
|
||||
@@ -282,7 +282,6 @@
|
||||
"Download finished": "下载完成",
|
||||
"DOWNLOAD & INSTALL": "下载并安装",
|
||||
"Changelog:": "更新日志:",
|
||||
"Redownload": "重新下载",
|
||||
"Software Update": "软件更新",
|
||||
"Title": "书名",
|
||||
"Date Read": "阅读日期",
|
||||
@@ -290,5 +289,12 @@
|
||||
"Format": "格式",
|
||||
"Ascending": "升序",
|
||||
"Descending": "降序",
|
||||
"Sort by...": "排序方式..."
|
||||
"Sort by...": "排序方式...",
|
||||
"Added:": "添加日期",
|
||||
"Format:": "格式",
|
||||
"GuanKiapTsingKhai-T": "原俠正楷-T",
|
||||
"Description:": "简介",
|
||||
"No description available": "暂无简介",
|
||||
"List": "列表",
|
||||
"Grid": "网格"
|
||||
}
|
||||
|
||||
@@ -282,7 +282,6 @@
|
||||
"Download finished": "下載完成",
|
||||
"DOWNLOAD & INSTALL": "下載並安裝",
|
||||
"Changelog:": "更新日誌:",
|
||||
"Redownload": "重新下載",
|
||||
"Software Update": "軟體更新",
|
||||
"Title": "書名",
|
||||
"Date Read": "閱讀日期",
|
||||
@@ -290,5 +289,12 @@
|
||||
"Format": "格式",
|
||||
"Ascending": "升序",
|
||||
"Descending": "降序",
|
||||
"Sort by...": "排序方式..."
|
||||
"Sort by...": "排序方式...",
|
||||
"Added:": "添加日期",
|
||||
"Format:": "格式",
|
||||
"GuanKiapTsingKhai-T": "原俠正楷-T",
|
||||
"Description:": "簡介",
|
||||
"No description available": "暫無簡介",
|
||||
"List": "列表",
|
||||
"Grid": "網格"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
{
|
||||
"releases": {
|
||||
"0.9.37": {
|
||||
"date": "2025-04-25",
|
||||
"notes": [
|
||||
"Full-Screen Toggle: You can now press F11 to quickly toggle full-screen mode",
|
||||
"List View: A new list layout is available for your bookshelf",
|
||||
"Book Details: Book descriptions from metadata are now shown in the details view",
|
||||
"Enhanced Android behavior: better navigation bar handling and status bar dismissal when resuming",
|
||||
"TTS now defaults to the book’s metadata language when needed",
|
||||
"Synced custom themes reliably across sessions"
|
||||
]
|
||||
},
|
||||
"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": [
|
||||
|
||||
@@ -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" />
|
||||
|
||||
-40
@@ -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>
|
||||
|
||||
+135
@@ -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,92 @@ 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) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
} else {
|
||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_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)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun get_status_bar_height(invoke: Invoke) {
|
||||
val ret = JSObject()
|
||||
try {
|
||||
val resourceId = activity.resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
val height = if (resourceId > 0) {
|
||||
activity.resources.getDimensionPixelSize(resourceId)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
ret.put("height", height)
|
||||
} catch (e: Exception) {
|
||||
ret.put("height", -1)
|
||||
ret.put("error", e.message)
|
||||
}
|
||||
invoke.resolve(ret)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ const COMMANDS: &[&str] = &[
|
||||
"copy_uri_to_path",
|
||||
"use_background_audio",
|
||||
"install_package",
|
||||
"set_system_ui_visibility",
|
||||
"get_status_bar_height",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
|
||||
+30
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-get-status-bar-height"
|
||||
description = "Enables the get_status_bar_height command without any pre-configured scope."
|
||||
commands.allow = ["get_status_bar_height"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-get-status-bar-height"
|
||||
description = "Denies the get_status_bar_height command without any pre-configured scope."
|
||||
commands.deny = ["get_status_bar_height"]
|
||||
+13
@@ -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"]
|
||||
+54
@@ -9,6 +9,8 @@ Default permissions for the plugin
|
||||
- `allow-copy-uri-to-path`
|
||||
- `allow-use-background-audio`
|
||||
- `allow-install-package`
|
||||
- `allow-set-system-ui-visibility`
|
||||
- `allow-get-status-bar-height`
|
||||
|
||||
## Permission Table
|
||||
|
||||
@@ -100,6 +102,32 @@ Denies the copy_uri_to_path command without any pre-configured scope.
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-get-status-bar-height`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the get_status_bar_height command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:deny-get-status-bar-height`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the get_status_bar_height command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`native-bridge:allow-install-package`
|
||||
|
||||
</td>
|
||||
@@ -126,6 +154,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,6 @@ permissions = [
|
||||
"allow-copy-uri-to-path",
|
||||
"allow-use-background-audio",
|
||||
"allow-install-package",
|
||||
"allow-set-system-ui-visibility",
|
||||
"allow-get-status-bar-height",
|
||||
]
|
||||
|
||||
+26
-2
@@ -330,6 +330,18 @@
|
||||
"const": "deny-copy-uri-to-path",
|
||||
"markdownDescription": "Denies the copy_uri_to_path command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the get_status_bar_height command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-get-status-bar-height",
|
||||
"markdownDescription": "Enables the get_status_bar_height command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the get_status_bar_height command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-get-status-bar-height",
|
||||
"markdownDescription": "Denies the get_status_bar_height command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the install_package command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
@@ -342,6 +354,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 +379,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`\n- `allow-get-status-bar-height`",
|
||||
"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`\n- `allow-get-status-bar-height`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -43,3 +43,18 @@ 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)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn get_status_bar_height<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
) -> Result<GetStatusBarHeightResponse> {
|
||||
app.native_bridge().get_status_bar_height()
|
||||
}
|
||||
|
||||
@@ -36,4 +36,15 @@ 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)
|
||||
}
|
||||
|
||||
pub fn get_status_bar_height(&self) -> crate::Result<GetStatusBarHeightResponse> {
|
||||
Err(crate::Error::UnsupportedPlatformError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
commands::copy_uri_to_path,
|
||||
commands::use_background_audio,
|
||||
commands::install_package,
|
||||
commands::set_system_ui_visibility,
|
||||
commands::get_status_bar_height,
|
||||
])
|
||||
.setup(|app, api| {
|
||||
#[cfg(mobile)]
|
||||
|
||||
@@ -66,3 +66,22 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> NativeBridge<R> {
|
||||
pub fn get_status_bar_height(&self) -> crate::Result<GetStatusBarHeightResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("get_status_bar_height", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,3 +44,24 @@ 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>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetStatusBarHeightResponse {
|
||||
pub height: u32,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
@@ -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<_>>()
|
||||
@@ -217,12 +220,30 @@ pub fn run() {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
app.deep_link().register_all()?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
fn has_xdg_mime() -> bool {
|
||||
std::process::Command::new("which")
|
||||
.arg("xdg-mime")
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
if has_xdg_mime() {
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
app.deep_link().register_all()?;
|
||||
} else {
|
||||
println!("xdg-mime not found; skipping deep link setup on Linux.");
|
||||
}
|
||||
}
|
||||
|
||||
app.handle().plugin(
|
||||
tauri_plugin_log::Builder::default()
|
||||
.level(log::LevelFilter::Info)
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTheme } from '@/hooks/useTheme';
|
||||
|
||||
export default function AuthErrorPage() {
|
||||
const router = useRouter();
|
||||
useTheme();
|
||||
useTheme({ systemUIVisible: false });
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
import { Book } from '@/types/book';
|
||||
import { LibraryViewModeType } from '@/types/settings';
|
||||
import { formatAuthors, formatTitle } from '@/utils/book';
|
||||
|
||||
interface BookCoverProps {
|
||||
book: Book;
|
||||
isPreview?: boolean;
|
||||
mode?: LibraryViewModeType;
|
||||
}
|
||||
|
||||
const BookCover: React.FC<BookCoverProps> = ({ book, isPreview }) => {
|
||||
const BookCover: React.FC<BookCoverProps> = ({ mode = 'grid', book, isPreview }) => {
|
||||
return (
|
||||
<div className='relative flex h-full w-full'>
|
||||
<Image
|
||||
@@ -31,8 +33,8 @@ const BookCover: React.FC<BookCoverProps> = ({ book, isPreview }) => {
|
||||
<div className='flex h-1/2 items-center justify-center'>
|
||||
<span
|
||||
className={clsx(
|
||||
isPreview ? 'line-clamp-2' : 'line-clamp-3',
|
||||
isPreview ? 'text-[0.5em]' : 'text-lg',
|
||||
isPreview ? 'line-clamp-2' : mode === 'grid' ? 'line-clamp-3' : 'line-clamp-2',
|
||||
isPreview ? 'text-[0.5em]' : mode === 'grid' ? 'text-lg' : 'text-sm',
|
||||
)}
|
||||
>
|
||||
{formatTitle(book.title)}
|
||||
@@ -43,7 +45,7 @@ const BookCover: React.FC<BookCoverProps> = ({ book, isPreview }) => {
|
||||
<span
|
||||
className={clsx(
|
||||
'text-neutral-content/50 line-clamp-1',
|
||||
isPreview ? 'text-[0.4em]' : 'text-base',
|
||||
isPreview ? 'text-[0.4em]' : mode === 'grid' ? 'text-base' : 'text-xs',
|
||||
)}
|
||||
>
|
||||
{formatAuthors(book.author)}
|
||||
|
||||
@@ -6,10 +6,13 @@ import { LiaCloudUploadAltSolid, LiaCloudDownloadAltSolid } from 'react-icons/li
|
||||
import { Book } from '@/types/book';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { LibraryViewModeType } from '@/types/settings';
|
||||
import { formatAuthors } from '@/utils/book';
|
||||
import ReadingProgress from './ReadingProgress';
|
||||
import BookCover from './BookCover';
|
||||
|
||||
interface BookItemProps {
|
||||
mode: LibraryViewModeType;
|
||||
book: Book;
|
||||
isSelectMode: boolean;
|
||||
selectedBooks: string[];
|
||||
@@ -20,6 +23,7 @@ interface BookItemProps {
|
||||
}
|
||||
|
||||
const BookItem: React.FC<BookItemProps> = ({
|
||||
mode,
|
||||
book,
|
||||
isSelectMode,
|
||||
selectedBooks,
|
||||
@@ -39,12 +43,19 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'book-item flex h-full flex-col',
|
||||
'book-item flex',
|
||||
mode === 'grid' && 'h-full flex-col',
|
||||
mode === 'list' && 'h-28 flex-row gap-4 overflow-hidden',
|
||||
appService?.hasContextMenu ? 'cursor-pointer' : '',
|
||||
)}
|
||||
>
|
||||
<div className='bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden shadow-md'>
|
||||
<BookCover book={book} />
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden shadow-md',
|
||||
mode === 'list' && 'min-w-20',
|
||||
)}
|
||||
>
|
||||
<BookCover mode={mode} book={book} />
|
||||
{selectedBooks.includes(book.hash) && (
|
||||
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
|
||||
)}
|
||||
@@ -58,11 +69,28 @@ const BookItem: React.FC<BookItemProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={clsx('flex w-full flex-col p-0 pt-2')}>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-[0.6em] text-xs font-semibold'>
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-full flex-col p-0',
|
||||
mode === 'grid' && 'pt-2',
|
||||
mode === 'list' && 'py-2',
|
||||
)}
|
||||
>
|
||||
<div className={clsx('min-w-0 flex-1', mode === 'list' && 'flex flex-col gap-2')}>
|
||||
<h4
|
||||
className={clsx(
|
||||
'overflow-hidden text-ellipsis font-semibold',
|
||||
mode === 'grid' && 'block whitespace-nowrap text-[0.6em] text-xs',
|
||||
mode === 'list' && 'line-clamp-2 text-base',
|
||||
)}
|
||||
>
|
||||
{book.title}
|
||||
</h4>
|
||||
{mode === 'list' && (
|
||||
<p className='text-neutral-content line-clamp-1 text-sm'>
|
||||
{formatAuthors(book.author, book.primaryLanguage) || ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx('flex items-center', book.progress ? 'justify-between' : 'justify-end')}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { MdDelete, MdOpenInNew, MdOutlineCancel } from 'react-icons/md';
|
||||
import { LuFolderPlus } from 'react-icons/lu';
|
||||
import { PiPlus } from 'react-icons/pi';
|
||||
import { Book, BooksGroup } from '@/types/book';
|
||||
import { LibraryViewModeType } from '@/types/settings';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useLibraryStore } from '@/store/libraryStore';
|
||||
@@ -16,7 +17,7 @@ import { isMd5 } from '@/utils/md5';
|
||||
|
||||
import Alert from '@/components/Alert';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import BookshelfItem, { generateBookshelfItems } from './BookshelfItem';
|
||||
import BookshelfItem, { generateGridItems, generateListItems } from './BookshelfItem';
|
||||
import GroupingModal from './GroupingModal';
|
||||
|
||||
interface BookshelfProps {
|
||||
@@ -55,12 +56,16 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
const [queryTerm, setQueryTerm] = useState<string | null>(null);
|
||||
const [navBooksGroup, setNavBooksGroup] = useState<BooksGroup | null>(null);
|
||||
const [importBookUrl] = useState(searchParams?.get('url') || '');
|
||||
const [sortBy, setSortBy] = useState(searchParams?.get('sort') || 'updated');
|
||||
const [sortOrder, setSortOrder] = useState(searchParams?.get('order') || 'desc');
|
||||
const [viewMode, setViewMode] = useState(searchParams?.get('view') || settings.libraryViewMode);
|
||||
const [sortBy, setSortBy] = useState(searchParams?.get('sort') || settings.librarySortBy);
|
||||
const [sortOrder, setSortOrder] = useState(
|
||||
searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc'),
|
||||
);
|
||||
const isImportingBook = useRef(false);
|
||||
|
||||
const { setLibrary } = useLibraryStore();
|
||||
const allBookshelfItems = generateBookshelfItems(libraryBooks);
|
||||
const allBookshelfItems =
|
||||
viewMode === 'grid' ? generateGridItems(libraryBooks) : generateListItems(libraryBooks);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelectMode) {
|
||||
@@ -93,6 +98,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
useEffect(() => {
|
||||
const group = searchParams?.get('group') || '';
|
||||
const query = searchParams?.get('q') || '';
|
||||
const view = searchParams?.get('view') || settings.libraryViewMode;
|
||||
const sort = searchParams?.get('sort') || settings.librarySortBy;
|
||||
const order = searchParams?.get('order') || (settings.librarySortAscending ? 'asc' : 'desc');
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
@@ -115,9 +121,16 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
} else {
|
||||
params.delete('order');
|
||||
}
|
||||
if (sort === 'updated' && order === 'desc') {
|
||||
if (view) {
|
||||
params.set('view', view);
|
||||
setViewMode(view);
|
||||
} else {
|
||||
params.delete('view');
|
||||
}
|
||||
if (sort === 'updated' && order === 'desc' && view === 'grid') {
|
||||
params.delete('sort');
|
||||
params.delete('order');
|
||||
params.delete('view');
|
||||
}
|
||||
if (group) {
|
||||
const booksGroup = allBookshelfItems.find(
|
||||
@@ -229,13 +242,16 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
<div className='bookshelf'>
|
||||
<div
|
||||
className={clsx(
|
||||
'transform-wrapper grid flex-1 gap-x-4 sm:gap-x-0',
|
||||
'grid-cols-3 sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12',
|
||||
'transform-wrapper',
|
||||
viewMode === 'grid' && 'grid flex-1 grid-cols-3 gap-x-4 px-4 sm:gap-x-0 sm:px-2',
|
||||
viewMode === 'grid' && 'sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12',
|
||||
viewMode === 'list' && 'flex flex-col',
|
||||
)}
|
||||
>
|
||||
{filteredBookshelfItems.map((item, index) => (
|
||||
<BookshelfItem
|
||||
key={`library-item-${index}`}
|
||||
mode={viewMode as LibraryViewModeType}
|
||||
item={item}
|
||||
isSelectMode={isSelectMode}
|
||||
selectedBooks={selectedBooks}
|
||||
@@ -251,7 +267,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{!navBooksGroup && allBookshelfItems.length > 0 && (
|
||||
{viewMode === 'grid' && !navBooksGroup && allBookshelfItems.length > 0 && (
|
||||
<div
|
||||
className={clsx(
|
||||
'border-1 bg-base-100 hover:bg-base-300/50 flex items-center justify-center',
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Menu, MenuItem } from '@tauri-apps/api/menu';
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener';
|
||||
import { getOSPlatform } from '@/utils/misc';
|
||||
import { getLocalBookFilename } from '@/utils/book';
|
||||
import { LibraryViewModeType } from '@/types/settings';
|
||||
import { BOOK_UNGROUPED_ID, BOOK_UNGROUPED_NAME } from '@/services/constants';
|
||||
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
|
||||
import { Book, BookGroupType, BooksGroup } from '@/types/book';
|
||||
@@ -18,7 +19,7 @@ import GroupItem from './GroupItem';
|
||||
|
||||
export type BookshelfItem = Book | BooksGroup;
|
||||
|
||||
export const generateBookshelfItems = (books: Book[]): (Book | BooksGroup)[] => {
|
||||
export const generateGridItems = (books: Book[]): (Book | BooksGroup)[] => {
|
||||
const groups: BooksGroup[] = books.reduce((acc: BooksGroup[], book: Book) => {
|
||||
if (book.deletedAt) return acc;
|
||||
book.groupId = book.groupId || BOOK_UNGROUPED_ID;
|
||||
@@ -47,6 +48,10 @@ export const generateBookshelfItems = (books: Book[]): (Book | BooksGroup)[] =>
|
||||
return [...ungroupedBooks, ...groupedBooks].sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
};
|
||||
|
||||
export const generateListItems = (books: Book[]): (Book | BooksGroup)[] => {
|
||||
return books.filter((book) => !book.deletedAt).sort((a, b) => b.updatedAt - a.updatedAt);
|
||||
};
|
||||
|
||||
export const generateGroupsList = (items: Book[]): BookGroupType[] => {
|
||||
return items
|
||||
.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
@@ -66,6 +71,7 @@ export const generateGroupsList = (items: Book[]): BookGroupType[] => {
|
||||
};
|
||||
|
||||
interface BookshelfItemProps {
|
||||
mode: LibraryViewModeType;
|
||||
item: BookshelfItem;
|
||||
isSelectMode: boolean;
|
||||
selectedBooks: string[];
|
||||
@@ -80,6 +86,7 @@ interface BookshelfItemProps {
|
||||
}
|
||||
|
||||
const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
mode,
|
||||
item,
|
||||
isSelectMode,
|
||||
selectedBooks,
|
||||
@@ -248,30 +255,35 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'sm:hover:bg-base-300/50 group flex h-full flex-col px-0 py-4 sm:px-4',
|
||||
pressing ? 'scale-95' : 'scale-100',
|
||||
)}
|
||||
style={{
|
||||
transition: 'transform 0.2s',
|
||||
}}
|
||||
{...handlers}
|
||||
>
|
||||
<div className='flex-grow'>
|
||||
{'format' in item ? (
|
||||
<BookItem
|
||||
book={item}
|
||||
isSelectMode={isSelectMode}
|
||||
selectedBooks={selectedBooks}
|
||||
transferProgress={transferProgress}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
showBookDetailsModal={showBookDetailsModal}
|
||||
/>
|
||||
) : (
|
||||
<GroupItem group={item} isSelectMode={isSelectMode} selectedBooks={selectedBooks} />
|
||||
<div className={clsx(mode === 'list' && 'sm:hover:bg-base-300/50 px-4 sm:px-6')}>
|
||||
<div
|
||||
className={clsx(
|
||||
'group',
|
||||
mode === 'grid' && 'sm:hover:bg-base-300/50 flex h-full flex-col px-0 py-4 sm:px-4',
|
||||
mode === 'list' && 'border-base-300 flex flex-col border-b py-2',
|
||||
pressing ? (mode === 'grid' ? 'scale-95' : 'scale-98') : 'scale-100',
|
||||
)}
|
||||
style={{
|
||||
transition: 'transform 0.2s',
|
||||
}}
|
||||
{...handlers}
|
||||
>
|
||||
<div className='flex-grow'>
|
||||
{'format' in item ? (
|
||||
<BookItem
|
||||
mode={mode}
|
||||
book={item}
|
||||
isSelectMode={isSelectMode}
|
||||
selectedBooks={selectedBooks}
|
||||
transferProgress={transferProgress}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
showBookDetailsModal={showBookDetailsModal}
|
||||
/>
|
||||
) : (
|
||||
<GroupItem group={item} isSelectMode={isSelectMode} selectedBooks={selectedBooks} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { MdOutlineMenu, MdArrowBackIosNew } from 'react-icons/md';
|
||||
import { IoMdCloseCircle } from 'react-icons/io';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
import { useTrafficLightStore } from '@/store/trafficLightStore';
|
||||
@@ -36,6 +37,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { appService } = useEnv();
|
||||
const { statusBarHeight } = useThemeStore();
|
||||
const {
|
||||
isTrafficLightVisible,
|
||||
initializeTrafficLightStore,
|
||||
@@ -92,10 +94,14 @@ 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), ${statusBarHeight}px)`
|
||||
: '',
|
||||
}}
|
||||
>
|
||||
<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 +181,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
|
||||
|
||||
@@ -132,7 +132,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-2 w-72 shadow-2xl'
|
||||
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'
|
||||
>
|
||||
{user ? (
|
||||
<MenuItem
|
||||
@@ -142,7 +142,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
: _('Logged in')
|
||||
}
|
||||
labelClass='!max-w-40'
|
||||
icon={
|
||||
Icon={
|
||||
avatarUrl ? (
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
@@ -153,34 +153,34 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
height={20}
|
||||
/>
|
||||
) : (
|
||||
<PiUserCircleCheck />
|
||||
PiUserCircleCheck
|
||||
)
|
||||
}
|
||||
>
|
||||
<ul>
|
||||
<Quota quotas={quotas} className='h-10 pl-4 pr-2' />
|
||||
<Quota quotas={quotas} className='h-10 pl-3 pr-2' />
|
||||
<MenuItem label={_('Account')} noIcon onClick={handleUserProfile} />
|
||||
</ul>
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem label={_('Sign In')} icon={<PiUserCircle />} onClick={handleUserLogin}></MenuItem>
|
||||
<MenuItem label={_('Sign In')} Icon={PiUserCircle} onClick={handleUserLogin}></MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
label={_('Auto Upload Books to Cloud')}
|
||||
icon={isAutoUpload ? <MdCheck className='text-base-content' /> : undefined}
|
||||
Icon={isAutoUpload ? MdCheck : undefined}
|
||||
onClick={toggleAutoUploadBooks}
|
||||
/>
|
||||
{isTauriAppPlatform() && !appService?.isMobile && (
|
||||
<MenuItem
|
||||
label={_('Auto Import on File Open')}
|
||||
icon={isAutoImportBooksOnOpen ? <MdCheck className='text-base-content' /> : undefined}
|
||||
Icon={isAutoImportBooksOnOpen ? MdCheck : undefined}
|
||||
onClick={toggleAutoImportBooksOnOpen}
|
||||
/>
|
||||
)}
|
||||
{appService?.hasUpdater && (
|
||||
<MenuItem
|
||||
label={_('Check Updates on Start')}
|
||||
icon={isAutoCheckUpdates ? <MdCheck className='text-base-content' /> : undefined}
|
||||
Icon={isAutoCheckUpdates ? MdCheck : undefined}
|
||||
onClick={toggleAutoCheckUpdates}
|
||||
/>
|
||||
)}
|
||||
@@ -189,13 +189,13 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
{appService?.hasWindow && (
|
||||
<MenuItem
|
||||
label={_('Always on Top')}
|
||||
icon={isAlwaysOnTop ? <MdCheck className='text-base-content' /> : undefined}
|
||||
Icon={isAlwaysOnTop ? MdCheck : undefined}
|
||||
onClick={toggleAlwaysOnTop}
|
||||
/>
|
||||
)}
|
||||
<MenuItem
|
||||
label={_('Keep Screen Awake')}
|
||||
icon={isScreenWakeLock ? <MdCheck className='text-base-content' /> : undefined}
|
||||
Icon={isScreenWakeLock ? MdCheck : undefined}
|
||||
onClick={toggleScreenWakeLock}
|
||||
/>
|
||||
<MenuItem label={_('Reload Page')} onClick={handleReloadPage} />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { MdCheck } from 'react-icons/md';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { LibrarySortByType } from '@/types/settings';
|
||||
import { LibrarySortByType, LibraryViewModeType } from '@/types/settings';
|
||||
import { navigateToLibrary } from '@/utils/nav';
|
||||
import MenuItem from '@/components/MenuItem';
|
||||
|
||||
@@ -19,9 +19,15 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
const { envConfig } = useEnv();
|
||||
const { settings, setSettings, saveSettings } = useSettingsStore();
|
||||
|
||||
const viewMode = settings.libraryViewMode;
|
||||
const sortBy = settings.librarySortBy;
|
||||
const isAscending = settings.librarySortAscending;
|
||||
|
||||
const viewOptions = [
|
||||
{ label: _('List'), value: 'list' },
|
||||
{ label: _('Grid'), value: 'grid' },
|
||||
];
|
||||
|
||||
const sortByOptions = [
|
||||
{ label: _('Title'), value: 'title' },
|
||||
{ label: _('Author'), value: 'author' },
|
||||
@@ -35,6 +41,17 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
{ label: _('Descending'), value: false },
|
||||
];
|
||||
|
||||
const handleSetViewMode = (value: LibraryViewModeType) => {
|
||||
settings.libraryViewMode = value;
|
||||
setSettings(settings);
|
||||
saveSettings(envConfig, settings);
|
||||
setIsDropdownOpen?.(false);
|
||||
|
||||
const params = new URLSearchParams(searchParams?.toString());
|
||||
params.set('view', value);
|
||||
navigateToLibrary(router, `${params.toString()}`);
|
||||
};
|
||||
|
||||
const handleSetSortBy = (value: LibrarySortByType) => {
|
||||
settings.librarySortBy = value;
|
||||
setSettings(settings);
|
||||
@@ -62,6 +79,16 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
tabIndex={0}
|
||||
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'
|
||||
>
|
||||
{viewOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
label={option.label}
|
||||
buttonClass='h-8'
|
||||
Icon={viewMode === option.value ? MdCheck : undefined}
|
||||
onClick={() => handleSetViewMode(option.value as LibraryViewModeType)}
|
||||
/>
|
||||
))}
|
||||
<hr className='border-base-200 my-1' />
|
||||
<MenuItem
|
||||
label={_('Sort by...')}
|
||||
buttonClass='h-8'
|
||||
@@ -73,7 +100,7 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
key={option.value}
|
||||
label={option.label}
|
||||
buttonClass='h-8'
|
||||
icon={sortBy === option.value ? <MdCheck className='text-base-content' /> : undefined}
|
||||
Icon={sortBy === option.value ? MdCheck : undefined}
|
||||
onClick={() => handleSetSortBy(option.value as LibrarySortByType)}
|
||||
/>
|
||||
))}
|
||||
@@ -83,9 +110,7 @@ const SortMenu: React.FC<SortMenuProps> = ({ setIsDropdownOpen }) => {
|
||||
key={option.value.toString()}
|
||||
label={option.label}
|
||||
buttonClass='h-8'
|
||||
icon={
|
||||
isAscending === option.value ? <MdCheck className='text-base-content' /> : undefined
|
||||
}
|
||||
Icon={isAscending === option.value ? MdCheck : undefined}
|
||||
onClick={() => handleSetSortAscending(option.value)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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';
|
||||
@@ -29,9 +28,14 @@ import { usePullToRefresh } from '@/hooks/usePullToRefresh';
|
||||
import { useTheme } from '@/hooks/useTheme';
|
||||
import { useDemoBooks } from './hooks/useDemoBooks';
|
||||
import { useBooksSync } from './hooks/useBooksSync';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
|
||||
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
|
||||
import { tauriHandleSetAlwaysOnTop, tauriQuitApp } from '@/utils/window';
|
||||
import {
|
||||
tauriHandleSetAlwaysOnTop,
|
||||
tauriHandleToggleFullScreen,
|
||||
tauriQuitApp,
|
||||
} from '@/utils/window';
|
||||
|
||||
import { AboutWindow } from '@/components/AboutWindow';
|
||||
import { UpdaterWindow } from '@/components/UpdaterWindow';
|
||||
@@ -60,9 +64,9 @@ 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 { statusBarHeight } = useThemeStore();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const isInitiating = useRef(false);
|
||||
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||
@@ -87,6 +91,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
useScreenWakeLock(settings.screenWakeLock);
|
||||
|
||||
useShortcuts({
|
||||
onToggleFullscreen: async () => {
|
||||
if (isTauriAppPlatform()) {
|
||||
await tauriHandleToggleFullScreen();
|
||||
}
|
||||
},
|
||||
onQuitApp: async () => {
|
||||
if (isTauriAppPlatform()) {
|
||||
await tauriQuitApp();
|
||||
@@ -94,11 +103,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 +208,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 +537,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',
|
||||
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), ${statusBarHeight}px)`
|
||||
: '48px',
|
||||
}}
|
||||
>
|
||||
<DropIndicator />
|
||||
<Bookshelf
|
||||
@@ -575,6 +584,9 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
|
||||
isOpen={!!showDetailsBook}
|
||||
book={showDetailsBook}
|
||||
onClose={() => setShowDetailsBook(null)}
|
||||
handleBookUpload={handleBookUpload}
|
||||
handleBookDownload={handleBookDownload}
|
||||
handleBookDelete={handleBookDelete}
|
||||
/>
|
||||
)}
|
||||
<AboutWindow />
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
|
||||
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { useThemeStore } from '@/store/themeStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useTrafficLightStore } from '@/store/trafficLightStore';
|
||||
@@ -43,6 +44,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
} = useTrafficLightStore();
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const { bookKeys, hoveredBookKey, setHoveredBookKey } = useReaderStore();
|
||||
const { systemUIVisible, statusBarHeight } = useThemeStore();
|
||||
const { isSideBarVisible } = useSidebarStore();
|
||||
const iconSize16 = useResponsiveSize(16);
|
||||
|
||||
@@ -89,20 +91,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), ${statusBarHeight}px)` : '',
|
||||
}}
|
||||
/>
|
||||
<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), ${statusBarHeight}px)`
|
||||
: '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 +148,10 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
|
||||
showMaximize={
|
||||
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
|
||||
}
|
||||
onClose={() => onCloseBook(bookKey)}
|
||||
onClose={() => {
|
||||
setHoveredBookKey(null);
|
||||
onCloseBook(bookKey);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
|
||||
import clsx from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { useEffect, Suspense, useRef } from 'react';
|
||||
import { useEffect, Suspense, useRef, useState } 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,31 @@ import ReaderContent from './ReaderContent';
|
||||
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const { envConfig, appService } = useEnv();
|
||||
const { settings, setSettings } = useSettingsStore();
|
||||
const { isDarkMode, showSystemUI, dismissSystemUI } = useThemeStore();
|
||||
const { hoveredBookKey } = useReaderStore();
|
||||
const { isSideBarVisible } = useSidebarStore();
|
||||
const { getVisibleLibrary, setLibrary } = useLibraryStore();
|
||||
const { setLibrary } = useLibraryStore();
|
||||
const [libraryLoaded, setLibraryLoaded] = useState(false);
|
||||
const isInitiating = useRef(false);
|
||||
|
||||
const { updateAppTheme } = useThemeStore();
|
||||
useTheme();
|
||||
useTheme({ systemUIVisible: false, appThemeColor: 'base-100' });
|
||||
useScreenWakeLock(settings.screenWakeLock);
|
||||
|
||||
useEffect(() => {
|
||||
updateAppTheme('base-100');
|
||||
const handleVisibilityChange = () => {
|
||||
if (appService?.isMobile && !document.hidden) {
|
||||
dismissSystemUI();
|
||||
setSystemUIVisibility({ visible: false, darkMode: isDarkMode });
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appService, isDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitiating.current) return;
|
||||
isInitiating.current = true;
|
||||
const initLibrary = async () => {
|
||||
@@ -36,14 +53,27 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
|
||||
const settings = await appService.loadSettings();
|
||||
setSettings(settings);
|
||||
setLibrary(await appService.loadLibraryBooks());
|
||||
setLibraryLoaded(true);
|
||||
};
|
||||
|
||||
initLibrary();
|
||||
// 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 &&
|
||||
libraryLoaded &&
|
||||
settings.globalReadSettings && (
|
||||
<div
|
||||
className={clsx(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -117,7 +117,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
<MenuItem
|
||||
label={_('Scrolled Mode')}
|
||||
shortcut='Shift+J'
|
||||
icon={isScrolledMode ? <MdCheck /> : undefined}
|
||||
Icon={isScrolledMode ? MdCheck : undefined}
|
||||
onClick={toggleScrolledMode}
|
||||
/>
|
||||
|
||||
@@ -132,7 +132,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
|
||||
? _('Light Mode')
|
||||
: _('Auto Mode')
|
||||
}
|
||||
icon={themeMode === 'dark' ? <BiMoon /> : themeMode === 'light' ? <BiSun /> : <TbSunMoon />}
|
||||
Icon={themeMode === 'dark' ? BiMoon : themeMode === 'light' ? BiSun : TbSunMoon}
|
||||
onClick={cycleThemeMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,13 +62,13 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
|
||||
.map((book) => (
|
||||
<MenuItem
|
||||
key={book.hash}
|
||||
icon={
|
||||
Icon={
|
||||
<Image
|
||||
src={book.coverImageUrl!}
|
||||
alt={book.title}
|
||||
width={56}
|
||||
height={80}
|
||||
className='aspect-auto max-h-8 max-w-6 rounded-sm shadow-md'
|
||||
className='aspect-auto max-h-8 max-w-4 rounded-sm shadow-md'
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
|
||||
@@ -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);
|
||||
@@ -98,7 +99,7 @@ const TTSControl = () => {
|
||||
const view = getView(bookKey);
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const bookData = getBookData(bookKey);
|
||||
if (!view || !viewSettings || !bookData) return;
|
||||
if (!view || !viewSettings || !bookData || !bookData.book) return;
|
||||
if (bookData.book?.format === 'PDF') {
|
||||
eventDispatcher.dispatch('toast', {
|
||||
message: _('TTS not supported for PDF'),
|
||||
@@ -107,6 +108,7 @@ const TTSControl = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const primaryLang = bookData.book.primaryLanguage;
|
||||
setBookKey(bookKey);
|
||||
|
||||
if (ttsControllerRef.current) {
|
||||
@@ -127,9 +129,13 @@ const TTSControl = () => {
|
||||
await ttsController.initViewTTS();
|
||||
const ssml = view.tts?.from(range);
|
||||
if (ssml) {
|
||||
const lang = parseSSMLLang(ssml) || 'en';
|
||||
setTtsLang(lang);
|
||||
let lang = parseSSMLLang(ssml) || 'en';
|
||||
// We will not trust 'en' language from ssml, as it may be a fallback or hardcoded value
|
||||
if (lang === 'en' && primaryLang && primaryLang !== 'en') {
|
||||
lang = primaryLang.split('-')[0]!;
|
||||
}
|
||||
setIsPlaying(true);
|
||||
setTtsLang(lang);
|
||||
|
||||
ttsController.setLang(lang);
|
||||
ttsController.setRate(viewSettings.ttsRate);
|
||||
|
||||
@@ -6,7 +6,7 @@ import useBooksManager from './useBooksManager';
|
||||
import { useSidebarStore } from '@/store/sidebarStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { getStyles } from '@/utils/style';
|
||||
import { tauriQuitApp } from '@/utils/window';
|
||||
import { tauriHandleToggleFullScreen, tauriQuitApp } from '@/utils/window';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL, ZOOM_STEP } from '@/services/constants';
|
||||
|
||||
@@ -84,6 +84,12 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const toggleFullscreen = async () => {
|
||||
if (isTauriAppPlatform()) {
|
||||
await tauriHandleToggleFullScreen();
|
||||
}
|
||||
};
|
||||
|
||||
const quitApp = async () => {
|
||||
// on web platform use browser's default shortcut to close the tab
|
||||
if (isTauriAppPlatform()) {
|
||||
@@ -136,6 +142,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
|
||||
onOpenFontLayoutSettings: () => setFontLayoutSettingsDialogOpen(true),
|
||||
onToggleSearchBar: showSearchBar,
|
||||
onReloadPage: reloadPage,
|
||||
onToggleFullscreen: toggleFullscreen,
|
||||
onQuitApp: quitApp,
|
||||
onGoLeft: goLeft,
|
||||
onGoRight: goRight,
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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');
|
||||
@@ -51,7 +52,7 @@ export const AboutWindow = () => {
|
||||
</div>
|
||||
<div className='flex select-text flex-col items-center'>
|
||||
<h2 className='text-2xl font-bold'>Readest</h2>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
<p className='text-neutral-content text-center text-sm'>
|
||||
{_('Version {{version}}', { version: packageJson.version })} {`(${browserInfo})`}
|
||||
</p>
|
||||
</div>
|
||||
@@ -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>
|
||||
|
||||
@@ -17,9 +17,10 @@ const Alert: React.FC<{
|
||||
'alert flex items-center justify-between',
|
||||
'bg-base-300 rounded-lg border-none p-4 shadow-2xl',
|
||||
'w-full max-w-[90vw] sm:max-w-[70vw] md:max-w-[50vw] lg:max-w-[40vw] xl:max-w-[40vw]',
|
||||
'min-w-[70vw] flex-col sm:min-w-[40vw] sm:flex-row',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<div className='flex items-center space-x-2 self-start'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
fill='none'
|
||||
@@ -34,12 +35,12 @@ const Alert: React.FC<{
|
||||
></path>
|
||||
</svg>
|
||||
<div className=''>
|
||||
<h3 className='font-sm text-base'>{title}</h3>
|
||||
<h3 className='font-sm text-start text-base sm:text-center'>{title}</h3>
|
||||
<div className='text-xs'>{message}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-wrap items-center justify-center gap-2'>
|
||||
<button className='btn btn-sm' onClick={onCancel}>
|
||||
<div className='flex flex-wrap items-center gap-2 self-end sm:max-w-[20vw]'>
|
||||
<button className='btn btn-sm btn-neutral' onClick={onCancel}>
|
||||
{_('Cancel')}
|
||||
</button>
|
||||
<button className='btn btn-sm btn-warning' onClick={onConfirm}>
|
||||
|
||||
@@ -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 sm:max-h-[90%]'
|
||||
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-6 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')}
|
||||
{formatAuthors(book.author, book.primaryLanguage) || _('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,18 @@ 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>
|
||||
<span className='font-bold'>{_('Description:')}</span>
|
||||
<p className='text-neutral-content text-sm'>
|
||||
{bookMeta.description || _('No description available')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
@@ -1,6 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useDefaultIconSize } from '@/hooks/useResponsiveSize';
|
||||
import { IconType } from 'react-icons';
|
||||
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
|
||||
|
||||
interface MenuItemProps {
|
||||
label: string;
|
||||
@@ -9,7 +10,7 @@ interface MenuItemProps {
|
||||
shortcut?: string;
|
||||
disabled?: boolean;
|
||||
noIcon?: boolean;
|
||||
icon?: React.ReactNode;
|
||||
Icon?: React.ReactNode | IconType;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
@@ -21,15 +22,15 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
shortcut,
|
||||
disabled,
|
||||
noIcon = false,
|
||||
icon,
|
||||
Icon,
|
||||
children,
|
||||
onClick,
|
||||
}) => {
|
||||
const iconSize = useDefaultIconSize();
|
||||
const iconSize = useResponsiveSize(16);
|
||||
const menuButton = (
|
||||
<button
|
||||
className={clsx(
|
||||
'hover:bg-base-300 text-base-content flex h-10 w-full items-center justify-between rounded-md p-2',
|
||||
'hover:bg-base-300 text-base-content flex h-10 w-full items-center justify-between rounded-md p-1',
|
||||
disabled && 'btn-disabled text-gray-400',
|
||||
buttonClass,
|
||||
)}
|
||||
@@ -37,7 +38,15 @@ const MenuItem: React.FC<MenuItemProps> = ({
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className='flex min-w-0 items-center'>
|
||||
{!noIcon && <span style={{ minWidth: `${iconSize}px` }}>{icon}</span>}
|
||||
{!noIcon && (
|
||||
<span style={{ minWidth: `${iconSize}px` }}>
|
||||
{typeof Icon === 'function' ? (
|
||||
<Icon className='text-base-content' size={iconSize} />
|
||||
) : (
|
||||
Icon
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={clsx('mx-2 flex-1 truncate text-base sm:text-sm', labelClass)}
|
||||
style={{ minWidth: 0 }}
|
||||
|
||||
@@ -40,7 +40,9 @@ const Quota: React.FC<QuotaProps> = ({ quotas, showProgress, className }) => {
|
||||
></div>
|
||||
)}
|
||||
|
||||
<div className={clsx('relative flex items-center justify-between p-2', className)}>
|
||||
<div
|
||||
className={clsx('relative flex items-center justify-between gap-4 p-2', className)}
|
||||
>
|
||||
<div className='lg:tooltip lg:tooltip-bottom' data-tip={quota.tooltip}>
|
||||
<span className='truncate'>{quota.name}</span>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -7,6 +7,7 @@ export interface ShortcutConfig {
|
||||
onToggleSelectMode: string[];
|
||||
onOpenFontLayoutSettings: string[];
|
||||
onReloadPage: string[];
|
||||
onToggleFullscreen: string[];
|
||||
onQuitApp: string[];
|
||||
onGoLeft: string[];
|
||||
onGoRight: string[];
|
||||
@@ -32,6 +33,7 @@ const DEFAULT_SHORTCUTS: ShortcutConfig = {
|
||||
onToggleSelectMode: ['shift+s'],
|
||||
onOpenFontLayoutSettings: ['shift+f'],
|
||||
onReloadPage: ['shift+r'],
|
||||
onToggleFullscreen: ['F11'],
|
||||
onQuitApp: ['ctrl+q', 'cmd+q'],
|
||||
onGoLeft: ['ArrowLeft', 'PageUp', 'h'],
|
||||
onGoRight: ['ArrowRight', 'PageDown', 'l', ' '],
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -63,8 +63,8 @@ const useShortcuts = (actions: KeyActionHandlers, dependencies: React.Dependency
|
||||
handler &&
|
||||
shortcutList?.some((shortcut) =>
|
||||
isShortcutMatch(shortcut, key, ctrlKey, altKey, metaKey, shiftKey),
|
||||
)
|
||||
) {
|
||||
)
|
||||
) {
|
||||
handler();
|
||||
return true;
|
||||
}
|
||||
@@ -76,11 +76,12 @@ const useShortcuts = (actions: KeyActionHandlers, dependencies: React.Dependency
|
||||
// Check if the focus is on an input, textarea, or contenteditable element
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
const isInteractiveElement =
|
||||
(activeElement.tagName === 'INPUT' ||
|
||||
activeElement.tagName === 'INPUT' ||
|
||||
activeElement.tagName === 'TEXTAREA' ||
|
||||
activeElement.isContentEditable);
|
||||
activeElement.isContentEditable;
|
||||
|
||||
const isNoteEditor = (activeElement.tagName === 'TEXTAREA' && activeElement.classList.contains('note-editor'))
|
||||
const isNoteEditor =
|
||||
activeElement.tagName === 'TEXTAREA' && activeElement.classList.contains('note-editor');
|
||||
|
||||
if (isInteractiveElement && !isNoteEditor) {
|
||||
return; // Skip handling if the user is typing in an input, textarea, or contenteditable
|
||||
@@ -89,7 +90,7 @@ const useShortcuts = (actions: KeyActionHandlers, dependencies: React.Dependency
|
||||
if (event instanceof KeyboardEvent) {
|
||||
const { key, ctrlKey, altKey, metaKey, shiftKey } = event;
|
||||
|
||||
if (isNoteEditor && !((key === "Enter" && ctrlKey) || (key == "Escape"))) {
|
||||
if (isNoteEditor && !((key === 'Enter' && ctrlKey) || key == 'Escape')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
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 { getStatusBarHeight, 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, setStatusBarHeight } = useThemeStore();
|
||||
|
||||
useEffect(() => {
|
||||
updateAppTheme(appThemeColor);
|
||||
if (appService?.isMobile) {
|
||||
setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode });
|
||||
}
|
||||
if (appService?.isAndroidApp) {
|
||||
getStatusBarHeight().then((res) => {
|
||||
if (res.height && res.height > 0) {
|
||||
setStatusBarHeight(res.height / window.devicePixelRatio);
|
||||
}
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appService, isDarkMode]);
|
||||
|
||||
useEffect(() => {
|
||||
const customThemes = settings.globalReadSettings?.customThemes ?? [];
|
||||
customThemes.forEach((customTheme) => {
|
||||
applyCustomTheme(customTheme);
|
||||
});
|
||||
localStorage.setItem('customThemes', JSON.stringify(customThemes));
|
||||
}, [settings]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -20,6 +20,11 @@ const getUserAndToken = async (authHeader: string | undefined) => {
|
||||
return { user, token };
|
||||
};
|
||||
|
||||
const LANG_V2_V1_MAP: Record<string, string> = {
|
||||
'ZH-HANS': 'ZH',
|
||||
'ZH-HANT': 'ZH-TW',
|
||||
};
|
||||
|
||||
const getDeepLAPIKey = (keys: string | undefined) => {
|
||||
const keyArray = keys?.split(',') ?? [];
|
||||
return keyArray.length ? keyArray[Math.floor(Math.random() * keyArray.length)] : '';
|
||||
@@ -52,12 +57,17 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
|
||||
const {
|
||||
text,
|
||||
source_lang: sourceLang = 'auto',
|
||||
target_lang: targetLang = 'en',
|
||||
source_lang: sourceLang = 'AUTO',
|
||||
target_lang: targetLang = 'EN',
|
||||
}: { text: string[]; source_lang: string; target_lang: string } = req.body;
|
||||
try {
|
||||
if (user && token) {
|
||||
console.log('deeplApiUrl', deeplApiUrl);
|
||||
const isV2Api = deeplApiUrl.endsWith('/v2/translate');
|
||||
const requestBody = {
|
||||
text: isV2Api ? text : (text[0] ?? ''),
|
||||
source_lang: isV2Api ? sourceLang : (LANG_V2_V1_MAP[sourceLang] ?? sourceLang),
|
||||
target_lang: isV2Api ? targetLang : (LANG_V2_V1_MAP[targetLang] ?? targetLang),
|
||||
};
|
||||
const response = await fetch(deeplApiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -65,10 +75,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
'x-fingerprint': process.env['DEEPL_X_FINGERPRINT'] || '',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(req.body),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
res.status(response.status);
|
||||
res.json(await response.json());
|
||||
const data = await response.json();
|
||||
const result = {
|
||||
...data,
|
||||
translations: data.translations || [
|
||||
{
|
||||
text: data.data,
|
||||
},
|
||||
],
|
||||
};
|
||||
res.status(response.status).json(result);
|
||||
} else {
|
||||
const result = await deeplQuery({
|
||||
text: text[0] ?? '',
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
formatTitle,
|
||||
formatAuthors,
|
||||
getFilename,
|
||||
getPrimaryLanguage,
|
||||
} from '@/utils/book';
|
||||
import { partialMD5 } from '@/utils/md5';
|
||||
import { BookDoc, DocumentLoader } from '@/libs/document';
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
DEFAULT_MOBILE_VIEW_SETTINGS,
|
||||
DEFAULT_SYSTEM_SETTINGS,
|
||||
DEFAULT_CJK_VIEW_SETTINGS,
|
||||
DEFAULT_MOBILE_READSETTINGS,
|
||||
} from './constants';
|
||||
import { getOSPlatform, isCJKEnv, isContentURI, isValidURL } from '@/utils/misc';
|
||||
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
|
||||
@@ -99,7 +101,10 @@ export abstract class BaseAppService implements AppService {
|
||||
...DEFAULT_SYSTEM_SETTINGS,
|
||||
version: SYSTEM_SETTINGS_VERSION,
|
||||
localBooksDir: await this.getInitBooksDir(),
|
||||
globalReadSettings: DEFAULT_READSETTINGS,
|
||||
globalReadSettings: {
|
||||
...DEFAULT_READSETTINGS,
|
||||
...(this.isMobile ? DEFAULT_MOBILE_READSETTINGS : {}),
|
||||
},
|
||||
globalViewSettings: {
|
||||
...DEFAULT_BOOK_LAYOUT,
|
||||
...DEFAULT_BOOK_STYLE,
|
||||
@@ -137,7 +142,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
|
||||
@@ -193,6 +199,7 @@ export abstract class BaseAppService implements AppService {
|
||||
format,
|
||||
title: formatTitle(loadedBook.metadata.title),
|
||||
author: formatAuthors(loadedBook.metadata.author, loadedBook.metadata.language),
|
||||
primaryLanguage: getPrimaryLanguage(loadedBook.metadata.language),
|
||||
createdAt: existingBook ? existingBook.createdAt : Date.now(),
|
||||
uploadedAt: existingBook ? existingBook.uploadedAt : null,
|
||||
deletedAt: transient ? Date.now() : null,
|
||||
@@ -203,6 +210,7 @@ export abstract class BaseAppService implements AppService {
|
||||
if (existingBook) {
|
||||
existingBook.title = book.title;
|
||||
existingBook.author = book.author;
|
||||
existingBook.primaryLanguage = existingBook.primaryLanguage ?? book.primaryLanguage;
|
||||
}
|
||||
|
||||
if (!(await this.fs.exists(getDir(book), 'Books'))) {
|
||||
|
||||
@@ -38,6 +38,7 @@ export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
|
||||
autoCheckUpdates: true,
|
||||
screenWakeLock: true,
|
||||
autoImportBooksOnOpen: false,
|
||||
libraryViewMode: 'grid',
|
||||
librarySortBy: 'updated',
|
||||
librarySortAscending: false,
|
||||
|
||||
@@ -63,6 +64,11 @@ export const DEFAULT_READSETTINGS: ReadSettings = {
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_MOBILE_READSETTINGS: Partial<ReadSettings> = {
|
||||
sideBarWidth: '25%',
|
||||
isSideBarPinned: false,
|
||||
};
|
||||
|
||||
export const DEFAULT_BOOK_FONT: BookFont = {
|
||||
serifFont: 'Bitter',
|
||||
sansSerifFont: 'Roboto',
|
||||
@@ -150,7 +156,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 +447,7 @@ export const CJK_FONTS_PATTENS = new RegExp(
|
||||
'Hei',
|
||||
'Yan',
|
||||
'Min',
|
||||
'Khai',
|
||||
'Yuan',
|
||||
'Song',
|
||||
'Ming',
|
||||
@@ -578,3 +589,5 @@ export const TRANSLATED_LANGS = {
|
||||
'zh-CN': '简体中文',
|
||||
'zh-TW': '正體中文',
|
||||
};
|
||||
|
||||
export const SUPPORTED_LANGS: Record<string, string> = { ...TRANSLATED_LANGS, zh: '中文' };
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { TTSGranularity } from '@/types/view';
|
||||
import { TTSUtils } from './TTSUtils';
|
||||
|
||||
export class EdgeTTSClient implements TTSClient {
|
||||
#primaryLang = 'en';
|
||||
#rate = 1.0;
|
||||
#pitch = 1.0;
|
||||
#voice: TTSVoice | null = null;
|
||||
@@ -50,7 +51,10 @@ export class EdgeTTSClient implements TTSClient {
|
||||
preload = false,
|
||||
): AsyncGenerator<TTSMessageEvent> {
|
||||
const { marks } = parseSSMLMarks(ssml);
|
||||
const lang = parseSSMLLang(ssml) || 'en';
|
||||
let lang = parseSSMLLang(ssml) || 'en';
|
||||
if (lang === 'en' && this.#primaryLang && this.#primaryLang !== 'en') {
|
||||
lang = this.#primaryLang;
|
||||
}
|
||||
let voiceId = 'en-US-AriaNeural';
|
||||
if (!this.#voice || this.#currentVoiceLang !== lang) {
|
||||
const preferredVoiceId = TTSUtils.getPreferredVoice('edge-tts', lang);
|
||||
@@ -207,6 +211,10 @@ export class EdgeTTSClient implements TTSClient {
|
||||
}
|
||||
}
|
||||
|
||||
setPrimaryLang(lang: string) {
|
||||
this.#primaryLang = lang;
|
||||
}
|
||||
|
||||
async setRate(rate: number) {
|
||||
// The Edge TTS API uses rate in [0.5 .. 2.0].
|
||||
this.#rate = rate;
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface TTSClient {
|
||||
pause(): Promise<void>;
|
||||
resume(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
setPrimaryLang(lang: string): void;
|
||||
setRate(rate: number): Promise<void>;
|
||||
setPitch(pitch: number): Promise<void>;
|
||||
setVoice(voice: string): Promise<void>;
|
||||
|
||||
@@ -219,6 +219,12 @@ export class TTSController extends EventTarget {
|
||||
|
||||
async setLang(lang: string) {
|
||||
this.ttsLang = lang;
|
||||
this.setPrimaryLang(lang);
|
||||
}
|
||||
|
||||
async setPrimaryLang(lang: string) {
|
||||
this.ttsEdgeClient.setPrimaryLang(lang);
|
||||
this.ttsWebClient.setPrimaryLang(lang);
|
||||
}
|
||||
|
||||
async setRate(rate: number) {
|
||||
|
||||
@@ -173,6 +173,7 @@ async function* speakWithMarks(
|
||||
}
|
||||
|
||||
export class WebSpeechClient implements TTSClient {
|
||||
#primaryLang = 'en';
|
||||
#rate = 1.0;
|
||||
#pitch = 1.0;
|
||||
#voice: SpeechSynthesisVoice | null = null;
|
||||
@@ -215,7 +216,10 @@ export class WebSpeechClient implements TTSClient {
|
||||
// no need to preload for web speech
|
||||
if (preload) return;
|
||||
|
||||
const lang = parseSSMLLang(ssml) || 'en';
|
||||
let lang = parseSSMLLang(ssml) || 'en';
|
||||
if (lang === 'en' && this.#primaryLang && this.#primaryLang !== 'en') {
|
||||
lang = this.#primaryLang;
|
||||
}
|
||||
if (!this.#voice || this.#currentVoiceLang !== lang) {
|
||||
const preferredVoiceId = TTSUtils.getPreferredVoice('web-speech', lang);
|
||||
const preferredVoice = this.#voices.find((v) => v.voiceURI === preferredVoiceId);
|
||||
@@ -262,6 +266,10 @@ export class WebSpeechClient implements TTSClient {
|
||||
this.#synth.cancel();
|
||||
}
|
||||
|
||||
setPrimaryLang(lang: string) {
|
||||
this.#primaryLang = lang;
|
||||
}
|
||||
|
||||
async setRate(rate: number) {
|
||||
// The Web Speech API uses utterance.rate in [0.1 .. 10],
|
||||
this.#rate = rate;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { updateTocCFI, updateTocID } from '@/utils/toc';
|
||||
import { useSettingsStore } from './settingsStore';
|
||||
import { useBookDataStore } from './bookDataStore';
|
||||
import { useLibraryStore } from './libraryStore';
|
||||
import { getPrimaryLanguage } from '@/utils/book';
|
||||
|
||||
interface ViewState {
|
||||
/* Unique key for each book view */
|
||||
@@ -32,7 +33,6 @@ interface ReaderStore {
|
||||
setBookKeys: (keys: string[]) => void;
|
||||
setHoveredBookKey: (key: string | null) => void;
|
||||
setBookmarkRibbonVisibility: (key: string, visible: boolean) => void;
|
||||
|
||||
setProgress: (
|
||||
key: string,
|
||||
location: string,
|
||||
@@ -65,7 +65,6 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
hoveredBookKey: null,
|
||||
setBookKeys: (keys: string[]) => set({ bookKeys: keys }),
|
||||
setHoveredBookKey: (key: string | null) => set({ hoveredBookKey: key }),
|
||||
|
||||
getView: (key: string | null) => (key && get().viewStates[key]?.view) || null,
|
||||
setView: (key: string, view) =>
|
||||
set((state) => ({
|
||||
@@ -130,6 +129,9 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
|
||||
}, {});
|
||||
updateTocCFI(bookDoc, bookDoc.toc, sections);
|
||||
}
|
||||
// Set the book's language for formerly imported books, newly imported books have this field set
|
||||
book.primaryLanguage =
|
||||
book.primaryLanguage ?? getPrimaryLanguage(bookDoc.metadata.language);
|
||||
useBookDataStore.setState((state) => ({
|
||||
booksData: {
|
||||
...state.booksData,
|
||||
|
||||
@@ -10,6 +10,11 @@ interface ThemeState {
|
||||
systemIsDarkMode: boolean;
|
||||
themeCode: ThemeCode;
|
||||
isDarkMode: boolean;
|
||||
systemUIVisible: boolean;
|
||||
statusBarHeight: number;
|
||||
setStatusBarHeight: (height: number) => void;
|
||||
showSystemUI: () => void;
|
||||
dismissSystemUI: () => void;
|
||||
getIsDarkMode: () => boolean;
|
||||
setThemeMode: (mode: ThemeMode) => void;
|
||||
setThemeColor: (color: string) => void;
|
||||
@@ -62,6 +67,11 @@ export const useThemeStore = create<ThemeState>((set, get) => {
|
||||
systemIsDarkMode,
|
||||
isDarkMode,
|
||||
themeCode,
|
||||
systemUIVisible: false,
|
||||
statusBarHeight: 24,
|
||||
showSystemUI: () => set({ systemUIVisible: true }),
|
||||
dismissSystemUI: () => set({ systemUIVisible: false }),
|
||||
setStatusBarHeight: (height: number) => set({ statusBarHeight: height }),
|
||||
getIsDarkMode: () => get().isDarkMode,
|
||||
setThemeMode: (mode) => {
|
||||
if (typeof window !== 'undefined' && localStorage) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface Book {
|
||||
|
||||
lastUpdated?: number; // deprecated in favor of updatedAt
|
||||
progress?: [number, number]; // Add progress field: [current, total], 1-based page number
|
||||
primaryLanguage?: string;
|
||||
}
|
||||
|
||||
export interface BookGroupType {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { CustomTheme } from '@/styles/themes';
|
||||
import { HighlightColor, HighlightStyle, ViewSettings } from './book';
|
||||
|
||||
export type ThemeType = 'light' | 'dark' | 'auto';
|
||||
export type LibraryViewModeType = 'grid' | 'list';
|
||||
export type LibrarySortByType = 'title' | 'author' | 'updated' | 'created' | 'size' | 'format';
|
||||
|
||||
export interface ReadSettings {
|
||||
@@ -27,6 +28,7 @@ export interface SystemSettings {
|
||||
autoCheckUpdates: boolean;
|
||||
screenWakeLock: boolean;
|
||||
autoImportBooksOnOpen: boolean;
|
||||
libraryViewMode: LibraryViewModeType;
|
||||
librarySortBy: LibrarySortByType;
|
||||
librarySortAscending: boolean;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Book, BookConfig, BookProgress, WritingMode } from '@/types/book';
|
||||
import { getUserLang, isContentURI, isValidURL, makeSafeFilename } from './misc';
|
||||
import { getStorageType } from './object';
|
||||
import { getDirFromLanguage } from './rtl';
|
||||
import { SUPPORTED_LANGS } from '@/services/constants';
|
||||
|
||||
export const getDir = (book: Book) => {
|
||||
return `${book.hash}`;
|
||||
@@ -107,15 +108,21 @@ export const formatPublisher = (publisher: string | LanguageMap) => {
|
||||
return typeof publisher === 'string' ? publisher : formatLanguageMap(publisher);
|
||||
};
|
||||
|
||||
export const formatLanguage = (lang: string | string[] | undefined) => {
|
||||
return Array.isArray(lang) ? lang.join(', ') : lang;
|
||||
const langCodeToLangName = (langCode: string) => {
|
||||
return SUPPORTED_LANGS[langCode] || langCode.toUpperCase();
|
||||
};
|
||||
|
||||
export const primaryLanguage = (lang: string | string[] | undefined) => {
|
||||
export const formatLanguage = (lang: string | string[] | undefined): string => {
|
||||
return Array.isArray(lang)
|
||||
? lang.map(langCodeToLangName).join(', ')
|
||||
: langCodeToLangName(lang || '');
|
||||
};
|
||||
|
||||
export const getPrimaryLanguage = (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 {
|
||||
@@ -159,6 +166,6 @@ export const getBookDirFromWritingMode = (writingMode: WritingMode) => {
|
||||
};
|
||||
|
||||
export const getBookDirFromLanguage = (language: string | string[] | undefined) => {
|
||||
const lang = primaryLanguage(language) || '';
|
||||
const lang = getPrimaryLanguage(language) || '';
|
||||
return getDirFromLanguage(lang);
|
||||
};
|
||||
|
||||
@@ -23,6 +23,21 @@ export interface InstallPackageResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface SetSystemUIVisibilityRequest {
|
||||
visible: boolean;
|
||||
darkMode: boolean;
|
||||
}
|
||||
|
||||
export interface SetSystemUIVisibilityResponse {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface GetStatusBarHeightResponse {
|
||||
height: number;
|
||||
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 +60,22 @@ 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;
|
||||
}
|
||||
|
||||
export async function getStatusBarHeight(): Promise<GetStatusBarHeightResponse> {
|
||||
const result = await invoke<GetStatusBarHeightResponse>(
|
||||
'plugin:native-bridge|get_status_bar_height',
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -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)')
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: 28b88e50c8...1f545c5ef5
+1
-1
Submodule packages/tauri updated: b154826881...29264c8244
Reference in New Issue
Block a user