forked from akai/readest
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 443809f5af | |||
| c009edca61 | |||
| 58c907c7cc | |||
| 5ca173818b | |||
| f64b5606f1 | |||
| 010568eeb1 | |||
| d24f737428 | |||
| 1635958ae3 | |||
| c8f0c09426 |
+5
-4
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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__',
|
||||
|
||||
@@ -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,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.": "無法獲取翻譯,請稍後再試。"
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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,
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user