Compare commits

...

32 Commits

Author SHA1 Message Date
Huang Xin 382e0ca3c6 release: version 0.9.41 (#1143) 2025-05-13 14:17:01 +02:00
Huang Xin d96fb32bcf fix: apply layout styles to div tag, closes #1130 (#1142) 2025-05-13 13:48:21 +02:00
Huang Xin 4779e2638f fix: disable updater for non-appimage app on linux, closes #1129 (#1141) 2025-05-13 12:50:40 +02:00
Huang Xin d094ba9916 chore: cache avatar image for offline usage (#1140) 2025-05-13 10:42:00 +02:00
Huang Xin e4ba0c302e fix: segment txt with paragraph count if no chapter titles are parsed, closes #1134 (#1139) 2025-05-13 10:21:47 +02:00
Huang Xin 0150177a9d chore: bump tauri to version 2.5.1 (#1138) 2025-05-13 09:30:34 +02:00
Huang Xin 74e32d47b0 release: add linux arch armhf (#1133) 2025-05-13 14:57:27 +08:00
Huang Xin 2dd7cf54e6 refactor: use text selector hook in annotator, closes #1020 (#1131) 2025-05-12 18:00:45 +02:00
Huang Xin a5cbfe97db fix: replace background when attribute changed, closes #1124 (#1126) 2025-05-11 17:07:17 +02:00
Huang Xin da49526f4c fix: now auto orientation follows system settings, closes #1093 and closes #1098 (#1122) 2025-05-11 14:57:05 +02:00
Huang Xin 2e46bc1627 css: get rid of hardcoded font color only when overriding font, closes #1095 (#1120) 2025-05-11 14:34:51 +02:00
Huang Xin 7e542e8694 fix: reorganize TTS control view hierarchy, closes #1080 (#1119) 2025-05-11 12:06:15 +02:00
Huang Xin 2a624ef57a css: only override background color in dark mode, closes #1066 (#1117) 2025-05-11 09:33:03 +02:00
Huang Xin 60ae0c9ac3 feat: show book file size in book details (#1114) 2025-05-10 18:28:08 +02:00
Huang Xin 110b4b37cd ui: various UI fixes and enhancements on mobile platforms (#1113) 2025-05-10 18:18:15 +02:00
Huang Xin f0edbf14a9 fix: padding top of the drag handler with the cutouts, closes #1101 (#1110) 2025-05-10 08:27:40 +02:00
Huang Xin 20669a9cb3 fix: show updating status in the about window, closes #1079 (#1109) 2025-05-10 08:00:25 +02:00
Huang Xin 3756bec66c css: special case for align center and zero text indent (#1105) 2025-05-09 14:40:55 +02:00
Huang Xin 3e1d03dc17 css: override text align and fix hardcoded text intent (#1104) 2025-05-09 14:20:38 +02:00
Huang Xin 4aebbf679f fix: various on styles of UI and footnote visibility (#1099)
* fix: footnote now should be hidden in srcdoc iframes

* fix: eliminate white flash on startup when using dark mode

* feat: rescale UI size with pixel density
2025-05-08 18:21:31 +02:00
Huang Xin 0a562d90ac fix: maintain library state across reader/library navigation, closes #1072 (#1096) 2025-05-08 09:26:58 +02:00
Huang Xin e91a39e8b6 release: version 0.9.40 (#1092) 2025-05-07 20:03:58 +02:00
Huang Xin 2006baac4b revert: unbreak style overriding, closes #1058 and closes #1075 (#1091) 2025-05-07 19:56:14 +02:00
Huang Xin 0d4f61f08a compat: fix insane img dimension might cause layout miscalculation, closes #1073 (#1090) 2025-05-07 18:33:13 +02:00
Huang Xin 0b5f0ede3b fix: close action bar when the footbar is dismissed, closes #1064 (#1086) 2025-05-07 14:44:17 +02:00
Huang Xin 99a146525f fix: transient navigation bar in Android 9, closes #1059 (#1085) 2025-05-07 14:11:46 +02:00
Huang Xin 6080f9e0f3 fix: revert to unlock screen orientation for library page, closes #1063 (#1084) 2025-05-07 13:50:40 +02:00
Huang Xin 6b3d41a4b8 fix: styling links and also the content of links, closes #1057 (#1083) 2025-05-07 12:46:28 +02:00
Huang Xin e403c009c2 fix: avoid glitches when expanding toc sub items, closes #1033 (#1081) 2025-05-07 12:22:42 +02:00
Huang Xin 14bf2bcf8f fix: segment separator now should be more than 8 dash, closes #1056 (#1077) 2025-05-07 09:58:30 +02:00
Huang Xin 7f66af71c8 fix: dialog drag handler layout, closes #1036 (#1076) 2025-05-07 09:27:20 +02:00
Huang Xin cbab201138 doc: format issue templates (#1071) 2025-05-07 06:04:20 +02:00
74 changed files with 1523 additions and 1039 deletions
+8 -4
View File
@@ -7,13 +7,17 @@ assignees: ''
---
**Does your feature request involve difficulty for you to complete a task? Please describe.**
A clear and concise description of what the problem is. Ex. I think it takes too many steps to [...]
> A clear and concise description of what the problem is. Ex. I think it takes too many steps to [...]
**Describe the solution you'd like**
A clear and concise description of what you'd like to happen.
> A clear and concise description of what you'd like to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
> A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any additional context or screenshots about the feature request here.
> Add any additional context or screenshots about the feature request here.
+10 -19
View File
@@ -11,22 +11,16 @@ assignees: ''
A clear and concise description of what the current behavior is.
Please also add **screenshots** of the existing application.
**Example:**
```
In DarkMode, when scrollbar are displayed (for example on Companies page, with enough companies in the list), we see a blank square in the bottom right corner
[screenshot]
```
> **Example:**
> In DarkMode, when scrollbar are displayed (for example on Companies page, with enough companies in the list), we see a blank square in the bottom right corner
> [screenshot]
## Expected behavior
A clear and concise description of what the expected behavior is.
**Example:**
```
The blank square should be transparent (invisible)
```
> **Example:**
> The blank square should be transparent (invisible)
## Technical inputs
@@ -34,11 +28,8 @@ Operating System:
Readest Version:
**Example:**
```
Operating System: macOS 14.3.1
Readest Version: 0.9.0
We are displaying custom scrollbars that disappear when the user is not scrolling. See ScrollWrapper.
Probably fixable with CSS
```
> **Example:**
> Operating System: Android 14 (WebView 135.0)
> Readest Version: 0.9.0
> We are displaying custom scrollbars that disappear when the user is not scrolling. See ScrollWrapper.
> Probably fixable with CSS
+17 -1
View File
@@ -67,6 +67,11 @@ jobs:
release: linux
arch: aarch64
rust_target: aarch64-unknown-linux-gnu
- os: ubuntu-22.04-arm
release: linux
arch: armhf
rust_target: arm-unknown-linux-gnueabihf
args: '--target arm-unknown-linux-gnueabihf --bundles deb'
- os: macos-latest
release: macos
arch: aarch64
@@ -133,11 +138,22 @@ jobs:
key: ${{ matrix.config.os }}-${{ matrix.config.release }}-${{ matrix.config.arch }}-cargo
- name: install dependencies (ubuntu only)
if: contains(matrix.config.os, 'ubuntu') && matrix.config.release != 'android'
if: contains(matrix.config.os, 'ubuntu') && matrix.config.release != 'android' && matrix.config.arch != 'armhf'
run: |
sudo apt-get update
sudo apt-get install -y pkg-config libfontconfig-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
- name: install dependencies (ubuntu only - armhf specific)
if: contains(matrix.config.os, 'ubuntu') && matrix.config.arch == 'armhf'
run: |
sudo dpkg --add-architecture armhf
sudo apt-get update
sudo apt-get install -y pkg-config libfontconfig-dev:armhf libgtk-3-dev:armhf libwebkit2gtk-4.1-dev:armhf libappindicator3-dev:armhf librsvg2-dev:armhf gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf
echo 'PKG_CONFIG_ALLOW_CROSS=1' >> $GITHUB_ENV
echo 'PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig:/usr/share/pkgconfig' >> $GITHUB_ENV
echo 'PKG_CONFIG_SYSROOT_DIR=/usr/arm-linux-gnueabihf' >> $GITHUB_ENV
echo 'CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc' >> $GITHUB_ENV
- name: create .env.local file for Next.js
run: |
echo "NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}" >> .env.local
+1 -1
View File
@@ -3,4 +3,4 @@
url = https://github.com/readest/foliate-js.git
[submodule "packages/tauri"]
path = packages/tauri
url = https://github.com/tauri-apps/tauri.git
url = https://github.com/readest/tauri.git
Generated
+490 -446
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.39",
"version": "0.9.41",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -42,19 +42,19 @@
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.47.7",
"@tauri-apps/api": "2.4.1",
"@tauri-apps/api": "2.5.0",
"@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "^2.2.1",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-haptics": "^2.2.4",
"@tauri-apps/plugin-http": "^2.4.3",
"@tauri-apps/plugin-log": "^2.3.1",
"@tauri-apps/plugin-log": "^2.4.0",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.7.0",
"@tauri-apps/plugin-updater": "^2.7.1",
"@zip.js/zip.js": "^2.7.53",
"aws4fetch": "^1.0.20",
"clsx": "^2.1.1",
@@ -80,7 +80,7 @@
},
"devDependencies": {
"@opennextjs/cloudflare": "^0.5.12",
"@tauri-apps/cli": "2.4.1",
"@tauri-apps/cli": "2.5.0",
"@types/cors": "^2.8.17",
"@types/cssbeautify": "^0.3.5",
"@types/node": "^22.10.1",
@@ -50,7 +50,6 @@
"Highlight": "تمييز",
"Horizontal Direction": "الاتجاه الأفقي",
"Hyphenation": "الواصلة",
"Identifier:": "المعرف:",
"Import Books": "استيراد الكتب",
"Language:": "اللغة:",
"Layout": "التخطيط",
@@ -303,5 +302,10 @@
"Orientation": "الاتجاه",
"Portrait": "العمودي",
"Landscape": "الأفقي",
"Open Last Book on Start": "فتح آخر كتاب عند البدء"
"Open Last Book on Start": "فتح آخر كتاب عند البدء",
"Checking for updates...": "جارٍ التحقق من التحديثات...",
"Error checking for updates": "خطأ في التحقق من التحديثات",
"Details": "التفاصيل",
"Cancel Selection": "إلغاء التحديد",
"File Size:": "حجم الملف:"
}
@@ -50,7 +50,6 @@
"Highlight": "Hervorheben",
"Horizontal Direction": "Horizontale Richtung",
"Hyphenation": "Silbentrennung",
"Identifier:": "Kennung:",
"Import Books": "Bücher importieren",
"Language:": "Sprache:",
"Layout": "Layout",
@@ -134,7 +133,7 @@
"Book downloaded: {{title}}": "Buch heruntergeladen: {{title}}",
"Failed to download book: {{title}}": "Fehler beim Herunterladen des Buches: {{title}}",
"Upload Book": "Buch hochladen",
"Auto Upload Books to Cloud": "Bücher automatisch in die Cloud hochladen",
"Auto Upload Books to Cloud": "Bücher automatisch hochladen",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% des Cloud-Speichers verwendet.",
"Storage": "Speicher",
"Book deleted: {{title}}": "Buch gelöscht: {{title}}",
@@ -303,5 +302,10 @@
"Orientation": "Orientierung",
"Portrait": "Hochformat",
"Landscape": "Querformat",
"Open Last Book on Start": "Letztes Buch beim Start öffnen"
"Open Last Book on Start": "Letztes Buch beim Start öffnen",
"Checking for updates...": "Auf Updates prüfen...",
"Error checking for updates": "Fehler beim Überprüfen auf Updates",
"Details": "Details",
"Cancel Selection": "Auswahl abbrechen",
"File Size:": "Dateigröße:"
}
@@ -50,7 +50,6 @@
"Highlight": "Επισήμανση",
"Horizontal Direction": "Οριζόντια κατεύθυνση",
"Hyphenation": "Συλλαβισμός",
"Identifier:": "Αναγνωριστικό:",
"Import Books": "Εισαγωγή βιβλίων",
"Language:": "Γλώσσα:",
"Layout": "Διάταξη",
@@ -134,12 +133,12 @@
"Book downloaded: {{title}}": "Το βιβλίο με τίτλο {{title}} κατέβηκε",
"Failed to download book: {{title}}": "Αποτυχία λήψης βιβλίου: {{title}}",
"Upload Book": "Ανέβασμα βιβλίου",
"Auto Upload Books to Cloud": "Αυτόματο ανέβασμα βιβλίων στο cloud",
"Auto Upload Books to Cloud": "Αυτόματη αποθήκευση στο cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% της αποθήκευσης στο cloud χρησιμοποιήθηκε.",
"Storage": "Αποθήκευση",
"Book deleted: {{title}}": "Το βιβλίο με τίτλο {{title}} διαγράφηκε",
"Failed to delete book: {{title}}": "Αποτυχία διαγραφής βιβλίου: {{title}}",
"Check Updates on Start": "Έλεγχος ενημερώσεων κατά την εκκίνηση",
"Check Updates on Start": "Αυτόματος έλεγχος ενημερώσεων",
"Insufficient storage quota": "Ανεπαρκές όριο αποθήκευσης",
"Font Weight": "Βάρος γραμματοσειράς",
"Line Spacing": "Διάκενο γραμμών",
@@ -296,12 +295,17 @@
"Grid": "Πλέγμα",
"(from 'As You Like It', Act II)": "('Όπως σας αρέσει', Πράξη II)",
"Link Color": "Χρώμα συνδέσμου",
"Volume Keys for Page Flip": "Πλήκτρα έντασης για ανατροπή σελίδας",
"Volume Keys for Page Flip": "Ανατροπή με πλήκτρα έντασης",
"Clicks for Page Flip": "Κλικ για ανατροπή σελίδας",
"Swap Clicks Area": "Ανταλλαγή περιοχής κλικ",
"Screen": "Οθόνη",
"Orientation": "Προσανατολισμός",
"Portrait": "Πορτραίτο",
"Landscape": "Τοπίο",
"Open Last Book on Start": "Άνοιγμα τελευταίου βιβλίου κατά την εκκίνηση"
"Open Last Book on Start": "Άνοιγμα τελευταίου βιβλίου",
"Checking for updates...": "Έλεγχος για ενημερώσεις...",
"Error checking for updates": "Σφάλμα κατά τον έλεγχο για ενημερώσεις",
"Details": "Πληρ.",
"Cancel Selection": "Ακύρωση επιλογής",
"File Size:": "Μέγεθος αρχείου:"
}
@@ -50,7 +50,6 @@
"Highlight": "Resaltar",
"Horizontal Direction": "Dirección horizontal",
"Hyphenation": "Guionización",
"Identifier:": "Identificador:",
"Import Books": "Importar libros",
"Language:": "Idioma:",
"Layout": "Diseño",
@@ -303,5 +302,10 @@
"Orientation": "Orientación",
"Portrait": "Vertical",
"Landscape": "Horizontal",
"Open Last Book on Start": "Abrir último libro al iniciar"
"Open Last Book on Start": "Abrir último libro al iniciar",
"Checking for updates...": "Comprobando actualizaciones...",
"Error checking for updates": "Error al comprobar actualizaciones",
"Details": "Detalles",
"Cancel Selection": "Cancelar selección",
"File Size:": "Peso del archivo:"
}
@@ -50,7 +50,6 @@
"Highlight": "Surligner",
"Horizontal Direction": "Direction horizontale",
"Hyphenation": "Césure",
"Identifier:": "Identifiant :",
"Import Books": "Importer des livres",
"Language:": "Langue :",
"Layout": "Mise en page",
@@ -303,5 +302,10 @@
"Orientation": "Orientation",
"Portrait": "Portrait",
"Landscape": "Paysage",
"Open Last Book on Start": "Ouvrir le dernier livre au démarrage"
"Open Last Book on Start": "Ouvrir le dernier livre au démarrage",
"Checking for updates...": "Vérification des mises à jour...",
"Error checking for updates": "Erreur lors de la vérification des mises à jour",
"Details": "Détails",
"Cancel Selection": "Annuler la sélection",
"File Size:": "Taille du fichier :"
}
@@ -50,7 +50,6 @@
"Highlight": "हाइलाइट",
"Horizontal Direction": "क्षैतिज दिशा",
"Hyphenation": "हाइफ़नेशन",
"Identifier:": "पहचानकर्ता:",
"Import Books": "पुस्तकें आयात करें",
"Language:": "भाषा:",
"Layout": "लेआउट",
@@ -303,5 +302,10 @@
"Orientation": "अवयव",
"Portrait": "पोर्ट्रेट",
"Landscape": "लैंडस्केप",
"Open Last Book on Start": "शुरू में अंतिम पुस्तक खोलें"
"Open Last Book on Start": "शुरू में अंतिम पुस्तक खोलें",
"Checking for updates...": "अपडेट की जांच कर रहा है...",
"Error checking for updates": "अपडेट की जांच करते समय त्रुटि",
"Details": "विवरण",
"Cancel Selection": "चयन रद्द करें",
"File Size:": "फ़ाइल आकार:"
}
@@ -50,7 +50,6 @@
"Highlight": "Sorot",
"Horizontal Direction": "Arah Horizontal",
"Hyphenation": "Pemenggalan Kata",
"Identifier:": "Pengenal:",
"Import Books": "Impor Buku",
"Language:": "Bahasa:",
"Layout": "Tata Letak",
@@ -303,5 +302,10 @@
"Orientation": "Orientasi",
"Portrait": "Potret",
"Landscape": "Lanskap",
"Open Last Book on Start": "Buka Buku Terakhir saat Memulai"
"Open Last Book on Start": "Buka Buku Terakhir saat Memulai",
"Checking for updates...": "Memeriksa pembaruan...",
"Error checking for updates": "Kesalahan saat memeriksa pembaruan",
"Details": "Detail",
"Cancel Selection": "Batalkan Pilihan",
"File Size:": "Ukuran File:"
}
@@ -50,7 +50,6 @@
"Highlight": "Evidenzia",
"Horizontal Direction": "Direzione orizzontale",
"Hyphenation": "Sillabazione",
"Identifier:": "Identificatore:",
"Import Books": "Importa libri",
"Language:": "Lingua:",
"Layout": "Layout",
@@ -303,5 +302,10 @@
"Orientation": "Orientamento",
"Portrait": "Ritratto",
"Landscape": "Paesaggio",
"Open Last Book on Start": "Apri ultimo libro all'avvio"
"Open Last Book on Start": "Apri ultimo libro all'avvio",
"Checking for updates...": "Controllo aggiornamenti...",
"Error checking for updates": "Errore durante il controllo degli aggiornamenti",
"Details": "Dettagli",
"Cancel Selection": "Annulla selezione",
"File Size:": "Dimensione file:"
}
@@ -50,7 +50,6 @@
"Highlight": "ハイライト",
"Horizontal Direction": "横書",
"Hyphenation": "ハイフネーション",
"Identifier:": "識別子:",
"Import Books": "書籍をインポート",
"Language:": "言語:",
"Layout": "レイアウト",
@@ -303,5 +302,10 @@
"Orientation": "向き",
"Portrait": "縦",
"Landscape": "横",
"Open Last Book on Start": "開始時に最後の書籍を開く"
"Open Last Book on Start": "開始時に最後の書籍を開く",
"Checking for updates...": "更新を確認中...",
"Error checking for updates": "更新の確認中にエラーが発生しました",
"Details": "詳細",
"Cancel Selection": "選択をキャンセル",
"File Size:": "ファイルサイズ:"
}
@@ -50,7 +50,6 @@
"Highlight": "하이라이트",
"Horizontal Direction": "수평 방향",
"Hyphenation": "하이픈 넣기",
"Identifier:": "식별자:",
"Import Books": "책 가져오기",
"Language:": "언어:",
"Layout": "레이아웃",
@@ -303,5 +302,10 @@
"Orientation": "방향",
"Portrait": "세로",
"Landscape": "가로",
"Open Last Book on Start": "시작 시 마지막 책 열기"
"Open Last Book on Start": "시작 시 마지막 책 열기",
"Checking for updates...": "업데이트 확인 중...",
"Error checking for updates": "업데이트 확인 중 오류 발생",
"Details": "세부정보",
"Cancel Selection": "선택 취소",
"File Size:": "파일 크기:"
}
@@ -50,7 +50,6 @@
"Highlight": "Zaznacz",
"Horizontal Direction": "Kierunek poziomy",
"Hyphenation": "Dzielenie wyrazów",
"Identifier:": "Identyfikator:",
"Import Books": "Importuj książki",
"Language:": "Język:",
"Layout": "Układ",
@@ -303,5 +302,10 @@
"Orientation": "Orientacja",
"Portrait": "Portret",
"Landscape": "Krajobraz",
"Open Last Book on Start": "Otwórz ostatnią książkę przy starcie"
"Open Last Book on Start": "Otwórz ostatnią książkę przy starcie",
"Checking for updates...": "Sprawdzanie aktualizacji...",
"Error checking for updates": "Błąd podczas sprawdzania aktualizacji",
"Details": "Szczegóły",
"Cancel Selection": "Anuluj zaznaczenie",
"File Size:": "Rozmiar pliku:"
}
@@ -50,7 +50,6 @@
"Highlight": "Destaque",
"Horizontal Direction": "Direção Horizontal",
"Hyphenation": "Hifenização",
"Identifier:": "Identificador:",
"Import Books": "Importar Livros",
"Language:": "Idioma:",
"Layout": "Layout",
@@ -303,5 +302,10 @@
"Orientation": "Orientação",
"Portrait": "Retrato",
"Landscape": "Paisagem",
"Open Last Book on Start": "Abrir o Último Livro ao Iniciar"
"Open Last Book on Start": "Abrir o Último Livro ao Iniciar",
"Checking for updates...": "Verificando atualizações...",
"Error checking for updates": "Erro ao verificar atualizações",
"Details": "Detalhes",
"Cancel Selection": "Cancelar Seleção",
"File Size:": "Tamanho do arquivo:"
}
@@ -50,7 +50,6 @@
"Highlight": "Выделить",
"Horizontal Direction": "Горизонтальное направление",
"Hyphenation": "Перенос слов",
"Identifier:": "Идентификатор:",
"Import Books": "Импорт книг",
"Language:": "Язык:",
"Layout": "Макет",
@@ -303,5 +302,10 @@
"Orientation": "Ориентация",
"Portrait": "Портрет",
"Landscape": "Ландшафт",
"Open Last Book on Start": "Открыть последнюю книгу при запуске"
"Open Last Book on Start": "Открыть последнюю книгу при запуске",
"Checking for updates...": "Проверка обновлений...",
"Error checking for updates": "Ошибка при проверке обновлений",
"Details": "Инфо",
"Cancel Selection": "Отменить выбор",
"File Size:": "Размер файла:"
}
@@ -50,7 +50,6 @@
"Highlight": "Vurgula",
"Horizontal Direction": "Yatay Yön",
"Hyphenation": "Heceleme",
"Identifier:": "Tanımlayıcı:",
"Import Books": "Kitapları İçe Aktar",
"Language:": "Dil:",
"Layout": "Düzen",
@@ -303,5 +302,10 @@
"Orientation": "Yönlendirme",
"Portrait": "Portre",
"Landscape": "Yatay",
"Open Last Book on Start": "Açılışta Son Kitabı Aç"
"Open Last Book on Start": "Açılışta Son Kitabı Aç",
"Checking for updates...": "Yenilikler kontrol ediliyor...",
"Error checking for updates": "Güncellemeleri kontrol ederken hata oluştu",
"Details": "Detaylar",
"Cancel Selection": "Işaretlemeyi İptal Et",
"File Size:": "Dosya Boyutu:"
}
@@ -50,7 +50,6 @@
"Highlight": "Виділення",
"Horizontal Direction": "Горизонтальний напрямок",
"Hyphenation": "Перенесення слів",
"Identifier:": "Ідентифікатор:",
"Import Books": "Імпортувати книги",
"Language:": "Мова:",
"Layout": "Макет",
@@ -303,5 +302,10 @@
"Orientation": "Орієнтація",
"Portrait": "Портрет",
"Landscape": "Ландшафт",
"Open Last Book on Start": "Відкрити останню книгу при запуску"
"Open Last Book on Start": "Відкрити останню книгу при запуску",
"Checking for updates...": "Перевірка наявності оновлень...",
"Error checking for updates": "Помилка перевірки наявності оновлень",
"Details": "Деталі",
"Cancel Selection": "Скасувати вибір",
"File Size:": "Розмір файлу:"
}
@@ -50,7 +50,6 @@
"Highlight": "Đánh dấu",
"Horizontal Direction": "Hướng ngang",
"Hyphenation": "Gạch nối từ",
"Identifier:": "Định danh:",
"Import Books": "Nhập sách",
"Language:": "Ngôn ngữ:",
"Layout": "Bố cục",
@@ -303,5 +302,10 @@
"Orientation": "Định hướng",
"Portrait": "Chân dung",
"Landscape": "Phong cảnh",
"Open Last Book on Start": "Mở sách cuối cùng khi khởi động"
"Open Last Book on Start": "Mở sách cuối cùng khi khởi động",
"Checking for updates...": "Đang kiểm tra cập nhật...",
"Error checking for updates": "Lỗi khi kiểm tra cập nhật",
"Details": "Chi tiết",
"Cancel Selection": "Hủy chọn",
"File Size:": "Kích thước tệp:"
}
@@ -50,7 +50,6 @@
"Highlight": "划线",
"Horizontal Direction": "横排",
"Hyphenation": "断字",
"Identifier:": "识别码",
"Import Books": "导入书籍",
"Language:": "语言",
"Layout": "布局",
@@ -303,5 +302,10 @@
"Orientation": "屏幕方向",
"Portrait": "竖屏",
"Landscape": "横屏",
"Open Last Book on Start": "启动时打开上次阅读的书籍"
"Open Last Book on Start": "启动时打开上次阅读的书籍",
"Checking for updates...": "检查更新中...",
"Error checking for updates": "检查更新时出错",
"Details": "详情",
"Cancel Selection": "取消选择",
"File Size:": "文件大小"
}
@@ -50,7 +50,6 @@
"Highlight": "劃線",
"Horizontal Direction": "橫排",
"Hyphenation": "斷字",
"Identifier:": "識別碼",
"Import Books": "導入書籍",
"Language:": "語言",
"Layout": "版面",
@@ -303,5 +302,10 @@
"Orientation": "螢幕方向",
"Portrait": "豎屏",
"Landscape": "橫屏",
"Open Last Book on Start": "啟動時打開上次閱讀的書籍"
"Open Last Book on Start": "啟動時打開上次閱讀的書籍",
"Checking for updates...": "檢查更新中...",
"Error checking for updates": "檢查更新時出錯",
"Details": "詳情",
"Cancel Selection": "取消選擇",
"File Size:": "檔案大小"
}
+25
View File
@@ -1,5 +1,30 @@
{
"releases": {
"0.9.41": {
"date": "2025-05-13",
"notes": [
"Improved UI and layout on mobile devices",
"Avatar images are now cached for offline use",
"Book file size is now displayed in the book details view",
"Update status is now correctly shown in the About window",
"TTS control positioning improved for notebook and RTL modes",
"TXT files are segmented by paragraph count if no chapter titles are found",
"Android: App now respects system auto-rotation settings",
"Linux: Updater is disabled on Linux when not using AppImage",
"Linux: Added support for Linux ARMHF builds",
"CSS: Resolved layout and color issues caused by hardcoded CSS"
]
},
"0.9.40": {
"date": "2025-05-08",
"notes": [
"Prevented oversized images from causing layout issues",
"Resolved issue with transient navigation bar on Android 9",
"Action bar now closes properly when the footbar is dismissed",
"Smoother experience when expanding Table of Contents sections",
"Reverted unintended style overrides"
]
},
"0.9.39": {
"date": "2025-05-07",
"notes": [
+2 -2
View File
@@ -31,9 +31,9 @@ reqwest = { version = "0.12", default-features = false, features = [
"stream",
] }
# FIXME: remove the devtools feature in production
tauri = { version = "2.4.1", features = [ "protocol-asset", "devtools"] }
tauri = { version = "2.5.1", features = [ "protocol-asset", "devtools"] }
tauri-build = "2.1.1"
tauri-plugin-log = "2.3.1"
tauri-plugin-log = "2.4.0"
tauri-plugin-fs = "2.2.1"
tauri-plugin-dialog = "2.2.1"
tauri-plugin-os = "2.2.1"
@@ -225,7 +225,7 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
controller.hide(WindowInsets.Type.systemBars())
}
}
} else {
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val compatController = WindowCompat.getInsetsController(window, decorView)
compatController?.let {
it.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
@@ -240,6 +240,21 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
it.hide(WindowInsetsCompat.Type.systemBars())
}
}
} 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
@@ -340,7 +355,7 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
when (orientation) {
"portrait" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
"landscape" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
"auto" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
"auto" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
else -> {
invoke.reject("Invalid orientation mode")
return
+15 -3
View File
@@ -191,9 +191,8 @@ pub fn run() {
let app_handle = app.handle().clone();
app.listen("window-ready", move |_| {
app_handle
.get_webview_window("main")
.unwrap()
let webview = app_handle.get_webview_window("main").unwrap();
webview
.eval("window.__READEST_CLI_ACCESS = true;")
.expect("Failed to set cli access config");
@@ -204,6 +203,19 @@ pub fn run() {
{
set_rounded_window(&app_handle, false);
}
#[cfg(target_os = "linux")]
{
let is_appimage = std::env::var("APPIMAGE").is_ok()
|| std::env::current_exe()
.map(|path| path.to_string_lossy().contains("/tmp/.mount_"))
.unwrap_or(false);
let script =
format!("window.__READEST_UPDATER_DISABLED = {};", !is_appimage);
webview
.eval(&script)
.expect("Failed to set updater disabled config");
}
});
}
+3 -3
View File
@@ -78,8 +78,8 @@ export default function AuthPage() {
useTheme({ systemUIVisible: false });
const getTauriRedirectTo = (isOAuth: boolean) => {
if (process.env.NODE_ENV === 'production' || appService?.isMobile || USE_APPLE_SIGN_IN) {
if (appService?.isMobile) {
if (process.env.NODE_ENV === 'production' || appService?.isMobileApp || USE_APPLE_SIGN_IN) {
if (appService?.isMobileApp) {
return isOAuth ? DEEPLINK_CALLBACK : WEB_AUTH_CALLBACK;
}
return DEEPLINK_CALLBACK;
@@ -172,7 +172,7 @@ export default function AuthPage() {
const startTauriOAuth = async () => {
try {
if (process.env.NODE_ENV === 'production' || appService?.isMobile || USE_APPLE_SIGN_IN) {
if (process.env.NODE_ENV === 'production' || appService?.isMobileApp || USE_APPLE_SIGN_IN) {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
currentWindow.listen('single-instance', ({ event, payload }) => {
@@ -100,7 +100,7 @@ const BookItem: React.FC<BookItemProps> = ({
{transferProgress !== null ? (
transferProgress === 100 ? null : (
<div
className='radial-progress opacity-0 group-hover:opacity-100'
className='radial-progress sm:opacity-0 sm:group-hover:opacity-100'
style={
{
'--value': transferProgress,
@@ -113,7 +113,7 @@ const BookItem: React.FC<BookItemProps> = ({
)
) : (
<button
className='show-detail-button opacity-0 group-hover:opacity-100'
className='show-detail-button -m-2 p-2 sm:opacity-0 sm:group-hover:opacity-100'
onPointerDown={(e) => stopEvent(e)}
onPointerUp={(e) => stopEvent(e)}
onPointerMove={(e) => stopEvent(e)}
@@ -133,17 +133,19 @@ const BookItem: React.FC<BookItemProps> = ({
)}
</button>
)}
<button
className='show-detail-button opacity-0 group-hover:opacity-100'
onPointerDown={(e) => stopEvent(e)}
onPointerUp={(e) => stopEvent(e)}
onPointerMove={(e) => stopEvent(e)}
onPointerCancel={(e) => stopEvent(e)}
onPointerLeave={(e) => stopEvent(e)}
onClick={() => showBookDetailsModal(book)}
>
<CiCircleMore size={iconSize15} />
</button>
{!appService?.isMobile && (
<button
className='show-detail-button -m-2 p-2 sm:opacity-0 sm:group-hover:opacity-100'
onPointerDown={(e) => stopEvent(e)}
onPointerUp={(e) => stopEvent(e)}
onPointerMove={(e) => stopEvent(e)}
onPointerCancel={(e) => stopEvent(e)}
onPointerLeave={(e) => stopEvent(e)}
onClick={() => showBookDetailsModal(book)}
>
<CiCircleMore size={iconSize15} />
</button>
)}
</div>
</div>
</div>
@@ -2,7 +2,7 @@ import clsx from 'clsx';
import * as React from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { MdDelete, MdOpenInNew, MdOutlineCancel } from 'react-icons/md';
import { MdDelete, MdOpenInNew, MdOutlineCancel, MdInfoOutline } from 'react-icons/md';
import { LuFolderPlus } from 'react-icons/lu';
import { PiPlus } from 'react-icons/pi';
import { Book, BooksGroup } from '@/types/book';
@@ -162,6 +162,13 @@ const Bookshelf: React.FC<BookshelfProps> = ({
navigateToReader(router, selectedBooks);
};
const openBookDetails = () => {
const book = libraryBooks.find((book) => book.hash === selectedBooks[0]);
if (book) {
handleShowDetailsBook(book);
}
};
const confirmDelete = async () => {
selectedBooks.forEach((id) => {
for (const book of libraryBooks.filter((book) => book.hash === id || book.groupId === id)) {
@@ -290,14 +297,14 @@ const Bookshelf: React.FC<BookshelfProps> = ({
<div
className={clsx(
'flex items-center justify-center shadow-lg',
'text-base-content bg-base-300 text-sm',
'bg-gray-600 text-xs text-white',
'mx-auto w-fit space-x-6 rounded-lg p-4',
)}
>
<button
onClick={openSelectedBooks}
className={clsx(
'flex flex-col items-center justify-center',
'flex flex-col items-center justify-center gap-1',
(!selectedBooks.length || !selectedBooks.every((id) => isMd5(id))) &&
'btn-disabled opacity-50',
)}
@@ -308,30 +315,43 @@ const Bookshelf: React.FC<BookshelfProps> = ({
<button
onClick={groupSelectedBooks}
className={clsx(
'flex flex-col items-center justify-center',
'flex flex-col items-center justify-center gap-1',
!selectedBooks.length && 'btn-disabled opacity-50',
)}
>
<LuFolderPlus />
<div>{_('Group')}</div>
</button>
<button
onClick={openBookDetails}
className={clsx(
'flex flex-col items-center justify-center gap-1',
(selectedBooks.length !== 1 || !selectedBooks.every((id) => isMd5(id))) &&
'btn-disabled opacity-50',
)}
>
<MdInfoOutline />
<div>{_('Details')}</div>
</button>
<button
onClick={deleteSelectedBooks}
className={clsx(
'flex flex-col items-center justify-center',
'flex flex-col items-center justify-center gap-1',
!selectedBooks.length && 'btn-disabled opacity-50',
)}
>
<MdDelete className='fill-red-500' />
<div className='text-red-500'>{_('Delete')}</div>
</button>
<button
onClick={() => handleSetSelectMode(false)}
className={clsx('flex flex-col items-center justify-center')}
>
<MdOutlineCancel />
<div>{_('Cancel')}</div>
</button>
{!appService?.isMobile && (
<button
onClick={() => handleSetSelectMode(false)}
className={clsx('flex flex-col items-center justify-center gap-1')}
>
<MdOutlineCancel />
<div>{_('Cancel')}</div>
</button>
)}
</div>
)}
</div>
@@ -1,3 +1,5 @@
import clsx from 'clsx';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import MenuItem from '@/components/MenuItem';
@@ -8,6 +10,7 @@ interface ImportMenuProps {
const ImportMenu: React.FC<ImportMenuProps> = ({ setIsDropdownOpen, onImportBooks }) => {
const _ = useTranslation();
const { appService } = useEnv();
const handleImportBooks = () => {
onImportBooks();
@@ -17,7 +20,10 @@ const ImportMenu: React.FC<ImportMenuProps> = ({ setIsDropdownOpen, onImportBook
return (
<ul
tabIndex={-1}
className='dropdown-content dropdown-center bg-base-100 menu rounded-box z-[1] mt-3 w-52 p-2 shadow'
className={clsx(
'dropdown-content bg-base-100 menu rounded-box z-[1] mt-3 w-52 p-2 shadow',
appService?.isMobile ? 'no-triangle' : 'dropdown-center',
)}
>
<MenuItem label={_('From Local File')} onClick={handleImportBooks} />
</ul>
@@ -142,7 +142,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
setSearchQuery('');
throttledUpdateQueryParam('');
}}
className='text-gray-400 hover:text-gray-600'
className='pe-1 text-gray-400 hover:text-gray-600'
aria-label={_('Clear Search')}
>
<IoMdCloseCircle className='h-4 w-4' />
@@ -150,7 +150,10 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
)}
<span className='bg-base-content/50 mx-2 h-4 w-[0.5px]'></span>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center'
className={clsx(
'exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center',
appService?.isMobile ? 'dropdown-end' : 'dropdown-center',
)}
buttonClassName='p-0 h-6 min-h-6 w-6 flex items-center justify-center'
toggleButton={
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Import Books')}>
@@ -160,47 +163,61 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
>
<ImportMenu onImportBooks={onImportBooks} />
</Dropdown>
<button
onClick={onToggleSelectMode}
aria-label={_('Select Multiple Books')}
className='h-6'
>
<div
className='lg:tooltip lg:tooltip-bottom cursor-pointer'
data-tip={_('Select Books')}
{appService?.isMobile ? null : (
<button
onClick={onToggleSelectMode}
aria-label={_('Select Multiple Books')}
className='h-6'
>
<PiSelectionAllDuotone
role='button'
className={`h-6 w-6 ${isSelectMode ? 'fill-gray-400' : 'fill-gray-500'}`}
/>
</div>
</button>
<div
className='lg:tooltip lg:tooltip-bottom cursor-pointer'
data-tip={_('Select Books')}
>
<PiSelectionAllDuotone
role='button'
className={`h-6 w-6 ${isSelectMode ? 'fill-gray-400' : 'fill-gray-500'}`}
/>
</div>
</button>
)}
</div>
</div>
<div className='flex h-full items-center gap-x-2 sm:gap-x-4'>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeCircle size={iconSize18} />}
>
<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={<MdOutlineMenu size={iconSize18} />}
>
<SettingsMenu />
</Dropdown>
{appService?.hasWindowBar && (
<WindowButtons
headerRef={headerRef}
showMinimize={windowButtonVisible}
showMaximize={windowButtonVisible}
showClose={windowButtonVisible}
/>
)}
</div>
{appService?.isMobile && isSelectMode ? (
<div className='w-max-[72px] w-min-[72px] flex h-full items-center'>
<button
onClick={onToggleSelectMode}
className='btn btn-ghost text-base-content/85 h-8 min-h-8 w-[72px] p-0'
aria-label={_('Cancel Selection')}
>
{_('Cancel')}
</button>
</div>
) : (
<div className='flex h-full items-center gap-x-2 sm:gap-x-4'>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeCircle size={iconSize18} />}
>
<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={<MdOutlineMenu size={iconSize18} />}
>
<SettingsMenu />
</Dropdown>
{appService?.hasWindowBar && (
<WindowButtons
headerRef={headerRef}
showMinimize={windowButtonVisible}
showMaximize={windowButtonVisible}
showClose={windowButtonVisible}
/>
)}
</div>
)}
</div>
</div>
);
@@ -1,3 +1,4 @@
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { PiUserCircle } from 'react-icons/pi';
@@ -134,7 +135,6 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
const isWebApp = isWebAppPlatform();
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
const userFullName = user?.user_metadata?.['full_name'];
const userDisplayName = userFullName ? userFullName.split(' ')[0] : null;
@@ -142,7 +142,10 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
return (
<div
tabIndex={0}
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'
className={clsx(
'settings-menu dropdown-content no-triangle border-base-100',
'z-20 mt-2 max-w-[90vw] shadow-2xl',
)}
>
{user ? (
<MenuItem
@@ -180,11 +183,13 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
onClick={toggleAutoImportBooksOnOpen}
/>
)}
<MenuItem
label={_('Open Last Book on Start')}
Icon={isOpenLastBooks ? MdCheck : undefined}
onClick={toggleOpenLastBooks}
/>
{isTauriAppPlatform() && (
<MenuItem
label={_('Open Last Book on Start')}
Icon={isOpenLastBooks ? MdCheck : undefined}
onClick={toggleOpenLastBooks}
/>
)}
{appService?.hasUpdater && (
<MenuItem
label={_('Check Updates on Start')}
@@ -208,7 +213,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
/>
<MenuItem label={_('Reload Page')} onClick={handleReloadPage} />
<hr className='border-base-200 my-1' />
{isWebApp && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
{isWebAppPlatform() && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
<MenuItem label={_('About Readest')} onClick={showAboutReadest} />
</div>
);
+33 -28
View File
@@ -78,6 +78,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const [booksTransferProgress, setBooksTransferProgress] = useState<{
[key: string]: number | null;
}>({});
const [pendingNavigationBookIds, setPendingNavigationBookIds] = useState<string[] | null>(null);
const [isDragging, setIsDragging] = useState(false);
const demoBooks = useDemoBooks();
const containerRef = useRef<HTMLDivElement>(null);
@@ -121,7 +122,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
useEffect(() => {
if (appService?.isMobileApp) {
lockScreenOrientation({ orientation: 'portrait' });
lockScreenOrientation({ orientation: 'auto' });
}
}, [appService]);
@@ -231,10 +232,10 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
console.log('Opening books:', bookIds);
if (bookIds.length > 0) {
setTimeout(() => {
navigateToReader(router, bookIds);
}, 0);
setPendingNavigationBookIds(bookIds);
return true;
}
return false;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
@@ -245,21 +246,31 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
lastBookIds: string[],
libraryBooks: Book[],
) => {
if (lastBookIds.length === 0) return;
if (lastBookIds.length === 0) return false;
const bookIds: string[] = [];
for (const bookId of lastBookIds) {
const book = libraryBooks.find((b) => b.hash === bookId);
if (book && (await appService.isBookAvailable(book))) {
bookIds.push(book.hash);
}
console.log('Opening last books:', bookIds);
}
console.log('Opening last books:', bookIds);
if (bookIds.length > 0) {
setPendingNavigationBookIds(bookIds);
return true;
}
return false;
};
useEffect(() => {
if (pendingNavigationBookIds) {
const bookIds = pendingNavigationBookIds;
setPendingNavigationBookIds(null);
if (bookIds.length > 0) {
setTimeout(() => {
navigateToReader(router, bookIds);
}, 0);
navigateToReader(router, bookIds);
}
}
};
}, [pendingNavigationBookIds, router]);
useEffect(() => {
if (isInitiating.current) return;
@@ -285,34 +296,28 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const settings = await appService.loadSettings();
setSettings(settings);
const libraryBooks = await appService.loadLibraryBooks();
if (checkOpenWithBooks) {
await handleOpenWithBooks(appService, libraryBooks);
} else {
setCheckOpenWithBooks(false);
setLibrary(libraryBooks);
}
if (checkLastOpenBooks && settings.openLastBooks) {
await handleOpenLastBooks(appService, settings.lastOpenBooks, libraryBooks);
} else {
setCheckLastOpenBooks(false);
}
// Reuse the library from the store when we return from the reader
const library = libraryBooks.length > 0 ? libraryBooks : await appService.loadLibraryBooks();
setCheckOpenWithBooks(checkOpenWithBooks && (await handleOpenWithBooks(appService, library)));
setCheckLastOpenBooks(
checkLastOpenBooks &&
settings.openLastBooks &&
(await handleOpenLastBooks(appService, settings.lastOpenBooks, library)),
);
setLibrary(library);
setLibraryLoaded(true);
if (loadingTimeout) clearTimeout(loadingTimeout);
setLoading(false);
};
const handleOpenWithBooks = async (appService: AppService, libraryBooks: Book[]) => {
const handleOpenWithBooks = async (appService: AppService, library: Book[]) => {
const openWithFiles = (await parseOpenWithFiles()) || [];
if (openWithFiles.length > 0) {
await processOpenWithFiles(appService, openWithFiles, libraryBooks);
} else {
setCheckOpenWithBooks(false);
setLibrary(libraryBooks);
return await processOpenWithFiles(appService, openWithFiles, library);
}
return false;
};
initLogin();
+12 -5
View File
@@ -1,7 +1,14 @@
import { redirectToLibrary } from '@/utils/nav';
'use client';
const HomePage = () => {
redirectToLibrary();
};
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
export default HomePage;
export default function HomePage() {
const router = useRouter();
useEffect(() => {
router.replace('/library');
}, [router]);
return null;
}
@@ -18,6 +18,7 @@ import Annotator from './annotator/Annotator';
import FootnotePopup from './FootnotePopup';
import HintInfo from './HintInfo';
import DoubleBorder from './DoubleBorder';
import TTSControl from './tts/TTSControl';
interface BooksGridProps {
bookKeys: string[];
@@ -42,7 +43,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
return (
<div
className={clsx('grid h-full flex-grow')}
className={clsx('relative grid h-full flex-grow')}
style={{
gridTemplateColumns: gridTemplate.columns,
gridTemplateRows: gridTemplate.rows,
@@ -161,6 +162,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
</div>
);
})}
<TTSControl />
</div>
);
};
@@ -42,7 +42,6 @@ const FoliateViewer: React.FC<{
const { getViewSettings, setViewSettings } = useReaderStore();
const { getParallels } = useParallelViewStore();
const { themeCode, isDarkMode } = useThemeStore();
const viewSettings = getViewSettings(bookKey)!;
const [toastMessage, setToastMessage] = useState('');
useEffect(() => {
@@ -50,7 +49,7 @@ const FoliateViewer: React.FC<{
return () => clearTimeout(timer);
}, [toastMessage]);
useUICSS(bookKey, viewSettings);
useUICSS(bookKey);
useProgressSync(bookKey);
useProgressAutoSave(bookKey);
@@ -72,7 +71,7 @@ const FoliateViewer: React.FC<{
bookKey,
viewSettings,
content: data,
transformers: ['punctuation'],
transformers: ['punctuation', 'footnote'],
};
return Promise.resolve(transformContent(ctx));
}
@@ -170,6 +169,7 @@ const FoliateViewer: React.FC<{
const width = containerRect?.width || window.innerWidth;
const height = containerRect?.height || window.innerHeight;
const viewSettings = getViewSettings(bookKey)!;
const writingMode = viewSettings.writingMode;
if (writingMode) {
const settingsDir = getBookDirFromWritingMode(writingMode);
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import clsx from 'clsx';
import { RiArrowLeftWideLine, RiArrowRightWideLine } from 'react-icons/ri';
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
@@ -122,6 +122,12 @@ const FooterBar: React.FC<FooterBarProps> = ({
}
};
useEffect(() => {
if (hoveredBookKey !== bookKey) {
setActionTab('');
}
}, [hoveredBookKey, bookKey]);
const getMarginProgressValue = (marginPx: number, gapPercent: number) => {
return (marginPx / 88 + gapPercent / 10) * 50;
};
@@ -94,7 +94,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
}, []);
useEffect(() => {
if (!appService?.isMobile) return;
if (!appService?.isMobileApp) return;
const systemUIVisible = !!hoveredBookKey;
setSystemUIVisibility({ visible: systemUIVisible, darkMode: isDarkMode });
if (systemUIVisible) {
@@ -27,7 +27,6 @@ import Spinner from '@/components/Spinner';
import SideBar from './sidebar/SideBar';
import Notebook from './notebook/Notebook';
import BooksGrid from './BooksGrid';
import TTSControl from './tts/TTSControl';
const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ ids, settings }) => {
const router = useRouter();
@@ -166,7 +165,6 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
<div className={clsx('flex', appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh')}>
<SideBar onGoToLibrary={handleCloseBooksToLibrary} />
<BooksGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
<TTSControl />
<Notebook />
{showDetailsBook && (
<BookDetailModal
@@ -1,14 +1,17 @@
import React from 'react';
import { RiFontSize } from 'react-icons/ri';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import Button from '@/components/Button';
const SettingsToggler = () => {
const _ = useTranslation();
const { setHoveredBookKey } = useReaderStore();
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
const handleToggleSettings = () => {
setHoveredBookKey('');
setFontLayoutSettingsDialogOpen(!isFontLayoutSettingsDialogOpen);
};
return (
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect } from 'react';
import { FiSearch } from 'react-icons/fi';
import { FiCopy } from 'react-icons/fi';
import { PiHighlighterFill } from 'react-icons/pi';
@@ -22,16 +22,10 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useFoliateEvents } from '../../hooks/useFoliateEvents';
import { useNotesSync } from '../../hooks/useNotesSync';
import {
getPopupPosition,
getPosition,
getTextFromRange,
Position,
TextSelection,
} from '@/utils/sel';
import { useTextSelector } from '../../hooks/useTextSelector';
import { getPopupPosition, getPosition, Position, TextSelection } from '@/utils/sel';
import { eventDispatcher } from '@/utils/event';
import { findTocItemBS } from '@/utils/toc';
import { transformContent } from '@/services/transformService';
import { HIGHLIGHT_COLOR_HEX } from '@/services/constants';
import AnnotationPopup from './AnnotationPopup';
import WiktionaryPopup from './WiktionaryPopup';
@@ -54,15 +48,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const bookData = getBookData(bookKey)!;
const view = getView(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const primaryLang = bookData.book?.primaryLanguage || 'en';
const isShowingPopup = useRef(false);
const isTextSelected = useRef(false);
const isUpToShowPopup = useRef(false);
const isTouchstarted = useRef(false);
const selectionStartRef = useRef<number | null>(null);
const selectionAnchorRef = useRef<{ node: Node; offset: number } | null>(null);
const [selection, setSelection] = useState<TextSelection | null>();
const [selection, setSelection] = useState<TextSelection | null>(null);
const [showAnnotPopup, setShowAnnotPopup] = useState(false);
const [showWiktionaryPopup, setShowWiktionaryPopup] = useState(false);
const [showWikipediaPopup, setShowWikipediaPopup] = useState(false);
@@ -91,137 +78,41 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const annotPopupHeight = useResponsiveSize(44);
const androidSelectionHandlerHeight = 0;
const handleDismissPopup = () => {
setSelection(null);
setShowAnnotPopup(false);
setShowWiktionaryPopup(false);
setShowWikipediaPopup(false);
setShowDeepLPopup(false);
};
const handleDismissPopupAndSelection = () => {
handleDismissPopup();
view?.deselect();
};
const {
handleScroll,
handlePointerup,
handleSelectionchange,
handleAnnotPopup,
handleShowAnnotation,
} = useTextSelector(bookKey, setSelection, handleDismissPopup);
const onLoad = (event: Event) => {
const detail = (event as CustomEvent).detail;
const { doc, index } = detail;
const isValidSelection = (sel: Selection) => {
return sel && sel.toString().trim().length > 0 && sel.rangeCount > 0;
};
const transformCtx = {
bookKey,
viewSettings: getViewSettings(bookKey)!,
content: '',
transformers: ['punctuation'],
reversePunctuationTransform: true,
};
const getAnnotationText = (range: Range) => {
return getTextFromRange(range, primaryLang.startsWith('ja') ? ['rt'] : []);
};
const makeSelection = async (sel: Selection, rebuildRange = false) => {
isTextSelected.current = true;
const range = sel.getRangeAt(0);
if (rebuildRange) {
sel.removeAllRanges();
sel.addRange(range);
}
transformCtx['content'] = getAnnotationText(range);
setSelection({ key: bookKey, text: await transformContent(transformCtx), range, index });
};
// FIXME: extremely hacky way to dismiss system selection tools on iOS
const makeSelectionOnIOS = async (sel: Selection) => {
isTextSelected.current = true;
const range = sel.getRangeAt(0);
setTimeout(() => {
sel.removeAllRanges();
setTimeout(async () => {
if (!isTextSelected.current) return;
sel.addRange(range);
transformCtx['content'] = getAnnotationText(range);
setSelection({ key: bookKey, text: await transformContent(transformCtx), range, index });
}, 40);
}, 0);
};
const handleSelectstart = () => {
selectionAnchorRef.current = null;
};
const handleSelectionchange = () => {
// Available on iOS, Android and Desktop, fired when the selection is changed
// Ideally the popup only shows when the selection is done,
// but on Android no proper events are fired to notify selection done or I didn't find it,
// we make the popup show when the selection is changed
if (osPlatform === 'ios' || appService?.isIOSApp) return;
const sel = doc.getSelection();
if (isValidSelection(sel)) {
if (osPlatform === 'android' && isTouchstarted.current) {
if (!selectionAnchorRef.current) {
const range = sel.getRangeAt(0);
selectionAnchorRef.current = {
node: range.startContainer,
offset: range.startOffset,
};
}
if (selectionAnchorRef.current) {
const currentRange = sel.getRangeAt(0);
if (
currentRange.startContainer !== selectionAnchorRef.current.node ||
currentRange.startOffset !== selectionAnchorRef.current.offset
) {
const newRange = doc.createRange();
newRange.setStart(selectionAnchorRef.current.node, selectionAnchorRef.current.offset);
newRange.setEnd(currentRange.endContainer, currentRange.endOffset);
sel.removeAllRanges();
sel.addRange(newRange);
}
}
makeSelection(sel, false);
}
} else if (!isUpToShowPopup.current) {
isTextSelected.current = false;
setShowAnnotPopup(false);
setShowWiktionaryPopup(false);
setShowWikipediaPopup(false);
setShowDeepLPopup(false);
}
};
const handlePointerup = () => {
// Available on iOS and Desktop, fired when release the long press
// Note that on Android, pointerup event is fired after an additional touch event
const sel = doc.getSelection();
if (isValidSelection(sel)) {
if (osPlatform === 'ios' || appService?.isIOSApp) {
makeSelectionOnIOS(sel);
} else {
makeSelection(sel, true);
}
}
};
const handleTouchstart = () => {
// Available on iOS and Android for the initial touch event
isTouchstarted.current = true;
};
const handleTouchmove = () => {
// Available on iOS, on Android not fired
// To make the popup not to follow the selection
setShowAnnotPopup(false);
};
const handleTouchend = () => {
// Available on iOS, on Android fired after an additional touch event
isTouchstarted.current = false;
};
const handleScroll = () => {
// Prevent the container from scrolling when text is selected in paginated mode
// This is a workaround for the issue #873
// TODO: support text selection across pages
if (
!viewSettings?.scrolled &&
isTextSelected.current &&
view?.renderer?.containerPosition &&
selectionStartRef.current
) {
view.renderer.containerPosition = selectionStartRef.current;
}
};
if (bookData.book?.format !== 'PDF') {
view?.renderer?.addEventListener('scroll', handleScroll);
detail.doc?.addEventListener('pointerup', handlePointerup);
detail.doc?.addEventListener('touchstart', handleTouchstart);
detail.doc?.addEventListener('touchmove', handleTouchmove);
detail.doc?.addEventListener('touchend', handleTouchend);
detail.doc?.addEventListener('selectstart', handleSelectstart);
detail.doc?.addEventListener('selectionchange', handleSelectionchange);
detail.doc?.addEventListener('pointerup', () => handlePointerup(doc, index));
detail.doc?.addEventListener('selectionchange', () => handleSelectionchange(doc, index));
// Disable the default context menu on mobile devices,
// although it should but doesn't work on iOS
@@ -269,60 +160,22 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const annotation = annotations.find((annotation) => annotation.cfi === cfi);
if (!annotation) return;
const selection = { key: bookKey, annotated: true, text: annotation.text ?? '', range, index };
isUpToShowPopup.current = true;
setSelectedStyle(annotation.style!);
setSelectedColor(annotation.color!);
setSelection(selection);
handleShowAnnotation();
};
useFoliateEvents(view, { onLoad, onDrawAnnotation, onShowAnnotation });
const handleDismissPopup = () => {
setSelection(null);
setShowAnnotPopup(false);
setShowWiktionaryPopup(false);
setShowWikipediaPopup(false);
setShowDeepLPopup(false);
isShowingPopup.current = false;
};
const handleDismissPopupAndSelection = () => {
handleDismissPopup();
view?.deselect();
isTextSelected.current = false;
};
useEffect(() => {
if (isTextSelected.current && !selectionStartRef.current) {
selectionStartRef.current = view?.renderer?.start || null;
} else if (!isTextSelected.current) {
selectionStartRef.current = null;
selectionAnchorRef.current = null;
}
handleAnnotPopup(showAnnotPopup);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isTextSelected.current]);
}, [showAnnotPopup]);
useEffect(() => {
const handleSingleClick = (): boolean => {
if (isUpToShowPopup.current) {
isUpToShowPopup.current = false;
return true;
}
if (isTextSelected.current) {
handleDismissPopupAndSelection();
return true;
}
if (showAnnotPopup || isShowingPopup.current) {
handleDismissPopup();
return true;
}
return false;
};
eventDispatcher.onSync('iframe-single-click', handleSingleClick);
eventDispatcher.on('export-annotations', handleExportMarkdown);
return () => {
eventDispatcher.offSync('iframe-single-click', handleSingleClick);
eventDispatcher.off('export-annotations', handleExportMarkdown);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -342,7 +195,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
viewSettings.vertical ? annotPopupWidth : annotPopupHeight,
popupPadding,
);
if (isTextSelected.current && annotPopupPos.dir === 'down' && osPlatform === 'android') {
if (annotPopupPos.dir === 'down' && osPlatform === 'android') {
triangPos.point.y += androidSelectionHandlerHeight;
annotPopupPos.point.y += androidSelectionHandlerHeight;
}
@@ -366,7 +219,6 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
setDictPopupPosition(dictPopupPos);
setTranslatorPopupPosition(transPopupPos);
setTrianglePosition(triangPos);
isShowingPopup.current = true;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selection, bookKey]);
@@ -16,11 +16,11 @@ const NotebookHeader: React.FC<{
const iconSize14 = useResponsiveSize(14);
return (
<div className='notebook-header relative flex h-11 items-center px-3' dir='ltr'>
<div className='absolute inset-0 flex items-center justify-center space-x-2'>
<div className='absolute inset-0 z-[-1] flex items-center justify-center space-x-2'>
<LuNotebookPen />
<div className='notebook-title hidden text-sm font-medium sm:flex'>{_('Notebook')}</div>
</div>
<div className='z-10 flex items-center gap-x-4'>
<div className='flex w-full items-center gap-x-4'>
<button
onClick={handleTogglePin}
className={clsx(
@@ -36,6 +36,8 @@ const NotebookHeader: React.FC<{
>
<MdArrowBackIosNew />
</button>
</div>
<div className='flex items-center justify-end gap-x-4'>
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
<FiSearch />
</button>
@@ -360,7 +360,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
onChange={setParagraphMargin}
min={0}
max={4}
step={0.5}
step={0.2}
/>
<NumberInput
label={_('Line Spacing')}
@@ -1,6 +1,7 @@
import clsx from 'clsx';
import React, { useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { BookConfig } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { RiFontSize } from 'react-icons/ri';
@@ -28,7 +29,10 @@ type TabConfig = {
const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ bookKey }) => {
const _ = useTranslation();
const { appService } = useEnv();
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
const tabsRef = useRef<HTMLDivElement | null>(null);
const [showTabLabels, setShowTabLabels] = useState(false);
const [activePanel, setActivePanel] = useState<SettingsPanelType>(
(localStorage.getItem('lastConfigPanel') || 'Font') as SettingsPanelType,
);
@@ -66,14 +70,56 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
setFontLayoutSettingsDialogOpen(false);
};
useEffect(() => {
const container = tabsRef.current;
if (!container) return;
const checkButtonWidths = () => {
const threshold = (container.clientWidth - 64) * 0.3;
const hideLabel = Array.from(container.querySelectorAll('button')).some((button) => {
const labelSpan = button.querySelector('span');
const labelText = labelSpan?.textContent || '';
const clone = button.cloneNode(true) as HTMLButtonElement;
clone.style.position = 'absolute';
clone.style.visibility = 'hidden';
clone.style.width = 'auto';
const cloneSpan = clone.querySelector('span');
if (cloneSpan) {
cloneSpan.classList.remove('hidden');
cloneSpan.textContent = labelText;
}
document.body.appendChild(clone);
const fullWidth = clone.scrollWidth;
document.body.removeChild(clone);
return fullWidth > threshold;
});
setShowTabLabels(!hideLabel);
};
checkButtonWidths();
const resizeObserver = new ResizeObserver(checkButtonWidths);
resizeObserver.observe(container);
const mutationObserver = new MutationObserver(checkButtonWidths);
mutationObserver.observe(container, {
subtree: true,
characterData: true,
});
return () => {
resizeObserver.disconnect();
mutationObserver.disconnect();
};
}, []);
return (
<>
<Dialog
isOpen={true}
onClose={handleClose}
className='modal-open'
boxClassName='sm:min-w-[520px]'
snapHeight={window.innerWidth < 640 ? 0.7 : undefined}
boxClassName={clsx('sm:min-w-[520px]', appService?.isMobile && 'sm:max-w-[90%] sm:w-3/4')}
snapHeight={appService?.isMobile ? 0.7 : undefined}
header={
<div className='flex w-full items-center justify-between'>
<button
@@ -85,10 +131,17 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
>
{isRtl ? <MdArrowForwardIos /> : <MdArrowBackIosNew />}
</button>
<div className='dialog-tabs flex h-10 max-w-[100%] flex-grow items-center gap-2 pl-4'>
<div
ref={tabsRef}
className={clsx(
'dialog-tabs flex h-10 w-full items-center',
showTabLabels ? 'gap-2 sm:gap-1' : 'gap-4',
)}
>
{tabConfig.map(({ tab, icon: Icon, label }) => (
<button
key={tab}
data-tab={tab}
className={clsx(
'btn btn-ghost text-base-content btn-sm',
activePanel === tab ? 'btn-active' : '',
@@ -96,21 +149,23 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
onClick={() => handleSetActivePanel(tab)}
>
<Icon className='mr-0' />
{window.innerWidth >= 500 ? label : ''}
<span className={clsx('label', !showTabLabels && 'hidden')}>{label}</span>
</button>
))}
</div>
<div className='flex h-full items-center justify-end gap-x-2'>
<Dropdown
className='dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0 flex items-center justify-center'
toggleButton={<PiDotsThreeVerticalBold />}
>
<DialogMenu />
</Dropdown>
<button
onClick={handleClose}
className={'bg-base-300/65 btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 sm:flex'}
className={
'bg-base-300/65 btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 p-0 sm:flex'
}
>
<svg
xmlns='http://www.w3.org/2000/svg'
@@ -123,7 +123,7 @@ const TOCView: React.FC<{
toc: TOCItem[];
}> = ({ bookKey, toc }) => {
const { getProgress } = useReaderStore();
const { sideBarBookKey } = useSidebarStore();
const { sideBarBookKey, isSideBarVisible } = useSidebarStore();
const progress = getProgress(bookKey);
const [expandedItems, setExpandedItems] = useState<string[]>([]);
@@ -146,9 +146,6 @@ const TOCView: React.FC<{
}
(currentItem as HTMLElement).setAttribute('aria-current', 'page');
}
if (currentHref) {
expandParents(toc, currentHref);
}
};
useEffect(() => {
@@ -165,13 +162,19 @@ const TOCView: React.FC<{
}
return () => observer.disconnect();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [viewRef]);
}, [viewRef.current]);
useEffect(() => {
if (!progress || eventDispatcher.dispatchSync('tts-is-speaking')) return;
if (sideBarBookKey !== bookKey) return;
if (!isSideBarVisible) return;
const { sectionHref: currentHref } = progress;
if (currentHref) {
expandParents(toc, currentHref);
}
scrollToProgress(progress);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [toc, progress, sideBarBookKey]);
}, [toc, progress, sideBarBookKey, isSideBarVisible]);
return (
<div className='rounded pt-2'>
@@ -38,6 +38,7 @@ const TTSControl = () => {
const [timeoutTimestamp, setTimeoutTimestamp] = useState(0);
const [timeoutFunc, setTimeoutFunc] = useState<ReturnType<typeof setTimeout> | null>(null);
const viewSettings = getViewSettings(bookKey);
const popupPadding = useResponsiveSize(POPUP_PADDING);
const maxWidth = window.innerWidth - 2 * popupPadding;
const popupWidth = Math.min(maxWidth, useResponsiveSize(POPUP_WIDTH));
@@ -286,16 +287,18 @@ const TTSControl = () => {
const updatePanelPosition = () => {
if (iconRef.current) {
const rect = iconRef.current.getBoundingClientRect();
const windowRect = document.documentElement.getBoundingClientRect();
const parentRect =
iconRef.current.parentElement?.getBoundingClientRect() ||
document.documentElement.getBoundingClientRect();
const trianglePos = {
dir: 'up',
point: { x: rect.left + rect.width / 2, y: rect.top - 12 },
point: { x: rect.left + rect.width / 2 - parentRect.left, y: rect.top - 12 },
} as Position;
const popupPos = getPopupPosition(
trianglePos,
windowRect,
parentRect,
popupWidth,
popupHeight,
popupPadding,
@@ -315,8 +318,23 @@ const TTSControl = () => {
setShowPanel(false);
};
useEffect(() => {
if (!iconRef.current || !showPanel) return;
const parentElement = iconRef.current.parentElement;
if (!parentElement) return;
const resizeObserver = new ResizeObserver(() => {
updatePanelPosition();
});
resizeObserver.observe(parentElement);
return () => {
resizeObserver.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showPanel]);
return (
<div>
<>
{showPanel && (
<div
className='fixed inset-0'
@@ -328,7 +346,8 @@ const TTSControl = () => {
<div
ref={iconRef}
className={clsx(
'fixed right-6 h-12 w-12',
'absolute h-12 w-12',
viewSettings?.rtl ? 'left-6' : 'right-6',
appService?.hasSafeAreaInset
? 'bottom-[calc(env(safe-area-inset-bottom)+70px)]'
: 'bottom-[70px] sm:bottom-14',
@@ -343,7 +362,7 @@ const TTSControl = () => {
height={popupHeight}
position={panelPosition}
trianglePosition={trianglePosition}
className='bg-base-200 absolute flex shadow-lg'
className='bg-base-200 flex shadow-lg'
>
<TTSPanel
bookKey={bookKey}
@@ -362,7 +381,7 @@ const TTSControl = () => {
/>
</Popup>
)}
</div>
</>
);
};
@@ -0,0 +1,158 @@
import { useEffect, useRef } from 'react';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { getOSPlatform } from '@/utils/misc';
import { eventDispatcher } from '@/utils/event';
import { getTextFromRange, TextSelection } from '@/utils/sel';
import { transformContent } from '@/services/transformService';
export const useTextSelector = (
bookKey: string,
setSelection: React.Dispatch<React.SetStateAction<TextSelection | null>>,
handleDismissPopup: () => void,
) => {
const { getBookData } = useBookDataStore();
const { getView, getViewSettings } = useReaderStore();
const view = getView(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const bookData = getBookData(bookKey)!;
const primaryLang = bookData.book?.primaryLanguage || 'en';
const osPlatform = getOSPlatform();
const isTextSelected = useRef(false);
const isAnnotPopuped = useRef(false);
const isAnnotUpToPopup = useRef(false);
const selectionPosition = useRef<number | null>(null);
const isValidSelection = (sel: Selection) => {
return sel && sel.toString().trim().length > 0 && sel.rangeCount > 0;
};
const transformCtx = {
bookKey,
viewSettings: getViewSettings(bookKey)!,
content: '',
transformers: ['punctuation'],
reversePunctuationTransform: true,
};
const getAnnotationText = async (range: Range) => {
transformCtx['content'] = getTextFromRange(range, primaryLang.startsWith('ja') ? ['rt'] : []);
return await transformContent(transformCtx);
};
const makeSelection = async (sel: Selection, index: number, rebuildRange = false) => {
isTextSelected.current = true;
const range = sel.getRangeAt(0);
if (rebuildRange) {
sel.removeAllRanges();
sel.addRange(range);
}
setSelection({ key: bookKey, text: await getAnnotationText(range), range, index });
};
// FIXME: extremely hacky way to dismiss system selection tools on iOS
const makeSelectionOnIOS = async (sel: Selection, index: number) => {
isTextSelected.current = true;
const range = sel.getRangeAt(0);
setTimeout(() => {
sel.removeAllRanges();
setTimeout(async () => {
if (!isTextSelected.current) return;
sel.addRange(range);
setSelection({ key: bookKey, text: await getAnnotationText(range), range, index });
}, 40);
}, 0);
};
const handleSelectionchange = (doc: Document, index: number) => {
// Available on iOS, Android and Desktop, fired when the selection is changed
// Ideally the popup only shows when the selection is done,
const sel = doc.getSelection() as Selection;
if (osPlatform === 'ios') return;
if (!isValidSelection(sel)) {
if (!isAnnotUpToPopup.current) {
handleDismissPopup();
isTextSelected.current = false;
}
return;
}
// On Android no proper events are fired to notify selection done,
// we make the popup show when the selection is changed
if (osPlatform === 'android') {
makeSelection(sel, index, false);
}
};
const handlePointerup = (doc: Document, index: number) => {
// Available on iOS and Desktop, fired at touchend or mouseup
// Note that on Android, pointerup event is fired after an additional touch event
const sel = doc.getSelection() as Selection;
if (isValidSelection(sel)) {
if (osPlatform === 'ios') {
makeSelectionOnIOS(sel, index);
} else {
makeSelection(sel, index, true);
}
}
};
const handleScroll = () => {
// Prevent the container from scrolling when text is selected in paginated mode
// This is a workaround for the issue #873
// TODO: support text selection across pages
if (!viewSettings?.scrolled && view?.renderer?.containerPosition && selectionPosition.current) {
view.renderer.containerPosition = selectionPosition.current;
}
};
const handleAnnotPopup = (showAnnotPopup: boolean) => {
setTimeout(
() => {
isAnnotPopuped.current = showAnnotPopup;
},
['android', 'ios'].includes(osPlatform) ? 0 : 500,
);
};
const handleShowAnnotation = () => {
isAnnotUpToPopup.current = true;
};
useEffect(() => {
if (isTextSelected.current && !selectionPosition.current) {
selectionPosition.current = view?.renderer?.start || null;
} else if (!isTextSelected.current) {
selectionPosition.current = null;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isTextSelected.current]);
useEffect(() => {
const handleSingleClick = (): boolean => {
if (isAnnotUpToPopup.current) {
isAnnotUpToPopup.current = false;
return true;
}
if (isTextSelected.current) {
handleDismissPopup();
isTextSelected.current = false;
return true;
}
if (isAnnotPopuped.current) {
handleDismissPopup();
return true;
}
return false;
};
eventDispatcher.onSync('iframe-single-click', handleSingleClick);
return () => {
eventDispatcher.offSync('iframe-single-click', handleSingleClick);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return {
handleScroll,
handlePointerup,
handleSelectionchange,
handleAnnotPopup,
handleShowAnnotation,
};
};
+34 -15
View File
@@ -18,10 +18,12 @@ export const setAboutDialogVisible = (visible: boolean) => {
}
};
type UpdateStatus = 'checking' | 'updating' | 'updated' | 'error';
export const AboutWindow = () => {
const _ = useTranslation();
const { appService } = useEnv();
const [isUpdated, setIsUpdated] = useState(false);
const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null);
const [browserInfo, setBrowserInfo] = useState('');
const [isOpen, setIsOpen] = useState(false);
@@ -46,46 +48,63 @@ export const AboutWindow = () => {
}, []);
const handleCheckUpdate = async () => {
const update = await checkForAppUpdates(_, false);
if (update) {
setIsOpen(false);
} else {
setIsUpdated(true);
setUpdateStatus('checking');
try {
const update = await checkForAppUpdates(_, false);
if (update) {
setIsOpen(false);
} else {
setUpdateStatus('updated');
}
} catch (error) {
console.info('Error checking for updates:', error);
setUpdateStatus('error');
}
};
const handleClose = () => {
setIsOpen(false);
setUpdateStatus(null);
};
return (
<Dialog
id='about_window'
isOpen={isOpen}
title={_('About Readest')}
onClose={() => setIsOpen(false)}
onClose={handleClose}
boxClassName='sm:!w-96 sm:h-auto'
>
<div className='about-content flex h-full flex-col items-center justify-center'>
<div className='flex flex-col items-center px-8'>
<div className='mb-3'>
<Image src='/icon.png' alt='App Logo' className='h-24 w-24' width={64} height={64} />
<div className='flex flex-col items-center gap-2 px-8'>
<div className='mb-2 mt-8'>
<Image src='/icon.png' alt='App Logo' className='h-20 w-20' width={64} height={64} />
</div>
<div className='flex select-text flex-col items-center'>
<h2 className='text-2xl font-bold'>Readest</h2>
<h2 className='mb-2 text-2xl font-bold'>Readest</h2>
<p className='text-neutral-content text-center text-sm'>
{_('Version {{version}}', { version: packageJson.version })} {`(${browserInfo})`}
</p>
</div>
<div className='h-5'>
{appService?.hasUpdater && !isUpdated && (
<span className='badge badge-primary mt-2 cursor-pointer' onClick={handleCheckUpdate}>
{appService?.hasUpdater && !updateStatus && (
<span className='badge badge-primary cursor-pointer p-1' onClick={handleCheckUpdate}>
{_('Check Update')}
</span>
)}
{isUpdated && (
{updateStatus === 'updated' && (
<p className='text-neutral-content mt-2 text-xs'>{_('Already the latest version')}</p>
)}
{updateStatus === 'checking' && (
<p className='text-neutral-content mt-2 text-xs'>{_('Checking for updates...')}</p>
)}
{updateStatus === 'error' && (
<p className='text-error mt-2 text-xs'>{_('Error checking for updates')}</p>
)}
</div>
</div>
<div className='divider py-12 sm:py-2'></div>
<div className='divider py-16 sm:py-2'></div>
<div className='flex flex-col items-center px-4 text-center' dir='ltr'>
<p className='text-neutral-content text-sm'>
@@ -10,6 +10,7 @@ import { useTranslation } from '@/hooks/useTranslation';
import {
formatAuthors,
formatDate,
formatFileSize,
formatLanguage,
formatPublisher,
formatSubject,
@@ -41,6 +42,7 @@ const BookDetailModal = ({
const [loading, setLoading] = useState(false);
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
const [bookMeta, setBookMeta] = useState<BookDoc['metadata'] | null>(null);
const [fileSize, setFileSize] = useState<number | null>(null);
const { envConfig } = useEnv();
const { settings } = useSettingsStore();
@@ -50,7 +52,9 @@ const BookDetailModal = ({
const appService = await envConfig.getAppService();
try {
const details = await appService.fetchBookDetails(book, settings);
const size = await appService.getBookFileSize(book);
setBookMeta(details);
setFileSize(size);
} finally {
if (loadingTimeout) clearTimeout(loadingTimeout);
setLoading(false);
@@ -173,12 +177,6 @@ const BookDetailModal = ({
{formatLanguage(bookMeta.language) || _('Unknown')}
</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Identifier:')}</span>
<p className='text-neutral-content line-clamp-1 text-sm'>
{bookMeta.identifier || 'N/A'}
</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('Subjects:')}</span>
<p className='text-neutral-content line-clamp-1 text-sm'>
@@ -191,6 +189,12 @@ const BookDetailModal = ({
{book.format || _('Unknown')}
</p>
</div>
<div className='overflow-hidden'>
<span className='font-bold'>{_('File Size:')}</span>
<p className='text-neutral-content line-clamp-1 text-sm'>
{formatFileSize(fileSize) || _('Unknown')}
</p>
</div>
</div>
<div>
<span className='font-bold'>{_('Description:')}</span>
+4 -1
View File
@@ -1,5 +1,6 @@
import React from 'react';
import clsx from 'clsx';
import React from 'react';
import { useEnv } from '@/context/EnvContext';
interface ButtonProps {
icon: React.ReactNode;
@@ -18,6 +19,7 @@ const Button: React.FC<ButtonProps> = ({
tooltipDirection = 'top',
className,
}) => {
const { appService } = useEnv();
return (
<div
className={clsx(
@@ -32,6 +34,7 @@ const Button: React.FC<ButtonProps> = ({
<button
className={clsx(
'btn btn-ghost h-8 min-h-8 w-8 p-0',
appService?.isMobileApp && 'hover:bg-transparent',
disabled && 'btn-disabled !bg-transparent',
className,
)}
+21 -30
View File
@@ -43,7 +43,7 @@ const Dialog: React.FC<DialogProps> = ({
const { appService } = useEnv();
const { systemUIVisible, statusBarHeight } = useThemeStore();
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
const [isFullHeightInMobile, setIsFullHeightInMobile] = React.useState(!snapHeight);
const [isFullHeightInMobile, setIsFullHeightInMobile] = useState(!snapHeight);
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
const iconSize22 = useResponsiveSize(22);
const isMobile = window.innerWidth < 640;
@@ -62,6 +62,7 @@ const Dialog: React.FC<DialogProps> = ({
useEffect(() => {
if (!isOpen) return;
setIsFullHeightInMobile(!snapHeight);
window.addEventListener('keydown', handleKeyDown);
if (appService?.isAndroidApp) {
acquireBackKeyInterception();
@@ -166,38 +167,28 @@ const Dialog: React.FC<DialogProps> = ({
window.innerWidth < window.innerHeight
? 'sm:h-[50%] sm:w-3/4'
: 'sm:h-[65%] sm:w-1/2 sm:max-w-[600px]',
appService?.hasSafeAreaInset &&
isFullHeightInMobile &&
'pt-[env(safe-area-inset-top)] sm:pt-0',
boxClassName,
)}
style={
snapHeight
? {
height: `${snapHeight * 100}%`,
bottom: 0,
}
: {}
}
style={{
paddingTop:
appService?.hasSafeAreaInset && isFullHeightInMobile
? `max(env(safe-area-inset-top), ${systemUIVisible ? statusBarHeight : 0}px)`
: '0px',
...(appService?.isMobile
? { height: snapHeight ? `${snapHeight * 100}%` : '100%', bottom: 0 }
: {}),
}}
>
{window.innerWidth < 640 && (
<div
className={clsx(
'drag-handle flex h-10 max-h-10 min-h-10 w-full cursor-row-resize items-center justify-center',
'transition-padding-top duration-300 ease-out',
)}
style={{
paddingTop:
appService?.isAndroidApp && systemUIVisible && isFullHeightInMobile
? statusBarHeight
: 0,
}}
onMouseDown={handleDragStart}
onTouchStart={handleDragStart}
>
<div className='bg-base-content/50 h-1 w-10 rounded-full'></div>
</div>
)}
<div
className={clsx(
'drag-handle h-10 max-h-10 min-h-10 w-full cursor-row-resize items-center justify-center',
'transition-padding-top flex duration-300 ease-out sm:hidden',
)}
onMouseDown={handleDragStart}
onTouchStart={handleDragStart}
>
<div className='bg-base-content/50 h-1 w-10 rounded-full'></div>
</div>
<div className='dialog-header bg-base-100 sticky top-1 z-10 flex items-center justify-between px-4'>
{header ? (
header
+5 -2
View File
@@ -75,7 +75,10 @@ const Slider: React.FC<SliderProps> = ({
<div
className='bg-base-300 absolute h-full rounded-full'
style={{
width: percentage > 0 ? `calc(${percentage}% + ${heightPx / 2}px)` : '0px',
width:
percentage > 0
? `max(calc(${percentage}% + ${heightPx / 2}px), ${heightPx}px)`
: '0px',
[isRtl ? 'right' : 'left']: 0,
}}
></div>
@@ -94,7 +97,7 @@ const Slider: React.FC<SliderProps> = ({
}}
>
<div
className={`bg-base-200 flex h-full items-center justify-center rounded-full text-sm shadow-md ${bubbleClassName}`}
className={`bg-base-200 flex h-full items-center justify-center rounded-full text-xs shadow-md ${bubbleClassName}`}
style={{ width: `${heightPx}px` }}
>
{bubbleElement || bubbleLabel}
+37 -1
View File
@@ -1,5 +1,6 @@
import clsx from 'clsx';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { IconType } from 'react-icons';
interface UserAvatarProps {
@@ -17,6 +18,41 @@ const UserAvatar: React.FC<UserAvatarProps> = ({
borderClassName,
DefaultIcon,
}) => {
const [cachedImageUrl, setCachedImageUrl] = useState<string | null>(null);
useEffect(() => {
if (!url) return;
const storageKey = `avatar_${btoa(url).replace(/[^a-zA-Z0-9]/g, '')}`;
const cached = localStorage.getItem(storageKey);
if (cached) {
setCachedImageUrl(cached);
return;
}
const cacheImage = async () => {
try {
const response = await fetch(url, { referrerPolicy: 'no-referrer' });
const blob = await response.blob();
const reader = new FileReader();
reader.onloadend = () => {
const base64data = reader.result as string;
try {
localStorage.setItem(storageKey, base64data);
setCachedImageUrl(base64data);
} catch (e) {
console.warn('Failed to cache avatar in localStorage:', e);
}
};
reader.readAsDataURL(blob);
} catch (error) {
console.error('Failed to cache avatar:', error);
}
};
cacheImage();
}, [url]);
return (
<div
className='relative flex h-full w-full items-center justify-center rounded-full'
@@ -25,7 +61,7 @@ const UserAvatar: React.FC<UserAvatarProps> = ({
{url ? (
<div>
<Image
src={url}
src={cachedImageUrl || url}
alt='User Avatar'
className={clsx('rounded-full', className, borderClassName)}
referrerPolicy='no-referrer'
+2 -2
View File
@@ -47,7 +47,7 @@ export const checkForAppUpdates = async (
return !!update;
} else if (OS_TYPE === 'android') {
try {
const response = await fetch(READEST_UPDATER_FILE);
const response = await fetch(READEST_UPDATER_FILE, { connectTimeout: 5000 });
const data = await response.json();
const isNewer = semver.gt(data.version, packageJson.version);
if (isNewer && ('android-arm64' in data.platforms || 'android-universal' in data.platforms)) {
@@ -56,7 +56,7 @@ export const checkForAppUpdates = async (
return isNewer;
} catch (err) {
console.warn('Failed to fetch Android update info', err);
return false;
throw new Error('Failed to fetch Android update info');
}
}
@@ -1,11 +1,17 @@
import { getOSPlatform } from '@/utils/misc';
import { useMediaQuery } from 'react-responsive';
// use desktop size as base size
export const useResponsiveSize = (baseSize: number) => {
const isPhone = useMediaQuery({ maxWidth: 480 });
const isTablet = useMediaQuery({ minWidth: 481, maxWidth: 1024 });
if (isPhone) return baseSize * 1.25;
if (isTablet) return baseSize * 1.15;
if (typeof window === 'undefined') {
return baseSize;
}
const pixelRatio = window.devicePixelRatio || 2.4;
const isMobile = ['android', 'ios'].includes(getOSPlatform());
if (isPhone && isMobile) return baseSize * (pixelRatio / 3) * 1.25;
if (isTablet && isMobile) return baseSize * (pixelRatio / 3) * 1.25;
return baseSize;
};
+1 -1
View File
@@ -27,7 +27,7 @@ export const useTheme = ({
useEffect(() => {
updateAppTheme(appThemeColor);
if (appService?.isMobile) {
if (appService?.isMobileApp) {
if (systemUIVisible) {
showSystemUI();
} else {
+4 -2
View File
@@ -1,9 +1,11 @@
import { ViewSettings } from '@/types/book';
import { useEffect, useState } from 'react';
import { useReaderStore } from '@/store/readerStore';
// This hook allows you to inject custom CSS into the reader UI.
// Note that the book content is rendered in an iframe, so UI CSS won't affect book rendering.
export const useUICSS = (bookKey: string, viewSettings: ViewSettings) => {
export const useUICSS = (bookKey: string) => {
const { getViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey);
const [styleElement, setStyleElement] = useState<HTMLStyleElement | null>(null);
useEffect(() => {
@@ -428,6 +428,20 @@ export abstract class BaseAppService implements AppService {
return false;
}
async getBookFileSize(book: Book): Promise<number | null> {
const fp = getLocalBookFilename(book);
if (await this.fs.exists(fp, 'Books')) {
const file = await this.fs.openFile(fp, 'Books');
const size = file.size;
const f = file as ClosableFile;
if (f && f.close) {
await f.close();
}
return size;
}
return null;
}
async loadBookContent(book: Book, settings: SystemSettings): Promise<BookContent> {
let file: File;
const fp = getLocalBookFilename(book);
+1 -1
View File
@@ -119,7 +119,7 @@ export const DEFAULT_BOOK_STYLE: BookStyle = {
invert: false,
theme: 'light',
overrideFont: false,
overrideLayout: true,
overrideLayout: false,
userStylesheet: '',
};
@@ -30,6 +30,7 @@ import { LOCAL_BOOKS_SUBDIR } from './constants';
declare global {
interface Window {
IS_ROUNDED?: boolean;
__READEST_UPDATER_DISABLED?: boolean;
}
}
@@ -222,7 +223,10 @@ export class NativeAppService extends BaseAppService {
override hasRoundedWindow = !(OS_TYPE === 'ios' || OS_TYPE === 'android') && !!window.IS_ROUNDED;
override hasSafeAreaInset = OS_TYPE === 'ios' || OS_TYPE === 'android';
override hasHaptics = OS_TYPE === 'ios' || OS_TYPE === 'android';
override hasUpdater = OS_TYPE !== 'ios' && !process.env['NEXT_PUBLIC_DISABLE_UPDATER'];
override hasUpdater =
OS_TYPE !== 'ios' &&
!process.env['NEXT_PUBLIC_DISABLE_UPDATER'] &&
!window.__READEST_UPDATER_DISABLED;
override resolvePath(fp: string, base: BaseDir): { baseDir: number; base: BaseDir; fp: string } {
return resolvePath(fp, base);
@@ -0,0 +1,14 @@
import type { Transformer } from './types';
export const footnoteTransformer: Transformer = {
name: 'footnote',
transform: async (ctx) => {
let result = ctx.content;
result = result.replace(
/<aside\s+epub:type\s*=\s*["'](footnote|endnote|note|rearnote)["']([^>]*)>/gi,
'<aside class="epubtype-footnote" epub:type="$1"$2>',
);
return result;
},
};
@@ -1,9 +1,11 @@
import type { Transformer } from './types';
import { footnoteTransformer } from './footnote';
import { translateTransformer } from './translate';
import { punctuationTransformer } from './punctuation';
export const availableTransformers: Transformer[] = [
punctuationTransformer,
translateTransformer,
footnoteTransformer,
// Add more transformers here
];
+1 -12
View File
@@ -3,8 +3,6 @@
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
border-radius: 10px;
scrollbar-gutter: auto !important;
overscroll-behavior: none;
@@ -23,15 +21,8 @@
scrollbar-gutter: unset !important;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #222222;
--foreground: #e0e0e0;
}
}
html[data-page='default'] {
background: #ffffff;
background: theme('colors.base-100');
}
html[data-page='library'] {
@@ -43,8 +34,6 @@ html[data-page='reader'] {
}
body {
color: var(--foreground);
background: var(--background);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
sans-serif;
border-radius: 10px;
+1
View File
@@ -58,6 +58,7 @@ export interface AppService {
uploadBook(book: Book, onProgress?: ProgressHandler): Promise<void>;
downloadBook(book: Book, onlyCover?: boolean, onProgress?: ProgressHandler): Promise<void>;
isBookAvailable(book: Book): Promise<boolean>;
getBookFileSize(book: Book): Promise<number | null>;
loadBookConfig(book: Book, settings: SystemSettings): Promise<BookConfig>;
fetchBookDetails(book: Book, settings: SystemSettings): Promise<BookDoc['metadata']>;
saveBookConfig(book: Book, config: BookConfig, settings?: SystemSettings): Promise<void>;
+13
View File
@@ -47,6 +47,7 @@ export const getBaseFilename = (filename: string) => {
const baseName = normalizedPath.split('/').pop()?.split('.').slice(0, -1).join('.') || '';
return baseName;
};
export const INIT_BOOK_CONFIG: BookConfig = {
updatedAt: 0,
};
@@ -141,6 +142,18 @@ export const formatSubject = (subject: string | string[] | undefined) => {
return Array.isArray(subject) ? subject.join(', ') : subject;
};
export const formatFileSize = (size: number | null) => {
if (size === null) return '';
const formatter = new Intl.NumberFormat('en', {
style: 'unit',
unit: 'byte',
unitDisplay: 'narrow',
notation: 'compact',
compactDisplay: 'short',
});
return formatter.format(size);
};
export const getCurrentPage = (book: Book, progress: BookProgress) => {
const bookFormat = book.format;
const { section, pageinfo } = progress;
+2 -2
View File
@@ -187,11 +187,11 @@ export class NativeFile extends File implements ClosableFile {
return new DeferredBlob(dataPromise, contentType);
}
override stream(): ReadableStream<Uint8Array> {
override stream(): ReadableStream<Uint8Array<ArrayBuffer>> {
const CHUNK_SIZE = 1024 * 1024;
let offset = 0;
return new ReadableStream<Uint8Array>({
return new ReadableStream<Uint8Array<ArrayBuffer>>({
pull: async (controller) => {
if (!this.#handle) {
controller.error(new Error('File handle is not open'));
+39 -23
View File
@@ -87,12 +87,12 @@ const getFontStyles = (
body * {
${overrideFont ? 'font-family: revert !important;' : ''}
}
a:any-link * {
a:any-link, a:any-link * {
${overrideFont ? `color: ${primary};` : ''}
}
/* https://github.com/whatwg/html/issues/5426 */
@media (prefers-color-scheme: dark) {
a:link * {
a:any-link, a:any-link * {
${overrideFont ? `color: ${primary};` : `color: lightblue;`}
}
}
@@ -210,25 +210,29 @@ const getLayoutStyles = (
${writingMode === 'auto' ? '' : `writing-mode: ${writingMode} !important;`}
text-align: var(--default-text-align);
max-height: unset;
}
html {
background-color: var(--theme-bg-color, transparent);
background: var(--background-set, none);
}
body.pbg, body *:not(a):not(#b1):not(#b1 *):not(#b2):not(#b2 *):not(img):not(.bg):not(.bg *):not(.vol):not(.vol *):not(.background):not(.background *) {
${bg === '#ffffff' ? '' : `background-color: ${bg} !important;`}
${isDarkMode ? `background-color: ${bg} !important;` : ''}
}
body {
overflow: unset;
zoom: ${zoomLevel};
}
svg, img {
height: auto;
width: auto;
background-color: transparent !important;
}
p:not(.poem):not(.poetry):not(.lh):not(:has(> :is(img, video, font, b, h1, h2, h3, h4, h5, table))), li, blockquote, dd {
p, div, li, blockquote, dd {
line-height: ${lineSpacing} ${overrideLayout ? '!important' : ''};
word-spacing: ${wordSpacing}px ${overrideLayout ? '!important' : ''};
letter-spacing: ${letterSpacing}px ${overrideLayout ? '!important' : ''};
text-indent: ${vertical ? textIndent * 1.2 : textIndent}em ${overrideLayout ? '!important' : ''};
text-align: ${overrideLayout ? 'var(--default-text-align)' : 'inherit'};
text-align: ${justify ? 'justify' : 'inherit'} ${overrideLayout ? '!important' : ''};
-webkit-hyphens: ${hyphenate ? 'auto' : 'manual'};
hyphens: ${hyphenate ? 'auto' : 'manual'};
-webkit-hyphenate-limit-before: 3;
@@ -237,15 +241,12 @@ const getLayoutStyles = (
hanging-punctuation: allow-end last;
widows: 2;
}
p {
p, div {
${vertical ? `margin-left: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''}
${vertical ? `margin-right: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''}
${!vertical ? `margin-top: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''}
${!vertical ? `margin-bottom: ${paragraphMargin}em ${overrideLayout ? '!important' : ''};` : ''}
}
li, p:has(> :is(img, video, font, b, h1, h2, h3, h4, h5, table)) {
text-indent: 0 !important;
}
/* prevent the above from overriding the align attribute */
[align="left"] { text-align: left; }
[align="right"] { text-align: right; }
@@ -255,6 +256,8 @@ const getLayoutStyles = (
pre {
white-space: pre-wrap !important;
}
.epubtype-footnote,
aside[epub|type~="endnote"],
aside[epub|type~="footnote"],
aside[epub|type~="note"],
@@ -262,6 +265,7 @@ const getLayoutStyles = (
display: none;
}
/* Now begins really dirty hacks to fix some badly designed epubs */
img.pi {
${vertical ? 'transform: rotate(90deg);' : ''}
${vertical ? 'transform-origin: center;' : ''}
@@ -270,20 +274,11 @@ const getLayoutStyles = (
${vertical ? `vertical-align: unset;` : ''}
}
aside[epub|type~="footnote"] {
display: none;
}
/* Now begins really dirty hacks to fix some badly designed epubs */
.duokan-footnote-content,
.duokan-footnote-item {
display: none;
}
.duokan-image-single {
height: 100vh !important;
}
.calibre {
color: unset;
}
@@ -434,10 +429,26 @@ export const transformStylesheet = (
const fontScale = isMobile ? 1.25 : 1;
const w = width * (1 - viewSettings.gapPercent / 100);
const h = height - viewSettings.marginPx * 2;
const ruleRegex = /([^{]+)({[^}]+})/g;
css = css.replace(ruleRegex, (match, selector, block) => {
const hasTextAlignCenter =
/text-align\s*:\s*center\s*;/.test(block) || /text-align\s*:\s*center\s*$/.test(block);
const hasTextIndentZero =
/text-indent\s*:\s*0\s*;/.test(block) || /text-indent\s*:\s*0\s*$/.test(block);
if (hasTextAlignCenter) {
block = block.replace(/(text-align\s*:\s*center)(\s*;|\s*$)/g, '$1 !important$2');
if (hasTextIndentZero) {
block = block.replace(/(text-indent\s*:\s*0)(\s*;|\s*$)/g, '$1 !important$2');
}
return selector + block;
}
return match;
});
// replace absolute font sizes with rem units
// replace vw and vh as they cause problems with layout
// replace hardcoded colors
return css
css = css
.replace(/font-size\s*:\s*xx-small/gi, 'font-size: 0.6rem')
.replace(/font-size\s*:\s*x-small/gi, 'font-size: 0.75rem')
.replace(/font-size\s*:\s*small/gi, 'font-size: 0.875rem')
@@ -455,8 +466,13 @@ export const transformStylesheet = (
return `font-size: ${rem}rem`;
})
.replace(/(\d*\.?\d+)vw/gi, (_, d) => (parseFloat(d) * w) / 100 + 'px')
.replace(/(\d*\.?\d+)vh/gi, (_, d) => (parseFloat(d) * h) / 100 + 'px')
.replace(/[\s;]color\s*:\s*#000000/gi, 'color: var(--theme-fg-color)')
.replace(/[\s;]color\s*:\s*#000/gi, 'color: var(--theme-fg-color)')
.replace(/[\s;]color\s*:\s*rgb\(0,\s*0,\s*0\)/gi, 'color: var(--theme-fg-color)');
.replace(/(\d*\.?\d+)vh/gi, (_, d) => (parseFloat(d) * h) / 100 + 'px');
if (viewSettings.overrideFont) {
css = css
.replace(/[\s;]color\s*:\s*#000000/gi, 'color: var(--theme-fg-color)')
.replace(/[\s;]color\s*:\s*#000/gi, 'color: var(--theme-fg-color)')
.replace(/[\s;]color\s*:\s*rgb\(0,\s*0,\s*0\)/gi, 'color: var(--theme-fg-color)');
}
return css;
};
+25 -5
View File
@@ -21,6 +21,7 @@ interface Txt2EpubOptions {
interface ExtractChapterOptions {
linesBetweenSegments: number;
paragraphsPerChapter?: number;
}
interface ConversionResult {
@@ -78,6 +79,12 @@ export class TxtToEpubConverter {
break;
}
}
if (chapters.length === 1) {
chapters = this.extractChapters(txtContent, metadata, {
linesBetweenSegments: 4,
paragraphsPerChapter: 100,
});
}
const blob = await this.createEpub(chapters, metadata);
return {
@@ -94,8 +101,8 @@ export class TxtToEpubConverter {
option: ExtractChapterOptions,
): Chapter[] {
const { language } = metadata;
const { linesBetweenSegments } = option;
const segmentRegex = new RegExp(`(?:\\r?\\n){${linesBetweenSegments},}|-{4,}\r?\n`);
const { linesBetweenSegments, paragraphsPerChapter } = option;
const segmentRegex = new RegExp(`(?:\\r?\\n){${linesBetweenSegments},}|-{8,}\r?\n`);
let chapterRegex: RegExp;
if (language === 'zh') {
chapterRegex = new RegExp(
@@ -111,13 +118,13 @@ export class TxtToEpubConverter {
);
} else {
chapterRegex =
/(?:^|\n|\s)(Chapter [0-9]+(?:[: ][^\n]*)?(?!\S)|Part [0-9]+(?:[: ][^\n]*)?(?!\S)|Prologue(?:[: ][^\n]*)?(?!\S)|Introduction(?:[: ][^\n]*)?(?!\S))/g;
/(?:^|\n|\s)(?:(Chapter|Part)\s+(\d+|[IVXLCDM]+)(?:[:.\-–—]?\s+[^\n]*)?|(?:Prologue|Epilogue|Introduction|Foreword)(?:[:.\-–—]?\s+[^\n]*)?)(?=\s|$)/gi;
}
const formatSegment = (segment: string): string => {
segment = escapeXml(segment);
return segment
.replace(/-{4,}|_{4,}/g, '\n')
.replace(/-{8,}|_{8,}/g, '\n')
.split(/\n+/)
.map((line) => line.trim())
.filter((line) => line)
@@ -148,6 +155,19 @@ export class TxtToEpubConverter {
const trimmedSegment = segment.replace(/<!--.*?-->/g, '').trim();
if (!trimmedSegment) continue;
if (paragraphsPerChapter && paragraphsPerChapter > 0) {
const paragraphs = trimmedSegment.split(/\n+/);
const totalParagraphs = paragraphs.length;
for (let i = 0; i < totalParagraphs; i += paragraphsPerChapter) {
const chunks = paragraphs.slice(i, i + paragraphsPerChapter);
const formattedSegment = formatSegment(chunks.join('\n'));
const title = `${chapters.length + 1}`;
const content = `<h2>${title}</h2><p>${formattedSegment}</p>`;
chapters.push({ title, content });
}
continue;
}
const segmentChapters = [];
const matches = joinAroundUndefined(trimmedSegment.split(chapterRegex));
for (let j = 1; j < matches.length; j += 2) {
@@ -156,7 +176,7 @@ export class TxtToEpubConverter {
let isVolume = false;
if (language === 'zh') {
isVolume = /第[一二三四五六七八九十百千万0-9]+卷/.test(title);
isVolume = /第[零〇一二三四五六七八九十百千万0-9]+卷/.test(title);
} else {
isVolume = /\b(Part|Volume|Book)\b/i.test(title);
}
+2
View File
@@ -7,6 +7,8 @@
"tauri": "pnpm --filter @readest/readest-app tauri"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.9.1",
"@sindresorhus/tsconfig": "^6.0.0",
"eslint": "^9",
"eslint-config-prettier": "^9.1.0",
+94 -88
View File
@@ -8,6 +8,12 @@ importers:
.:
devDependencies:
'@eslint/eslintrc':
specifier: ^3.3.1
version: 3.3.1
'@eslint/js':
specifier: ^9.9.1
version: 9.16.0
'@sindresorhus/tsconfig':
specifier: ^6.0.0
version: 6.0.0
@@ -54,8 +60,8 @@ importers:
specifier: ^2.47.7
version: 2.47.7
'@tauri-apps/api':
specifier: 2.4.1
version: 2.4.1
specifier: 2.5.0
version: 2.5.0
'@tauri-apps/plugin-cli':
specifier: ^2.2.0
version: 2.2.0
@@ -75,8 +81,8 @@ importers:
specifier: ^2.4.3
version: 2.4.3
'@tauri-apps/plugin-log':
specifier: ^2.3.1
version: 2.3.1
specifier: ^2.4.0
version: 2.4.0
'@tauri-apps/plugin-opener':
specifier: ^2.2.6
version: 2.2.6
@@ -90,8 +96,8 @@ importers:
specifier: ~2.2.1
version: 2.2.1
'@tauri-apps/plugin-updater':
specifier: ^2.7.0
version: 2.7.0
specifier: ^2.7.1
version: 2.7.1
'@zip.js/zip.js':
specifier: ^2.7.53
version: 2.7.53
@@ -163,8 +169,8 @@ importers:
specifier: ^0.5.12
version: 0.5.12(wrangler@4.4.0)
'@tauri-apps/cli':
specifier: 2.4.1
version: 2.4.1
specifier: 2.5.0
version: 2.5.0
'@types/cors':
specifier: ^2.8.17
version: 2.8.17
@@ -1555,8 +1561,8 @@ packages:
resolution: {integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/eslintrc@3.2.0':
resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==}
'@eslint/eslintrc@3.3.1':
resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.16.0':
@@ -2483,82 +2489,82 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tauri-apps/api@2.4.1':
resolution: {integrity: sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg==}
'@tauri-apps/api@2.5.0':
resolution: {integrity: sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==}
'@tauri-apps/cli-darwin-arm64@2.4.1':
resolution: {integrity: sha512-QME7s8XQwy3LWClTVlIlwXVSLKkeJ/z88pr917Mtn9spYOjnBfsgHAgGdmpWD3NfJxjg7CtLbhH49DxoFL+hLg==}
'@tauri-apps/cli-darwin-arm64@2.5.0':
resolution: {integrity: sha512-VuVAeTFq86dfpoBDNYAdtQVLbP0+2EKCHIIhkaxjeoPARR0sLpFHz2zs0PcFU76e+KAaxtEtAJAXGNUc8E1PzQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.4.1':
resolution: {integrity: sha512-/r89IcW6Ya1sEsFUEH7wLNruDTj7WmDWKGpPy7gATFtQr5JEY4heernqE82isjTUimnHZD8SCr0jA3NceI4ybw==}
'@tauri-apps/cli-darwin-x64@2.5.0':
resolution: {integrity: sha512-hUF01sC06cZVa8+I0/VtsHOk9BbO75rd+YdtHJ48xTdcYaQ5QIwL4yZz9OR1AKBTaUYhBam8UX9Pvd5V2/4Dpw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.4.1':
resolution: {integrity: sha512-9tDijkRB+CchAGjXxYdY9l/XzFpLp1yihUtGXJz9eh+3qIoRI043n3e+6xmU8ZURr7XPnu+R4sCmXs6HD+NCEQ==}
'@tauri-apps/cli-linux-arm-gnueabihf@2.5.0':
resolution: {integrity: sha512-LQKqttsK252LlqYyX8R02MinUsfFcy3+NZiJwHFgi5Y3+ZUIAED9cSxJkyNtuY5KMnR4RlpgWyLv4P6akN1xhg==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.4.1':
resolution: {integrity: sha512-pnFGDEXBAzS4iDYAVxTRhAzNu3K2XPGflYyBc0czfHDBXopqRgMyj5Q9Wj7HAwv6cM8BqzXINxnb2ZJFGmbSgA==}
'@tauri-apps/cli-linux-arm64-gnu@2.5.0':
resolution: {integrity: sha512-mTQufsPcpdHg5RW0zypazMo4L55EfeE5snTzrPqbLX4yCK2qalN7+rnP8O8GT06xhp6ElSP/Ku1M2MR297SByQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.4.1':
resolution: {integrity: sha512-Hp0zXgeZNKmT+eoJSCxSBUm2QndNuRxR55tmIeNm3vbyUMJN/49uW7nurZ5fBPsacN4Pzwlx1dIMK+Gnr9A69w==}
'@tauri-apps/cli-linux-arm64-musl@2.5.0':
resolution: {integrity: sha512-rQO1HhRUQqyEaal5dUVOQruTRda/TD36s9kv1hTxZiFuSq3558lsTjAcUEnMAtBcBkps20sbyTJNMT0AwYIk8Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-riscv64-gnu@2.4.1':
resolution: {integrity: sha512-3T3bo2E4fdYRvzcXheWUeQOVB+LunEEi92iPRgOyuSVexVE4cmHYl+MPJF+EUV28Et0hIVTsHibmDO0/04lAFg==}
'@tauri-apps/cli-linux-riscv64-gnu@2.5.0':
resolution: {integrity: sha512-7oS18FN46yDxyw1zX/AxhLAd7T3GrLj3Ai6s8hZKd9qFVzrAn36ESL7d3G05s8wEtsJf26qjXnVF4qleS3dYsA==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.4.1':
resolution: {integrity: sha512-kLN0FdNONO+2i+OpU9+mm6oTGufRC00e197TtwjpC0N6K2K8130w7Q3FeODIM2CMyg0ov3tH+QWqKW7GNhHFzg==}
'@tauri-apps/cli-linux-x64-gnu@2.5.0':
resolution: {integrity: sha512-SG5sFNL7VMmDBdIg3nO3EzNRT306HsiEQ0N90ILe3ZABYAVoPDO/ttpCO37ApLInTzrq/DLN+gOlC/mgZvLw1w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.4.1':
resolution: {integrity: sha512-a8exvA5Ub9eg66a6hsMQKJIkf63QAf9OdiuFKOsEnKZkNN2x0NLgfvEcqdw88VY0UMs9dBoZ1AGbWMeYnLrLwQ==}
'@tauri-apps/cli-linux-x64-musl@2.5.0':
resolution: {integrity: sha512-QXDM8zp/6v05PNWju5ELsVwF0VH1n6b5pk2E6W/jFbbiwz80Vs1lACl9pv5kEHkrxBj+aWU/03JzGuIj2g3SkQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.4.1':
resolution: {integrity: sha512-4JFrslsMCJQG1c573T9uqQSAbF3j/tMKkMWzsIssv8jvPiP++OG61A2/F+y9te9/Q/O95cKhDK63kaiO5xQaeg==}
'@tauri-apps/cli-win32-arm64-msvc@2.5.0':
resolution: {integrity: sha512-pFSHFK6b+o9y4Un8w0gGLwVyFTZaC3P0kQ7umRt/BLDkzD5RnQ4vBM7CF8BCU5nkwmEBUCZd7Wt3TWZxe41o6Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.4.1':
resolution: {integrity: sha512-9eXfFORehYSCRwxg2KodfmX/mhr50CI7wyBYGbPLePCjr5z0jK/9IyW6r0tC+ZVjwpX48dkk7hKiUgI25jHjzA==}
'@tauri-apps/cli-win32-ia32-msvc@2.5.0':
resolution: {integrity: sha512-EArv1IaRlogdLAQyGlKmEqZqm5RfHCUMhJoedWu7GtdbOMUfSAz6FMX2boE1PtEmNO4An+g188flLeVErrxEKg==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.4.1':
resolution: {integrity: sha512-60a4Ov7Jrwqz2hzDltlS7301dhSAmM9dxo+IRBD3xz7yobKrgaHXYpWvnRomYItHcDd51VaKc9292H8/eE/gsw==}
'@tauri-apps/cli-win32-x64-msvc@2.5.0':
resolution: {integrity: sha512-lj43EFYbnAta8pd9JnUq87o+xRUR0odz+4rixBtTUwUgdRdwQ2V9CzFtsMu6FQKpFQ6mujRK6P1IEwhL6ADRsQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.4.1':
resolution: {integrity: sha512-9Ta81jx9+57FhtU/mPIckDcOBtPTUdKM75t4+aA0X84b8Sclb0jy1xA8NplmcRzp2fsfIHNngU2NiRxsW5+yOQ==}
'@tauri-apps/cli@2.5.0':
resolution: {integrity: sha512-rAtHqG0Gh/IWLjN2zTf3nZqYqbo81oMbqop56rGTjrlWk9pTTAjkqOjSL9XQLIMZ3RbeVjveCqqCA0s8RnLdMg==}
engines: {node: '>= 10'}
hasBin: true
@@ -2580,8 +2586,8 @@ packages:
'@tauri-apps/plugin-http@2.4.3':
resolution: {integrity: sha512-Us8X+FikzpaZRNr4kH4HLwyXascHbM42p6LxAqRTQnHPrrqp1usaH4vxWAZalPvTbHJ3gBEMJPHusFJgtjGJjA==}
'@tauri-apps/plugin-log@2.3.1':
resolution: {integrity: sha512-nnKGHENWt7teqvUlIKxd6bp2wCUrrLvCvajN6CWbyrHBNKPi/pyKELzD511siEMDEdndbiZ/GEhiK0xBtZopRg==}
'@tauri-apps/plugin-log@2.4.0':
resolution: {integrity: sha512-j7yrDtLNmayCBOO2esl3aZv9jSXy2an8MDLry3Ys9ZXerwUg35n1Y2uD8HoCR+8Ng/EUgx215+qOUfJasjYrHw==}
'@tauri-apps/plugin-opener@2.2.6':
resolution: {integrity: sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w==}
@@ -2595,8 +2601,8 @@ packages:
'@tauri-apps/plugin-shell@2.2.1':
resolution: {integrity: sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA==}
'@tauri-apps/plugin-updater@2.7.0':
resolution: {integrity: sha512-oBug5UCH2wOsoYk0LW5LEMAT51mszjg11s8eungRH26x/qOrEjLvnuJJoxVVr9nsWowJ6vnpXKS+lUMfFTlvHQ==}
'@tauri-apps/plugin-updater@2.7.1':
resolution: {integrity: sha512-1OPqEY/z7NDVSeTEMIhD2ss/vXWdpfZ5Th2Mk0KtPR/RA6FKuOTDGZQhxoyYBk0pcZJ+nNZUbl/IujDCLBApjA==}
'@tsconfig/node10@1.0.11':
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
@@ -7250,7 +7256,7 @@ snapshots:
'@babel/traverse': 7.26.7
'@babel/types': 7.26.7
convert-source-map: 2.0.0
debug: 4.3.7
debug: 4.4.0
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -7858,7 +7864,7 @@ snapshots:
'@babel/parser': 7.26.7
'@babel/template': 7.25.9
'@babel/types': 7.26.7
debug: 4.3.7
debug: 4.4.0
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -8090,10 +8096,10 @@ snapshots:
'@eslint/core@0.9.0': {}
'@eslint/eslintrc@3.2.0':
'@eslint/eslintrc@3.3.1':
dependencies:
ajv: 6.12.6
debug: 4.3.7
debug: 4.4.0
espree: 10.3.0
globals: 14.0.0
ignore: 5.3.2
@@ -8114,7 +8120,7 @@ snapshots:
'@fabianlars/tauri-plugin-oauth@2.0.0':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@fastify/busboy@2.1.1': {}
@@ -9268,102 +9274,102 @@ snapshots:
dependencies:
tslib: 2.8.1
'@tauri-apps/api@2.4.1': {}
'@tauri-apps/api@2.5.0': {}
'@tauri-apps/cli-darwin-arm64@2.4.1':
'@tauri-apps/cli-darwin-arm64@2.5.0':
optional: true
'@tauri-apps/cli-darwin-x64@2.4.1':
'@tauri-apps/cli-darwin-x64@2.5.0':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.4.1':
'@tauri-apps/cli-linux-arm-gnueabihf@2.5.0':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.4.1':
'@tauri-apps/cli-linux-arm64-gnu@2.5.0':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.4.1':
'@tauri-apps/cli-linux-arm64-musl@2.5.0':
optional: true
'@tauri-apps/cli-linux-riscv64-gnu@2.4.1':
'@tauri-apps/cli-linux-riscv64-gnu@2.5.0':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.4.1':
'@tauri-apps/cli-linux-x64-gnu@2.5.0':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.4.1':
'@tauri-apps/cli-linux-x64-musl@2.5.0':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.4.1':
'@tauri-apps/cli-win32-arm64-msvc@2.5.0':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.4.1':
'@tauri-apps/cli-win32-ia32-msvc@2.5.0':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.4.1':
'@tauri-apps/cli-win32-x64-msvc@2.5.0':
optional: true
'@tauri-apps/cli@2.4.1':
'@tauri-apps/cli@2.5.0':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.4.1
'@tauri-apps/cli-darwin-x64': 2.4.1
'@tauri-apps/cli-linux-arm-gnueabihf': 2.4.1
'@tauri-apps/cli-linux-arm64-gnu': 2.4.1
'@tauri-apps/cli-linux-arm64-musl': 2.4.1
'@tauri-apps/cli-linux-riscv64-gnu': 2.4.1
'@tauri-apps/cli-linux-x64-gnu': 2.4.1
'@tauri-apps/cli-linux-x64-musl': 2.4.1
'@tauri-apps/cli-win32-arm64-msvc': 2.4.1
'@tauri-apps/cli-win32-ia32-msvc': 2.4.1
'@tauri-apps/cli-win32-x64-msvc': 2.4.1
'@tauri-apps/cli-darwin-arm64': 2.5.0
'@tauri-apps/cli-darwin-x64': 2.5.0
'@tauri-apps/cli-linux-arm-gnueabihf': 2.5.0
'@tauri-apps/cli-linux-arm64-gnu': 2.5.0
'@tauri-apps/cli-linux-arm64-musl': 2.5.0
'@tauri-apps/cli-linux-riscv64-gnu': 2.5.0
'@tauri-apps/cli-linux-x64-gnu': 2.5.0
'@tauri-apps/cli-linux-x64-musl': 2.5.0
'@tauri-apps/cli-win32-arm64-msvc': 2.5.0
'@tauri-apps/cli-win32-ia32-msvc': 2.5.0
'@tauri-apps/cli-win32-x64-msvc': 2.5.0
'@tauri-apps/plugin-cli@2.2.0':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-deep-link@2.2.1':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-dialog@2.2.1':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-fs@2.2.1':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-haptics@2.2.4':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-http@2.4.3':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-log@2.3.1':
'@tauri-apps/plugin-log@2.4.0':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-opener@2.2.6':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-os@2.2.1':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-process@2.2.1':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-shell@2.2.1':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@tauri-apps/plugin-updater@2.7.0':
'@tauri-apps/plugin-updater@2.7.1':
dependencies:
'@tauri-apps/api': 2.4.1
'@tauri-apps/api': 2.5.0
'@tsconfig/node10@1.0.11':
optional: true
@@ -9483,7 +9489,7 @@ snapshots:
'@typescript-eslint/types': 8.16.0
'@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2)
'@typescript-eslint/visitor-keys': 8.16.0
debug: 4.3.7
debug: 4.4.0
eslint: 9.16.0(jiti@1.21.6)
optionalDependencies:
typescript: 5.7.2
@@ -9499,7 +9505,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 8.16.0(typescript@5.7.2)
'@typescript-eslint/utils': 8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2)
debug: 4.3.7
debug: 4.4.0
eslint: 9.16.0(jiti@1.21.6)
ts-api-utils: 1.4.3(typescript@5.7.2)
optionalDependencies:
@@ -9513,7 +9519,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 8.16.0
'@typescript-eslint/visitor-keys': 8.16.0
debug: 4.3.7
debug: 4.4.0
fast-glob: 3.3.2
is-glob: 4.0.3
minimatch: 9.0.5
@@ -10479,7 +10485,7 @@ snapshots:
eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@9.16.0(jiti@1.21.6)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.3.7
debug: 4.4.0
enhanced-resolve: 5.17.1
eslint: 9.16.0(jiti@1.21.6)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0(jiti@1.21.6))(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.16.0(jiti@1.21.6))
@@ -10600,7 +10606,7 @@ snapshots:
'@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.19.0
'@eslint/core': 0.9.0
'@eslint/eslintrc': 3.2.0
'@eslint/eslintrc': 3.3.1
'@eslint/js': 9.16.0
'@eslint/plugin-kit': 0.2.3
'@humanfs/node': 0.16.6