Compare commits

...

9 Commits

Author SHA1 Message Date
Huang Xin 443809f5af Release version 0.9.6 (#191) 2025-01-18 18:44:16 +01:00
Huang Xin c009edca61 Fix translation API (#189)
* Fix translation API

* Add missing translations
2025-01-17 19:10:27 +01:00
Huang Xin 58c907c7cc Various enhancements for mobile platforms (#188)
* Larger area to toggle header/footer bar

* Don't show rounded window on web platform

* Disable tooltips on mobile platforms as there is no mouse pointer

* Dedupe voices from web speech API
2025-01-17 15:42:16 +01:00
Huang Xin 5ca173818b Fix corner case of sync progress failure (#187) 2025-01-17 13:45:29 +01:00
Huang Xin f64b5606f1 Hide book title in header bar for small screens (#185) 2025-01-17 00:28:06 +01:00
Huang Xin 010568eeb1 Responsive layout for sidebar and settings dialog on mobile devices (#184) 2025-01-17 00:17:51 +01:00
Huang Xin d24f737428 Style tweaks: slightly lighter base-300 colors and padding around tab icons in sidebar (#183) 2025-01-16 19:07:23 +01:00
Huang Xin 1635958ae3 More precise translations for some options and add Greek translations (#182)
* More precise translations for some options, closes #165

* Add Greek translation, closes #181
2025-01-16 15:18:14 +01:00
Huang Xin c8f0c09426 Use dynamic viewport unit dvh to avoid page covered by toolbar and address bar in mobile browsers (#160) 2025-01-15 19:11:05 +01:00
58 changed files with 726 additions and 323 deletions
+5 -4
View File
@@ -77,10 +77,11 @@ Now you're all setup and can start implementing your changes.
This project is a monorepo. The code for the `readest-app` is in the `app/readest-app` directory. Here are some useful scripts for developing the frontend only without compiling Tauri:
| Command | Description |
| ---------------- | -------------------------------------------------- |
| `pnpm dev-web` | Starts the development server for the web app only |
| `pnpm build-web` | Builds the web app |
| Command | Description |
| ------------------------------------------ | -------------------------------------------------- |
| `pnpm dev-web` | Starts the development server for the web app only |
| `TAURI_DEV_HOST=192.168.1.22 pnpm dev-web` | Start the dev server on a LAN address |
| `pnpm build-web` | Builds the web app |
Recommended Visual Studio Code plugins for development:
+8
View File
@@ -0,0 +1,8 @@
NEXT_PUBLIC_POSTHOG_KEY=YOUR_POSTHOG_KEY
NEXT_PUBLIC_POSTHOG_HOST=YOUR_POSTHOG_HOST
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
DEEPL_PRO_API_KEY=YOUR_DEEPL_PRO_API_KEY
DEEPL_FREE_API_KEY=YOUR_DEEPL_FREE_API_KEY
+16 -1
View File
@@ -8,7 +8,22 @@ module.exports = {
list: ['_'],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
lngs: ['de', 'ja', 'es', 'fr', 'it', 'ko', 'pt', 'ru', 'tr', 'id', 'vi', 'zh-CN', 'zh-TW'],
lngs: [
'de',
'ja',
'es',
'fr',
'it',
'el',
'ko',
'pt',
'ru',
'tr',
'id',
'vi',
'zh-CN',
'zh-TW',
],
ns: ['translation'],
defaultNs: 'translation',
defaultValue: '__STRING_NOT_TRANSLATED__',
-8
View File
@@ -19,14 +19,6 @@ const nextConfig = {
// Configure assetPrefix or else the server won't properly resolve your assets.
assetPrefix: isProd ? '' : `http://${internalHost}:3000`,
reactStrictMode: true,
async rewrites() {
return [
{
source: '/api/deepl/:path*',
destination: 'https://api-free.deepl.com/v2/:path*',
},
];
},
};
export default nextConfig;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.5",
"version": "0.9.6",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -41,7 +41,6 @@
"Font Size": "Schriftgröße",
"From Local File": "Aus lokaler Datei",
"Full Justification": "Blocksatz",
"Gaps (%)": "Abstände (%)",
"Global Settings": "Globale Einstellungen",
"Go Back": "Zurück",
"Go Forward": "Vorwärts",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "Pos. {{currentPage}} / {{totalPage}}",
"Logged in": "Angemeldet",
"Logged in as {{userDisplayName}}": "Angemeldet als {{userDisplayName}}",
"Margins (px)": "Ränder (px)",
"Match Case": "Groß-/Kleinschreibung beachten",
"Match Diacritics": "Akzente beachten",
"Match Whole Words": "Ganze Wörter",
@@ -81,7 +79,6 @@
"Open": "Öffnen",
"Open Book": "Buch öffnen",
"Original Text": "Originaltext",
"Override Publisher Font": "Verleger-Schriftart überschreiben",
"Page": "Seite",
"Paging Animation": "Blätter-Animation",
"Paragraph": "Absatz",
@@ -130,5 +127,11 @@
"Wikipedia": "Wikipedia",
"Writing Mode": "Schreibmodus",
"Your Library": "Ihre Bibliothek",
"TTS not supported for PDF": "TTS wird für PDF nicht unterstützt"
"TTS not supported for PDF": "TTS wird für PDF nicht unterstützt",
"Override Book Font": "Buch-Schriftart überschreiben",
"Vertical Margins (px)": "Vertikale Ränder (px)",
"Horizontal Margins (%)": "Horizontale Ränder (%)",
"Apply to all books": "Auf alle Bücher anwenden",
"Apply to this book": "Auf dieses Buch anwenden",
"Unable to fetch the translation. Try again later.": "Übersetzung konnte nicht abgerufen werden. Versuchen Sie es später erneut."
}
@@ -0,0 +1,137 @@
{
"(detected)": "(εντοπίστηκε)",
"About Readest": "Σχετικά με το Readest",
"Add your notes here...": "Προσθέστε τις σημειώσεις σας εδώ...",
"Animation": "Κινούμενα σχέδια",
"Annotate": "Σχολιασμός",
"Apply": "Εφαρμογή",
"Are you sure to delete the selected books?": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τα επιλεγμένα βιβλία;",
"Auto Mode": "Αυτόματη λειτουργία",
"Behavior": "Συμπεριφορά",
"Book": "Βιβλίο",
"Book Cover": "Εξώφυλλο βιβλίου",
"Bookmark": "Σελιδοδείκτης",
"Cancel": "Ακύρωση",
"Chapter": "Κεφάλαιο",
"Cherry": "Κερασί",
"Color": "Χρώμα",
"Confirm": "Επιβεβαίωση",
"Confirm Deletion": "Επιβεβαίωση διαγραφής",
"Copied to notebook": "Αντιγράφηκε στο σημειωματάριο",
"Copy": "Αντιγραφή",
"Custom CSS": "Προσαρμοσμένο CSS",
"Dark Mode": "Σκοτεινή λειτουργία",
"Default": "Προεπιλογή",
"Default Font": "Προεπιλεγμένη γραμματοσειρά",
"Default Font Size": "Προεπιλεγμένο μέγεθος γραμματοσειράς",
"Delete": "Διαγραφή",
"Delete Highlight": "Διαγραφή επισήμανσης",
"Dictionary": "Λεξικό",
"Disable Click-to-Flip": "Απενεργοποίηση κλικ για γύρισμα σελίδας",
"Download Readest": "Λήψη Readest",
"Edit": "Επεξεργασία",
"Enter your custom CSS here...": "Εισάγετε το προσαρμοσμένο CSS εδώ...",
"Excerpts": "Αποσπάσματα",
"Failed to import book(s): {{filenames}}": "Αποτυχία εισαγωγής βιβλίου(ων): {{filenames}}",
"Fast": "Γρήγορα",
"Font": "Γραμματοσειρά",
"Font & Layout": "Γραμματοσειρά & Διάταξη",
"Font Face": "Τύπος γραμματοσειράς",
"Font Family": "Οικογένεια γραμματοσειράς",
"Font Size": "Μέγεθος γραμματοσειράς",
"From Local File": "Από τοπικό αρχείο",
"Full Justification": "Πλήρης στοίχιση",
"Global Settings": "Γενικές ρυθμίσεις",
"Go Back": "Πίσω",
"Go Forward": "Μπροστά",
"Go Left": "Αριστερά",
"Go Right": "Δεξιά",
"Grass": "Γρασίδι",
"Gray": "Γκρι",
"Gruvbox": "Gruvbox",
"Highlight": "Επισήμανση",
"Horizontal Direction": "Οριζόντια κατεύθυνση",
"Hyphenation": "Συλλαβισμός",
"Identifier:": "Αναγνωριστικό:",
"Import Books": "Εισαγωγή βιβλίων",
"Invert Colors in Dark Mode": "Αντιστροφή χρωμάτων στη σκοτεινή λειτουργία",
"Language:": "Γλώσσα:",
"Layout": "Διάταξη",
"Light Mode": "Φωτεινή λειτουργία",
"Line Height": "Ύψος γραμμής",
"Loading...": "Φόρτωση...",
"Loc. {{currentPage}} / {{totalPage}}": "Θέση {{currentPage}} / {{totalPage}}",
"Logged in": "Συνδεδεμένος",
"Logged in as {{userDisplayName}}": "Συνδεδεμένος ως {{userDisplayName}}",
"Match Case": "Ταίριασμα πεζών-κεφαλαίων",
"Match Diacritics": "Ταίριασμα διακριτικών σημείων",
"Match Whole Words": "Ταίριασμα ολόκληρων λέξεων",
"Maximum Block Size": "Μέγιστο μέγεθος μπλοκ",
"Maximum Inline Size": "Μέγιστο μέγεθος γραμμής",
"Maximum Number of Columns": "Μέγιστος αριθμός στηλών",
"Minimum Font Size": "Ελάχιστο μέγεθος γραμματοσειράς",
"Misc": "Διάφορα",
"Monospace Font": "Γραμματοσειρά σταθερού πλάτους",
"More Info": "Περισσότερες πληροφορίες",
"Nord": "Nord",
"Notebook": "Σημειωματάριο",
"Notes": "Σημειώσεις",
"Open": "Άνοιγμα",
"Open Book": "Άνοιγμα βιβλίου",
"Original Text": "Αρχικό κείμενο",
"Page": "Σελίδα",
"Paging Animation": "Κινούμενα σχέδια σελιδοποίησης",
"Paragraph": "Παράγραφος",
"Parallel Read": "Παράλληλη ανάγνωση",
"Published:": "Δημοσιεύτηκε:",
"Publisher:": "Εκδότης:",
"Reading progress synced": "Ο συγχρονισμός προόδου ανάγνωσης ολοκληρώθηκε",
"Reload Page": "Επαναφόρτωση σελίδας",
"Reveal in File Explorer": "Εμφάνιση στον εξερευνητή αρχείων",
"Reveal in Finder": "Εμφάνιση στον Finder",
"Reveal in Folder": "Εμφάνιση στο φάκελο",
"Sans-Serif Font": "Γραμματοσειρά Sans-Serif",
"Save": "Αποθήκευση",
"Scrolled Mode": "Λειτουργία κύλισης",
"Search": "Αναζήτηση",
"Search books...": "Αναζήτηση βιβλίων...",
"Search...": "Αναζήτηση...",
"Select Book": "Επιλογή βιβλίου",
"Select books": "Επιλογή βιβλίων",
"Select multiple books": "Επιλογή πολλαπλών βιβλίων",
"Sepia": "Σέπια",
"Serif Font": "Γραμματοσειρά Serif",
"Show Book Details": "Εμφάνιση λεπτομερειών βιβλίου",
"Sidebar": "Πλευρική γραμμή",
"Sign In": "Σύνδεση",
"Sign Out": "Αποσύνδεση",
"Sky": "Ουρανός",
"Slow": "Αργά",
"Solarized": "Solarized",
"Speak": "Ομιλία",
"Subjects:": "Θέματα:",
"System Fonts": "Γραμματοσειρές συστήματος",
"Table of Contents": "Πίνακας περιεχομένων",
"TTS not supported in this device": "Η μετατροπή κειμένου σε ομιλία δεν υποστηρίζεται σε αυτή τη συσκευή",
"Theme Color": "Χρώμα θέματος",
"Theme Mode": "Λειτουργία θέματος",
"Translate": "Μετάφραση",
"Translated Text": "Μεταφρασμένο κείμενο",
"Unknown": "Άγνωστο",
"Untitled": "Χωρίς τίτλο",
"Updated:": "Ενημερώθηκε:",
"User avatar": "Εικόνα χρήστη",
"Version {{version}}": "Έκδοση {{version}}",
"Vertical Direction": "Κάθετη κατεύθυνση",
"Welcome to your library. You can import your books here and read them anytime.": "Καλώς ήρθατε στη βιβλιοθήκη σας. Μπορείτε να εισάγετε τα βιβλία σας εδώ και να τα διαβάσετε οποιαδήποτε στιγμή.",
"Wikipedia": "Βικιπαίδεια",
"Writing Mode": "Λειτουργία γραφής",
"Your Library": "Η βιβλιοθήκη σας",
"TTS not supported for PDF": "Η μετατροπή κειμένου σε ομιλία δεν υποστηρίζεται για αρχεία PDF",
"Override Book Font": "Παράκαμψη γραμματοσειράς βιβλίου",
"Vertical Margins (px)": "Κάθετα περιθώρια (px)",
"Horizontal Margins (%)": "Οριζόντια περιθώρια (%)",
"Apply to all books": "Εφαρμογή σε όλα τα βιβλία",
"Apply to this book": "Εφαρμογή σε αυτό το βιβλίο",
"Unable to fetch the translation. Try again later.": "Αδυναμία λήψης της μετάφρασης. Δοκιμάστε ξανά αργότερα."
}
@@ -41,7 +41,6 @@
"Font Size": "Tamaño de fuente",
"From Local File": "Desde archivo local",
"Full Justification": "Justificación completa",
"Gaps (%)": "Espacios (%)",
"Global Settings": "Configuración global",
"Go Back": "Volver",
"Go Forward": "Avanzar",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "Pos. {{currentPage}} / {{totalPage}}",
"Logged in": "Sesión iniciada",
"Logged in as {{userDisplayName}}": "Sesión iniciada como {{userDisplayName}}",
"Margins (px)": "Márgenes (px)",
"Match Case": "Coincidir mayúsculas y minúsculas",
"Match Diacritics": "Coincidir diacríticos",
"Match Whole Words": "Coincidir palabras completas",
@@ -81,7 +79,6 @@
"Open": "Abrir",
"Open Book": "Abrir libro",
"Original Text": "Texto original",
"Override Publisher Font": "Sobrescribir fuente del editor",
"Page": "Página",
"Paging Animation": "Animación de paginación",
"Paragraph": "Párrafo",
@@ -130,5 +127,11 @@
"Wikipedia": "Wikipedia",
"Writing Mode": "Modo de escritura",
"Your Library": "Tu biblioteca",
"TTS not supported for PDF": "TTS no es compatible con PDF"
"TTS not supported for PDF": "TTS no es compatible con PDF",
"Override Book Font": "Sobrescribir fuente del libro",
"Vertical Margins (px)": "Márgenes verticales (px)",
"Horizontal Margins (%)": "Márgenes horizontales (%)",
"Apply to all books": "Aplicar a todos los libros",
"Apply to this book": "Aplicar a este libro",
"Unable to fetch the translation. Try again later.": "No se puede obtener la traducción. Inténtalo de nuevo más tarde."
}
@@ -41,7 +41,6 @@
"Font Size": "Taille de police",
"From Local File": "Depuis un fichier local",
"Full Justification": "Justification complète",
"Gaps (%)": "Espaces (%)",
"Global Settings": "Paramètres globaux",
"Go Back": "Retour",
"Go Forward": "Avancer",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "Pos. {{currentPage}} / {{totalPage}}",
"Logged in": "Connecté",
"Logged in as {{userDisplayName}}": "Connecté en tant que {{userDisplayName}}",
"Margins (px)": "Marges (px)",
"Match Case": "Respecter la casse",
"Match Diacritics": "Respecter les accents",
"Match Whole Words": "Mots entiers",
@@ -81,7 +79,6 @@
"Open": "Ouvrir",
"Open Book": "Ouvrir le livre",
"Original Text": "Texte original",
"Override Publisher Font": "Remplacer la police de l'éditeur",
"Page": "Page",
"Paging Animation": "Animation de page",
"Paragraph": "Paragraphe",
@@ -130,5 +127,11 @@
"Wikipedia": "Wikipédia",
"Writing Mode": "Mode écriture",
"Your Library": "Votre bibliothèque",
"TTS not supported for PDF": "La synthèse vocale n'est pas prise en charge pour les fichiers PDF"
"TTS not supported for PDF": "La synthèse vocale n'est pas prise en charge pour les fichiers PDF",
"Override Book Font": "Remplacer la police du livre",
"Vertical Margins (px)": "Marges verticales (px)",
"Horizontal Margins (%)": "Marges horizontales (%)",
"Apply to all books": "Appliquer à tous les livres",
"Apply to this book": "Appliquer à ce livre",
"Unable to fetch the translation. Try again later.": "Impossible de récupérer la traduction. Réessayez plus tard."
}
@@ -41,7 +41,6 @@
"Font Size": "Ukuran Font",
"From Local File": "Dari File Lokal",
"Full Justification": "Rata Penuh",
"Gaps (%)": "Jarak (%)",
"Global Settings": "Pengaturan Global",
"Go Back": "Kembali",
"Go Forward": "Maju",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "Lok. {{currentPage}} / {{totalPage}}",
"Logged in": "Sudah Masuk",
"Logged in as {{userDisplayName}}": "Masuk sebagai {{userDisplayName}}",
"Margins (px)": "Margin (px)",
"Match Case": "Cocokkan Huruf Besar/Kecil",
"Match Diacritics": "Cocokkan Diakritik",
"Match Whole Words": "Cocokkan Kata Utuh",
@@ -81,7 +79,6 @@
"Open": "Buka",
"Open Book": "Buka Buku",
"Original Text": "Teks Asli",
"Override Publisher Font": "Ganti Font Penerbit",
"Page": "Halaman",
"Paging Animation": "Animasi Halaman",
"Paragraph": "Paragraf",
@@ -130,5 +127,11 @@
"Wikipedia": "Wikipedia",
"Writing Mode": "Mode Menulis",
"Your Library": "Perpustakaan Anda",
"TTS not supported for PDF": "TTS tidak didukung untuk PDF"
"TTS not supported for PDF": "TTS tidak didukung untuk PDF",
"Override Book Font": "Ganti Font Buku",
"Vertical Margins (px)": "Margin Vertikal (px)",
"Horizontal Margins (%)": "Margin Horizontal (%)",
"Apply to all books": "Terapkan ke semua buku",
"Apply to this book": "Terapkan ke buku ini",
"Unable to fetch the translation. Try again later.": "Tidak dapat mengambil terjemahan. Coba lagi nanti."
}
@@ -41,7 +41,6 @@
"Font Size": "Dimensione carattere",
"From Local File": "Da file locale",
"Full Justification": "Giustificazione completa",
"Gaps (%)": "Spaziature (%)",
"Global Settings": "Impostazioni globali",
"Go Back": "Indietro",
"Go Forward": "Avanti",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "Pos. {{currentPage}} / {{totalPage}}",
"Logged in": "Accesso effettuato",
"Logged in as {{userDisplayName}}": "Accesso effettuato come {{userDisplayName}}",
"Margins (px)": "Margini (px)",
"Match Case": "Maiuscole/minuscole",
"Match Diacritics": "Corrispondenza diacritici",
"Match Whole Words": "Parole intere",
@@ -81,7 +79,6 @@
"Open": "Apri",
"Open Book": "Apri libro",
"Original Text": "Testo originale",
"Override Publisher Font": "Sostituisci font editore",
"Page": "Pagina",
"Paging Animation": "Animazione cambio pagina",
"Paragraph": "Paragrafo",
@@ -130,5 +127,11 @@
"Wikipedia": "Wikipedia",
"Writing Mode": "Modalità scrittura",
"Your Library": "La tua biblioteca",
"TTS not supported for PDF": "TTS non supportato per PDF"
"TTS not supported for PDF": "TTS non supportato per PDF",
"Override Book Font": "Sovrascrivi font libro",
"Vertical Margins (px)": "Margini verticali (px)",
"Horizontal Margins (%)": "Margini orizzontali (%)",
"Apply to all books": "Applica a tutti i libri",
"Apply to this book": "Applica a questo libro",
"Unable to fetch the translation. Try again later.": "Impossibile recuperare la traduzione. Riprova più tardi."
}
@@ -41,7 +41,6 @@
"Font Size": "フォントサイズ",
"From Local File": "ローカルファイルから",
"Full Justification": "両端揃え",
"Gaps (%)": "間隔 (%)",
"Global Settings": "全体設定",
"Go Back": "戻る",
"Go Forward": "進む",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "位置 {{currentPage}} / {{totalPage}}",
"Logged in": "ログイン済み",
"Logged in as {{userDisplayName}}": "{{userDisplayName}}としてログイン中",
"Margins (px)": "余白 (px)",
"Match Case": "大文字・小文字を区別",
"Match Diacritics": "アクセント記号を区別",
"Match Whole Words": "単語全体を一致",
@@ -81,7 +79,6 @@
"Open": "開く",
"Open Book": "書籍を開く",
"Original Text": "原文",
"Override Publisher Font": "出版社フォントを上書き",
"Page": "ページ",
"Paging Animation": "ページめくりアニメーション",
"Paragraph": "段落",
@@ -130,5 +127,11 @@
"Wikipedia": "ウィキペディア",
"Writing Mode": "書き込みモード",
"Your Library": "ライブラリー",
"TTS not supported for PDF": "PDFではTTSがサポートされていません"
"TTS not supported for PDF": "PDFではTTSがサポートされていません",
"Override Book Font": "書籍フォントを上書き",
"Vertical Margins (px)": "縦マージン(px",
"Horizontal Margins (%)": "横マージン(%",
"Apply to all books": "すべての書籍に適用",
"Apply to this book": "この書籍に適用",
"Unable to fetch the translation. Try again later.": "翻訳を取得できません。後で再試行してください。"
}
@@ -41,7 +41,6 @@
"Font Size": "글꼴 크기",
"From Local File": "로컬 파일에서",
"Full Justification": "양쪽 정렬",
"Gaps (%)": "간격 (%)",
"Global Settings": "전역 설정",
"Go Back": "뒤로",
"Go Forward": "앞으로",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "위치 {{currentPage}} / {{totalPage}}",
"Logged in": "로그인됨",
"Logged in as {{userDisplayName}}": "{{userDisplayName}}(으)로 로그인됨",
"Margins (px)": "여백 (px)",
"Match Case": "대소문자 구분",
"Match Diacritics": "발음 구별 부호 구분",
"Match Whole Words": "전체 단어 일치",
@@ -81,7 +79,6 @@
"Open": "열기",
"Open Book": "책 열기",
"Original Text": "원본 텍스트",
"Override Publisher Font": "출판사 글꼴 재정의",
"Page": "페이지",
"Paging Animation": "페이지 넘김 애니메이션",
"Paragraph": "단락",
@@ -130,5 +127,11 @@
"Wikipedia": "위키피디아",
"Writing Mode": "글쓰기 모드",
"Your Library": "내 서재",
"TTS not supported for PDF": "PDF에서 TTS가 지원되지 않음"
"TTS not supported for PDF": "PDF에서 TTS가 지원되지 않음",
"Override Book Font": "책 글꼴 덮어쓰기",
"Vertical Margins (px)": "수직 여백 (px)",
"Horizontal Margins (%)": "수평 여백 (%)",
"Apply to all books": "모든 책에 적용",
"Apply to this book": "이 책에 적용",
"Unable to fetch the translation. Try again later.": "번역을 가져올 수 없습니다. 나중에 다시 시도하세요."
}
@@ -41,7 +41,6 @@
"Font Size": "Tamanho da Fonte",
"From Local File": "Do Arquivo Local",
"Full Justification": "Justificação Completa",
"Gaps (%)": "Espaçamentos (%)",
"Global Settings": "Configurações Globais",
"Go Back": "Voltar",
"Go Forward": "Avançar",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "Loc. {{currentPage}} / {{totalPage}}",
"Logged in": "Conectado",
"Logged in as {{userDisplayName}}": "Conectado como {{userDisplayName}}",
"Margins (px)": "Margens (px)",
"Match Case": "Diferenciar Maiúsculas e Minúsculas",
"Match Diacritics": "Corresponder Acentos",
"Match Whole Words": "Corresponder Palavras Inteiras",
@@ -81,7 +79,6 @@
"Open": "Abrir",
"Open Book": "Abrir Livro",
"Original Text": "Texto Original",
"Override Publisher Font": "Substituir Fonte do Editor",
"Page": "Página",
"Paging Animation": "Animação de Página",
"Paragraph": "Parágrafo",
@@ -130,5 +127,11 @@
"Wikipedia": "Wikipédia",
"Writing Mode": "Modo de Escrita",
"Your Library": "Sua Biblioteca",
"TTS not supported for PDF": "TTS não suportado para PDF"
"TTS not supported for PDF": "TTS não suportado para PDF",
"Override Book Font": "Sobrescrever Fonte do Livro",
"Vertical Margins (px)": "Margens Verticais (px)",
"Horizontal Margins (%)": "Margens Horizontais (%)",
"Apply to all books": "Aplicar a todos os livros",
"Apply to this book": "Aplicar a este livro",
"Unable to fetch the translation. Try again later.": "Não foi possível buscar a tradução. Tente novamente mais tarde."
}
@@ -41,7 +41,6 @@
"Font Size": "Размер шрифта",
"From Local File": "Из локального файла",
"Full Justification": "Полное выравнивание",
"Gaps (%)": "Промежутки (%)",
"Global Settings": "Общие настройки",
"Go Back": "Назад",
"Go Forward": "Вперёд",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "Поз. {{currentPage}} / {{totalPage}}",
"Logged in": "Выполнен вход",
"Logged in as {{userDisplayName}}": "Вход выполнен как {{userDisplayName}}",
"Margins (px)": "Отступы (px)",
"Match Case": "Учитывать регистр",
"Match Diacritics": "Учитывать диакритические знаки",
"Match Whole Words": "Только целые слова",
@@ -81,7 +79,6 @@
"Open": "Открыть",
"Open Book": "Открыть книгу",
"Original Text": "Оригинальный текст",
"Override Publisher Font": "Переопределить шрифт издателя",
"Page": "Страница",
"Paging Animation": "Анимация перелистывания",
"Paragraph": "Абзац",
@@ -130,5 +127,11 @@
"Wikipedia": "Википедия",
"Writing Mode": "Режим записи",
"Your Library": "Ваша библиотека",
"TTS not supported for PDF": "TTS не поддерживается для PDF"
"TTS not supported for PDF": "TTS не поддерживается для PDF",
"Override Book Font": "Переопределить шрифт книги",
"Vertical Margins (px)": "Вертикальные поля (px)",
"Horizontal Margins (%)": "Горизонтальные поля (%)",
"Apply to all books": "Применить ко всем книгам",
"Apply to this book": "Применить к этой книге",
"Unable to fetch the translation. Try again later.": "Не удалось получить перевод. Попробуйте позже."
}
@@ -41,7 +41,6 @@
"Font Size": "Yazı Boyutu",
"From Local File": "Yerel Dosyadan",
"Full Justification": "Tam Hizalama",
"Gaps (%)": "Boşluklar (%)",
"Global Settings": "Genel Ayarlar",
"Go Back": "Geri Git",
"Go Forward": "İleri Git",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "Konum {{currentPage}} / {{totalPage}}",
"Logged in": "Giriş Yapıldı",
"Logged in as {{userDisplayName}}": "{{userDisplayName}} olarak giriş yapıldı",
"Margins (px)": "Kenar Boşlukları (px)",
"Match Case": "Büyük/Küçük Harf Eşleştir",
"Match Diacritics": "Aksan İşaretlerini Eşleştir",
"Match Whole Words": "Tam Kelimeleri Eşleştir",
@@ -81,7 +79,6 @@
"Open": "Aç",
"Open Book": "Kitabı Aç",
"Original Text": "Orijinal Metin",
"Override Publisher Font": "Yayıncı Yazı Tipini Geçersiz Kıl",
"Page": "Sayfa",
"Paging Animation": "Sayfa Çevirme Animasyonu",
"Paragraph": "Paragraf",
@@ -130,5 +127,11 @@
"Wikipedia": "Vikipedi",
"Writing Mode": "Yazma Modu",
"Your Library": "Kütüphaneniz",
"TTS not supported for PDF": "PDF için TTS desteklenmiyor"
"TTS not supported for PDF": "PDF için TTS desteklenmiyor",
"Override Book Font": "Kitap Yazı Tipini Geçersiz Kıl",
"Vertical Margins (px)": "Dikey Kenar Boşlukları (px)",
"Horizontal Margins (%)": "Yatay Kenar Boşlukları (%)",
"Apply to all books": "Tüm kitaplara uygula",
"Apply to this book": "Bu kitaba uygula",
"Unable to fetch the translation. Try again later.": "Çeviri alınamıyor. Daha sonra tekrar deneyin."
}
@@ -41,7 +41,6 @@
"Font Size": "Cỡ chữ",
"From Local File": "Từ tệp cục bộ",
"Full Justification": "Căn đều hai bên",
"Gaps (%)": "Khoảng cách (%)",
"Global Settings": "Cài đặt chung",
"Go Back": "Quay lại",
"Go Forward": "Tiến tới",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "Vị trí {{currentPage}} / {{totalPage}}",
"Logged in": "Đã đăng nhập",
"Logged in as {{userDisplayName}}": "Đăng nhập với tên {{userDisplayName}}",
"Margins (px)": "Lề (px)",
"Match Case": "Phân biệt chữ hoa/thường",
"Match Diacritics": "Phân biệt dấu",
"Match Whole Words": "Khớp toàn bộ từ",
@@ -81,7 +79,6 @@
"Open": "Mở",
"Open Book": "Mở sách",
"Original Text": "Văn bản gốc",
"Override Publisher Font": "Ghi đè phông chữ của nhà xuất bản",
"Page": "Trang",
"Paging Animation": "Hiệu ứng lật trang",
"Paragraph": "Đoạn văn",
@@ -130,5 +127,11 @@
"Wikipedia": "Wikipedia",
"Writing Mode": "Chế độ viết",
"Your Library": "Thư viện của bạn",
"TTS not supported for PDF": "TTS không được hỗ trợ cho PDF"
"TTS not supported for PDF": "TTS không được hỗ trợ cho PDF",
"Override Book Font": "Ghi đè phông chữ sách",
"Vertical Margins (px)": "Lề dọc (px)",
"Horizontal Margins (%)": "Lề ngang (%)",
"Apply to all books": "Áp dụng cho tất cả sách",
"Apply to this book": "Áp dụng cho sách này",
"Unable to fetch the translation. Try again later.": "Không thể tải dịch vụ. Vui lòng thử lại sau."
}
@@ -41,7 +41,6 @@
"Font Size": "字号",
"From Local File": "从本地文件导入",
"Full Justification": "两端对齐",
"Gaps (%)": "间距 (%)",
"Global Settings": "全局设置",
"Go Back": "返回",
"Go Forward": "前进",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "位置 {{currentPage}} / {{totalPage}}",
"Logged in": "已登录",
"Logged in as {{userDisplayName}}": "{{userDisplayName}} 已登录",
"Margins (px)": "页边距 (px)",
"Match Case": "匹配大小写",
"Match Diacritics": "匹配重音符号",
"Match Whole Words": "匹配整个单词",
@@ -81,7 +79,6 @@
"Open": "打开",
"Open Book": "打开书籍",
"Original Text": "原文",
"Override Publisher Font": "覆盖内置字体",
"Page": "页面",
"Paging Animation": "翻页动画",
"Paragraph": "段落",
@@ -130,5 +127,11 @@
"Wikipedia": "维基百科",
"Writing Mode": "排版模式",
"Your Library": "书库",
"TTS not supported for PDF": "PDF 文档暂不支持 TTS"
"TTS not supported for PDF": "PDF 文档暂不支持 TTS",
"Override Book Font": "覆盖书籍字体",
"Vertical Margins (px)": "垂直边距(px",
"Horizontal Margins (%)": "水平边距(%",
"Apply to all books": "应用于所有书籍",
"Apply to this book": "应用于此书籍",
"Unable to fetch the translation. Try again later.": "无法获取翻译,请稍后重试。"
}
@@ -41,7 +41,6 @@
"Font Size": "字號",
"From Local File": "從本地檔案導入",
"Full Justification": "兩端對齊",
"Gaps (%)": "間距 (%)",
"Global Settings": "全局設置",
"Go Back": "返回",
"Go Forward": "前進",
@@ -64,7 +63,6 @@
"Loc. {{currentPage}} / {{totalPage}}": "位置 {{currentPage}} / {{totalPage}}",
"Logged in": "已登入",
"Logged in as {{userDisplayName}}": "{{userDisplayName}} 已登入",
"Margins (px)": "頁邊距 (px)",
"Match Case": "匹配大小寫",
"Match Diacritics": "匹配重音符號",
"Match Whole Words": "匹配整個單詞",
@@ -81,7 +79,6 @@
"Open": "打開",
"Open Book": "打開書籍",
"Original Text": "原文",
"Override Publisher Font": "覆蓋內置字體",
"Page": "頁面",
"Paging Animation": "翻頁動畫",
"Paragraph": "段落",
@@ -130,5 +127,11 @@
"Wikipedia": "維基百科",
"Writing Mode": "排版模式",
"Your Library": "書庫",
"TTS not supported for PDF": "PDF 文檔暫不支持 TTS"
"TTS not supported for PDF": "PDF 文檔暫不支持 TTS",
"Override Book Font": "覆蓋書籍字體",
"Vertical Margins (px)": "垂直邊距(px",
"Horizontal Margins (%)": "水平邊距(%",
"Apply to all books": "應用於所有書籍",
"Apply to this book": "應用於此書籍",
"Unable to fetch the translation. Try again later.": "無法獲取翻譯,請稍後再試。"
}
+4
View File
@@ -1,5 +1,9 @@
{
"releases": {
"0.9.6": {
"date": "2025-01-18",
"notes": ["Various enhancements for mobile platforms", "Fix DeepL translation API"]
},
"0.9.5": {
"date": "2025-01-15",
"notes": [
+7 -1
View File
@@ -1,5 +1,6 @@
'use client';
import clsx from 'clsx';
import * as React from 'react';
import { useState, useRef, useEffect, Suspense } from 'react';
import { useRouter } from 'next/navigation';
@@ -233,7 +234,12 @@ const LibraryPage = () => {
}
return (
<div className='library-page rounded-window bg-base-200/50 text-base-content flex h-full min-h-screen select-none flex-col overflow-hidden'>
<div
className={clsx(
'library-page bg-base-200/50 text-base-content flex h-dvh select-none flex-col overflow-hidden',
isTauriAppPlatform() && 'rounded-window',
)}
>
<div className='fixed top-0 z-40 w-full'>
<LibraryHeader
isSelectMode={isSelectMode}
@@ -4,6 +4,7 @@ import { useSettingsStore } from '@/store/settingsStore';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { isTauriAppPlatform } from '@/services/environment';
import FoliateViewer from './FoliateViewer';
import getGridTemplate from '@/utils/grid';
import SectionInfo from './SectionInfo';
@@ -59,7 +60,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
<div
id={`gridcell-${bookKey}`}
key={bookKey}
className='rounded-window relative h-full w-full overflow-hidden'
className={`${isTauriAppPlatform() ? 'rounded-window' : ''} relative h-full w-full overflow-hidden`}
>
{isBookmarked && <Ribbon width={marginGap} />}
<HeaderBar
@@ -8,6 +8,7 @@ import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTranslation } from '@/hooks/useTranslation';
import { eventDispatcher } from '@/utils/event';
import { isTauriAppPlatform } from '@/services/environment';
import Button from '@/components/Button';
interface FooterBarProps {
@@ -60,8 +61,9 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
<div
className={clsx(
'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center px-4',
'shadow-xs bg-base-100 rounded-window-bottom-right transition-opacity duration-300',
!isSideBarVisible && 'rounded-window-bottom-left',
'shadow-xs bg-base-100 transition-opacity duration-300',
isTauriAppPlatform() && 'rounded-window-bottom-right',
!isSideBarVisible && isTauriAppPlatform() && 'rounded-window-bottom-left',
isHoveredAnim && 'hover-bar-anim',
hoveredBookKey === bookKey ? `opacity-100` : `opacity-0`,
)}
@@ -5,12 +5,14 @@ import { PiDotsThreeVerticalBold } from 'react-icons/pi';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { isTauriAppPlatform } from '@/services/environment';
import useTrafficLight from '@/hooks/useTrafficLight';
import WindowButtons from '@/components/WindowButtons';
import Dropdown from '@/components/Dropdown';
import SidebarToggler from './SidebarToggler';
import BookmarkToggler from './BookmarkToggler';
import NotebookToggler from './NotebookToggler';
import SettingsToggler from './SettingsToggler';
import ViewMenu from './ViewMenu';
interface HeaderBarProps {
@@ -48,8 +50,9 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
className={clsx(
`header-bar absolute top-0 z-10 flex h-11 w-full items-center pr-4`,
isTrafficLightVisible && isTopLeft && !isSideBarVisible ? 'pl-16' : 'pl-4',
`shadow-xs bg-base-100 rounded-window-top-right transition-opacity duration-300`,
!isSideBarVisible && 'rounded-window-top-left',
`shadow-xs bg-base-100 transition-opacity duration-300`,
isTauriAppPlatform() && 'rounded-window-top-right',
!isSideBarVisible && isTauriAppPlatform() && 'rounded-window-top-left',
isHoveredAnim && 'hover-bar-anim',
hoveredBookKey === bookKey || isDropdownOpen ? `visible` : `opacity-0`,
isDropdownOpen && 'header-bar-pinned',
@@ -57,16 +60,17 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
onMouseEnter={() => setHoveredBookKey(bookKey)}
onMouseLeave={() => setHoveredBookKey('')}
>
<div className='sidebar-bookmark-toggler flex h-full items-center'>
<div className='sidebar-bookmark-toggler flex h-full items-center space-x-2 sm:space-x-4'>
<SidebarToggler bookKey={bookKey} />
<BookmarkToggler bookKey={bookKey} />
</div>
<div className='header-title flex flex-1 items-center justify-center'>
<div className='header-title pointer-events-none absolute inset-0 hidden items-center justify-center sm:flex'>
<h2 className='line-clamp-1 max-w-[80%] text-center text-xs font-semibold'>{bookTitle}</h2>
</div>
<div className='flex h-full items-center space-x-2'>
<div className='ml-auto flex h-full items-center space-x-2 sm:space-x-4'>
<SettingsToggler />
<NotebookToggler bookKey={bookKey} />
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
@@ -1,5 +1,5 @@
import React from 'react';
import { TbLayoutSidebarRight, TbLayoutSidebarRightFilled } from 'react-icons/tb';
import { LuNotebookPen } from 'react-icons/lu';
import { useSidebarStore } from '@/store/sidebarStore';
import { useNotebookStore } from '@/store/notebookStore';
@@ -26,9 +26,9 @@ const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
<Button
icon={
sideBarBookKey == bookKey && isNotebookVisible ? (
<TbLayoutSidebarRightFilled size={20} className='text-base-content' />
<LuNotebookPen size={16} className='text-base-content' />
) : (
<TbLayoutSidebarRight size={20} className='text-base-content' />
<LuNotebookPen size={16} className='text-base-content' />
)
}
onClick={handleToggleSidebar}
@@ -1,11 +1,13 @@
'use client';
import clsx from 'clsx';
import * as React from 'react';
import { useEffect, Suspense, useRef } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore';
import { isTauriAppPlatform } from '@/services/environment';
import { AboutWindow } from '@/components/AboutWindow';
import { Toast } from '@/components/Toast';
import ReaderContent from './ReaderContent';
@@ -33,7 +35,12 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
return (
library.length > 0 &&
settings.globalReadSettings && (
<div className='reader-page rounded-window bg-base-100 text-base-content min-h-screen select-none'>
<div
className={clsx(
`reader-page bg-base-100 text-base-content select-none`,
isTauriAppPlatform() && 'rounded-window',
)}
>
<Suspense>
<ReaderContent ids={ids} settings={settings} />
<AboutWindow />
@@ -145,7 +145,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
setTimeout(() => setLoading(true), 300);
return (
loading && (
<div className={'hero hero-content min-h-screen'}>
<div className={'hero hero-content h-dvh'}>
<Spinner loading={true} />
</div>
)
@@ -153,7 +153,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
}
return (
<div className='flex h-screen'>
<div className='flex h-dvh'>
<SideBar onGoToLibrary={handleCloseBooksToLibrary} />
<BooksGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
<TTSControl />
@@ -0,0 +1,24 @@
import React from 'react';
import { RiFontSize } from 'react-icons/ri';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import Button from '@/components/Button';
const SettingsToggler = () => {
const _ = useTranslation();
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
const handleToggleSettings = () => {
setFontLayoutSettingsDialogOpen(!isFontLayoutSettingsDialogOpen);
};
return (
<Button
icon={<RiFontSize size={16} className='text-base-content' />}
onClick={handleToggleSettings}
tooltip={_('Font & Layout')}
tooltipDirection='bottom'
></Button>
);
};
export default SettingsToggler;
@@ -92,7 +92,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
<button
onClick={zoomOut}
className={clsx(
'hover:bg-base-200 text-base-content rounded-full p-2',
'hover:bg-base-300 text-base-content rounded-full p-2',
zoomLevel <= MIN_ZOOM_LEVEL && 'btn-disabled text-gray-400',
)}
>
@@ -100,7 +100,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
</button>
<button
className={clsx(
'hover:bg-base-200 text-base-content h-8 min-h-8 w-[50%] rounded-md p-1 text-center',
'hover:bg-base-300 text-base-content h-8 min-h-8 w-[50%] rounded-md p-1 text-center',
)}
onClick={resetZoom}
>
@@ -109,7 +109,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
<button
onClick={zoomIn}
className={clsx(
'hover:bg-base-200 text-base-content rounded-full p-2',
'hover:bg-base-300 text-base-content rounded-full p-2',
zoomLevel >= MAX_ZOOM_LEVEL && 'btn-disabled text-gray-400',
)}
>
@@ -1,9 +1,10 @@
import React, { useEffect, useState } from 'react';
import Popup from '@/components/Popup';
import { Position } from '@/utils/sel';
import { getAPIBaseUrl } from '@/services/environment';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { isWebAppPlatform } from '@/services/environment';
import { useAuth } from '@/context/AuthContext';
const LANGUAGES = {
AUTO: 'Auto Detect',
@@ -23,6 +24,8 @@ const LANGUAGES = {
'ZH-HANT': 'Chinese (Traditional)',
};
const DEEPL_API_ENDPOINT = getAPIBaseUrl() + '/deepl/translate';
interface DeepLPopupProps {
text: string;
position: Position;
@@ -39,6 +42,7 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
popupHeight,
}) => {
const _ = useTranslation();
const { token } = useAuth();
const { settings, setSettings } = useSettingsStore();
const [sourceLang, setSourceLang] = useState('AUTO');
const [targetLang, setTargetLang] = useState(settings.globalReadSettings.translateTargetLang);
@@ -63,23 +67,12 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
setError(null);
setTranslation(null);
if (!process.env['NEXT_PUBLIC_DEEPL_API_KEY']) {
console.error('DeepL API key not found. Set NEXT_PUBLIC_DEEPL_API_KEY in .env.local');
}
const { fetch, url } = isWebAppPlatform()
? { fetch: window.fetch, url: '/api/deepl/translate' }
: {
fetch: (await import('@tauri-apps/plugin-http')).fetch,
url: 'https://api-free.deepl.com/v2/translate',
};
try {
const response = await fetch(url, {
const response = await fetch(DEEPL_API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `DeepL-Auth-Key ${process.env['NEXT_PUBLIC_DEEPL_API_KEY']}`,
Authorization: `Bearer ${token ?? ''}`,
},
body: JSON.stringify({
text: [text],
@@ -106,14 +99,15 @@ const DeepLPopup: React.FC<DeepLPopupProps> = ({
setTranslation(translatedText);
} catch (err) {
console.error(err);
setError('Unable to fetch the translation. Try again later.');
setError(_('Unable to fetch the translation. Try again later.'));
} finally {
setLoading(false);
}
};
fetchTranslation();
}, [text, sourceLang, targetLang]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [text, token, sourceLang, targetLang]);
return (
<div>
@@ -15,7 +15,7 @@ const PopupButton: React.FC<PopupButtonProps> = ({ showTooltip, tooltipText, Ico
};
return (
<div
className='tooltip tooltip-bottom'
className='lg:tooltip lg:tooltip-bottom'
data-tip={!buttonClicked && showTooltip ? tooltipText : null}
>
<button
@@ -1,26 +1,44 @@
import clsx from 'clsx';
import React from 'react';
import { FiSearch } from 'react-icons/fi';
import { LuNotebookPen } from 'react-icons/lu';
import { MdOutlinePushPin, MdPushPin } from 'react-icons/md';
import { useTranslation } from '@/hooks/useTranslation';
const NotebookHeader: React.FC<{
isPinned: boolean;
handleClose: () => void;
handleTogglePin: () => void;
}> = ({ isPinned, handleTogglePin }) => {
}> = ({ isPinned, handleClose, handleTogglePin }) => {
const _ = useTranslation();
return (
<div className='notebook-header relative flex h-11 items-center px-3'>
<div className='absolute inset-0 flex items-center justify-center'>
<div className='absolute inset-0 flex items-center justify-center space-x-2'>
<LuNotebookPen size={14} />
<div className='notebook-title text-sm font-medium'>{_('Notebook')}</div>
</div>
<div className='z-10 flex items-center space-x-3'>
<div className='z-10 flex items-center gap-x-3'>
<button
onClick={handleTogglePin}
className={`${isPinned ? 'bg-base-300' : 'bg-base-300/65'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
className={clsx(
'btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 sm:flex',
isPinned ? 'bg-base-300' : 'bg-base-300/65',
)}
>
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
</button>
<button
onClick={handleClose}
className={'bg-base-300/65 btn btn-ghost btn-circle h-6 min-h-6 w-6 sm:hidden'}
>
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
<path
fill='currentColor'
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
/>
</svg>
</button>
<button className='btn btn-ghost left-0 h-8 min-h-8 w-8 p-0'>
<FiSearch size={18} />
</button>
@@ -8,13 +8,15 @@ import { useSidebarStore } from '@/store/sidebarStore';
import { useNotebookStore } from '@/store/notebookStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useEnv } from '@/context/EnvContext';
import useDragBar from '../../hooks/useDragBar';
import NotebookHeader from './Header';
import NoteEditor from './NoteEditor';
import { TextSelection } from '@/utils/sel';
import { BookNote } from '@/types/book';
import { uniqueId } from '@/utils/misc';
import { eventDispatcher } from '@/utils/event';
import { isTauriAppPlatform } from '@/services/environment';
import useDragBar from '../../hooks/useDragBar';
import BooknoteItem from '../sidebar/BooknoteItem';
import NotebookHeader from './Header';
import NoteEditor from './NoteEditor';
const MIN_NOTEBOOK_WIDTH = 0.15;
const MAX_NOTEBOOK_WIDTH = 0.45;
@@ -31,10 +33,23 @@ const Notebook: React.FC = ({}) => {
const { setNotebookWidth, setNotebookVisible, toggleNotebookPin } = useNotebookStore();
const { setNotebookNewAnnotation, setNotebookEditAnnotation } = useNotebookStore();
const onNavigateEvent = async () => {
const pinButton = document.querySelector('.sidebar-pin-btn');
const isPinButtonHidden = !pinButton || window.getComputedStyle(pinButton).display === 'none';
if (isPinButtonHidden) {
setNotebookVisible(false);
}
};
useEffect(() => {
setNotebookWidth(settings.globalReadSettings.notebookWidth);
setNotebookPin(settings.globalReadSettings.isNotebookPinned);
setNotebookVisible(settings.globalReadSettings.isNotebookPinned);
eventDispatcher.on('navigate', onNavigateEvent);
return () => {
eventDispatcher.off('navigate', onNavigateEvent);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -126,7 +141,7 @@ const Notebook: React.FC = ({}) => {
<div
className={clsx(
'notebook-container bg-base-200 right-0 z-20 h-full min-w-60 select-none',
'rounded-window-top-right rounded-window-bottom-right',
isTauriAppPlatform() && 'rounded-window-top-right rounded-window-bottom-right',
!isNotebookPinned && 'shadow-2xl',
)}
style={{
@@ -135,11 +150,23 @@ const Notebook: React.FC = ({}) => {
position: isNotebookPinned ? 'relative' : 'absolute',
}}
>
<style jsx>{`
@media (max-width: 640px) {
.notebook-container {
width: 100%;
min-width: 100%;
}
}
`}</style>
<div
className='drag-bar absolute left-0 top-0 h-full w-0.5 cursor-col-resize'
onMouseDown={handleMouseDown}
/>
<NotebookHeader isPinned={isNotebookPinned} handleTogglePin={handleTogglePin} />
<NotebookHeader
isPinned={isNotebookPinned}
handleClose={() => setNotebookVisible(false)}
handleTogglePin={handleTogglePin}
/>
<div className='max-h-[calc(100vh-44px)] overflow-y-auto px-3'>
<div>{excerptNotes.length > 0 && <p className='pt-1 text-sm'>{_('Excerpts')}</p>}</div>
<ul className=''>
@@ -28,7 +28,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='flex items-center justify-between'>
<h2 className='font-medium'>{_('Theme Mode')}</h2>
<div className='flex gap-2'>
<div className='tooltip tooltip-bottom' data-tip={_('Auto Mode')}>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Auto Mode')}>
<button
className={`btn btn-ghost btn-circle ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => updateThemeMode('auto')}
@@ -37,7 +37,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
</button>
</div>
<div className='tooltip tooltip-bottom' data-tip={_('Light Mode')}>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Light Mode')}>
<button
className={`btn btn-ghost btn-circle ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
onClick={() => updateThemeMode('light')}
@@ -46,7 +46,7 @@ const ColorPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
</button>
</div>
<div className='tooltip tooltip-bottom' data-tip={_('Dark Mode')}>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Dark Mode')}>
<button
className={`btn btn-ghost btn-circle ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
onClick={() => updateThemeMode('dark')}
@@ -29,7 +29,12 @@ const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
<span style={{ minWidth: '20px' }}>
{isFontLayoutSettingsGlobal && <MdCheck size={20} className='text-base-content' />}
</span>
<div className='tooltip' data-tip='Uncheck for current book settings'>
<div
className='lg:tooltip'
data-tip={
isFontLayoutSettingsGlobal ? _('Apply to all books') : _('Apply to this book')
}
>
<span className='ml-2'>{_('Global Settings')}</span>
</div>
</div>
@@ -218,7 +218,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
</div>
<div className='config-item config-item-bottom'>
<span className=''>{_('Override Publisher Font')}</span>
<span className=''>{_('Override Book Font')}</span>
<input
type='checkbox'
className='toggle'
@@ -148,7 +148,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='flex items-center justify-between'>
<h2 className='font-medium'>{_('Writing Mode')}</h2>
<div className='flex gap-2'>
<div className='tooltip tooltip-bottom' data-tip={_('Default')}>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Default')}>
<button
className={`btn btn-ghost btn-circle ${writingMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('auto')}
@@ -157,7 +157,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
</button>
</div>
<div className='tooltip tooltip-bottom' data-tip={_('Horizontal Direction')}>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Horizontal Direction')}>
<button
className={`btn btn-ghost btn-circle ${writingMode === 'horizontal-tb' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('horizontal-tb')}
@@ -166,7 +166,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
</button>
</div>
<div className='tooltip tooltip-bottom' data-tip={_('Vertical Direction')}>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Vertical Direction')}>
<button
className={`btn btn-ghost btn-circle ${writingMode === 'vertical-rl' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('vertical-rl')}
@@ -220,7 +220,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
<div className='divide-base-200 divide-y'>
<NumberInput
className='config-item-top'
label={_('Margins (px)')}
label={_('Vertical Margins (px)')}
value={marginPx}
onChange={setMarginPx}
min={0}
@@ -228,7 +228,7 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
step={4}
/>
<NumberInput
label={_('Gaps (%)')}
label={_('Horizontal Margins (%)')}
value={gapPercent}
onChange={setGapPercent}
min={0}
@@ -1,3 +1,4 @@
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import { BookConfig } from '@/types/book';
import { useSettingsStore } from '@/store/settingsStore';
@@ -18,6 +19,12 @@ import MiscPanel from './MiscPanel';
type SettingsPanelType = 'Font' | 'Layout' | 'Color' | 'Misc';
type TabConfig = {
tab: SettingsPanelType;
icon: React.ElementType;
label: string;
};
const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ bookKey }) => {
const _ = useTranslation();
const [activePanel, setActivePanel] = useState<SettingsPanelType>('Font');
@@ -37,39 +44,47 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const tabConfig = [
{
tab: 'Font',
icon: RiFontSize,
label: _('Font'),
},
{
tab: 'Layout',
icon: RiDashboardLine,
label: _('Layout'),
},
{
tab: 'Color',
icon: VscSymbolColor,
label: _('Color'),
},
{
tab: 'Misc',
icon: IoAccessibilityOutline,
label: _('Misc'),
},
] as TabConfig[];
return (
<dialog className='modal modal-open min-w-90 w-full !bg-[rgba(0,0,0,0.2)]'>
<div className='modal-box settings-content flex h-[65%] w-1/2 min-w-[480px] max-w-full flex-col p-0'>
<dialog className='modal modal-open sm:min-w-90 h-full w-full !bg-[rgba(0,0,0,0.2)] sm:w-full'>
<div className='modal-box settings-content flex h-full max-h-full w-full max-w-full flex-col rounded-none p-0 sm:h-[65%] sm:w-1/2 sm:min-w-[480px] sm:max-w-full sm:rounded-2xl'>
<div className='dialog-header bg-base-100 sticky top-0 z-10 flex items-center justify-center px-4 pt-2'>
<div className='dialog-tabs flex h-10 max-w-[80%] flex-grow items-center justify-around'>
<button
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Font' ? 'btn-active' : ''}`}
onClick={() => setActivePanel('Font')}
>
<RiFontSize size={20} className='mr-0' />
{_('Font')}
</button>
<button
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Layout' ? 'btn-active' : ''}`}
onClick={() => setActivePanel('Layout')}
>
<RiDashboardLine size={20} className='mr-0' />
{_('Layout')}
</button>
<button
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Color' ? 'btn-active' : ''}`}
onClick={() => setActivePanel('Color')}
>
<VscSymbolColor size={20} className='mr-0' />
{_('Color')}
</button>
<button
className={`btn btn-ghost text-base-content h-8 min-h-8 px-4 ${activePanel === 'Misc' ? 'btn-active' : ''}`}
onClick={() => setActivePanel('Misc')}
>
<IoAccessibilityOutline size={20} className='mr-0' />
{_('Misc')}
</button>
<div className='dialog-tabs flex h-10 max-w-[75%] flex-grow items-center justify-around sm:max-w-[80%]'>
{tabConfig.map(({ tab, icon: Icon, label }) => (
<button
key={tab}
className={clsx(
'btn btn-ghost text-base-content h-8 min-h-8',
activePanel === tab ? 'btn-active' : '',
)}
onClick={() => setActivePanel(tab)}
>
<Icon size={20} className='mr-0' />
{window.innerWidth >= 500 ? label : ''}
</button>
))}
</div>
<div className='flex h-full items-center justify-end'>
<Dropdown
@@ -1,3 +1,4 @@
import clsx from 'clsx';
import React from 'react';
import Image from 'next/image';
@@ -10,10 +11,11 @@ import { DOWNLOAD_READEST_URL } from '@/services/constants';
import useBooksManager from '../../hooks/useBooksManager';
interface BookMenuProps {
menuClassName?: string;
setIsDropdownOpen?: (isOpen: boolean) => void;
}
const BookMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen }) => {
const _ = useTranslation();
const { library } = useLibraryStore();
const { openParallelView } = useBooksManager();
@@ -39,7 +41,10 @@ const BookMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
return (
<div
tabIndex={0}
className='book-menu dropdown-content dropdown-center border-base-100 z-20 mt-3 w-60 shadow-2xl'
className={clsx(
'book-menu dropdown-content border-base-100 z-20 w-60 shadow-2xl',
menuClassName,
)}
>
<MenuItem label={_('Parallel Read')} noIcon>
<ul className='max-h-60 overflow-y-auto'>
@@ -9,6 +9,7 @@ import { useNotebookStore } from '@/store/notebookStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
import useScrollToItem from '../../hooks/useScrollToItem';
import { eventDispatcher } from '@/utils/event';
interface BooknoteItemProps {
bookKey: string;
@@ -29,6 +30,8 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
const handleClickItem = (event: React.MouseEvent) => {
event.preventDefault();
eventDispatcher.dispatch('navigate', { bookKey, cfi });
getView(bookKey)?.goTo(cfi);
if (note) {
setNotebookVisible(true);
@@ -122,12 +125,16 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
)}
onClick={editNote.bind(null, item)}
>
<div className={clsx(
'align-bottom text-blue-400',
'transition duration-300 ease-in-out',
'group-hover:opacity-100 opacity-0',
'hover:text-blue-600',
)}>{_('Edit')}</div>
<div
className={clsx(
'align-bottom text-blue-400',
'transition duration-300 ease-in-out',
'opacity-0 group-hover:opacity-100',
'hover:text-blue-600',
)}
>
{_('Edit')}
</div>
</button>
)}
<button
@@ -137,12 +144,16 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
)}
onClick={deleteNote.bind(null, item)}
>
<div className={clsx(
'align-bottom text-red-400',
'transition duration-300 ease-in-out',
'group-hover:opacity-100 opacity-0',
'hover:text-red-600',
)}>{_('Delete')}</div>
<div
className={clsx(
'align-bottom text-red-400',
'transition duration-300 ease-in-out',
'opacity-0 group-hover:opacity-100',
'hover:text-red-600',
)}
>
{_('Delete')}
</div>
</button>
</div>
</div>
@@ -12,9 +12,10 @@ const SidebarHeader: React.FC<{
isPinned: boolean;
isSearchBarVisible: boolean;
onGoToLibrary: () => void;
onClose: () => void;
onTogglePin: () => void;
onToggleSearchBar: () => void;
}> = ({ isPinned, isSearchBarVisible, onGoToLibrary, onTogglePin, onToggleSearchBar }) => {
}> = ({ isPinned, isSearchBarVisible, onGoToLibrary, onClose, onTogglePin, onToggleSearchBar }) => {
const { isTrafficLightVisible } = useTrafficLight();
return (
<div
@@ -39,7 +40,11 @@ const SidebarHeader: React.FC<{
<FiSearch size={18} className='text-base-content' />
</button>
<Dropdown
className='dropdown-bottom flex justify-center'
className={clsx(
window.innerWidth < 640 && 'dropdown-end',
'dropdown-bottom flex justify-center',
)}
menuClassName={window.innerWidth < 640 ? 'no-triangle mt-1' : 'dropdown-center mt-3'}
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<MdOutlineMenu size={20} className='fill-base-content' />}
>
@@ -48,10 +53,24 @@ const SidebarHeader: React.FC<{
<div className='right-0 flex h-8 w-8 items-center justify-center'>
<button
onClick={onTogglePin}
className={`${isPinned ? 'bg-base-300' : 'bg-base-300/65'} btn btn-ghost btn-circle h-6 min-h-6 w-6`}
className={clsx(
'sidebar-pin-btn btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 sm:flex',
isPinned ? 'bg-base-300' : 'bg-base-300/65',
)}
>
{isPinned ? <MdPushPin size={14} /> : <MdOutlinePushPin size={14} />}
</button>
<button
onClick={onClose}
className={'bg-base-300/65 btn btn-ghost btn-circle h-6 min-h-6 w-6 sm:hidden'}
>
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
<path
fill='currentColor'
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
/>
</svg>
</button>
</div>
</div>
</div>
@@ -166,7 +166,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
className='w-full bg-transparent p-2 font-sans text-sm font-light focus:outline-none'
/>
<div className='flex h-8 w-8 items-center rounded-r-lg bg-gray-300'>
<div className='bg-base-300 flex h-8 w-8 items-center rounded-r-lg'>
<Dropdown
className='dropdown-bottom flex justify-center'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0 rounded-none rounded-r-lg'
@@ -9,100 +9,68 @@ interface SearchOptionsProps {
setIsDropdownOpen?: (isOpen: boolean) => void;
}
interface OptionProps {
label: string;
isActive: boolean;
onClick: () => void;
}
const Option: React.FC<OptionProps> = ({ label, isActive, onClick }) => (
<button
className='hover:bg-base-300 flex w-full items-center justify-between rounded-md p-2'
onClick={onClick}
>
<div className='flex items-center'>
<span style={{ minWidth: '20px' }}>
{isActive && <MdCheck size={20} className='text-base-content' />}
</span>
<span className='ml-2'>{label}</span>
</div>
</button>
);
const SearchOptions: React.FC<SearchOptionsProps> = ({
searchConfig,
onSearchConfigChanged,
setIsDropdownOpen,
}) => {
const _ = useTranslation();
const handleSetScope = () => {
onSearchConfigChanged({
...searchConfig,
scope: searchConfig.scope === 'book' ? 'section' : 'book',
});
setIsDropdownOpen?.(false);
};
const handleSetMatchCase = () => {
onSearchConfigChanged({ ...searchConfig, matchCase: !searchConfig.matchCase });
setIsDropdownOpen?.(false);
};
const handleSetMatchWholeWords = () => {
onSearchConfigChanged({ ...searchConfig, matchWholeWords: !searchConfig.matchWholeWords });
setIsDropdownOpen?.(false);
};
const handleSetMatchDiacritics = () => {
onSearchConfigChanged({ ...searchConfig, matchDiacritics: !searchConfig.matchDiacritics });
const updateConfig = (key: keyof BookSearchConfig, value: boolean | string) => {
onSearchConfigChanged({ ...searchConfig, [key]: value });
setIsDropdownOpen?.(false);
};
return (
<div
tabIndex={0}
className='book-menu dropdown-content dropdown-center z-20 mt-3 w-56 border bg-white shadow-2xl'
className='book-menu dropdown-content dropdown-center border-base-200 z-20 mt-3 w-56 border shadow-2xl'
>
<button
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
onClick={handleSetScope}
>
<div className='flex items-center'>
<span style={{ minWidth: '20px' }}>
{searchConfig.scope === 'book' && <MdCheck size={20} className='text-base-content' />}
</span>
<span className='ml-2'>{_('Book')}</span>
</div>
</button>
<button
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
onClick={handleSetScope}
>
<div className='flex items-center'>
<span style={{ minWidth: '20px' }}>
{searchConfig.scope === 'section' && (
<MdCheck size={20} className='text-base-content' />
)}
</span>
<span className='ml-2'>{_('Chapter')}</span>
</div>
</button>
<hr className='my-1' />
<button
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
onClick={handleSetMatchCase}
>
<div className='flex items-center'>
<span style={{ minWidth: '20px' }}>
{searchConfig.matchCase && <MdCheck size={20} className='text-base-content' />}
</span>
<span className='ml-2'>{_('Match Case')}</span>
</div>
</button>
<button
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
onClick={handleSetMatchWholeWords}
>
<div className='flex items-center'>
<span style={{ minWidth: '20px' }}>
{searchConfig.matchWholeWords && <MdCheck size={20} className='text-base-content' />}
</span>
<span className='ml-2'>{_('Match Whole Words')}</span>
</div>
</button>
<button
className='flex w-full items-center justify-between rounded-md p-2 hover:bg-gray-100'
onClick={handleSetMatchDiacritics}
>
<div className='flex items-center'>
<span style={{ minWidth: '20px' }}>
{searchConfig.matchDiacritics && <MdCheck size={20} className='text-base-content' />}
</span>
<span className='ml-2'>{_('Match Diacritics')}</span>
</div>
</button>
<Option
label={_('Book')}
isActive={searchConfig.scope === 'book'}
onClick={() => updateConfig('scope', 'book')}
/>
<Option
label={_('Chapter')}
isActive={searchConfig.scope === 'section'}
onClick={() => updateConfig('scope', 'section')}
/>
<hr className='border-base-200 my-1' />
<Option
label={_('Match Case')}
isActive={searchConfig.matchCase}
onClick={() => updateConfig('matchCase', !searchConfig.matchCase)}
/>
<Option
label={_('Match Whole Words')}
isActive={searchConfig.matchWholeWords}
onClick={() => updateConfig('matchWholeWords', !searchConfig.matchWholeWords)}
/>
<Option
label={_('Match Diacritics')}
isActive={searchConfig.matchDiacritics}
onClick={() => updateConfig('matchDiacritics', !searchConfig.matchDiacritics)}
/>
</div>
);
};
@@ -7,6 +7,7 @@ import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { BookSearchResult } from '@/types/book';
import { eventDispatcher } from '@/utils/event';
import { isTauriAppPlatform } from '@/services/environment';
import SidebarHeader from './Header';
import SidebarContent from './Content';
import BookCard from './BookCard';
@@ -38,7 +39,7 @@ const SideBar: React.FC<{
handleSideBarTogglePin,
} = useSidebar(
settings.globalReadSettings.sideBarWidth,
settings.globalReadSettings.isSideBarPinned,
window.innerWidth >= 640 ? settings.globalReadSettings.isSideBarPinned : false,
);
const onSearchEvent = async (event: CustomEvent) => {
@@ -48,10 +49,20 @@ const SideBar: React.FC<{
setSearchTerm(term);
};
const onNavigateEvent = async () => {
const pinButton = document.querySelector('.sidebar-pin-btn');
const isPinButtonHidden = !pinButton || window.getComputedStyle(pinButton).display === 'none';
if (isPinButtonHidden) {
setSideBarVisible(false);
}
};
useEffect(() => {
eventDispatcher.on('search', onSearchEvent);
eventDispatcher.on('navigate', onNavigateEvent);
return () => {
eventDispatcher.off('search', onSearchEvent);
eventDispatcher.off('navigate', onNavigateEvent);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -79,6 +90,7 @@ const SideBar: React.FC<{
useShortcuts({ onToggleSearchBar: handleToggleSearchBar }, [sideBarBookKey]);
const handleSearchResultClick = (cfi: string) => {
onNavigateEvent();
getView(sideBarBookKey)?.goTo(cfi);
};
@@ -94,8 +106,8 @@ const SideBar: React.FC<{
<>
<div
className={clsx(
'sidebar-container bg-base-200 z-20 flex h-full min-w-60 flex-col',
'rounded-window-top-left rounded-window-bottom-left select-none',
'sidebar-container bg-base-200 z-20 flex h-full min-w-60 select-none flex-col',
isTauriAppPlatform() && 'rounded-window-top-left rounded-window-bottom-left',
!isSideBarPinned && 'shadow-2xl',
)}
style={{
@@ -104,11 +116,20 @@ const SideBar: React.FC<{
position: isSideBarPinned ? 'relative' : 'absolute',
}}
>
<style jsx>{`
@media (max-width: 640px) {
.sidebar-container {
width: 100%;
min-width: 100%;
}
}
`}</style>
<div className='flex-shrink-0'>
<SidebarHeader
isPinned={isSideBarPinned}
isSearchBarVisible={isSearchBarVisible}
onGoToLibrary={onGoToLibrary}
onClose={() => setSideBarVisible(false)}
onTogglePin={handleSideBarTogglePin}
onToggleSearchBar={handleToggleSearchBar}
/>
@@ -1,3 +1,4 @@
import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import { TOCItem } from '@/libs/document';
@@ -5,7 +6,7 @@ import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { findParentPath } from '@/utils/toc';
import { getContentMd5 } from '@/utils/misc';
import clsx from 'clsx';
import { eventDispatcher } from '@/utils/event';
const createExpanderIcon = (isExpanded: boolean) => {
return (
@@ -43,6 +44,7 @@ const TOCItemView: React.FC<{
const handleClickItem = (event: React.MouseEvent) => {
event.preventDefault();
eventDispatcher.dispatch('navigate', { bookKey, href: item.href });
if (item.href) {
getView(bookKey)?.goTo(item.href);
}
@@ -13,42 +13,41 @@ const TabNavigation: React.FC<{
const tabs = ['toc', 'annotations', 'bookmarks'];
return (
<div className="bottom-tab border-base-300/50 flex w-full border-t relative">
<div className='bottom-tab border-base-300/50 relative flex w-full border-t'>
<div
className={clsx(
'absolute bottom-0 left-0 h-full w-1/3 bg-base-300 -z-10',
'transition-transform duration-300 transform',
'bg-base-300 absolute bottom-1.5 left-1 -z-10 h-[calc(100%-12px)] w-[calc(33.3%-8px)] rounded-lg',
'transform transition-transform duration-300',
activeTab === 'toc' && 'translate-x-0',
activeTab === 'annotations' && 'translate-x-full',
activeTab === 'bookmarks' && 'translate-x-[200%]',
activeTab === 'annotations' && 'translate-x-[calc(100%+8px)]',
activeTab === 'bookmarks' && 'translate-x-[calc(200%+16px)]',
)}
/>
{tabs.map((tab) => (
<div
key={tab}
className='tooltip z-50 tooltip-top m-1.5 flex-1 rounded-md p-2 cursor-pointer'
data-tip={tab === 'toc' ? _('Table of Contents') : tab === 'annotations' ? _('Annotate') : _('Bookmark')}
>
<div
className={clsx(
'',
)}
onClick={() => onTabChange(tab)}
>
{
tab === 'toc' ? (
<MdToc size={20} className='mx-auto' />
) : tab === 'annotations' ? (
<MdEditNote size={20} className='mx-auto' />
) : (
<MdBookmarkBorder size={20} className='mx-auto' />
)
}
</div>
<div
key={tab}
className='lg:tooltip lg:tooltip-top z-50 m-1.5 flex-1 cursor-pointer rounded-md p-2'
data-tip={
tab === 'toc'
? _('Table of Contents')
: tab === 'annotations'
? _('Annotate')
: _('Bookmark')
}
>
<div className={clsx('')} onClick={() => onTabChange(tab)}>
{tab === 'toc' ? (
<MdToc size={20} className='mx-auto' />
) : tab === 'annotations' ? (
<MdEditNote size={20} className='mx-auto' />
) : (
<MdBookmarkBorder size={20} className='mx-auto' />
)}
</div>
</div>
))}
</div>
)
);
};
export default TabNavigation;
@@ -29,14 +29,7 @@ export const useClickEvent = (
if (!consumed) {
const centerStartX = rect.left + rect.width * 0.375;
const centerEndX = rect.left + rect.width * 0.625;
const centerStartY = rect.top + rect.height * 0.375;
const centerEndY = rect.top + rect.height * 0.625;
if (
screenX >= centerStartX &&
screenX <= centerEndX &&
screenY >= centerStartY &&
screenY <= centerEndY
) {
if (screenX >= centerStartX && screenX <= centerEndX) {
// toggle visibility of the header bar and the footer bar
setHoveredBookKey(hoveredBookKey ? '' : bookKey);
} else if (screenX >= rect.left + rect.width / 2) {
@@ -29,6 +29,7 @@ export const useProgressSync = (bookKey: string) => {
const compressedConfig = JSON.parse(
serializeConfig(newConfig, settings.globalViewSettings, DEFAULT_BOOK_SEARCH_CONFIG),
);
delete compressedConfig.booknotes;
syncConfigs([compressedConfig], bookHash, 'push');
};
const pullConfig = (bookKey: string) => {
@@ -40,18 +41,20 @@ export const useProgressSync = (bookKey: string) => {
if (!configSynced.current) {
pullConfig(bookKey);
} else {
pushConfig(bookKey, config);
if (config && config.progress && config.progress[0] > 0) {
pushConfig(bookKey, config);
}
}
};
useEffect(() => {
if (!user) return;
pullConfig(bookKey);
return () => {
if (configSynced.current) {
pushConfig(bookKey, config);
}
};
// return () => {
// if (configSynced.current) {
// pushConfig(bookKey, config);
// }
// };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+8 -4
View File
@@ -20,14 +20,18 @@ const Button: React.FC<ButtonProps> = ({
}) => {
return (
<div
className={clsx('tooltip z-50', tooltip && `tooltip-${tooltipDirection}`, {
'tooltip-hidden': !tooltip,
})}
className={clsx(
'lg:tooltip z-50 h-8 min-h-8 w-8',
tooltip && `lg:tooltip-${tooltipDirection}`,
{
'tooltip-hidden': !tooltip,
},
)}
data-tip={tooltip}
>
<button
className={clsx(
'btn btn-ghost mx-2 h-8 min-h-8 w-8 p-0',
'btn btn-ghost h-8 min-h-8 w-8 p-0',
disabled && 'btn-disabled !bg-transparent',
className,
)}
+5 -3
View File
@@ -3,14 +3,16 @@ import React, { useState, isValidElement, ReactElement } from 'react';
interface DropdownProps {
className?: string;
menuClassName?: string;
buttonClassName?: string;
toggleButton: React.ReactNode;
children: ReactElement<{ setIsDropdownOpen: (isOpen: boolean) => void }>;
children: ReactElement<{ setIsDropdownOpen: (isOpen: boolean) => void; menuClassName?: string }>;
onToggle?: (isOpen: boolean) => void;
}
const Dropdown: React.FC<DropdownProps> = ({
className,
menuClassName,
buttonClassName,
toggleButton,
children,
@@ -30,7 +32,7 @@ const Dropdown: React.FC<DropdownProps> = ({
};
const childrenWithToggle = isValidElement(children)
? React.cloneElement(children, { setIsDropdownOpen })
? React.cloneElement(children, { setIsDropdownOpen, menuClassName })
: children;
return (
@@ -42,7 +44,7 @@ const Dropdown: React.FC<DropdownProps> = ({
<div
tabIndex={-1}
onClick={toggleDropdown}
className={clsx('dropdown-toggle', buttonClassName, isOpen && 'bg-base-200')}
className={clsx('dropdown-toggle', buttonClassName, isOpen && 'bg-base-300/50')}
>
{toggleButton}
</div>
+2 -2
View File
@@ -25,7 +25,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
const menuButton = (
<button
className={clsx(
'hover:bg-neutral text-base-content flex h-10 w-full items-center justify-between rounded-md p-2',
'hover:bg-base-300 text-base-content flex h-10 w-full items-center justify-between rounded-md p-2',
disabled && 'btn-disabled text-gray-400',
)}
onClick={onClick}
@@ -44,7 +44,7 @@ const MenuItem: React.FC<MenuItemProps> = ({
<ul className='menu rounded-box m-0 p-0'>
<li>
<details>
<summary className='hover:bg-neutral p-0 pr-3'>{menuButton}</summary>
<summary className='hover:bg-base-300 p-0 pr-3'>{menuButton}</summary>
{children}
</details>
</li>
+10 -3
View File
@@ -30,6 +30,8 @@ const computeMaxTimestamp = (records: BookDataRecord[]): number => {
return maxTime;
};
const SEVEN_DAYS_IN_MS = 7 * 24 * 60 * 60 * 1000;
export function useSync(bookKey?: string) {
const { settings } = useSettingsStore();
const { getConfig, setConfig } = useBookDataStore();
@@ -40,14 +42,19 @@ export function useSync(bookKey?: string) {
const [syncingNotes, setSyncingNotes] = useState(false);
const [syncError, setSyncError] = useState<string | null>(null);
const lastSyncedBooksAt = settings.lastSyncedAtBooks ?? 0;
const lastSyncedConfigsAt = config?.lastSyncedAtConfig ?? settings.lastSyncedAtConfigs ?? 0;
const lastSyncedNotesAt = config?.lastSyncedAtNotes ?? settings.lastSyncedAtNotes ?? 0;
const [lastSyncedAtBooks, setLastSyncedAtBooks] = useState<number>(
settings.lastSyncedAtBooks ?? 0,
lastSyncedBooksAt > 0 ? lastSyncedBooksAt - SEVEN_DAYS_IN_MS : 0,
);
const [lastSyncedAtConfigs, setLastSyncedAtConfigs] = useState<number>(
config?.lastSyncedAtConfig ?? settings.lastSyncedAtConfigs ?? 0,
lastSyncedConfigsAt > 0 ? lastSyncedConfigsAt - SEVEN_DAYS_IN_MS : 0,
);
const [lastSyncedAtNotes, setLastSyncedAtNotes] = useState<number>(
config?.lastSyncedAtNotes ?? settings.lastSyncedAtNotes ?? 0,
lastSyncedNotesAt > 0 ? lastSyncedNotesAt - SEVEN_DAYS_IN_MS : 0,
);
const [syncing, setSyncing] = useState(false);
+2 -8
View File
@@ -1,14 +1,8 @@
import { supabase } from '@/utils/supabase';
import { Book, BookConfig, BookNote, BookDataRecord } from '@/types/book';
import { READEST_WEB_BASE_URL } from '@/services/constants';
import { isWebAppPlatform } from '@/services/environment';
import { getAPIBaseUrl, isWebAppPlatform } from '@/services/environment';
// Develop Sync API only in development mode and web platform
// with command `pnpm dev-web`
const SYNC_API_ENDPOINT =
process.env['NODE_ENV'] === 'development' && isWebAppPlatform()
? '/api/sync'
: `${READEST_WEB_BASE_URL}/api/sync`;
const SYNC_API_ENDPOINT = getAPIBaseUrl() + '/sync';
export type SyncType = 'books' | 'configs' | 'notes';
export type SyncOp = 'push' | 'pull' | 'both';
+12 -3
View File
@@ -1,4 +1,5 @@
import { AppProps } from 'next/app';
import Head from 'next/head';
import Providers from '@/components/Providers';
import '../styles/globals.css';
@@ -6,9 +7,17 @@ import '../styles/fonts.css';
function MyApp({ Component, pageProps }: AppProps) {
return (
<Providers>
<Component {...pageProps} />
</Providers>
<>
<Head>
<meta
name='viewport'
content='width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, user-scalable=no'
/>
</Head>
<Providers>
<Component {...pageProps} />
</Providers>
</>
);
}
@@ -0,0 +1,44 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { supabase } from '@/utils/supabase';
const DEEPL_FREE_API = 'https://api-free.deepl.com/v2/translate';
const DEEPL_PRO_API = 'https://api.deepl.com/v2/translate';
const getUserAndToken = async (authHeader: string | undefined) => {
if (!authHeader) return {};
const token = authHeader.replace('Bearer ', '');
const {
data: { user },
error,
} = await supabase.auth.getUser(token);
if (error || !user) return {};
return { user, token };
};
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { user, token } = await getUserAndToken(req.headers['authorization']);
const deeplApiUrl = user && token ? DEEPL_PRO_API : DEEPL_FREE_API;
const deeplAuthKey =
user && token ? process.env['DEEPL_PRO_API_KEY'] : process.env['DEEPL_FREE_API_KEY'];
try {
console.log('Proxying DeepL request:', deeplApiUrl, deeplAuthKey);
const response = await fetch(deeplApiUrl, {
method: 'POST',
headers: {
Authorization: `DeepL-Auth-Key ${deeplAuthKey}`,
'Content-Type': 'application/json',
},
body: req.method === 'POST' ? JSON.stringify(req.body) : undefined,
});
res.status(response.status);
res.json(await response.json());
} catch (error) {
console.error('Error proxying DeepL request:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
};
export default handler;
@@ -1,4 +1,5 @@
import { AppService } from '@/types/system';
import { READEST_WEB_BASE_URL } from './constants';
declare global {
interface Window {
@@ -12,6 +13,14 @@ export const isWebAppPlatform = () => process.env['NEXT_PUBLIC_APP_PLATFORM'] ==
export const hasUpdater = () => window.__READEST_UPDATER_ACCESS === true;
export const hasCli = () => window.__READEST_CLI_ACCESS === true;
// Dev API only in development mode and web platform
// with command `pnpm dev-web`
// for production build or tauri app use the production Web API
export const getAPIBaseUrl = () =>
process.env['NODE_ENV'] === 'development' && isWebAppPlatform()
? '/api'
: `${READEST_WEB_BASE_URL}/api`;
export interface EnvConfigType {
getAppService: () => Promise<AppService>;
}
@@ -297,9 +297,23 @@ export class WebSpeechClient implements TTSClient {
.filter((voice) => voice.lang.startsWith(locale))
.filter((voice) => isValidVoice(voice.voiceURI || ''))
.filter(isNotBlacklisted);
const voices = filteredVoices.map((voice) => {
return { id: voice.voiceURI, name: voice.name, lang: voice.lang } as TTSVoice;
});
const seenIds = new Set<string>();
const voices = filteredVoices
.map(
(voice) =>
({
id: voice.voiceURI,
name: voice.name,
lang: voice.lang,
}) as TTSVoice,
)
.filter((voice) => {
if (seenIds.has(voice.id)) {
return false;
}
seenIds.add(voice.id);
return true;
});
voices.forEach((voice) => {
voice.disabled = !this.available;
});
+2 -2
View File
@@ -11,7 +11,7 @@ const generateLightPalette = ({ bg, fg, primary }: BaseColor) => {
return {
'base-100': bg, // Main background
'base-200': tinycolor(bg).darken(5).toHexString(), // Slightly darker
'base-300': tinycolor(bg).darken(15).toHexString(), // More darker
'base-300': tinycolor(bg).darken(12).toHexString(), // More darker
'base-content': fg, // Main text color
neutral: tinycolor(bg).darken(15).desaturate(20).toHexString(), // Muted neutral
'neutral-content': tinycolor(fg).lighten(20).desaturate(20).toHexString(), // Slightly lighter text
@@ -25,7 +25,7 @@ const generateDarkPalette = ({ bg, fg, primary }: BaseColor) => {
return {
'base-100': bg, // Main background
'base-200': tinycolor(bg).lighten(5).toHexString(), // Slightly lighter
'base-300': tinycolor(bg).lighten(15).toHexString(), // More lighter
'base-300': tinycolor(bg).lighten(12).toHexString(), // More lighter
'base-content': fg, // Main text color
neutral: tinycolor(bg).lighten(15).desaturate(20).toHexString(), // Muted neutral
'neutral-content': tinycolor(fg).darken(20).desaturate(20).toHexString(), // Darkened text