forked from akai/readest
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 382e0ca3c6 | |||
| d96fb32bcf | |||
| 4779e2638f | |||
| d094ba9916 | |||
| e4ba0c302e | |||
| 0150177a9d | |||
| 74e32d47b0 | |||
| 2dd7cf54e6 | |||
| a5cbfe97db | |||
| da49526f4c | |||
| 2e46bc1627 | |||
| 7e542e8694 | |||
| 2a624ef57a | |||
| 60ae0c9ac3 | |||
| 110b4b37cd | |||
| f0edbf14a9 | |||
| 20669a9cb3 | |||
| 3756bec66c | |||
| 3e1d03dc17 | |||
| 4aebbf679f | |||
| 0a562d90ac |
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@readest/readest-app",
|
||||
"version": "0.9.40",
|
||||
"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:": "檔案大小"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
{
|
||||
"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": [
|
||||
|
||||
@@ -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"
|
||||
|
||||
+1
-1
@@ -355,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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -167,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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export const useTheme = ({
|
||||
|
||||
useEffect(() => {
|
||||
updateAppTheme(appThemeColor);
|
||||
if (appService?.isMobile) {
|
||||
if (appService?.isMobileApp) {
|
||||
if (systemUIVisible) {
|
||||
showSystemUI();
|
||||
} else {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -216,21 +216,23 @@ const getLayoutStyles = (
|
||||
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, 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;
|
||||
@@ -239,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; }
|
||||
@@ -257,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"],
|
||||
@@ -264,11 +265,7 @@ const getLayoutStyles = (
|
||||
display: none;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* Now begins really dirty hacks to fix some badly designed epubs */
|
||||
img.pi {
|
||||
${vertical ? 'transform: rotate(90deg);' : ''}
|
||||
${vertical ? 'transform-origin: center;' : ''}
|
||||
@@ -277,11 +274,6 @@ 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;
|
||||
@@ -437,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')
|
||||
@@ -458,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;
|
||||
};
|
||||
|
||||
@@ -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,7 +101,7 @@ export class TxtToEpubConverter {
|
||||
option: ExtractChapterOptions,
|
||||
): Chapter[] {
|
||||
const { language } = metadata;
|
||||
const { linesBetweenSegments } = option;
|
||||
const { linesBetweenSegments, paragraphsPerChapter } = option;
|
||||
const segmentRegex = new RegExp(`(?:\\r?\\n){${linesBetweenSegments},}|-{8,}\r?\n`);
|
||||
let chapterRegex: RegExp;
|
||||
if (language === 'zh') {
|
||||
@@ -111,7 +118,7 @@ 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 => {
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
+1
-1
Submodule packages/foliate-js updated: 89285c2cd4...88edc25834
+1
-1
Submodule packages/tauri updated: 29264c8244...914e100aa8
Generated
+94
-88
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user