Compare commits

...

210 Commits

Author SHA1 Message Date
Huang Xin e77c377839 release: version 0.9.15 (#425) 2025-02-21 19:44:28 +01:00
Huang Xin c2d75deab4 fix: override font color from calibre (#422) 2025-02-21 17:07:27 +01:00
Huang Xin e7579e5867 ui: wrap keyboard shortcuts in kbd tag (#421) 2025-02-21 16:43:38 +01:00
Huang Xin fcc933a243 doc: add instruction for starting development for the Web version only 2025-02-21 16:08:19 +01:00
Huang Xin bbc457a9a5 fix: scroll to current TOC item when toggling sidebar, closes #418 (#420) 2025-02-21 14:18:00 +01:00
Huang Xin f8bd4a0e2c doc: update feature-request template 2025-02-21 12:52:52 +01:00
Huang Xin 3ada880155 ui: fix os platform detection for iPad and layout tweaks for iPad (#416) 2025-02-21 01:11:33 +01:00
Huang Xin 66866372ea ui: responsive font size for book notes (#415) 2025-02-20 12:02:55 +01:00
Huang Xin b321986c40 fix: display complete notes in note list and save note drafts, closes #405 (#412) 2025-02-20 02:13:33 +01:00
Huang Xin 829a698ece auth: use native sign in with Apple on iOS & i18n for auth page (#411) 2025-02-19 23:49:15 +01:00
Huang Xin 978a512ebf mobile: enable paging animation by default on mobile platforms (#410) 2025-02-19 20:27:22 +01:00
Huang Xin aca8c93efb fix: avoid repeative navigation to login after closing login page (#409) 2025-02-19 17:21:03 +01:00
Huang Xin 8cb81b3795 release: version 0.9.13 (#404) 2025-02-19 01:20:16 +01:00
Huang Xin 91716df5e0 feat: add option to keep screen awake when reading (#403) 2025-02-19 00:33:23 +01:00
Huang Xin f5f07e9392 fix: handle malformed href in PDF TOC, closes #357 (#402) 2025-02-18 23:29:23 +01:00
Huang Xin 7e385db4ed compat: support weread footernote, closes #391 (#401) 2025-02-18 22:23:05 +01:00
Huang Xin 1b99915663 fix: override some styles to show footnotes, closes #383 (#400) 2025-02-18 21:12:56 +01:00
Huang Xin 0a213d6a0e chore: clean up oauth callback (#399) 2025-02-18 18:12:25 +01:00
Huang Xin f9e9c90386 fix: swipe gesture in library page should not unintentionally trigger tap (#396) 2025-02-18 13:29:07 +01:00
Huang Xin 8b2888af34 ui: fix title bar padding in the library page (#395) 2025-02-18 12:42:45 +01:00
Huang Xin ffb00f4e0d ux: make the top and bottom toolbar transient and hide in normal reading status (#394) 2025-02-18 12:07:04 +01:00
Huang Xin 77da5423a6 ui: fix notebook layout (#392) 2025-02-17 23:23:24 +01:00
Huang Xin ee201d4571 ui: fix alert position in BookDetailModal and toast content max width (#390) 2025-02-17 14:05:18 +01:00
Huang Xin e48cfc407c release: script for iOS appstore upload (#388) 2025-02-16 20:50:57 +01:00
Huang Xin d49be1f9ca ui: fix alert layout not centered after refactoring (#387) 2025-02-16 15:50:28 +01:00
Huang Xin c46cf5b0bf UX enhancements of grid view in mobile platforms (#379)
* ux: context menu on both group and book items, also closes #376

* ui: more compact grid view of bookshelf on mobile platforms
2025-02-16 01:02:41 +01:00
jackplo 3078b09ba4 FR #241: Added Shortcuts to notetaking UI (#378) 2025-02-15 22:28:33 +01:00
Huang Xin 05cb53ede4 fix: use mouse pos in screen coordinates for click-to-flip, closes #351 (#375) 2025-02-15 02:34:17 +01:00
Huang Xin bd6ef7fe8e release: version 0.9.11
* ui: tweaks on header bar layout

* release: build arm64 linux binaries
2025-02-14 02:31:03 +01:00
Huang Xin b33d85f439 ui: fix layout in mobile platforms (#371) 2025-02-14 00:19:25 +01:00
Huang Xin 95a69d6bdf ux: polish ux of grouping dialog (#370) 2025-02-13 22:49:59 +01:00
Huang Xin ed9a0c6f20 ux: hide action bar when modal is shown (#369) 2025-02-13 21:34:56 +01:00
Huang Xin 71b075dfa8 feat: organize the library with groups (#368) 2025-02-13 18:45:57 +01:00
Huang Xin 937832a8ee ui: fix toast position on mobile devices (#350) 2025-02-10 23:15:43 +01:00
Huang Xin 881a54f155 cjk: default text indent for CJK and fix unusual OEBPS naming, closes #346 (#349) 2025-02-10 22:05:58 +01:00
Huang Xin 2dd0a20925 fix: reintroduce hold detection to avoid unwanted single click (#348) 2025-02-10 20:31:27 +01:00
Huang Xin b7ae3f29d9 chore: support deployment with open-next on cloudflare (#347) 2025-02-10 19:28:06 +01:00
Huang Xin 609ddb776b ux: disable double click timeout on mobile devices for more performant page turning (#339) 2025-02-09 21:36:08 +01:00
Huang Xin 7847360588 cjk: set full justification by default for CJK even on mobile devices (#338) 2025-02-09 20:32:30 +01:00
Huang Xin 8e7240bb9b ux: process synced books in update time ascending order (#337) 2025-02-09 18:58:56 +01:00
Huang Xin effbc08561 mobile: use sticky position for action bars for mobile compatibility (#335) 2025-02-09 18:35:24 +01:00
Huang Xin 6b057ebc14 feat: add override option for paragraph spacing settings, closes #329 (#334) 2025-02-09 16:05:37 +01:00
Huang Xin 26b64adca6 fix: add blob CSP for font and don't override <p> margin by default (#327)
* android: add fullscreen theme

* fix: add blob CSP for font and don't override <p> margin by default, closes #320
2025-02-09 00:54:06 +01:00
Huang Xin 9577f6a026 android: compatibility for Android 8 (#326) 2025-02-08 20:02:05 +01:00
Huang Xin 25da9210e2 readme: update roadmap and badge-platforms (#325) 2025-02-08 19:04:24 +01:00
Huang Xin 8b80c52c54 release: version 0.9.10 (#324) 2025-02-08 18:53:50 +01:00
Huang Xin d3d496b583 mobile: fix TTS icon position on iOS (#322) 2025-02-08 18:21:47 +01:00
Huang Xin ad1365fa23 mobile: fix layout of book detail modal and refresh indicator (#321) 2025-02-08 17:57:49 +01:00
Huang Xin 0ba3b510ce fix: importing fb2.zip file and CSP for embedded cover image (#319)
* fix: don't set window shadow below Win 11

* fix: importing fb2.zip file and CSP for embedded cover image
2025-02-08 16:19:54 +01:00
Huang Xin 1da0817145 mobile: fix sidebar height on iOS (#318)
* mobile: fix sidebar height on iOS

* fix: tts should not resume playing when closing book
2025-02-08 13:32:15 +01:00
Huang Xin 151509b84c mobile: fix iOS safe area insets (#317) 2025-02-08 03:38:17 +01:00
Huang Xin 282b129bf5 mobile: save book config when progress changes on Android (#315) 2025-02-07 23:22:55 +01:00
Huang Xin 7dfd7b9ca2 mobile: fix hasSafeAreaInset as PWA is only for WebAppService (#314) 2025-02-07 22:35:24 +01:00
Huang Xin ce00c3e923 mobile: immersive UI for android (#313) 2025-02-07 22:23:42 +01:00
Huang Xin e2e3418412 mobile: support safe area insets for mobile platforms (#312) 2025-02-07 22:15:18 +01:00
Huang Xin c79657a1ee fix: don't autoscroll to current item in TOC when TTS is on (#311) 2025-02-07 19:08:50 +01:00
Huang Xin dee1e28a61 feat: add options to adjust paragraph margin, word spacing, letter spacing, text indent and font weight (#310) 2025-02-07 18:23:12 +01:00
Huang Xin adfdb10d8e fix: compatibility fix for some formerly imported corrupted metadata (#307)
* fix: compatibility fix for some formerly imported corrupted metadata

* fix: use font family names of sys fonts
2025-02-07 02:54:29 +01:00
Huang Xin 023ba663e3 fix: stop showing annotator popup when speaking with TTS on Android (#306)
* fix: stop showing annotator popup when speaking with TTS on Android

* chore: get rid of the warning complains of rustc
2025-02-07 01:20:25 +01:00
Huang Xin b2ef55832c hotfix: fix window cannot be dragged when light traffic widget is shown on macOS (#304)
* fix: fix window cannot be dragged when light traffic widget is shown on macOS

* fix: detecting tauri focus event for deeplink redirect in oauth

* release: version 0.9.9
2025-02-06 20:54:39 +01:00
Huang Xin b879184dc6 ux: incrementally loading new books after synchronization (#303)
* auth: workaround to get deeplink data when completing oauth on Android

* ux: incrementally loading new books after synchronization
2025-02-06 19:07:48 +01:00
Huang Xin 8bd9e18381 auth: add applinks for auth on android and ios (#302) 2025-02-06 16:16:15 +01:00
Huang Xin 4121195fd3 fix: remove symbolic fonts in default sys fonts list (#301)
* chore: fix script for appstore release

* fix: remove symbolic fonts in default sys fonts list
2025-02-06 11:32:56 +01:00
Huang Xin cebe8812d3 i18n: translate Sans-serif and Serif font labels (#300) 2025-02-05 16:57:53 +01:00
Huang Xin 0a13f36e33 sync: add toast error for failed uploading when the cloud quota is insufficient (#299) 2025-02-05 15:48:59 +01:00
Huang Xin ded6053134 fix: UA detection of Safari with AppleWebKit (#298)
* fix: UA detection of Safari with AppleWebKit

* readme: add hello-github badge
2025-02-05 11:24:16 +01:00
Huang Xin 1c6cc2bca0 tts: change voice language at speak time when the book language is not accurate (#296) 2025-02-04 23:44:59 +01:00
Huang Xin 6ca96be917 fix: swap paging keys when writing direction is set in config (#295) 2025-02-04 23:03:20 +01:00
Huang Xin a057af7881 fix: avoid over-highlighting on some blank space when selecting multiple paragraphs, closes #238 (#294) 2025-02-04 22:09:13 +01:00
Huang Xin 279504395d fix: handle zoomed text rect on Safari when highlighting, closes #67 (#293) 2025-02-04 19:59:21 +01:00
Huang Xin cfd5d5d106 mobile: several fixes for Android build (#292) 2025-02-04 00:07:55 +01:00
Huang Xin 93eacf91a0 feat: load system fonts list at runtime on desktop, closes #287 #237 (#290) 2025-02-03 00:41:30 +01:00
Huang Xin dc6cbd2029 chore: bump update of next.js (#289) 2025-02-02 22:30:38 +01:00
Huang Xin 643fdd24a9 chore: bump update of tauri (#288) 2025-02-02 22:19:49 +01:00
Huang Xin 5c55233c5c fix: occasionally the scroll end does not reach to the last subpixel (#284) 2025-02-01 17:54:11 +01:00
Huang Xin 4d7db3592f release: version 0.9.8 2025-02-01 16:53:04 +01:00
Huang Xin 792e8657f7 ux: continue to scroll into next section at the bottom of the scroll view, closes #274 (#282) 2025-02-01 16:07:13 +01:00
Huang Xin 7a786478f1 ui: fix menu item flex layout (#281) 2025-02-01 14:57:43 +01:00
Huang Xin 0963969330 compat: more robust way to get book cover and compat author parsing (#280)
This should fix #278 and fix #275
2025-02-01 14:35:08 +01:00
Huang Xin 6889d6341f settings: add option to disable auto check updates (#279) 2025-02-01 13:45:55 +01:00
Huang Xin 44885f2901 sync: add download/upload progress indicator (#277)
* sync: add download/upload progress indicator

* chore: apt update before installing system dependencies
2025-02-01 12:17:21 +01:00
Huang Xin b6c8bfb52b fix: stale settings used when syncing books (#273)
* fix: add passive: true to touchstart event listener when pulling to refresh

* fix: stale settings used when syncing books and move auto upload from import menu to settings menu
2025-01-30 23:50:06 +01:00
Huang Xin 90e46860a5 ux: make toast info and hint info less disturbing (#272) 2025-01-30 16:46:17 +01:00
Huang Xin fef75f123d sync: pull to refresh library (#271) 2025-01-30 01:47:42 +01:00
Huang Xin 27c6ed402e font: override font size by default, font family is overridden by flag (#270) 2025-01-29 23:05:27 +01:00
Huang Xin 85c14002c6 auth: navigate to auth page when API calls are not authenticated (#269) 2025-01-29 22:46:13 +01:00
Huang Xin b7d06b6312 sync: use the location CFI to eliminate occasional one-off page glitches (#268) 2025-01-29 21:32:12 +01:00
Huang Xin cb04af1b17 mobile: set larger default font size and disable full justification for mobile by default (#266) 2025-01-29 13:31:30 +01:00
Huang Xin 01ff687472 sync: fix redundant progress sync when changing window size (#265) 2025-01-29 12:29:26 +01:00
Huang Xin 27eca300fa sync: fix tauri storage API (#264) 2025-01-29 01:49:58 +01:00
Huang Xin 377f5c314d sync: fix cors for storage api (#263) 2025-01-28 22:04:34 +01:00
Huang Xin c0d715bf77 sync: check local file availability before opening (#262) 2025-01-28 21:02:19 +01:00
Huang Xin 171a7ee759 sync: support quota of cloud storage (#261) 2025-01-28 19:05:20 +01:00
Huang Xin efd83180f1 sync: initial support to sync document files across devices (#260) 2025-01-28 00:03:01 +01:00
Palash Ahuja 4cea0bf75a Fix minor spelling error on the contributing guideline (#255) 2025-01-26 00:59:26 +01:00
Huang Xin a827316982 ue: make text highlighting unified on Desktop, Android and iOS (#256) 2025-01-26 00:57:59 +01:00
Huang Xin dc8cae5c77 fix: vertical swipes should not trigger horizontal scroll, closes #232 (#252) 2025-01-25 20:07:08 +01:00
Huang Xin 0fd0204869 fix: make sidebar content respects safe-area-inset-bottom (#251) 2025-01-25 19:42:16 +01:00
Huang Xin 8fd92efbc7 pwa: update next-pwa to latest maintained version (#250) 2025-01-25 18:45:43 +01:00
Huang Xin 4ec4b22038 fix: refresh config to get latest booknotes to show, closes #49 (#245) 2025-01-25 02:22:53 +01:00
Huang Xin 6b09145546 ui: fix highlight text not clear in dark mode (#244)
* ui: fix highlight background color in dark mode

* i18n: more consistent tanslations
2025-01-25 00:55:53 +01:00
Huang Xin ae60606470 mobile: disable contextmenu only for mobile devices (#243) 2025-01-24 23:11:05 +01:00
Huang Xin 6c6af63ecc mobile: fix annotator tools for Android, closes #200 (#242) 2025-01-24 22:53:43 +01:00
Huang Xin 942524c110 mobile: fix annotator tools in iOS browsers (#239)
* chore: disable PWA in dev mode for better performance

* mobile: fix annotator tools in iOS browsers
2025-01-24 13:30:42 +01:00
Huang Xin 18dfd73bed mobile: responsive sizes for annotation tools (#236)
* chore: refine assetPrefix in config for easier debug over LAN

* mobile: responsive sizes for annotation tools
2025-01-23 21:31:41 +01:00
Huang Xin 58d3e7b221 mobile: some UI tweaks and add system fonts (#233) 2025-01-23 16:53:42 +01:00
Huang Xin d757555fcb pwa: save a page reload between library and reader page navigation (#231) 2025-01-23 14:44:14 +01:00
Huang Xin 6315e4b751 readme: add PWA in supported platforms (#229) 2025-01-23 12:16:38 +01:00
Huang Xin 1f5c96fcc2 Release version 0.9.7 (#228) 2025-01-23 11:31:00 +01:00
Huang Xin 475bab24c5 mobile: responsive font size in UI (#227) 2025-01-23 10:36:03 +01:00
Huang Xin 369d894557 Don't fallback to Noto Sans JP, use serif fallbacks (#225)
It's safe to use 'MiSans L3' as fallbacks because it has only rare chars in GB18030 L3
2025-01-23 08:34:39 +01:00
QP Hou 41657d4c1d disable swipe up gesture detection in scrolled mode (#224) 2025-01-23 08:21:54 +01:00
Huang Xin ac03d7f817 Loop silent audio to unblock iOS web audio in silent mode (#223) 2025-01-23 02:20:59 +01:00
Huang Xin 399f374dfa Add font 'Noto Serif JP', closes #169 (#222) 2025-01-23 01:24:26 +01:00
Huang Xin fb4853a9d4 Enable WebAudio to play even when the mute toggle switch is ON, closes #201 (#221) 2025-01-23 00:52:44 +01:00
Huang Xin c678d8fa44 PWA app theme color in the header and safe area avoiding home indicator (#220) 2025-01-23 00:22:09 +01:00
Huang Xin 4d5297fdb5 Layout tweaks on mobile browsers (#219) 2025-01-22 19:37:49 +01:00
Huang Xin 41bcf05127 Add missing translations (#218) 2025-01-22 18:48:09 +01:00
Huang Xin 533324e558 Responsive icon sizes for phones, tablets and desktops, closes #214 (#217) 2025-01-22 18:33:00 +01:00
Huang Xin c8bfb64ad3 Dynamically set theme color for PWA browser UI elements (#213) 2025-01-22 01:20:41 +01:00
Huang Xin ee0d7d7ad0 Support PWA for the web version (#212)
* Support PWA for the web version

* Remove unused Cargo.lock
2025-01-22 00:18:19 +01:00
Huang Xin fc2cad3980 Make dialogs responsive for mobile devices (#209)
* Make dialogs responsive for mobile devices

* Support mouse backward/forward buttons

* Use back icon to close layer 2 components on mobile devices
Currently layer 2 components are: 1. sidebar 2. notebook 3. dialogs
They are all fullscreen on mobile devices.

* Add translations
2025-01-21 19:49:41 +01:00
QP Hou 07116bdae8 ci: run rust clippy/fmt (#207) 2025-01-21 07:20:26 +01:00
QP Hou a7ba5db830 dev: add editorconfig (#206) 2025-01-21 07:18:00 +01:00
QP Hou 7fcaab8b9f mobile: swipe up to toggle header/footer (#205) 2025-01-21 07:17:36 +01:00
Jiucheng(Oliver) a853bc52d2 Add delete functionality to BookDetailModal with confirmation alert (#204) 2025-01-21 07:11:46 +01:00
Huang Xin e34978672e Various fixes and enhancements on updater, header bar and tts (#203)
* Show 0% as valid reading progress

* Add check update button in the about window

* Fix title overflow, fix tts won't continue at chapter end
2025-01-20 22:34:05 +01:00
Huang Xin 751bf3836c Disable app updater for app store version and support more languages (#202)
* Disable app updater for app store version

* Add translations for uk, pl and hi
2025-01-20 17:14:26 +01:00
Huang Xin 87069c7308 Fix TTS on iOS browsers, closes #172 (#198) 2025-01-20 01:53:12 +01:00
QP Hou ca2a62a921 support setting dev environment using nix (#195) 2025-01-19 09:36:49 +01:00
Huang Xin f53f5b7594 Add cors for translation API (#193) 2025-01-18 19:56:12 +01:00
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
Huang Xin 60a9eb5b57 Release version 0.9.5 (#159) 2025-01-15 17:55:50 +01:00
Egan Gumiwang Pratama Bisma 46fdea3522 feat: provide reading progress indicator for each books in the bookshelf (#153)
* feat: provide reading progress indicator for each books in the bookshelf

* refactor: simplify bookshelf component and remove unused hook; enhance reading progress calculation

- Removed the `useBookConfigLoader` hook as it was no longer needed.
- Simplified the bookshelf items generation by eliminating the use of `useMemo`.
- Improved the reading progress calculation in the `ReadingProgress` component to directly use the book's progress data.
- Updated the `Book` type to include a `progress` field for better tracking of reading status.

* fix formatting for readerStore
2025-01-15 17:17:07 +01:00
Huang Xin ba82203629 Set webpage title to book title (#155) 2025-01-14 17:02:23 +01:00
Huang Xin 8dbcf25331 Toggle TTS stop when it's already running (#154) 2025-01-14 15:32:47 +01:00
Huang Xin 7e9ddcd7cc Preload the next two paragraphs in advance for TTS playback (#151) 2025-01-14 01:45:54 +01:00
Huang Xin 211fc2cd22 Add more system fonts (#150) 2025-01-13 23:27:52 +01:00
Huang Xin 3578d7eba8 Handle publisher with language map in book metadata (#149) 2025-01-13 22:53:58 +01:00
Huang Xin d220adbfdf TTS UX enhancements (#148)
* Fix toast of PDF unsupported for TTS

* Use vertical scroll for large dropdown menus
2025-01-13 20:04:04 +01:00
Huang Xin 752f4449b4 Add fallback fonts for GB18030-2022 L3 charsets (#147)
* Add fallback fonts for GB18030-2022 L3 charsets

* Update README to credit fonts used in this application
2025-01-13 19:14:43 +01:00
Huang Xin 66f369b269 Fix multiple voices when setting rate and voices in Firefox (#146) 2025-01-13 00:19:49 +01:00
Huang Xin 2251d65de4 Release hotfix of occasionally speak not stop when setting rate and voice 2025-01-11 19:08:23 +01:00
Huang Xin 463ea39467 Fix occasionally current speak not stopped when changing voice or rate (#144)
* Fix occasionally current speak not stopped when changing voice or rate

* Inherit tts rate when changing tts engine
2025-01-11 18:11:14 +01:00
Huang Xin 87bc842525 Set default sidebar width to 15% of screen width (#143) 2025-01-11 11:34:43 +01:00
Huang Xin 2ad67a0503 Init TTSController async when showing TTS indicator and add more voices (#142) 2025-01-11 11:15:21 +01:00
Huang Xin 9f894e1d9e Use default voice in Edge TTS (#141) 2025-01-10 15:26:01 +01:00
Huang Xin cc97a1b36f Update README (#140) 2025-01-10 14:36:37 +01:00
Huang Xin 4e42ec4a44 Release 0.9.2 (#139) 2025-01-10 14:13:56 +01:00
Huang Xin 00003a9415 Preloading audio for Edge TTS and other fixes (#138)
* Blacklist some low-quality web tts voices

* Dropdown layout tweaks

* Handle no audio data received in Edge TTS

* Preloading audio for Edge TTS
2025-01-10 14:11:09 +01:00
Huang Xin 7402141237 Add Edge TTS backend, selecting different voices will change TTS backend dynamically (#137) 2025-01-10 02:35:23 +01:00
Huang Xin e0323bb0ed Fix access token sometimes invalid in browser context (#136)
* Update dependencies

* Fix access token sometimes invalid in browser context
2025-01-09 17:58:55 +01:00
Egan Gumiwang Pratama Bisma 112c5fa979 enforce numeric inputs on NumberInput and prevent NaN text (#135) 2025-01-09 12:51:06 +01:00
Egan Gumiwang Pratama Bisma 86dc32dab3 persist max block size setting in LayoutPanel component (#134)
https://github.com/readest/readest/issues/131
2025-01-09 12:23:11 +01:00
Huajin 33d4dcd740 Better animation for sidebar transitions (#133)
* Add transition for BooknoteItem

* Add Tab switch animations and tooltip 'Toc' translations.

* Use 'Table of Contents' instead of 'Toc' as key.
2025-01-09 12:16:07 +01:00
Huang Xin 36d6074468 Filter voices list and shutdown TTS when closing book (#130) 2025-01-09 03:33:52 +01:00
Huang Xin cc1396876e Add select voice button in the TTS panel (#129) 2025-01-09 01:34:30 +01:00
Huang Xin f26de3ab0d Add speech speed slider in the TTS panel (#128) 2025-01-08 21:31:12 +01:00
Huang Xin 814a0032d0 Add 'Speak' button in the footbar (#127) 2025-01-08 19:44:04 +01:00
Huang Xin c6defb97ee Fix TTS might stop at the end of the section (#126) 2025-01-08 17:50:36 +01:00
Huang Xin ddee347eb3 Fix text color in misc panel and adjust order of annotator tools (#125) 2025-01-08 16:50:57 +01:00
Huang Xin 83b10a1df8 Fix color of return button in auth page in dark mode (#124) 2025-01-08 15:44:14 +01:00
Huang Xin 3e43905321 Fix TTS paused after forward/backward (#123) 2025-01-08 15:18:58 +01:00
Huang Xin 89b2eba2af Refactor Toast to global component (#122)
* Refactor Toast to global component, use eventDispatcher to dispatch toast events

* Error handling for TTS
2025-01-08 14:12:38 +01:00
Huang Xin 15d8d61841 Support more than 2 columns in page layout, closes #119 (#121) 2025-01-07 22:32:07 +01:00
Huang Xin 07b04b82e6 Initial implementation of TTS with Web Speech API (#120)
* Initial implementation of TTS with Web Speech API

* Add translations for annotator tools
2025-01-07 21:57:55 +01:00
Huang Xin a59a3b633b Fix progress overridden by newly opened book (#117) 2025-01-06 19:19:02 +01:00
Huang Xin cccd4c2b16 Various fixes (#116)
* Fix missing translations in translator popup

* Disable annotator for PDFs for now, closes #115

* Fix triangle in shadow of popup making it darker
2025-01-06 01:33:35 +01:00
Huang Xin 990f61fb72 Fix layout issues on Windows (#113) 2025-01-05 20:11:23 +01:00
Huang Xin d51f5a632a Regenerate tauri signing key to solve InvalidSignature error when updating 2025-01-05 18:07:49 +01:00
Huang Xin 3e9b89de0e Swap font when downloading online fonts, closes #104 (#111) 2025-01-05 17:26:36 +01:00
Huang Xin f900608d6a Update report-bug template to collect OS and version info 2025-01-05 16:23:48 +01:00
Huang Xin ca56c5a73d Fix no rounded window in reader page on Linux, closes #107 (#110) 2025-01-05 15:50:12 +01:00
Huang Xin 57b72d6d57 Release exe portable files directly to workaround the stupid virus warning on Windows 2025-01-05 14:19:18 +01:00
Huang Xin 76c5f58547 Restore window position and size, closes #7 (#109) 2025-01-05 14:08:56 +01:00
Huang Xin e4a6ae01e0 Release portable binaries for Windows (#102) 2025-01-04 19:46:57 +01:00
Huang Xin 2763c18d6f Fix dropdown close overlay not working on Windows/Linux (#101) 2025-01-04 17:13:53 +01:00
Huang Xin 71d0735917 Various fixes (#99)
* Add shortcut to page turning with the space key on keyboard

* Properly handle auto refresh token with supabase

* Fix settings not stored in direct quit of app, closes #94
2025-01-04 14:18:57 +01:00
Huang Xin 3484c893df Fix deeplink on Windows (#95) 2025-01-04 11:20:31 +01:00
Huang Xin e885d22631 Add deeplink to parse oauth callback url in tauri apps (#92) 2025-01-03 23:24:30 +01:00
Huang Xin 3ad26d9d8f Add vertical/horizontal switch in Layout configs for CJK books (#91) 2025-01-03 21:24:15 +01:00
Huang Xin b699fc98c2 Fix title layout of book details modal and apply of custom css deletion (#90)
* Fix custom css cannot apply after deletion

* Fix title layout of book details modal, closes #89
2025-01-03 19:12:25 +01:00
Huang Xin 2c9fe8e4f4 Better Custom CSS editor (#88) 2025-01-03 14:52:31 +01:00
Huang Xin f425ff8ffa Release version 0.8.9 (#85)
* Update README

* Release version 0.8.9
2025-01-02 23:44:35 +01:00
Huang Xin d6e0fd3b4d Fix supabase settings of oauth for tauri app (#83) 2025-01-02 23:03:50 +01:00
dependabot[bot] 2e2fad849b Bump the github-actions group with 3 updates (#82) 2025-01-02 15:31:39 +01:00
Christian Clauss 572ddd923c Fix typos discovered by codespell (#80) 2025-01-02 15:29:27 +01:00
Christian Clauss adab6f8d9b Keep GitHub Actions up to date with GitHub's Dependabot (#81) 2025-01-02 15:27:16 +01:00
Ray-D-Song f3f8df033d Fix background color of search results (#79) 2025-01-02 09:34:19 +01:00
Huang Xin 50f75c1f18 Show error toast for failed imported books and styles for dropdown menus (#76)
* Styles for dropdown menus

* Show error toast for failed imported books
2024-12-31 18:35:51 +01:00
Huang Xin 1ad13d0b7e Fix translations and line-height settings not working for some books (#73) 2024-12-31 14:42:08 +01:00
Huang Xin cd67f6ec3f Various enhancements and fixes (#70)
* Fix excerpts cannot be deleted in Chrome browsers

* Show book details in reader sidebar

* Set minimum search term length to 1 for CJK languages

* Use more system fonts as custom fonts, closes #31
2024-12-31 01:08:27 +01:00
Huang Xin 1be567ed4a Setup CI for pull request: only build the web-app 2024-12-31 01:01:35 +01:00
Huang Xin 0a46fe8648 Add missing hasWindowBar in BaseAppService 2024-12-30 09:49:20 +01:00
QP Hou abb1b7ae9c add support for ios dev build (#68) 2024-12-30 09:37:01 +01:00
Huang Xin 75e872af51 Bookshelf enhancements: 1. show details in context menu; 2. support some not found book covers (#64)
* Add show book details and open book in context menu

* Support extracting some book covers not included in the meta or guide sections
2024-12-30 00:09:23 +01:00
chrox d1b6a0db1e Release version 0.8.8 2024-12-29 13:09:40 +01:00
chrox 870607f9c3 Add missing translations 2024-12-29 10:42:35 +01:00
chrox fc163926ad Use mainBinaryName for binary execuable name 2024-12-29 10:02:30 +01:00
Huang Xin 4a33409559 Some CSS tweaks to better support dark mode and embedded fonts (#63) 2024-12-28 23:24:59 +01:00
chrox b8af483229 Rename binary execuable to readest for better cli interface, closes #61
Note that this does not affect the product name which is still Readest
and the package name is not changed.
2024-12-28 17:13:37 +01:00
chrox ba9490ad00 Use ubuntu-22.04 to build Linux binaries to support Debian Bookworm
Add Windows arm64 build
Fix release notes format
2024-12-28 10:16:55 +01:00
231 changed files with 16245 additions and 2490 deletions
+3
View File
@@ -0,0 +1,3 @@
[*.{ts,tsx}]
indent_style = space
indent_size = 2
+1 -1
View File
@@ -6,7 +6,7 @@ labels: enhancement
assignees: ''
---
**Does your feature request involve difficulty completing a task? Please describe.**
**Does your feature request involve difficulty for you to complete a task? Please describe.**
A clear and concise description of what the problem is. Ex. I think it takes too many steps to [...]
**Describe the solution you'd like**
+9 -3
View File
@@ -1,7 +1,7 @@
---
name: Report a bug
about: Report a bug or a functional regression
title: 'Ex: In DarkMode, a blank square appears in bottom right corner while scrolling'
title: 'Example: In DarkMode, a blank square appears in bottom right corner while scrolling'
labels: ['type: bug']
assignees: ''
---
@@ -30,9 +30,15 @@ The blank square should be transparent (invisible)
## Technical inputs
Operating System:
Readest Version:
**Example:**
```
- We are displaying custom scrollbars that disappear when the user is not scrolling. See ScrollWrapper.
- Probably fixable with CSS
Operating System: macOS 14.3.1
Readest Version: 0.9.0
We are displaying custom scrollbars that disappear when the user is not scrolling. See ScrollWrapper.
Probably fixable with CSS
```
+13
View File
@@ -0,0 +1,13 @@
# Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
- package-ecosystem: github-actions
directory: /
groups:
github-actions:
patterns:
- "*" # Group all Actions updates into a single larger pull request
schedule:
interval: weekly
+59
View File
@@ -0,0 +1,59 @@
name: PR checks
on:
pull_request:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
rust_lint:
runs-on: ubuntu-latest
env:
RUSTFLAGS: '-C target-cpu=skylake'
steps:
- uses: actions/checkout@v4
with:
submodules: 'true'
- name: Install minimal stable with clippy and rustfmt
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
override: true
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y pkg-config libfontconfig-dev libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libsoup-3.0-dev
- name: Format
working-directory: apps/readest-app/src-tauri
run: cargo fmt --check
- name: Clippy Check
working-directory: apps/readest-app/src-tauri
run: cargo clippy -- -D warnings
build_web_app:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: 'true'
- name: setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.1
- name: setup node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: install Dependencies
working-directory: apps/readest-app
run: |
pnpm install && pnpm setup-pdfjs
- name: build the web App
working-directory: apps/readest-app
run: |
pnpm build-web
+79 -25
View File
@@ -2,6 +2,12 @@ name: Release Readest
on:
workflow_dispatch:
inputs:
test_ubuntu_arm:
description: 'Run only ubuntu-22.04-arm'
required: false
default: true
type: boolean
release:
types: [published]
@@ -11,28 +17,31 @@ jobs:
contents: write
runs-on: ubuntu-latest
outputs:
release_id: ${{ steps.get-release.outputs.result }}
release_note: ${{ steps.get-release-notes.outputs.result }}
release_id: ${{ steps.get-release.outputs.release_id }}
release_tag: ${{ steps.get-release.outputs.release_tag }}
release_note: ${{ steps.get-release-notes.outputs.release_note }}
release_version: ${{ steps.get-release-notes.outputs.release_version }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
- name: get version
run: echo "PACKAGE_VERSION=$(node -p "require('./apps/readest-app/package.json').version")" >> $GITHUB_ENV
- name: get release
id: get-release
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
script: |
const { data } = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo,
})
return data.id
core.setOutput('release_id', data.id);
core.setOutput('release_tag', data.tag_name);
- name: get release notes
id: get-release-notes
uses: actions/github-script@v6
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
@@ -40,9 +49,10 @@ jobs:
const releaseNotesFileContent = fs.readFileSync('./apps/readest-app/release-notes.json', 'utf8');
const releaseNotes = JSON.parse(releaseNotesFileContent).releases[version] || {};
const notes = releaseNotes.notes || [];
const releaseNote = notes.map(note => `- ${note}`).join('\n');
console.log('Release note:\n', releaseNote);
return releaseNote;
const releaseNote = notes.map((note, index) => `${index + 1}. ${note}`).join(' ');
console.log('Formatted release note:', releaseNote);
core.setOutput('release_version', version);
core.setOutput('release_note', releaseNote);
build-tauri:
needs: get-release
@@ -52,20 +62,30 @@ jobs:
fail-fast: false
matrix:
config:
- os: ubuntu-latest
- os: ubuntu-22.04
arch: x86_64
rust_target: x86_64-unknown-linux-gnu
- os: ubuntu-22.04-arm
arch: aarch64
rust_target: aarch64-unknown-linux-gnu
- os: macos-latest
arch: aarch64
rust_target: x86_64-apple-darwin,aarch64-apple-darwin
args: '--target universal-apple-darwin'
- os: windows-latest
arch: x86_64
rust_target: x86_64-pc-windows-msvc
args: '--target x86_64-pc-windows-msvc'
- os: windows-latest
arch: aarch64
rust_target: aarch64-pc-windows-msvc
args: '--target aarch64-pc-windows-msvc --bundles nsis'
runs-on: ${{ matrix.config.os }}
timeout-minutes: 20
timeout-minutes: 30
if: ${{ github.event.inputs.test_arm_only }} != 'true' || matrix.config.os == 'ubuntu-22.04-arm'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: initialize git submodules
run: git submodule update --init --recursive
@@ -76,14 +96,11 @@ jobs:
version: 9.14.4
- name: setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: install rimraf
run: npm install -g rimraf
- name: install dependencies
run: pnpm install
@@ -97,24 +114,24 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.config.os }}-cargo-${{ hashFiles('apps/readest-app/src-tauri/Cargo.lock') }}
workspaces: apps/readest-app/src-tauri -> target
key: ${{ matrix.config.os }}-cargo-${{ hashFiles('Cargo.lock') }}
- name: create .env.local file for Next.js
run: |
echo "NEXT_PUBLIC_POSTHOG_KEY=${{ secrets.NEXT_PUBLIC_POSTHOG_KEY }}" >> .env.local
echo "NEXT_PUBLIC_POSTHOG_HOST=${{ secrets.NEXT_PUBLIC_POSTHOG_HOST }}" >> .env.local
echo "NEXT_PUBLIC_DEEPL_API_KEY=${{ secrets.NEXT_PUBLIC_DEEPL_API_KEY }}" >> .env.local
echo "NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}" >> .env.local
echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}" >> .env.local
echo "NEXT_PUBLIC_APP_PLATFORM=tauri" >> .env.local
- name: copy .env.local to apps/readest-app
run: cp .env.local apps/readest-app/.env.local
- name: install dependencies (ubuntu only)
if: matrix.config.os == 'ubuntu-latest'
if: contains(matrix.config.os, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
sudo apt-get install -y pkg-config libfontconfig-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- uses: tauri-apps/tauri-action@v0
env:
@@ -131,7 +148,35 @@ jobs:
projectPath: apps/readest-app
releaseId: ${{ needs.get-release.outputs.release_id }}
releaseBody: ${{ needs.get-release.outputs.release_note }}
args: ${{ matrix.config.os == 'macos-latest' && '--target universal-apple-darwin' || '' }}
args: ${{ matrix.config.args || '' }}
- name: upload portable binaries (Windows only)
if: matrix.config.os == 'windows-latest'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
echo "Uploading Portable Binaries"
arch=${{ matrix.config.arch }}
version=${{ needs.get-release.outputs.release_version }}
if [ "$arch" = "x86_64" ]; then
bin_file="Readest_${version}_x64-portable.exe"
elif [ "$arch" = "aarch64" ]; then
bin_file="Readest_${version}_arm64-portable.exe"
else
echo "Unknown architecture: $arch"
exit 1
fi
exe_file="target/${{ matrix.config.rust_target }}/release/readest.exe"
# Browsers on Windows won't download zip files that contain exe files
# so upload the exe files instead. This is totally stupid.
# powershell.exe -Command "Compress-Archive -Path $exe_file -DestinationPath $bin_file -Force"
cp $exe_file $bin_file
echo "Uploading $bin_file to GitHub release"
gh release upload ${{ needs.get-release.outputs.release_tag }} $bin_file --clobber
update-release:
permissions:
@@ -142,17 +187,26 @@ jobs:
steps:
- name: update release
id: update-release
uses: actions/github-script@v6
uses: actions/github-script@v7
env:
release_id: ${{ needs.get-release.outputs.release_id }}
release_tag: ${{ needs.get-release.outputs.release_tag }}
release_note: ${{ needs.get-release.outputs.release_note }}
with:
script: |
const { data } = await github.rest.repos.generateReleaseNotes({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: process.env.release_tag,
})
const notes = process.env.release_note.split(/(?:\d\.\s)/).filter(Boolean);
const formattedNotes = notes.map(note => `* ${note.trim()}`).join("\n");
const body = `## Release Highlight\n${formattedNotes}\n\n${data.body}`;
github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: process.env.release_id,
body: process.env.release_note,
body: body,
draft: false,
prerelease: false
})
-25
View File
@@ -1,25 +0,0 @@
name: Create vercel preview URL on pull request
on:
pull_request:
branches: [main, master]
permissions:
contents: write
deployments: write
pull-requests: write
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: 'true'
- uses: amondnet/vercel-action@v25
id: vercel-deploy
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID}}
vercel-project-id: ${{ secrets.PROJECT_ID}}
- name: preview-url
run: |
echo ${{ steps.vercel-deploy.outputs.preview-url }}
+3
View File
@@ -35,3 +35,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# Rust build
target
+2 -2
View File
@@ -33,7 +33,7 @@ To get started with Readest, follow these steps to clone and build the project.
#### 1. Clone the Repository
```bash
git clone https://github.com/chrox/readest.git
git clone https://github.com/readest/readest.git
cd readest
git submodule update --init --recursive
```
@@ -75,7 +75,7 @@ Now you're all setup and can start implementing your changes.
### Implement 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:
This project is a monorepo. The code for the `readest-app` is in the `apps/readest-app` directory. Here are some useful scripts for developing the frontend only without compiling Tauri:
| Command | Description |
| ---------------- | -------------------------------------------------- |
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
[workspace]
members = ["apps/readest-app/src-tauri"]
resolver = "2"
+96 -42
View File
@@ -1,17 +1,18 @@
<div align="center">
<a href="https://readest.com?utm_source=github&utm_medium=referral&utm_campaign=readme" target="_blank">
<img src="https://github.com/chrox/readest/blob/main/apps/readest-app/src-tauri/icons/icon.png?raw=true" alt="Readest Logo" width="20%" />
<img src="https://github.com/readest/readest/blob/main/apps/readest-app/src-tauri/icons/icon.png?raw=true" alt="Readest Logo" width="20%" />
</a>
<h1>Readest</h1>
<br>
[Readest][link-website] is an open-source ebook reader designed for immersive and deep reading experiences. Built as a modern rewrite of [Foliate](https://github.com/johnfactotum/foliate), it leverages [Next.js 15](https://github.com/vercel/next.js) and [Tauri v2](https://github.com/tauri-apps/tauri) to offer a seamless cross-platform experience on macOS, Windows, Linux and Web, with support for mobile platforms coming soon.
[Readest][link-website] is an open-source ebook reader designed for immersive and deep reading experiences. Built as a modern rewrite of [Foliate](https://github.com/johnfactotum/foliate), it leverages [Next.js 15](https://github.com/vercel/next.js) and [Tauri v2](https://github.com/tauri-apps/tauri) to deliver a smooth, cross-platform experience across macOS, Windows, Linux, Android, iOS, and the Web.
[![Website][badge-website]][link-website]
[![Web App][badge-web-app]][link-web-readest]
[![OS][badge-platforms]][link-website]
[![][badge-discord]][link-discord]
<br>
[![][badge-hellogithub]][link-hellogithub]
[![][badge-discord]][link-discord]
[![AGPL Licence][badge-license]](LICENSE)
[![Latest release][badge-release]][link-gh-releases]
[![Last commit][badge-last-commit]][link-gh-commits]
@@ -39,38 +40,37 @@
<div align="left">✅ Implemented</div>
| **Feature** | **Description** | **Status** |
| --------------------------------------- | ---------------------------------------------------------------------------------- | ---------- |
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, PDF (experimental) | ✅ |
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ |
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience. | ✅ |
| **Excerpt Text for Note-Taking** | Easily excerpt text from books for detailed notes and analysis. | ✅ |
| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ |
| **Translate with DeepL** | Translate selected text instantly using DeepL for accurate translations. | ✅ |
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ |
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ |
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
| **Sync across Platforms** | Synchronize reading progress, notes, and bookmarks across all supported platforms. | ✅ |
| **Feature** | **Description** | **Status** |
| --------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------- |
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, PDF (experimental) | ✅ |
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ |
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience. | ✅ |
| **Excerpt Text for Note-Taking** | Easily excerpt text from books for detailed notes and analysis. | ✅ |
| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ |
| **Translate with DeepL** | Translate selected text instantly using DeepL for accurate translations. | ✅ |
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ |
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ |
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
| **Text-to-Speech (TTS) Support** | Enable text-to-speech functionality for a more accessible reading experience. | ✅ |
## Planned Features
<div align="left">🛠 Building</div>
<div align="left">🔄 Planned</div>
| **Feature** | **Description** | **Priority** |
| -------------------------------- | ------------------------------------------------------------------------------------------ | ------------ |
| **Support iOS and Android** | Expand the APP to work on iOS and Android devices. | 🛠 |
| **Text-to-Speech (TTS) Support** | Enable text-to-speech functionality for a more accessible reading experience. | 🛠 |
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🔄 |
| **Keyboard Navigation** | Implement vimium-style keybindings for book navigation. | 🔄 |
| **Library Management** | Organize, sort, and manage your entire ebook library. | 🔄 |
| **Support OPDS/Calibre** | Integrate OPDS/Calibre to access online libraries and catalogs. | 🔄 |
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 |
| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🔄 |
| **In-Library Full-Text Search** | Search across your entire ebook library to find topics and quotes. | 🔄 |
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🔄 |
| **Feature** | **Description** | **Priority** |
| ------------------------------- | ------------------------------------------------------------------------------------------ | ------------ |
| **Library Management** | Organize, sort, and manage your entire ebook library. | 🛠 |
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
| **Sync with Koreader** | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🔄 |
| **Keyboard Navigation** | Implement vimium-style keybindings for book navigation. | 🔄 |
| **Support OPDS/Calibre** | Integrate OPDS/Calibre to access online libraries and catalogs. | 🔄 |
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 |
| **Advanced Reading Stats** | Track reading time, pages read, and more for detailed insights. | 🔄 |
| **In-Library Full-Text Search** | Search across your entire ebook library to find topics and quotes. | 🔄 |
Stay tuned for continuous improvements and updates! Contributions and suggestions are always welcome—let's build the ultimate reading experience together. 😊
@@ -80,11 +80,13 @@ Stay tuned for continuous improvements and updates! Contributions and suggestion
![DeepL](./data/screenshots/deepl.png)
![TTS](./data/screenshots/tts_control.png)
![Footnote](./data/screenshots/footnote_popover.png)
![Wikipedia](./data/screenshots/wikipedia_vertical.png)
![Dark Mode](./data/screenshots/dark_mode.png)
![Themeing Dark Mode](./data/screenshots/theming_dark_mode.png)
---
@@ -118,7 +120,7 @@ To get started with Readest, follow these steps to clone and build the project.
### 1. Clone the Repository
```bash
git clone https://github.com/chrox/readest.git
git clone https://github.com/readest/readest.git
cd readest
git submodule update --init --recursive
```
@@ -148,21 +150,62 @@ For Windows targets, “Build Tools for Visual Studio 2022” (or a higher editi
```bash
pnpm tauri dev
# or start development for the Web version only
pnpm dev-web
```
For Android:
```bash
# Initialize the Android environment (run once)
pnpm tauri android init
pnpm tauri android dev
# or if you want to dev on a real device
pnpm tauri android dev --host
```
For iOS:
```bash
# Set up the iOS environment (run once)
pnpm tauri ios init
pnpm tauri ios dev
# or if you want to dev on a real device
pnpm tauri ios dev --host
```
### 5. Build for Production
```bash
pnpm tauri build
pnpm tauri android build
pnpm tauri ios build
```
### 6. Setup dev environment with Nix
If you have Nix installed, you can leverage flake to enter a development shell
with all the necessary dependencies:
```bash
nix develop ./ops # enter a dev shell for the web app
nix develop ./ops#ios # enter a dev shell for the ios app
nix develop ./ops#android # enter a dev shell for the android app
```
### 7. More information
Please check the [wiki][link-gh-wiki] of this project for more information on development.
## Contributors
Readest is open-source, and contributions are welcome! Feel free to open issues, suggest features, or submit pull requests. Please **review our [contributing guidelines](CONTRIBUTING.md) before you start**. We also welcome you to join our [Discord][link-discord] community for either support or contributing guidance.
<a href="https://github.com/chrox/readest/graphs/contributors">
<a href="https://github.com/readest/readest/graphs/contributors">
<p align="left">
<img width="100" src="https://contrib.rocks/image?repo=chrox/readest" alt="A table of avatars from the project's contributors" />
<img width="300" src="https://contrib.rocks/image?repo=readest/readest" alt="A table of avatars from the project's contributors" />
</p>
</a>
@@ -170,12 +213,20 @@ Readest is open-source, and contributions are welcome! Feel free to open issues,
Readest is free software: you can redistribute it and/or modify it under the terms of the [GNU Affero General Public License](https://www.gnu.org/licenses/agpl-3.0.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. See the [LICENSE](LICENSE) file for details.
The following JavaScript libraries are bundled in this software:
The following libraries and frameworks are used in this software:
- [foliate-js](https://github.com/johnfactotum/foliate-js), which is MIT licensed.
- [zip.js](https://github.com/gildas-lormeau/zip.js), which is licensed under the BSD-3-Clause license.
- [fflate](https://github.com/101arrowz/fflate), which is MIT licensed.
- [PDF.js](https://github.com/mozilla/pdf.js), which is licensed under Apache License 2.0.
- [next.js](https://github.com/vercel/next.js), which is MIT licensed.
- [react](https://github.com/facebook/react), which is MIT licensed.
- [react-icons](https://github.com/react-icons/react-icons), which has various open-source licenses.
- [tauri](https://github.com/tauri-apps/tauri), which is MIT licensed.
The following fonts are utilized in this software, either bundled within the application or provided through web fonts:
[Bitter](https://fonts.google.com/?query=Bitter), [Fira Code](https://fonts.google.com/?query=Fira+Code), [Literata](https://fonts.google.com/?query=Literata), [Merriweather](https://fonts.google.com/?query=Merriweather), [Noto Sans](https://fonts.google.com/?query=Noto+Sans), [Roboto](https://fonts.google.com/?query=Roboto), [LXGW WenKai](https://github.com/lxgw/LxgwWenKai), [MiSans](https://hyperos.mi.com/font/en/), [Source Han](https://github.com/adobe-fonts/source-han-sans/), [WenQuanYi Micro Hei](http://wenq.org/wqy2/)
---
@@ -183,18 +234,21 @@ The following JavaScript libraries are bundled in this software:
[badge-website]: https://img.shields.io/badge/website-readest.com-orange
[badge-web-app]: https://img.shields.io/badge/read%20online-web.readest.com-orange
[badge-license]: https://img.shields.io/github/license/chrox/readest?color=teal
[badge-release]: https://img.shields.io/github/release/chrox/readest?color=green
[badge-platforms]: https://img.shields.io/badge/OS-macOS%2C%20Windows%2C%20Linux%2C%20Web-green
[badge-last-commit]: https://img.shields.io/github/last-commit/chrox/readest?color=green
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/chrox/readest
[badge-license]: https://img.shields.io/github/license/readest/readest?color=teal
[badge-release]: https://img.shields.io/github/release/readest/readest?color=green
[badge-platforms]: https://img.shields.io/badge/platforms-macOS%2C%20Windows%2C%20Linux%2C%20Android%2C%20iOS%2C%20Web%2C%20PWA-green
[badge-last-commit]: https://img.shields.io/github/last-commit/readest/readest?color=green
[badge-commit-activity]: https://img.shields.io/github/commit-activity/m/readest/readest
[badge-discord]: https://img.shields.io/discord/1314226120886976544?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square
[badge-hellogithub]: https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8a5b6ade2aee461a8bd94e59200682a7&claim_uid=eRLUbPOy2qZtDgw&theme=small
[link-macos-appstore]: https://apps.apple.com/app/apple-store/id6738622779?pt=127463130&ct=github&mt=8
[link-website]: https://readest.com?utm_source=github&utm_medium=referral&utm_campaign=readme
[link-web-readest]: https://web.readest.com
[link-gh-releases]: https://github.com/chrox/readest/releases
[link-gh-commits]: https://github.com/chrox/readest/commits/main
[link-gh-pulse]: https://github.com/chrox/readest/pulse
[link-gh-releases]: https://github.com/readest/readest/releases
[link-gh-commits]: https://github.com/readest/readest/commits/main
[link-gh-pulse]: https://github.com/readest/readest/pulse
[link-gh-wiki]: https://github.com/readest/readest/wiki
[link-discord]: https://discord.gg/gntyVNk3BJ
[link-parallel-read]: https://readest.com/#parallel-read
[link-koreader]: https://github.com/koreader/koreader
[link-hellogithub]: https://hellogithub.com/repository/8a5b6ade2aee461a8bd94e59200682a7
+15
View File
@@ -0,0 +1,15 @@
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
R2_TOKEN_VALUE=YOUR_R2_TOKEN_VALUE
R2_ACCESS_KEY_ID=YOUR_R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY=YOUR_R2_SECRET_ACCESS_KEY
R2_BUCKET_NAME=YOUR_R2_BUCKET_NAME
R2_ACCOUNT_ID=YOUR_R2_ACCOUNT_ID
R2_REGION=YOUR_R2_REGION
+11
View File
@@ -40,6 +40,10 @@ tauri.*.conf.json
# vercel
.vercel
# open-next
.open-next
.wrangler
# typescript
*.tsbuildinfo
next-env.d.ts
@@ -49,3 +53,10 @@ src-tauri/gen
# vendor
/public/vendor
# Auto Generated PWA files
/public/sw.js
/public/workbox-*.js
/public/fallback-*.js
/public/swe-worker-*.js
+20 -2
View File
@@ -3,12 +3,30 @@ module.exports = {
output: '.',
options: {
debug: false,
sort: true,
sort: false,
func: {
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',
'uk',
'pl',
'pt',
'ru',
'tr',
'hi',
'id',
'vi',
'zh-CN',
'zh-TW',
],
ns: ['translation'],
defaultNs: 'translation',
defaultValue: '__STRING_NOT_TRANSLATED__',
+29 -9
View File
@@ -1,13 +1,13 @@
/** @type {import('next').NextConfig} */
const isProd = process.env.NODE_ENV === 'production';
import withPWAInit from '@ducanh2912/next-pwa';
const internalHost = process.env.TAURI_DEV_HOST || 'localhost';
const isDev = process.env['NODE_ENV'] === 'development';
const appPlatform = process.env['NEXT_PUBLIC_APP_PLATFORM'];
/** @type {import('next').NextConfig} */
const nextConfig = {
// Ensure Next.js uses SSG instead of SSR
// https://nextjs.org/docs/pages/building-your-application/deploying/static-exports
output: process.env['NEXT_PUBLIC_APP_PLATFORM'] === 'web' ? undefined : 'export',
output: appPlatform === 'web' || isDev ? undefined : 'export',
// Note: This feature is required to use the Next.js Image component in SSG mode.
// See https://nextjs.org/docs/messages/export-image-api for different workarounds.
images: {
@@ -17,16 +17,36 @@ const nextConfig = {
appIsrStatus: false,
},
// Configure assetPrefix or else the server won't properly resolve your assets.
assetPrefix: isProd ? '' : `http://${internalHost}:3000`,
assetPrefix: '',
reactStrictMode: true,
async rewrites() {
async headers() {
return [
{
source: '/api/deepl/:path*',
destination: 'https://api-free.deepl.com/v2/:path*',
source: '/.well-known/apple-app-site-association',
headers: [
{
key: 'Content-Type',
value: 'application/json',
},
],
},
];
},
};
export default nextConfig;
const withPWA = withPWAInit({
dest: 'public',
disable: isDev || appPlatform !== 'web',
cacheOnFrontEndNav: true,
aggressiveFrontEndNavCaching: true,
reloadOnOnline: true,
swcMinify: true,
fallbacks: {
document: '/offline',
},
workboxOptions: {
disableDevLogs: true,
},
});
export default withPWA(nextConfig);
+29
View File
@@ -0,0 +1,29 @@
// @ts-nocheck
// default open-next.config.ts file created by @opennextjs/cloudflare
import type { OpenNextConfig } from '@opennextjs/aws/types/open-next.js';
import cache from '@opennextjs/cloudflare/kv-cache';
const config: OpenNextConfig = {
default: {
override: {
wrapper: 'cloudflare-node',
converter: 'edge',
// set `incrementalCache` to "dummy" to disable KV cache
incrementalCache: async () => cache,
tagCache: 'dummy',
queue: 'dummy',
},
},
middleware: {
external: true,
override: {
wrapper: 'cloudflare-edge',
converter: 'edge',
proxyExternalRequest: 'fetch',
},
},
};
export default config;
+25 -13
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.8.7",
"version": "0.9.15",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -26,24 +26,34 @@
"build-macos-universial": "dotenv -e .env.tauri.local -e .env.apple-nonstore.local -- tauri build -t universal-apple-darwin --bundles dmg",
"build-macos-universial-appstore": "dotenv -e .env.tauri.local -e .env.apple-appstore.local -- tauri build -t universal-apple-darwin --bundles app --config src-tauri/tauri.appstore.conf.json",
"build-macos-universial-appstore-dev": "dotenv -e .env.tauri.local -e .env.apple-appstore-dev.local -- tauri build -t universal-apple-darwin --bundles app --config src-tauri/tauri.appstore-dev.conf.json",
"release-macos-universial-appstore": "dotenv -e .env.tauri.local -e .env.apple-appstore.local -- bash scripts/release-mac-appstore.sh"
"build-ios": "dotenv -e .env.ios-appstore-dev.local -- tauri ios build",
"build-ios-appstore": "dotenv -e .env.ios-appstore.local -- tauri ios build --export-method app-store-connect",
"release-macos-universial-appstore": "dotenv -e .env.tauri.local -e .env.apple-appstore.local -- bash scripts/release-mac-appstore.sh",
"release-ios-appstore": "dotenv -e .env.ios-appstore.local -- bash scripts/release-ios-appstore.sh",
"preview": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare && wrangler dev",
"deploy": "NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare && wrangler deploy",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.735.0",
"@aws-sdk/s3-request-presigner": "^3.735.0",
"@ducanh2912/next-pwa": "^10.2.9",
"@fabianlars/tauri-plugin-oauth": "2",
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.47.7",
"@tauri-apps/api": "2.1.1",
"@tauri-apps/api": "2.2.0",
"@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "^2.2.0",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-http": "^2.2.0",
"@tauri-apps/plugin-log": "^2.2.0",
"@tauri-apps/plugin-opener": "^2.2.2",
"@tauri-apps/plugin-http": "^2.3.0",
"@tauri-apps/plugin-log": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.5",
"@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-process": "^2.2.0",
"@tauri-apps/plugin-shell": "~2.2.0",
"@tauri-apps/plugin-updater": "^2.3.0",
"@tauri-apps/plugin-updater": "^2.4.0",
"@zip.js/zip.js": "^2.7.53",
"clsx": "^2.1.1",
"cors": "^2.8.5",
@@ -53,17 +63,19 @@
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.1",
"js-md5": "^0.8.3",
"next": "15.1.0",
"posthog-js": "^1.194.1",
"jwt-decode": "^4.0.0",
"next": "15.1.6",
"posthog-js": "^1.205.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-i18next": "^15.2.0",
"react-icons": "^5.3.0",
"react-icons": "^5.4.0",
"react-responsive": "^10.0.0",
"tinycolor2": "^1.6.0",
"zustand": "5.0.1"
},
"devDependencies": {
"@tauri-apps/cli": "2.1.0",
"@tauri-apps/cli": "2.2.7",
"@types/cors": "^2.8.17",
"@types/cssbeautify": "^0.3.5",
"@types/node": "^22.10.1",
@@ -72,7 +84,7 @@
"@types/tinycolor2": "^1.4.6",
"autoprefixer": "^10.4.20",
"cpx2": "^8.0.0",
"daisyui": "^4.12.14",
"daisyui": "^4.12.23",
"dotenv-cli": "^7.4.4",
"eslint": "^9.16.0",
"eslint-config-next": "15.0.3",
@@ -83,7 +95,7 @@
"postcss-cli": "^11.0.0",
"postcss-nested": "^7.0.2",
"raw-loader": "^4.0.2",
"tailwindcss": "^3.4.15",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2"
}
}
@@ -0,0 +1,17 @@
{
"applinks": {
"details": [
{
"appIDs": [
"J5W48D69VR.com.bilingify.readest"
],
"components": [
{
"/": "/auth/*",
"comment": "Matches any URL whose path starts with /auth/"
}
]
}
]
}
}
@@ -0,0 +1,12 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.bilingify.readest",
"sha256_cert_fingerprints": [
"65:2D:11:67:76:12:29:14:18:42:CB:3D:18:50:B6:E4:7E:46:E1:2F:4B:E4:7F:5A:6C:14:B6:D7:12:74:1E:82"
]
}
}
]
@@ -1,7 +1,11 @@
{
"(detected)": "(erkannt)",
"About Readest": "Über Readest",
"Add your notes here...": "Fügen Sie hier Ihre Notizen hinzu...",
"Animation": "Animation",
"Annotate": "Annotieren",
"Apply": "Anwenden",
"Are you sure to delete the selected books?": "Möchten Sie die ausgewählten Bücher wirklich löschen?",
"Auto Mode": "Automatischer Modus",
"Behavior": "Verhalten",
"Book": "Buch",
@@ -13,25 +17,29 @@
"Color": "Farbe",
"Confirm": "Bestätigen",
"Confirm Deletion": "Löschen bestätigen",
"Copied to notebook": "In Notizbuch kopiert",
"Copy": "Kopieren",
"Custom CSS": "Benutzerdefiniertes CSS",
"Dark Mode": "Dunkelmodus",
"Default": "Standard",
"Default Font": "Standardschriftart",
"Default Font Size": "Standard-Schriftgröße",
"Delete": "Löschen",
"Delete Highlight": "Hervorhebung löschen",
"Dictionary": "Wörterbuch",
"Disable Click-to-Flip": "Klick zum Blättern deaktivieren",
"Download Readest": "Readest herunterladen",
"Edit": "Bearbeiten",
"Enter your custom CSS here...": "Geben Sie hier Ihr benutzerdefiniertes CSS ein...",
"Excerpts": "Auszüge",
"Failed to import book(s): {{filenames}}": "Fehler beim Importieren von Buch/Büchern: {{filenames}}",
"Fast": "Schnell",
"Font": "Schriftart",
"Font & Layout": "Schrift & Layout",
"Font Face": "Schriftschnitt",
"Font Family": "Schriftfamilie",
"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",
@@ -40,6 +48,8 @@
"Grass": "Grasgrün",
"Gray": "Grau",
"Gruvbox": "Gruvbox",
"Highlight": "Hervorheben",
"Horizontal Direction": "Horizontale Richtung",
"Hyphenation": "Silbentrennung",
"Identifier:": "Kennung:",
"Import Books": "Bücher importieren",
@@ -47,12 +57,10 @@
"Language:": "Sprache:",
"Layout": "Layout",
"Light Mode": "Hellmodus",
"Line Height": "Zeilenhöhe",
"Loading...": "Wird geladen...",
"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",
@@ -67,14 +75,14 @@
"Notebook": "Notizbuch",
"Notes": "Notizen",
"Open": "Öffnen",
"Override Publisher Font": "Verleger-Schriftart überschreiben",
"Original Text": "Originaltext",
"Page": "Seite",
"Paging Animation": "Blätter-Animation",
"Paragraph": "Absatz",
"Parallel Read": "Paralleles Lesen",
"Published:": "Veröffentlicht:",
"Publisher:": "Verlag:",
"Reading progress synced": "Lesevorgang synchronisiert",
"Reading Progress Synced": "Lesevorgang synchronisiert",
"Reload Page": "Seite neu laden",
"Reveal in File Explorer": "Im Datei-Explorer anzeigen",
"Reveal in Finder": "Im Finder anzeigen",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "Serifenlose Schriftart",
"Save": "Speichern",
"Scrolled Mode": "Scroll-Modus",
"Search books...": "Bücher suchen...",
"Search": "Suchen",
"Search Books...": "Bücher suchen...",
"Search...": "Suchen...",
"Select books": "Bücher auswählen",
"Select multiple books": "Mehrere Bücher auswählen",
"Select Book": "Buch auswählen",
"Select Books": "Bücher auswählen",
"Select Multiple Books": "Mehrere Bücher auswählen",
"Sepia": "Sepia",
"Serif Font": "Serifenschrift",
"Show Book Details": "Buchdetails anzeigen",
"Sidebar": "Seitenleiste",
"Sign In": "Anmelden",
"Sign Out": "Abmelden",
"Sky": "Himmelblau",
"Slow": "Langsam",
"Solarized": "Solarisiert",
"Speak": "Sprechen",
"Subjects:": "Themen:",
"System Fonts": "System-Schriftarten",
"TTS not supported in this device": "TTS wird auf diesem Gerät nicht unterstützt",
"Theme Color": "Themenfarbe",
"Theme Mode": "Themenmodus",
"Translate": "Übersetzen",
"Translated Text": "Übersetzter Text",
"Unknown": "Unbekannt",
"Untitled": "Ohne Titel",
"Updated:": "Aktualisiert:",
"User avatar": "Benutzer-Avatar",
"Version {{version}}": "Version {{version}}",
"Vertical Direction": "Vertikale Richtung",
"Welcome to your library. You can import your books here and read them anytime.": "Willkommen in Ihrer Bibliothek. Sie können hier Ihre Bücher importieren und jederzeit lesen.",
"Your Library": "Ihre Bibliothek"
"Wikipedia": "Wikipedia",
"Writing Mode": "Schreibmodus",
"Your Library": "Ihre Bibliothek",
"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.",
"Update Now!": "Jetzt aktualisieren!",
"Update": "Aktualisieren",
"Check Update": "Aktualisierung prüfen",
"Already the latest version": "Bereits die neueste Version",
"Book Details": "Buchdetails",
"From Local File": "Aus lokaler Datei",
"TOC": "Inhaltsverzeichnis",
"Book uploaded: {{title}}": "Buch hochgeladen: {{title}}",
"Failed to upload book: {{title}}": "Fehler beim Hochladen des Buches: {{title}}",
"Book downloaded: {{title}}": "Buch heruntergeladen: {{title}}",
"Failed to download book: {{title}}": "Fehler beim Herunterladen des Buches: {{title}}",
"Upload Book": "Buch hochladen",
"Auto Upload Books to Cloud": "Bücher automatisch in die Cloud hochladen",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% des Cloud-Speichers verwendet.",
"Storage": "Speicher",
"Book deleted: {{title}}": "Buch gelöscht: {{title}}",
"Failed to delete book: {{title}}": "Fehler beim Löschen des Buches: {{title}}",
"Check Updates on Start": "Aktualisierungen beim Start prüfen",
"Insufficient storage quota": "Unzureichendes Speicherkontingent",
"Font Weight": "Schriftgewicht",
"Line Spacing": "Zeilenabstand",
"Word Spacing": "Wortabstand",
"Letter Spacing": "Buchstabenabstand",
"Text Indent": "Texteinzug",
"Paragraph Margin": "Absatzrand",
"Override Book Layout": "Buch-Layout überschreiben",
"Add to Group": "Zur Gruppe hinzufügen",
"Untitled Group": "Unbenannte Gruppe",
"Group Books": "Bücher gruppieren",
"Remove From Group": "Aus Gruppe entfernen",
"Create New Group": "Neue Gruppe erstellen",
"Deselect Book": "Buch abwählen",
"Download Book": "Buch herunterladen",
"Deselect Group": "Gruppe abwählen",
"Select Group": "Gruppe auswählen",
"Keep Screen Awake": "Bildschirm aktiv halten",
"Email address": "E-Mail-Adresse",
"Your Password": "Ihr Passwort",
"Your email address": "Ihre E-Mail-Adresse",
"Your password": "Ihr Passwort",
"Sign in": "Anmelden",
"Signing in...": "Anmeldung...",
"Sign in with {{provider}}": "Mit {{provider}} anmelden",
"Already have an account? Sign in": "Haben Sie bereits ein Konto? Anmelden",
"Create a Password": "Passwort erstellen",
"Sign up": "Registrieren",
"Signing up...": "Registrierung...",
"Dont have an account? Sign up": "Kein Konto? Registrieren",
"Check your email for the confirmation link": "Überprüfen Sie Ihre E-Mails auf den Bestätigungslink",
"Signing in ...": "Anmeldung ...",
"Send a magic link email": "Magic-Link-E-Mail senden",
"Check your email for the magic link": "Überprüfen Sie Ihre E-Mails auf den Magic-Link",
"Send reset password instructions": "Anweisungen zum Zurücksetzen des Passworts senden",
"Sending reset instructions ...": "Sende Anweisungen zum Zurücksetzen ...",
"Forgot your password?": "Passwort vergessen?",
"Check your email for the password reset link": "Überprüfen Sie Ihre E-Mails auf den Link zum Zurücksetzen des Passworts",
"New Password": "Neues Passwort",
"Your new password": "Ihr neues Passwort",
"Update password": "Passwort aktualisieren",
"Updating password ...": "Passwort wird aktualisiert ...",
"Your password has been updated": "Ihr Passwort wurde aktualisiert",
"Phone number": "Telefonnummer",
"Your phone number": "Ihre Telefonnummer",
"Token": "Token",
"Your OTP token": "Ihr OTP-Token",
"Verify token": "Token überprüfen",
"Sign in with Google": "Mit Google anmelden",
"Sign in with Apple": "Mit Apple anmelden",
"Sign in with GitHub": "Mit GitHub anmelden"
}
@@ -0,0 +1,202 @@
{
"(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": "Μέγεθος γραμματοσειράς",
"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": "Φωτεινή λειτουργία",
"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": "Άνοιγμα",
"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": "Γραμματοσειρές συστήματος",
"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.": "Αδυναμία λήψης της μετάφρασης. Δοκιμάστε ξανά αργότερα.",
"Update Now!": "Ενημέρωση τώρα!",
"Update": "Ενημέρωση",
"Check Update": "Έλεγχος ενημέρωσης",
"Already the latest version": "Ήδη η τελευταία έκδοση",
"Book Details": "Λεπτομέρειες βιβλίου",
"From Local File": "Από τοπικό αρχείο",
"TOC": "Πίνακας περιεχομένων",
"Book uploaded: {{title}}": "Το βιβλίο με τίτλο {{title}} ανέβηκε",
"Failed to upload book: {{title}}": "Αποτυχία ανέβασμα βιβλίου: {{title}}",
"Book downloaded: {{title}}": "Το βιβλίο με τίτλο {{title}} κατέβηκε",
"Failed to download book: {{title}}": "Αποτυχία λήψης βιβλίου: {{title}}",
"Upload Book": "Ανέβασμα βιβλίου",
"Auto Upload Books to Cloud": "Αυτόματο ανέβασμα βιβλίων στο cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% της αποθήκευσης στο cloud χρησιμοποιήθηκε.",
"Storage": "Αποθήκευση",
"Book deleted: {{title}}": "Το βιβλίο με τίτλο {{title}} διαγράφηκε",
"Failed to delete book: {{title}}": "Αποτυχία διαγραφής βιβλίου: {{title}}",
"Check Updates on Start": "Έλεγχος ενημερώσεων κατά την εκκίνηση",
"Insufficient storage quota": "Ανεπαρκές όριο αποθήκευσης",
"Font Weight": "Βάρος γραμματοσειράς",
"Line Spacing": "Διάκενο γραμμών",
"Word Spacing": "Διάκενο λέξεων",
"Letter Spacing": "Διάκενο γραμμάτων",
"Text Indent": "Εσοχή κειμένου",
"Paragraph Margin": "Περιθώριο παραγράφου",
"Override Book Layout": "Παράκαμψη διάταξης βιβλίου",
"Add to Group": "Προσθήκη στην ομάδα",
"Untitled Group": "Ομάδα χωρίς τίτλο",
"Group Books": "Ομαδοποίηση βιβλίων",
"Remove From Group": "Αφαίρεση από την ομάδα",
"Create New Group": "Δημιουργία νέας ομάδας",
"Deselect Book": "Αποεπιλογή βιβλίου",
"Download Book": "Λήψη βιβλίου",
"Deselect Group": "Αποεπιλογή ομάδας",
"Select Group": "Επιλογή ομάδας",
"Keep Screen Awake": "Διατήρηση ενεργού οθόνης",
"Email address": "Διεύθυνση email",
"Your Password": "Ο κωδικός σας",
"Your email address": "Η διεύθυνση email σας",
"Your password": "Ο κωδικός πρόσβασής σας",
"Sign in": "Σύνδεση",
"Signing in...": "Σύνδεση...",
"Sign in with {{provider}}": "Σύνδεση με {{provider}}",
"Already have an account? Sign in": "Έχετε ήδη λογαριασμό; Συνδεθείτε",
"Create a Password": "Δημιουργία κωδικού πρόσβασης",
"Sign up": "Εγγραφή",
"Signing up...": "Εγγραφή...",
"Dont have an account? Sign up": "Δεν έχετε λογαριασμό; Εγγραφείτε",
"Check your email for the confirmation link": "Ελέγξτε το email σας για τον σύνδεσμο επιβεβαίωσης",
"Signing in ...": "Σύνδεση ...",
"Send a magic link email": "Αποστολή email με μαγικό σύνδεσμο",
"Check your email for the magic link": "Ελέγξτε το email σας για τον μαγικό σύνδεσμο",
"Send reset password instructions": "Αποστολή οδηγιών επαναφοράς κωδικού πρόσβασης",
"Sending reset instructions ...": "Αποστολή οδηγιών επαναφοράς ...",
"Forgot your password?": "Ξεχάσατε τον κωδικό σας;",
"Check your email for the password reset link": "Ελέγξτε το email σας για τον σύνδεσμο επαναφοράς κωδικού",
"New Password": "Νέος κωδικός πρόσβασης",
"Your new password": "Ο νέος σας κωδικός",
"Update password": "Ενημέρωση κωδικού πρόσβασης",
"Updating password ...": "Ενημέρωση κωδικού ...",
"Your password has been updated": "Ο κωδικός πρόσβασής σας ενημερώθηκε",
"Phone number": "Αριθμός τηλεφώνου",
"Your phone number": "Ο αριθμός τηλεφώνου σας",
"Token": "Κωδικός επαλήθευσης",
"Your OTP token": "Ο κωδικός OTP σας",
"Verify token": "Επαλήθευση κωδικού",
"Sign in with Google": "Σύνδεση με Google",
"Sign in with Apple": "Σύνδεση με Apple",
"Sign in with GitHub": "Σύνδεση με GitHub"
}
@@ -1,7 +1,11 @@
{
"(detected)": "(detectado)",
"About Readest": "Acerca de Readest",
"Add your notes here...": "Añade tus notas aquí...",
"Animation": "Animación",
"Annotate": "Anotar",
"Apply": "Aplicar",
"Are you sure to delete the selected books?": "¿Estás seguro de eliminar los libros seleccionados?",
"Auto Mode": "Modo automático",
"Behavior": "Comportamiento",
"Book": "Libro",
@@ -13,25 +17,29 @@
"Color": "Color",
"Confirm": "Confirmar",
"Confirm Deletion": "Confirmar eliminación",
"Copied to notebook": "Copiado al cuaderno",
"Copy": "Copiar",
"Custom CSS": "CSS personalizado",
"Dark Mode": "Modo oscuro",
"Default": "Predeterminado",
"Default Font": "Fuente predeterminada",
"Default Font Size": "Tamaño de fuente predeterminado",
"Delete": "Eliminar",
"Delete Highlight": "Eliminar resaltado",
"Dictionary": "Diccionario",
"Disable Click-to-Flip": "Desactivar clic para voltear",
"Download Readest": "Descargar Readest",
"Edit": "Editar",
"Enter your custom CSS here...": "Introduce tu CSS personalizado aquí...",
"Excerpts": "Extractos",
"Failed to import book(s): {{filenames}}": "Error al importar libro(s): {{filenames}}",
"Fast": "Rápido",
"Font": "Fuente",
"Font & Layout": "Fuente y diseño",
"Font Face": "Tipo de fuente",
"Font Family": "Familia de fuente",
"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",
@@ -40,6 +48,8 @@
"Grass": "Hierba",
"Gray": "Gris",
"Gruvbox": "Gruvbox",
"Highlight": "Resaltar",
"Horizontal Direction": "Dirección horizontal",
"Hyphenation": "Guionización",
"Identifier:": "Identificador:",
"Import Books": "Importar libros",
@@ -47,12 +57,10 @@
"Language:": "Idioma:",
"Layout": "Diseño",
"Light Mode": "Modo claro",
"Line Height": "Altura de línea",
"Loading...": "Cargando...",
"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",
@@ -67,14 +75,14 @@
"Notebook": "Cuaderno",
"Notes": "Notas",
"Open": "Abrir",
"Override Publisher Font": "Sobrescribir fuente del editor",
"Original Text": "Texto original",
"Page": "Página",
"Paging Animation": "Animación de paginación",
"Paragraph": "Párrafo",
"Parallel Read": "Lectura paralela",
"Published:": "Publicado:",
"Publisher:": "Editorial:",
"Reading progress synced": "Progreso de lectura sincronizado",
"Reading Progress Synced": "Progreso de lectura sincronizado",
"Reload Page": "Recargar página",
"Reveal in File Explorer": "Mostrar en el Explorador de archivos",
"Reveal in Finder": "Mostrar en Finder",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "Fuente sans-serif",
"Save": "Guardar",
"Scrolled Mode": "Modo desplazamiento",
"Search books...": "Buscar libros...",
"Search": "Buscar",
"Search Books...": "Buscar libros...",
"Search...": "Buscar...",
"Select books": "Seleccionar libros",
"Select multiple books": "Seleccionar varios libros",
"Select Book": "Seleccionar libro",
"Select Books": "Seleccionar libros",
"Select Multiple Books": "Seleccionar varios libros",
"Sepia": "Sepia",
"Serif Font": "Fuente serif",
"Show Book Details": "Mostrar detalles del libro",
"Sidebar": "Barra lateral",
"Sign In": "Iniciar sesión",
"Sign Out": "Cerrar sesión",
"Sky": "Cielo",
"Slow": "Lento",
"Solarized": "Solarizado",
"Speak": "Leer",
"Subjects:": "Temas:",
"System Fonts": "Fuentes del sistema",
"TTS not supported in this device": "TTS no es compatible con este dispositivo",
"Theme Color": "Color del tema",
"Theme Mode": "Modo del tema",
"Translate": "Traducir",
"Translated Text": "Texto traducido",
"Unknown": "Desconocido",
"Untitled": "Sin título",
"Updated:": "Actualizado:",
"User avatar": "Avatar del usuario",
"Version {{version}}": "Versión {{version}}",
"Vertical Direction": "Dirección vertical",
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenido a tu biblioteca. Puedes importar tus libros aquí y leerlos en cualquier momento.",
"Your Library": "Tu biblioteca"
"Wikipedia": "Wikipedia",
"Writing Mode": "Modo de escritura",
"Your Library": "Tu biblioteca",
"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.",
"Update Now!": "¡Actualizar ahora!",
"Update": "Actualizar",
"Check Update": "Comprobar actualización",
"Already the latest version": "Ya es la última versión",
"Book Details": "Detalles del libro",
"From Local File": "Desde archivo local",
"TOC": "Índice",
"Book uploaded: {{title}}": "Libro subido: {{title}}",
"Failed to upload book: {{title}}": "Error al subir libro: {{title}}",
"Book downloaded: {{title}}": "Libro descargado: {{title}}",
"Failed to download book: {{title}}": "Error al descargar libro: {{title}}",
"Upload Book": "Subir libro",
"Auto Upload Books to Cloud": "Subir libros automáticamente a la nube",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% del almacenamiento en la nube utilizado.",
"Storage": "Almacenamiento",
"Book deleted: {{title}}": "Libro eliminado: {{title}}",
"Failed to delete book: {{title}}": "Error al eliminar libro: {{title}}",
"Check Updates on Start": "Comprobar actualizaciones al iniciar",
"Insufficient storage quota": "Cuota de almacenamiento insuficiente",
"Font Weight": "Peso de la fuente",
"Line Spacing": "Espaciado de línea",
"Word Spacing": "Espaciado de palabras",
"Letter Spacing": "Espaciado de letras",
"Text Indent": "Sangría de texto",
"Paragraph Margin": "Margen de párrafo",
"Override Book Layout": "Sobrescribir diseño del libro",
"Add to Group": "Añadir a grupo",
"Untitled Group": "Grupo sin título",
"Group Books": "Agrupar libros",
"Remove From Group": "Eliminar del grupo",
"Create New Group": "Crear nuevo grupo",
"Deselect Book": "Deseleccionar libro",
"Download Book": "Descargar libro",
"Deselect Group": "Deseleccionar grupo",
"Select Group": "Seleccionar grupo",
"Keep Screen Awake": "Mantener pantalla activa",
"Email address": "Dirección de correo electrónico",
"Your Password": "Tu contraseña",
"Your email address": "Tu dirección de correo electrónico",
"Your password": "Tu contraseña",
"Sign in": "Iniciar sesión",
"Signing in...": "Iniciando sesión...",
"Sign in with {{provider}}": "Iniciar sesión con {{provider}}",
"Already have an account? Sign in": "¿Ya tienes una cuenta? Inicia sesión",
"Create a Password": "Crear una contraseña",
"Sign up": "Registrarse",
"Signing up...": "Registrándose...",
"Dont have an account? Sign up": "¿No tienes una cuenta? Regístrate",
"Check your email for the confirmation link": "Revisa tu correo electrónico para el enlace de confirmación",
"Signing in ...": "Iniciando sesión ...",
"Send a magic link email": "Enviar un enlace mágico por correo",
"Check your email for the magic link": "Revisa tu correo electrónico para el enlace mágico",
"Send reset password instructions": "Enviar instrucciones para restablecer la contraseña",
"Sending reset instructions ...": "Enviando instrucciones de restablecimiento ...",
"Forgot your password?": "¿Olvidaste tu contraseña?",
"Check your email for the password reset link": "Revisa tu correo electrónico para el enlace de restablecimiento de contraseña",
"New Password": "Nueva contraseña",
"Your new password": "Tu nueva contraseña",
"Update password": "Actualizar contraseña",
"Updating password ...": "Actualizando contraseña ...",
"Your password has been updated": "Tu contraseña ha sido actualizada",
"Phone number": "Número de teléfono",
"Your phone number": "Tu número de teléfono",
"Token": "Código de verificación",
"Your OTP token": "Tu código OTP",
"Verify token": "Verificar código",
"Sign in with Google": "Iniciar sesión con Google",
"Sign in with Apple": "Iniciar sesión con Apple",
"Sign in with GitHub": "Iniciar sesión con GitHub"
}
@@ -1,7 +1,11 @@
{
"(detected)": "(détecté)",
"About Readest": "À propos de Readest",
"Add your notes here...": "Ajoutez vos notes ici...",
"Animation": "Animation",
"Annotate": "Annoter",
"Apply": "Appliquer",
"Are you sure to delete the selected books?": "Êtes-vous sûr de vouloir supprimer les livres sélectionnés ?",
"Auto Mode": "Mode automatique",
"Behavior": "Comportement",
"Book": "Livre",
@@ -13,25 +17,29 @@
"Color": "Couleur",
"Confirm": "Confirmer",
"Confirm Deletion": "Confirmer la suppression",
"Copied to notebook": "Copié dans le carnet de notes",
"Copy": "Copier",
"Custom CSS": "CSS personnalisé",
"Dark Mode": "Mode sombre",
"Default": "Par défaut",
"Default Font": "Police par défaut",
"Default Font Size": "Taille de police par défaut",
"Delete": "Supprimer",
"Delete Highlight": "Supprimer le surlignage",
"Dictionary": "Dictionnaire",
"Disable Click-to-Flip": "Désactiver le clic pour tourner",
"Download Readest": "Télécharger Readest",
"Edit": "Modifier",
"Enter your custom CSS here...": "Saisissez votre CSS personnalisé ici...",
"Excerpts": "Extraits",
"Failed to import book(s): {{filenames}}": "Échec de l'importation du/des livre(s) : {{filenames}}",
"Fast": "Rapide",
"Font": "Police",
"Font & Layout": "Police et mise en page",
"Font Face": "Style de police",
"Font Family": "Famille de police",
"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",
@@ -40,6 +48,8 @@
"Grass": "Herbe",
"Gray": "Gris",
"Gruvbox": "Gruvbox",
"Highlight": "Surligner",
"Horizontal Direction": "Direction horizontale",
"Hyphenation": "Césure",
"Identifier:": "Identifiant :",
"Import Books": "Importer des livres",
@@ -47,12 +57,10 @@
"Language:": "Langue :",
"Layout": "Mise en page",
"Light Mode": "Mode clair",
"Line Height": "Hauteur de ligne",
"Loading...": "Chargement...",
"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",
@@ -67,14 +75,14 @@
"Notebook": "Carnet de notes",
"Notes": "Notes",
"Open": "Ouvrir",
"Override Publisher Font": "Remplacer la police de l'éditeur",
"Original Text": "Texte original",
"Page": "Page",
"Paging Animation": "Animation de page",
"Paragraph": "Paragraphe",
"Parallel Read": "Lecture parallèle",
"Published:": "Publié :",
"Publisher:": "Éditeur :",
"Reading progress synced": "Progression de lecture synchronisée",
"Reading Progress Synced": "Progression de lecture synchronisée",
"Reload Page": "Recharger la page",
"Reveal in File Explorer": "Afficher dans l'explorateur de fichiers",
"Reveal in Finder": "Afficher dans le Finder",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "Police sans empattement",
"Save": "Enregistrer",
"Scrolled Mode": "Mode défilement",
"Search books...": "Rechercher des livres...",
"Search": "Rechercher",
"Search Books...": "Rechercher des livres...",
"Search...": "Rechercher...",
"Select books": "Sélectionner des livres",
"Select multiple books": "Sélectionner plusieurs livres",
"Select Book": "Sélectionner un livre",
"Select Books": "Sélectionner des livres",
"Select Multiple Books": "Sélectionner plusieurs livres",
"Sepia": "Sépia",
"Serif Font": "Police avec empattement",
"Show Book Details": "Afficher les détails du livre",
"Sidebar": "Barre latérale",
"Sign In": "Se connecter",
"Sign Out": "Se déconnecter",
"Sky": "Ciel",
"Slow": "Lent",
"Solarized": "Solarisé",
"Speak": "Lire",
"Subjects:": "Sujets :",
"System Fonts": "Polices système",
"TTS not supported in this device": "La synthèse vocale n'est pas prise en charge sur cet appareil",
"Theme Color": "Couleur du thème",
"Theme Mode": "Mode du thème",
"Translate": "Traduire",
"Translated Text": "Texte traduit",
"Unknown": "Inconnu",
"Untitled": "Sans titre",
"Updated:": "Mis à jour :",
"User avatar": "Avatar de l'utilisateur",
"Version {{version}}": "Version {{version}}",
"Vertical Direction": "Direction verticale",
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenue dans votre bibliothèque. Vous pouvez importer vos livres ici et les lire à tout moment.",
"Your Library": "Votre bibliothèque"
"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",
"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.",
"Update Now!": "Mettre à jour maintenant !",
"Update": "Mettre à jour",
"Check Update": "Vérifier la mise à jour",
"Already the latest version": "Déjà la dernière version",
"Book Details": "Détails du livre",
"From Local File": "Depuis un fichier local",
"TOC": "Table des matières",
"Book uploaded: {{title}}": "Livre téléchargé : {{title}}",
"Failed to upload book: {{title}}": "Échec du téléchargement du livre : {{title}}",
"Book downloaded: {{title}}": "Livre téléchargé : {{title}}",
"Failed to download book: {{title}}": "Échec du téléchargement du livre : {{title}}",
"Upload Book": "Télécharger un livre",
"Auto Upload Books to Cloud": "Télécharger automatiquement les livres dans le cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% de l'espace de stockage dans le cloud utilisé.",
"Storage": "Stockage",
"Book deleted: {{title}}": "Livre supprimé : {{title}}",
"Failed to delete book: {{title}}": "Échec de la suppression du livre : {{title}}",
"Check Updates on Start": "Vérifier les mises à jour au démarrage",
"Insufficient storage quota": "Quota de stockage insuffisant",
"Font Weight": "Poids de la police",
"Line Spacing": "Interligne",
"Word Spacing": "Espacement des mots",
"Letter Spacing": "Espacement des lettres",
"Text Indent": "Retrait du texte",
"Paragraph Margin": "Marge de paragraphe",
"Override Book Layout": "Remplacer la mise en page du livre",
"Add to Group": "Ajouter au groupe",
"Untitled Group": "Groupe sans titre",
"Group Books": "Regrouper les livres",
"Remove From Group": "Retirer du groupe",
"Create New Group": "Créer un nouveau groupe",
"Deselect Book": "Désélectionner le livre",
"Download Book": "Télécharger le livre",
"Deselect Group": "Désélectionner le groupe",
"Select Group": "Sélectionner le groupe",
"Keep Screen Awake": "Maintenir l'écran allumé",
"Email address": "Adresse e-mail",
"Your Password": "Votre mot de passe",
"Your email address": "Votre adresse e-mail",
"Your password": "Votre mot de passe",
"Sign in": "Se connecter",
"Signing in...": "Connexion en cours...",
"Sign in with {{provider}}": "Se connecter avec {{provider}}",
"Already have an account? Sign in": "Vous avez déjà un compte ? Connectez-vous",
"Create a Password": "Créer un mot de passe",
"Sign up": "S'inscrire",
"Signing up...": "Inscription en cours...",
"Dont have an account? Sign up": "Vous n'avez pas de compte ? Inscrivez-vous",
"Check your email for the confirmation link": "Vérifiez votre e-mail pour le lien de confirmation",
"Signing in ...": "Connexion en cours ...",
"Send a magic link email": "Envoyer un e-mail avec un lien magique",
"Check your email for the magic link": "Vérifiez votre e-mail pour le lien magique",
"Send reset password instructions": "Envoyer les instructions de réinitialisation du mot de passe",
"Sending reset instructions ...": "Envoi des instructions de réinitialisation ...",
"Forgot your password?": "Mot de passe oublié ?",
"Check your email for the password reset link": "Vérifiez votre e-mail pour le lien de réinitialisation du mot de passe",
"New Password": "Nouveau mot de passe",
"Your new password": "Votre nouveau mot de passe",
"Update password": "Mettre à jour le mot de passe",
"Updating password ...": "Mise à jour du mot de passe ...",
"Your password has been updated": "Votre mot de passe a été mis à jour",
"Phone number": "Numéro de téléphone",
"Your phone number": "Votre numéro de téléphone",
"Token": "Code de vérification",
"Your OTP token": "Votre code OTP",
"Verify token": "Vérifier le code",
"Sign in with Google": "Se connecter avec Google",
"Sign in with Apple": "Se connecter avec Apple",
"Sign in with GitHub": "Se connecter avec GitHub"
}
@@ -0,0 +1,202 @@
{
"(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": "फ़ॉन्ट साइज़",
"Full Justification": "पूर्ण जस्टिफिकेशन",
"Global Settings": "वैश्विक सेटिंग्स",
"Go Back": "वापस जाएं",
"Go Forward": "आगे जाएं",
"Go Left": "बाएं जाएं",
"Go Right": "दाएं जाएं",
"Grass": "घास",
"Gray": "ग्रे",
"Gruvbox": "ग्रूवबॉक्स",
"Highlight": "हाइलाइट",
"Horizontal Direction": "क्षैतिज दिशा",
"Hyphenation": "हाइफ़नेशन",
"Identifier:": "पहचानकर्ता:",
"Import Books": "पुस्तकें आयात करें",
"Invert Colors in Dark Mode": "डार्क मोड में रंग उलटें",
"Language:": "भाषा:",
"Layout": "लेआउट",
"Light Mode": "लाइट मोड",
"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": "नॉर्ड",
"Notebook": "नोटबुक",
"Notes": "नोट्स",
"Open": "खोलें",
"Original Text": "मूल पाठ",
"Page": "पृष्ठ",
"Paging Animation": "पेजिंग एनीमेशन",
"Paragraph": "पैराग्राफ",
"Parallel Read": "समानांतर पढ़ें",
"Published:": "प्रकाशित:",
"Publisher:": "प्रकाशक:",
"Reading Progress Synced": "पढ़ने की प्रगति सिंक की गई",
"Reload Page": "पृष्ठ रीलोड करें",
"Reveal in File Explorer": "फ़ाइल एक्सप्लोरर में दिखाएं",
"Reveal in Finder": "फाइंडर में दिखाएं",
"Reveal in Folder": "फ़ोल्डर में दिखाएं",
"Sans-Serif Font": "सैन्स-सेरिफ फ़ॉन्ट",
"Save": "सहेजें",
"Scrolled Mode": "स्क्रॉल मोड",
"Search": "खोजें",
"Search Books...": "पुस्तकें खोजें...",
"Search...": "खोजें...",
"Select Book": "पुस्तक चुनें",
"Select Books": "पुस्तकें चुनें",
"Select Multiple Books": "कई पुस्तकें चुनें",
"Sepia": "सेपिया",
"Serif Font": "सेरिफ फ़ॉन्ट",
"Show Book Details": "पुस्तक विवरण दिखाएं",
"Sidebar": "साइडबार",
"Sign In": "साइन इन करें",
"Sign Out": "साइन आउट करें",
"Sky": "आकाश",
"Slow": "धीमा",
"Solarized": "सोलराइज्ड",
"Speak": "बोलें",
"Subjects:": "विषय:",
"System Fonts": "सिस्टम फ़ॉन्ट्स",
"TTS not supported in this device": "यह डिवाइस TTS का समर्थन नहीं करता",
"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 के लिए 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.": "अनुवाद प्राप्त करने में असमर्थ। बाद में पुनः प्रयास करें।",
"Update Now!": "अभी अपडेट करें!",
"Update": "अपडेट करें",
"Check Update": "अपडेट जांचें",
"Already the latest version": "पहले से ही नवीनतम संस्करण",
"Book Details": "पुस्तक विवरण",
"From Local File": "स्थानीय फ़ाइल से",
"TOC": "सामग्री",
"Book uploaded: {{title}}": "पुस्तक अपलोड की गई: {{title}}",
"Failed to upload book: {{title}}": "पुस्तक अपलोड करने में विफल: {{title}}",
"Book downloaded: {{title}}": "पुस्तक डाउनलोड की गई: {{title}}",
"Failed to download book: {{title}}": "पुस्तक डाउनलोड करने में विफल: {{title}}",
"Upload Book": "पुस्तक अपलोड करें",
"Auto Upload Books to Cloud": "पुस्तकें बादल में स्वचालित रूप से अपलोड करें",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% क्लाउड स्टोरेज का उपयोग किया गया है।",
"Storage": "स्टोरेज",
"Book deleted: {{title}}": "पुस्तक हटाई गई: {{title}}",
"Failed to delete book: {{title}}": "पुस्तक हटाने में विफल: {{title}}",
"Check Updates on Start": "शुरू में अपडेट जांचें",
"Insufficient storage quota": "अपर्याप्त स्टोरेज कोटा",
"Font Weight": "फ़ॉन्ट वजन",
"Line Spacing": "लाइन स्पेसिंग",
"Word Spacing": "शब्द स्पेसिंग",
"Letter Spacing": "अक्षर स्पेसिंग",
"Text Indent": "टेक्स्ट इंडेंट",
"Paragraph Margin": "पैराग्राफ मार्जिन",
"Override Book Layout": "पुस्तक लेआउट ओवरराइड करें",
"Add to Group": "समूह में जोड़ें",
"Untitled Group": "अभिनामित समूह",
"Group Books": "समूह पुस्तकें",
"Remove From Group": "समूह से हटाएं",
"Create New Group": "नया समूह बनाएं",
"Deselect Book": "पुस्तक का चयन रद्द करें",
"Download Book": "पुस्तक डाउनलोड करें",
"Deselect Group": "समूह का चयन रद्द करें",
"Select Group": "समूह चुनें",
"Keep Screen Awake": "स्क्रीन जागरूक रखें",
"Email address": "ईमेल पता",
"Your Password": "आपका पासवर्ड",
"Your email address": "आपका ईमेल पता",
"Your password": "आपका पासवर्ड",
"Sign in": "साइन इन करें",
"Signing in...": "साइन इन हो रहा है...",
"Sign in with {{provider}}": "{{provider}} के साथ साइन इन करें",
"Already have an account? Sign in": "पहले से ही एक खाता है? साइन इन करें",
"Create a Password": "पासवर्ड बनाएं",
"Sign up": "साइन अप करें",
"Signing up...": "साइन अप हो रहा है...",
"Dont have an account? Sign up": "कोई खाता नहीं है? साइन अप करें",
"Check your email for the confirmation link": "पुष्टि लिंक के लिए अपना ईमेल जांचें",
"Signing in ...": "साइन इन हो रहा है ...",
"Send a magic link email": "जादू लिंक ईमेल भेजें",
"Check your email for the magic link": "जादू लिंक के लिए अपना ईमेल जांचें",
"Send reset password instructions": "पासवर्ड रीसेट निर्देश भेजें",
"Sending reset instructions ...": "रीसेट निर्देश भेजे जा रहे हैं ...",
"Forgot your password?": "पासवर्ड भूल गए?",
"Check your email for the password reset link": "पासवर्ड रीसेट लिंक के लिए अपना ईमेल जांचें",
"New Password": "नया पासवर्ड",
"Your new password": "आपका नया पासवर्ड",
"Update password": "पासवर्ड अपडेट करें",
"Updating password ...": "पासवर्ड अपडेट किया जा रहा है ...",
"Your password has been updated": "आपका पासवर्ड अपडेट कर दिया गया है",
"Phone number": "फोन नंबर",
"Your phone number": "आपका फोन नंबर",
"Token": "टोकन",
"Your OTP token": "आपका ओटीपी टोकन",
"Verify token": "टोकन सत्यापित करें",
"Sign in with Google": "Google के साथ साइन इन करें",
"Sign in with Apple": "Apple के साथ साइन इन करें",
"Sign in with GitHub": "GitHub के साथ साइन इन करें"
}
@@ -1,7 +1,11 @@
{
"(detected)": "(terdeteksi)",
"About Readest": "Tentang Readest",
"Add your notes here...": "Tambahkan catatan Anda di sini...",
"Animation": "Animasi",
"Annotate": "Anotasi",
"Apply": "Terapkan",
"Are you sure to delete the selected books?": "Apakah Anda yakin ingin menghapus buku yang dipilih?",
"Auto Mode": "Mode Otomatis",
"Behavior": "Perilaku",
"Book": "Buku",
@@ -13,25 +17,29 @@
"Color": "Warna",
"Confirm": "Konfirmasi",
"Confirm Deletion": "Konfirmasi Penghapusan",
"Copied to notebook": "Disalin ke buku catatan",
"Copy": "Salin",
"Custom CSS": "CSS Kustom",
"Dark Mode": "Mode Gelap",
"Default": "Default",
"Default Font": "Font Default",
"Default Font Size": "Ukuran Font Default",
"Delete": "Hapus",
"Delete Highlight": "Hapus Sorotan",
"Dictionary": "Kamus",
"Disable Click-to-Flip": "Nonaktifkan Klik untuk Membalik",
"Download Readest": "Unduh Readest",
"Edit": "Edit",
"Enter your custom CSS here...": "Masukkan CSS kustom Anda di sini...",
"Excerpts": "Kutipan",
"Failed to import book(s): {{filenames}}": "Gagal mengimpor buku: {{filenames}}",
"Fast": "Cepat",
"Font": "Font",
"Font & Layout": "Font & Tata Letak",
"Font Face": "Jenis Font",
"Font Family": "Keluarga Font",
"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",
@@ -40,6 +48,8 @@
"Grass": "Rumput",
"Gray": "Abu-abu",
"Gruvbox": "Gruvbox",
"Highlight": "Sorot",
"Horizontal Direction": "Arah Horizontal",
"Hyphenation": "Pemenggalan Kata",
"Identifier:": "Pengenal:",
"Import Books": "Impor Buku",
@@ -47,12 +57,10 @@
"Language:": "Bahasa:",
"Layout": "Tata Letak",
"Light Mode": "Mode Terang",
"Line Height": "Tinggi Baris",
"Loading...": "Memuat...",
"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",
@@ -67,14 +75,14 @@
"Notebook": "Buku Catatan",
"Notes": "Catatan",
"Open": "Buka",
"Override Publisher Font": "Ganti Font Penerbit",
"Original Text": "Teks Asli",
"Page": "Halaman",
"Paging Animation": "Animasi Halaman",
"Paragraph": "Paragraf",
"Parallel Read": "Baca Paralel",
"Published:": "Diterbitkan:",
"Publisher:": "Penerbit:",
"Reading progress synced": "Progres membaca disinkronkan",
"Reading Progress Synced": "Progres membaca disinkronkan",
"Reload Page": "Muat Ulang Halaman",
"Reveal in File Explorer": "Tampilkan di File Explorer",
"Reveal in Finder": "Tampilkan di Finder",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "Font Sans-Serif",
"Save": "Simpan",
"Scrolled Mode": "Mode Gulir",
"Search books...": "Cari buku...",
"Search": "Cari",
"Search Books...": "Cari buku...",
"Search...": "Cari...",
"Select books": "Pilih buku",
"Select multiple books": "Pilih beberapa buku",
"Select Book": "Pilih Buku",
"Select Books": "Pilih buku",
"Select Multiple Books": "Pilih beberapa buku",
"Sepia": "Sepia",
"Serif Font": "Font Serif",
"Show Book Details": "Tampilkan Detail Buku",
"Sidebar": "Bilah Samping",
"Sign In": "Masuk",
"Sign Out": "Keluar",
"Sky": "Langit",
"Slow": "Lambat",
"Solarized": "Solarized",
"Speak": "Bicara",
"Subjects:": "Subjek:",
"System Fonts": "Font Sistem",
"TTS not supported in this device": "TTS tidak didukung di perangkat ini",
"Theme Color": "Warna Tema",
"Theme Mode": "Mode Tema",
"Translate": "Terjemahkan",
"Translated Text": "Teks Terjemahan",
"Unknown": "Tidak Diketahui",
"Untitled": "Tanpa Judul",
"Updated:": "Diperbarui:",
"User avatar": "Avatar Pengguna",
"Version {{version}}": "Versi {{version}}",
"Vertical Direction": "Arah Vertikal",
"Welcome to your library. You can import your books here and read them anytime.": "Selamat datang di perpustakaan Anda. Anda dapat mengimpor buku-buku Anda di sini dan membacanya kapan saja.",
"Your Library": "Perpustakaan Anda"
"Wikipedia": "Wikipedia",
"Writing Mode": "Mode Menulis",
"Your Library": "Perpustakaan Anda",
"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.",
"Update Now!": "Perbarui Sekarang!",
"Update": "Perbarui",
"Check Update": "Periksa pembaruan",
"Already the latest version": "Sudah versi terbaru",
"Book Details": "Detail Buku",
"From Local File": "Dari file lokal",
"TOC": "Daftar Isi",
"Book uploaded: {{title}}": "Buku diunggah: {{title}}",
"Failed to upload book: {{title}}": "Gagal mengunggah buku: {{title}}",
"Book downloaded: {{title}}": "Buku diunduh: {{title}}",
"Failed to download book: {{title}}": "Gagal mengunduh buku: {{title}}",
"Upload Book": "Unggah Buku",
"Auto Upload Books to Cloud": "Unggah Buku Otomatis ke Cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% dari Penyimpanan Cloud Digunakan.",
"Storage": "Penyimpanan",
"Book deleted: {{title}}": "Buku dihapus: {{title}}",
"Failed to delete book: {{title}}": "Gagal menghapus buku: {{title}}",
"Check Updates on Start": "Periksa Pembaruan saat Memulai",
"Insufficient storage quota": "Kuota penyimpanan tidak mencukupi",
"Font Weight": "Berat Font",
"Line Spacing": "Spasi Baris",
"Word Spacing": "Spasi Kata",
"Letter Spacing": "Spasi Huruf",
"Text Indent": "Indentasi Teks",
"Paragraph Margin": "Margin Paragraf",
"Override Book Layout": "Ganti Tata Letak Buku",
"Add to Group": "Tambahkan ke Grup",
"Untitled Group": "Grup Tanpa Judul",
"Group Books": "Grupkan Buku",
"Remove From Group": "Hapus dari Grup",
"Create New Group": "Buat Grup Baru",
"Deselect Book": "Batalkan Pilihan Buku",
"Download Book": "Unduh Buku",
"Deselect Group": "Batalkan Pilihan Grup",
"Select Group": "Pilih Grup",
"Keep Screen Awake": "Biarkan Layar Tetap Hidup",
"Email address": "Alamat email",
"Your Password": "Kata sandi Anda",
"Your email address": "Alamat email Anda",
"Your password": "Kata sandi Anda",
"Sign in": "Masuk",
"Signing in...": "Sedang masuk...",
"Sign in with {{provider}}": "Masuk dengan {{provider}}",
"Already have an account? Sign in": "Sudah punya akun? Masuk",
"Create a Password": "Buat kata sandi",
"Sign up": "Daftar",
"Signing up...": "Sedang mendaftar...",
"Dont have an account? Sign up": "Belum punya akun? Daftar",
"Check your email for the confirmation link": "Periksa email Anda untuk tautan konfirmasi",
"Signing in ...": "Sedang masuk ...",
"Send a magic link email": "Kirim email tautan ajaib",
"Check your email for the magic link": "Periksa email Anda untuk tautan ajaib",
"Send reset password instructions": "Kirim instruksi reset kata sandi",
"Sending reset instructions ...": "Mengirim instruksi reset ...",
"Forgot your password?": "Lupa kata sandi Anda?",
"Check your email for the password reset link": "Periksa email Anda untuk tautan reset kata sandi",
"New Password": "Kata sandi baru",
"Your new password": "Kata sandi baru Anda",
"Update password": "Perbarui kata sandi",
"Updating password ...": "Memperbarui kata sandi ...",
"Your password has been updated": "Kata sandi Anda telah diperbarui",
"Phone number": "Nomor telepon",
"Your phone number": "Nomor telepon Anda",
"Token": "Token",
"Your OTP token": "Token OTP Anda",
"Verify token": "Verifikasi token",
"Sign in with Google": "Masuk dengan Google",
"Sign in with Apple": "Masuk dengan Apple",
"Sign in with GitHub": "Masuk dengan GitHub"
}
@@ -1,7 +1,11 @@
{
"(detected)": "(rilevato)",
"About Readest": "Informazioni su Readest",
"Add your notes here...": "Aggiungi qui le tue note...",
"Animation": "Animazione",
"Annotate": "Annota",
"Apply": "Applica",
"Are you sure to delete the selected books?": "Sei sicuro di voler eliminare i libri selezionati?",
"Auto Mode": "Modalità automatica",
"Behavior": "Comportamento",
"Book": "Libro",
@@ -13,25 +17,29 @@
"Color": "Colore",
"Confirm": "Conferma",
"Confirm Deletion": "Conferma eliminazione",
"Copied to notebook": "Copiato nel quaderno",
"Copy": "Copia",
"Custom CSS": "CSS personalizzato",
"Dark Mode": "Modalità scura",
"Default": "Predefinito",
"Default Font": "Font predefinito",
"Default Font Size": "Dimensione font predefinita",
"Delete": "Elimina",
"Delete Highlight": "Elimina evidenziazione",
"Dictionary": "Dizionario",
"Disable Click-to-Flip": "Disattiva click per voltare pagina",
"Download Readest": "Scarica Readest",
"Edit": "Modifica",
"Enter your custom CSS here...": "Inserisci qui il tuo CSS personalizzato...",
"Excerpts": "Estratti",
"Failed to import book(s): {{filenames}}": "Impossibile importare il/i libro/i: {{filenames}}",
"Fast": "Veloce",
"Font": "Font",
"Font & Layout": "Font e Layout",
"Font Face": "Tipo di carattere",
"Font Family": "Famiglia di caratteri",
"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",
@@ -40,6 +48,8 @@
"Grass": "Erba",
"Gray": "Grigio",
"Gruvbox": "Gruvbox",
"Highlight": "Evidenzia",
"Horizontal Direction": "Direzione orizzontale",
"Hyphenation": "Sillabazione",
"Identifier:": "Identificatore:",
"Import Books": "Importa libri",
@@ -47,12 +57,10 @@
"Language:": "Lingua:",
"Layout": "Layout",
"Light Mode": "Modalità chiara",
"Line Height": "Altezza riga",
"Loading...": "Caricamento...",
"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",
@@ -67,14 +75,14 @@
"Notebook": "Quaderno",
"Notes": "Note",
"Open": "Apri",
"Override Publisher Font": "Sostituisci font editore",
"Original Text": "Testo originale",
"Page": "Pagina",
"Paging Animation": "Animazione cambio pagina",
"Paragraph": "Paragrafo",
"Parallel Read": "Lettura parallela",
"Published:": "Pubblicato:",
"Publisher:": "Editore:",
"Reading progress synced": "Progresso lettura sincronizzato",
"Reading Progress Synced": "Progresso lettura sincronizzato",
"Reload Page": "Ricarica pagina",
"Reveal in File Explorer": "Mostra in Esplora file",
"Reveal in Finder": "Mostra nel Finder",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "Font sans-serif",
"Save": "Salva",
"Scrolled Mode": "Modalità scorrimento",
"Search books...": "Cerca libri...",
"Search": "Cerca",
"Search Books...": "Cerca libri...",
"Search...": "Cerca...",
"Select books": "Seleziona libri",
"Select multiple books": "Seleziona più libri",
"Select Book": "Seleziona libro",
"Select Books": "Seleziona libri",
"Select Multiple Books": "Seleziona più libri",
"Sepia": "Seppia",
"Serif Font": "Font serif",
"Show Book Details": "Mostra dettagli libro",
"Sidebar": "Barra laterale",
"Sign In": "Accedi",
"Sign Out": "Esci",
"Sky": "Cielo",
"Slow": "Lento",
"Solarized": "Solarizzato",
"Speak": "Leggi",
"Subjects:": "Argomenti:",
"System Fonts": "Font di sistema",
"TTS not supported in this device": "TTS non supportato su questo dispositivo",
"Theme Color": "Colore tema",
"Theme Mode": "Modalità tema",
"Translate": "Traduci",
"Translated Text": "Testo tradotto",
"Unknown": "Sconosciuto",
"Untitled": "Senza titolo",
"Updated:": "Aggiornato:",
"User avatar": "Avatar utente",
"Version {{version}}": "Versione {{version}}",
"Vertical Direction": "Direzione verticale",
"Welcome to your library. You can import your books here and read them anytime.": "Benvenuto nella tua biblioteca. Puoi importare i tuoi libri qui e leggerli in qualsiasi momento.",
"Your Library": "La tua biblioteca"
"Wikipedia": "Wikipedia",
"Writing Mode": "Modalità scrittura",
"Your Library": "La tua biblioteca",
"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.",
"Update Now!": "Aggiorna ora!",
"Update": "Aggiorna",
"Check Update": "Controlla aggiornamento",
"Already the latest version": "Già l'ultima versione",
"Book Details": "Dettagli libro",
"From Local File": "Da file locale",
"TOC": "Sommario",
"Book uploaded: {{title}}": "Libro caricato: {{title}}",
"Failed to upload book: {{title}}": "Caricamento libro non riuscito: {{title}}",
"Book downloaded: {{title}}": "Libro scaricato: {{title}}",
"Failed to download book: {{title}}": "Download libro non riuscito: {{title}}",
"Upload Book": "Carica libro",
"Auto Upload Books to Cloud": "Carica libri automaticamente su cloud",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% dello spazio di archiviazione cloud utilizzato.",
"Storage": "Archiviazione",
"Book deleted: {{title}}": "Libro eliminato: {{title}}",
"Failed to delete book: {{title}}": "Eliminazione libro non riuscita: {{title}}",
"Check Updates on Start": "Controlla aggiornamenti all'avvio",
"Insufficient storage quota": "Quota di archiviazione insufficiente",
"Font Weight": "Peso font",
"Line Spacing": "Interlinea",
"Word Spacing": "Spaziatura parole",
"Letter Spacing": "Spaziatura lettere",
"Text Indent": "Rientro testo",
"Paragraph Margin": "Margine paragrafo",
"Override Book Layout": "Sovrascrivi layout libro",
"Add to Group": "Aggiungi al gruppo",
"Untitled Group": "Gruppo senza titolo",
"Group Books": "Raggruppa libri",
"Remove From Group": "Rimuovi dal",
"Create New Group": "Crea nuovo gruppo",
"Deselect Book": "Deseleziona libro",
"Download Book": "Scarica libro",
"Deselect Group": "Deseleziona gruppo",
"Select Group": "Seleziona gruppo",
"Keep Screen Awake": "Mantieni schermo attivo",
"Email address": "Indirizzo email",
"Your Password": "La tua password",
"Your email address": "Il tuo indirizzo email",
"Your password": "La tua password",
"Sign in": "Accedi",
"Signing in...": "Accesso in corso...",
"Sign in with {{provider}}": "Accedi con {{provider}}",
"Already have an account? Sign in": "Hai già un account? Accedi",
"Create a Password": "Crea una password",
"Sign up": "Registrati",
"Signing up...": "Registrazione in corso...",
"Dont have an account? Sign up": "Non hai un account? Registrati",
"Check your email for the confirmation link": "Controlla la tua email per il link di conferma",
"Signing in ...": "Accesso in corso ...",
"Send a magic link email": "Invia un'email con link magico",
"Check your email for the magic link": "Controlla la tua email per il link magico",
"Send reset password instructions": "Invia istruzioni per reimpostare la password",
"Sending reset instructions ...": "Invio istruzioni di reimpostazione ...",
"Forgot your password?": "Hai dimenticato la password?",
"Check your email for the password reset link": "Controlla la tua email per il link di reimpostazione della password",
"New Password": "Nuova password",
"Your new password": "La tua nuova password",
"Update password": "Aggiorna password",
"Updating password ...": "Aggiornamento password ...",
"Your password has been updated": "La tua password è stata aggiornata",
"Phone number": "Numero di telefono",
"Your phone number": "Il tuo numero di telefono",
"Token": "Token",
"Your OTP token": "Il tuo token OTP",
"Verify token": "Verifica token",
"Sign in with Google": "Accedi con Google",
"Sign in with Apple": "Accedi con Apple",
"Sign in with GitHub": "Accedi con GitHub"
}
@@ -1,7 +1,11 @@
{
"(detected)": "(検出)",
"About Readest": "Readestについて",
"Add your notes here...": "ここにメモを追加...",
"Animation": "アニメーション",
"Annotate": "注釈",
"Apply": "適用",
"Are you sure to delete the selected books?": "選択した書籍を削除してもよろしいですか?",
"Auto Mode": "自動モード",
"Behavior": "動作",
"Book": "書籍",
@@ -13,25 +17,29 @@
"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": "両端揃え",
"Gaps (%)": "間隔 (%)",
"Global Settings": "全体設定",
"Go Back": "戻る",
"Go Forward": "進む",
@@ -40,6 +48,8 @@
"Grass": "グリーン",
"Gray": "グレー",
"Gruvbox": "グルーブボックス",
"Highlight": "ハイライト",
"Horizontal Direction": "横書",
"Hyphenation": "ハイフネーション",
"Identifier:": "識別子:",
"Import Books": "書籍をインポート",
@@ -47,12 +57,10 @@
"Language:": "言語:",
"Layout": "レイアウト",
"Light Mode": "ライトモード",
"Line Height": "行間",
"Loading...": "読み込み中...",
"Loc. {{currentPage}} / {{totalPage}}": "位置 {{currentPage}} / {{totalPage}}",
"Logged in": "ログイン済み",
"Logged in as {{userDisplayName}}": "{{userDisplayName}}としてログイン中",
"Margins (px)": "余白 (px)",
"Match Case": "大文字・小文字を区別",
"Match Diacritics": "アクセント記号を区別",
"Match Whole Words": "単語全体を一致",
@@ -67,14 +75,14 @@
"Notebook": "ノート",
"Notes": "メモ",
"Open": "開く",
"Override Publisher Font": "出版社フォントを上書き",
"Original Text": "原文",
"Page": "ページ",
"Paging Animation": "ページめくりアニメーション",
"Paragraph": "段落",
"Parallel Read": "並列読書",
"Published:": "出版日:",
"Publisher:": "出版社:",
"Reading progress synced": "読書進捗が同期されました",
"Reading Progress Synced": "読書進捗が同期されました",
"Reload Page": "ページを再読み込み",
"Reveal in File Explorer": "エクスプローラーで表示",
"Reveal in Finder": "Finderで表示",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "ゴシック体",
"Save": "保存",
"Scrolled Mode": "スクロールモード",
"Search books...": "書籍を検索...",
"Search": "検索",
"Search Books...": "書籍を検索...",
"Search...": "検索...",
"Select books": "書籍を選択",
"Select multiple books": "複数の書籍を選択",
"Select Book": "書籍を選択",
"Select Books": "書籍を選択",
"Select Multiple Books": "複数の書籍を選択",
"Sepia": "セピア",
"Serif Font": "明朝体",
"Serif Font": "セリフ体",
"Show Book Details": "書籍の詳細を表示",
"Sidebar": "サイドバー",
"Sign In": "サインイン",
"Sign Out": "サインアウト",
"Sky": "スカイ",
"Slow": "低速",
"Solarized": "ソーラライズド",
"Speak": "読み上げ",
"Subjects:": "主題:",
"System Fonts": "システムフォント",
"TTS not supported in this device": "このデバイスではTTSがサポートされていません",
"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.": "ライブラリーへようこそ。ここに書籍をインポートして、いつでも読むことができます。",
"Your Library": "ライブラリー"
"Wikipedia": "ウィキペディア",
"Writing Mode": "書き込みモード",
"Your Library": "ライブラリー",
"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.": "翻訳を取得できません。後で再試行してください。",
"Update Now!": "今すぐ更新!",
"Update": "更新",
"Check Update": "更新を確認",
"Already the latest version": "すでに最新バージョン",
"Book Details": "書籍の詳細",
"From Local File": "ローカルファイルから",
"TOC": "目次",
"Book uploaded: {{title}}": "書籍がアップロードされました:{{title}}",
"Failed to upload book: {{title}}": "書籍のアップロードに失敗しました:{{title}}",
"Book downloaded: {{title}}": "書籍がダウンロードされました:{{title}}",
"Failed to download book: {{title}}": "書籍のダウンロードに失敗しました:{{title}}",
"Upload Book": "書籍をアップロード",
"Auto Upload Books to Cloud": "書籍を自動的にクラウドにアップロード",
"{{percentage}}% of Cloud Storage Used.": "クラウドストレージの{{percentage}}%が使用されています。",
"Storage": "ストレージ",
"Book deleted: {{title}}": "書籍が削除されました:{{title}}",
"Failed to delete book: {{title}}": "書籍の削除に失敗しました:{{title}}",
"Check Updates on Start": "開始時に更新を確認",
"Insufficient storage quota": "ストレージクォータが不足しています",
"Font Weight": "フォントウェイト",
"Line Spacing": "行間",
"Word Spacing": "単語間隔",
"Letter Spacing": "文字間隔",
"Text Indent": "テキストインデント",
"Paragraph Margin": "段落マージン",
"Override Book Layout": "書籍のレイアウトを上書き",
"Add to Group": "グループに追加",
"Untitled Group": "無題のグループ",
"Group Books": "書籍をグループ化",
"Remove From Group": "グループから削除",
"Create New Group": "新しいグループを作成",
"Deselect Book": "書籍の選択を解除",
"Download Book": "書籍をダウンロード",
"Deselect Group": "グループの選択を解除",
"Select Group": "グループを選択",
"Keep Screen Awake": "画面をオンのままにする",
"Email address": "メールアドレス",
"Your Password": "あなたのパスワード",
"Your email address": "あなたのメールアドレス",
"Your password": "あなたのパスワード",
"Sign in": "サインイン",
"Signing in...": "サインイン中...",
"Sign in with {{provider}}": "{{provider}}でサインイン",
"Already have an account? Sign in": "すでにアカウントがありますか? サインイン",
"Create a Password": "パスワードを作成",
"Sign up": "サインアップ",
"Signing up...": "サインアップ中...",
"Dont have an account? Sign up": "アカウントをお持ちでないですか? サインアップ",
"Check your email for the confirmation link": "確認リンクのためにメールを確認してください",
"Signing in ...": "サインイン中 ...",
"Send a magic link email": "マジックリンクメールを送信",
"Check your email for the magic link": "マジックリンクのためにメールを確認してください",
"Send reset password instructions": "パスワードリセットの指示を送信",
"Sending reset instructions ...": "リセット指示を送信中 ...",
"Forgot your password?": "パスワードを忘れましたか?",
"Check your email for the password reset link": "パスワードリセットリンクのためにメールを確認してください",
"New Password": "新しいパスワード",
"Your new password": "あなたの新しいパスワード",
"Update password": "パスワードを更新",
"Updating password ...": "パスワード更新中 ...",
"Your password has been updated": "あなたのパスワードは更新されました",
"Phone number": "電話番号",
"Your phone number": "あなたの電話番号",
"Token": "トークン",
"Your OTP token": "あなたのOTPトークン",
"Verify token": "トークンを確認",
"Sign in with Google": "Googleでサインイン",
"Sign in with Apple": "Appleでサインイン",
"Sign in with GitHub": "GitHubでサインイン"
}
@@ -1,7 +1,11 @@
{
"(detected)": "(감지됨)",
"About Readest": "Readest 정보",
"Add your notes here...": "여기에 메모를 추가하세요...",
"Animation": "애니메이션",
"Annotate": "주석",
"Apply": "적용",
"Are you sure to delete the selected books?": "선택한 책을 삭제하시겠습니까?",
"Auto Mode": "자동 모드",
"Behavior": "동작",
"Book": "책",
@@ -13,25 +17,29 @@
"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": "양쪽 정렬",
"Gaps (%)": "간격 (%)",
"Global Settings": "전역 설정",
"Go Back": "뒤로",
"Go Forward": "앞으로",
@@ -40,6 +48,8 @@
"Grass": "잔디색",
"Gray": "회색",
"Gruvbox": "그러브박스",
"Highlight": "하이라이트",
"Horizontal Direction": "수평 방향",
"Hyphenation": "하이픈 넣기",
"Identifier:": "식별자:",
"Import Books": "책 가져오기",
@@ -47,12 +57,10 @@
"Language:": "언어:",
"Layout": "레이아웃",
"Light Mode": "라이트 모드",
"Line Height": "줄 간격",
"Loading...": "로딩 중...",
"Loc. {{currentPage}} / {{totalPage}}": "위치 {{currentPage}} / {{totalPage}}",
"Logged in": "로그인됨",
"Logged in as {{userDisplayName}}": "{{userDisplayName}}(으)로 로그인됨",
"Margins (px)": "여백 (px)",
"Match Case": "대소문자 구분",
"Match Diacritics": "발음 구별 부호 구분",
"Match Whole Words": "전체 단어 일치",
@@ -67,14 +75,14 @@
"Notebook": "노트북",
"Notes": "메모",
"Open": "열기",
"Override Publisher Font": "출판사 글꼴 재정의",
"Original Text": "원본 텍스트",
"Page": "페이지",
"Paging Animation": "페이지 넘김 애니메이션",
"Paragraph": "단락",
"Parallel Read": "병렬 읽기",
"Published:": "출판일:",
"Publisher:": "출판사:",
"Reading progress synced": "읽기 진행 상황 동기화됨",
"Reading Progress Synced": "읽기 진행 상황 동기화됨",
"Reload Page": "페이지 새로고침",
"Reveal in File Explorer": "파일 탐색기에서 표시",
"Reveal in Finder": "Finder에서 표시",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "산세리프체",
"Save": "저장",
"Scrolled Mode": "스크롤 모드",
"Search books...": "책 검색...",
"Search": "검색",
"Search Books...": "책 검색...",
"Search...": "검색...",
"Select books": "책 선택",
"Select multiple books": "여러 책 선택",
"Select Book": "책 선택",
"Select Books": "책 선택",
"Select Multiple Books": "여러 책 선택",
"Sepia": "세피아",
"Serif Font": "세리프체",
"Show Book Details": "책 세부 정보 표시",
"Sidebar": "사이드바",
"Sign In": "로그인",
"Sign Out": "로그아웃",
"Sky": "하늘색",
"Slow": "느림",
"Solarized": "솔라라이즈드",
"Speak": "말하기",
"Subjects:": "주제:",
"System Fonts": "시스템 글꼴",
"TTS not supported in this device": "이 장치에서 TTS가 지원되지 않음",
"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.": "내 서재에 오신 것을 환영합니다. 여기에서 책을 가져와 언제든지 읽을 수 있습니다.",
"Your Library": "내 서재"
"Wikipedia": "위키피디아",
"Writing Mode": "글쓰기 모드",
"Your Library": "내 서재",
"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.": "번역을 가져올 수 없습니다. 나중에 다시 시도하세요.",
"Update Now!": "지금 업데이트!",
"Update": "업데이트",
"Check Update": "업데이트 확인",
"Already the latest version": "이미 최신 버전",
"Book Details": "책 세부 정보",
"From Local File": "로컬 파일에서",
"TOC": "목차",
"Book uploaded: {{title}}": "책 업로드됨: {{title}}",
"Failed to upload book: {{title}}": "책 업로드 실패: {{title}}",
"Book downloaded: {{title}}": "책 다운로드됨: {{title}}",
"Failed to download book: {{title}}": "책 다운로드 실패: {{title}}",
"Upload Book": "책 업로드",
"Auto Upload Books to Cloud": "책 자동으로 클라우드에 업로드",
"{{percentage}}% of Cloud Storage Used.": "클라우드 저장소 사용량 {{percentage}}%.",
"Storage": "저장소",
"Book deleted: {{title}}": "책 삭제됨: {{title}}",
"Failed to delete book: {{title}}": "책 삭제 실패: {{title}}",
"Check Updates on Start": "시작 시 업데이트 확인",
"Insufficient storage quota": "저장소 할당량 부족",
"Font Weight": "글꼴 두께",
"Line Spacing": "줄 간격",
"Word Spacing": "단어 간격",
"Letter Spacing": "글자 간격",
"Text Indent": "들여쓰기",
"Paragraph Margin": "단락 여백",
"Override Book Layout": "책 레이아웃 덮어쓰기",
"Add to Group": "그룹에 추가",
"Untitled Group": "제목 없는 그룹",
"Group Books": "책 그룹화",
"Remove From Group": "그룹에서 제거",
"Create New Group": "새 그룹 만들기",
"Deselect Book": "책 선택 해제",
"Download Book": "책 다운로드",
"Deselect Group": "그룹 선택 해제",
"Select Group": "그룹 선택",
"Keep Screen Awake": "화면 켜짐 유지",
"Email address": "이메일 주소",
"Your Password": "비밀번호",
"Your email address": "당신의 이메일 주소",
"Your password": "당신의 비밀번호",
"Sign in": "로그인",
"Signing in...": "로그인 중...",
"Sign in with {{provider}}": "{{provider}}로 로그인",
"Already have an account? Sign in": "이미 계정이 있습니까? 로그인",
"Create a Password": "비밀번호 만들기",
"Sign up": "회원가입",
"Signing up...": "회원가입 중...",
"Dont have an account? Sign up": "계정이 없습니까? 회원가입",
"Check your email for the confirmation link": "확인 링크를 위해 이메일을 확인하세요",
"Signing in ...": "로그인 중 ...",
"Send a magic link email": "마법의 링크 이메일 보내기",
"Check your email for the magic link": "마법의 링크를 위해 이메일을 확인하세요",
"Send reset password instructions": "비밀번호 재설정 지침 보내기",
"Sending reset instructions ...": "재설정 지침 보내는 중 ...",
"Forgot your password?": "비밀번호를 잊으셨나요?",
"Check your email for the password reset link": "비밀번호 재설정 링크를 위해 이메일을 확인하세요",
"New Password": "새로운 비밀번호",
"Your new password": "당신의 새로운 비밀번호",
"Update password": "비밀번호 업데이트",
"Updating password ...": "비밀번호 업데이트 중 ...",
"Your password has been updated": "비밀번호가 업데이트되었습니다",
"Phone number": "전화번호",
"Your phone number": "당신의 전화번호",
"Token": "토큰",
"Your OTP token": "당신의 OTP 토큰",
"Verify token": "토큰 확인",
"Sign in with Google": "Google로 로그인",
"Sign in with Apple": "Apple로 로그인",
"Sign in with GitHub": "GitHub로 로그인"
}
@@ -0,0 +1,202 @@
{
"(detected)": "(wykryto)",
"About Readest": "O Readest",
"Add your notes here...": "Dodaj swoje notatki tutaj...",
"Animation": "Animacja",
"Annotate": "Adnotacja",
"Apply": "Zastosuj",
"Are you sure to delete the selected books?": "Czy na pewno chcesz usunąć wybrane książki?",
"Auto Mode": "Tryb automatyczny",
"Behavior": "Zachowanie",
"Book": "Książka",
"Book Cover": "Okładka książki",
"Bookmark": "Zakładka",
"Cancel": "Anuluj",
"Chapter": "Rozdział",
"Cherry": "Wiśniowy",
"Color": "Kolor",
"Confirm": "Potwierdź",
"Confirm Deletion": "Potwierdź usunięcie",
"Copied to notebook": "Skopiowano do notatnika",
"Copy": "Kopiuj",
"Custom CSS": "Własny CSS",
"Dark Mode": "Tryb ciemny",
"Default": "Domyślne",
"Default Font": "Domyślna czcionka",
"Default Font Size": "Domyślny rozmiar czcionki",
"Delete": "Usuń",
"Delete Highlight": "Usuń zaznaczenie",
"Dictionary": "Słownik",
"Disable Click-to-Flip": "Wyłącz przewracanie kliknięciem",
"Download Readest": "Pobierz Readest",
"Edit": "Edytuj",
"Enter your custom CSS here...": "Wprowadź własny CSS tutaj...",
"Excerpts": "Fragmenty",
"Failed to import book(s): {{filenames}}": "Nie udało się zaimportować książek: {{filenames}}",
"Fast": "Szybko",
"Font": "Czcionka",
"Font & Layout": "Czcionka i układ",
"Font Face": "Krój czcionki",
"Font Family": "Rodzina czcionek",
"Font Size": "Rozmiar czcionki",
"Full Justification": "Pełne justowanie",
"Global Settings": "Ustawienia globalne",
"Go Back": "Wstecz",
"Go Forward": "Dalej",
"Go Left": "W lewo",
"Go Right": "W prawo",
"Grass": "Trawiasty",
"Gray": "Szary",
"Gruvbox": "Ciepły pomarańczowy",
"Highlight": "Zaznacz",
"Horizontal Direction": "Kierunek poziomy",
"Hyphenation": "Dzielenie wyrazów",
"Identifier:": "Identyfikator:",
"Import Books": "Importuj książki",
"Invert Colors in Dark Mode": "Odwróć kolory w trybie ciemnym",
"Language:": "Język:",
"Layout": "Układ",
"Light Mode": "Tryb jasny",
"Loading...": "Ładowanie...",
"Loc. {{currentPage}} / {{totalPage}}": "Poz. {{currentPage}} / {{totalPage}}",
"Logged in": "Zalogowano",
"Logged in as {{userDisplayName}}": "Zalogowano jako {{userDisplayName}}",
"Match Case": "Uwzględnij wielkość liter",
"Match Diacritics": "Uwzględnij znaki diakrytyczne",
"Match Whole Words": "Uwzględnij całe słowa",
"Maximum Block Size": "Maksymalny rozmiar bloku",
"Maximum Inline Size": "Maksymalny rozmiar linii",
"Maximum Number of Columns": "Maksymalna liczba kolumn",
"Minimum Font Size": "Minimalny rozmiar czcionki",
"Misc": "Różne",
"Monospace Font": "Czcionka o stałej szerokości",
"More Info": "Więcej informacji",
"Nord": "Północny",
"Notebook": "Notatnik",
"Notes": "Notatki",
"Open": "Otwórz",
"Original Text": "Tekst oryginalny",
"Page": "Strona",
"Paging Animation": "Animacja przewracania stron",
"Paragraph": "Akapit",
"Parallel Read": "Czytanie równoległe",
"Published:": "Opublikowano:",
"Publisher:": "Wydawca:",
"Reading Progress Synced": "Zsynchronizowano postęp czytania",
"Reload Page": "Odśwież stronę",
"Reveal in File Explorer": "Pokaż w Eksploratorze plików",
"Reveal in Finder": "Pokaż w Finderze",
"Reveal in Folder": "Pokaż w folderze",
"Sans-Serif Font": "Czcionka bezszeryfowa",
"Save": "Zapisz",
"Scrolled Mode": "Tryb przewijania",
"Search": "Szukaj",
"Search Books...": "Szukaj książek...",
"Search...": "Szukaj...",
"Select Book": "Wybierz książkę",
"Select Books": "Wybierz książki",
"Select Multiple Books": "Wybierz wiele książek",
"Sepia": "Sepia",
"Serif Font": "Czcionka szeryfowa",
"Show Book Details": "Pokaż szczegóły książki",
"Sidebar": "Panel boczny",
"Sign In": "Zaloguj się",
"Sign Out": "Wyloguj się",
"Sky": "Niebieski",
"Slow": "Wolno",
"Solarized": "Słoneczny",
"Speak": "Czytaj",
"Subjects:": "Tematy:",
"System Fonts": "Czcionki systemowe",
"TTS not supported in this device": "TTS nie jest obsługiwane na tym urządzeniu",
"Theme Color": "Kolor motywu",
"Theme Mode": "Tryb motywu",
"Translate": "Tłumacz",
"Translated Text": "Przetłumaczony tekst",
"Unknown": "Nieznane",
"Untitled": "Bez tytułu",
"Updated:": "Zaktualizowano:",
"User avatar": "Awatar użytkownika",
"Version {{version}}": "Wersja {{version}}",
"Vertical Direction": "Kierunek pionowy",
"Welcome to your library. You can import your books here and read them anytime.": "Witaj w swojej bibliotece. Możesz tutaj zaimportować swoje książki i czytać je w dowolnym momencie.",
"Wikipedia": "Wikipedia",
"Writing Mode": "Tryb pisania",
"Your Library": "Twoja biblioteka",
"TTS not supported for PDF": "TTS nie jest obsługiwane dla plików PDF",
"Override Book Font": "Nadpisz czcionkę książki",
"Vertical Margins (px)": "Marginesy pionowe (px)",
"Horizontal Margins (%)": "Marginesy poziome (%)",
"Apply to All Books": "Zastosuj do wszystkich książek",
"Apply to This Book": "Zastosuj do tej książki",
"Unable to fetch the translation. Try again later.": "Nie udało się pobrać tłumaczenia. Spróbuj ponownie później.",
"Update Now!": "Zaktualizuj teraz!",
"Update": "Zaktualizuj",
"Check Update": "Sprawdź aktualizację",
"Already the latest version": "Już najnowsza wersja",
"Book Details": "Szczegóły książki",
"From Local File": "Z pliku lokalnego",
"TOC": "Spis treści",
"Book uploaded: {{title}}": "Książka przesłana: {{title}}",
"Failed to upload book: {{title}}": "Nie udało się przesłać książki: {{title}}",
"Book downloaded: {{title}}": "Książka pobrana: {{title}}",
"Failed to download book: {{title}}": "Nie udało się pobrać książki: {{title}}",
"Upload Book": "Prześlij książkę",
"Auto Upload Books to Cloud": "Automatycznie przesyłaj książki do chmury",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% przestrzeni dyskowej w chmurze użytej.",
"Storage": "Przechowywanie",
"Book deleted: {{title}}": "Książka usunięta: {{title}}",
"Failed to delete book: {{title}}": "Nie udało się usunąć książki: {{title}}",
"Check Updates on Start": "Sprawdź aktualizacje przy starcie",
"Insufficient storage quota": "Niewystarczająca kwota przechowywania",
"Font Weight": "Grubość czcionki",
"Line Spacing": "Interlinia",
"Word Spacing": "Odstęp między wyrazami",
"Letter Spacing": "Odstęp między literami",
"Text Indent": "Wcięcie tekstu",
"Paragraph Margin": "Margines akapitu",
"Override Book Layout": "Nadpisz układ książki",
"Add to Group": "Dodaj do grupy",
"Untitled Group": "Grupa bez tytułu",
"Group Books": "Grupuj książki",
"Remove From Group": "Usuń z grupy",
"Create New Group": "Utwórz nową grupę",
"Deselect Book": "Odznacz książkę",
"Download Book": "Pobierz książkę",
"Deselect Group": "Odznacz grupę",
"Select Group": "Wybierz grupę",
"Keep Screen Awake": "Nie wygaszaj ekranu",
"Email address": "Adres email",
"Your Password": "Twoje hasło",
"Your email address": "Twój adres email",
"Your password": "Twoje hasło",
"Sign in": "Zaloguj się",
"Signing in...": "Logowanie...",
"Sign in with {{provider}}": "Zaloguj się za pomocą {{provider}}",
"Already have an account? Sign in": "Masz już konto? Zaloguj się",
"Create a Password": "Utwórz hasło",
"Sign up": "Zarejestruj się",
"Signing up...": "Rejestracja...",
"Dont have an account? Sign up": "Nie masz konta? Zarejestruj się",
"Check your email for the confirmation link": "Sprawdź swój email, aby uzyskać link potwierdzający",
"Signing in ...": "Logowanie ...",
"Send a magic link email": "Wyślij link magiczny na email",
"Check your email for the magic link": "Sprawdź swój email, aby uzyskać link magiczny",
"Send reset password instructions": "Wyślij instrukcje resetowania hasła",
"Sending reset instructions ...": "Wysyłanie instrukcji resetowania hasła ...",
"Forgot your password?": "Zapomniałeś hasła?",
"Check your email for the password reset link": "Sprawdź swój email, aby uzyskać link resetujący hasło",
"New Password": "Nowe hasło",
"Your new password": "Twoje nowe hasło",
"Update password": "Zaktualizuj hasło",
"Updating password ...": "Aktualizacja hasła ...",
"Your password has been updated": "Twoje hasło zostało zaktualizowane",
"Phone number": "Numer telefonu",
"Your phone number": "Twój numer telefonu",
"Token": "Token",
"Your OTP token": "Twój OTP token",
"Verify token": "Weryfikacja tokena",
"Sign in with Google": "Zaloguj się za pomocą Google",
"Sign in with Apple": "Zaloguj się za pomocą Apple",
"Sign in with GitHub": "Zaloguj się za pomocą GitHub"
}
@@ -1,7 +1,11 @@
{
"(detected)": "(detectado)",
"About Readest": "Sobre o Readest",
"Add your notes here...": "Adicione suas notas aqui...",
"Animation": "Animação",
"Annotate": "Anotar",
"Apply": "Aplicar",
"Are you sure to delete the selected books?": "Tem certeza de que deseja excluir os livros selecionados?",
"Auto Mode": "Modo Automático",
"Behavior": "Comportamento",
"Book": "Livro",
@@ -13,25 +17,29 @@
"Color": "Cor",
"Confirm": "Confirmar",
"Confirm Deletion": "Confirmar Exclusão",
"Copied to notebook": "Copiado para o caderno",
"Copy": "Copiar",
"Custom CSS": "CSS Personalizado",
"Dark Mode": "Modo Escuro",
"Default": "Padrão",
"Default Font": "Fonte Padrão",
"Default Font Size": "Tamanho da Fonte Padrão",
"Delete": "Excluir",
"Delete Highlight": "Excluir Destaque",
"Dictionary": "Dicionário",
"Disable Click-to-Flip": "Desativar Clique para Virar",
"Download Readest": "Baixar Readest",
"Edit": "Editar",
"Enter your custom CSS here...": "Insira seu CSS personalizado aqui...",
"Excerpts": "Trechos",
"Failed to import book(s): {{filenames}}": "Falha ao importar livro(s): {{filenames}}",
"Fast": "Rápido",
"Font": "Fonte",
"Font & Layout": "Fonte e Layout",
"Font Face": "Estilo da Fonte",
"Font Family": "Família da Fonte",
"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",
@@ -40,6 +48,8 @@
"Grass": "Grama",
"Gray": "Cinza",
"Gruvbox": "Gruvbox",
"Highlight": "Destaque",
"Horizontal Direction": "Direção Horizontal",
"Hyphenation": "Hifenização",
"Identifier:": "Identificador:",
"Import Books": "Importar Livros",
@@ -47,12 +57,10 @@
"Language:": "Idioma:",
"Layout": "Layout",
"Light Mode": "Modo Claro",
"Line Height": "Altura da Linha",
"Loading...": "Carregando...",
"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",
@@ -67,14 +75,14 @@
"Notebook": "Caderno",
"Notes": "Notas",
"Open": "Abrir",
"Override Publisher Font": "Substituir Fonte do Editor",
"Original Text": "Texto Original",
"Page": "Página",
"Paging Animation": "Animação de Página",
"Paragraph": "Parágrafo",
"Parallel Read": "Leitura Paralela",
"Published:": "Publicado:",
"Publisher:": "Editora:",
"Reading progress synced": "Progresso de leitura sincronizado",
"Reading Progress Synced": "Progresso de leitura sincronizado",
"Reload Page": "Recarregar Página",
"Reveal in File Explorer": "Mostrar no Explorador de Arquivos",
"Reveal in Finder": "Mostrar no Finder",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "Fonte Sans-Serif",
"Save": "Salvar",
"Scrolled Mode": "Modo de Rolagem",
"Search books...": "Procurar livros...",
"Search": "Buscar",
"Search Books...": "Procurar livros...",
"Search...": "Pesquisar...",
"Select books": "Selecionar livros",
"Select multiple books": "Selecionar múltiplos livros",
"Select Book": "Selecionar Livro",
"Select Books": "Selecionar livros",
"Select Multiple Books": "Selecionar múltiplos livros",
"Sepia": "Sépia",
"Serif Font": "Fonte Serif",
"Show Book Details": "Mostrar Detalhes do Livro",
"Sidebar": "Barra Lateral",
"Sign In": "Entrar",
"Sign Out": "Sair",
"Sky": "Céu",
"Slow": "Lento",
"Solarized": "Solarizado",
"Speak": "Falar",
"Subjects:": "Assuntos:",
"System Fonts": "Fontes do Sistema",
"TTS not supported in this device": "TTS não suportado neste dispositivo",
"Theme Color": "Cor do Tema",
"Theme Mode": "Modo do Tema",
"Translate": "Traduzir",
"Translated Text": "Texto Traduzido",
"Unknown": "Desconhecido",
"Untitled": "Sem Título",
"Updated:": "Atualizado:",
"User avatar": "Avatar do usuário",
"Version {{version}}": "Versão {{version}}",
"Vertical Direction": "Direção Vertical",
"Welcome to your library. You can import your books here and read them anytime.": "Bem-vindo à sua biblioteca. Você pode importar seus livros aqui e lê-los a qualquer momento.",
"Your Library": "Sua Biblioteca"
"Wikipedia": "Wikipédia",
"Writing Mode": "Modo de Escrita",
"Your Library": "Sua Biblioteca",
"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.",
"Update Now!": "Atualizar Agora!",
"Update": "Atualizar",
"Check Update": "Verificar atualização",
"Already the latest version": "Já é a versão mais recente",
"Book Details": "Detalhes do Livro",
"From Local File": "Do arquivo local",
"TOC": "Índice",
"Book uploaded: {{title}}": "Livro enviado: {{title}}",
"Failed to upload book: {{title}}": "Falha ao enviar livro: {{title}}",
"Book downloaded: {{title}}": "Livro baixado: {{title}}",
"Failed to download book: {{title}}": "Falha ao baixar livro: {{title}}",
"Upload Book": "Enviar Livro",
"Auto Upload Books to Cloud": "Enviar Livros para a Nuvem Automaticamente",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% do Armazenamento na Nuvem Usado.",
"Storage": "Armazenamento",
"Book deleted: {{title}}": "Livro excluído: {{title}}",
"Failed to delete book: {{title}}": "Falha ao excluir livro: {{title}}",
"Check Updates on Start": "Verificar Atualizações ao Iniciar",
"Insufficient storage quota": "Cota de armazenamento insuficiente",
"Font Weight": "Peso da Fonte",
"Line Spacing": "Espaçamento de Linha",
"Word Spacing": "Espaçamento de Palavra",
"Letter Spacing": "Espaçamento de Letra",
"Text Indent": "Recuo de Texto",
"Paragraph Margin": "Margem de Parágrafo",
"Override Book Layout": "Sobrescrever Layout do Livro",
"Add to Group": "Adicionar ao Grupo",
"Untitled Group": "Grupo Sem Título",
"Group Books": "Agrupar Livros",
"Remove From Group": "Remover do Grupo",
"Create New Group": "Criar Novo Grupo",
"Deselect Book": "Desmarcar Livro",
"Download Book": "Baixar Livro",
"Deselect Group": "Desmarcar Grupo",
"Select Group": "Selecionar Grupo",
"Keep Screen Awake": "Manter a Tela Ativa",
"Email address": "Endereço de e-mail",
"Your Password": "Sua senha",
"Your email address": "Seu endereço de e-mail",
"Your password": "Sua senha",
"Sign in": "Entrar",
"Signing in...": "Entrando...",
"Sign in with {{provider}}": "Entrar com {{provider}}",
"Already have an account? Sign in": "Já tem uma conta? Entrar",
"Create a Password": "Criar uma senha",
"Sign up": "Cadastrar-se",
"Signing up...": "Cadastrando...",
"Dont have an account? Sign up": "Não tem uma conta? Cadastre-se",
"Check your email for the confirmation link": "Verifique seu e-mail para o link de confirmação",
"Signing in ...": "Entrando ...",
"Send a magic link email": "Enviar um link mágico por e-mail",
"Check your email for the magic link": "Verifique seu e-mail para o link mágico",
"Send reset password instructions": "Enviar instruções para redefinir a senha",
"Sending reset instructions ...": "Enviando instruções para redefinir a senha ...",
"Forgot your password?": "Esqueceu sua senha?",
"Check your email for the password reset link": "Verifique seu e-mail para o link de redefinição de senha",
"New Password": "Nova senha",
"Your new password": "Sua nova senha",
"Update password": "Atualizar senha",
"Updating password ...": "Atualizando senha ...",
"Your password has been updated": "Sua senha foi atualizada",
"Phone number": "Número de telefone",
"Your phone number": "Seu número de telefone",
"Token": "Token",
"Your OTP token": "Seu token OTP",
"Verify token": "Verificar token",
"Sign in with Google": "Entrar com o Google",
"Sign in with Apple": "Entrar com o Apple",
"Sign in with GitHub": "Entrar com o GitHub"
}
@@ -1,7 +1,11 @@
{
"(detected)": "(обнаружено)",
"About Readest": "О Readest",
"Add your notes here...": "Добавьте свои заметки здесь...",
"Animation": "Анимация",
"Annotate": "Аннотация",
"Apply": "Применить",
"Are you sure to delete the selected books?": "Вы уверены, что хотите удалить выбранные книги?",
"Auto Mode": "Автоматический режим",
"Behavior": "Поведение",
"Book": "Книга",
@@ -13,25 +17,29 @@
"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": "Полное выравнивание",
"Gaps (%)": "Промежутки (%)",
"Global Settings": "Общие настройки",
"Go Back": "Назад",
"Go Forward": "Вперёд",
@@ -40,6 +48,8 @@
"Grass": "Трава",
"Gray": "Серый",
"Gruvbox": "Gruvbox",
"Highlight": "Выделить",
"Horizontal Direction": "Горизонтальное направление",
"Hyphenation": "Перенос слов",
"Identifier:": "Идентификатор:",
"Import Books": "Импорт книг",
@@ -47,12 +57,10 @@
"Language:": "Язык:",
"Layout": "Макет",
"Light Mode": "Светлая тема",
"Line Height": "Межстрочный интервал",
"Loading...": "Загрузка...",
"Loc. {{currentPage}} / {{totalPage}}": "Поз. {{currentPage}} / {{totalPage}}",
"Logged in": "Выполнен вход",
"Logged in as {{userDisplayName}}": "Вход выполнен как {{userDisplayName}}",
"Margins (px)": "Отступы (px)",
"Match Case": "Учитывать регистр",
"Match Diacritics": "Учитывать диакритические знаки",
"Match Whole Words": "Только целые слова",
@@ -67,14 +75,14 @@
"Notebook": "Блокнот",
"Notes": "Заметки",
"Open": "Открыть",
"Override Publisher Font": "Переопределить шрифт издателя",
"Original Text": "Оригинальный текст",
"Page": "Страница",
"Paging Animation": "Анимация перелистывания",
"Paragraph": "Абзац",
"Parallel Read": "Параллельное чтение",
"Published:": "Опубликовано:",
"Publisher:": "Издатель:",
"Reading progress synced": "Синхронизирован прогресс чтения",
"Reading Progress Synced": "Синхронизирован прогресс чтения",
"Reload Page": "Перезагрузить страницу",
"Reveal in File Explorer": "Показать в проводнике",
"Reveal in Finder": "Показать в Finder",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "Шрифт без засечек",
"Save": "Сохранить",
"Scrolled Mode": "Режим прокрутки",
"Search books...": "Поиск книг...",
"Search": "Поиск",
"Search Books...": "Поиск книг...",
"Search...": "Поиск...",
"Select books": "Выбрать книги",
"Select multiple books": "Выбрать несколько книг",
"Select Book": "Выбрать книгу",
"Select Books": "Выбрать книги",
"Select Multiple Books": "Выбрать несколько книг",
"Sepia": "Сепия",
"Serif Font": "Шрифт с засечками",
"Show Book Details": "Показать детали книги",
"Sidebar": "Боковая панель",
"Sign In": "Войти",
"Sign Out": "Выйти",
"Sky": "Небесный",
"Slow": "Медленно",
"Solarized": "Солнечный",
"Speak": "Произнести",
"Subjects:": "Темы:",
"System Fonts": "Системные шрифты",
"TTS not supported in this device": "TTS не поддерживается на этом устройстве",
"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.": "Добро пожаловать в вашу библиотеку. Вы можете импортировать сюда книги и читать их в любое время.",
"Your Library": "Ваша библиотека"
"Wikipedia": "Википедия",
"Writing Mode": "Режим записи",
"Your Library": "Ваша библиотека",
"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.": "Не удалось получить перевод. Попробуйте позже.",
"Update Now!": "Обновить сейчас!",
"Update": "Обновить",
"Check Update": "Проверить обновление",
"Already the latest version": "Уже последняя версия",
"Book Details": "Детали книги",
"From Local File": "Из локального файла",
"TOC": "Содержание",
"Book uploaded: {{title}}": "Книга загружена: {{title}}",
"Failed to upload book: {{title}}": "Не удалось загрузить книгу: {{title}}",
"Book downloaded: {{title}}": "Книга скачана: {{title}}",
"Failed to download book: {{title}}": "Не удалось скачать книгу: {{title}}",
"Upload Book": "Загрузить книгу",
"Auto Upload Books to Cloud": "Автоматическая загрузка книг в облако",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% использовано облачного хранилища.",
"Storage": "Хранилище",
"Book deleted: {{title}}": "Книга удалена: {{title}}",
"Failed to delete book: {{title}}": "Не удалось удалить книгу: {{title}}",
"Check Updates on Start": "Проверять обновления при запуске",
"Insufficient storage quota": "Недостаточно квоты хранилища",
"Font Weight": "Насыщенность шрифта",
"Line Spacing": "Межстрочный интервал",
"Word Spacing": "Межсловный интервал",
"Letter Spacing": "Межбуквенный интервал",
"Text Indent": "Отступ текста",
"Paragraph Margin": "Отступ абзаца",
"Override Book Layout": "Переопределить макет книги",
"Add to Group": "Добавить в группу",
"Untitled Group": "Группа без названия",
"Group Books": "Группировать книги",
"Remove From Group": "Удалить из группы",
"Create New Group": "Создать новую группу",
"Deselect Book": "Отменить выбор книги",
"Download Book": "Скачать книгу",
"Deselect Group": "Отменить выбор группы",
"Select Group": "Выбрать группу",
"Keep Screen Awake": "Не выключать экран",
"Email address": "Адрес электронной почты",
"Your Password": "Ваш пароль",
"Your email address": "Ваш адрес электронной почты",
"Your password": "Ваш пароль",
"Sign in": "Войти",
"Signing in...": "Вход...",
"Sign in with {{provider}}": "Войти с помощью {{provider}}",
"Already have an account? Sign in": "Уже есть аккаунт? Войдите",
"Create a Password": "Создать пароль",
"Sign up": "Зарегистрироваться",
"Signing up...": "Регистрация...",
"Dont have an account? Sign up": "Нет аккаунта? Зарегистрируйтесь",
"Check your email for the confirmation link": "Проверьте свою почту для подтверждающей ссылки",
"Signing in ...": "Вход ...",
"Send a magic link email": "Отправить магическую ссылку по электронной почте",
"Check your email for the magic link": "Проверьте свою почту для магической ссылки",
"Send reset password instructions": "Отправить инструкции по сбросу пароля",
"Sending reset instructions ...": "Отправка инструкций по сбросу пароля ...",
"Forgot your password?": "Забыли пароль?",
"Check your email for the password reset link": "Проверьте свою почту для ссылки для сброса пароля",
"New Password": "Новый пароль",
"Your new password": "Ваш новый пароль",
"Update password": "Обновить пароль",
"Updating password ...": "Обновление пароля ...",
"Your password has been updated": "Ваш пароль был обновлен",
"Phone number": "Номер телефона",
"Your phone number": "Ваш номер телефона",
"Token": "Токен",
"Your OTP token": "Ваш OTP токен",
"Verify token": "Проверить токен",
"Sign in with Google": "Войти через Google",
"Sign in with Apple": "Войти через Apple",
"Sign in with GitHub": "Войти через GitHub"
}
@@ -1,7 +1,11 @@
{
"(detected)": "(algılandı)",
"About Readest": "Readest Hakkında",
"Add your notes here...": "Notlarınızı buraya ekleyin...",
"Animation": "Animasyon",
"Annotate": "Not Ekle",
"Apply": "Uygula",
"Are you sure to delete the selected books?": "Seçili kitapları silmek istediğinizden emin misiniz?",
"Auto Mode": "Otomatik Mod",
"Behavior": "Davranış",
"Book": "Kitap",
@@ -13,25 +17,29 @@
"Color": "Renk",
"Confirm": "Onayla",
"Confirm Deletion": "Silmeyi Onayla",
"Copied to notebook": "Not defterine kopyalandı",
"Copy": "Kopyala",
"Custom CSS": "Özel CSS",
"Dark Mode": "Karanlık Mod",
"Default": "Varsayılan",
"Default Font": "Varsayılan Yazı Tipi",
"Default Font Size": "Varsayılan Yazı Boyutu",
"Delete": "Sil",
"Delete Highlight": "Vurguyu Sil",
"Dictionary": "Sözlük",
"Disable Click-to-Flip": "Tıkla-Çevir'i Devre Dışı Bırak",
"Download Readest": "Readest'i İndir",
"Edit": "Düzenle",
"Enter your custom CSS here...": "Özel CSS'nizi buraya girin...",
"Excerpts": "Alıntılar",
"Failed to import book(s): {{filenames}}": "Kitap(lar) içe aktarılamadı: {{filenames}}",
"Fast": "Hızlı",
"Font": "Yazı Tipi",
"Font & Layout": "Yazı Tipi ve Düzen",
"Font Face": "Yazı Tipi Yüzü",
"Font Family": "Yazı Tipi Ailesi",
"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",
@@ -40,6 +48,8 @@
"Grass": "Çimen",
"Gray": "Gri",
"Gruvbox": "Gruvbox",
"Highlight": "Vurgula",
"Horizontal Direction": "Yatay Yön",
"Hyphenation": "Heceleme",
"Identifier:": "Tanımlayıcı:",
"Import Books": "Kitapları İçe Aktar",
@@ -47,12 +57,10 @@
"Language:": "Dil:",
"Layout": "Düzen",
"Light Mode": "Aydınlık Mod",
"Line Height": "Satır Yüksekliği",
"Loading...": "Yükleniyor...",
"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",
@@ -67,14 +75,14 @@
"Notebook": "Not Defteri",
"Notes": "Notlar",
"Open": "Aç",
"Override Publisher Font": "Yayıncı Yazı Tipini Geçersiz Kıl",
"Original Text": "Orijinal Metin",
"Page": "Sayfa",
"Paging Animation": "Sayfa Çevirme Animasyonu",
"Paragraph": "Paragraf",
"Parallel Read": "Paralel Okuma",
"Published:": "Yayınlanma:",
"Publisher:": "Yayıncı:",
"Reading progress synced": "Okuma ilerlemesi senkronize edildi",
"Reading Progress Synced": "Okuma ilerlemesi senkronize edildi",
"Reload Page": "Sayfayı Yenile",
"Reveal in File Explorer": "Dosya Gezgininde Göster",
"Reveal in Finder": "Finder'da Göster",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "Sans-Serif Yazı Tipi",
"Save": "Kaydet",
"Scrolled Mode": "Kaydırma Modu",
"Search books...": "Kitap ara...",
"Search": "Ara",
"Search Books...": "Kitap ara...",
"Search...": "Ara...",
"Select books": "Kitapları seç",
"Select multiple books": "Birden fazla kitap seç",
"Select Book": "Kitap Seç",
"Select Books": "Kitapları seç",
"Select Multiple Books": "Birden fazla kitap seç",
"Sepia": "Sepya",
"Serif Font": "Serif Yazı Tipi",
"Show Book Details": "Kitap Detaylarını Göster",
"Sidebar": "Kenar Çubuğu",
"Sign In": "Giriş Yap",
"Sign Out": "Çıkış Yap",
"Sky": "Gökyüzü",
"Slow": "Yavaş",
"Solarized": "Solarized",
"Speak": "Konuş",
"Subjects:": "Konular:",
"System Fonts": "Sistem Yazı Tipleri",
"TTS not supported in this device": "Bu cihazda TTS desteklenmiyor",
"Theme Color": "Tema Rengi",
"Theme Mode": "Tema Modu",
"Translate": "Çevir",
"Translated Text": "Çevrilen Metin",
"Unknown": "Bilinmiyor",
"Untitled": "Başlıksız",
"Updated:": "Güncellendi:",
"User avatar": "Kullanıcı avatarı",
"Version {{version}}": "Sürüm {{version}}",
"Vertical Direction": "Dikey Yön",
"Welcome to your library. You can import your books here and read them anytime.": "Kütüphanenize hoş geldiniz. Buradan kitaplarınızı içe aktarabilir ve istediğiniz zaman okuyabilirsiniz.",
"Your Library": "Kütüphaneniz"
"Wikipedia": "Vikipedi",
"Writing Mode": "Yazma Modu",
"Your Library": "Kütüphaneniz",
"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.",
"Update Now!": "Şimdi Güncelle!",
"Update": "Güncelle",
"Check Update": "Güncellemeyi kontrol et",
"Already the latest version": "Zaten en son sürüm",
"Book Details": "Kitap Detayları",
"From Local File": "Yerel dosyadan",
"TOC": "İçindekiler",
"Book uploaded: {{title}}": "Kitap yüklendi: {{title}}",
"Failed to upload book: {{title}}": "Kitap yüklenemedi: {{title}}",
"Book downloaded: {{title}}": "Kitap indirildi: {{title}}",
"Failed to download book: {{title}}": "Kitap indirilemedi: {{title}}",
"Upload Book": "Kitap Yükle",
"Auto Upload Books to Cloud": "Kitapları Otomatik Olarak Buluta Yükle",
"{{percentage}}% of Cloud Storage Used.": "Bulut Depolamanın %{{percentage}}'si kullanıldı.",
"Storage": "Depolama",
"Book deleted: {{title}}": "Kitap silindi: {{title}}",
"Failed to delete book: {{title}}": "Kitap silinemedi: {{title}}",
"Check Updates on Start": "Başlangıçta Güncellemeleri Kontrol Et",
"Insufficient storage quota": "Yetersiz depolama kotası",
"Font Weight": "Yazı Tipi Ağırlığı",
"Line Spacing": "Satır Aralığı",
"Word Spacing": "Kelime Aralığı",
"Letter Spacing": "Harf Aralığı",
"Text Indent": "Metin Girintisi",
"Paragraph Margin": "Paragraf Kenarı",
"Override Book Layout": "Kitap Düzenini Geçersiz Kıl",
"Add to Group": "Gruba Ekle",
"Untitled Group": "Başlıksız Grup",
"Group Books": "Kitapları Grupla",
"Remove From Group": "Grubu Kaldır",
"Create New Group": "Yeni Grup Oluştur",
"Deselect Book": "Kitabı Seçme",
"Download Book": "Kitabı İndir",
"Deselect Group": "Grubu Seçme",
"Select Group": "Grup Seç",
"Keep Screen Awake": "Ekranı Açık Tut",
"Email address": "E-posta adresi",
"Your Password": "Şifreniz",
"Your email address": "E-posta adresiniz",
"Your password": "Şifreniz",
"Sign in": "Giriş yap",
"Signing in...": "Giriş yapılıyor...",
"Sign in with {{provider}}": "{{provider}} ile giriş yap",
"Already have an account? Sign in": "Hesabınız var mı? Giriş yapın",
"Create a Password": "Şifre oluştur",
"Sign up": "Kaydol",
"Signing up...": "Kaydolunuyor...",
"Dont have an account? Sign up": "Hesabınız yok mu? Kaydolun",
"Check your email for the confirmation link": "Onay bağlantısı için e-posta adresinizi kontrol edin",
"Signing in ...": "Giriş yapılıyor...",
"Send a magic link email": "Sihirli bağlantı e-postası gönder",
"Check your email for the magic link": "Sihirli bağlantı için e-posta adresinizi kontrol edin",
"Send reset password instructions": "Şifre sıfırlama talimatları gönder",
"Sending reset instructions ...": "Şifre sıfırlama talimatları gönderiliyor...",
"Forgot your password?": "Şifrenizi mi unuttunuz?",
"Check your email for the password reset link": "Şifre sıfırlama bağlantısı için e-posta adresinizi kontrol edin",
"New Password": "Yeni şifre",
"Your new password": "Yeni şifreniz",
"Update password": "Şifreyi güncelle",
"Updating password ...": "Şifre güncelleniyor...",
"Your password has been updated": "Şifreniz güncellendi",
"Phone number": "Telefon numarası",
"Your phone number": "Telefon numaranız",
"Token": "Token",
"Your OTP token": "OTP token'ınız",
"Verify token": "Token'ı doğrula",
"Sign in with Google": "Google ile giriş yap",
"Sign in with Apple": "Apple ile giriş yap",
"Sign in with GitHub": "GitHub ile giriş yap"
}
@@ -0,0 +1,202 @@
{
"(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": "Розмір шрифту",
"Full Justification": "Повне вирівнювання",
"Global Settings": "Глобальні налаштування",
"Go Back": "Назад",
"Go Forward": "Вперед",
"Go Left": "Вліво",
"Go Right": "Вправо",
"Grass": "Трав'яний",
"Gray": "Сірий",
"Gruvbox": "Теплий помаранчевий",
"Highlight": "Виділення",
"Horizontal Direction": "Горизонтальний напрямок",
"Hyphenation": "Перенесення слів",
"Identifier:": "Ідентифікатор:",
"Import Books": "Імпортувати книги",
"Invert Colors in Dark Mode": "Інвертувати кольори в темному режимі",
"Language:": "Мова:",
"Layout": "Макет",
"Light Mode": "Світлий режим",
"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": "Північний",
"Notebook": "Нотатник",
"Notes": "Нотатки",
"Open": "Відкрити",
"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": "Шрифт без засічок",
"Save": "Зберегти",
"Scrolled Mode": "Режим прокрутки",
"Search": "Пошук",
"Search Books...": "Шукати книги...",
"Search...": "Пошук...",
"Select Book": "Вибрати книгу",
"Select Books": "Вибрати книги",
"Select Multiple Books": "Вибрати кілька книг",
"Sepia": "Сепія",
"Serif Font": "Шрифт із засічками",
"Show Book Details": "Показати деталі книги",
"Sidebar": "Бічна панель",
"Sign In": "Увійти",
"Sign Out": "Вийти",
"Sky": "Небесний",
"Slow": "Повільно",
"Solarized": "Сонячний",
"Speak": "Озвучити",
"Subjects:": "Теми:",
"System Fonts": "Системні шрифти",
"TTS not supported in this device": "TTS не підтримується на цьому пристрої",
"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": "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.": "Не вдалося отримати переклад. Спробуйте пізніше.",
"Update Now!": "Оновити зараз!",
"Update": "Оновити",
"Check Update": "Перевірити оновлення",
"Already the latest version": "Вже остання версія",
"Book Details": "Деталі книги",
"From Local File": "З локального файлу",
"TOC": "Зміст",
"Book uploaded: {{title}}": "Книгу завантажено: {{title}}",
"Failed to upload book: {{title}}": "Не вдалося завантажити книгу: {{title}}",
"Book downloaded: {{title}}": "Книгу завантажено: {{title}}",
"Failed to download book: {{title}}": "Не вдалося завантажити книгу: {{title}}",
"Upload Book": "Завантажити книгу",
"Auto Upload Books to Cloud": "Автоматично завантажувати книги в хмару",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% використано хмарного сховища.",
"Storage": "Сховище",
"Book deleted: {{title}}": "Книгу видалено: {{title}}",
"Failed to delete book: {{title}}": "Не вдалося видалити книгу: {{title}}",
"Check Updates on Start": "Перевіряти оновлення при запуску",
"Insufficient storage quota": "Недостатньо квоти сховища",
"Font Weight": "Насиченість шрифту",
"Line Spacing": "Міжрядковий інтервал",
"Word Spacing": "Міжслівний інтервал",
"Letter Spacing": "Міжбуквений інтервал",
"Text Indent": "Відступ тексту",
"Paragraph Margin": "Відступ абзацу",
"Override Book Layout": "Перевизначити макет книги",
"Add to Group": "Додати до групи",
"Untitled Group": "Група без назви",
"Group Books": "Групувати книги",
"Remove From Group": "Видалити з групи",
"Create New Group": "Створити нову групу",
"Deselect Book": "Скасувати вибір книги",
"Download Book": "Завантажити книгу",
"Deselect Group": "Скасувати вибір групи",
"Select Group": "Вибрати групу",
"Keep Screen Awake": "Не вимикати екран",
"Email address": "Адреса електронної пошти",
"Your Password": "Ваш пароль",
"Your email address": "Ваша електронна адреса",
"Your password": "Ваш пароль",
"Sign in": "Увійти",
"Signing in...": "Вхід...",
"Sign in with {{provider}}": "Увійти через {{provider}}",
"Already have an account? Sign in": "Уже є акаунт? Увійти",
"Create a Password": "Створити пароль",
"Sign up": "Зареєструватися",
"Signing up...": "Реєстрація...",
"Dont have an account? Sign up": "Немає акаунту? Зареєструйтеся",
"Check your email for the confirmation link": "Перевірте свою електронну пошту для підтвердження посилання",
"Signing in ...": "Увійти...",
"Send a magic link email": "Відправити магічне посилання на електронну пошту",
"Check your email for the magic link": "Перевірте свою електронну пошту для магічного посилання",
"Send reset password instructions": "Надіслати інструкції для скидання пароля",
"Sending reset instructions ...": "Надсилаються інструкції для скидання пароля...",
"Forgot your password?": "Забули пароль?",
"Check your email for the password reset link": "Перевірте свою електронну пошту для посилання на скидання пароля",
"New Password": "Новий пароль",
"Your new password": "Ваш новий пароль",
"Update password": "Оновити пароль",
"Updating password ...": "Оновлення пароля...",
"Your password has been updated": "Ваш пароль оновлено",
"Phone number": "Номер телефону",
"Your phone number": "Ваш номер телефону",
"Token": "Токен",
"Your OTP token": "Ваш OTP токен",
"Verify token": "Перевірити токен",
"Sign in with Google": "Увійти через Google",
"Sign in with Apple": "Увійти через Apple",
"Sign in with GitHub": "Увійти через GitHub"
}
@@ -1,7 +1,11 @@
{
"(detected)": "(đã phát hiện)",
"About Readest": "Về Readest",
"Add your notes here...": "Thêm ghi chú của bạn vào đây...",
"Animation": "Hiệu ứng động",
"Annotate": "Chú thích",
"Apply": "Áp dụng",
"Are you sure to delete the selected books?": "Bạn có chắc chắn muốn xóa các sách đã chọn?",
"Auto Mode": "Chế độ tự động",
"Behavior": "Hành vi",
"Book": "Sách",
@@ -13,25 +17,29 @@
"Color": "Màu sắc",
"Confirm": "Xác nhận",
"Confirm Deletion": "Xác nhận xóa",
"Copied to notebook": "Đã sao chép vào sổ ghi chú",
"Copy": "Sao chép",
"Custom CSS": "CSS tùy chỉnh",
"Dark Mode": "Chế độ tối",
"Default": "Mặc định",
"Default Font": "Phông chữ mặc định",
"Default Font Size": "Cỡ chữ mặc định",
"Delete": "Xóa",
"Delete Highlight": "Xóa văn bản được đánh dấu",
"Dictionary": "Từ điển",
"Disable Click-to-Flip": "Tắt tính năng nhấp để lật trang",
"Download Readest": "Tải Readest",
"Edit": "Chỉnh sửa",
"Enter your custom CSS here...": "Nhập CSS tùy chỉnh của bạn vào đây...",
"Excerpts": "Trích dẫn",
"Failed to import book(s): {{filenames}}": "Không thể nhập sách: {{filenames}}",
"Fast": "Nhanh",
"Font": "Phông chữ",
"Font & Layout": "Phông chữ & Bố cục",
"Font Face": "Kiểu chữ",
"Font Family": "Họ phông chữ",
"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",
@@ -40,6 +48,8 @@
"Grass": "Xanh cỏ",
"Gray": "Xám",
"Gruvbox": "Gruvbox",
"Highlight": "Đánh dấu",
"Horizontal Direction": "Hướng ngang",
"Hyphenation": "Gạch nối từ",
"Identifier:": "Định danh:",
"Import Books": "Nhập sách",
@@ -47,12 +57,10 @@
"Language:": "Ngôn ngữ:",
"Layout": "Bố cục",
"Light Mode": "Chế độ sáng",
"Line Height": "Độ cao dòng",
"Loading...": "Đang tải...",
"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ừ",
@@ -67,14 +75,14 @@
"Notebook": "Sổ ghi chép",
"Notes": "Ghi chú",
"Open": "Mở",
"Override Publisher Font": "Ghi đè phông chữ của nhà xuất bản",
"Original Text": "Văn bản gốc",
"Page": "Trang",
"Paging Animation": "Hiệu ứng lật trang",
"Paragraph": "Đoạn văn",
"Parallel Read": "Đọc song song",
"Published:": "Xuất bản:",
"Publisher:": "Nhà xuất bản:",
"Reading progress synced": "Tiến độ đọc đã được đồng bộ",
"Reading Progress Synced": "Tiến độ đọc đã được đồng bộ",
"Reload Page": "Tải lại trang",
"Reveal in File Explorer": "Hiển thị trong File Explorer",
"Reveal in Finder": "Hiển thị trong Finder",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "Phông chữ không chân",
"Save": "Lưu",
"Scrolled Mode": "Chế độ cuộn",
"Search books...": "Tìm kiếm sách...",
"Search": "Tìm kiếm",
"Search Books...": "Tìm kiếm sách...",
"Search...": "Tìm kiếm...",
"Select books": "Chọn sách",
"Select multiple books": "Chọn nhiều sách",
"Select Book": "Chọn sách",
"Select Books": "Chọn sách",
"Select Multiple Books": "Chọn nhiều sách",
"Sepia": "Nâu cổ",
"Serif Font": "Phông chữ có chân",
"Show Book Details": "Hiển thị chi tiết sách",
"Sidebar": "Thanh bên",
"Sign In": "Đăng nhập",
"Sign Out": "Đăng xuất",
"Sky": "Xanh trời",
"Slow": "Chậm",
"Solarized": "Solarized",
"Speak": "Đọc",
"Subjects:": "Chủ đề:",
"System Fonts": "Phông chữ hệ thống",
"TTS not supported in this device": "TTS không được hỗ trợ trên thiết bị này",
"Theme Color": "Màu chủ đề",
"Theme Mode": "Chế độ chủ đề",
"Translate": "Dịch",
"Translated Text": "Văn bản dịch",
"Unknown": "Không xác định",
"Untitled": "Không có tiêu đề",
"Updated:": "Cập nhật:",
"User avatar": "Ảnh đại diện",
"Version {{version}}": "Phiên bản {{version}}",
"Vertical Direction": "Hướng dọc",
"Welcome to your library. You can import your books here and read them anytime.": "Chào mừng đến với thư viện của bạn. Bạn có thể nhập sách vào đây và đọc bất cứ lúc nào.",
"Your Library": "Thư viện của bạn"
"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",
"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.",
"Update Now!": "Cập nhật ngay!",
"Update": "Cập nhật",
"Check Update": "Kiểm tra cập nhật",
"Already the latest version": "Đã là phiên bản mới nhất",
"Book Details": "Chi tiết sách",
"From Local File": "Từ tệp địa phương",
"TOC": "Mục lục",
"Book uploaded: {{title}}": "Sách đã tải lên: {{title}}",
"Failed to upload book: {{title}}": "Không thể tải lên sách: {{title}}",
"Book downloaded: {{title}}": "Sách đã tải về: {{title}}",
"Failed to download book: {{title}}": "Không thể tải về sách: {{title}}",
"Upload Book": "Tải lên sách",
"Auto Upload Books to Cloud": "Tự động tải sách lên đám mây",
"{{percentage}}% of Cloud Storage Used.": "{{percentage}}% dung lượng lưu trữ đám mây đã sử dụng.",
"Storage": "Lưu trữ",
"Book deleted: {{title}}": "Sách đã xóa: {{title}}",
"Failed to delete book: {{title}}": "Không thể xóa sách: {{title}}",
"Check Updates on Start": "Kiểm tra cập nhật khi khởi động",
"Insufficient storage quota": "Không đủ lượng lưu trữ",
"Font Weight": "Độ đậm của phông chữ",
"Line Spacing": "Khoảng cách dòng",
"Word Spacing": "Khoảng cách từ",
"Letter Spacing": "Khoảng cách chữ",
"Text Indent": "Thụt lề",
"Paragraph Margin": "Lề đoạn",
"Override Book Layout": "Ghi đè bố cục sách",
"Add to Group": "Thêm vào nhóm",
"Untitled Group": "Nhóm không có tiêu đề",
"Group Books": "Nhóm sách",
"Remove From Group": "Xóa khỏi nhóm",
"Create New Group": "Tạo nhóm mới",
"Deselect Book": "Bỏ chọn sách",
"Download Book": "Tải sách",
"Deselect Group": "Bỏ chọn nhóm",
"Select Group": "Chọn nhóm",
"Keep Screen Awake": "Giữ màn hình luôn bật",
"Email address": "Địa chỉ email",
"Your Password": "Mật khẩu của bạn",
"Your email address": "Địa chỉ email của bạn",
"Your password": "Mật khẩu của bạn",
"Sign in": "Đăng nhập",
"Signing in...": "Đang đăng nhập...",
"Sign in with {{provider}}": "Đăng nhập với {{provider}}",
"Already have an account? Sign in": "Đã có tài khoản? Đăng nhập",
"Create a Password": "Tạo mật khẩu",
"Sign up": "Đăng ký",
"Signing up...": "Đang đăng ký...",
"Dont have an account? Sign up": "Chưa có tài khoản? Đăng ký",
"Check your email for the confirmation link": "Kiểm tra email của bạn để nhận liên kết xác nhận",
"Signing in ...": "Đang đăng nhập ...",
"Send a magic link email": "Gửi email liên kết ma thuật",
"Check your email for the magic link": "Kiểm tra email của bạn để nhận liên kết ma thuật",
"Send reset password instructions": "Gửi hướng dẫn đặt lại mật khẩu",
"Sending reset instructions ...": "Đang gửi hướng dẫn đặt lại mật khẩu ...",
"Forgot your password?": "Quên mật khẩu?",
"Check your email for the password reset link": "Kiểm tra email của bạn để nhận liên kết đặt lại mật khẩu",
"New Password": "Mật khẩu mới",
"Your new password": "Mật khẩu mới của bạn",
"Update password": "Cập nhật mật khẩu",
"Updating password ...": "Đang cập nhật mật khẩu ...",
"Your password has been updated": "Mật khẩu của bạn đã được cập nhật",
"Phone number": "Số điện thoại",
"Your phone number": "Số điện thoại của bạn",
"Token": "Mã token",
"Your OTP token": "Mã OTP của bạn",
"Verify token": "Xác minh mã token",
"Sign in with Google": "Đăng nhập với Google",
"Sign in with Apple": "Đăng nhập với Apple",
"Sign in with GitHub": "Đăng nhập với GitHub"
}
@@ -1,7 +1,11 @@
{
"(detected)": "(检测到)",
"About Readest": "关于 Readest",
"Add your notes here...": "在这里添加您的笔记...",
"Animation": "动画",
"Annotate": "笔记",
"Apply": "应用",
"Are you sure to delete the selected books?": "您确定要删除所选书籍吗?",
"Auto Mode": "自动主题",
"Behavior": "行为",
"Book": "书籍",
@@ -13,25 +17,29 @@
"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": "两端对齐",
"Gaps (%)": "间距 (%)",
"Global Settings": "全局设置",
"Go Back": "返回",
"Go Forward": "前进",
@@ -40,6 +48,8 @@
"Grass": "青草",
"Gray": "素雅",
"Gruvbox": "暖橘",
"Highlight": "划线",
"Horizontal Direction": "横排",
"Hyphenation": "断字",
"Identifier:": "识别码",
"Import Books": "导入书籍",
@@ -47,12 +57,10 @@
"Language:": "语言",
"Layout": "布局",
"Light Mode": "浅色主题",
"Line Height": "行高",
"Loading...": "加载中...",
"Loc. {{currentPage}} / {{totalPage}}": "位置 {{currentPage}} / {{totalPage}}",
"Logged in": "已登录",
"Logged in as {{userDisplayName}}": "{{userDisplayName}} 已登录",
"Margins (px)": "页边距 (px)",
"Match Case": "匹配大小写",
"Match Diacritics": "匹配重音符号",
"Match Whole Words": "匹配整个单词",
@@ -67,14 +75,14 @@
"Notebook": "笔记本",
"Notes": "笔记",
"Open": "打开",
"Override Publisher Font": "覆盖内置字体",
"Page": "页",
"Original Text": "原文",
"Page": "页",
"Paging Animation": "翻页动画",
"Paragraph": "段落",
"Parallel Read": "并排阅读",
"Published:": "出版日期",
"Publisher:": "出版商",
"Reading progress synced": "阅读进度已同步",
"Reading Progress Synced": "阅读进度已同步",
"Reload Page": "重新加载页面",
"Reveal in File Explorer": "在文件管理器中显示",
"Reveal in Finder": "在访达中显示",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "无衬线字体",
"Save": "保存",
"Scrolled Mode": "滚动模式",
"Search books...": "搜索书籍...",
"Search": "搜索",
"Search Books...": "搜索书籍...",
"Search...": "搜索...",
"Select books": "选择书籍",
"Select multiple books": "选择多本书籍",
"Select Book": "选择书籍",
"Select Books": "选择书籍",
"Select Multiple Books": "选择多本书籍",
"Sepia": "旧韵",
"Serif Font": "衬线字体",
"Show Book Details": "显示书籍详情",
"Sidebar": "侧边栏",
"Sign In": "登录",
"Sign Out": "登出",
"Sky": "天青",
"Slow": "慢",
"Solarized": "日晖",
"Speak": "朗读",
"Subjects:": "主题",
"System Fonts": "系统字体",
"TTS not supported in this device": "此设备不支持 TTS",
"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.": "书库空空如也,您可以导入您的书籍并随时阅读。",
"Your Library": "书库"
"Wikipedia": "维基百科",
"Writing Mode": "排版模式",
"Your Library": "书库",
"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.": "无法获取翻译,请稍后重试。",
"Update Now!": "立即更新!",
"Update": "更新",
"Check Update": "检查更新",
"Already the latest version": "已经是最新版本",
"Book Details": "书籍详情",
"From Local File": "从本地文件导入",
"TOC": "目录",
"Book uploaded: {{title}}": "书籍已上传:{{title}}",
"Failed to upload book: {{title}}": "上传书籍失败:{{title}}",
"Book downloaded: {{title}}": "书籍已下载:{{title}}",
"Failed to download book: {{title}}": "下载书籍失败:{{title}}",
"Upload Book": "上传书籍",
"Auto Upload Books to Cloud": "自动上传书籍到云端",
"{{percentage}}% of Cloud Storage Used.": "已使用 {{percentage}}% 云存储空间",
"Storage": "存储空间",
"Book deleted: {{title}}": "书籍已删除:{{title}}",
"Failed to delete book: {{title}}": "删除书籍失败:{{title}}",
"Check Updates on Start": "启动时检查更新",
"Insufficient storage quota": "云存储空间不足",
"Font Weight": "字重",
"Line Spacing": "行间距",
"Word Spacing": "词间距",
"Letter Spacing": "字间距",
"Text Indent": "首行缩进",
"Paragraph Margin": "段间距",
"Override Book Layout": "覆盖版面布局",
"Add to Group": "添加到分组",
"Untitled Group": "无标题分组",
"Group Books": "分组书籍",
"Remove From Group": "从分组中移除",
"Create New Group": "创建新分组",
"Deselect Book": "取消选择书籍",
"Download Book": "下载书籍",
"Deselect Group": "取消选择分组",
"Select Group": "选择分组",
"Keep Screen Awake": "保持屏幕常亮",
"Email address": "电子邮件地址",
"Your Password": "您的密码",
"Your email address": "您的电子邮件地址",
"Your password": "您的密码",
"Sign in": "登录",
"Signing in...": "正在登录...",
"Sign in with {{provider}}": "使用 {{provider}} 登录",
"Already have an account? Sign in": "已经有账号?登录",
"Create a Password": "创建密码",
"Sign up": "注册",
"Signing up...": "正在注册...",
"Dont have an account? Sign up": "没有账号?注册",
"Check your email for the confirmation link": "检查您的电子邮件以获取确认链接",
"Signing in ...": "正在登录...",
"Send a magic link email": "发送魔法链接邮件",
"Check your email for the magic link": "检查您的电子邮件以获取魔法链接",
"Send reset password instructions": "发送重置密码说明",
"Sending reset instructions ...": "正在发送重置密码说明...",
"Forgot your password?": "忘记密码?",
"Check your email for the password reset link": "检查您的电子邮件以获取密码重置链接",
"New Password": "新密码",
"Your new password": "您的新密码",
"Update password": "更新密码",
"Updating password ...": "正在更新密码...",
"Your password has been updated": "您的密码已更新",
"Phone number": "电话号码",
"Your phone number": "您的电话号码",
"Token": "令牌",
"Your OTP token": "您的 OTP 令牌",
"Verify token": "验证令牌",
"Sign in with Google": "使用 Google 登录",
"Sign in with Apple": "使用 Apple 登录",
"Sign in with GitHub": "使用 GitHub 登录"
}
@@ -1,7 +1,11 @@
{
"(detected)": "(檢測到)",
"About Readest": "關於 Readest",
"Add your notes here...": "在這裡添加您的筆記...",
"Animation": "動畫",
"Annotate": "筆記",
"Apply": "應用",
"Are you sure to delete the selected books?": "您確定要刪除所選書籍嗎?",
"Auto Mode": "自動主題",
"Behavior": "行為",
"Book": "書籍",
@@ -13,25 +17,29 @@
"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": "兩端對齊",
"Gaps (%)": "間距 (%)",
"Global Settings": "全局設置",
"Go Back": "返回",
"Go Forward": "前進",
@@ -40,6 +48,8 @@
"Grass": "青草",
"Gray": "素雅",
"Gruvbox": "暖橘",
"Highlight": "劃線",
"Horizontal Direction": "橫排",
"Hyphenation": "斷字",
"Identifier:": "識別碼",
"Import Books": "導入書籍",
@@ -47,12 +57,10 @@
"Language:": "語言",
"Layout": "版面",
"Light Mode": "淺色主題",
"Line Height": "行高",
"Loading...": "載入中...",
"Loc. {{currentPage}} / {{totalPage}}": "位置 {{currentPage}} / {{totalPage}}",
"Logged in": "已登入",
"Logged in as {{userDisplayName}}": "{{userDisplayName}} 已登入",
"Margins (px)": "頁邊距 (px)",
"Match Case": "匹配大小寫",
"Match Diacritics": "匹配重音符號",
"Match Whole Words": "匹配整個單詞",
@@ -67,14 +75,14 @@
"Notebook": "筆記本",
"Notes": "筆記",
"Open": "打開",
"Override Publisher Font": "覆蓋內置字體",
"Page": "頁",
"Original Text": "原文",
"Page": "頁",
"Paging Animation": "翻頁動畫",
"Paragraph": "段落",
"Parallel Read": "並排閱讀",
"Published:": "出版日期",
"Publisher:": "出版商",
"Reading progress synced": "閱讀進度已同步",
"Reading Progress Synced": "閱讀進度已同步",
"Reload Page": "重新載入頁面",
"Reveal in File Explorer": "在檔案管理器中顯示",
"Reveal in Finder": "在訪達中顯示",
@@ -82,25 +90,113 @@
"Sans-Serif Font": "無襯線字體",
"Save": "保存",
"Scrolled Mode": "滾動模式",
"Search books...": "搜索書籍...",
"Search": "搜索",
"Search Books...": "搜索書籍...",
"Search...": "搜索...",
"Select books": "選擇書籍",
"Select multiple books": "選擇多本書籍",
"Select Book": "選擇書籍",
"Select Books": "選擇書籍",
"Select Multiple Books": "選擇多本書籍",
"Sepia": "舊韻",
"Serif Font": "襯線字體",
"Show Book Details": "顯示書籍詳情",
"Sidebar": "側邊欄",
"Sign In": "登入",
"Sign Out": "登出",
"Sky": "天青",
"Slow": "慢",
"Solarized": "日暉",
"Speak": "朗讀",
"Subjects:": "主題",
"System Fonts": "系統字體",
"TTS not supported in this device": "此設備不支持 TTS",
"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.": "書庫空空如也,您可以導入您的書籍並隨時閱讀。",
"Your Library": "書庫"
"Wikipedia": "維基百科",
"Writing Mode": "排版模式",
"Your Library": "書庫",
"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.": "無法獲取翻譯,請稍後再試。",
"Update Now!": "立即更新!",
"Update": "更新",
"Check Update": "檢查更新",
"Already the latest version": "已經是最新版本",
"Book Details": "書籍詳情",
"From Local File": "從本地文件導入",
"TOC": "目錄",
"Book uploaded: {{title}}": "書籍已上傳:{{title}}",
"Failed to upload book: {{title}}": "書籍上傳失敗:{{title}}",
"Book downloaded: {{title}}": "書籍已下載:{{title}}",
"Failed to download book: {{title}}": "書籍下載失敗:{{title}}",
"Upload Book": "上傳書籍",
"Auto Upload Books to Cloud": "自動上傳書籍到雲端",
"{{percentage}}% of Cloud Storage Used.": "已使用 {{percentage}}% 雲存儲空間",
"Storage": "存儲空間",
"Book deleted: {{title}}": "書籍已刪除:{{title}}",
"Failed to delete book: {{title}}": "書籍刪除失敗:{{title}}",
"Check Updates on Start": "啟動時檢查更新",
"Insufficient storage quota": "雲存儲空間不足",
"Font Weight": "字重",
"Line Spacing": "行間距",
"Word Spacing": "字間距",
"Letter Spacing": "字間距",
"Text Indent": "首行縮進",
"Paragraph Margin": "段間距",
"Override Book Layout": "覆蓋版面佈局",
"Add to Group": "添加到組",
"Untitled Group": "無標題組",
"Group Books": "分組書籍",
"Remove From Group": "從組中移除",
"Create New Group": "創建新組",
"Deselect Book": "取消選擇書籍",
"Download Book": "下載書籍",
"Deselect Group": "取消選擇組",
"Select Group": "選擇組",
"Keep Screen Awake": "保持屏幕常亮",
"Email address": "電子郵件地址",
"Your Password": "您的密碼",
"Your email address": "您的電子郵件地址",
"Your password": "您的密碼",
"Sign in": "登入",
"Signing in...": "正在登入...",
"Sign in with {{provider}}": "使用 {{provider}} 登入",
"Already have an account? Sign in": "已經有帳號?登入",
"Create a Password": "創建密碼",
"Sign up": "註冊",
"Signing up...": "正在註冊...",
"Dont have an account? Sign up": "沒有帳號?註冊",
"Check your email for the confirmation link": "檢查您的電子郵件以獲取確認連結",
"Signing in ...": "正在登入...",
"Send a magic link email": "發送魔法連結郵件",
"Check your email for the magic link": "檢查您的電子郵件以獲取魔法連結",
"Send reset password instructions": "發送重設密碼指示",
"Sending reset instructions ...": "正在發送重設密碼指示...",
"Forgot your password?": "忘記密碼?",
"Check your email for the password reset link": "檢查您的電子郵件以獲取密碼重設連結",
"New Password": "新密碼",
"Your new password": "您的新密碼",
"Update password": "更新密碼",
"Updating password ...": "正在更新密碼...",
"Your password has been updated": "您的密碼已更新",
"Phone number": "電話號碼",
"Your phone number": "您的電話號碼",
"Token": "令牌",
"Your OTP token": "您的 OTP 令牌",
"Verify token": "驗證令牌",
"Sign in with Google": "使用 Google 登入",
"Sign in with Apple": "使用 Apple 登入",
"Sign in with GitHub": "使用 GitHub 登入"
}
+26
View File
@@ -0,0 +1,26 @@
{
"name": "Readest",
"short_name": "Readest",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"description": "Readest is an open-source eBook reader supporting EPUB, PDF, and sync across devices.",
"icons": [
{
"src": "/icon.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/icon.png",
"type": "image/png",
"sizes": "256x256"
},
{
"src": "/icon.png",
"type": "image/png",
"sizes": "512x512"
}
],
"splash_pages": null
}
+114 -1
View File
@@ -1,5 +1,118 @@
{
"releases": {
"0.9.15": {
"date": "2025-02-21",
"notes": [
"Fix hardcoded font color from calibre",
"Fix scroll to current TOC item when toggling sidebar",
"Various fixes and enhancements on iOS platform"
]
},
"0.9.13": {
"date": "2025-02-19",
"notes": [
"Add option to keep screen awake when reading",
"Add shortcuts to notetaking UI",
"UX enhancements of grid view in mobile platforms"
]
},
"0.9.11": {
"date": "2025-02-14",
"notes": [
"Organize the library with customizable groups/folders",
"Alpha release for Android and iOS platforms",
"Various fixes and enhancements on Android and iOS platforms"
]
},
"0.9.10": {
"date": "2025-02-08",
"notes": [
"Add options to adjust paragraph margin, word spacing, letter spacing, text indent and font weight",
"Alpha release for Android and iOS platforms"
]
},
"0.9.9": {
"date": "2025-02-06",
"notes": [
"Load system fonts list at runtime on desktop",
"Incrementally load new books in synchronization",
"Change voice language dynamically by detecting text language",
"Various fixes and enhancements on sync, fonts and highlighting"
]
},
"0.9.8": {
"date": "2025-02-01",
"notes": [
"Support cloud sync for book files",
"Use larger font size on mobile devices for better reading experience",
"Various fixes and enhancements on sync, rendering and settings"
]
},
"0.9.7": {
"date": "2025-01-23",
"notes": [
"Support PWA for the web version",
"Swipe up to toggle header/footer",
"Various fixes and enhancements on updater, header bar and tts"
]
},
"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": [
"Various TTS related UX enhancements",
"Add more system fonts as custom fonts",
"Show reading progress in bookshelf"
]
},
"0.9.3": {
"date": "2025-01-11",
"notes": [
"Hotfix occasionally current speak not stopped when changing voice or rate",
"Inherit tts rate when changing tts engine"
]
},
"0.9.2": {
"date": "2025-01-10",
"notes": [
"Support Text-to-Speech with Edge TTS and Web Speech API.",
"Various fixes and enhancements."
]
},
"0.9.1": {
"date": "2025-01-06",
"notes": [
"Restore window position and size when reopening app.",
"Swap font when downloading online fonts.",
"Fix layout issues on Windows and Linux."
]
},
"0.9.0": {
"date": "2025-01-04",
"notes": [
"Better Custom CSS editor and OAuth process.",
"Add vertical/horizontal switch for CJK books.",
"Fix dropdown close overlay not working on Windows/Linux."
]
},
"0.8.9": {
"date": "2025-01-02",
"notes": [
"Context menu on bookshelf for quick actions.",
"Add more system fonts as custom fonts.",
"Fix google and github login for native apps."
]
},
"0.8.8": {
"date": "2024-12-29",
"notes": [
"Use readest binary name for better CLI interface.",
"Some CSS tweaks to better support dark mode and embedded fonts."
]
},
"0.8.7": {
"date": "2024-12-27",
"notes": [
@@ -37,7 +150,7 @@
"notes": [
"Support file associations for one-click open with Readest.",
"Support APP auto updater.",
"Suppport online web version."
"Support online web version."
]
},
"0.7.9": {
@@ -0,0 +1,6 @@
pnpm tauri ios build --export-method app-store-connect
BUNDLE_DIR=src-tauri/gen/apple/build/arm64
IPA_BUNDLE=$BUNDLE_DIR/Readest.ipa
xcrun altool --upload-app --type ios --file $IPA_BUNDLE --apiKey $APPLE_API_KEY --apiIssuer $APPLE_API_ISSUER
@@ -1,6 +1,6 @@
pnpm run build-macos-universial-appstore
BUNDLE_DIR=src-tauri/target/universal-apple-darwin/release/bundle/macos
BUNDLE_DIR=../../target/universal-apple-darwin/release/bundle/macos
APP_BUNDLE=$BUNDLE_DIR/Readest.app
INSTALLER_BUNDLE=$BUNDLE_DIR/Readest.pkg
+16 -2
View File
@@ -1,12 +1,12 @@
[package]
name = "Readest"
version = "0.2.0"
version = "0.2.1"
description = "Your online library"
authors = ["Bilingify LLC"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.71"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -21,6 +21,15 @@ tauri-build = { version = "2.0.3", features = [] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
thiserror = "2"
tokio = { version = "1", features = ["fs"] }
tokio-util = { version = "0.7", features = ["codec"] }
futures-util = "0.3"
read-progress-stream = "1.0.0"
reqwest = { version = "0.12", default-features = false, features = [
"json",
"stream",
] }
# FIXME: remove the devtools feature in production
tauri = { version = "2.1.1", features = [ "protocol-asset", "devtools"] }
tauri-build = "2.0.3"
@@ -34,6 +43,8 @@ tauri-plugin-shell = "2"
tauri-plugin-process = "2"
tauri-plugin-oauth = "2"
tauri-plugin-opener = "2.2.2"
tauri-plugin-deep-link = "2"
tauri-plugin-sign-in-with-apple = "1.0.2"
[patch.crates-io]
tauri = { path = "../../../packages/tauri/crates/tauri" }
@@ -44,4 +55,7 @@ rand = "0.8"
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
tauri-plugin-cli = "2"
tauri-plugin-single-instance = "2.2.0"
tauri-plugin-updater = "2"
tauri-plugin-window-state = "2.2.0"
font-enumeration = "0.9.0"
@@ -44,6 +44,9 @@
"allow": [
{
"url": "https://*.deepl.com"
},
{
"url": "https://*.cloudflarestorage.com"
}
]
},
@@ -51,17 +54,22 @@
"os:default",
"core:window:default",
"core:window:allow-close",
"core:window:allow-center",
"core:window:allow-minimize",
"core:window:allow-maximize",
"core:window:allow-set-size",
"core:window:allow-set-focus",
"core:window:allow-is-maximized",
"core:window:allow-start-dragging",
"core:window:allow-toggle-maximize",
"shell:default",
"updater:default",
"process:default",
"process:allow-exit",
"process:allow-restart",
"cli:default",
"oauth:allow-start",
"oauth:allow-cancel",
"opener:default"
"sign-in-with-apple:default",
"opener:default",
"deep-link:default"
]
}
@@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"windows": ["main"],
"platforms": ["linux", "macOS", "windows"],
"permissions": [
"updater:default",
"cli:default"
]
}
@@ -0,0 +1,80 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rust")
}
val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
android {
compileSdk = 34
namespace = "com.bilingify.readest"
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "com.bilingify.readest"
minSdk = 26
targetSdk = 34
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
signingConfigs {
create("release") {
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["password"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["password"] as String
}
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
signingConfig = signingConfigs.getByName("release")
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
}
rust {
rootDirRel = "../../../"
}
dependencies {
implementation("androidx.webkit:webkit:1.6.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.activity:activity-ktx:1.9.3")
implementation("com.google.android.material:material:1.8.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
apply(from = "tauri.build.gradle.kts")
@@ -0,0 +1,33 @@
package com.bilingify.readest
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowInsets
import android.view.WindowInsetsController
import androidx.activity.enableEdgeToEdge
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
hideSystemUI()
}
private fun hideSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.decorView.windowInsetsController?.let { controller ->
controller.hide(WindowInsets.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
} else {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
)
}
}
}
@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.readest" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>
</style>
</resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

+110 -29
View File
@@ -9,33 +9,34 @@ extern crate objc;
#[cfg(target_os = "macos")]
mod menu;
#[cfg(target_os = "macos")]
mod tauri_traffic_light_positioner_plugin;
mod traffic_light_plugin;
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use std::path::PathBuf;
use tauri::{command, Window};
use tauri::{AppHandle, Emitter, Manager, Url};
use tauri::{WebviewUrl, WebviewWindowBuilder};
use tauri_plugin_dialog;
use tauri_plugin_fs::FsExt;
use tauri_plugin_oauth::start;
#[cfg(desktop)]
use tauri::Listener;
use std::path::PathBuf;
#[cfg(desktop)]
use tauri::{AppHandle, Listener, Manager, Url};
#[cfg(desktop)]
use tauri_plugin_fs::FsExt;
mod transfer_file;
use tauri::{command, Emitter, WebviewUrl, WebviewWindowBuilder, Window};
use tauri_plugin_oauth::start;
use transfer_file::{download_file, upload_file};
#[cfg(desktop)]
fn allow_file_in_scopes(app: &AppHandle, files: Vec<PathBuf>) {
let fs_scope = app.fs_scope();
let asset_protocol_scope = app.asset_protocol_scope();
for file in &files {
if let Err(e) = fs_scope.allow_file(&file) {
if let Err(e) = fs_scope.allow_file(file) {
eprintln!("Failed to allow file in fs_scope: {}", e);
} else {
println!("Allowed file in fs_scope: {:?}", file);
}
if let Err(e) = asset_protocol_scope.allow_file(&file) {
if let Err(e) = asset_protocol_scope.allow_file(file) {
eprintln!("Failed to allow file in asset_protocol_scope: {}", e);
} else {
println!("Allowed file in asset_protocol_scope: {:?}", file);
@@ -70,13 +71,36 @@ async fn start_server(window: Window) -> Result<u16, String> {
.map_err(|err| err.to_string())
}
#[cfg(desktop)]
#[command]
async fn list_fonts() -> Result<Vec<String>, String> {
let font_collection = font_enumeration::Collection::new().unwrap();
let mut fonts = Vec::new();
for font in font_collection.all() {
fonts.push(font.family_name.clone());
}
Ok(fonts)
}
#[derive(Clone, serde::Serialize)]
#[allow(dead_code)]
struct Payload {
args: Vec<String>,
cwd: String,
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let builder = tauri::Builder::default()
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_oauth::init())
.invoke_handler(tauri::generate_handler![start_server])
.invoke_handler(tauri::generate_handler![
start_server,
download_file,
upload_file,
#[cfg(desktop)]
list_fonts
])
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_http::init())
@@ -84,8 +108,29 @@ pub fn run() {
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init());
#[cfg(desktop)]
let builder = builder.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
let _ = app
.get_webview_window("main")
.expect("no main window")
.set_focus();
app.emit("single-instance", Payload { args: argv, cwd })
.unwrap();
}));
let builder = builder.plugin(tauri_plugin_deep_link::init());
#[cfg(desktop)]
let builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
#[cfg(desktop)]
let builder = builder.plugin(tauri_plugin_window_state::Builder::default().build());
#[cfg(target_os = "macos")]
let builder = builder.plugin(tauri_traffic_light_positioner_plugin::init());
let builder = builder.plugin(traffic_light_plugin::init());
#[cfg(target_os = "ios")]
let builder = builder.plugin(tauri_plugin_sign_in_with_apple::init());
builder
.setup(|#[allow(unused_variables)] app| {
@@ -94,7 +139,7 @@ pub fn run() {
let mut files = Vec::new();
// NOTICE: `args` may include URL protocol (`your-app-protocol://`)
// or arguments (`--`) if your app supports them.
// files may aslo be passed as `file://path/to/file`
// files may also be passed as `file://path/to/file`
for maybe_file in std::env::args().skip(1) {
// skip flags like -f or --flag
if maybe_file.starts_with("-") {
@@ -120,8 +165,25 @@ pub fn run() {
});
}
}
#[cfg(desktop)]
app.handle().plugin(tauri_plugin_cli::init())?;
{
app.handle().plugin(tauri_plugin_cli::init())?;
let app_handle = app.handle().clone();
app.listen("window-ready", move |_| {
app_handle.get_webview_window("main").unwrap()
.eval("window.__READEST_CLI_ACCESS = true; window.__READEST_UPDATER_ACCESS = true;")
.expect("Failed to set cli access config");
});
}
#[cfg(any(windows, target_os = "linux"))]
{
use tauri_plugin_deep_link::DeepLinkExt;
app.deep_link().register_all()?;
}
#[cfg(desktop)]
if cfg!(debug_assertions) {
app.handle().plugin(
@@ -130,10 +192,13 @@ pub fn run() {
.build(),
)?;
}
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default());
#[cfg(desktop)]
let win_builder = win_builder
.inner_size(800.0, 600.0)
.resizable(true)
.maximized(true);
.resizable(true);
#[cfg(target_os = "macos")]
let win_builder = win_builder
@@ -141,20 +206,36 @@ pub fn run() {
.title_bar_style(TitleBarStyle::Overlay)
.title("");
#[cfg(not(target_os = "macos"))]
let win_builder = win_builder
.decorations(false)
.transparent(true)
.title("Readest");
#[cfg(all(not(target_os = "macos"), desktop))]
let win_builder = {
let mut win_builder = win_builder
.decorations(false)
.transparent(true)
.visible(false)
.title("Readest");
if cfg!(target_os = "windows") {
let version = tauri_plugin_os::version();
if version <= tauri_plugin_os::Version::from_string("10.0.19045") {
win_builder = win_builder.shadow(false);
} else {
win_builder = win_builder.shadow(true);
}
} else {
win_builder = win_builder.shadow(true);
}
win_builder
};
win_builder.build().unwrap();
// let win = win_builder.build().unwrap();
// win.open_devtools();
#[cfg(target_os = "macos")]
menu::setup_macos_menu(&app.handle())?;
menu::setup_macos_menu(app.handle())?;
app.handle().emit("window-ready", {}).unwrap();
app.handle().emit("window-ready", ()).unwrap();
Ok(())
})
@@ -163,7 +244,7 @@ pub fn run() {
.run(
#[allow(unused_variables)]
|app_handle, event| {
#[cfg(any(target_os = "macos", target_os = "ios"))]
#[cfg(target_os = "macos")]
if let tauri::RunEvent::Opened { urls } = event {
let files = urls
.into_iter()
@@ -171,7 +252,7 @@ pub fn run() {
.collect::<Vec<_>>();
let app_handler_clone = app_handle.clone();
allow_file_in_scopes(&app_handle, files.clone());
allow_file_in_scopes(app_handle, files.clone());
app_handle.listen("window-ready", move |_| {
println!("Window is ready, proceeding to handle files.");
set_window_open_with_files(&app_handler_clone, files.clone());
@@ -25,7 +25,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
setup_traffic_light_positioner(window);
}
});
return;
})
.build()
}
@@ -118,7 +117,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
}
extern "C" fn on_window_did_resize<R: Runtime>(this: &Object, _cmd: Sel, notification: id) {
unsafe {
with_window_state(&*this, |state: &mut WindowState<R>| {
with_window_state(this, |state: &mut WindowState<R>| {
let id = state
.window
.ns_window()
@@ -216,7 +215,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
notification: id,
) {
unsafe {
with_window_state(&*this, |state: &mut WindowState<R>| {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("did-enter-fullscreen", ())
@@ -233,7 +232,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
notification: id,
) {
unsafe {
with_window_state(&*this, |state: &mut WindowState<R>| {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("will-enter-fullscreen", ())
@@ -250,7 +249,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
notification: id,
) {
unsafe {
with_window_state(&*this, |state: &mut WindowState<R>| {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("did-exit-fullscreen", ())
@@ -274,7 +273,7 @@ pub fn setup_traffic_light_positioner<R: Runtime>(window: Window<R>) {
notification: id,
) {
unsafe {
with_window_state(&*this, |state: &mut WindowState<R>| {
with_window_state(this, |state: &mut WindowState<R>| {
state
.window
.emit("will-exit-fullscreen", ())
@@ -0,0 +1,203 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! Upload files from disk to a remote server over HTTP.
//!
//! Download files from a remote HTTP server to disk.
use futures_util::TryStreamExt;
use serde::{ser::Serializer, Serialize};
use tauri::{command, ipc::Channel};
use tokio::{
fs::File,
io::{AsyncWriteExt, BufWriter},
};
use tokio_util::codec::{BytesCodec, FramedRead};
use read_progress_stream::ReadProgressStream;
use std::collections::HashMap;
use std::time::Instant;
type Result<T> = std::result::Result<T, Error>;
// The TransferStats struct tracks both transfer speed and cumulative transfer progress.
pub struct TransferStats {
accumulated_chunk_len: usize, // Total length of chunks transferred in the current period
accumulated_time: u128, // Total time taken for the transfers in the current period
pub transfer_speed: u64, // Calculated transfer speed in bytes per second
pub total_transferred: u64, // Cumulative total of all transferred data
start_time: Instant, // Time when the current period started
granularity: u32, // Time period (in milliseconds) over which the transfer speed is calculated
}
impl TransferStats {
// Initializes a new TransferStats instance with the specified granularity.
pub fn start(granularity: u32) -> Self {
Self {
accumulated_chunk_len: 0,
accumulated_time: 0,
transfer_speed: 0,
total_transferred: 0,
start_time: Instant::now(),
granularity,
}
}
// Records the transfer of a data chunk and updates both transfer speed and total progress.
pub fn record_chunk_transfer(&mut self, chunk_len: usize) {
let now = Instant::now();
let it_took = now.duration_since(self.start_time).as_millis();
self.accumulated_chunk_len += chunk_len;
self.total_transferred += chunk_len as u64;
self.accumulated_time += it_took;
// Calculate transfer speed if accumulated time exceeds granularity.
if self.accumulated_time >= self.granularity as u128 {
self.transfer_speed =
(self.accumulated_chunk_len as u128 / self.accumulated_time * 1024) as u64;
self.accumulated_chunk_len = 0;
self.accumulated_time = 0;
}
// Reset the start time for the next period.
self.start_time = now;
}
}
// Provides a default implementation for TransferStats with a granularity of 500 milliseconds.
impl Default for TransferStats {
fn default() -> Self {
Self::start(500) // Default granularity is 500 ms
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Request(#[from] reqwest::Error),
#[error("{0}")]
ContentLength(String),
#[error("request failed with status code {0}: {1}")]
HttpErrorCode(u16, String),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProgressPayload {
progress: u64,
total: u64,
transfer_speed: u64,
}
#[command]
pub async fn download_file(
url: &str,
file_path: &str,
headers: HashMap<String, String>,
body: Option<String>,
on_progress: Channel<ProgressPayload>,
) -> Result<()> {
let client = reqwest::Client::new();
let mut request = if let Some(body) = body {
client.post(url).body(body)
} else {
client.get(url)
};
// Loop trought the headers keys and values
// and add them to the request object.
for (key, value) in headers {
request = request.header(&key, value);
}
let response = request.send().await?;
if !response.status().is_success() {
return Err(Error::HttpErrorCode(
response.status().as_u16(),
response.text().await.unwrap_or_default(),
));
}
let total = response.content_length().unwrap_or(0);
let mut file = BufWriter::new(File::create(file_path).await?);
let mut stream = response.bytes_stream();
let mut stats = TransferStats::default();
while let Some(chunk) = stream.try_next().await? {
file.write_all(&chunk).await?;
stats.record_chunk_transfer(chunk.len());
let _ = on_progress.send(ProgressPayload {
progress: stats.total_transferred,
total,
transfer_speed: stats.transfer_speed,
});
}
file.flush().await?;
Ok(())
}
#[command]
pub async fn upload_file(
url: &str,
file_path: &str,
method: &str,
headers: HashMap<String, String>,
on_progress: Channel<ProgressPayload>,
) -> Result<String> {
let file = File::open(file_path).await?;
let file_len = file.metadata().await.unwrap().len();
let client = reqwest::Client::new();
let mut request = match method.to_uppercase().as_str() {
"POST" => client.post(url),
"PUT" => client.put(url),
_ => return Err(Error::ContentLength("Invalid HTTP method".into())),
};
request = request
.header(reqwest::header::CONTENT_LENGTH, file_len)
.body(file_to_body(on_progress.clone(), file, file_len));
for (key, value) in headers {
request = request.header(&key, value);
}
let response = request.send().await?;
if response.status().is_success() {
response.text().await.map_err(Into::into)
} else {
Err(Error::HttpErrorCode(
response.status().as_u16(),
response.text().await.unwrap_or_default(),
))
}
}
fn file_to_body(channel: Channel<ProgressPayload>, file: File, file_len: u64) -> reqwest::Body {
let stream = FramedRead::new(file, BytesCodec::new()).map_ok(|r| r.freeze());
let mut stats = TransferStats::default();
reqwest::Body::wrap_stream(ReadProgressStream::new(
stream,
Box::new(move |progress_chunk, _progress_total| {
stats.record_chunk_transfer(progress_chunk as usize);
let _ = channel.send(ProgressPayload {
progress: stats.total_transferred,
total: file_len,
transfer_speed: stats.transfer_speed,
});
}),
))
}
+19 -5
View File
@@ -1,8 +1,9 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Readest",
"version": "../package.json",
"mainBinaryName": "readest",
"identifier": "com.bilingify.readest",
"version": "../package.json",
"build": {
"frontendDist": "../out",
"devUrl": "http://localhost:3000",
@@ -13,10 +14,11 @@
"windows": [],
"security": {
"csp": {
"default-src": "'self' 'unsafe-inline' blob: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost https://fonts.gstatic.com https://db.onlinewebfonts.com",
"connect-src": "'self' blob: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com",
"default-src": "'self' 'unsafe-inline' blob: data: customprotocol: asset: http://asset.localhost ipc: http://ipc.localhost",
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost https://*.sentry.io https://*.posthog.com https://*.deepl.com https://*.wikipedia.org https://*.wiktionary.org https://*.supabase.co https://*.readest.com wss://speech.platform.bing.com https://*.cloudflarestorage.com",
"img-src": "'self' blob: data: asset: http://asset.localhost https://*",
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost",
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com",
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://fonts.gstatic.com https://db.onlinewebfonts.com https://cdn.jsdelivr.net",
"frame-src": "'self' blob: asset: http://asset.localhost",
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com"
},
@@ -54,6 +56,12 @@
"section": "text"
}
},
"android": {
"minSdkVersion": 26
},
"iOS": {
"developmentTeam": "J5W48D69VR"
},
"fileAssociations": [
{
"name": "epub",
@@ -129,8 +137,14 @@
}
]
},
"deep-link": {
"mobile": [{ "host": "web.readest.com" }],
"desktop": {
"schemes": ["readest"]
}
},
"updater": {
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU0OTAxMURGQkUzQjFENTQKUldSVUhUdSszeEdRNUExdmFkWnlvYWNYNG5wamkxMmUxRk5SejlMOTJVd28yNXlVTDh6Wld4OC8K",
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEJFMEQ1QjE2OEU1NEIzNTEKUldSUnMxU09GbHNOdmpEaWFMT1crRFpEV2VORzQ2MklxaFc0M1R0ci9xY2c1bENXS0xhM1R1L2sK",
"endpoints": ["https://github.com/readest/readest/releases/latest/download/latest.json"]
}
}
+180 -49
View File
@@ -7,7 +7,6 @@ import { Auth } from '@supabase/auth-ui-react';
import { ThemeSupa } from '@supabase/auth-ui-shared';
import { FcGoogle } from 'react-icons/fc';
import { FaApple } from 'react-icons/fa';
import { VscAzure } from 'react-icons/vsc';
import { FaGithub } from 'react-icons/fa';
import { IoArrowBack } from 'react-icons/io5';
@@ -16,13 +15,21 @@ import { supabase } from '@/utils/supabase';
import { useTheme } from '@/hooks/useTheme';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { isTauriAppPlatform } from '@/services/environment';
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oauth';
import { openUrl } from '@tauri-apps/plugin-opener';
import { handleAuthCallback } from '@/helpers/auth';
import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
type OAuthProvider = 'google' | 'apple' | 'azure' | 'github';
interface SingleInstancePayload {
args: string[];
cwd: string;
}
interface ProviderLoginProp {
provider: OAuthProvider;
handleSignIn: (provider: OAuthProvider) => void;
@@ -30,6 +37,9 @@ interface ProviderLoginProp {
label: string;
}
const WEB_AUTH_CALLBACK = 'https://web.readest.com/auth/callback';
const DEEPLINK_CALLBACK = 'readest://auth/callback';
const ProviderLogin: React.FC<ProviderLoginProp> = ({ provider, handleSignIn, Icon, label }) => {
return (
<button
@@ -39,23 +49,52 @@ const ProviderLogin: React.FC<ProviderLoginProp> = ({ provider, handleSignIn, Ic
'bg-base-100 border-gray-300 shadow-sm transition hover:bg-gray-50',
)}
>
<Icon size={20} />
<Icon />
<span className='text-neutral-content/70 px-2 text-sm'>{label}</span>
</button>
);
};
export default function AuthPage() {
const _ = useTranslation();
const router = useRouter();
const { login } = useAuth();
const { envConfig } = useEnv();
const { envConfig, appService } = useEnv();
const { isDarkMode } = useTheme();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [port, setPort] = useState<number | null>(null);
const isOAuthServerRunning = useRef(false);
const [isMounted, setIsMounted] = useState(false);
const signIn = async (provider: OAuthProvider) => {
const getTauriRedirectTo = () => {
return process.env.NODE_ENV === 'production' || appService?.isMobile
? appService?.isMobile
? WEB_AUTH_CALLBACK
: DEEPLINK_CALLBACK
: `http://localhost:${port}`; // only for development env on Desktop
};
const tauriSignInApple = async () => {
if (!supabase) {
throw new Error('No backend connected');
}
supabase.auth.signOut();
const request = {
scope: ['fullName', 'email'] as Scope[],
};
const appleAuthResponse = await getAppleIdAuth(request);
if (appleAuthResponse.identityToken) {
const { error } = await supabase.auth.signInWithIdToken({
provider: 'apple',
token: appleAuthResponse.identityToken,
});
if (error) {
console.error('Authentication error:', error);
}
}
};
const tauriSignIn = async (provider: OAuthProvider) => {
if (!supabase) {
throw new Error('No backend connected');
}
@@ -64,7 +103,7 @@ export default function AuthPage() {
provider,
options: {
skipBrowserRedirect: true,
redirectTo: `http://localhost:${port}`,
redirectTo: getTauriRedirectTo(),
},
});
@@ -72,37 +111,57 @@ export default function AuthPage() {
console.error('Authentication error:', error);
return;
}
openUrl(data.url);
await openUrl(data.url);
};
const startOAuthServer = async () => {
const handleOAuthUrl = async (url: string) => {
console.log('Received OAuth URL:', url);
const hashMatch = url.match(/#(.*)/);
if (hashMatch) {
const hash = hashMatch[1];
const params = new URLSearchParams(hash);
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const next = params.get('next') ?? '/';
if (accessToken) {
handleAuthCallback({ accessToken, refreshToken, next, login, navigate: router.push });
}
}
};
const startTauriOAuth = async () => {
try {
const port = await start();
setPort(port);
console.log(`OAuth server started on port ${port}`);
if (process.env.NODE_ENV === 'production' || appService?.isMobile) {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
currentWindow.listen('single-instance', ({ event, payload }) => {
console.log('Received deep link:', event, payload);
const { args } = payload as SingleInstancePayload;
if (args?.[1]) {
handleOAuthUrl(args[1]);
}
});
await onOpenUrl((urls) => {
urls.forEach((url) => {
handleOAuthUrl(url);
});
});
} else {
const port = await start();
setPort(port);
console.log(`OAuth server started on port ${port}`);
await onUrl((url) => {
console.log('Received OAuth URL:', url);
const hashMatch = url.match(/#(.*)/);
if (hashMatch) {
const hash = hashMatch[1];
const params = new URLSearchParams(hash);
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const next = params.get('next') ?? '/';
handleAuthCallback({ accessToken, refreshToken, next, login, navigate: router.push });
}
});
await onInvalidUrl((url) => {
console.log('Received invalid OAuth URL:', url);
});
await onUrl(handleOAuthUrl);
await onInvalidUrl((url) => {
console.log('Received invalid OAuth URL:', url);
});
}
} catch (error) {
console.error('Error starting OAuth server:', error);
}
};
const stopOAuthServer = async () => {
const stopTauriOAuth = async () => {
try {
if (port) {
await cancel(port);
@@ -121,15 +180,77 @@ export default function AuthPage() {
router.back();
};
const getAuthLocalization = () => {
return {
variables: {
sign_in: {
email_label: _('Email address'),
password_label: _('Your Password'),
email_input_placeholder: _('Your email address'),
password_input_placeholder: _('Your password'),
button_label: _('Sign in'),
loading_button_label: _('Signing in...'),
social_provider_text: _('Sign in with {{provider}}'),
link_text: _('Already have an account? Sign in'),
},
sign_up: {
email_label: _('Email address'),
password_label: _('Create a Password'),
email_input_placeholder: _('Your email address'),
password_input_placeholder: _('Your password'),
button_label: _('Sign up'),
loading_button_label: _('Signing up...'),
social_provider_text: _('Sign in with {{provider}}'),
link_text: _('Dont have an account? Sign up'),
confirmation_text: _('Check your email for the confirmation link'),
},
magic_link: {
email_input_label: _('Email address'),
email_input_placeholder: _('Your email address'),
button_label: _('Sign in'),
loading_button_label: _('Signing in ...'),
link_text: _('Send a magic link email'),
confirmation_text: _('Check your email for the magic link'),
},
forgotten_password: {
email_label: _('Email address'),
password_label: _('Your Password'),
email_input_placeholder: _('Your email address'),
button_label: _('Send reset password instructions'),
loading_button_label: _('Sending reset instructions ...'),
link_text: _('Forgot your password?'),
confirmation_text: _('Check your email for the password reset link'),
},
update_password: {
password_label: _('New Password'),
password_input_placeholder: _('Your new password'),
button_label: _('Update password'),
loading_button_label: _('Updating password ...'),
confirmation_text: _('Your password has been updated'),
},
verify_otp: {
email_input_label: _('Email address'),
email_input_placeholder: _('Your email address'),
phone_input_label: _('Phone number'),
phone_input_placeholder: _('Your phone number'),
token_input_label: _('Token'),
token_input_placeholder: _('Your OTP token'),
button_label: _('Verify token'),
loading_button_label: _('Signing in ...'),
},
},
};
};
useEffect(() => {
if (!isTauriAppPlatform()) return;
if (isOAuthServerRunning.current) return;
isOAuthServerRunning.current = true;
startOAuthServer();
startTauriOAuth();
return () => {
isOAuthServerRunning.current = false;
stopOAuthServer();
stopTauriOAuth();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -138,7 +259,8 @@ export default function AuthPage() {
const { data: subscription } = supabase.auth.onAuthStateChange((_event, session) => {
if (session?.access_token && session.user) {
login(session.access_token, session.user);
router.push('/library');
const redirectTo = new URLSearchParams(window.location.search).get('redirect');
router.push(redirectTo ?? '/library');
}
});
@@ -156,38 +278,45 @@ export default function AuthPage() {
return null;
}
// For tauri app development, use a custom OAuth server to handle the OAuth callback
// For tauri app production, use deeplink to handle the OAuth callback
// For web app, use the built-in OAuth callback page /auth/callback
return isTauriAppPlatform() ? (
<div className='flex pt-11'>
<div
className={clsx(
'mt-6 flex',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
appService?.hasTrafficLight && 'pt-11',
)}
>
<button
onClick={handleGoBack}
className='btn btn-ghost fixed left-3 top-11 h-8 min-h-8 w-8 p-0'
className={clsx(
'btn btn-ghost fixed left-4 h-8 min-h-8 w-8 p-0',
appService?.hasSafeAreaInset && 'top-[calc(env(safe-area-inset-top)+16px)]',
appService?.hasTrafficLight && 'top-11',
)}
>
<IoArrowBack size={20} />
<IoArrowBack className='text-base-content' />
</button>
<div style={{ maxWidth: '420px', margin: 'auto', padding: '2rem' }}>
<ProviderLogin
provider='google'
handleSignIn={signIn}
handleSignIn={tauriSignIn}
Icon={FcGoogle}
label='Sign in with Google'
label={_('Sign in with Google')}
/>
<ProviderLogin
provider='apple'
handleSignIn={signIn}
handleSignIn={appService?.isIOSApp ? tauriSignInApple : tauriSignIn}
Icon={FaApple}
label='Sign in with Apple'
/>
<ProviderLogin
provider='azure'
handleSignIn={signIn}
Icon={VscAzure}
label='Sign in with Azure'
label={_('Sign in with Apple')}
/>
<ProviderLogin
provider='github'
handleSignIn={signIn}
handleSignIn={tauriSignIn}
Icon={FaGithub}
label='Sign in with GitHub'
label={_('Sign in with GitHub')}
/>
<hr className='my-3 mt-6 w-64 border-t border-gray-200' />
<Auth
@@ -196,7 +325,8 @@ export default function AuthPage() {
theme={isDarkMode ? 'dark' : 'light'}
magicLink={true}
providers={[]}
redirectTo={`http://localhost:${port}`}
redirectTo={getTauriRedirectTo()}
localization={getAuthLocalization()}
/>
</div>
</div>
@@ -204,17 +334,18 @@ export default function AuthPage() {
<div style={{ maxWidth: '420px', margin: 'auto', padding: '2rem', paddingTop: '4rem' }}>
<button
onClick={handleGoBack}
className='btn btn-ghost fixed left-10 top-6 h-8 min-h-8 w-8 p-0'
className='btn btn-ghost fixed left-6 top-6 h-8 min-h-8 w-8 p-0'
>
<IoArrowBack size={20} />
<IoArrowBack className='text-base-content' />
</button>
<Auth
supabaseClient={supabase}
appearance={{ theme: ThemeSupa }}
theme={isDarkMode ? 'dark' : 'light'}
magicLink={true}
providers={['google', 'apple', 'azure', 'github']}
providers={['google', 'apple', 'github']}
redirectTo='/auth/callback'
localization={getAuthLocalization()}
/>
</div>
);
@@ -0,0 +1,34 @@
import { invoke } from '@tauri-apps/api/core';
export type Scope = 'fullName' | 'email';
export interface AppleIDAuthorizationRequest {
scope: Scope[];
nonce?: string;
state?: string;
}
export interface AppleIDAuthorizationResponse {
// usually not null
userIdentifier: string | null;
givenName: string | null;
familyName: string | null;
email: string | null;
authorizationCode: string;
identityToken: string | null;
state: string | null;
}
export async function getAppleIdAuth(
request: AppleIDAuthorizationRequest,
): Promise<AppleIDAuthorizationResponse> {
const result = await invoke<AppleIDAuthorizationResponse>(
'plugin:sign-in-with-apple|get_apple_id_credential',
{
payload: request,
},
);
return result;
}
+34 -1
View File
@@ -13,16 +13,49 @@ const description =
'Perfect for deep reading, analysis, and understanding. Explore now!';
const previewImage = 'https://cdn.readest.com/images/open_graph_preview_read_now.png';
export const metadata = {
title,
description,
generator: 'Next.js',
manifest: '/manifest.json',
keywords: ['epub', 'pdf', 'ebook', 'reader', 'readest', 'pwa'],
authors: [
{
name: 'readest',
url: 'https://github.com/readest/readest',
},
],
icons: [
{ rel: 'apple-touch-icon', url: '/apple-touch-icon.png' },
{ rel: 'icon', url: '/icon.png' },
],
};
export const viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
viewportFit: 'cover',
themeColor: 'white',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<title>{title}</title>
<meta
name='viewport'
content='minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover'
/>
<meta name='mobile-web-app-capable' content='yes' />
<meta name='apple-mobile-web-app-capable' content='yes' />
<meta name='apple-mobile-web-app-status-bar-style' content='default' />
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1' />
<meta name='apple-mobile-web-app-title' content='Readest' />
<link rel='apple-touch-icon' sizes='180x180' href='/apple-touch-icon.png' />
<link rel='icon' href='/favicon.ico' />
<link rel='manifest' href='/manifest.json' />
<meta name='description' content={description} />
<meta property='og:url' content={url} />
<meta property='og:type' content='website' />
@@ -0,0 +1,57 @@
import clsx from 'clsx';
import Image from 'next/image';
import { Book } from '@/types/book';
import { formatAuthors, formatTitle } from '@/utils/book';
interface BookCoverProps {
book: Book;
isPreview?: boolean;
}
const BookCover: React.FC<BookCoverProps> = ({ book, isPreview }) => {
return (
<div className='relative flex h-full w-full'>
<Image
src={book.coverImageUrl!}
alt={book.title}
fill={true}
className='object-cover'
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).nextElementSibling?.classList.remove('invisible');
}}
/>
<div
className={clsx(
'invisible absolute inset-0 rounded-none p-2',
'text-neutral-content text-center font-serif font-medium',
isPreview && 'bg-base-200/50',
)}
>
<div className='flex h-1/2 items-center justify-center'>
<span
className={clsx(
isPreview ? 'line-clamp-2' : 'line-clamp-3',
isPreview ? 'text-[0.5em]' : 'text-lg',
)}
>
{formatTitle(book.title)}
</span>
</div>
<div className='h-1/6'></div>
<div className='flex h-1/3 items-center justify-center'>
<span
className={clsx(
'text-neutral-content/50 line-clamp-1',
isPreview ? 'text-[0.4em]' : 'text-base',
)}
>
{formatAuthors(book.author)}
</span>
</div>
</div>
</div>
);
};
export default BookCover;
@@ -0,0 +1,126 @@
import clsx from 'clsx';
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
import { CiCircleMore } from 'react-icons/ci';
import { LiaCloudUploadAltSolid, LiaCloudDownloadAltSolid } from 'react-icons/lia';
import { Book } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import ReadingProgress from './ReadingProgress';
import BookCover from './BookCover';
interface BookItemProps {
book: Book;
isSelectMode: boolean;
selectedBooks: string[];
transferProgress: number | null;
handleBookUpload: (book: Book) => void;
handleBookDownload: (book: Book) => void;
showBookDetailsModal: (book: Book) => void;
}
const BookItem: React.FC<BookItemProps> = ({
book,
isSelectMode,
selectedBooks,
transferProgress,
handleBookUpload,
handleBookDownload,
showBookDetailsModal,
}) => {
const iconSize15 = useResponsiveSize(15);
const { appService } = useEnv();
const stopEvent = (e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault();
e.stopPropagation();
};
return (
<div
className={clsx(
'book-item flex h-full flex-col',
appService?.hasContextMenu ? 'cursor-pointer' : '',
)}
>
<div className='bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden shadow-md'>
<BookCover book={book} />
{selectedBooks.includes(book.hash) && (
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
)}
{isSelectMode && (
<div className='absolute bottom-1 right-1'>
{selectedBooks.includes(book.hash) ? (
<MdCheckCircle className='fill-blue-500' />
) : (
<MdCheckCircleOutline className='fill-gray-300 drop-shadow-sm' />
)}
</div>
)}
</div>
<div className={clsx('flex w-full flex-col p-0 pt-2')}>
<div className='min-w-0 flex-1'>
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-[0.6em] text-xs font-semibold'>
{book.title}
</h4>
</div>
<div
className={clsx('flex items-center', book.progress ? 'justify-between' : 'justify-end')}
>
{book.progress && <ReadingProgress book={book} />}
<div className='flex items-center gap-x-1'>
{transferProgress !== null ? (
transferProgress === 100 ? null : (
<div
className='radial-progress opacity-0 group-hover:opacity-100'
style={
{
'--value': transferProgress,
'--size': `${iconSize15}px`,
'--thickness': '2px',
} as React.CSSProperties
}
role='progressbar'
></div>
)
) : (
<button
className='show-detail-button opacity-0 group-hover:opacity-100'
onPointerDown={(e) => stopEvent(e)}
onPointerUp={(e) => stopEvent(e)}
onPointerMove={(e) => stopEvent(e)}
onPointerCancel={(e) => stopEvent(e)}
onPointerLeave={(e) => stopEvent(e)}
onClick={() => {
if (!book.uploadedAt) {
handleBookUpload(book);
} else if (!book.downloadedAt) {
handleBookDownload(book);
}
}}
>
{!book.uploadedAt && <LiaCloudUploadAltSolid size={iconSize15} />}
{book.uploadedAt && !book.downloadedAt && (
<LiaCloudDownloadAltSolid size={iconSize15} />
)}
</button>
)}
<button
className='show-detail-button opacity-0 group-hover:opacity-100'
onPointerDown={(e) => stopEvent(e)}
onPointerUp={(e) => stopEvent(e)}
onPointerMove={(e) => stopEvent(e)}
onPointerCancel={(e) => stopEvent(e)}
onPointerLeave={(e) => stopEvent(e)}
onClick={() => showBookDetailsModal(book)}
>
<CiCircleMore size={iconSize15} />
</button>
</div>
</div>
</div>
</div>
);
};
export default BookItem;
@@ -1,92 +1,68 @@
import clsx from 'clsx';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import { PiPlus } from 'react-icons/pi';
import { MdDelete, MdOpenInNew } from 'react-icons/md';
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
import { CiCircleMore } from 'react-icons/ci';
import { useRouter, useSearchParams } from 'next/navigation';
import Image from 'next/image';
import { Menu, MenuItem } from '@tauri-apps/api/menu';
import { revealItemInDir } from '@tauri-apps/plugin-opener';
import { useEffect, useRef, useState } from 'react';
import { MdDelete, MdOpenInNew, MdOutlineCancel } from 'react-icons/md';
import { LuFolderPlus } from 'react-icons/lu';
import { PiPlus } from 'react-icons/pi';
import { Book, BooksGroup } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { navigateToReader } from '@/utils/nav';
import { getOSPlatform } from '@/utils/misc';
import { getFilename } from '@/utils/book';
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
import { navigateToLibrary, navigateToReader } from '@/utils/nav';
import { isMd5 } from '@/utils/md5';
import Alert from '@/components/Alert';
import Spinner from '@/components/Spinner';
import { isTauriAppPlatform } from '@/services/environment';
import BookDetailModal from '@/components/BookDetailModal';
type BookshelfItem = Book | BooksGroup;
const UNGROUPED_NAME = 'ungrouped';
const generateBookshelfItems = (books: Book[]): BookshelfItem[] => {
const groups: BooksGroup[] = books.reduce((acc: BooksGroup[], book: Book) => {
book.group = book.group || UNGROUPED_NAME;
const groupIndex = acc.findIndex((group) => group.name === book.group);
const booksGroup = acc[acc.findIndex((group) => group.name === book.group)];
if (booksGroup) {
booksGroup.books.push(book);
booksGroup.updatedAt = Math.max(acc[groupIndex]!.updatedAt, book.updatedAt);
} else {
acc.push({
name: book.group,
books: [book],
updatedAt: book.updatedAt,
});
}
return acc;
}, []);
const ungroupedBooks: Book[] = groups.find((group) => group.name === UNGROUPED_NAME)?.books || [];
const groupedBooks: BooksGroup[] = groups.filter((group) => group.name !== UNGROUPED_NAME);
return [...ungroupedBooks, ...groupedBooks].sort((a, b) => b.updatedAt - a.updatedAt);
};
import BookshelfItem, { generateBookshelfItems } from './BookshelfItem';
import GroupingModal from './GroupingModal';
interface BookshelfProps {
libraryBooks: Book[];
isSelectMode: boolean;
onImportBooks: () => void;
handleImportBooks: () => void;
handleBookUpload: (book: Book) => void;
handleBookDownload: (book: Book) => void;
handleBookDelete: (book: Book) => void;
handleSetSelectMode: (selectMode: boolean) => void;
handleShowDetailsBook: (book: Book) => void;
booksTransferProgress: { [key: string]: number | null };
}
const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImportBooks }) => {
const Bookshelf: React.FC<BookshelfProps> = ({
libraryBooks,
isSelectMode,
handleImportBooks,
handleBookUpload,
handleBookDownload,
handleBookDelete,
handleSetSelectMode,
handleShowDetailsBook,
booksTransferProgress,
}) => {
const _ = useTranslation();
const router = useRouter();
const searchParams = useSearchParams();
const { envConfig, appService } = useEnv();
const { settings } = useSettingsStore();
const { deleteBook } = useLibraryStore();
const { appService } = useEnv();
const [loading, setLoading] = useState(false);
const [selectedBooks, setSelectedBooks] = useState<string[]>([]);
const [showSelectModeActions, setShowSelectModeActions] = useState(false);
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
const [clickedImage, setClickedImage] = useState<string | null>(null);
const [showGroupingModal, setShowGroupingModal] = useState(false);
const [navBooksGroup, setNavBooksGroup] = useState<BooksGroup | null>(null);
const [importBookUrl] = useState(searchParams?.get('url') || '');
const isImportingBook = useRef(false);
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedBook, setSelectedBook] = useState<Book | null>(null);
const showMoreDetails = (book: Book) => {
setIsModalOpen(true);
setSelectedBook(book);
};
const closeModal = () => {
setIsModalOpen(false);
};
const { setLibrary } = useLibraryStore();
const allBookshelfItems = generateBookshelfItems(libraryBooks);
useEffect(() => {
setSelectedBooks([]);
if (isSelectMode) {
setShowSelectModeActions(true);
} else {
setSelectedBooks([]);
setShowSelectModeActions(false);
}
}, [isSelectMode]);
useEffect(() => {
@@ -108,22 +84,27 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [importBookUrl, appService]);
const bookshelfItems = generateBookshelfItems(libraryBooks);
const handleBookClick = (id: string) => {
if (isSelectMode) {
toggleSelection(id);
useEffect(() => {
const group = searchParams?.get('group') || '';
if (group) {
const booksGroup = allBookshelfItems.find(
(item) => 'name' in item && item.id === group,
) as BooksGroup;
if (booksGroup) {
setNavBooksGroup(booksGroup);
} else {
navigateToLibrary(router);
}
} else {
setClickedImage(id);
setTimeout(() => setClickedImage(null), 300);
setTimeout(() => setLoading(true), 200);
navigateToReader(router, [id]);
setNavBooksGroup(null);
navigateToLibrary(router);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams, libraryBooks, showGroupingModal]);
const toggleSelection = (id: string) => {
setSelectedBooks((prev) =>
prev.includes(id) ? prev.filter((bookId) => bookId !== id) : [...prev, id],
prev.includes(id) ? prev.filter((selectedId) => selectedId !== id) : [...prev, id],
);
};
@@ -132,177 +113,158 @@ const Bookshelf: React.FC<BookshelfProps> = ({ libraryBooks, isSelectMode, onImp
navigateToReader(router, selectedBooks);
};
const confirmDelete = () => {
for (const selectedBook of selectedBooks) {
const book = libraryBooks.find((b) => b.hash === selectedBook);
if (book) {
deleteBook(envConfig, book);
const confirmDelete = async () => {
selectedBooks.forEach((id) => {
for (const book of libraryBooks.filter((book) => book.hash === id || book.groupId === id)) {
if (book && !book.deletedAt) {
handleBookDelete(book);
}
}
}
});
setSelectedBooks([]);
setShowDeleteAlert(false);
setShowSelectModeActions(true);
};
const deleteSelectedBooks = () => {
setShowSelectModeActions(false);
setShowDeleteAlert(true);
};
const bookContextMenuHandler = async (book: Book, e: React.MouseEvent) => {
if (!isTauriAppPlatform()) return;
e.preventDefault();
e.stopPropagation();
const osPlatform = getOSPlatform();
const fileRevealLabel =
FILE_REVEAL_LABELS[osPlatform as FILE_REVEAL_PLATFORMS] || FILE_REVEAL_LABELS.default;
const showBookInFinderMenuItem = await MenuItem.new({
text: _(fileRevealLabel),
action: async () => {
const folder = `${settings.localBooksDir}/${getFilename(book)}`;
revealItemInDir(folder);
},
});
const deleteBookMenuItem = await MenuItem.new({
text: _('Delete'),
action: async () => {
deleteBook(envConfig, book);
},
});
const menu = await Menu.new();
menu.append(showBookInFinderMenuItem);
menu.append(deleteBookMenuItem);
menu.popup();
const groupSelectedBooks = () => {
setShowSelectModeActions(false);
setShowGroupingModal(true);
};
const currentBookshelfItems = navBooksGroup ? navBooksGroup.books : allBookshelfItems;
return (
<div className='bookshelf'>
<div className='grid grid-cols-3 gap-0 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8'>
{bookshelfItems.map((item, index) => (
<div
<div
className={clsx(
'transform-wrapper grid flex-1 gap-x-4 sm:gap-x-0',
'grid-cols-3 sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8',
)}
>
{currentBookshelfItems.map((item, index) => (
<BookshelfItem
key={`library-item-${index}`}
className='hover:bg-base-300/50 flex h-full flex-col p-4'
>
<div className='flex-grow'>
{'format' in item ? (
<div
className='book-item cursor-pointer'
onContextMenu={bookContextMenuHandler.bind(null, item as Book)}
>
<div
key={(item as Book).hash}
className='bg-base-100 shadow-md'
onClick={() => handleBookClick(item.hash)}
>
<div className='relative aspect-[28/41]'>
<Image
src={item.coverImageUrl!}
alt={item.title}
fill={true}
className='object-cover'
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
(e.target as HTMLImageElement).nextElementSibling?.classList.remove(
'invisible',
);
}}
/>
<div
className={clsx(
'invisible absolute inset-0 flex items-center justify-center p-1',
'text-neutral-content rounded-none text-center font-serif text-base font-medium',
)}
>
{item.title}
</div>
{(selectedBooks.includes(item.hash) || clickedImage === item.hash) && (
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
)}
{isSelectMode && (
<div className='absolute bottom-1 right-1'>
{selectedBooks.includes(item.hash) ? (
<MdCheckCircle size={20} className='fill-blue-500' />
) : (
<MdCheckCircleOutline
size={20}
className='fill-gray-300 drop-shadow-sm'
/>
)}
</div>
)}
</div>
</div>
<div className='card-body flex flex-row items-center justify-between p-0 pt-2'>
<h4 className='card-title line-clamp-1 text-[0.6em] text-xs font-semibold'>
{(item as Book).title}
</h4>
<div
className='card-detail'
role='button'
onClick={showMoreDetails.bind(null, item as Book)}
>
<CiCircleMore size={15} />
</div>
</div>
</div>
) : (
<div>
{(item as BooksGroup).books.map((book) => (
<div key={book.hash} className='card bg-base-100 w-full shadow-md'>
<figure>
<Image
width={10}
height={10}
src={book.coverImageUrl!}
alt={book.title || ''}
className='h-48 w-full object-cover'
/>
</figure>
</div>
))}
<h2 className='mb-2 text-lg font-bold'>{(item as BooksGroup).name}</h2>
</div>
)}
</div>
</div>
item={item}
isSelectMode={isSelectMode}
selectedBooks={selectedBooks}
setLoading={setLoading}
toggleSelection={toggleSelection}
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
handleBookDelete={handleBookDelete}
handleSetSelectMode={handleSetSelectMode}
handleShowDetailsBook={handleShowDetailsBook}
transferProgress={
'hash' in item ? booksTransferProgress[(item as Book).hash] || null : null
}
/>
))}
{bookshelfItems.length > 0 && (
{!navBooksGroup && allBookshelfItems.length > 0 && (
<div
className='border-1 bg-base-100 hover:bg-base-300/50 m-4 flex aspect-[28/41] items-center justify-center'
className={clsx(
'border-1 bg-base-100 hover:bg-base-300/50 flex items-center justify-center',
'mx-0 my-4 aspect-[28/41] sm:mx-4',
)}
role='button'
onClick={onImportBooks}
onClick={handleImportBooks}
>
<PiPlus className='size-10' color='gray' />
</div>
)}
</div>
{selectedBooks.length > 0 && (
<div className='text-base-content bg-base-300 fixed bottom-4 left-1/2 flex -translate-x-1/2 transform space-x-4 rounded-lg p-4 shadow-lg'>
<button onClick={openSelectedBooks} className='flex items-center space-x-2'>
<MdOpenInNew />
<span>{_('Open')}</span>
</button>
<button onClick={deleteSelectedBooks} className='flex items-center space-x-2'>
<MdDelete className='fill-red-500' />
<span className='text-red-500'>{_('Delete')}</span>
</button>
</div>
)}
{loading && (
<div className='fixed inset-0 z-50 flex items-center justify-center'>
<Spinner loading />
</div>
)}
{showDeleteAlert && (
<Alert
title={_('Confirm Deletion')}
message='Are you sure to delete the selected books?'
onClickCancel={() => setShowDeleteAlert(false)}
onClickConfirm={confirmDelete}
/>
<div className='fixed bottom-0 left-0 right-0 z-40 pb-[calc(env(safe-area-inset-bottom)+16px)]'>
{isSelectMode && showSelectModeActions && (
<div
className={clsx(
'flex items-center justify-center shadow-lg',
'text-base-content bg-base-300 text-sm',
'mx-auto w-fit space-x-6 rounded-lg p-4',
)}
>
<button
onClick={openSelectedBooks}
className={clsx(
'flex flex-col items-center justify-center',
(!selectedBooks.length || !selectedBooks.every((id) => isMd5(id))) &&
'btn-disabled opacity-50',
)}
>
<MdOpenInNew />
<div>{_('Open')}</div>
</button>
<button
onClick={groupSelectedBooks}
className={clsx(
'flex flex-col items-center justify-center',
!selectedBooks.length && 'btn-disabled opacity-50',
)}
>
<LuFolderPlus />
<div>{_('Add to Group')}</div>
</button>
<button
onClick={deleteSelectedBooks}
className={clsx(
'flex flex-col items-center justify-center',
!selectedBooks.length && 'btn-disabled opacity-50',
)}
>
<MdDelete className='fill-red-500' />
<div className='text-red-500'>{_('Delete')}</div>
</button>
<button
onClick={() => handleSetSelectMode(false)}
className={clsx('flex flex-col items-center justify-center')}
>
<MdOutlineCancel />
<div>{_('Cancel')}</div>
</button>
</div>
)}
</div>
{showGroupingModal && (
<div>
<GroupingModal
libraryBooks={libraryBooks}
selectedBooks={selectedBooks}
onCancel={() => {
setShowGroupingModal(false);
setShowSelectModeActions(true);
}}
onConfirm={() => {
setShowGroupingModal(false);
handleSetSelectMode(false);
}}
/>
</div>
)}
{selectedBook && (
<BookDetailModal isOpen={isModalOpen} book={selectedBook} onClose={closeModal} />
{showDeleteAlert && (
<div
className={clsx(
'fixed bottom-0 left-0 right-0 z-50 flex justify-center',
'pb-[calc(env(safe-area-inset-bottom)+16px)]',
)}
>
<Alert
title={_('Confirm Deletion')}
message={_('Are you sure to delete the selected books?')}
onCancel={() => {
setShowDeleteAlert(false);
setShowSelectModeActions(true);
}}
onConfirm={confirmDelete}
/>
</div>
)}
</div>
);
@@ -0,0 +1,278 @@
import clsx from 'clsx';
import { useRouter } from 'next/navigation';
import { navigateToLibrary, navigateToReader } from '@/utils/nav';
import { useEnv } from '@/context/EnvContext';
import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useLongPress } from '@/hooks/useLongPress';
import { Menu, MenuItem } from '@tauri-apps/api/menu';
import { revealItemInDir } from '@tauri-apps/plugin-opener';
import { getOSPlatform } from '@/utils/misc';
import { getFilename } from '@/utils/book';
import { BOOK_UNGROUPED_ID, BOOK_UNGROUPED_NAME } from '@/services/constants';
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
import { Book, BookGroupType, BooksGroup } from '@/types/book';
import BookItem from './BookItem';
import GroupItem from './GroupItem';
export type BookshelfItem = Book | BooksGroup;
export const generateBookshelfItems = (books: Book[]): (Book | BooksGroup)[] => {
const groups: BooksGroup[] = books.reduce((acc: BooksGroup[], book: Book) => {
if (book.deletedAt) return acc;
book.groupId = book.groupId || BOOK_UNGROUPED_ID;
book.groupName = book.groupName || BOOK_UNGROUPED_NAME;
const groupIndex = acc.findIndex((group) => group.id === book.groupId);
const booksGroup = acc[acc.findIndex((group) => group.id === book.groupId)];
if (booksGroup) {
booksGroup.books.push(book);
booksGroup.updatedAt = Math.max(acc[groupIndex]!.updatedAt, book.updatedAt);
} else {
acc.push({
id: book.groupId,
name: book.groupName,
books: [book],
updatedAt: book.updatedAt,
});
}
return acc;
}, []);
groups.forEach((group) => {
group.books.sort((a, b) => b.updatedAt - a.updatedAt);
});
const ungroupedBooks: Book[] =
groups.find((group) => group.name === BOOK_UNGROUPED_NAME)?.books || [];
const groupedBooks: BooksGroup[] = groups.filter((group) => group.name !== BOOK_UNGROUPED_NAME);
return [...ungroupedBooks, ...groupedBooks].sort((a, b) => b.updatedAt - a.updatedAt);
};
export const generateGroupsList = (items: Book[]): BookGroupType[] => {
return items
.sort((a, b) => b.updatedAt - a.updatedAt)
.reduce((acc: BookGroupType[], item: Book) => {
if (item.deletedAt) return acc;
if (
item.groupId &&
item.groupName &&
item.groupId !== BOOK_UNGROUPED_ID &&
item.groupName !== BOOK_UNGROUPED_NAME &&
!acc.find((group) => group.id === item.groupId)
) {
acc.push({ id: item.groupId, name: item.groupName });
}
return acc;
}, []) as BookGroupType[];
};
interface BookshelfItemProps {
item: BookshelfItem;
isSelectMode: boolean;
selectedBooks: string[];
transferProgress: number | null;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
toggleSelection: (hash: string) => void;
handleBookUpload: (book: Book) => void;
handleBookDownload: (book: Book) => void;
handleBookDelete: (book: Book) => void;
handleSetSelectMode: (selectMode: boolean) => void;
handleShowDetailsBook: (book: Book) => void;
}
const BookshelfItem: React.FC<BookshelfItemProps> = ({
item,
isSelectMode,
selectedBooks,
transferProgress,
setLoading,
toggleSelection,
handleBookUpload,
handleBookDownload,
handleBookDelete,
handleSetSelectMode,
handleShowDetailsBook,
}) => {
const _ = useTranslation();
const router = useRouter();
const { envConfig, appService } = useEnv();
const { settings } = useSettingsStore();
const { updateBook } = useLibraryStore();
const showBookDetailsModal = async (book: Book) => {
if (await makeBookAvailable(book)) {
handleShowDetailsBook(book);
}
};
const makeBookAvailable = async (book: Book) => {
if (book.uploadedAt && !book.downloadedAt) {
let available = false;
const loadingTimeout = setTimeout(() => setLoading(true), 200);
try {
await handleBookDownload(book);
updateBook(envConfig, book);
available = true;
} finally {
if (loadingTimeout) clearTimeout(loadingTimeout);
setLoading(false);
return available;
}
}
return true;
};
const handleBookClick = async (book: Book) => {
if (isSelectMode) {
toggleSelection(book.hash);
} else {
if (!(await makeBookAvailable(book))) return;
navigateToReader(router, [book.hash]);
}
};
const handleGroupClick = (group: BooksGroup) => {
if (isSelectMode) {
toggleSelection(group.id);
} else {
navigateToLibrary(router, `group=${group.id}`);
}
};
const bookContextMenuHandler = async (book: Book) => {
if (!appService?.hasContextMenu) return;
const osPlatform = getOSPlatform();
const fileRevealLabel =
FILE_REVEAL_LABELS[osPlatform as FILE_REVEAL_PLATFORMS] || FILE_REVEAL_LABELS.default;
const selectBookMenuItem = await MenuItem.new({
text: selectedBooks.includes(book.hash) ? _('Deselect Book') : _('Select Book'),
action: async () => {
if (!isSelectMode) handleSetSelectMode(true);
toggleSelection(book.hash);
},
});
const showBookInFinderMenuItem = await MenuItem.new({
text: _(fileRevealLabel),
action: async () => {
const folder = `${settings.localBooksDir}/${getFilename(book)}`;
revealItemInDir(folder);
},
});
const showBookDetailsMenuItem = await MenuItem.new({
text: _('Show Book Details'),
action: async () => {
showBookDetailsModal(book);
},
});
const downloadBookMenuItem = await MenuItem.new({
text: _('Download Book'),
action: async () => {
handleBookDownload(book);
},
});
const uploadBookMenuItem = await MenuItem.new({
text: _('Upload Book'),
action: async () => {
handleBookUpload(book);
},
});
const deleteBookMenuItem = await MenuItem.new({
text: _('Delete'),
action: async () => {
await handleBookDelete(book);
},
});
const menu = await Menu.new();
menu.append(selectBookMenuItem);
menu.append(showBookDetailsMenuItem);
menu.append(showBookInFinderMenuItem);
if (book.uploadedAt && !book.downloadedAt) {
menu.append(downloadBookMenuItem);
}
if (!book.uploadedAt && book.downloadedAt) {
menu.append(uploadBookMenuItem);
}
menu.append(deleteBookMenuItem);
menu.popup();
};
const groupContextMenuHandler = async (group: BooksGroup) => {
if (!appService?.hasContextMenu) return;
const selectGroupMenuItem = await MenuItem.new({
text: selectedBooks.includes(group.id) ? _('Deselect Group') : _('Select Group'),
action: async () => {
if (!isSelectMode) handleSetSelectMode(true);
toggleSelection(group.id);
},
});
const deleteGroupMenuItem = await MenuItem.new({
text: _('Delete'),
action: async () => {
for (const book of group.books) {
await handleBookDelete(book);
}
},
});
const menu = await Menu.new();
menu.append(selectGroupMenuItem);
menu.append(deleteGroupMenuItem);
menu.popup();
};
const { pressing, handlers } = useLongPress({
onLongPress: () => {
if (!isSelectMode) {
handleSetSelectMode(true);
}
if ('format' in item) {
toggleSelection((item as Book).hash);
} else {
toggleSelection((item as BooksGroup).id);
}
},
onTap: () => {
if ('format' in item) {
handleBookClick(item as Book);
} else {
handleGroupClick(item as BooksGroup);
}
},
onContextMenu: () => {
if ('format' in item) {
bookContextMenuHandler(item as Book);
} else {
groupContextMenuHandler(item as BooksGroup);
}
},
});
return (
<div
className={clsx(
'sm:hover:bg-base-300/50 group flex h-full flex-col px-0 py-4 sm:px-4',
pressing ? 'scale-95' : 'scale-100',
)}
style={{
transition: 'transform 0.2s',
}}
{...handlers}
>
<div className='flex-grow'>
{'format' in item ? (
<BookItem
book={item}
isSelectMode={isSelectMode}
selectedBooks={selectedBooks}
transferProgress={transferProgress}
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
showBookDetailsModal={showBookDetailsModal}
/>
) : (
<GroupItem group={item} isSelectMode={isSelectMode} selectedBooks={selectedBooks} />
)}
</div>
</div>
);
};
export default BookshelfItem;
@@ -0,0 +1,52 @@
import clsx from 'clsx';
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { BooksGroup } from '@/types/book';
import BookCover from './BookCover';
interface GroupItemProps {
group: BooksGroup;
isSelectMode: boolean;
selectedBooks: string[];
}
const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, selectedBooks }) => {
const { appService } = useEnv();
return (
<div
className={clsx(
'group-item flex h-full flex-col',
appService?.hasContextMenu ? 'cursor-pointer' : '',
)}
>
<div className='bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden p-2 shadow-md'>
<div className='grid w-full grid-cols-2 grid-rows-2 gap-1 overflow-hidden'>
{group.books.slice(0, 4).map((book) => (
<div key={book.hash} className='relative aspect-[28/41] h-full w-full'>
<BookCover book={book} isPreview />
</div>
))}
</div>
{selectedBooks.includes(group.id) && (
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
)}
{isSelectMode && (
<div className='absolute bottom-1 right-1'>
{selectedBooks.includes(group.id) ? (
<MdCheckCircle className='fill-blue-500' />
) : (
<MdCheckCircleOutline className='fill-gray-300 drop-shadow-sm' />
)}
</div>
)}
</div>
<div className='min-w-0 flex-1 pt-2'>
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-xs font-semibold'>
{group.name}
</h4>
</div>
</div>
);
};
export default GroupItem;
@@ -0,0 +1,247 @@
import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import { MdCheck } from 'react-icons/md';
import { HiOutlineFolder, HiOutlineFolderAdd, HiOutlineFolderRemove } from 'react-icons/hi';
import { Book, BookGroupType } from '@/types/book';
import { isMd5, md5Fingerprint } from '@/utils/md5';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { useLibraryStore } from '@/store/libraryStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { BOOK_UNGROUPED_ID, BOOK_UNGROUPED_NAME } from '@/services/constants';
import { generateGroupsList } from './BookshelfItem';
interface GroupingModalProps {
libraryBooks: Book[];
selectedBooks: string[];
onCancel: () => void;
onConfirm: () => void;
}
const GroupingModal: React.FC<GroupingModalProps> = ({
libraryBooks,
selectedBooks,
onCancel,
onConfirm,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const { setLibrary } = useLibraryStore();
const groupsList = generateGroupsList(libraryBooks);
const [showInput, setShowInput] = useState(false);
const [editGroupName, setEditGroupName] = useState(_('Untitled Group'));
const [selectedGroup, setSelectedGroup] = useState<BookGroupType | null>(null);
const [newGroups, setNewGroups] = useState<BookGroupType[]>([]);
const [allGroups, setAllGroups] = useState<BookGroupType[]>(groupsList);
const editorRef = useRef<HTMLInputElement>(null);
const iconSize = useResponsiveSize(16);
const isSelectedBooksHasGroup =
selectedBooks.some((hash) => !isMd5(hash)) ||
selectedBooks
.map((hash) => libraryBooks.find((book) => book.hash === hash)?.groupId)
.some((group) => group && group !== BOOK_UNGROUPED_NAME);
const handleCreateGroup = () => {
setShowInput(true);
};
const handleRemoveFromGroup = () => {
selectedBooks.forEach((id) => {
for (const book of libraryBooks.filter((book) => book.hash === id || book.groupId === id)) {
if (
book &&
book.groupId &&
book.groupName &&
book.groupId !== BOOK_UNGROUPED_ID &&
book.groupName !== BOOK_UNGROUPED_NAME
) {
book.groupId = undefined;
book.groupName = undefined;
book.updatedAt = Date.now();
}
}
});
setLibrary(libraryBooks);
appService?.saveLibraryBooks(libraryBooks);
onConfirm();
};
const handleConfirmCreateGroup = () => {
const groupName = editGroupName.trim();
if (groupName) {
const newGroup = { id: md5Fingerprint(groupName), name: groupName };
const existingGroupIndex = newGroups.findIndex((group) => group.name === groupName);
if (existingGroupIndex > -1) {
newGroups.splice(existingGroupIndex, 1);
}
newGroups.push(newGroup);
setSelectedGroup(newGroup);
setNewGroups(newGroups);
for (const newGroup of newGroups) {
const existingGroupIndex = groupsList.findIndex((group) => group.id === newGroup.id);
if (existingGroupIndex > -1) {
groupsList.splice(existingGroupIndex, 1);
}
groupsList.unshift(newGroup);
}
setAllGroups(groupsList);
const untitledGroupPattern = new RegExp(`^${_('Untitled Group')}\\s*(\\d+)?$`);
const untitledGroupNumbers = groupsList
.map((group) => {
const match = group.name.match(untitledGroupPattern);
return match ? parseInt(match[1] || '0', 10) : null;
})
.filter((num) => num !== null);
const nextNumber =
untitledGroupNumbers.length > 0 ? Math.max(...untitledGroupNumbers) + 1 : 1;
setEditGroupName(`${_('Untitled Group')} ${nextNumber}`);
setShowInput(false);
}
};
const handleToggleSelectGroup = (group: BookGroupType) => {
setSelectedGroup((prevGroup) => (prevGroup?.id === group.id ? null : group));
};
const handleConfirmGrouping = () => {
selectedBooks.forEach((id) => {
for (const book of libraryBooks.filter((book) => book.hash === id || book.groupId === id)) {
if (book && selectedGroup) {
book.groupId = selectedGroup.id;
book.groupName = selectedGroup.name;
book.updatedAt = Date.now();
}
}
});
setLibrary(libraryBooks);
appService?.saveLibraryBooks(libraryBooks);
onConfirm();
};
useEffect(() => {
if (editorRef.current) {
editorRef.current.select();
}
}, [showInput]);
useEffect(() => {
const groupIds = selectedBooks
.map((id) => libraryBooks.find((book) => book.hash === id || book.groupId === id)?.groupId)
.filter((groupId) => groupId);
if (Array.from(new Set(groupIds)).length === 1) {
setSelectedGroup(groupsList.find((group) => group.id === groupIds[0]) || null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedBooks]);
return (
<div className='fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50'>
<div
className={clsx(
'modal-box bg-base-100 overflow-y-auto rounded-2xl shadow-xl',
'max-h-[85%] w-[95%] min-w-64 max-w-[440px] p-6 sm:w-[70%]',
)}
>
<h2 className='text-center text-lg font-bold'>{_('Group Books')}</h2>
<div className={clsx('mt-4 grid grid-cols-1 gap-2 text-base md:grid-cols-2')}>
{isSelectedBooksHasGroup && (
<div
onClick={handleRemoveFromGroup}
role='button'
className='flex items-center space-x-2 p-2 text-blue-500'
>
<HiOutlineFolderRemove size={iconSize} />
<span>{_('Remove From Group')}</span>
</div>
)}
<div
onClick={handleCreateGroup}
role='button'
className='flex items-center space-x-2 p-2 text-blue-500'
>
<HiOutlineFolderAdd size={iconSize} />
<span>{_('Create New Group')}</span>
</div>
</div>
{showInput && (
<div className='mt-4 flex items-center gap-2'>
<input
type='text'
autoFocus
ref={editorRef}
value={editGroupName}
onChange={(e) => setEditGroupName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleConfirmCreateGroup();
e.stopPropagation();
}}
className='input input-ghost w-full border-0 px-2 text-base !outline-none sm:text-sm'
/>
<button
className={clsx(
'btn btn-ghost settings-content hover:bg-transparent',
'flex h-[1.3em] min-h-[1.3em] items-end p-0',
editorRef.current && editorRef.current.value ? '' : 'btn-disabled !bg-opacity-0',
)}
onClick={() => handleConfirmCreateGroup()}
>
<div className='pr-1 align-bottom text-base text-blue-500 sm:text-sm'>
{_('Save')}
</div>
</button>
</div>
)}
<ul className='groups-list mt-4 grid grid-cols-2 gap-2'>
{allGroups.map((group, index) => (
<button
key={index}
className={clsx(
'hover:bg-base-300 text-base-content flex w-full',
'items-center justify-between rounded-md px-2 py-2',
)}
onClick={() => handleToggleSelectGroup(group)}
>
<div className='flex min-w-0 items-center'>
<span style={{ minWidth: `${iconSize}px` }}>
<HiOutlineFolder size={iconSize} />
</span>
<span
className={clsx('mx-2 flex-1 truncate text-base sm:text-sm')}
style={{ minWidth: 0 }}
>
{group.name}
</span>
</div>
<span className='text-neutral-content flex shrink-0 text-sm'>
{selectedGroup && selectedGroup.id == group.id && (
<MdCheck className='fill-blue-500' size={iconSize} />
)}
</span>
</button>
))}
</ul>
<div className='mt-6 flex justify-end gap-x-8 p-2'>
<button onClick={onCancel} className='flex items-center'>
{_('Cancel')}
</button>
<button
onClick={handleConfirmGrouping}
className={clsx(
'flex items-center text-blue-500',
!selectedGroup && 'btn-disabled opacity-50',
)}
>
{_('Confirm')}
</button>
</div>
</div>
</div>
);
};
export default GroupingModal;
@@ -0,0 +1,27 @@
import { useTranslation } from '@/hooks/useTranslation';
import MenuItem from '@/components/MenuItem';
interface ImportMenuProps {
setIsDropdownOpen?: (open: boolean) => void;
onImportBooks: () => void;
}
const ImportMenu: React.FC<ImportMenuProps> = ({ setIsDropdownOpen, onImportBooks }) => {
const _ = useTranslation();
const handleImportBooks = () => {
onImportBooks();
setIsDropdownOpen?.(false);
};
return (
<ul
tabIndex={-1}
className='dropdown-content dropdown-center bg-base-100 menu rounded-box z-[1] mt-3 w-52 p-2 shadow'
>
<MenuItem label={_('From Local File')} onClick={handleImportBooks} />
</ul>
);
};
export default ImportMenu;
@@ -1,16 +1,21 @@
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { FaSearch } from 'react-icons/fa';
import { PiPlus } from 'react-icons/pi';
import { PiSelectionAllDuotone } from 'react-icons/pi';
import { MdOutlineMenu } from 'react-icons/md';
import { MdArrowBackIosNew } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { navigateToLibrary } from '@/utils/nav';
import useTrafficLight from '@/hooks/useTrafficLight';
import WindowButtons from '@/components/WindowButtons';
import Dropdown from '@/components/Dropdown';
import SettingsMenu from './SettingsMenu';
import ImportMenu from './ImportMenu';
interface LibraryHeaderProps {
isSelectMode: boolean;
@@ -24,9 +29,13 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
onToggleSelectMode,
}) => {
const _ = useTranslation();
const router = useRouter();
const searchParams = useSearchParams();
const { appService } = useEnv();
const { isTrafficLightVisible } = useTrafficLight();
const headerRef = useRef<HTMLDivElement>(null);
const iconSize16 = useResponsiveSize(16);
const iconSize20 = useResponsiveSize(20);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -40,54 +49,68 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
};
}, [onToggleSelectMode]);
const windowButtonVisible = appService?.hasWindowBar && !isTrafficLightVisible;
const isInGroupView = !!searchParams?.get('group');
return (
<div
ref={headerRef}
className={clsx(
'titlebar z-10 h-11 w-full py-2 pr-6',
isTrafficLightVisible ? 'pl-16' : 'pl-2',
'titlebar z-10 flex h-11 w-full items-center py-2 pr-4 sm:pr-6',
appService?.hasSafeAreaInset && 'mt-[env(safe-area-inset-top)]',
isTrafficLightVisible ? 'pl-16' : 'pl-0 sm:pl-2',
)}
>
<div className='flex items-center justify-between space-x-6'>
<div className='exclude-title-bar-mousedown sm:w relative flex w-full items-center pl-4'>
<span className='absolute left-8 text-gray-500'>
<FaSearch className='h-4 w-4' />
</span>
<input
type='text'
placeholder={_('Search books...')}
spellCheck='false'
className={clsx(
'input rounded-badge bg-base-300/50 h-7 w-full pl-10 pr-10',
'font-sans text-sm font-light',
'border-none focus:outline-none focus:ring-0',
)}
/>
<div className='absolute right-4 flex items-center space-x-4 text-gray-500'>
<span className='mx-2 h-5 w-[1px] bg-gray-400'></span>
<div className='dropdown dropdown-bottom flex h-5 cursor-pointer justify-center'>
<div className='lg:tooltip lg:tooltip-bottom' data-tip='Add books'>
<PiPlus tabIndex={-1} className='h-5 w-5' />
<div className='flex w-full items-center justify-between space-x-6 sm:space-x-12'>
<div className='exclude-title-bar-mousedown relative flex w-full items-center pl-4'>
{isInGroupView && (
<button
onClick={() => {
navigateToLibrary(router);
}}
className='ml-[-6px] mr-4 flex h-7 min-h-7 w-7 items-center p-0'
>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Go Back')}>
<MdArrowBackIosNew size={iconSize20} />
</div>
<ul
tabIndex={-1}
className='dropdown-content dropdown-center menu rounded-box z-[1] mt-3 w-52 p-2 shadow'
>
<li>
<button className='text-base-content' onClick={onImportBooks}>
{_('From Local File')}
</button>
</li>
</ul>
</div>
</button>
)}
<div className='relative flex h-7 w-full items-center'>
<span className='absolute left-3 text-gray-500'>
<FaSearch className='h-4 w-4' />
</span>
<input
type='text'
placeholder={_('Search Books...')}
spellCheck='false'
className={clsx(
'input rounded-badge bg-base-300/50 h-7 w-full pl-10 pr-10',
'font-sans text-sm font-light',
'border-none focus:outline-none focus:ring-0',
)}
/>
</div>
<div className='absolute right-4 flex items-center space-x-2 text-gray-500 sm:space-x-4'>
<span className='mx-2 h-6 w-[1px] bg-gray-400'></span>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center'
buttonClassName='p-0 h-6 min-h-6 w-6 flex items-center justify-center'
toggleButton={
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Import Books')}>
<PiPlus className='m-0.5 h-5 w-5' />
</div>
}
>
<ImportMenu onImportBooks={onImportBooks} />
</Dropdown>
<button
onClick={onToggleSelectMode}
aria-label={_('Select multiple books')}
aria-label={_('Select Multiple Books')}
className='h-6'
>
<div
className='lg:tooltip lg:tooltip-bottom cursor-pointer'
data-tip={_('Select books')}
data-tip={_('Select Books')}
>
<PiSelectionAllDuotone
role='button'
@@ -97,20 +120,22 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
</button>
</div>
</div>
<div className='flex h-full items-center'>
<div className='flex h-full items-center gap-x-2 sm:gap-x-4'>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end mr-2'
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<MdOutlineMenu size={16} />}
toggleButton={<MdOutlineMenu size={iconSize16} />}
>
<SettingsMenu />
</Dropdown>
<WindowButtons
headerRef={headerRef}
showMinimize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
showMaximize={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
showClose={!isTrafficLightVisible && appService?.appPlatform !== 'web'}
/>
{appService?.hasWindowBar && (
<WindowButtons
headerRef={headerRef}
showMinimize={windowButtonVisible}
showMaximize={windowButtonVisible}
showClose={windowButtonVisible}
/>
)}
</div>
</div>
</div>
@@ -0,0 +1,43 @@
import type React from 'react';
import { memo, useMemo } from 'react';
import type { Book } from '@/types/book';
interface ReadingProgressProps {
book: Book;
}
const getProgressPercentage = (book: Book) => {
if (!book.progress || !book.progress[1]) {
return null;
}
if (book.progress && book.progress[1] === 1) {
return 100;
}
return Math.round((book.progress[0] / book.progress[1]) * 100);
};
const ReadingProgress: React.FC<ReadingProgressProps> = memo(
({ book }) => {
const progressPercentage = useMemo(() => getProgressPercentage(book), [book]);
if (progressPercentage === null) {
return null;
}
return (
<div className='text-neutral-content/70 flex justify-between text-xs'>
<span>{progressPercentage}%</span>
</div>
);
},
(prevProps, nextProps) => {
return (
prevProps.book.hash === nextProps.book.hash &&
prevProps.book.updatedAt === nextProps.book.updatedAt
);
},
);
ReadingProgress.displayName = 'ReadingProgress';
export default ReadingProgress;
@@ -1,17 +1,22 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/navigation';
import { PiUserCircle } from 'react-icons/pi';
import { PiUserCircleCheck } from 'react-icons/pi';
import { MdCheck } from 'react-icons/md';
import { setAboutDialogVisible } from '@/components/AboutWindow';
import { isWebAppPlatform } from '@/services/environment';
import { hasUpdater, isWebAppPlatform } from '@/services/environment';
import { DOWNLOAD_READEST_URL } from '@/services/constants';
import { useAuth } from '@/context/AuthContext';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { getStoragePlanData } from '@/utils/access';
import { navigateToLogin } from '@/utils/nav';
import { QuotaType } from '@/types/user';
import MenuItem from '@/components/MenuItem';
import Quota from '@/components/Quota';
interface BookMenuProps {
setIsDropdownOpen?: (isOpen: boolean) => void;
@@ -21,8 +26,12 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
const _ = useTranslation();
const router = useRouter();
const { envConfig } = useEnv();
const { user, logout } = useAuth();
const { token, user, logout } = useAuth();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [quotas, setQuotas] = React.useState<QuotaType[]>([]);
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
const [isAutoCheckUpdates, setIsAutoCheckUpdates] = useState(settings.autoCheckUpdates);
const [isScreenWakeLock, setIsScreenWakeLock] = useState(settings.screenWakeLock);
const showAboutReadest = () => {
setAboutDialogVisible(true);
@@ -34,7 +43,7 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
};
const handleUserLogin = () => {
router.push('/auth');
navigateToLogin(router);
setIsDropdownOpen?.(false);
};
@@ -46,6 +55,52 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
setIsDropdownOpen?.(false);
};
const handleReloadPage = () => {
window.location.reload();
setIsDropdownOpen?.(false);
};
const toggleAutoUploadBooks = () => {
settings.autoUpload = !settings.autoUpload;
setSettings(settings);
saveSettings(envConfig, settings);
setIsAutoUpload(settings.autoUpload);
if (settings.autoUpload && !user) {
navigateToLogin(router);
}
};
const toggleAutoCheckUpdates = () => {
settings.autoCheckUpdates = !settings.autoCheckUpdates;
setSettings(settings);
saveSettings(envConfig, settings);
setIsAutoCheckUpdates(settings.autoCheckUpdates);
};
const toggleScreenWakeLock = () => {
settings.screenWakeLock = !settings.screenWakeLock;
setSettings(settings);
saveSettings(envConfig, settings);
setIsScreenWakeLock(settings.screenWakeLock);
};
useEffect(() => {
if (!user || !token) return;
const storagPlan = getStoragePlanData(token);
const storageQuota: QuotaType = {
name: _('Storage'),
tooltip: _('{{percentage}}% of Cloud Storage Used.', {
percentage: Math.round((storagPlan.usage / storagPlan.quota) * 100),
}),
used: Math.round(storagPlan.usage / 1024 / 1024),
total: Math.round(storagPlan.quota / 1024 / 1024),
unit: 'MB',
};
setQuotas([storageQuota]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
const isWebApp = isWebAppPlatform();
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
const userFullName = user?.user_metadata?.['full_name'];
@@ -54,7 +109,7 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
return (
<div
tabIndex={0}
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-3 w-60 shadow-2xl'
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-3 w-72 shadow-2xl'
>
{user ? (
<MenuItem
@@ -75,21 +130,37 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
height={20}
/>
) : (
<PiUserCircleCheck size={20} />
<PiUserCircleCheck />
)
}
>
<ul>
<Quota quotas={quotas} />
<MenuItem label={_('Sign Out')} noIcon onClick={handleUserLogout} />
</ul>
</MenuItem>
) : (
<MenuItem
label={_('Sign In')}
icon={<PiUserCircle size={20} />}
onClick={handleUserLogin}
></MenuItem>
<MenuItem label={_('Sign In')} icon={<PiUserCircle />} onClick={handleUserLogin}></MenuItem>
)}
<MenuItem
label={_('Auto Upload Books to Cloud')}
icon={isAutoUpload ? <MdCheck className='text-base-content' /> : undefined}
onClick={toggleAutoUploadBooks}
/>
{hasUpdater() && (
<MenuItem
label={_('Check Updates on Start')}
icon={isAutoCheckUpdates ? <MdCheck className='text-base-content' /> : undefined}
onClick={toggleAutoCheckUpdates}
/>
)}
<MenuItem
label={_('Keep Screen Awake')}
icon={isScreenWakeLock ? <MdCheck className='text-base-content' /> : undefined}
onClick={toggleScreenWakeLock}
/>
<hr className='border-base-200 my-1' />
<MenuItem label={_('Reload Page')} onClick={handleReloadPage} />
<hr className='border-base-200 my-1' />
{isWebApp && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
<MenuItem label={_('About Readest')} onClick={showAboutReadest} />
@@ -0,0 +1,128 @@
import { useEffect, useRef } from 'react';
import { useAuth } from '@/context/AuthContext';
import { useEnv } from '@/context/EnvContext';
import { useSync } from '@/hooks/useSync';
import { useLibraryStore } from '@/store/libraryStore';
import { SYNC_BOOKS_INTERVAL_SEC } from '@/services/constants';
import { Book } from '@/types/book';
export interface UseBooksSyncProps {
onSyncStart?: () => void;
onSyncEnd?: () => void;
}
export const useBooksSync = ({ onSyncStart, onSyncEnd }: UseBooksSyncProps) => {
const { user } = useAuth();
const { appService } = useEnv();
const { library, setLibrary } = useLibraryStore();
const { syncedBooks, syncBooks, lastSyncedAtBooks } = useSync();
const syncBooksPullingRef = useRef(false);
const pullLibrary = async () => {
if (!user) return;
syncBooks([], 'pull');
};
const pushLibrary = async () => {
if (!user) return;
const newBooks = getNewBooks();
syncBooks(newBooks, 'push');
};
useEffect(() => {
if (!user) return;
if (syncBooksPullingRef.current) return;
syncBooksPullingRef.current = true;
pullLibrary();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const lastSyncTime = useRef<number>(0);
const syncTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const getNewBooks = () => {
if (!user) return [];
const newBooks = library.filter(
(book) => lastSyncedAtBooks < book.updatedAt || lastSyncedAtBooks < (book.deletedAt ?? 0),
);
return newBooks;
};
useEffect(() => {
if (!user) return;
const now = Date.now();
const timeSinceLastSync = now - lastSyncTime.current;
if (timeSinceLastSync > SYNC_BOOKS_INTERVAL_SEC * 1000) {
lastSyncTime.current = now;
const newBooks = getNewBooks();
syncBooks(newBooks, 'both');
} else {
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current);
syncTimeoutRef.current = setTimeout(
() => {
lastSyncTime.current = Date.now();
const newBooks = getNewBooks();
syncBooks(newBooks, 'both');
syncTimeoutRef.current = null;
},
SYNC_BOOKS_INTERVAL_SEC * 1000 - timeSinceLastSync,
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [library]);
const updateLibrary = async () => {
if (!syncedBooks?.length) return;
// Process old books first so that when we update the library the order is preserved
syncedBooks.sort((a, b) => a.updatedAt - b.updatedAt);
const processOldBook = async (oldBook: Book) => {
const matchingBook = syncedBooks.find((newBook) => newBook.hash === oldBook.hash);
if (matchingBook) {
if (!matchingBook.deletedAt && matchingBook.uploadedAt && !oldBook.downloadedAt) {
await appService?.downloadBook(oldBook, true);
}
const mergedBook =
matchingBook.updatedAt > oldBook.updatedAt
? { ...oldBook, ...matchingBook, updatedAt: oldBook.updatedAt }
: { ...matchingBook, ...oldBook, updatedAt: oldBook.updatedAt };
mergedBook.progress = matchingBook.progress ?? oldBook.progress;
return mergedBook;
}
return oldBook;
};
const updatedLibrary = await Promise.all(library.map(processOldBook));
const processNewBook = async (newBook: Book) => {
if (!updatedLibrary.some((oldBook) => oldBook.hash === newBook.hash)) {
if (newBook.uploadedAt && !newBook.deletedAt) {
try {
await appService?.downloadBook(newBook, true);
newBook.coverImageUrl = await appService?.generateCoverImageUrl(newBook);
updatedLibrary.push(newBook);
setLibrary(updatedLibrary);
} catch {
console.error('Failed to download book:', newBook);
}
}
}
};
onSyncStart?.();
const batchSize = 3;
for (let i = 0; i < syncedBooks.length; i += batchSize) {
const batch = syncedBooks.slice(i, i + batchSize);
await Promise.all(batch.map(processNewBook));
}
onSyncEnd?.();
setLibrary(updatedLibrary);
appService?.saveLibraryBooks(updatedLibrary);
};
useEffect(() => {
updateLibrary();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [syncedBooks]);
return { pullLibrary, pushLibrary };
};
+202 -28
View File
@@ -1,14 +1,19 @@
'use client';
import clsx from 'clsx';
import * as React from 'react';
import { useState, useRef, useEffect, Suspense } from 'react';
import { useState, useRef, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Book } from '@/types/book';
import { AppService } from '@/types/system';
import { navigateToReader } from '@/utils/nav';
import { navigateToLogin, navigateToReader } from '@/utils/nav';
import { getBaseFilename, listFormater } from '@/utils/book';
import { eventDispatcher } from '@/utils/event';
import { ProgressPayload } from '@/utils/transfer';
import { throttle } from '@/utils/throttle';
import { parseOpenWithFiles } from '@/helpers/cli';
import { isTauriAppPlatform } from '@/services/environment';
import { isTauriAppPlatform, hasUpdater } from '@/services/environment';
import { checkForAppUpdates } from '@/helpers/updater';
import { FILE_ACCEPT_FORMATS, SUPPORTED_FILE_EXTS } from '@/services/constants';
@@ -18,12 +23,17 @@ import { useTheme } from '@/hooks/useTheme';
import { useTranslation } from '@/hooks/useTranslation';
import { useLibraryStore } from '@/store/libraryStore';
import { useSettingsStore } from '@/store/settingsStore';
import { usePullToRefresh } from '@/hooks/usePullToRefresh';
import { useDemoBooks } from './hooks/useDemoBooks';
import { useBooksSync } from './hooks/useBooksSync';
import { AboutWindow } from '@/components/AboutWindow';
import { Toast } from '@/components/Toast';
import Spinner from '@/components/Spinner';
import LibraryHeader from './components/LibraryHeader';
import Bookshelf from './components/Bookshelf';
import { AboutWindow } from '@/components/AboutWindow';
import BookDetailModal from '@/components/BookDetailModal';
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
const LibraryPage = () => {
const router = useRouter();
@@ -31,35 +41,60 @@ const LibraryPage = () => {
const { token, user } = useAuth();
const {
library: libraryBooks,
updateBook,
setLibrary,
checkOpenWithBooks,
clearOpenWithBooks,
} = useLibraryStore();
useTheme();
const _ = useTranslation();
const { setSettings, saveSettings } = useSettingsStore();
const { updateAppTheme } = useTheme();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [loading, setLoading] = useState(false);
const isInitiating = useRef(false);
const [libraryLoaded, setLibraryLoaded] = useState(false);
const [isSelectMode, setIsSelectMode] = useState(false);
const [showDetailsBook, setShowDetailsBook] = useState<Book | null>(null);
const [booksTransferProgress, setBooksTransferProgress] = useState<{
[key: string]: number | null;
}>({});
const demoBooks = useDemoBooks();
const containerRef = useRef<HTMLDivElement>(null);
const { pullLibrary, pushLibrary } = useBooksSync({
onSyncStart: () => setLoading(true),
onSyncEnd: () => setLoading(false),
});
usePullToRefresh(containerRef, pullLibrary);
useScreenWakeLock(settings.screenWakeLock);
useEffect(() => {
const doAppUpdates = async () => {
if (isTauriAppPlatform()) {
await checkForAppUpdates();
updateAppTheme('base-200');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const doCheckAppUpdates = async () => {
if (hasUpdater() && settings.autoCheckUpdates) {
await checkForAppUpdates(_);
}
};
doAppUpdates();
}, []);
doCheckAppUpdates();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings]);
const processOpenWithFiles = React.useCallback(
async (appService: AppService, openWithFiles: string[], libraryBooks: Book[]) => {
const bookIds: string[] = [];
for (const file of openWithFiles) {
const book = await appService.importBook(file, libraryBooks);
if (book) {
bookIds.push(book.hash);
console.log('Open with book:', file);
try {
const book = await appService.importBook(file, libraryBooks);
if (book) {
bookIds.push(book.hash);
}
} catch (error) {
console.log('Failed to import book:', file, error);
}
}
setLibrary(libraryBooks);
@@ -149,9 +184,27 @@ const LibraryPage = () => {
const importBooks = async (files: [string | File]) => {
setLoading(true);
const failedFiles = [];
for (const file of files) {
await appService?.importBook(file, libraryBooks);
setLibrary(libraryBooks);
try {
const book = await appService?.importBook(file, libraryBooks);
setLibrary(libraryBooks);
if (user && book && !book.uploadedAt && settings.autoUpload) {
console.log('Uploading book:', book.title);
handleBookUpload(book);
}
} catch (error) {
const filename = typeof file === 'string' ? file : file.name;
const baseFilename = getBaseFilename(filename);
failedFiles.push(baseFilename);
eventDispatcher.dispatch('toast', {
message: _('Failed to import book(s): {{filenames}}', {
filenames: listFormater(false).format(failedFiles),
}),
type: 'error',
});
console.error('Failed to import book:', filename, error);
}
}
appService?.saveLibraryBooks(libraryBooks);
setLoading(false);
@@ -175,13 +228,102 @@ const LibraryPage = () => {
});
};
const updateBookTransferProgress = throttle((bookHash: string, progress: ProgressPayload) => {
if (progress.total === 0) return;
const progressPct = (progress.progress / progress.total) * 100;
setBooksTransferProgress((prev) => ({
...prev,
[bookHash]: progressPct,
}));
}, 500);
const handleBookUpload = async (book: Book) => {
try {
await appService?.uploadBook(book, (progress) => {
updateBookTransferProgress(book.hash, progress);
});
await updateBook(envConfig, book);
pushLibrary();
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
message: _('Book uploaded: {{title}}', {
title: book.title,
}),
});
} catch (err) {
if (err instanceof Error) {
if (err.message.includes('Not authenticated') && settings.keepLogin) {
navigateToLogin(router);
return;
} else if (err.message.includes('Insufficient storage quota')) {
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Insufficient storage quota'),
});
return;
}
}
eventDispatcher.dispatch('toast', {
type: 'error',
message: _('Failed to upload book: {{title}}', {
title: book.title,
}),
});
}
};
const handleBookDownload = async (book: Book) => {
try {
await appService?.downloadBook(book, false, (progress) => {
updateBookTransferProgress(book.hash, progress);
});
await updateBook(envConfig, book);
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
message: _('Book downloaded: {{title}}', {
title: book.title,
}),
});
} catch {
eventDispatcher.dispatch('toast', {
message: _('Failed to download book: {{title}}', {
title: book.title,
}),
type: 'error',
});
}
};
const handleBookDelete = async (book: Book) => {
try {
await appService?.deleteBook(book, !!book.uploadedAt);
await updateBook(envConfig, book);
pushLibrary();
eventDispatcher.dispatch('toast', {
type: 'info',
timeout: 2000,
message: _('Book deleted: {{title}}', {
title: book.title,
}),
});
} catch {
eventDispatcher.dispatch('toast', {
message: _('Failed to delete book: {{title}}', {
title: book.title,
}),
type: 'error',
});
}
};
const handleImportBooks = async () => {
console.log('Importing books...');
let files;
if (isTauriAppPlatform()) {
const { type } = await import('@tauri-apps/plugin-os');
if (['android', 'ios'].includes(type())) {
if (appService?.isMobile) {
files = (await selectFilesWeb()) as [File];
} else {
files = (await selectFilesTauri()) as [string];
@@ -196,6 +338,14 @@ const LibraryPage = () => {
setIsSelectMode(!isSelectMode);
};
const handleSetSelectMode = (selectMode: boolean) => {
setIsSelectMode(selectMode);
};
const handleShowDetailsBook = (book: Book) => {
setShowDetailsBook(book);
};
if (!appService) {
return null;
}
@@ -211,7 +361,13 @@ 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 text-base-content flex select-none flex-col overflow-hidden',
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
appService?.hasRoundedWindow && 'rounded-window',
)}
>
<div className='fixed top-0 z-40 w-full'>
<LibraryHeader
isSelectMode={isSelectMode}
@@ -225,15 +381,25 @@ const LibraryPage = () => {
</div>
)}
{libraryLoaded &&
(libraryBooks.length > 0 ? (
<div className='mt-12 flex-grow overflow-auto px-2'>
<Suspense>
<Bookshelf
libraryBooks={libraryBooks}
isSelectMode={isSelectMode}
onImportBooks={handleImportBooks}
/>
</Suspense>
(libraryBooks.some((book) => !book.deletedAt) ? (
<div
ref={containerRef}
className={clsx(
'mt-12 flex-grow overflow-auto px-4 sm:px-2',
appService?.hasSafeAreaInset && 'mt-[calc(48px+env(safe-area-inset-top))]',
)}
>
<Bookshelf
libraryBooks={libraryBooks}
isSelectMode={isSelectMode}
handleImportBooks={handleImportBooks}
handleBookUpload={handleBookUpload}
handleBookDownload={handleBookDownload}
handleBookDelete={handleBookDelete}
handleSetSelectMode={handleSetSelectMode}
handleShowDetailsBook={handleShowDetailsBook}
booksTransferProgress={booksTransferProgress}
/>
</div>
) : (
<div className='hero h-screen items-center justify-center'>
@@ -252,7 +418,15 @@ const LibraryPage = () => {
</div>
</div>
))}
{showDetailsBook && (
<BookDetailModal
isOpen={!!showDetailsBook}
book={showDetailsBook}
onClose={() => setShowDetailsBook(null)}
/>
)}
<AboutWindow />
<Toast />
</div>
);
};
+17
View File
@@ -0,0 +1,17 @@
import Image from 'next/image';
export default function Offline() {
return (
<div className='flex min-h-screen flex-col items-center justify-center bg-gray-100 text-center'>
<div className='mb-4'>
<Image src='/icon.png' alt='App Icon' width={100} height={100} className='rounded-lg' />
</div>
<h1 className='text-2xl font-bold text-gray-800'>Readest</h1>
<p className='mt-2 text-gray-600'>
It seems you&apos;re offline. Please check your internet connection and try again.
</p>
</div>
);
}
+2 -14
View File
@@ -1,19 +1,7 @@
'use client';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { navigateToLibrary } from '@/utils/nav';
import Spinner from '@/components/Spinner';
import { redirectToLibrary } from '@/utils/nav';
const HomePage = () => {
const router = useRouter();
useEffect(() => {
navigateToLibrary(router);
}, [router]);
return <Spinner loading={true} />;
redirectToLibrary();
};
export default HomePage;
@@ -97,9 +97,9 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
<Button
icon={
isBookmarked ? (
<MdOutlineBookmark size={20} className='text-base-content' />
<MdOutlineBookmark className='text-base-content' />
) : (
<MdOutlineBookmarkAdd size={20} className='text-base-content' />
<MdOutlineBookmarkAdd className='text-base-content' />
)
}
onClick={toggleBookmark}
@@ -1,7 +1,11 @@
import React from 'react';
import clsx from 'clsx';
import React, { useEffect } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useSidebarStore } from '@/store/sidebarStore';
import FoliateViewer from './FoliateViewer';
import getGridTemplate from '@/utils/grid';
import SectionInfo from './SectionInfo';
@@ -11,8 +15,8 @@ import PageInfoView from './PageInfo';
import Ribbon from './Ribbon';
import SettingsDialog from './settings/SettingsDialog';
import Annotator from './annotator/Annotator';
import { useBookDataStore } from '@/store/bookDataStore';
import FootnotePopup from './FootnotePopup';
import HintInfo from './HintInfo';
interface BooksGridProps {
bookKeys: string[];
@@ -20,14 +24,27 @@ interface BooksGridProps {
}
const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const { appService } = useEnv();
const { getConfig, getBookData } = useBookDataStore();
const { getProgress, getViewState, getViewSettings } = useReaderStore();
const { sideBarBookKey } = useSidebarStore();
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
const gridTemplate = getGridTemplate(bookKeys.length, window.innerWidth / window.innerHeight);
useEffect(() => {
if (!sideBarBookKey) return;
const bookData = getBookData(sideBarBookKey);
if (!bookData || !bookData.book) return;
document.title = bookData.book.title;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sideBarBookKey]);
return (
<div
className='grid h-full flex-grow'
className={clsx(
'grid h-full flex-grow',
appService?.hasSafeAreaInset && 'pt-[env(safe-area-inset-top)]',
)}
style={{
gridTemplateColumns: gridTemplate.columns,
gridTemplateRows: gridTemplate.rows,
@@ -49,7 +66,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
<div
id={`gridcell-${bookKey}`}
key={bookKey}
className='relative h-full w-full overflow-hidden'
className={`${appService?.hasRoundedWindow ? 'rounded-window' : ''} relative h-full w-full overflow-hidden`}
>
{isBookmarked && <Ribbon width={marginGap} />}
<HeaderBar
@@ -65,6 +82,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
{viewSettings.scrolled ? null : (
<>
<SectionInfo section={sectionLabel} gapLeft={marginGap} />
<HintInfo bookKey={bookKey} gapRight={marginGap} />
<PageInfoView
bookFormat={book.format}
section={section ?? null}
@@ -4,11 +4,13 @@ import { BookConfig } from '@/types/book';
import { FoliateView, wrappedFoliateView } from '@/types/view';
import { useReaderStore } from '@/store/readerStore';
import { useParallelViewStore } from '@/store/parallelViewStore';
import { useClickEvent } from '../hooks/useClickEvent';
import { useClickEvent, useTouchEvent } from '../hooks/useIframeEvents';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { useProgressSync } from '../hooks/useProgressSync';
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
import { useAutoHideScrollbar } from '../hooks/useAutoHideScrollbar';
import { getStyles, mountAdditionalFonts } from '@/utils/style';
import { getBookDirFromWritingMode } from '@/utils/book';
import { useTheme } from '@/hooks/useTheme';
import { ONE_COLUMN_MAX_INLINE_SIZE } from '@/services/constants';
import {
@@ -17,8 +19,10 @@ import {
handleMouseup,
handleClick,
handleWheel,
handleTouchStart,
handleTouchMove,
handleTouchEnd,
} from '../utils/iframeEventHandlers';
import Toast from '@/components/Toast';
const FoliateViewer: React.FC<{
bookKey: string;
@@ -39,11 +43,11 @@ const FoliateViewer: React.FC<{
return () => clearTimeout(timer);
}, [toastMessage]);
useProgressSync(bookKey, setToastMessage);
useProgressSync(bookKey);
useProgressAutoSave(bookKey);
const progressRelocateHandler = (event: Event) => {
const detail = (event as CustomEvent).detail;
// console.log('relocate:', detail);
setProgress(bookKey, detail.cfi, detail.tocItem, detail.section, detail.location, detail.range);
};
@@ -69,6 +73,9 @@ const FoliateViewer: React.FC<{
detail.doc.addEventListener('mouseup', handleMouseup.bind(null, bookKey));
detail.doc.addEventListener('click', handleClick.bind(null, bookKey));
detail.doc.addEventListener('wheel', handleWheel.bind(null, bookKey));
detail.doc.addEventListener('touchstart', handleTouchStart.bind(null, bookKey));
detail.doc.addEventListener('touchmove', handleTouchMove.bind(null, bookKey));
detail.doc.addEventListener('touchend', handleTouchEnd.bind(null, bookKey));
}
}
};
@@ -76,6 +83,18 @@ const FoliateViewer: React.FC<{
const docRelocateHandler = (event: Event) => {
const detail = (event as CustomEvent).detail;
if (detail.reason !== 'scroll' && detail.reason !== 'page') return;
if (detail.reason === 'scroll') {
const renderer = viewRef.current?.renderer;
if (renderer) {
if (renderer.start <= 0) {
viewRef.current?.prev(1);
// sometimes viewSize has subpixel value that the end never reaches
} else if (renderer.end + 1 >= renderer.viewSize) {
viewRef.current?.next(1);
}
}
}
const parallelViews = getParallels(bookKey);
if (parallelViews && parallelViews.size > 0) {
parallelViews.forEach((key) => {
@@ -89,7 +108,9 @@ const FoliateViewer: React.FC<{
}
};
useTouchEvent(bookKey, viewRef);
const { handleTurnPage } = useClickEvent(bookKey, viewRef, containerRef);
useFoliateEvents(viewRef.current, {
onLoad: docLoadHandler,
onRelocate: progressRelocateHandler,
@@ -123,6 +144,11 @@ const FoliateViewer: React.FC<{
const viewSettings = getViewSettings(bookKey)!;
view.renderer.setStyles?.(getStyles(viewSettings, themeCode));
const writingMode = viewSettings.writingMode;
if (writingMode) {
view.book.dir = getBookDirFromWritingMode(writingMode);
}
const isScrolled = viewSettings.scrolled!;
const marginPx = viewSettings.marginPx!;
const gapPercent = viewSettings.gapPercent!;
@@ -164,9 +190,6 @@ const FoliateViewer: React.FC<{
onClick={(event) => handleTurnPage(event)}
ref={containerRef}
/>
{toastMessage && (
<Toast message={toastMessage} toastClass='toast-top toast-end' alertClass='alert-success' />
)}
</>
);
};
@@ -2,10 +2,13 @@ import React from 'react';
import clsx from 'clsx';
import { RiArrowLeftWideLine, RiArrowRightWideLine } from 'react-icons/ri';
import { RiArrowGoBackLine, RiArrowGoForwardLine } from 'react-icons/ri';
import { FaHeadphones } from 'react-icons/fa6';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTranslation } from '@/hooks/useTranslation';
import { eventDispatcher } from '@/utils/event';
import Button from '@/components/Button';
interface FooterBarProps {
@@ -16,9 +19,11 @@ interface FooterBarProps {
const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim }) => {
const _ = useTranslation();
const { hoveredBookKey, setHoveredBookKey, getView } = useReaderStore();
const { appService } = useEnv();
const { hoveredBookKey, setHoveredBookKey, getView, getProgress } = useReaderStore();
const { isSideBarVisible } = useSidebarStore();
const view = getView(bookKey);
const progress = getProgress(bookKey);
const handleProgressChange = (event: React.ChangeEvent) => {
const newProgress = parseInt((event.target as HTMLInputElement).value, 10);
@@ -41,33 +46,43 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
view?.history.forward();
};
const handleSpeakText = async () => {
if (!view || !progress) return;
const { range } = progress;
if (eventDispatcher.dispatchSync('tts-is-speaking')) {
eventDispatcher.dispatch('tts-stop', { bookKey });
} else {
eventDispatcher.dispatch('tts-speak', { bookKey, range });
}
};
const pageinfoValid = pageinfo && pageinfo.total > 0 && pageinfo.current >= 0;
const progressFraction = pageinfoValid ? pageinfo.current / pageinfo.total : 0;
return (
<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',
'footer-bar absolute bottom-0 z-10 flex h-12 w-full items-center gap-x-4 px-4',
'shadow-xs bg-base-100 transition-opacity duration-300',
appService?.hasSafeAreaInset && 'pb-[env(safe-area-inset-bottom)]',
appService?.hasRoundedWindow && 'rounded-window-bottom-right',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-bottom-left',
isHoveredAnim && 'hover-bar-anim',
hoveredBookKey === bookKey ? `opacity-100` : `opacity-0`,
)}
onMouseEnter={() => setHoveredBookKey(bookKey)}
onMouseLeave={() => setHoveredBookKey('')}
>
<div className='hidden sm:flex'>
<Button icon={<RiArrowLeftWideLine />} onClick={handleGoPrev} tooltip={_('Go Left')} />
</div>
<Button
icon={<RiArrowLeftWideLine size={20} />}
onClick={handleGoPrev}
tooltip={_('Go Left')}
/>
<Button
icon={<RiArrowGoBackLine size={20} />}
icon={<RiArrowGoBackLine />}
onClick={handleGoBack}
tooltip={_('Go Back')}
disabled={!view?.history.canGoBack}
/>
<Button
icon={<RiArrowGoForwardLine size={20} />}
icon={<RiArrowGoForwardLine />}
onClick={handleGoForward}
tooltip={_('Go Forward')}
disabled={!view?.history.canGoForward}
@@ -83,11 +98,10 @@ const FooterBar: React.FC<FooterBarProps> = ({ bookKey, pageinfo, isHoveredAnim
value={pageinfoValid ? progressFraction * 100 : 0}
onChange={(e) => handleProgressChange(e)}
/>
<Button
icon={<RiArrowRightWideLine size={20} />}
onClick={handleGoNext}
tooltip={_('Go Right')}
/>
<Button icon={<FaHeadphones />} onClick={handleSpeakText} tooltip={_('Speak')} />
<div className='hidden sm:flex'>
<Button icon={<RiArrowRightWideLine />} onClick={handleGoNext} tooltip={_('Go Right')} />
</div>
</div>
);
};
@@ -4,8 +4,9 @@ import { BookDoc } from '@/libs/document';
import { useReaderStore } from '@/store/readerStore';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { useTheme } from '@/hooks/useTheme';
import { getStyles } from '@/utils/style';
import { getFootnoteStyles, getStyles } from '@/utils/style';
import { getPopupPosition, getPosition, Position } from '@/utils/sel';
import { eventDispatcher } from '@/utils/event';
import { FoliateView } from '@/types/view';
import { FootnoteHandler } from 'foliate-js/footnotes.js';
import Popup from '@/components/Popup';
@@ -28,6 +29,7 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
const { getView, getViewSettings } = useReaderStore();
const { themeCode } = useTheme();
const view = getView(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const footnoteHandler = new FootnoteHandler();
useEffect(() => {
@@ -47,7 +49,9 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
const backgroundColor = getComputedStyle(popupContainer).backgroundColor;
popupTheme.bg = backgroundColor;
}
renderer.setStyles?.(getStyles(viewSettings, popupTheme));
const mainStyles = getStyles(viewSettings, popupTheme);
const footnoteStyles = getFootnoteStyles();
renderer.setStyles?.(`${mainStyles}\n${footnoteStyles}`);
};
const handleRender = (e: Event) => {
@@ -73,7 +77,13 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
const rect = gridFrame.getBoundingClientRect();
const viewSettings = getViewSettings(bookKey)!;
const triangPos = getPosition(detail.a, rect, viewSettings.vertical);
const popupPos = getPopupPosition(triangPos, rect, popupWidth, popupHeight, popupPadding);
const popupPos = getPopupPosition(
triangPos,
rect,
viewSettings.vertical ? popupHeight : popupWidth,
viewSettings.vertical ? popupWidth : popupHeight,
popupPadding,
);
setTrianglePosition(triangPos);
setPopupPosition(popupPos);
@@ -97,16 +107,52 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
setShowPopup(false);
};
const handleFootnotePopupEvent = (event: CustomEvent) => {
const { element, footnote } = event.detail;
const gridFrame = document.querySelector(`#gridcell-${bookKey}`);
if (!gridFrame) return;
const rect = gridFrame.getBoundingClientRect();
const viewSettings = getViewSettings(bookKey)!;
const triangPos = getPosition(element, rect, viewSettings.vertical);
const popupPos = getPopupPosition(
triangPos,
rect,
viewSettings.vertical ? popupHeight : popupWidth,
viewSettings.vertical ? popupWidth : popupHeight,
popupPadding,
);
if (footnoteRef.current) {
const elem = document.createElement('p');
elem.textContent = footnote;
elem.setAttribute('style', `padding: 16px; hanging-punctuation: allow-end last;`);
footnoteRef.current.replaceChildren(elem);
setShowPopup(true);
setTrianglePosition(triangPos);
setPopupPosition(popupPos);
}
};
useFoliateEvents(view, {
onLinkClick: docLinkHandler,
});
useEffect(() => {
eventDispatcher.on('footnote-popup', handleFootnotePopupEvent);
return () => {
eventDispatcher.off('footnote-popup', handleFootnotePopupEvent);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (footnoteViewRef.current) {
footnoteRef.current?.replaceChildren(footnoteViewRef.current);
}
}, [footnoteRef]);
const width = viewSettings.vertical ? popupHeight : popupWidth;
const height = viewSettings.vertical ? popupWidth : popupHeight;
return (
<div>
{showPopup && (
@@ -117,8 +163,8 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
/>
)}
<Popup
width={popupWidth}
height={popupHeight}
width={width}
height={height}
position={showPopup ? popupPosition! : undefined}
trianglePosition={showPopup ? trianglePosition! : undefined}
className='select-text overflow-y-auto'
@@ -127,8 +173,8 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
className=''
ref={footnoteRef}
style={{
width: `${popupWidth}px`,
height: `${popupHeight}px`,
width: `${width}px`,
height: `${height}px`,
}}
></div>
</Popup>
@@ -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 { useResponsiveSize } from '@/hooks/useResponsiveSize';
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 {
@@ -36,6 +38,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const { hoveredBookKey, setHoveredBookKey, bookKeys } = useReaderStore();
const { isSideBarVisible } = useSidebarStore();
const iconSize16 = useResponsiveSize(16);
const handleToggleDropdown = (isOpen: boolean) => {
setIsDropdownOpen(isOpen);
@@ -48,8 +51,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`,
appService?.hasRoundedWindow && 'rounded-window-top-right',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window-top-left',
isHoveredAnim && 'hover-bar-anim',
hoveredBookKey === bookKey || isDropdownOpen ? `visible` : `opacity-0`,
isDropdownOpen && 'header-bar-pinned',
@@ -57,21 +61,22 @@ 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 bg-base-100 z-20 flex h-full items-center space-x-4'>
<SidebarToggler bookKey={bookKey} />
<BookmarkToggler bookKey={bookKey} />
</div>
<div className='header-title flex flex-1 items-center justify-center'>
<h2 className='line-clamp-1 max-w-[80%] text-center text-xs font-semibold'>{bookTitle}</h2>
<div className='header-title z-15 pointer-events-none absolute inset-0 hidden items-center justify-center sm:flex'>
<h2 className='line-clamp-1 max-w-[50%] text-center text-xs font-semibold'>{bookTitle}</h2>
</div>
<div className='flex h-full items-center space-x-2'>
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4'>
<SettingsToggler />
<NotebookToggler bookKey={bookKey} />
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeVerticalBold size={16} />}
toggleButton={<PiDotsThreeVerticalBold size={iconSize16} />}
onToggle={handleToggleDropdown}
>
<ViewMenu bookKey={bookKey} onSetSettingsDialogOpen={onSetSettingsDialogOpen} />
@@ -80,8 +85,12 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
<WindowButtons
className='window-buttons flex h-full items-center'
headerRef={headerRef}
showMinimize={bookKeys.length == 1 && appService?.appPlatform !== 'web'}
showMaximize={bookKeys.length == 1 && appService?.appPlatform !== 'web'}
showMinimize={
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
}
showMaximize={
bookKeys.length == 1 && !isTrafficLightVisible && appService?.appPlatform !== 'web'
}
onClose={() => onCloseBook(bookKey)}
/>
</div>
@@ -0,0 +1,58 @@
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import { useSidebarStore } from '@/store/sidebarStore';
import { eventDispatcher } from '@/utils/event';
import useTrafficLight from '@/hooks/useTrafficLight';
interface SectionInfoProps {
bookKey: string;
gapRight: string;
}
const HintInfo: React.FC<SectionInfoProps> = ({ bookKey, gapRight }) => {
const { isSideBarVisible } = useSidebarStore();
const { isTrafficLightVisible } = useTrafficLight();
const [hintMessage, setHintMessage] = React.useState<string | null>(null);
const hintTimeout = useRef(2000);
const dismissTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleShowHint = (event: CustomEvent) => {
const { message, bookKey: hintBookKey, timeout = 2000 } = event.detail;
if (hintBookKey !== bookKey) return;
setHintMessage(message);
hintTimeout.current = timeout;
};
useEffect(() => {
eventDispatcher.on('hint', handleShowHint);
return () => {
eventDispatcher.off('hint', handleShowHint);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (dismissTimeout.current) clearTimeout(dismissTimeout.current);
dismissTimeout.current = setTimeout(() => setHintMessage(''), hintTimeout.current);
return () => {
if (dismissTimeout.current) clearTimeout(dismissTimeout.current);
};
}, [hintMessage]);
return (
<div
className={clsx(
'pageinfo absolute right-0 top-0 flex max-w-[50%] items-end',
isTrafficLightVisible && !isSideBarVisible ? 'h-[44px]' : 'h-[30px]',
hintMessage ? '' : '',
)}
style={{ right: gapRight }}
>
<h2 className='text-neutral-content line-clamp-1 text-center font-sans text-xs font-light'>
{hintMessage || ''}
</h2>
</div>
);
};
export default HintInfo;
@@ -1,9 +1,10 @@
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';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import Button from '@/components/Button';
interface NotebookTogglerProps {
@@ -14,6 +15,8 @@ const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
const _ = useTranslation();
const { sideBarBookKey, setSideBarBookKey } = useSidebarStore();
const { isNotebookVisible, toggleNotebook } = useNotebookStore();
const iconSize16 = useResponsiveSize(16);
const handleToggleSidebar = () => {
if (sideBarBookKey === bookKey) {
toggleNotebook();
@@ -26,9 +29,9 @@ const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
<Button
icon={
sideBarBookKey == bookKey && isNotebookVisible ? (
<TbLayoutSidebarRightFilled size={20} className='text-base-content' />
<LuNotebookPen size={iconSize16} className='text-base-content' />
) : (
<TbLayoutSidebarRight size={20} className='text-base-content' />
<LuNotebookPen size={iconSize16} className='text-base-content' />
)
}
onClick={handleToggleSidebar}
@@ -1,22 +1,29 @@
'use client';
import clsx from 'clsx';
import * as React from 'react';
import { useEffect, Suspense, useRef } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useTheme } from '@/hooks/useTheme';
import { useLibraryStore } from '@/store/libraryStore';
import ReaderContent from './ReaderContent';
import { AboutWindow } from '@/components/AboutWindow';
import { useSettingsStore } from '@/store/settingsStore';
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
import { AboutWindow } from '@/components/AboutWindow';
import { Toast } from '@/components/Toast';
import ReaderContent from './ReaderContent';
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
const { envConfig } = useEnv();
const { envConfig, appService } = useEnv();
const { settings, setSettings } = useSettingsStore();
const { library, setLibrary } = useLibraryStore();
const isInitiating = useRef(false);
const { updateAppTheme } = useTheme();
useScreenWakeLock(settings.screenWakeLock);
useEffect(() => {
updateAppTheme('base-100');
if (isInitiating.current) return;
isInitiating.current = true;
const initLibrary = async () => {
@@ -33,10 +40,16 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
return (
library.length > 0 &&
settings.globalReadSettings && (
<div className='reader-page bg-base-100 text-base-content min-h-screen select-none'>
<div
className={clsx(
`reader-page bg-base-100 text-base-content select-none`,
appService?.hasRoundedWindow && 'rounded-window',
)}
>
<Suspense>
<ReaderContent ids={ids} settings={settings} />
<AboutWindow />
<Toast />
</Suspense>
</div>
)

Some files were not shown because too many files have changed in this diff Show More