Compare commits

...

150 Commits

Author SHA1 Message Date
Huang Xin 721e70d027 release: version 0.9.93 (#2486) 2025-11-20 06:33:58 +01:00
Huang Xin 3c1a7eac86 fix(library): fixed occasional freeze when navigating back to the library (#2485)
Closes #2482.
2025-11-20 06:15:49 +01:00
Huang Xin f8e21dbd4c fix(sync): fixed sync issues affecting new accounts, closes #2481 (#2483) 2025-11-20 04:28:41 +01:00
Huang Xin d16a35d26f fix(layout): constrain image height within table cells in paginated mode (#2480)
Closes #2472.
2025-11-19 18:06:39 +01:00
Huang Xin f881caf794 chore(pwa): config workbox to skip precaching next.js internal files (#2478) 2025-11-19 14:38:04 +01:00
54wedge eb6fa276de fix(txt): register style.css to content.opf (#2476)
* fix(txt): register style.css to content.opf

* refactor: code styling

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2025-11-19 13:59:04 +01:00
Huang Xin e987c8b37a chore(pwa): get rid of public headers requests (#2477) 2025-11-19 13:31:21 +01:00
Huang Xin 26d76e27ac fix(txt): more tolerant encoding detection for utf-8 (#2475) 2025-11-19 07:47:10 +01:00
Huang Xin 975549fca0 fix(font): avoid overriding monospace fonts for code blocks, closes #1805 (#2474) 2025-11-19 06:57:45 +01:00
Huang Xin 9a6bed52dd fix(sync): propagate book delete status to other devices (#2473) 2025-11-19 05:47:59 +01:00
Huang Xin efd9ad12a9 release: version 0.9.92 (#2469) 2025-11-18 15:15:27 +01:00
Huang Xin 93a71a0fd0 fix(bookshelf): ensure 'select all' only targets books in the active group (#2468) 2025-11-18 14:34:32 +01:00
Huang Xin d373d7ac28 fix(txt): handle chapter title patterns (#2467) 2025-11-18 14:09:03 +01:00
Huang Xin e0e991b599 fix(kosync): proxy kosync request only for the web app, closes #2440 (#2466) 2025-11-18 13:44:30 +01:00
Huang Xin 6d5b16ea8b fix(iap): open external app for payment (#2465) 2025-11-18 12:07:43 +01:00
Huang Xin bfac67d3f0 fix: ger rid of the AbortSignal polyfill (#2463) 2025-11-18 09:10:53 +01:00
Huang Xin 0fd316d775 fix(sync): retry sync books for previous failed sync, closes #2441 (#2462) 2025-11-17 18:52:48 +01:00
Huang Xin c42fcf9ac4 fix: save reading progress when closing app directly in reader page, closes #2346 (#2461) 2025-11-17 16:35:56 +01:00
Huang Xin 04ade02a06 fix(layout): enlarge clickable area for the close button in the reader page (#2459) 2025-11-17 15:22:05 +01:00
Huang Xin 8ee53d3367 fix(mobi): properly handle empty fragments for MOBI (#2456) 2025-11-17 05:09:09 +01:00
Huang Xin 42111945b8 fix(layout): scale table to fit within column constraints, closes #2445 (#2455) 2025-11-17 04:52:23 +01:00
Huang Xin 6fde157047 feat(bookshelf): support nested groups in the bookshelf, closes #568 (#2449) 2025-11-16 13:59:37 +01:00
Huang Xin 65ec5c9842 fix(library): avoid invalid regular expression in library search (#2439) 2025-11-16 14:07:29 +08:00
Huang Xin a6444b5b33 feat: supported updating email address for Readest account (#2437) 2025-11-12 19:03:02 +01:00
Huang Xin c7238cb04c fix(css): overriding book color for more semantic tags, closes #2433 (#2436) 2025-11-11 19:44:13 +01:00
Huang Xin 366e5fafad fix: fixed multiple windows are opened when opening with readest, closes #2429 (#2435) 2025-11-11 19:36:28 +01:00
Huang Xin fb5710c134 fix(pdf): disabled pagination with swipe gesture below minimum velocity for PDFs, closes #2428 (#2434) 2025-11-11 18:20:45 +01:00
ByteFlow c4d9652335 fix(annotator): enhance PDF context menu for translation and improve touch handling (#2430)
* fix(annotator): enhance PDF context menu for translation and improve touch handling

* feat(annotator, shortcuts): add keyboard shortcuts for selection actions

* feat(annotator): reposition popups on scroll to enhance user experience

* feat(annotator): disable currently unsupported annotator functions for PDFs

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2025-11-11 15:10:44 +01:00
Huang Xin df6027cf9f fix(eink): avoid gray color in E-Ink mode (#2427) 2025-11-09 19:17:35 +01:00
Huang Xin e3f7abf7f1 fix(tts): fixed incorrectly selected voices, closes #2386 (#2426) 2025-11-09 19:11:12 +01:00
Huang Xin c539917d7e compat(footnote): support more footnote formats (#2425) 2025-11-09 18:07:39 +01:00
Huang Xin 5eb870c978 fix(layout): don't hide horizontal scrollbar, closes #2418 (#2424) 2025-11-09 17:03:24 +01:00
Huang Xin b075c46ba3 compat(sync): compatibility for kosync server implementations that do not return timestamp (#2422) 2025-11-09 14:42:07 +01:00
Huang Xin 4b6c7776f5 fix(eink): no shade in eink mode (#2421) 2025-11-09 14:34:26 +01:00
Huang Xin 745a14195d fix(layout): fixed header bar z index (#2413) 2025-11-06 10:56:28 +01:00
Huang Xin fb0f660eaf fix(android): fixed back gesture to close config panel, closes #2406 (#2411) 2025-11-05 22:45:13 +01:00
Huang Xin 04a3030b79 fix(metadata): fixed changing cover image in Readest apps, closes #2402 (#2410) 2025-11-05 20:43:42 +01:00
Huang Xin 5a3114de48 fix(android): get brightness of the current window on Android, closes #2389 (#2409) 2025-11-05 19:36:45 +01:00
Huang Xin b808fc5954 fix: default to not override book color for PDF, closes #2397 (#2404) 2025-11-04 18:46:37 +01:00
Huang Xin 0ec4e37c13 fix(css): support style transformer for inline css (#2403)
* fix(css): support style transformer for inline css

* fix(eink): bold text for current chapter in the TOC, closes #2401
2025-11-04 18:36:46 +01:00
Huang Xin 9d301631ca fix(layout): restrict image container within column boundaries, closes #2390 (#2398) 2025-11-03 16:35:08 +01:00
Huang Xin ec7145bb01 fix(upload): sanitize filenames before uploading to cloud bucket (#2396) 2025-11-03 11:17:24 +01:00
Huang Xin f79d84b5fa fix(eink): more readablility for E-Ink mode on the library page, closes #2364 (#2395)
This also closes #2391 and closes #2394.
2025-11-03 09:23:35 +01:00
Huang Xin d00f1e0def feat(import): import files directly into the current book group (#2393) 2025-11-03 05:47:26 +01:00
Huang Xin 637a813732 fix(settings): don't show system fonts on Android, closes #2381 (#2388) 2025-11-02 09:49:25 +01:00
Huang Xin 4f33c9280b fix(layout): fine tuning of column width in scrolled mode and vertical mode, closes #2383 (#2387) 2025-11-02 09:34:57 +01:00
Huang Xin d54c752637 fix(koplugin): properly refresh access token (#2384) 2025-11-02 04:50:54 +01:00
Huang Xin 984d5d198d fix(css): avoid overriding table background color by default, closes #2377 (#2379) 2025-11-01 15:09:46 +01:00
Huang Xin 2568778a87 feat(layout): support for duokan bleed layout, closes #2374 (#2378) 2025-11-01 14:26:48 +01:00
Huang Xin 0d805a64f6 compat: polyfill AbortController/AbortSignal on older browsers (#2371) 2025-10-31 06:15:59 +01:00
Huang Xin 066d1c5b1e fix(import): resolve import failures for certain EPUB files, closes #230 (#2370)
If the publisher has already been parsed, don’t remap it from the author or contributor fields again.
This prevents failures when parsing metadata like <dc:creator opf:role="pbl" />.
2025-10-31 05:12:24 +01:00
Huang Xin cdc1950c3a fix(layout): various fixes on the book and reader styles and layouts, closes #2368 (#2369) 2025-10-30 19:34:55 +01:00
Huang Xin 7142874513 fix(ui): display exact storage capacity in the user profile (#2366) 2025-10-30 11:50:39 +01:00
Huang Xin 409c2f62be release: version 0.9.91 (#2362) 2025-10-29 16:37:43 +01:00
Huang Xin 82922c5053 feat(eink): add an option to save last book cover on Android, closes #1414 (#2361) 2025-10-29 16:15:08 +01:00
Huang Xin 9c5bcb13e2 feat(plan): show life-time plan name in user profile (#2360) 2025-10-29 15:36:12 +01:00
Huang Xin 1d87dcdb7f feat: add options to customize the style of highlighted text of current sentence of TTS (#2358) 2025-10-29 10:17:36 +01:00
Huang Xin c7ba44a91e eink: disable all shadows in E-Ink mode (#2354) 2025-10-29 06:46:17 +01:00
Huang Xin 19b79c5b35 fix(android): fixed Android launcher icon size on some Android systems (#2353) 2025-10-29 06:19:02 +01:00
Huang Xin 45216763e3 fix(compat): use xml serializer for sanitized doc for better compatibility (#2352) 2025-10-29 05:12:19 +01:00
Huang Xin 123293dc0e fix: fixed crash when switching allow script (#2351) 2025-10-29 03:17:17 +01:00
Huang Xin 78e61060d2 fix(tts): fixed listener of tts events (#2349) 2025-10-28 18:36:57 +01:00
Huang Xin dd5371d2fd release: version 0.9.90 (#2344) 2025-10-28 05:44:07 +01:00
Huang Xin da041d1f38 fix(ui): hover to display footer bar in non-maximized windows on desktop (#2343) 2025-10-28 05:30:41 +01:00
Huang Xin d01599a2b4 feat(security): sanitize XHTML safely to prevent XSS when scripts are disabled, closes #2316 (#2342) 2025-10-28 04:28:48 +01:00
Huang Xin e80a2268ac feat(ui): add non-linear scale to brightness slider for better low-value control (#2338)
Implement logarithmic mapping for screen brightness slider to improve
usability at low brightness levels where small percentage changes have
significant visual impact.
2025-10-27 13:45:43 +01:00
Huang Xin 3b86229c8f perf(sync): batched updating notes and books when syncing (#2337) 2025-10-27 13:37:04 +01:00
Huang Xin c4d498d183 perf: much faster sync of the whole library (#2336) 2025-10-27 05:33:12 +01:00
Huang Xin 9bc46914e2 feat(iap): support expanding cloud storage with IAP on iOS and Android (#2331) 2025-10-26 10:18:56 +01:00
AlI 75f982e677 i18n(fa): refactor Farsi translation (#2330)
Restructure the Farsi translation to align with the format and key organization used in other translation files. This improves consistency and maintainability.
2025-10-26 08:58:57 +01:00
AlI f58bfeb1d0 fix(layout): Fix popup menu for RTL languages (#2327) 2025-10-26 08:51:25 +08:00
AlI ebb077f6e0 i18n: add translations for Persian/Farsi(fa), closes #2321 (#2326) 2025-10-25 15:32:24 +02:00
Huang Xin fdf6908fc6 feat: expand cloud storage with one-time payment (#2325) 2025-10-25 11:46:51 +02:00
Huang Xin 7936660868 docs: add sponsor links in README (#2318) 2025-10-24 15:22:08 +02:00
Huang Xin 554dd3798f fix: more explanatory error message when importing books, closes #2309 (#2314) 2025-10-24 07:15:41 +02:00
Huang Xin 7f40d8e12c fix(eink): more readability for progress info and section info in E-Ink mode, closes #2311 (#2313) 2025-10-24 06:14:25 +02:00
Huang Xin 31d6f44ff5 fix(iOS): restore last page if app is terminated in the background (#2312) 2025-10-24 05:42:52 +02:00
Huang Xin 86efcde927 feat(tts): add an option to select target language for TTS on translated books (#2310) 2025-10-23 19:34:43 +02:00
Huang Xin 4acbc33762 fonts: add PT font families (#2308) 2025-10-23 16:05:28 +02:00
Huang Xin 7cd3ebfcba compat: support .fb.zip files as FBZ format (#2307)
Although .fb.zip is not an officially recognized extension, many FBZ files use this format, so it's now treated as FBZ for compatibility.
2025-10-23 15:38:44 +02:00
Huang Xin 55c97cab7e perf: improve smoothness of Arrow Up/Down scrolling, closes #2080 (#2306) 2025-10-23 15:32:51 +02:00
Huang Xin 5bde091704 feat(settings): add an option to click both sides of the screen to paginate forward, closes #1951 (#2305) 2025-10-23 13:57:03 +02:00
Huang Xin 70489b46a5 fix(search): correctly display results when searching from selected text (#2304) 2025-10-23 11:52:49 +02:00
Huang Xin d7e89cc01f fix(layout): responsive layout for the custom highlight colors options (#2303) 2025-10-23 11:51:31 +02:00
Huang Xin a1f6030e75 fix(iOS): detect and recover from WebContent process termination (#2302) 2025-10-23 10:37:07 +02:00
Huang Xin e7b2d8435e feat(eink): set underline text decoration for links in E-Ink mode, closes #2293 (#2299) 2025-10-22 18:52:30 +02:00
Huang Xin f890a9633b feat(settings): add an option to set auto screen brightness (#2297) 2025-10-22 18:45:51 +02:00
Huang Xin 5bdcd3124b chore: bump tauri to version 2.9.0 (#2296) 2025-10-22 17:51:35 +02:00
Huang Xin 752b3f8e4c chore: bump nextjs, opennextjs and supabasejs (#2295) 2025-10-22 16:19:06 +02:00
Huang Xin 001e836ae3 chore: bump eslint-config-next to version 16 (#2294) 2025-10-22 15:57:04 +02:00
Huang Xin 50d7f9a9dd feat(android): support custom data location on external sdcard (#2292) 2025-10-22 10:14:41 +02:00
Huang Xin 34a5e58872 feat: add keyboard shortcuts to go prev/next sections, closes #2287 (#2291) 2025-10-22 07:41:33 +02:00
Huang Xin 34fbea6a44 css: properly handle theme color in dark mode for code blocks, closes #2281 (#2290) 2025-10-22 06:56:53 +02:00
Huang Xin 8737535b90 feat: support Android IAP (#2286) 2025-10-21 18:24:50 +02:00
Huang Xin 28ef414c3d feat: supported footnotes for definition list, closes #2278 (#2279) 2025-10-20 10:38:46 +02:00
dependabot[bot] 16279949c1 chore(deps): bump actions/setup-node in the github-actions group (#2277) 2025-10-20 12:25:42 +08:00
Huang Xin 444e22dd0e chore: i18n for highlight color settings (#2275) 2025-10-19 18:59:26 +02:00
仿生猫梦见苦力怕 a231506713 fix(css): support at rules of CSS and fix CSS formatter (#2269)
* refactor: replace manual CSS validator with PostCSS

* Revert "refactor: replace manual CSS validator with PostCSS"

This reverts commit 609ed7dd5a50debcea0303cc1043440af9849ddc.

* fix: support at rules of CSS and fix CSS formatter

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2025-10-19 18:09:04 +02:00
airwish 5dcabba73c chore: add contribution settings (#2274) 2025-10-19 17:14:20 +02:00
AlI cd71c494da feat(settings): add custom highlight color picker (#2273)
Add ability to customize highlight colors with hex color picker. Users can now set custom colors for all five highlight styles (red, violet, blue, green, yellow) in the settings panel.

Fixes #2271
2025-10-19 17:03:59 +02:00
Huang Xin f66642b8ec tts: avoid using AnaNeural as default English voice (#2272) 2025-10-19 14:58:51 +02:00
Huang Xin 2283e1b0f2 fix(layout): fix inline image, closes #2263 (#2266) 2025-10-18 17:25:37 +02:00
Daniil Leontev e0eb725d8b fix: add more elements to color overrides (#2265) 2025-10-18 22:25:02 +08:00
Huang Xin 05e9549410 release: version 0.9.88 (#2258) 2025-10-17 18:06:00 +02:00
Huang Xin 35fbce3506 feat(settings): added an option to disable animation in E-Ink mode, closes #2253 (#2257) 2025-10-17 18:00:58 +02:00
Huang Xin c1d6825793 fix: more sensitive snap to paginate, closes #2252 (#2256) 2025-10-17 16:05:39 +02:00
Huang Xin ae6c6970b4 feat: localize number in progress info in vertical layout (#2255) 2025-10-17 12:17:36 +02:00
Huang Xin f8fef57cf3 fix(layout): overriding book layout no longer overrides explicit text alignment, closes #2249 (#2254) 2025-10-17 11:44:32 +02:00
Huang Xin 4dc191735d fix(layout): prevent TTS indicator from disappearing too quickly to open configuration panel, closes #2247 (#2248) 2025-10-16 16:49:05 +02:00
Huang Xin 4f11f437e1 release: version 0.9.87 (#2245) 2025-10-16 10:27:34 +02:00
Huang Xin 5773e31990 compat(tts): fix crash with TTS on some versions of Android systems (#2244) 2025-10-16 10:10:04 +02:00
Huang Xin 9dda5d4c88 fix(tts): scroll more accurately to the highlighted text in TTS when header/footer bars are shown (#2242) 2025-10-15 17:50:56 +02:00
Huang Xin 7f5b6959e1 font: update font LXGW WenKai to the latest version (#2240) 2025-10-15 15:17:53 +02:00
Huang Xin 126981d085 fix(theme): fixed auto theme mode for new reader window (#2238) 2025-10-15 14:36:02 +02:00
Huang Xin 854a578929 compat: support parsing more footnotes, closes #2234 (#2237) 2025-10-15 11:59:27 +02:00
Huang Xin 6dd0daa597 txt: add chapter title pattern, closes #2232 (#2233) 2025-10-14 18:01:23 +02:00
Huang Xin fd24a5e9ac release: version 0.9.86 (#2230) 2025-10-14 14:08:45 +02:00
Huang Xin 948b35f244 perf: support concurrent book uploading, closes #2208 (#2229) 2025-10-14 14:00:31 +02:00
Huang Xin 96cc182a8c feat: support overriding html language code with language code in metadata, closes #2222 (#2227) 2025-10-14 13:09:23 +02:00
Huang Xin e2291ed5b1 feat(koplugin): register sync actions in gesture manager closes #2224 (#2226) 2025-10-14 09:54:08 +02:00
Huang Xin 60ddb17547 feat: add support for per-book background image settings in addition to global settings, also closes #2223 (#2225) 2025-10-14 08:54:26 +02:00
Huang Xin 9a991a31ba fix: handle empty custom textures (#2221) 2025-10-13 20:08:22 +02:00
Huang Xin 6de10c6c3b release: version 0.9.85 (#2220) 2025-10-13 19:15:55 +02:00
Huang Xin c7d49c1504 fix: load custom texture before applying it (#2219) 2025-10-13 19:04:45 +02:00
Huang Xin c721bd4d97 chore: enable turbopack by default in development env (#2218) 2025-10-13 16:39:17 +02:00
Huang Xin 5b0a1968d8 chore: add readest.koplugin in the release workflow (#2217) 2025-10-13 16:26:11 +02:00
Huang Xin ad9064fa46 fix(toc): fixed issues of scroll to active items in unpinned sidebar, closes #2213 (#2216) 2025-10-13 15:29:51 +02:00
Huang Xin f1c92e5702 feat: support customizing background images, closes #2018 (#2214) 2025-10-13 14:04:13 +02:00
Huang Xin 92c1441922 translator: add translator target language of Persian, closes #2206 (#2212) 2025-10-12 13:53:23 +02:00
Huang Xin 3820950c7c fix: show only the filtered books in groups (#2209) 2025-10-12 11:38:47 +02:00
Huang Xin 08e558bd41 fix: actually allow script in the transform target, closes #2200 (#2203) 2025-10-11 12:59:55 +02:00
Huang Xin 00af4a3d60 layout: make TTS icon less distracting during read-aloud mode (#2198) 2025-10-10 19:14:57 +02:00
Huang Xin ada427b134 feat(settings): add settings to adjust screen brightness when reading, closes #1532 (#2197) 2025-10-10 19:02:35 +02:00
Huang Xin f696b9a573 fix: handle scrolling/panning in zoomed in PDFs, closes #2184 (#2194) 2025-10-10 07:56:41 +02:00
Huang Xin 195beeacac chore: don't init posthog if users opt-out the usage data telemetry, closes #2187 (#2192) 2025-10-10 07:03:49 +02:00
Huang Xin 5b23ed35b6 txt: properly detect chapters in TXT files with dot-number format, closes #2188 (#2191) 2025-10-10 06:45:50 +02:00
Huang Xin 42c8702704 fix(cbz): fix issue where CBZ files fail to reopen after being closed, closes #2183 (#2190) 2025-10-10 05:50:40 +02:00
Huang Xin ad81376da8 fix(css): auto height of inline images with object-fit style (#2189) 2025-10-10 04:10:05 +02:00
Huang Xin 562fea3a36 fix(layout): fix inconsistent background color for unpinned drag bars (#2186) 2025-10-09 18:35:00 +02:00
Huang Xin b69d9ed69f tts: improve media session control compatibility across more Android systems (#2185) 2025-10-09 17:53:07 +02:00
Huang Xin ad6a21a68f refactor: split FooterBar into modular components for desktop and mobile (#2181) 2025-10-08 07:47:39 +02:00
Huang Xin 173aa0fcd1 layout: fix dragbar flickering when opening a book (#2177) 2025-10-06 05:52:36 +02:00
Huang Xin 788e08a75a shortcut: use shift+space to go backward in the reader page (#2176) 2025-10-06 05:31:16 +02:00
Huang Xin ec397a2daf fix: draw annotations of the current section on load, closes #2163 (#2174) 2025-10-05 17:47:34 +02:00
Huang Xin 2a5758fbc4 compat: disable customize root dir for macOS App Store builds (#2171)
CustomizeRootDir has a blocker on macOS App Store builds due to Security Scoped Resource restrictions.
See: https://github.com/tauri-apps/tauri/issues/3716
2025-10-05 08:28:45 +02:00
Huang Xin 562ccd4b9e feat: add keyboard shortcuts for toggling bookmarks/sidebar, importing books, closing window (#2170)
And make the shortcuts compatible with GNOME Human Interface Guidelines.
This closes #2079 and closes #2168.
2025-10-04 16:12:31 +02:00
make_aguess 4d577774f3 Adds adaptive icon for Android launcher (#2162)
Introduces an adaptive icon for the Android launcher, defining
separate layers for background, foreground, and monochrome
elements. Enhances visual consistency and supports adaptive
icon features on modern Android devices.
2025-10-04 09:00:33 +02:00
Huang Xin 4c10c16491 i18n: add translations for Swedish(sv) (#2159) 2025-10-02 14:25:01 +02:00
Huang Xin 7695f31a5a translator: translate to and from Norwegian Bokmål(nb) (#2158) 2025-10-02 14:09:01 +02:00
Huang Xin 06b27f8d39 chore: fix portable exe build for Windows (#2156) 2025-10-02 00:11:21 +08:00
291 changed files with 14998 additions and 4073 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
version: 10.14.0
- name: setup node
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
+26 -2
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: setup node
uses: actions/setup-node@v5
uses: actions/setup-node@v6
- name: get version
run: echo "PACKAGE_VERSION=$(node -p "require('./apps/readest-app/package.json').version")" >> $GITHUB_ENV
- name: get release
@@ -81,6 +81,28 @@ jobs:
prerelease: false
})
build-koreader-plugin:
needs: get-release
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: create KOReader plugin zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
version=${{ needs.get-release.outputs.release_version }}
plugin_zip="Readest-${version}-1.koplugin.zip"
cd apps/readest.koplugin
zip -r ../../${plugin_zip} .
cd ../..
echo "Uploading ${plugin_zip} to GitHub release"
gh release upload ${{ needs.get-release.outputs.release_tag }} ${plugin_zip} --clobber
build-tauri:
needs: get-release
permissions:
@@ -135,7 +157,7 @@ jobs:
version: 10.14.0
- name: setup node
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
@@ -292,6 +314,8 @@ jobs:
if: matrix.config.os == 'windows-latest'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
shell: bash
run: |
echo "Building Portable Binaries"
+8
View File
@@ -0,0 +1,8 @@
{
"recommendations": [
"ms-vscode.vscode-typescript-next",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"rust-lang.rust-analyzer"
]
}
Generated
+508 -552
View File
File diff suppressed because it is too large Load Diff
+7 -9
View File
@@ -261,17 +261,15 @@ Readest is open-source, and contributions are welcome! Feel free to open issues,
## Support
If Readest has been useful to you, consider supporting its development. Your contribution helps us squash bugs faster, improve performance, and keep building great features.
If Readest has been useful to you, consider supporting its development. You can [become a sponsor on GitHub](https://github.com/sponsors/readest) or just [donate with crypto](https://donate.readest.com). Your contribution helps us squash bugs faster, improve performance, and keep building great features.
### How to Donate
### Sponsors
1. **GitHub Sponsors**
Back the project directly on GitHub:
👉 [https://github.com/sponsors/readest](https://github.com/sponsors/readest)
2. **Crypto Donations**
Prefer crypto? You can donate here:
👉 [https://donate.readest.com/](https://donate.readest.com/)
<p align="center">
<a title="Browser testing via LambdaTest" href="https://www.lambdatest.com/?utm_source=readest&utm_medium=sponsor" target="_blank">
<img src="https://www.lambdatest.com/blue-logo.png" style="vertical-align: middle;" width="250" />
</a>
</p>
## License
+17 -21
View File
@@ -1,25 +1,21 @@
import path from 'node:path';
import js from '@eslint/js';
import { defineConfig, globalIgnores } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import { fileURLToPath } from 'node:url';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
...compat.extends('next/core-web-vitals', 'next/typescript'),
const eslintConfig = defineConfig([
...nextVitals,
{
plugins: {
'jsx-a11y': jsxA11y,
},
rules: {
...jsxA11y.configs.recommended.rules,
},
rules: jsxA11y.configs.recommended.rules,
},
];
globalIgnores([
'node_modules/**',
'.next/**',
'.open-next/**',
'out/**',
'build/**',
'public/**',
'next-env.d.ts',
]),
]);
export default eslintConfig;
@@ -12,12 +12,14 @@ module.exports = {
'de',
'ja',
'es',
'fa',
'fr',
'it',
'el',
'ko',
'uk',
'nl',
'sv',
'pl',
'pt',
'ru',
+28
View File
@@ -50,6 +50,15 @@ const nextConfig = {
},
],
},
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
};
@@ -57,6 +66,8 @@ const nextConfig = {
const withPWA = withPWAInit({
dest: 'public',
disable: isDev || appPlatform !== 'web',
cacheStartUrl: false,
dynamicStartUrl: false,
cacheOnFrontEndNav: true,
aggressiveFrontEndNavCaching: true,
reloadOnOnline: true,
@@ -66,6 +77,23 @@ const withPWA = withPWAInit({
},
workboxOptions: {
disableDevLogs: true,
manifestTransforms: [
(manifestEntries) => {
const manifest = manifestEntries.filter((entry) => {
const url = entry.url;
return (
!url.includes('dynamic-css-manifest.json') &&
!url.includes('middleware-manifest.json') &&
!url.includes('react-loadable-manifest.json') &&
!url.includes('build-manifest.json') &&
!url.includes('_buildManifest.js') &&
!url.includes('_ssgManifest.js') &&
!url.includes('_headers')
);
});
return { manifest };
},
],
},
});
+15 -12
View File
@@ -1,16 +1,16 @@
{
"name": "@readest/readest-app",
"version": "0.9.82",
"version": "0.9.93",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
"dev": "dotenv -e .env.tauri -- next dev --turbopack",
"build": "dotenv -e .env.tauri -- next build",
"start": "dotenv -e .env.tauri -- next start",
"dev-web": "dotenv -e .env.web -- next dev",
"dev-web": "dotenv -e .env.web -- next dev --turbopack",
"build-web": "dotenv -e .env.web -- next build",
"start-web": "dotenv -e .env.web -- next start",
"i18n:extract": "i18next-scanner",
"lint": "next lint",
"lint": "eslint .",
"test": "dotenv -e .env -e .env.test.local vitest",
"tauri": "tauri",
"prepare-public-vendor": "mkdirp ./public/vendor/pdfjs",
@@ -47,13 +47,13 @@
"@aws-sdk/s3-request-presigner": "^3.735.0",
"@ducanh2912/next-pwa": "^10.2.9",
"@fabianlars/tauri-plugin-oauth": "2",
"@opennextjs/cloudflare": "^1.6.5",
"@opennextjs/cloudflare": "^1.11.0",
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.4.0",
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.55.0",
"@tauri-apps/api": "2.8.0",
"@supabase/supabase-js": "^2.76.1",
"@tauri-apps/api": "2.9.0",
"@tauri-apps/plugin-cli": "^2.4.0",
"@tauri-apps/plugin-deep-link": "^2.4.3",
"@tauri-apps/plugin-dialog": "^2.4.0",
@@ -67,14 +67,17 @@
"@tauri-apps/plugin-shell": "~2.3.1",
"@tauri-apps/plugin-updater": "^2.9.0",
"@zip.js/zip.js": "^2.7.53",
"abortcontroller-polyfill": "^1.7.8",
"app-store-server-api": "^0.17.1",
"aws4fetch": "^1.0.20",
"clsx": "^2.1.1",
"cors": "^2.8.5",
"cssbeautify": "^0.3.1",
"dayjs": "^1.11.13",
"dompurify": "^3.3.0",
"foliate-js": "workspace:*",
"franc-min": "^6.2.0",
"google-auth-library": "^10.4.1",
"googleapis": "^164.1.0",
"highlight.js": "^11.11.1",
"i18next": "^24.2.0",
"i18next-browser-languagedetector": "^8.0.2",
@@ -84,7 +87,7 @@
"js-md5": "^0.8.3",
"jwt-decode": "^4.0.0",
"marked": "^15.0.12",
"next": "15.5.0",
"next": "15.5.6",
"overlayscrollbars": "^2.11.4",
"overlayscrollbars-react": "^0.5.6",
"posthog-js": "^1.246.0",
@@ -105,7 +108,7 @@
"devDependencies": {
"@next/bundle-analyzer": "^15.4.2",
"@tailwindcss/typography": "^0.5.16",
"@tauri-apps/cli": "2.8.4",
"@tauri-apps/cli": "2.9.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.3.0",
"@types/cors": "^2.8.17",
@@ -125,7 +128,7 @@
"daisyui": "^4.12.24",
"dotenv-cli": "^7.4.4",
"eslint": "^9.16.0",
"eslint-config-next": "15.0.3",
"eslint-config-next": "16.0.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"i18next-scanner": "^4.6.0",
"jsdom": "^26.1.0",
@@ -139,6 +142,6 @@
"typescript": "^5.7.2",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4",
"wrangler": "^4.31.0"
"wrangler": "^4.44.0"
}
}
-2
View File
@@ -1,2 +0,0 @@
/_next/static/*
Cache-Control: public,max-age=31536000,immutable
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

@@ -220,9 +220,9 @@
"Auto Import on File Open": "استيراد تلقائي عند فتح الملف",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "لم يتم التعرف على أي فصول.",
"Failed to parse the EPUB file.": "فشل في تحليل ملف EPUB.",
"This book format is not supported.": "تنسيق الكتاب هذا غير مدعوم.",
"No chapters detected": "لم يتم التعرف على أي فصول",
"Failed to parse the EPUB file": "فشل في تحليل ملف EPUB",
"This book format is not supported": "تنسيق الكتاب هذا غير مدعوم",
"Unable to fetch the translation. Please log in first and try again.": "تعذر جلب الترجمة. يرجى تسجيل الدخول أولاً ثم المحاولة مرة أخرى.",
"Group": "تجميع",
"Always on Top": "دائمًا في المقدمة",
@@ -259,8 +259,6 @@
"(from 'As You Like It', Act II)": "(من 'كما تحب'، الفصل الثاني)",
"Link Color": "لون الرابط",
"Volume Keys for Page Flip": "مفاتيح الصوت لتقليب الصفحات",
"Clicks for Page Flip": "نقرات لتقليب الصفحات",
"Swap Clicks Area": "تبديل منطقة النقرات",
"Screen": "الشاشة",
"Orientation": "الاتجاه",
"Portrait": "عمودي",
@@ -300,7 +298,6 @@
"Disable Translation": "تعطيل الترجمة",
"Scroll": "تمرير",
"Overlap Pixels": "تداخل البكسلات",
"Click": "نقر",
"System Language": "لغة النظام",
"Security": "الأمان",
"Allow JavaScript": "السماح بـ JavaScript",
@@ -381,7 +378,6 @@
"Email:": "البريد الإلكتروني:",
"Plan:": "الخطة:",
"Amount:": "المبلغ:",
"Subscription ID:": "رقم الاشتراك:",
"Go to Library": "الانتقال إلى المكتبة",
"Need help? Contact our support team at support@readest.com": "تحتاج مساعدة؟ تواصل مع فريق الدعم على support@readest.com",
"Free Plan": "الخطة المجانية",
@@ -615,5 +611,66 @@
"File count: {{size}}": "عدد الملفات: {{size}}",
"Background Read Aloud": "قراءة بصوت عالٍ في الخلفية",
"Ready to read aloud": "جاهز للقراءة بصوت عالٍ",
"Read Aloud": "القراءة بصوت عالٍ"
"Read Aloud": "القراءة بصوت عالٍ",
"Screen Brightness": "سطوع الشاشة",
"Background Image": "صورة الخلفية",
"Import Image": "استيراد صورة",
"Opacity": "شفافية",
"Size": "حجم",
"Cover": "غطاء",
"Contain": "احتواء",
"{{number}} pages left in chapter": "تبقّت {{number}} صفحات في هذا الفصل",
"Device": "الجهاز",
"E-Ink Mode": "وضع الحبر الإلكتروني",
"Highlight Colors": "ألوان التمييز",
"Auto Screen Brightness": "سطوع الشاشة التلقائي",
"Pagination": "التقسيم إلى صفحات",
"Disable Double Tap": "تعطيل النقر المزدوج",
"Tap to Paginate": "اضغط للتقسيم إلى صفحات",
"Click to Paginate": "انقر للتقسيم إلى صفحات",
"Tap Both Sides": "اضغط على كلا الجانبين",
"Click Both Sides": "انقر على كلا الجانبين",
"Swap Tap Sides": "تبديل جانبي اللمس",
"Swap Click Sides": "تبديل جانبي النقر",
"Source and Translated": "المصدر والمترجم",
"Translated Only": "المترجم فقط",
"Source Only": "المصدر فقط",
"TTS Text": "نص تحويل النص إلى كلام",
"The book file is corrupted": "ملف الكتاب تالف",
"The book file is empty": "ملف الكتاب فارغ",
"Failed to open the book file": "فشل في فتح ملف الكتاب",
"On-Demand Purchase": "الشراء عند الطلب",
"Full Customization": "تخصيص كامل",
"Lifetime Plan": "خطة مدى الحياة",
"One-Time Payment": "دفعة لمرة واحدة",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "قم بإجراء دفعة واحدة للاستمتاع بالوصول مدى الحياة إلى ميزات معينة على جميع الأجهزة. اشترِ ميزات أو خدمات معينة فقط عند الحاجة إليها.",
"Expand Cloud Sync Storage": "توسيع مساحة تخزين السحابة",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "قم بتوسيع مساحة التخزين السحابية الخاصة بك إلى الأبد من خلال عملية شراء لمرة واحدة. كل عملية شراء إضافية تضيف المزيد من المساحة.",
"Unlock All Customization Options": "فتح جميع خيارات التخصيص",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "فتح سمات إضافية، خطوط، خيارات تخطيط، وخدمات قراءة بصوت عالٍ، ومترجمين، وخدمات تخزين سحابية.",
"Purchase Successful!": "تمت عملية الشراء بنجاح!",
"lifetime": "مدى الحياة",
"Thank you for your purchase! Your payment has been processed successfully.": "شكرًا لشرائك! تم معالجة الدفع بنجاح.",
"Order ID:": "معرف الطلب:",
"TTS Highlighting": "تمييز تحويل النص إلى كلام",
"Style": "النمط",
"Underline": "تسطير",
"Strikethrough": "يتوسطه خط",
"Squiggly": "متعرج",
"Outline": "مخطط",
"Save Current Color": "حفظ اللون الحالي",
"Quick Colors": "ألوان سريعة",
"Highlighter": "محدد النص",
"Save Book Cover": "حفظ غلاف الكتاب",
"Auto-save last book cover": "حفظ غلاف الكتاب الأخير تلقائيًا",
"Back": "عودة",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "تم إرسال بريد التأكيد! يرجى التحقق من عناوين بريدك الإلكتروني القديمة والجديدة لتأكيد التغيير.",
"Failed to update email": "فشل في تحديث البريد الإلكتروني",
"New Email": "البريد الإلكتروني الجديد",
"Your new email": "بريدك الإلكتروني الجديد",
"Updating email ...": "جارٍ تحديث البريد الإلكتروني ...",
"Update email": "تحديث البريد الإلكتروني",
"Current email": "البريد الإلكتروني الحالي",
"Update Email": "تحديث البريد الإلكتروني",
"All": "الكل"
}
@@ -105,9 +105,9 @@
"Book Covers": "বইয়ের মলাট",
"Sort by...": "সাজানোর ভিত্তি...",
"No supported files found. Supported formats: {{formats}}": "কোন সমর্থিত ফাইল পাওয়া যায়নি। সমর্থিত ফরম্যাট: {{formats}}",
"No chapters detected.": "কোন অধ্যায় শনাক্ত হয়নি।",
"Failed to parse the EPUB file.": "EPUB ফাইল পার্স করতে ব্যর্থ।",
"This book format is not supported.": "এই বই ফরম্যাট সমর্থিত নয়।",
"No chapters detected": "কোন অধ্যায় শনাক্ত হয়নি।",
"Failed to parse the EPUB file": "EPUB ফাইল পার্স করতে ব্যর্থ।",
"This book format is not supported": "এই বই ফরম্যাট সমর্থিত নয়।",
"Failed to import book(s): {{filenames}}": "বই আমদানি ব্যর্থ: {{filenames}}",
"Book uploaded: {{title}}": "বই আপলোড হয়েছে: {{title}}",
"Insufficient storage quota": "অপর্যাপ্ত স্টোরেজ কোটা",
@@ -204,9 +204,6 @@
"Scrolled Mode": "স্ক্রল মোড",
"Continuous Scroll": "ক্রমাগত স্ক্রল",
"Overlap Pixels": "ওভারল্যাপ পিক্সেল",
"Click": "ক্লিক",
"Clicks for Page Flip": "পৃষ্ঠা উল্টানোর জন্য ক্লিক",
"Swap Clicks Area": "ক্লিক এরিয়া অদলবদল",
"Disable Double Click": "ডাবল ক্লিক নিষ্ক্রিয়",
"Volume Keys for Page Flip": "পৃষ্ঠা উল্টানোর জন্য ভলিউম কী",
"Animation": "অ্যানিমেশন",
@@ -379,7 +376,6 @@
"Email:": "ইমেইল:",
"Plan:": "প্ল্যান:",
"Amount:": "পরিমাণ:",
"Subscription ID:": "সাবস্ক্রিপশন ID:",
"Go to Library": "লাইব্রেরিতে যান",
"Need help? Contact our support team at support@readest.com": "সাহায্য দরকার? support@readest.com এ আমাদের সাপোর্ট টিমের সাথে যোগাযোগ করুন",
"Free Plan": "ফ্রি প্ল্যান",
@@ -599,5 +595,66 @@
"File count: {{size}}": "ফাইলের সংখ্যা: {{size}}",
"Background Read Aloud": "পটভূমিতে উচ্চস্বরে পড়ুন",
"Ready to read aloud": "উচ্চস্বরে পড়ার জন্য প্রস্তুত",
"Read Aloud": "উচ্চস্বরে পড়ুন"
"Read Aloud": "উচ্চস্বরে পড়ুন",
"Screen Brightness": "স্ক্রিন উজ্জ্বলতা",
"Background Image": "ব্যাকগ্রাউন্ড ছবি",
"Import Image": "ছবি আমদানি করুন",
"Opacity": "স্বচ্ছতা",
"Size": "আকার",
"Cover": "ঢাকা",
"Contain": "ধারণ",
"{{number}} pages left in chapter": "অধ্যায়ে {{number}} পৃষ্ঠা বাকি",
"Device": "যন্ত্র",
"E-Ink Mode": "ই-ইঙ্ক মোড",
"Highlight Colors": "হাইলাইট রঙ",
"Auto Screen Brightness": "স্বয়ংক্রিয় স্ক্রিন উজ্জ্বলতা",
"Pagination": "পৃষ্ঠা বিন্যাস",
"Disable Double Tap": "ডাবল ট্যাপ অক্ষম করুন",
"Tap to Paginate": "পৃষ্ঠায় যেতে ট্যাপ করুন",
"Click to Paginate": "পৃষ্ঠায় যেতে ক্লিক করুন",
"Tap Both Sides": "দুই পাশে ট্যাপ করুন",
"Click Both Sides": "দুই পাশে ক্লিক করুন",
"Swap Tap Sides": "ট্যাপের পাশে পরিবর্তন করুন",
"Swap Click Sides": "ক্লিকের পাশে পরিবর্তন করুন",
"Source and Translated": "উৎস এবং অনূদিত",
"Translated Only": "শুধুমাত্র অনূদিত",
"Source Only": "শুধুমাত্র উৎস",
"TTS Text": "TTS টেক্সট",
"The book file is corrupted": "বইয়ের ফাইলটি ক্ষতিগ্রস্ত।",
"The book file is empty": "বইয়ের ফাইলটি খালি।",
"Failed to open the book file": "বইয়ের ফাইল খুলতে ব্যর্থ।",
"On-Demand Purchase": "অনুরোধে ক্রয়",
"Full Customization": "সম্পূর্ণ কাস্টমাইজেশন",
"Lifetime Plan": "জীবনকাল পরিকল্পনা",
"One-Time Payment": "এককালীন পেমেন্ট",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "নির্দিষ্ট বৈশিষ্ট্যগুলিতে সমস্ত ডিভাইসে জীবনকাল অ্যাক্সেস উপভোগ করতে এককালীন পেমেন্ট করুন। যখন আপনার প্রয়োজন তখনই নির্দিষ্ট বৈশিষ্ট্য বা পরিষেবাগুলি কিনুন।",
"Expand Cloud Sync Storage": "ক্লাউড সিঙ্ক স্টোরেজ বাড়ান",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "এককালীন ক্রয়ের মাধ্যমে আপনার ক্লাউড স্টোরেজ চিরকাল বাড়ান। প্রতিটি অতিরিক্ত ক্রয় আরও স্থান যোগ করে।",
"Unlock All Customization Options": "সমস্ত কাস্টমাইজেশন বিকল্প আনলক করুন",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "অতিরিক্ত থিম, ফন্ট, লেআউট বিকল্প এবং উচ্চস্বরে পড়া, অনুবাদক, ক্লাউড স্টোরেজ পরিষেবাগুলি আনলক করুন।",
"Purchase Successful!": "ক্রয় সফল!",
"lifetime": "জীবনকাল",
"Thank you for your purchase! Your payment has been processed successfully.": "আপনার ক্রয়ের জন্য ধন্যবাদ! আপনার পেমেন্ট সফলভাবে প্রক্রিয়া করা হয়েছে।",
"Order ID:": "অর্ডার আইডি:",
"TTS Highlighting": "টিটিএস হাইলাইটিং",
"Style": "শৈলী",
"Underline": "আন্ডারলাইন",
"Strikethrough": "কাটা লেখা",
"Squiggly": "ঢেউখেলানো দাগ",
"Outline": "বাহিরের রেখা",
"Save Current Color": "বর্তমান রঙ সংরক্ষণ করুন",
"Quick Colors": "দ্রুত রংসমূহ",
"Highlighter": "হাইলাইটার",
"Save Book Cover": "বইয়ের মলাট সংরক্ষণ করুন",
"Auto-save last book cover": "শেষ বইয়ের মলাট স্বয়ংক্রিয়ভাবে সংরক্ষণ করুন",
"Back": "পেছনে",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "নিশ্চিতকরণ ইমেল পাঠানো হয়েছে! পরিবর্তন নিশ্চিত করতে আপনার পুরানো এবং নতুন ইমেল ঠিকানা পরীক্ষা করুন।",
"Failed to update email": "ইমেল আপডেট করতে ব্যর্থ",
"New Email": "নতুন ইমেল",
"Your new email": "আপনার নতুন ইমেল",
"Updating email ...": "ইমেল আপডেট হচ্ছে ...",
"Update email": "ইমেল আপডেট করুন",
"Current email": "বর্তমান ইমেল",
"Update Email": "ইমেল আপডেট করুন",
"All": "সব"
}
@@ -220,9 +220,9 @@
"Auto Import on File Open": "ཡིག་ཆ་ཁ་ཕྱེ་སྐབས་རང་འགུལ་གྱིས་ནང་འདྲེན།",
"LXGW WenKai GB Screen": "ཤ་ཝུ་ཝུན་ཁའེ།",
"LXGW WenKai TC": "ཤ་ཝུ་ཝུན་ཁའེ།",
"No chapters detected.": "ལེའུ་བརྟག་དཔྱད་བྱུང་མེད།",
"Failed to parse the EPUB file.": "EPUB ཡིག་ཆ་འགྲེལ་བཤད་བྱེད་ཐུབ་མེད།",
"This book format is not supported.": "དཔེ་དེབ་ཀྱི་རྣམ་པ་འདི་རྒྱབ་སྐྱོར་མི་བྱེད།",
"No chapters detected": "ལེའུ་བརྟག་དཔྱད་བྱུང་མེད།",
"Failed to parse the EPUB file": "EPUB ཡིག་ཆ་འགྲེལ་བཤད་བྱེད་ཐུབ་མེད།",
"This book format is not supported": "དཔེ་དེབ་ཀྱི་རྣམ་པ་འདི་རྒྱབ་སྐྱོར་མི་བྱེད།",
"Unable to fetch the translation. Please log in first and try again.": "སྒྱུར་བཅོས་ལེན་ཐུབ་མེད། ནང་བསྐྱོད་བྱས་རྗེས་ཡང་བསྐྱར་ཚོད་ལྟ་བྱེད་རོགས།",
"Group": "སྡེ་ཚན།",
"Always on Top": "སྒེའུ་ཁུང་གནས་སུ་བཀོད་པ།",
@@ -259,8 +259,6 @@
"(from 'As You Like It', Act II)": "《ཐམས་ཅད་དགའ་བ》ནས་དྲངས་པ། ལེའུ་གཉིས་པ།",
"Link Color": "སྦྲེལ་ཐག་གི་ཚོན་མདོག",
"Volume Keys for Page Flip": "སྒྲ་ཚད་ལྡེ་མིག་གིས་ཤོག་ལྷེ་བསྒྱུར་བ།",
"Clicks for Page Flip": "མནན་ནས་ཤོག་ལྷེ་བསྒྱུར་བ།",
"Swap Clicks Area": "མནན་སའི་ཁོངས་སུ་བརྗེ་བ།",
"Screen": "བརྡ་ཁྱབ་ངོས།",
"Orientation": "བརྡ་ཁྱབ་ངོས་ཀྱི་ཁ་ཕྱོགས།",
"Portrait": "ཀྲིང་ངོས།",
@@ -295,7 +293,6 @@
"Disable Translation": "སྒྱུར་བཅོས་སྒོ་བརྒྱབ།",
"Scroll": "འཁོར་རྒྱུག",
"Overlap Pixels": "ལྷན་པའི་པིག་སེལ།",
"Click": "མནན་པ།",
"System Language": "མ་ལག་གི་སྐད་ཡིག",
"Security": "བདེ་འཇགས།",
"Allow JavaScript": "JavaScript ཆོག་མཆན།",
@@ -361,7 +358,6 @@
"Email:": "གློག་རྡུལ་ཡིག་ཟམ།",
"Plan:": "ཐུམ་སྒྲིལ།",
"Amount:": "དངུལ་གྲངས།",
"Subscription ID:": "མཚམས་སྦྱོར་ ID །",
"Go to Library": "དཔེ་མཛོད་ལ་འགྲོ།",
"Need help? Contact our support team at support@readest.com": "རོགས་རམ་དགོས་སམ། རྒྱབ་སྐྱོར་ཚོགས་པར་འབྲེལ་བ་གནང་རོགས། support@readest.com",
"Free Plan": "རིན་མེད་ཐུམ་སྒྲིལ།",
@@ -595,5 +591,66 @@
"File count: {{size}}": "དེབ་གནས་བཅས་པའི་ཨང་། {{size}}",
"Background Read Aloud": "གནས་སྟངས་ཀྱིས་ཀློག་བྱེད།",
"Ready to read aloud": "དེབ་ཀྱི་སྤྱོད་བྱས་མ་ཐུབ།",
"Read Aloud": "ཀློག་བྱེད།"
"Read Aloud": "ཀློག་བྱེད།",
"Screen Brightness": "རྒྱབ་སྐོར་གྱི་འོད་ཟེར།",
"Background Image": "གནས་སྟངས་ཀྱི་རི་མོ།",
"Import Image": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
"Opacity": "སྤོ་སྐོར་གྱི་འོད་ཟེར།",
"Size": "ཨང་",
"Cover": "དེབ་གྱི་སྤྱོད་བྱས་མ་ཐུབ།",
"Contain": "འབྱོར་བ།",
"{{number}} pages left in chapter": "དོན་ཚན་ནང་ཤོག་ཨང་ {{number}} ལོག་གི་འདུག།",
"Device": "རྐྱབ་སྐོར",
"E-Ink Mode": "ཡིག་སྒྱུར་སྤོ་བ",
"Highlight Colors": "མདོག་ཚད་ཀྱི་བསྡུར་བ།",
"Auto Screen Brightness": "རྒྱབ་སྐོར་གྱི་འོད་ཟེར་རང་འགུལ།",
"Pagination": "ཤོག་གདོང་།",
"Disable Double Tap": "བརྒྱབ་གདོང་གཉིས་མ་འགྱོད།",
"Tap to Paginate": "ཤོག་གདོང་ལ་ཐོག་འགྲོ།",
"Click to Paginate": "ཤོག་གདོང་ལ་ཁྱེད་ལུས་འགྲོ།",
"Tap Both Sides": "གཡས་གཉིས་ལ་ཐོག་འགྲོ།",
"Click Both Sides": "གཡས་གཉིས་ལ་ཁྱེད་ལུས་འགྲོ།",
"Swap Tap Sides": "ཐོག་གཡས་གཉིས་བརྗེ་བ།",
"Swap Click Sides": "ཁྱེད་ལུས་གཡས་གཉིས་བརྗེ་བ།",
"Source and Translated": "ཡིག་སྒྱུར་སྤོ་བ།",
"Translated Only": "མདོག་ཚད་ཀྱི་བསྡུར་བ།",
"Source Only": "ཡིག་སྒྱུར་སྤོ་བ།",
"TTS Text": "TTS ཡིག",
"The book file is corrupted": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
"The book file is empty": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
"Failed to open the book file": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
"On-Demand Purchase": "অনুরোধে ক্রয়",
"Full Customization": "সম্পূর্ণ কাস্টমাইজেশন",
"Lifetime Plan": "জীবনকাল পরিকল্পনা",
"One-Time Payment": "এককালীন পেমেন্ট",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "নির্দিষ্ট বৈশিষ্ট্যগুলিতে সমস্ত ডিভাইসে জীবনকাল অ্যাক্সেস উপভোগ করতে এককালীন পেমেন্ট করুন। যখন আপনার প্রয়োজন তখনই নির্দিষ্ট বৈশিষ্ট্য বা পরিষেবাগুলি কিনুন।",
"Expand Cloud Sync Storage": "ক্লাউড সিঙ্ক স্টোরেজ বাড়ান",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "এককালীন ক্রয়ের মাধ্যমে আপনার ক্লাউড স্টোরেজ চিরকাল বাড়ান। প্রতিটি অতিরিক্ত ক্রয় আরও স্থান যোগ করে।",
"Unlock All Customization Options": "সমস্ত কাস্টমাইজেশন বিকল্প আনলক করুন",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "অতিরিক্ত থিম, ফন্ট, লেআউট বিকল্প এবং উচ্চস্বরে পড়া, অনুবাদক, ক্লাউড স্টোরেজ পরিষেবাগুলি আনলক করুন।",
"Purchase Successful!": "ক্রয় সফল!",
"lifetime": "জীবনকাল",
"Thank you for your purchase! Your payment has been processed successfully.": "আপনার ক্রয়ের জন্য ধন্যবাদ! আপনার পেমেন্ট সফলভাবে প্রক্রিয়াকৃত হয়েছে।",
"Order ID:": "অর্ডার আইডি:",
"TTS Highlighting": "ཡིག་འབྲུ་སྦྱོར་བའི་གསལ་བཤད།",
"Style": "བཟོ་ལྟ།",
"Underline": "འོག་ཐིག",
"Strikethrough": "འཐེན་ཐིག",
"Squiggly": "འཁྱོག་ཐིག",
"Outline": "མཐའ་ཐིག",
"Save Current Color": "མིག་སྔའི་ཚོན་གཏན་འཁེལ་བྱེད།",
"Quick Colors": "མྱུར་ཚོན།",
"Highlighter": "མདོག་ཚད་ཀྱི་བསྡུར་བ།",
"Save Book Cover": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
"Auto-save last book cover": "དེབ་གནས་བཅས་པའི་སྤྱོད་བྱས་མ་ཐུབ།",
"Back": "ལོག་བྱེད།",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "གཏན་འཁེལ་ཡིག་འཕྲིན་བཏང་ཡོད། ཡིག་འཕྲིན་རྣམས་གཉིས་ལ་བལྟས་ནས་བརྗེ་བ་དེ་གཏན་འཁེལ་བྱོས།",
"Failed to update email": "ཡིག་འཕྲིན་གསར་བསྒྱུར་ཕམ་པ།",
"New Email": "ཡིག་འཕྲིན་གསར་པ།",
"Your new email": "ཁྱེད་ཀྱི་ཡིག་འཕྲིན་གསར་པ།",
"Updating email ...": "ཡིག་འཕྲིན་གསར་བསྒྱུར་བཞིན་པ། ...",
"Update email": "ཡིག་འཕྲིན་གསར་བསྒྱུར།",
"Current email": "མིག་སྔའི་ཡིག་འཕྲིན།",
"Update Email": "ཡིག་འཕྲིན་གསར་བསྒྱུར།",
"All": "ཆེན་མོ།"
}
@@ -220,9 +220,9 @@
"Auto Import on File Open": "Automatischer Import beim Öffnen einer Datei",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "Keine Kapitel erkannt.",
"Failed to parse the EPUB file.": "Fehler beim Parsen der EPUB-Datei.",
"This book format is not supported.": "Dieses Buchformat wird nicht unterstützt.",
"No chapters detected": "Keine Kapitel erkannt",
"Failed to parse the EPUB file": "Fehler beim Parsen der EPUB-Datei",
"This book format is not supported": "Dieses Buchformat wird nicht unterstützt",
"Unable to fetch the translation. Please log in first and try again.": "Übersetzung konnte nicht abgerufen werden. Bitte zuerst anmelden und erneut versuchen.",
"Group": "Gruppieren",
"Always on Top": "Immer im Vordergrund",
@@ -259,8 +259,6 @@
"(from 'As You Like It', Act II)": "(aus 'Wie es euch gefällt', Akt II)",
"Link Color": "Linkfarbe",
"Volume Keys for Page Flip": "Volumen-Tasten für Seitenwechsel",
"Clicks for Page Flip": "Tippen zum Blättern",
"Swap Clicks Area": "Tippbereiche tauschen",
"Screen": "Bildschirm",
"Orientation": "Orientierung",
"Portrait": "Hochformat",
@@ -296,7 +294,6 @@
"Disable Translation": "Übersetzung deaktivieren",
"Scroll": "Scrollen",
"Overlap Pixels": "Überlappungspixel",
"Click": "Tippen",
"System Language": "Systemsprache",
"Security": "Sicherheit",
"Allow JavaScript": "JavaScript zulassen",
@@ -365,7 +362,6 @@
"Email:": "E-Mail:",
"Plan:": "Tarif:",
"Amount:": "Betrag:",
"Subscription ID:": "Abo-ID:",
"Go to Library": "Zur Bibliothek",
"Need help? Contact our support team at support@readest.com": "Brauchen Sie Hilfe? Kontaktieren Sie unser Support-Team unter support@readest.com",
"Free Plan": "Gratis-Tarif",
@@ -599,5 +595,66 @@
"File count: {{size}}": "Dateianzahl: {{size}}",
"Background Read Aloud": "Hintergrund-Vorlesen",
"Ready to read aloud": "Bereit zum Vorlesen",
"Read Aloud": "Vorlesen"
"Read Aloud": "Vorlesen",
"Screen Brightness": "Bildschirmhelligkeit",
"Background Image": "Hintergrundbild",
"Import Image": "Bild importieren",
"Opacity": "Opazität",
"Size": "Größe",
"Cover": "Cover",
"Contain": "Inhalt",
"{{number}} pages left in chapter": "{{number}} Seiten verbleibend im Kapitel",
"Device": "Gerät",
"E-Ink Mode": "E-Ink-Modus",
"Highlight Colors": "Hervorhebungsfarben",
"Auto Screen Brightness": "Automatische Bildschirmhelligkeit",
"Pagination": "Seitenumbruch",
"Disable Double Tap": "Doppeltippen deaktivieren",
"Tap to Paginate": "Tippen zum Blättern",
"Click to Paginate": "Klicken zum Blättern",
"Tap Both Sides": "Tippen Sie auf beide Seiten",
"Click Both Sides": "Klicken Sie auf beide Seiten",
"Swap Tap Sides": "Seiten für das Tippen tauschen",
"Swap Click Sides": "Seiten für das Klicken tauschen",
"Source and Translated": "Quell- und Übersetzungstext",
"Translated Only": "Nur Übersetzungstext",
"Source Only": "Nur Quelltext",
"TTS Text": "TTS-Text",
"The book file is corrupted": "Die Buchdatei ist beschädigt",
"The book file is empty": "Die Buchdatei ist leer",
"Failed to open the book file": "Fehler beim Öffnen der Buchdatei",
"On-Demand Purchase": "Kauf auf Anfrage",
"Full Customization": "Vollständige Anpassung",
"Lifetime Plan": "Lebenslange Planung",
"One-Time Payment": "Einmalige Zahlung",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Zahlen Sie einmalig, um lebenslangen Zugriff auf bestimmte Funktionen auf allen Geräten zu genießen. Kaufen Sie bestimmte Funktionen oder Dienstleistungen nur, wenn Sie sie benötigen.",
"Expand Cloud Sync Storage": "Cloud-Synchronisierungs-Speicher erweitern",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Erweitern Sie Ihren Cloud-Speicher für immer mit einem einmaligen Kauf. Jeder zusätzliche Kauf fügt mehr Speicherplatz hinzu.",
"Unlock All Customization Options": "Alle Anpassungsoptionen freischalten",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Zusätzliche Themen, Schriftarten, Layoutoptionen und Vorlesefunktionen, Übersetzer, Cloud-Speicherdienste freischalten.",
"Purchase Successful!": "Kauf erfolgreich!",
"lifetime": "lebenslang",
"Thank you for your purchase! Your payment has been processed successfully.": "Danke für Ihren Kauf! Ihre Zahlung wurde erfolgreich verarbeitet.",
"Order ID:": "Bestell-ID:",
"TTS Highlighting": "TTS-Hervorhebung",
"Style": "Stil",
"Underline": "Unterstreichen",
"Strikethrough": "Durchgestrichen",
"Squiggly": "Wellig",
"Outline": "Umriss",
"Save Current Color": "Aktuelle Farbe speichern",
"Quick Colors": "Schnellfarben",
"Highlighter": "Textmarker",
"Save Book Cover": "Buchcover speichern",
"Auto-save last book cover": "Letztes Buchcover automatisch speichern",
"Back": "Zurück",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Bestätigungs-E-Mail gesendet! Bitte überprüfe deine alte und neue E-Mail-Adresse, um die Änderung zu bestätigen.",
"Failed to update email": "E-Mail konnte nicht aktualisiert werden",
"New Email": "Neue E-Mail",
"Your new email": "Deine neue E-Mail-Adresse",
"Updating email ...": "E-Mail wird aktualisiert ...",
"Update email": "E-Mail aktualisieren",
"Current email": "Aktuelle E-Mail",
"Update Email": "E-Mail aktualisieren",
"All": "Alle"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "Αυτόματη εισαγωγή κατά το άνοιγμα αρχείου",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "Δεν εντοπίστηκαν κεφάλαια.",
"Failed to parse the EPUB file.": "Αποτυχία ανάλυσης του αρχείου EPUB.",
"This book format is not supported.": "Αυτή η μορφή βιβλίου δεν υποστηρίζεται.",
"No chapters detected": "Δεν εντοπίστηκαν κεφάλαια",
"Failed to parse the EPUB file": "Αποτυχία ανάλυσης του αρχείου EPUB",
"This book format is not supported": "Αυτή η μορφή βιβλίου δεν υποστηρίζεται",
"Unable to fetch the translation. Please log in first and try again.": "Αδυναμία λήψης της μετάφρασης. Παρακαλώ συνδεθείτε πρώτα και δοκιμάστε ξανά.",
"Group": "Ομάδα",
"Always on Top": "Πάντα στην κορυφή",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "('Όπως σας αρέσει', Πράξη II)",
"Link Color": "Χρώμα συνδέσμου",
"Volume Keys for Page Flip": "Ανατροπή με πλήκτρα έντασης",
"Clicks for Page Flip": "Κλικ για ανατροπή σελίδας",
"Swap Clicks Area": "Ανταλλαγή περιοχής κλικ",
"Screen": "Οθόνη",
"Orientation": "Προσανατολισμός",
"Portrait": "Πορτραίτο",
@@ -297,7 +295,6 @@
"Disable Translation": "Απενεργοποίηση μετάφρασης",
"Scroll": "Κύλιση",
"Overlap Pixels": "Επικάλυψη εικονοστοιχείων",
"Click": "Κλικ",
"System Language": "Γλώσσα συστήματος",
"Security": "Ασφάλεια",
"Allow JavaScript": "Επιτρέψτε JavaScript",
@@ -366,7 +363,6 @@
"Email:": "Email:",
"Plan:": "Πρόγραμμα:",
"Amount:": "Ποσό:",
"Subscription ID:": "ID συνδρομής:",
"Go to Library": "Μετάβαση στη βιβλιοθήκη",
"Need help? Contact our support team at support@readest.com": "Χρειάζεστε βοήθεια; Επικοινωνήστε με την υποστήριξη στο support@readest.com",
"Free Plan": "Δωρεάν πρόγραμμα",
@@ -599,5 +595,66 @@
"File count: {{size}}": "Αριθμός αρχείων: {{size}}",
"Background Read Aloud": "Ανάγνωση στο παρασκήνιο",
"Ready to read aloud": "Έτοιμο για ανάγνωση",
"Read Aloud": "Ανάγνωση"
"Read Aloud": "Ανάγνωση",
"Screen Brightness": "Φωτεινότητα οθόνης",
"Background Image": "Εικόνα φόντου",
"Import Image": "Εισαγωγή εικόνας",
"Opacity": "Αδιαφάνεια",
"Size": "Μέγεθος",
"Cover": "Εξώφυλλο",
"Contain": "Περιέχει",
"{{number}} pages left in chapter": "Μένουν {{number}} σελίδες στο κεφάλαιο",
"Device": "Συσκευή",
"E-Ink Mode": "Λειτουργία E-Ink",
"Highlight Colors": "Χρώματα επισήμανσης",
"Auto Screen Brightness": "Αυτόματη φωτεινότητα οθόνης",
"Pagination": "Σελιδοποίηση",
"Disable Double Tap": "Απενεργοποίηση διπλού ταπ",
"Tap to Paginate": "Ταπ για σελιδοποίηση",
"Click to Paginate": "Κλικ για σελιδοποίηση",
"Tap Both Sides": "Ταπ και στις δύο πλευρές",
"Click Both Sides": "Κλικ και στις δύο πλευρές",
"Swap Tap Sides": "Ανταλλαγή πλευρών ταπ",
"Swap Click Sides": "Ανταλλαγή πλευρών κλικ",
"Source and Translated": "Πηγή και Μεταφρασμένο",
"Translated Only": "Μόνο Μεταφρασμένο",
"Source Only": "Μόνο πηγή",
"TTS Text": "Κείμενο TTS",
"The book file is corrupted": "Το αρχείο του βιβλίου είναι κατεστραμμένο",
"The book file is empty": "Το αρχείο του βιβλίου είναι κενό",
"Failed to open the book file": "Αποτυχία ανοίγματος του αρχείου βιβλίου",
"On-Demand Purchase": "Αγορά κατόπιν αιτήματος",
"Full Customization": "Πλήρης προσαρμογή",
"Lifetime Plan": "Σχέδιο ζωής",
"One-Time Payment": "Εφάπαξ πληρωμή",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Κάντε μια εφάπαξ πληρωμή για να απολαύσετε διαρκή πρόσβαση σε συγκεκριμένες δυνατότητες σε όλες τις συσκευές. Αγοράστε συγκεκριμένες δυνατότητες ή υπηρεσίες μόνο όταν τις χρειάζεστε.",
"Expand Cloud Sync Storage": "Επέκταση αποθήκευσης συγχρονισμού Cloud",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Επεκτείνετε την αποθήκευση cloud σας για πάντα με μια εφάπαξ αγορά. Κάθε επιπλέον αγορά προσθέτει περισσότερο χώρο.",
"Unlock All Customization Options": "Ξεκλειδώστε όλες τις επιλογές προσαρμογής",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Ξεκλειδώστε επιπλέον θέματα, γραμματοσειρές, επιλογές διάταξης και δυνατότητες ανάγνωσης, μεταφραστές, υπηρεσίες αποθήκευσης cloud.",
"Purchase Successful!": "Η αγορά ήταν επιτυχής!",
"lifetime": "διαρκής",
"Thank you for your purchase! Your payment has been processed successfully.": "Σας ευχαριστούμε για την αγορά σας! Η πληρωμή σας έχει επεξεργαστεί με επιτυχία.",
"Order ID:": "Αριθμός παραγγελίας:",
"TTS Highlighting": "Επισήμανση TTS",
"Style": "Στυλ",
"Underline": "Υπογράμμιση",
"Strikethrough": "Διαγραμμένο",
"Squiggly": "Κυματιστό",
"Outline": "Περίγραμμα",
"Save Current Color": "Αποθήκευση τρέχουσας χρώματος",
"Quick Colors": "Γρήγορα χρώματα",
"Highlighter": "Υπογραμμιστής",
"Save Book Cover": "Αποθήκευση εξωφύλλου βιβλίου",
"Auto-save last book cover": "Αυτόματη αποθήκευση τελευταίου εξωφύλλου βιβλίου",
"Back": "Πίσω",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Το email επιβεβαίωσης στάλθηκε! Παρακαλώ έλεγξε την παλιά και τη νέα σου διεύθυνση email για να επιβεβαιώσεις την αλλαγή.",
"Failed to update email": "Αποτυχία ενημέρωσης email",
"New Email": "Νέο email",
"Your new email": "Το νέο σου email",
"Updating email ...": "Ενημέρωση email ...",
"Update email": "Ενημέρωση email",
"Current email": "Τρέχον email",
"Update Email": "Ενημέρωση email",
"All": "Όλα"
}
@@ -248,9 +248,9 @@
"Auto Import on File Open": "Importación automática al abrir archivo",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "No se detectaron capítulos.",
"Failed to parse the EPUB file.": "Error al analizar el archivo EPUB.",
"This book format is not supported.": "Este formato de libro no es compatible.",
"No chapters detected": "No se detectaron capítulos",
"Failed to parse the EPUB file": "Error al analizar el archivo EPUB",
"This book format is not supported": "Este formato de libro no es compatible",
"Unable to fetch the translation. Please log in first and try again.": "No se puede obtener la traducción. Por favor, inicia sesión primero e inténtalo de nuevo.",
"Group": "Grupo",
"Always on Top": "Siempre en la parte superior",
@@ -287,8 +287,6 @@
"(from 'As You Like It', Act II)": "(de 'Como gustéis', Acto II)",
"Link Color": "Color del enlace",
"Volume Keys for Page Flip": "Botones de volumen para pasar página",
"Clicks for Page Flip": "Toques para pasar página",
"Swap Clicks Area": "Invertir zonas de toque",
"Screen": "Pantalla",
"Orientation": "Orientación",
"Portrait": "Vertical",
@@ -325,7 +323,6 @@
"Disable Translation": "Deshabilitar traducción",
"Scroll": "Desplazar",
"Overlap Pixels": "Superponer píxeles",
"Click": "Click",
"System Language": "Idioma del sistema",
"Security": "Seguridad",
"Allow JavaScript": "Permitir JavaScript",
@@ -396,7 +393,6 @@
"Email:": "Correo electrónico:",
"Plan:": "Plan:",
"Amount:": "Importe:",
"Subscription ID:": "ID de suscripción:",
"Go to Library": "Ir a la biblioteca",
"Need help? Contact our support team at support@readest.com": "¿Necesitas ayuda? Contacta con nuestro equipo de soporte en support@readest.com",
"Free Plan": "Plan gratuito",
@@ -603,5 +599,66 @@
"File count: {{size}}": "Número de archivos: {{size}}",
"Background Read Aloud": "Lectura en segundo plano",
"Ready to read aloud": "Listo para leer en voz alta",
"Read Aloud": "Leer en voz alta"
"Read Aloud": "Leer en voz alta",
"Screen Brightness": "Brillo de pantalla",
"Background Image": "Imagen de fondo",
"Import Image": "Importar imagen",
"Opacity": "Opacidad",
"Size": "Tamaño",
"Cover": "Cubierta",
"Contain": "Contener",
"{{number}} pages left in chapter": "{{number}} páginas restantes en el capítulo",
"Device": "Dispositivo",
"E-Ink Mode": "Modo E-Ink",
"Highlight Colors": "Colores de resaltado",
"Auto Screen Brightness": "Brillo automático de pantalla",
"Pagination": "Paginación",
"Disable Double Tap": "Deshabilitar Doble Toque",
"Tap to Paginate": "Tocar para Paginación",
"Click to Paginate": "Hacer Clic para Paginación",
"Tap Both Sides": "Tocar Ambos Lados",
"Click Both Sides": "Hacer Clic en Ambos Lados",
"Swap Tap Sides": "Intercambiar Lados de Toque",
"Swap Click Sides": "Intercambiar Lados de Clic",
"Source and Translated": "Fuente y Traducido",
"Translated Only": "Solo Traducido",
"Source Only": "Solo Fuente",
"TTS Text": "TTS Texto",
"The book file is corrupted": "El archivo del libro está dañado",
"The book file is empty": "El archivo del libro está vacío",
"Failed to open the book file": "Error al abrir el archivo del libro",
"On-Demand Purchase": "Compra bajo demanda",
"Full Customization": "Personalización Completa",
"Lifetime Plan": "Plan de por vida",
"One-Time Payment": "Pago Único",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Realiza un solo pago para disfrutar de acceso de por vida a funciones específicas en todos los dispositivos. Compra funciones o servicios específicos solo cuando los necesites.",
"Expand Cloud Sync Storage": "Expandir Almacenamiento de Sincronización en la Nube",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Expande tu almacenamiento en la nube para siempre con una compra única. Cada compra adicional agrega más espacio.",
"Unlock All Customization Options": "Desbloquear Todas las Opciones de Personalización",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Desbloquear temas adicionales, fuentes, opciones de diseño y funciones de lectura en voz alta, traductores, servicios de almacenamiento en la nube.",
"Purchase Successful!": "¡Compra Exitosa!",
"lifetime": "de por vida",
"Thank you for your purchase! Your payment has been processed successfully.": "¡Gracias por tu compra! Tu pago se ha procesado correctamente.",
"Order ID:": "ID de pedido:",
"TTS Highlighting": "TTS Resaltado",
"Style": "Estilo",
"Underline": "Subrayado",
"Strikethrough": "Tachado",
"Squiggly": "Ondulado",
"Outline": "Contorno",
"Save Current Color": "Guardar Color Actual",
"Quick Colors": "Colores Rápidos",
"Highlighter": "Resaltador",
"Save Book Cover": "Guardar Portada del Libro",
"Auto-save last book cover": "Guardar automáticamente la última portada del libro",
"Back": "Atrás",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "¡Correo de confirmación enviado! Por favor, revisa tus direcciones de correo electrónico antigua y nueva para confirmar el cambio.",
"Failed to update email": "Error al actualizar el correo electrónico",
"New Email": "Nuevo correo electrónico",
"Your new email": "Tu nuevo correo electrónico",
"Updating email ...": "Actualizando correo electrónico ...",
"Update email": "Actualizar correo electrónico",
"Current email": "Correo electrónico actual",
"Update Email": "Actualizar correo electrónico",
"All": "Todos"
}
@@ -0,0 +1,660 @@
{
"(detected)": "(تشخیص‌داده‌شده)",
"About Readest": "درباره‌ی Readest",
"Add your notes here...": "یادداشت‌های خود را اینجا اضافه کنید...",
"Animation": "انیمیشن",
"Annotate": "حاشیه‌نویسی",
"Apply": "اِعمال",
"Auto Mode": "حالت خودکار",
"Behavior": "رفتار",
"Book": "کتاب",
"Book Cover": "جلد کتاب",
"Bookmark": "نشانک",
"Cancel": "لغو",
"Chapter": "فصل",
"Cherry": "گیلاسی",
"Color": "رنگ",
"Confirm": "تأیید",
"Confirm Deletion": "تأیید حذف",
"Copied to notebook": "به دفترچه یادداشت کپی شد",
"Copy": "کپی",
"Dark Mode": "حالت تاریک",
"Default": "پیش‌فرض",
"Default Font": "فونت پیش‌فرض",
"Default Font Size": "اندازه فونت پیش‌فرض",
"Delete": "حذف",
"Delete Highlight": "حذف هایلایت",
"Dictionary": "فرهنگ لغت",
"Download Readest": "دانلود Readest",
"Edit": "ویرایش",
"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": "رفتن به جلو",
"Grass": "چمنی",
"Gray": "خاکستری",
"Gruvbox": "Gruvbox",
"Highlight": "هایلایت",
"Horizontal Direction": "جهت افقی",
"Hyphenation": "تقسیم‌بندی کلمات",
"Import Books": "وارد کردن کتاب‌ها",
"Layout": "چیدمان",
"Light Mode": "حالت روشن",
"Loading...": "در حال بارگیری...",
"Logged in": "وارد شده‌اید",
"Logged in as {{userDisplayName}}": "شما با نام «{{userDisplayName}}» وارد شده‌اید",
"Match Case": "تطابق حروف بزرگ/کوچک",
"Match Diacritics": "تطابق اعراب",
"Match Whole Words": "تطابق کل کلمات",
"Maximum Number of Columns": "حداکثر تعداد ستون‌ها",
"Minimum Font Size": "حداقل اندازه فونت",
"Monospace Font": "فونت تک‌فاصله (Monospace)",
"More Info": "اطلاعات بیشتر",
"Nord": "Nord",
"Notebook": "دفترچه یادداشت",
"Notes": "یادداشت‌ها",
"Open": "باز کردن",
"Original Text": "متن اصلی",
"Page": "صفحه",
"Paging Animation": "انیمیشن ورق زدن",
"Paragraph": "پاراگراف",
"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": "انتخاب کتاب‌ها",
"Sepia": "سپیا",
"Serif Font": "فونت Serif",
"Show Book Details": "نمایش جزئیات کتاب",
"Sidebar": "نوار کناری",
"Sign In": "ورود",
"Sign Out": "خروج",
"Sky": "آسمانی",
"Slow": "آهسته",
"Solarized": "آفتابی",
"Speak": "خواندن",
"Subjects": "موضوعات",
"System Fonts": "فونت‌های سیستم",
"Theme Color": "رنگ تم",
"Theme Mode": "حالت تم",
"Translate": "ترجمه",
"Translated Text": "متن ترجمه‌شده",
"Unknown": "ناشناخته",
"Untitled": "بدون عنوان",
"Updated": "به‌روزرسانی شده",
"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": "جایگزینی فونت کتاب",
"Apply to All Books": "اِعمال برای همه‌ی کتاب‌ها",
"Apply to This Book": "اِعمال برای این کتاب",
"Unable to fetch the translation. Try again later.": "نمی‌توان ترجمه را دریافت کرد. بعداً دوباره تلاش کنید.",
"Check Update": "بررسی به‌روزرسانی",
"Already the latest version": "آخرین نسخه نصب است.",
"Book Details": "جزئیات کتاب",
"From Local File": "از فایل محلی",
"TOC": "فهرست مطالب",
"Table of Contents": "فهرست مطالب",
"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": "بارگذاری خودکار کتاب‌ها در فضای ابری",
"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": "جایگزینی چیدمان کتاب",
"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...": "در حال ثبت‌نام...",
"Don't 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": "ورود با گوگل",
"Sign in with Apple": "ورود با اپل",
"Sign in with GitHub": "ورود با گیت‌هاب",
"Account": "حساب",
"Failed to delete user. Please try again later.": "حذف کاربر ناموفق بود. لطفاً بعداً دوباره تلاش کنید.",
"Community Support": "پشتیبانی جامعه",
"Priority Support": "پشتیبانی اولویت‌دار",
"Loading profile...": "در حال بارگذاری پروفایل...",
"Delete Account": "حذف حساب",
"Delete Your Account?": "حذف حساب کاربری؟",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "این عمل غیرقابل بازگشت است. تمام داده‌های شما در فضای ابری به‌طور دائمی حذف خواهند شد.",
"Delete Permanently": "حذف دائمی",
"RTL Direction": "جهت راست‌به‌چپ",
"Maximum Column Height": "حداکثر ارتفاع ستون",
"Maximum Column Width": "حداکثر عرض ستون",
"Continuous Scroll": "پیمایش مداوم",
"Fullscreen": "تمام‌صفحه",
"No supported files found. Supported formats: {{formats}}": "فایل پشتیبانی‌شده‌ای یافت نشد. فرمت‌های قابل پشتیبانی: {{formats}}",
"Drop to Import Books": "برای وارد کردن کتاب‌ها، فایل را رها کنید",
"Custom": "سفارشی",
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "تمام دنیا صحنه‌ای است،\nو همه‌ی مردان و زنان تنها بازیگرانی بیش نیستند؛\nآن‌ها ورود و خروج خود را دارند،\nو هر آدمی در طول زمان نقش‌های بسیاری ایفا می‌کند،\nاعمال او هفت دوره‌اند.\n\n— ویلیام شکسپیر",
"Custom Theme": "تم سفارشی",
"Theme Name": "نام تم",
"Text Color": "رنگ متن",
"Background Color": "رنگ پس‌زمینه",
"Preview": "پیش‌نمایش",
"Contrast": "کنتراست",
"Sunset": "غروب",
"Double Border": "حاشیه‌ی دوبل",
"Border Color": "رنگ حاشیه",
"Border Frame": "قاب حاشیه",
"Show Header": "نمایش سربرگ",
"Show Footer": "نمایش پاورقی",
"Small": "کوچک",
"Large": "بزرگ",
"Auto": "خودکار",
"Language": "زبان",
"No annotations to export": "هیچ حاشیه‌نویسی‌ای برای خروجی‌گرفتن وجود ندارد",
"Author": "نویسنده",
"Exported from Readest": "خروجی‌گرفته‌شده از Readest",
"Highlights & Annotations": "هایلایت‌ها و حاشیه‌نویسی‌ها",
"Note": "یادداشت",
"Copied to clipboard": "به کلیپ‌بورد کپی شد",
"Export Annotations": "خروجی‌گرفتن از حاشیه‌نویسی‌ها",
"Auto Import on File Open": "وارد کردن خودکار هنگام باز کردن فایل",
"LXGW WenKai GB Screen": "LXGW WenKai GB Screen",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected": "هیچ فصلی یافت نشد",
"Failed to parse the EPUB file": "خطا در پردازش فایل EPUB",
"This book format is not supported": "این فرمت کتاب پشتیبانی نمی‌شود",
"Unable to fetch the translation. Please log in first and try again.": "نمی‌توان ترجمه را دریافت کرد. لطفاً ابتدا وارد شوید و دوباره تلاش کنید.",
"Group": "گروه",
"Always on Top": "همیشه در بالاترین لایه پنجره‌ها",
"No Timeout": "بدون محدودیت زمانی",
"{{value}} minute": "{{value}} دقیقه",
"{{value}} minutes": "{{value}} دقیقه",
"{{value}} hour": "{{value}} ساعت",
"{{value}} hours": "{{value}} ساعت",
"CJK Font": "فونت CJK",
"Clear Search": "پاک کردن جستجو",
"Header & Footer": "سربرگ و پاورقی",
"Apply also in Scrolled Mode": "همچنین در حالت پیمایشی اِعمال شود",
"A new version of Readest is available!": "نسخه‌ی جدیدی از Readest در دسترس است!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "نسخه‌ی {{newVersion}} Readest در دسترس است (نسخه‌ی نصب‌شده: {{currentVersion}}).",
"Download and install now?": "الان دانلود و نصب شود؟",
"Downloading {{downloaded}} of {{contentLength}}": "در حال دانلود {{downloaded}} از {{contentLength}}",
"Download finished": "دانلود تکمیل شد",
"DOWNLOAD & INSTALL": "دانلود و نصب",
"Changelog": "لیست تغییرات",
"Software Update": "به‌روزرسانی نرم‌افزار",
"Title": "عنوان",
"Date Read": "تاریخ مطالعه",
"Date Added": "تاریخ اضافه‌شدن",
"Format": "نوع فرمت",
"Ascending": "صعودی",
"Descending": "نزولی",
"Sort by...": "مرتب‌سازی بر اساس...",
"Added": "افزوده شده",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Description": "توضیحات",
"No description available": "هیچ توضیحی موجود نیست",
"List": "لیست",
"Grid": "جدول",
"(from 'As You Like It', Act II)": "(از «همان‌طور که دوست دارید»، پرده‌ی دوم)",
"Link Color": "رنگ لینک",
"Volume Keys for Page Flip": "استفاده از کلیدهای صدا برای ورق زدن صفحه",
"Screen": "صفحه",
"Orientation": "جهت",
"Portrait": "عمودی",
"Landscape": "افقی",
"Open Last Book on Start": "باز کردن آخرین کتاب هنگام شروع برنامه",
"Checking for updates...": "در حال بررسی به‌روزرسانی‌ها...",
"Error checking for updates": "خطا در بررسی به‌روزرسانی‌ها",
"Details": "جزئیات",
"File Size": "حجم فایل",
"Auto Detect": "تشخیص خودکار",
"Next Section": "بخش بعدی",
"Previous Section": "بخش قبلی",
"Next Page": "صفحه‌ی بعد",
"Previous Page": "صفحه‌ی قبل",
"Are you sure to delete {{count}} selected book(s)?_one": "آیا از حذف {{count}} کتاب انتخاب‌شده مطمئن هستید؟",
"Are you sure to delete {{count}} selected book(s)?_other": "آیا از حذف {{count}} کتاب انتخاب‌شده مطمئن هستید؟",
"Are you sure to delete the selected book?": "آیا از حذف کتاب انتخاب‌شده مطمئن هستید؟",
"Deselect": "لغو انتخاب",
"Select All": "انتخاب همه",
"No translation available.": "هیچ ترجمه‌ای در دسترس نیست.",
"Translated by {{provider}}.": "ترجمه‌شده توسط {{provider}}.",
"DeepL": "DeepL",
"Google Translate": "ترجمه‌ی Google",
"Azure Translator": "مترجم Azure",
"Invert Image In Dark Mode": "معکوس کردن تصویر در حالت تاریک",
"Help improve Readest": "کمک به بهبود Readest",
"Sharing anonymized statistics": "اشتراک‌گذاری آمار ناشناس",
"Interface Language": "زبان برنامه",
"Translation": "ترجمه",
"Enable Translation": "فعال کردن ترجمه",
"Translation Service": "سرویس ترجمه",
"Translate To": "ترجمه به",
"Disable Translation": "غیرفعال کردن ترجمه",
"Scroll": "پیمایش",
"Overlap Pixels": "پیکسل‌های هم‌پوشانی",
"System Language": "زبان سیستم",
"Security": "امنیت",
"Allow JavaScript": "اجازه‌ی اجرای JavaScript",
"Enable only if you trust the file.": "فقط در صورتی فعال کنید که به فایل اعتماد دارید.",
"Sort TOC by Page": "مرتب‌سازی فهرست مطالب بر اساس صفحه",
"Search in {{count}} Book(s)..._one": "جستجو در {{count}} کتاب...",
"Search in {{count}} Book(s)..._other": "جستجو در {{count}} کتاب...",
"No notes match your search": "هیچ یادداشتی با جستجوی شما مطابقت ندارد",
"Search notes and excerpts...": "جستجوی یادداشت‌ها و گزیده‌ها...",
"Sign in to Sync": "برای همگام‌سازی وارد شوید",
"Synced at {{time}}": "همگام‌سازی شده در {{time}}",
"Never synced": "هرگز همگام‌سازی نشده",
"Show Remaining Time": "نمایش زمان باقی‌مانده",
"{{time}} min left in chapter": "{{time}} دقیقه تا انتهای فصل",
"Override Book Color": "جایگزینی رنگ کتاب",
"Login Required": "نیازمند ورود",
"Quota Exceeded": "سهمیه به پایان رسیده است",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% از کاراکترهای ترجمه‌ی روزانه استفاده شده است.",
"Translation Characters": "کاراکترهای ترجمه",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} صدا",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} صدا",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "مشکلی پیش آمد. نگران نباشید—تیم ما مطلع شد و در حال رفع مشکل است.",
"Error Details:": "جزئیات خطا:",
"Try Again": "دوباره تلاش کنید",
"Need help?": "به کمک نیاز دارید؟",
"Contact Support": "تماس با پشتیبانی",
"Code Highlighting": "هایلایت کد",
"Enable Highlighting": "فعال کردن هایلایت",
"Code Language": "زبان کد",
"Top Margin (px)": "حاشیه‌ی بالا (px)",
"Bottom Margin (px)": "حاشیه‌ی پایین (px)",
"Right Margin (px)": "حاشیه‌ی راست (px)",
"Left Margin (px)": "حاشیه‌ی چپ (px)",
"Column Gap (%)": "فاصله‌ی ستون (%)",
"Always Show Status Bar": "نمایش همیشگی نوار وضعیت",
"Custom Content CSS": "CSS محتوای سفارشی",
"Enter CSS for book content styling...": "CSS برای استایل محتوای کتاب وارد کنید...",
"Custom Reader UI CSS": "CSS رابط خواننده‌ی سفارشی",
"Enter CSS for reader interface styling...": "CSS برای استایل رابط خواننده وارد کنید...",
"Crop": "برش",
"Book Covers": "جلد کتاب‌ها",
"Fit": "متناسب",
"Reset {{settings}}": "بازنشانی {{settings}}",
"Reset Settings": "بازنشانی تنظیمات",
"{{count}} pages left in chapter_one": "{{count}} صفحه تا انتهای فصل",
"{{count}} pages left in chapter_other": "{{count}} صفحه تا انتهای فصل",
"Show Remaining Pages": "نمایش صفحات باقی‌مانده",
"Source Han Serif CN VF": "Source Han Serif CN VF",
"Huiwen-mincho": "Huiwen-mincho",
"KingHwa_OldSong": "KingHwa_OldSong",
"Manage Subscription": "مدیریت اشتراک",
"Coming Soon": "به‌زودی",
"Upgrade to {{plan}}": "ارتقاء به {{plan}}",
"Upgrade to Plus or Pro": "ارتقاء به Plus یا Pro",
"Current Plan": "طرح فعلی",
"Plan Limits": "محدودیت‌های طرح",
"Processing your payment...": "در حال پردازش پرداخت شما...",
"Please wait while we confirm your subscription.": "لطفاً تا تأیید اشتراک صبور باشید.",
"Payment Processing": "پردازش پرداخت",
"Your payment is being processed. This usually takes a few moments.": "پرداخت شما در حال پردازش است. معمولاً چند لحظه طول می‌کشد.",
"Payment Failed": "پرداخت ناموفق",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "نتوانستیم اشتراک شما را پردازش کنیم. لطفاً دوباره تلاش کنید یا در صورت ادامه‌ی مشکل با پشتیبانی تماس بگیرید.",
"Back to Profile": "بازگشت به پروفایل",
"Subscription Successful!": "اشتراک با موفقیت فعال شد!",
"Thank you for your subscription! Your payment has been processed successfully.": "از اشتراک شما سپاس‌گزاریم! پرداخت شما با موفقیت انجام شد.",
"Email:": "ایمیل:",
"Plan:": "طرح:",
"Amount:": "مبلغ:",
"Go to Library": "رفتن به کتابخانه",
"Need help? Contact our support team at support@readest.com": "نیاز به کمک دارید؟ با تیم پشتیبانی ما به آدرس support@readest.com تماس بگیرید",
"Free Plan": "طرح رایگان",
"month": "ماه",
"AI Translations (per day)": "ترجمه‌های هوش مصنوعی (در روز)",
"Plus Plan": "طرح Plus",
"Includes All Free Plan Benefits": "شامل تمام امکانات طرح رایگان",
"Pro Plan": "طرح Pro",
"More AI Translations": "ترجمه‌های هوش مصنوعی بیشتر",
"Complete Your Subscription": "تکمیل اشتراک",
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% از فضای همگام‌سازی ابری استفاده شده است.",
"Cloud Sync Storage": "ذخیره‌سازی همگام‌سازی ابری",
"Parallel Read": "خواندن موازی",
"Disable": "غیرفعال",
"Enable": "فعال",
"Upgrade to Readest Premium": "ارتقاء به Readest Premium",
"Show Source Text": "نمایش متن منبع",
"Cross-Platform Sync": "همگام‌سازی چندپلتفرمی",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "کتابخانه، وضعیت مطالعه، هایلایت‌ها و یادداشت‌های خود را بین همه‌ی دستگاه‌ها به‌صورت یکپارچه همگام‌سازی کنید — دیگر هرگز محل مطالعه‌تان را از دست ندهید.",
"Customizable Reading": "تجربه‌ی مطالعه شخصی‌سازی‌شده",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "همه‌چیز را با فونت‌ها، چیدمان‌ها، تِم‌ها و تنظیمات نمایش پیشرفته قابل تغییر کنید تا تجربه‌ی خواندنِ ایده‌آل خود را بسازید.",
"AI Read Aloud": "خواندن صوتی با هوش مصنوعی",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "از خواندن بدون دخالت دست با صداهای طبیعیِ هوش مصنوعی لذت ببرید که کتاب‌هایتان را جان می‌بخشند.",
"AI Translations": "ترجمه‌های هوش مصنوعی",
"Connect with fellow readers and get help fast in our friendly community channels.": "با دیگر خوانندگان ارتباط برقرار کنید و از کانال‌های دوستانه‌ی جامعه‌ی ما کمک بگیرید.",
"Unlimited AI Read Aloud Hours": "ساعات نامحدود خواندن صوتی با هوش مصنوعی",
"Listen without limits—convert as much text as you like into immersive audio.": "بدون محدودیت گوش دهید — هر میزان متن را که بخواهید می‌توانید به صدایی جذاب تبدیل کنید.",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "با استفاده‌ی روزانه‌ی بیشتر و گزینه‌های پیشرفته، قابلیت‌های ترجمه را افزایش دهید.",
"DeepL Pro Access": "دسترسی به DeepL Pro",
"Enjoy faster responses and dedicated assistance whenever you need help.": "از پاسخ‌های سریع‌تر و کمک اختصاصی هر زمان که نیاز دارید برخوردار شوید.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "سهمیه‌ی ترجمه‌ی روزانه به پایان رسیده است. برای ادامه‌ی استفاده از ترجمه‌های هوش مصنوعی، طرح خود را ارتقاء دهید.",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "هر متنی را فوراً با قدرت Google، Azure یا DeepL ترجمه کنید — محتوای هر زبانی را بفهمید.",
"Includes All Plus Plan Benefits": "شامل تمام مزایای طرح Plus",
"Early Feature Access": "دسترسی زودهنگام به ویژگی‌ها",
"Be the first to explore new features, updates, and innovations before anyone else.": "اولین نفری باشید که ویژگی‌ها، به‌روزرسانی‌ها و نوآوری‌های جدید را تجربه می‌کنید.",
"Advanced AI Tools": "ابزارهای هوش مصنوعی پیشرفته",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "از ابزارهای قدرتمند هوش مصنوعی برای خواندن هوشمندتر، ترجمه و کشف محتوا بهره ببرید.",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "تا روزانه ۱۰۰٬۰۰۰ کاراکتر را با دقیق‌ترین ابزار ترجمه، ترجمه کنید.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "تا روزانه ۵۰۰٬۰۰۰ کاراکتر را با دقیق‌ترین ابزار ترجمه، ترجمه کنید.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "مجموعه‌ی خواندن خود را تا ۵ گیگابایت به‌صورت امن در فضای ابری ذخیره و دسترسی داشته باشید.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "کل مجموعه‌ی خواندن خود را تا ۲۰ گیگابایت به‌صورت ایمن در فضای ابری ذخیره و دسترسی داشته باشید.",
"Deleted cloud backup of the book: {{title}}": "نسخه‌ی پشتیبان ابری کتاب «{{title}}» حذف شد.",
"Are you sure to delete the cloud backup of the selected book?": "آیا از حذف نسخه‌ی پشتیبان ابری کتاب انتخاب‌شده مطمئن هستید؟",
"What's New in Readest": "چه چیز تازه‌ای در Readest است",
"Enter book title": "عنوان کتاب را وارد کنید",
"Subtitle": "زیرعنوان",
"Enter book subtitle": "زیرعنوان کتاب را وارد کنید",
"Enter author name": "نام نویسنده را وارد کنید",
"Series": "سری",
"Enter series name": "نام سری را وارد کنید",
"Series Index": "شماره‌ی سری",
"Enter series index": "شماره‌ی سری را وارد کنید",
"Total in Series": "تعداد کل در سری",
"Enter total books in series": "تعداد کل کتاب‌ها در سری را وارد کنید",
"Enter publisher": "ناشر را وارد کنید",
"Publication Date": "تاریخ انتشار",
"Identifier": "شناسه",
"Enter book description": "توضیحات کتاب را وارد کنید",
"Change cover image": "تغییر تصویر جلد",
"Replace": "جایگزین",
"Unlock cover": "باز کردن قفل جلد",
"Lock cover": "قفل کردن جلد",
"Auto-Retrieve Metadata": "دریافت خودکار فراداده",
"Auto-Retrieve": "دریافت خودکار",
"Unlock all fields": "باز کردن قفل همه‌ی فیلدها",
"Unlock All": "باز کردن قفل همه",
"Lock all fields": "قفل کردن همه‌ی فیلدها",
"Lock All": "قفل کردن همه",
"Reset": "بازنشانی",
"Edit Metadata": "ویرایش فراداده",
"Locked": "قفل شده",
"Select Metadata Source": "انتخاب منبع فراداده",
"Keep manual input": "حفظ ورودی دستی",
"Google Books": "کتاب‌های گوگل",
"Open Library": "Open Library",
"Fiction, Science, History": "داستان، علم، تاریخ",
"Open Book in New Window": "باز کردن کتاب در پنجره‌ی جدید",
"Voices for {{lang}}": "صداها برای {{lang}}",
"Yandex Translate": "ترجمه‌ی Yandex",
"YYYY or YYYY-MM-DD": "YYYY یا YYYY-MM-DD",
"Restore Purchase": "بازیابی خرید",
"No purchases found to restore.": "هیچ خریدی برای بازیابی یافت نشد.",
"Failed to restore purchases.": "بازیابی خریدها ناموفق بود.",
"Failed to manage subscription.": "مدیریت اشتراک ناموفق بود.",
"Failed to load subscription plans.": "بارگذاری طرح‌های اشتراک ناموفق بود.",
"year": "سال",
"Failed to create checkout session": "ایجاد صفحه‌ی پرداخت ناموفق بود",
"Storage": "ذخیره‌سازی",
"Terms of Service": "شرایط و قوانین",
"Privacy Policy": "سیاست حفظ حریم خصوصی",
"Disable Double Click": "غیرفعال کردن کلیک دوبل",
"TTS not supported for this document": "قابلیت تبدیل متن به گفتار برای این سند پشتیبانی نمی‌شود",
"Reset Password": "بازنشانی رمز عبور",
"Show Reading Progress": "نمایش وضعیت مطالعه",
"Reading Progress Style": "سبک نمایش وضعیت مطالعه",
"Page Number": "شماره صفحه",
"Percentage": "درصد",
"Deleted local copy of the book: {{title}}": "نسخه‌ی محلی کتاب «{{title}}» حذف شد.",
"Failed to delete cloud backup of the book: {{title}}": "حذف نسخه‌ی پشتیبان ابری کتاب «{{title}}» ناموفق بود.",
"Failed to delete local copy of the book: {{title}}": "حذف نسخه‌ی محلی کتاب «{{title}}» ناموفق بود.",
"Are you sure to delete the local copy of the selected book?": "آیا از حذف نسخه‌ی محلی کتاب انتخاب‌شده مطمئن هستید؟",
"Remove from Cloud & Device": "حذف از فضای ابری و دستگاه",
"Remove from Cloud Only": "حذف فقط از فضای ابری",
"Remove from Device Only": "حذف فقط از دستگاه",
"Disconnected": "قطع اتصال",
"KOReader Sync Settings": "تنظیمات همگام‌سازی KOReader",
"Sync Strategy": "استراتژی همگام‌سازی",
"Ask on conflict": "پرسش هنگام تداخل",
"Always use latest": "همیشه از آخرین استفاده شود",
"Send changes only": "فقط ارسال تغییرات",
"Receive changes only": "فقط دریافت تغییرات",
"Checksum Method": "روش Checksum",
"File Content (recommended)": "محتوای فایل (توصیه‌شده)",
"File Name": "نام فایل",
"Device Name": "نام دستگاه",
"Connect to your KOReader Sync server.": "به سرور همگام‌سازی KOReader خود متصل شوید.",
"Server URL": "آدرس سرور",
"Username": "نام کاربری",
"Your Username": "نام کاربری شما",
"Password": "رمز عبور",
"Connect": "اتصال",
"KOReader Sync": "همگام‌سازی KOReader",
"Sync Conflict": "تداخل همگام‌سازی",
"Sync reading progress from \"{{deviceName}}\"?": "همگام‌سازی وضعیت مطالعه از «{{deviceName}}»؟",
"another device": "دستگاه دیگر",
"Local Progress": "وضعیت مطالعه محلی",
"Remote Progress": "وضعیت مطالعه از راه دور",
"Page {{page}} of {{total}} ({{percentage}}%)": "صفحه‌ی {{page}} از {{total}} ({{percentage}}%)",
"Current position": "موقعیت فعلی",
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "تقریباً صفحه‌ی {{page}} از {{total}} ({{percentage}}%)",
"Approximately {{percentage}}%": "تقریباً {{percentage}}%",
"Failed to connect": "اتصال برقرار نشد",
"Sync Server Connected": "سرور همگام‌سازی متصل شد",
"Sync as {{userDisplayName}}": "همگام‌سازی به‌عنوان {{userDisplayName}}",
"Custom Fonts": "فونت‌های سفارشی",
"Cancel Delete": "لغو حذف",
"Import Font": "وارد کردن فونت",
"Delete Font": "حذف فونت",
"Tips": "نکات",
"Custom fonts can be selected from the Font Face menu": "فونت‌های سفارشی را می‌توان از منوی انتخاب فونت برگزید",
"Manage Custom Fonts": "مدیریت فونت‌های سفارشی",
"Select Files": "انتخاب فایل‌ها",
"Select Image": "انتخاب تصویر",
"Select Video": "انتخاب ویدیو",
"Select Audio": "انتخاب صوت",
"Select Fonts": "انتخاب فونت‌ها",
"Supported font formats: .ttf, .otf, .woff, .woff2": "فرمت‌های فونت پشتیبانی‌شده: .ttf, .otf, .woff, .woff2",
"Push Progress": "ارسال وضعیت مطالعه",
"Pull Progress": "دریافت وضعیت مطالعه",
"Previous Paragraph": "پاراگراف قبلی",
"Previous Sentence": "جمله‌ی قبلی",
"Pause": "توقف",
"Play": "پخش",
"Next Sentence": "جمله‌ی بعدی",
"Next Paragraph": "پاراگراف بعدی",
"Separate Cover Page": "جدا کردن صفحه‌ی جلد",
"Resize Notebook": "تغییر اندازه دفترچه یادداشت",
"Resize Sidebar": "تغییر اندازه نوار کناری",
"Get Help from the Readest Community": "دریافت کمک از جامعه‌ی Readest",
"Remove cover image": "حذف تصویر جلد",
"Bookshelf": "قفسه‌ی کتاب",
"View Menu": "نمایش منو",
"Settings Menu": "منوی تنظیمات",
"View account details and quota": "مشاهده‌ی جزئیات حساب و وضعیت سهمیه",
"Library Header": "سربرگ کتابخانه",
"Book Content": "محتوای کتاب",
"Footer Bar": "نوار پاورقی",
"Header Bar": "نوار سربرگ",
"View Options": "گزینه‌های نمایش",
"Book Menu": "منوی کتاب",
"Search Options": "گزینه‌های جستجو",
"Close": "بستن",
"Delete Book Options": "گزینه‌های حذف کتاب",
"ON": "روشن",
"OFF": "خاموش",
"Reading Progress": "وضعیت مطالعه",
"Page Margin": "حاشیه صفحه",
"Remove Bookmark": "حذف نشانک",
"Add Bookmark": "افزودن نشانک",
"Books Content": "محتوای کتاب‌ها",
"Jump to Location": "پرش به مکان",
"Unpin Notebook": "برداشتن سنجاق دفترچه یادداشت",
"Pin Notebook": "سنجاق کردن دفترچه یادداشت",
"Hide Search Bar": "پنهان کردن نوار جستجو",
"Show Search Bar": "نمایش نوار جستجو",
"On {{current}} of {{total}} page": "در صفحه‌ی {{current}} از {{total}}",
"Section Title": "عنوان بخش",
"Decrease": "کاهش",
"Increase": "افزایش",
"Settings Panels": "پنل‌های تنظیمات",
"Settings": "تنظیمات",
"Unpin Sidebar": "برداشتن سنجاق نوار کناری",
"Pin Sidebar": "سنجاق کردن نوار کناری",
"Toggle Sidebar": "تغییر وضعیت نوار کناری",
"Toggle Translation": "تغییر وضعیت ترجمه",
"Translation Disabled": "ترجمه غیرفعال است",
"Minimize": "کوچک کردن",
"Maximize or Restore": "بزرگ کردن یا بازگردانی",
"Exit Parallel Read": "خروج از خواندن موازی",
"Enter Parallel Read": "ورود به خواندن موازی",
"Zoom Level": "اندازه بزرگ‌نمایی",
"Zoom Out": "کوچک‌نمایی",
"Reset Zoom": "بازنشانی بزرگ‌نمایی",
"Zoom In": "بزرگ‌نمایی",
"Zoom Mode": "حالت بزرگ‌نمایی",
"Single Page": "تک‌صفحه‌ای",
"Auto Spread": "گسترش خودکار",
"Fit Page": "تناسب صفحه",
"Fit Width": "تناسب با عرض",
"Failed to select directory": "انتخاب مسیر ناموفق بود",
"The new data directory must be different from the current one.": "مسیر داده‌ی جدید باید با مسیر فعلی متفاوت باشد.",
"Migration failed: {{error}}": "انتقال ناموفق بود: {{error}}",
"Change Data Location": "تغییر محل داده‌ها",
"Current Data Location": "محل فعلی داده‌ها",
"Total size: {{size}}": "حجم کل: {{size}}",
"Calculating file info...": "در حال محاسبه‌ی اطلاعات فایل...",
"New Data Location": "محل جدید داده‌ها",
"Choose Different Folder": "انتخاب پوشه‌ی دیگر",
"Choose New Folder": "انتخاب پوشه‌ی جدید",
"Migrating data...": "در حال انتقال داده‌ها...",
"Copying: {{file}}": "در حال کپی‌کردن: {{file}}",
"{{current}} of {{total}} files": "{{current}} از {{total}} فایل‌ها",
"Migration completed successfully!": "انتقال با موفقیت انجام شد!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "داده‌های شما به محل جدید منتقل شدند. لطفاً برای تکمیل فرایند، برنامه را مجدداً راه‌اندازی کنید.",
"Migration failed": "انتقال ناموفق بود",
"Important Notice": "اطلاعیه‌ی مهم",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "این کار، همه‌ی داده‌های برنامه را به محل جدید منتقل خواهد کرد. مطمئن شوید که در مقصد، فضای کافی وجود دارد.",
"Restart App": "راه‌اندازی مجدد برنامه",
"Start Migration": "شروع انتقال",
"Advanced Settings": "تنظیمات پیشرفته",
"File count: {{size}}": "تعداد فایل‌ها: {{size}}",
"Background Read Aloud": "خواندن با صدای بلند در پس‌زمینه",
"Ready to read aloud": "آماده برای خواندن با صدای بلند",
"Read Aloud": "خواندن با صدای بلند",
"Screen Brightness": "روشنایی صفحه",
"Background Image": "تصویر پس‌زمینه",
"Import Image": "وارد کردن تصویر",
"Opacity": "شفافیت",
"Size": "اندازه",
"Cover": "جلد",
"Contain": "شامل",
"{{number}} pages left in chapter": "{{number}} صفحه تا انتهای فصل",
"Device": "دستگاه",
"E-Ink Mode": "حالت E-Ink",
"Highlight Colors": "رنگ‌های هایلایت",
"Auto Screen Brightness": "روشنایی خودکار صفحه",
"Pagination": "صفحه‌بندی",
"Disable Double Tap": "غیرفعال کردن کلیک دوبل",
"Tap to Paginate": "ضربه برای صفحه‌بندی",
"Click to Paginate": "کلیک برای صفحه‌بندی",
"Tap Both Sides": "ضربه هر دو طرف",
"Click Both Sides": "کلیک هر دو طرف",
"Swap Tap Sides": "جابجایی طرف‌های ضربه",
"Swap Click Sides": "جابجایی طرف‌های کلیک",
"Source and Translated": "منبع و ترجمه شده",
"Translated Only": "فقط ترجمه شده",
"Source Only": "فقط منبع",
"TTS Text": "متن TTS",
"The book file is corrupted": "فایل کتاب خراب است",
"The book file is empty": "فایل کتاب خالی است",
"Failed to open the book file": "خطا در باز کردن فایل کتاب",
"On-Demand Purchase": "خرید سفارشی",
"Full Customization": "سفارشی‌سازی کامل",
"Lifetime Plan": "طرح مادام‌العمر",
"One-Time Payment": "پرداخت یک‌باره",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "یک پرداخت یک‌باره انجام دهید تا به‌صورت مادام‌العمر به ویژگی‌های خاص در تمام دستگاه‌ها دسترسی پیدا کنید. فقط زمانی که به ویژگی‌ها یا خدمات خاصی نیاز دارید، آن‌ها را خریداری کنید.",
"Expand Cloud Sync Storage": "گسترش فضای همگام‌سازی ابری",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "فضای ابری خود را به‌صورت دائمی با یک خرید یک‌باره گسترش دهید. هر خرید اضافی فضای بیشتری اضافه می‌کند.",
"Unlock All Customization Options": "باز کردن تمام گزینه‌های سفارشی‌سازی",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "باز کردن تم‌ها، فونت‌ها، گزینه‌های چیدمان و خواندن با صدای بلند، مترجم‌ها، خدمات ذخیره‌سازی ابری اضافی.",
"Purchase Successful!": "خرید با موفقیت انجام شد!",
"lifetime": "مادام‌العمر",
"Thank you for your purchase! Your payment has been processed successfully.": "از خرید شما متشکریم! پرداخت شما با موفقیت انجام شد.",
"Order ID:": "شناسه سفارش:",
"TTS Highlighting": "هایلایت TTS",
"Style": "سبک",
"Underline": "خط زیرین",
"Strikethrough": "خط‌خورده",
"Squiggly": "خط‌موج‌دار",
"Outline": "خط‌کشی",
"Save Current Color": "ذخیره رنگ فعلی",
"Quick Colors": "رنگ‌های سریع",
"Highlighter": "ماژیک هایلایت",
"Save Book Cover": "ذخیره جلد کتاب",
"Auto-save last book cover": "ذخیره خودکار آخرین جلد کتاب",
"Back": "بازگشت",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "ایمیل تأیید ارسال شد! لطفاً ایمیل قدیمی و جدید خود را بررسی کنید تا تغییر را تأیید نمایید.",
"Failed to update email": "به‌روزرسانی ایمیل ناموفق بود",
"New Email": "ایمیل جدید",
"Your new email": "ایمیل جدید شما",
"Updating email ...": "در حال به‌روزرسانی ایمیل ...",
"Update email": "به‌روزرسانی ایمیل",
"Current email": "ایمیل فعلی",
"Update Email": "به‌روزرسانی ایمیل",
"All": "همه"
}
@@ -220,9 +220,9 @@
"Auto Import on File Open": "Importation automatique à l'ouverture du fichier",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "Aucun chapitre détecté.",
"Failed to parse the EPUB file.": "Échec de l'analyse du fichier EPUB.",
"This book format is not supported.": "Ce format de livre n'est pas pris en charge.",
"No chapters detected": "Aucun chapitre détecté",
"Failed to parse the EPUB file": "Échec de l'analyse du fichier EPUB",
"This book format is not supported": "Ce format de livre n'est pas pris en charge",
"Unable to fetch the translation. Please log in first and try again.": "Impossible de récupérer la traduction. Veuillez d'abord vous connecter et réessayer.",
"Group": "Groupe",
"Always on Top": "Toujours au-dessus",
@@ -259,8 +259,6 @@
"(from 'As You Like It', Act II)": "(de 'Comme il vous plaira', Acte II)",
"Link Color": "Couleur du lien",
"Volume Keys for Page Flip": "Boutons volume pour tourner la page",
"Clicks for Page Flip": "Taps pour tourner la page",
"Swap Clicks Area": "Inverser zones de tap",
"Screen": "Écran",
"Orientation": "Orientation",
"Portrait": "Portrait",
@@ -297,7 +295,6 @@
"Disable Translation": " désactiver la traduction",
"Scroll": "Défilement",
"Overlap Pixels": "Pixels de chevauchement",
"Click": "Cliquer",
"System Language": "Langue du système",
"Security": "Sécurité",
"Allow JavaScript": "Autoriser JavaScript",
@@ -369,7 +366,6 @@
"Email:": "E-mail:",
"Plan:": "Abonnement:",
"Amount:": "Montant:",
"Subscription ID:": "ID dabonnement:",
"Go to Library": "Aller à la bibliothèque",
"Need help? Contact our support team at support@readest.com": "Besoin daide? Contactez notre équipe à support@readest.com",
"Free Plan": "Abonnement gratuit",
@@ -603,5 +599,66 @@
"File count: {{size}}": "Nombre de fichiers : {{size}}",
"Background Read Aloud": "Lecture Vocale en Arrière-Plan",
"Ready to read aloud": "Prêt à lire à haute voix",
"Read Aloud": "Lire à haute voix"
"Read Aloud": "Lire à haute voix",
"Screen Brightness": "Luminosité de l'Écran",
"Background Image": "Image de Fond",
"Import Image": "Importer une Image",
"Opacity": "Opacité",
"Size": "Taille",
"Cover": "Couverture",
"Contain": "Contenir",
"{{number}} pages left in chapter": "{{number}} pages restantes dans le chapitre",
"Device": "Appareil",
"E-Ink Mode": "Mode E-Ink",
"Highlight Colors": "Couleurs de Surlignage",
"Auto Screen Brightness": "Luminosité Automatique de l'Écran",
"Pagination": "Pagination",
"Disable Double Tap": "Désactiver le Double Tap",
"Tap to Paginate": "Tapoter pour Paginater",
"Click to Paginate": "Cliquer pour Paginater",
"Tap Both Sides": "Tapoter des Deux Côtés",
"Click Both Sides": "Cliquer des Deux Côtés",
"Swap Tap Sides": "Échanger les Côtés de Tap",
"Swap Click Sides": "Échanger les Côtés de Clic",
"Source and Translated": "Source et Traduit",
"Translated Only": "Traduit Seulement",
"Source Only": "Source Seulement",
"TTS Text": "TTS Texte",
"The book file is corrupted": "Le fichier du livre est corrompu",
"The book file is empty": "Le fichier du livre est vide",
"Failed to open the book file": "Échec de l'ouverture du fichier du livre",
"On-Demand Purchase": "Achat à la Demande",
"Full Customization": "Personnalisation Complète",
"Lifetime Plan": "Plan de Vie",
"One-Time Payment": "Paiement Unique",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Effectuez un paiement unique pour profiter d'un accès à vie à des fonctionnalités spécifiques sur tous les appareils. Achetez des fonctionnalités ou des services spécifiques uniquement lorsque vous en avez besoin.",
"Expand Cloud Sync Storage": "Étendre le Stockage de Synchronisation Cloud",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Étendez votre stockage cloud pour toujours avec un achat unique. Chaque achat supplémentaire ajoute plus d'espace.",
"Unlock All Customization Options": "Débloquer Toutes les Options de Personnalisation",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Débloquer des thèmes supplémentaires, des polices, des options de mise en page et des fonctions de lecture à haute voix, des traducteurs, des services de stockage cloud.",
"Purchase Successful!": "Achat Réussi!",
"lifetime": "à vie",
"Thank you for your purchase! Your payment has been processed successfully.": "Merci pour votre achat ! Votre paiement a été traité avec succès.",
"Order ID:": "ID de commande :",
"TTS Highlighting": "Surbrillance TTS",
"Style": "Style",
"Underline": "Souligner",
"Strikethrough": "Barré",
"Squiggly": "Ondulé",
"Outline": "Contour",
"Save Current Color": "Enregistrer la Couleur Actuelle",
"Quick Colors": "Couleurs Rapides",
"Highlighter": "Surligneur",
"Save Book Cover": "Enregistrer la Couverture du Livre",
"Auto-save last book cover": "Enregistrer automatiquement la dernière couverture du livre",
"Back": "Retour",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "E-mail de confirmation envoyé ! Veuillez vérifier vos anciennes et nouvelles adresses e-mail pour confirmer le changement.",
"Failed to update email": "Échec de la mise à jour de le-mail",
"New Email": "Nouvel e-mail",
"Your new email": "Votre nouvel e-mail",
"Updating email ...": "Mise à jour de le-mail ...",
"Update email": "Mettre à jour le-mail",
"Current email": "E-mail actuel",
"Update Email": "Mettre à jour le-mail",
"All": "Tous"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "फ़ाइल खोलने पर स्वचालित आयात",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "कोई अध्याय नहीं मिला।",
"Failed to parse the EPUB file.": "EPUB फ़ाइल को पार्स करने में विफल।",
"This book format is not supported.": "यह पुस्तक प्रारूप समर्थित नहीं है।",
"No chapters detected": "कोई अध्याय नहीं मिला।",
"Failed to parse the EPUB file": "EPUB फ़ाइल को पार्स करने में विफल।",
"This book format is not supported": "यह पुस्तक प्रारूप समर्थित नहीं है।",
"Unable to fetch the translation. Please log in first and try again.": "अनुवाद प्राप्त करने में असमर्थ। कृपया पहले लॉग इन करें और पुनः प्रयास करें।",
"Group": "समूह",
"Always on Top": "सर्वोच्च पर हमेशा",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "('जैसा आप इसे पसंद करते हैं', अधिनियम II)",
"Link Color": "लिंक रंग",
"Volume Keys for Page Flip": "पृष्ठ फ़्लिप के लिए वॉल्यूम कुंजियाँ",
"Clicks for Page Flip": "पृष्ठ फ़्लिप के लिए क्लिक करें",
"Swap Clicks Area": "क्लिक क्षेत्र स्वैप करें",
"Screen": "स्क्रीन",
"Orientation": "अवयव",
"Portrait": "पोर्ट्रेट",
@@ -297,7 +295,6 @@
"Disable Translation": "अनुवाद अक्षम करें",
"Scroll": "स्क्रॉल",
"Overlap Pixels": "ओवरलैप पिक्सेल",
"Click": "क्लिक करें",
"System Language": "सिस्टम भाषा",
"Security": "सुरक्षा",
"Allow JavaScript": "जावास्क्रिप्ट की अनुमति दें",
@@ -366,7 +363,6 @@
"Email:": "ईमेल:",
"Plan:": "योजना:",
"Amount:": "राशि:",
"Subscription ID:": "सदस्यता आईडी:",
"Go to Library": "लाइब्रेरी पर जाएँ",
"Need help? Contact our support team at support@readest.com": "मदद चाहिए? हमारी सहायता टीम से संपर्क करें: support@readest.com",
"Free Plan": "मुफ्त योजना",
@@ -599,5 +595,66 @@
"File count: {{size}}": "फ़ाइलों की संख्या: {{size}}",
"Background Read Aloud": "पृष्ठभूमि में वाचन",
"Ready to read aloud": "उच्च स्वर में पढ़ने के लिए तैयार",
"Read Aloud": "उच्च स्वर में पढ़ें"
"Read Aloud": "उच्च स्वर में पढ़ें",
"Screen Brightness": "स्क्रीन ब्राइटनेस",
"Background Image": "पृष्ठभूमि छवि",
"Import Image": "छवि आयात करें",
"Opacity": "अस्पष्टता",
"Size": "आकार",
"Cover": "कवर",
"Contain": "समाहित करें",
"{{number}} pages left in chapter": "इस अध्याय में {{number}} पृष्ठ शेष हैं",
"Device": "डिवाइस",
"E-Ink Mode": "ई-इंक मोड",
"Highlight Colors": "हाइलाइट रंग",
"Auto Screen Brightness": "स्वचालित स्क्रीन ब्राइटनेस",
"Pagination": "पृष्ठांकन",
"Disable Double Tap": "डबल टैप अक्षम करें",
"Tap to Paginate": "पृष्ठांकन के लिए टैप करें",
"Click to Paginate": "पृष्ठांकन के लिए क्लिक करें",
"Tap Both Sides": "दोनों पक्षों पर टैप करें",
"Click Both Sides": "दोनों पक्षों पर क्लिक करें",
"Swap Tap Sides": "स्वैप टैप साइड्स",
"Swap Click Sides": "स्वैप क्लिक साइड्स",
"Source and Translated": "स्रोत और अनुवादित",
"Translated Only": "केवल अनुवादित",
"Source Only": "केवल स्रोत",
"TTS Text": "TTS टेक्स्ट",
"The book file is corrupted": "पुस्तक फ़ाइल भ्रष्ट है।",
"The book file is empty": "पुस्तक फ़ाइल खाली है।",
"Failed to open the book file": "पुस्तक फ़ाइल खोलने में विफल।",
"On-Demand Purchase": "आवश्यकता के अनुसार खरीदारी",
"Full Customization": "पूर्ण अनुकूलन",
"Lifetime Plan": "जीवनकाल योजना",
"One-Time Payment": "एक बार का भुगतान",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "सभी उपकरणों पर विशिष्ट सुविधाओं तक जीवनकाल पहुंच का आनंद लेने के लिए एक बार का भुगतान करें। जब आपको उनकी आवश्यकता हो, तो केवल विशिष्ट सुविधाओं या सेवाओं को खरीदें।",
"Expand Cloud Sync Storage": "क्लाउड सिंक स्टोरेज का विस्तार करें",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "एक बार के खरीदारी के साथ अपने क्लाउड स्टोरेज का विस्तार करें। प्रत्येक अतिरिक्त खरीदारी अधिक स्थान जोड़ती है।",
"Unlock All Customization Options": "सभी अनुकूलन विकल्प अनलॉक करें",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "अतिरिक्त थीम, फ़ॉन्ट, लेआउट विकल्प और पढ़ने के लिए, अनुवादक, क्लाउड स्टोरेज सेवाएँ अनलॉक करें।",
"Purchase Successful!": "खरीदारी सफल!",
"lifetime": "जीवनकाल",
"Thank you for your purchase! Your payment has been processed successfully.": "आपकी खरीदारी के लिए धन्यवाद! आपका भुगतान सफलतापूर्वक संसाधित हो गया है।",
"Order ID:": "ऑर्डर आईडी:",
"TTS Highlighting": "TTS हाइलाइटिंग",
"Style": "शैली",
"Underline": "रेखांकित",
"Strikethrough": "रेखांकित करें",
"Squiggly": "स्क्विगली",
"Outline": "आउटलाइन",
"Save Current Color": "वर्तमान रंग सहेजें",
"Quick Colors": "त्वरित रंग",
"Highlighter": "हाइलाइटर",
"Save Book Cover": "बुक कवर सहेजें",
"Auto-save last book cover": "अंतिम पुस्तक कवर को स्वचालित रूप से सहेजें",
"Back": "वापस",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "पुष्टिकरण ईमेल भेज दिया गया है! कृपया परिवर्तन की पुष्टि करने के लिए अपना पुराना और नया ईमेल पता जांचें।",
"Failed to update email": "ईमेल अपडेट करने में विफल",
"New Email": "नया ईमेल",
"Your new email": "आपका नया ईमेल",
"Updating email ...": "ईमेल अपडेट किया जा रहा है ...",
"Update email": "ईमेल अपडेट करें",
"Current email": "वर्तमान ईमेल",
"Update Email": "ईमेल अपडेट करें",
"All": "सभी"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "Impor Otomatis saat File Dibuka",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "Tidak ada bab yang terdeteksi.",
"Failed to parse the EPUB file.": "Gagal mengurai file EPUB.",
"This book format is not supported.": "Format buku ini tidak didukung.",
"No chapters detected": "Tidak ada bab yang terdeteksi",
"Failed to parse the EPUB file": "Gagal mengurai file EPUB",
"This book format is not supported": "Format buku ini tidak didukung",
"Unable to fetch the translation. Please log in first and try again.": "Tidak dapat mengambil terjemahan. Silakan masuk terlebih dahulu dan coba lagi.",
"Group": "Grup",
"Always on Top": "Selalu di Atas",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "(dari 'Seperti yang Anda Inginkan', Bab II)",
"Link Color": "Warna Tautan",
"Volume Keys for Page Flip": "Volume Keys untuk Pembalikan Halaman",
"Clicks for Page Flip": "Klik untuk Pembalikan Halaman",
"Swap Clicks Area": "Area Klik Tukar",
"Screen": "Screen",
"Orientation": "Orientasi",
"Portrait": "Potret",
@@ -296,7 +294,6 @@
"Disable Translation": "Nonaktifkan Terjemahan",
"Scroll": "Scroll",
"Overlap Pixels": "Overlap Piksel",
"Click": "Klik",
"System Language": "Bahasa Sistem",
"Security": "Keamanan",
"Allow JavaScript": "Izinkan JavaScript",
@@ -362,7 +359,6 @@
"Email:": "Email:",
"Plan:": "Paket:",
"Amount:": "Jumlah:",
"Subscription ID:": "ID Langganan:",
"Go to Library": "Pergi ke Perpustakaan",
"Need help? Contact our support team at support@readest.com": "Perlu bantuan? Hubungi tim dukungan kami di support@readest.com",
"Free Plan": "Paket Gratis",
@@ -595,5 +591,66 @@
"File count: {{size}}": "Jumlah file: {{size}}",
"Background Read Aloud": "Bacakan Latar Belakang",
"Ready to read aloud": "Siap untuk dibacakan",
"Read Aloud": "Bacakan"
"Read Aloud": "Bacakan",
"Screen Brightness": "Kecerahan Layar",
"Background Image": "Gambar Latar Belakang",
"Import Image": "Impor Gambar",
"Opacity": "Keburaman",
"Size": "Ukuran",
"Cover": "Sampul",
"Contain": "Mengandung",
"{{number}} pages left in chapter": "{{number}} halaman tersisa di bab",
"Device": "Perangkat",
"E-Ink Mode": "Mode E-Ink",
"Highlight Colors": "Warna Sorotan",
"Auto Screen Brightness": "Auto Kecerahan Layar",
"Pagination": "Paginasi",
"Disable Double Tap": "Nonaktifkan Ketuk Ganda",
"Tap to Paginate": "Ketuk untuk Paginasi",
"Click to Paginate": "Klik untuk Paginasi",
"Tap Both Sides": "Ketuk Kedua Sisi",
"Click Both Sides": "Klik Kedua Sisi",
"Swap Tap Sides": "Tukar Sisi Ketuk",
"Swap Click Sides": "Tukar Sisi Klik",
"Source and Translated": "Sumber dan Terjemahan",
"Translated Only": "Hanya Terjemahan",
"Source Only": "Hanya Sumber",
"TTS Text": "Teks TTS",
"The book file is corrupted": "File buku rusak",
"The book file is empty": "File buku kosong",
"Failed to open the book file": "Gagal membuka file buku",
"On-Demand Purchase": "Pembelian Sesuai Permintaan",
"Full Customization": "Kustomisasi Penuh",
"Lifetime Plan": "Rencana Seumur Hidup",
"One-Time Payment": "Pembayaran Sekali",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Lakukan pembayaran sekali untuk menikmati akses seumur hidup ke fitur tertentu di semua perangkat. Beli fitur atau layanan tertentu hanya saat Anda membutuhkannya.",
"Expand Cloud Sync Storage": "Perluas Penyimpanan Sinkronisasi Cloud",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Perluas penyimpanan cloud Anda selamanya dengan pembelian sekali. Setiap pembelian tambahan menambah lebih banyak ruang.",
"Unlock All Customization Options": "Buka Semua Opsi Kustomisasi",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Buka tema tambahan, font, opsi tata letak, dan baca nyaring, penerjemah, layanan penyimpanan cloud.",
"Purchase Successful!": "Pembelian Berhasil!",
"lifetime": "seumur hidup",
"Thank you for your purchase! Your payment has been processed successfully.": "Terima kasih atas pembelian Anda! Pembayaran Anda telah berhasil diproses.",
"Order ID:": "ID Pesanan:",
"TTS Highlighting": "TTS Menyorot",
"Style": "Gaya",
"Underline": "Garisan Bawah",
"Strikethrough": "Garis Tengah",
"Squiggly": "Bergelombang",
"Outline": "Garis Besar",
"Save Current Color": "Simpan Warna Saat Ini",
"Quick Colors": "Warna Cepat",
"Highlighter": "Penyorot",
"Save Book Cover": "Simpan Sampul Buku",
"Auto-save last book cover": "Simpan otomatis sampul buku terakhir",
"Back": "Kembali",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Email konfirmasi telah dikirim! Silakan periksa alamat email lama dan baru Anda untuk mengonfirmasi perubahan.",
"Failed to update email": "Gagal memperbarui email",
"New Email": "Email baru",
"Your new email": "Email baru Anda",
"Updating email ...": "Memperbarui email ...",
"Update email": "Perbarui email",
"Current email": "Email saat ini",
"Update Email": "Perbarui email",
"All": "Semua"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "Importazione automatica all'apertura del file",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "Nessun capitolo rilevato.",
"Failed to parse the EPUB file.": "Impossibile analizzare il file EPUB.",
"This book format is not supported.": "Questo formato di libro non è supportato.",
"No chapters detected": "Nessun capitolo rilevato",
"Failed to parse the EPUB file": "Impossibile analizzare il file EPUB",
"This book format is not supported": "Questo formato di libro non è supportato",
"Unable to fetch the translation. Please log in first and try again.": "Impossibile recuperare la traduzione. Accedi prima e riprova.",
"Group": "Gruppo",
"Always on Top": "Sempre in primo piano",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "(da 'Come vi piace', Atto II)",
"Link Color": "Colore link",
"Volume Keys for Page Flip": "Tasti volume per girare pagina",
"Clicks for Page Flip": "Clic per girare pagina",
"Swap Clicks Area": "Inverti area dei clic",
"Screen": "Schermo",
"Orientation": "Orientamento",
"Portrait": "Ritratto",
@@ -298,7 +296,6 @@
"Disable Translation": "Disabilita traduzione",
"Scroll": "Scorri",
"Overlap Pixels": "Pixel di sovrapposizione",
"Click": "Fai clic",
"System Language": "Lingua di sistema",
"Security": "Security",
"Allow JavaScript": "Consenti JavaScript",
@@ -370,7 +367,6 @@
"Email:": "Email:",
"Plan:": "Piano:",
"Amount:": "Importo:",
"Subscription ID:": "ID Abbonamento:",
"Go to Library": "Vai alla Libreria",
"Need help? Contact our support team at support@readest.com": "Hai bisogno di aiuto? Contatta il nostro supporto a support@readest.com",
"Free Plan": "Piano Gratuito",
@@ -603,5 +599,66 @@
"File count: {{size}}": "Conteggio file: {{size}}",
"Background Read Aloud": "Riproduzione in background",
"Ready to read aloud": "Pronto per la lettura ad alta voce",
"Read Aloud": "Leggi ad alta voce"
"Read Aloud": "Leggi ad alta voce",
"Screen Brightness": "Luminosità dello schermo",
"Background Image": "Immagine di sfondo",
"Import Image": "Importa immagine",
"Opacity": "Opacità",
"Size": "Dimensione",
"Cover": "Copertura",
"Contain": "Contenere",
"{{number}} pages left in chapter": "{{number}} pagine rimaste nel capitolo",
"Device": "Dispositivo",
"E-Ink Mode": "Modalità E-Ink",
"Highlight Colors": "Colori Evidenziazione",
"Auto Screen Brightness": "Luminosità schermo automatica",
"Pagination": "Impaginazione",
"Disable Double Tap": "Disabilita Doppio Tap",
"Tap to Paginate": "Tocca per Impaginare",
"Click to Paginate": "Clicca per Impaginare",
"Tap Both Sides": "Tocca Entrambi i Lati",
"Click Both Sides": "Clicca Entrambi i Lati",
"Swap Tap Sides": "Scambia Sides Tap",
"Swap Click Sides": "Scambia Sides Click",
"Source and Translated": "Fonte e Tradotto",
"Translated Only": "Solo Tradotto",
"Source Only": "Solo Fonte",
"TTS Text": "Testo TTS",
"The book file is corrupted": "Il file del libro è danneggiato",
"The book file is empty": "Il file del libro è vuoto",
"Failed to open the book file": "Impossibile aprire il file del libro",
"On-Demand Purchase": "Acquisto su Richiesta",
"Full Customization": "Personalizzazione Completa",
"Lifetime Plan": "Piano a Vita",
"One-Time Payment": "Pagamento Una Tantum",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Effettua un pagamento una tantum per godere dell'accesso a vita a funzionalità specifiche su tutti i dispositivi. Acquista funzionalità o servizi specifici solo quando ne hai bisogno.",
"Expand Cloud Sync Storage": "Espandi Spazio di Archiviazione Cloud",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Espandi il tuo spazio di archiviazione cloud per sempre con un acquisto una tantum. Ogni acquisto aggiuntivo aumenta lo spazio.",
"Unlock All Customization Options": "Sblocca Tutte le Opzioni di Personalizzazione",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Sblocca temi aggiuntivi, font, opzioni di layout e lettura ad alta voce, traduttori, servizi di archiviazione cloud.",
"Purchase Successful!": "Acquisto Riuscito!",
"lifetime": "a vita",
"Thank you for your purchase! Your payment has been processed successfully.": "Grazie per il tuo acquisto! Il pagamento è stato elaborato con successo.",
"Order ID:": "ID Ordine:",
"TTS Highlighting": "Evidenziazione TTS",
"Style": "Stile",
"Underline": "Sottolineato",
"Strikethrough": "Barrato",
"Squiggly": "Ondulato",
"Outline": "Contorno",
"Save Current Color": "Salva Colore Corrente",
"Quick Colors": "Colori Veloci",
"Highlighter": "Evidenziatore",
"Save Book Cover": "Salva Copertina del Libro",
"Auto-save last book cover": "Salva automaticamente l'ultima copertina del libro",
"Back": "Indietro",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Email di conferma inviata! Controlla il tuo vecchio e nuovo indirizzo email per confermare la modifica.",
"Failed to update email": "Aggiornamento dell'email non riuscito",
"New Email": "Nuova email",
"Your new email": "La tua nuova email",
"Updating email ...": "Aggiornamento dell'email ...",
"Update email": "Aggiorna email",
"Current email": "Email attuale",
"Update Email": "Aggiorna email",
"All": "Tutti"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "ファイルを開くと自動インポート",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "章が検出されません",
"Failed to parse the EPUB file.": "EPUBファイルの解析に失敗しました",
"This book format is not supported.": "この書籍形式はサポートされていません",
"No chapters detected": "章が検出されません",
"Failed to parse the EPUB file": "EPUBファイルの解析に失敗しました",
"This book format is not supported": "この書籍形式はサポートされていません",
"Unable to fetch the translation. Please log in first and try again.": "翻訳を取得できません。まずログインして再試行してください。",
"Group": "グループ",
"Always on Top": "常に最前面",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "(「お気に召すまま」第2幕)",
"Link Color": "リンクカラー",
"Volume Keys for Page Flip": "音量キーでページめくり",
"Clicks for Page Flip": "タップでページめくり",
"Swap Clicks Area": "タップ領域を入れ替え",
"Screen": "画面",
"Orientation": "向き",
"Portrait": "縦",
@@ -296,7 +294,6 @@
"Disable Translation": "翻訳を無効にする",
"Scroll": "スクロール",
"Overlap Pixels": "重なりピクセル",
"Click": "クリック",
"System Language": "システム言語",
"Security": "セキュリティ",
"Allow JavaScript": "JavaScriptを許可",
@@ -362,7 +359,6 @@
"Email:": "メール:",
"Plan:": "プラン:",
"Amount:": "金額:",
"Subscription ID:": "サブスクリプションID:",
"Go to Library": "ライブラリへ移動",
"Need help? Contact our support team at support@readest.com": "サポートが必要ですか? support@readest.com までお問い合わせください。",
"Free Plan": "無料プラン",
@@ -595,5 +591,66 @@
"File count: {{size}}": "ファイル数: {{size}}",
"Background Read Aloud": "バックグラウンド読み上げ",
"Ready to read aloud": "読み上げの準備ができました",
"Read Aloud": "読み上げ"
"Read Aloud": "読み上げ",
"Screen Brightness": "画面の明るさ",
"Background Image": "背景画像",
"Import Image": "画像をインポート",
"Opacity": "不透明度",
"Size": "サイズ",
"Cover": "カバー",
"Contain": "含む",
"{{number}} pages left in chapter": "章に{{number}}ページ残り",
"Device": "デバイス",
"E-Ink Mode": "E-Inkモード",
"Highlight Colors": "ハイライトカラー",
"Auto Screen Brightness": "自動画面の明るさ",
"Pagination": "ページネーション",
"Disable Double Tap": "ダブルタップを無効にする",
"Tap to Paginate": "タップしてページを切り替える",
"Click to Paginate": "クリックしてページを切り替える",
"Tap Both Sides": "両側をタップ",
"Click Both Sides": "両側をクリック",
"Swap Tap Sides": "タップサイドを入れ替える",
"Swap Click Sides": "クリックサイドを入れ替える",
"Source and Translated": "原文と翻訳",
"Translated Only": "翻訳のみ",
"Source Only": "原文のみ",
"TTS Text": "TTSテキスト",
"The book file is corrupted": "本のファイルが破損しています",
"The book file is empty": "本のファイルが空です",
"Failed to open the book file": "本のファイルを開くことができませんでした",
"On-Demand Purchase": "オンデマンド購入",
"Full Customization": "フルカスタマイズ",
"Lifetime Plan": "ライフタイムプラン",
"One-Time Payment": "ワンタイムペイメント",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "すべてのデバイスで特定の機能にライフタイムアクセスを楽しむために、1回の支払いを行います。必要なときにのみ特定の機能やサービスを購入します。",
"Expand Cloud Sync Storage": "クラウド同期ストレージを拡張",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "1回の購入でクラウドストレージを永遠に拡張します。追加の購入ごとに、さらに多くのスペースが追加されます。",
"Unlock All Customization Options": "すべてのカスタマイズオプションを解除",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "追加のテーマ、フォント、レイアウトオプション、読み上げ、翻訳者、クラウドストレージサービスを解除します。",
"Purchase Successful!": "購入成功!",
"lifetime": "ライフタイム",
"Thank you for your purchase! Your payment has been processed successfully.": "ご購入ありがとうございます!お支払いが正常に処理されました。",
"Order ID:": "購入ID",
"TTS Highlighting": "TTSハイライト",
"Style": "スタイル",
"Underline": "下線",
"Strikethrough": "取り消し線",
"Squiggly": "波線",
"Outline": "アウトライン",
"Save Current Color": "現在の色を保存",
"Quick Colors": "クイックカラー",
"Highlighter": "蛍光ペン",
"Save Book Cover": "本のカバーを保存",
"Auto-save last book cover": "最後の本のカバーを自動保存",
"Back": "戻る",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "確認メールを送信しました。変更を確認するために、古いメールアドレスと新しいメールアドレスを確認してください。",
"Failed to update email": "メールの更新に失敗しました",
"New Email": "新しいメールアドレス",
"Your new email": "あなたの新しいメールアドレス",
"Updating email ...": "メールを更新しています ...",
"Update email": "メールを更新",
"Current email": "現在のメールアドレス",
"Update Email": "メールを更新",
"All": "すべて"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "파일 열 때 자동 가져오기",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "장 발견되지 않음.",
"Failed to parse the EPUB file.": "EPUB 파일 구문 분석 실패.",
"This book format is not supported.": "이 책 형식은 지원되지 않습니다.",
"No chapters detected": "장 발견되지 않음",
"Failed to parse the EPUB file": "EPUB 파일 구문 분석 실패",
"This book format is not supported": "이 책 형식은 지원되지 않습니다",
"Unable to fetch the translation. Please log in first and try again.": "번역을 가져올 수 없습니다. 먼저 로그인하고 다시 시도하세요.",
"Group": "그룹",
"Always on Top": "항상 위에",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "(당신이 좋아하는 대로, 2막)",
"Link Color": "링크 색상",
"Volume Keys for Page Flip": "페이지 넘김을 위한 볼륨 키",
"Clicks for Page Flip": "페이지 넘김을 위한 클릭",
"Swap Clicks Area": "클릭 영역 교환",
"Screen": "화면",
"Orientation": "방향",
"Portrait": "세로",
@@ -296,7 +294,6 @@
"Disable Translation": "번역 비활성화",
"Scroll": "스크롤",
"Overlap Pixels": "겹치는 픽셀",
"Click": "클릭",
"System Language": "시스템 언어",
"Security": "보안",
"Allow JavaScript": "JavaScript 허용",
@@ -362,7 +359,6 @@
"Email:": "이메일:",
"Plan:": "요금제:",
"Amount:": "금액:",
"Subscription ID:": "구독 ID:",
"Go to Library": "라이브러리로 이동",
"Need help? Contact our support team at support@readest.com": "도움이 필요하시면 support@readest.com으로 문의해주세요.",
"Free Plan": "무료 요금제",
@@ -595,5 +591,66 @@
"File count: {{size}}": "파일 수: {{size}}",
"Background Read Aloud": "백그라운드 음성 읽기",
"Ready to read aloud": "읽기 준비 완료",
"Read Aloud": "음성 읽기"
"Read Aloud": "음성 읽기",
"Screen Brightness": "화면 밝기",
"Background Image": "배경 이미지",
"Import Image": "이미지 가져오기",
"Opacity": "불투명도",
"Size": "크기",
"Cover": "표지",
"Contain": "포함",
"{{number}} pages left in chapter": "남은 페이지 {{number}}장",
"Device": "장치",
"E-Ink Mode": "E-Ink 모드",
"Highlight Colors": "하이라이트 색상",
"Auto Screen Brightness": "자동 화면 밝기",
"Pagination": "페이지 매김",
"Disable Double Tap": "더블 탭 비활성화",
"Tap to Paginate": "탭하여 페이지 매김",
"Click to Paginate": "클릭하여 페이지 매김",
"Tap Both Sides": "양쪽을 탭",
"Click Both Sides": "양쪽을 클릭",
"Swap Tap Sides": "탭 사이드 전환",
"Swap Click Sides": "클릭 사이드 전환",
"Source and Translated": "원본 및 번역",
"Translated Only": "번역만",
"Source Only": "원본만",
"TTS Text": "TTS 텍스트",
"The book file is corrupted": "책 파일이 손상되었습니다",
"The book file is empty": "책 파일이 비어 있습니다",
"Failed to open the book file": "책 파일을 열지 못했습니다",
"On-Demand Purchase": "온디맨드 구매",
"Full Customization": "풀 커스터마이징",
"Lifetime Plan": "평생 플랜",
"One-Time Payment": "일회성 결제",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "모든 장치에서 특정 기능에 대한 평생 액세스를 즐기기 위해 단일 결제를 수행합니다. 필요할 때만 특정 기능이나 서비스를 구매합니다.",
"Expand Cloud Sync Storage": "클라우드 동기화 저장소 확장",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "1회 구매로 클라우드 저장소를 영구적으로 확장합니다. 추가 구매 시 더 많은 공간이 추가됩니다.",
"Unlock All Customization Options": "모든 커스터마이징 옵션 잠금 해제",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "추가 테마, 글꼴, 레이아웃 옵션 및 읽기, 번역기, 클라우드 스토리지 서비스를 잠금 해제합니다.",
"Purchase Successful!": "구매 성공!",
"lifetime": "평생",
"Thank you for your purchase! Your payment has been processed successfully.": "구매해주셔서 감사합니다! 결제가 성공적으로 처리되었습니다.",
"Order ID:": "주문 ID:",
"TTS Highlighting": "TTS 하이라이팅",
"Style": "스타일",
"Underline": "밑줄",
"Strikethrough": "취소선",
"Squiggly": "물결선",
"Outline": "윤곽선",
"Save Current Color": "현재 색상 저장",
"Quick Colors": "빠른 색상",
"Highlighter": "형광펜",
"Save Book Cover": "책 표지 저장",
"Auto-save last book cover": "마지막 책 표지 자동 저장",
"Back": "뒤로",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "확인 이메일이 전송되었습니다! 변경을 확인하려면 이전 이메일 주소와 새 이메일 주소를 확인하세요.",
"Failed to update email": "이메일 업데이트 실패",
"New Email": "새 이메일",
"Your new email": "새 이메일 주소",
"Updating email ...": "이메일 업데이트 중...",
"Update email": "이메일 업데이트",
"Current email": "현재 이메일",
"Update Email": "이메일 업데이트",
"All": "전체"
}
@@ -81,9 +81,9 @@
"Descending": "Aflopend",
"Sort by...": "Sorteren op...",
"No supported files found. Supported formats: {{formats}}": "Geen ondersteunde bestanden gevonden. Ondersteunde formaten: {{formats}}",
"No chapters detected.": "Geen hoofdstukken gedetecteerd.",
"Failed to parse the EPUB file.": "EPUB-bestand kon niet worden verwerkt.",
"This book format is not supported.": "Dit boekformaat wordt niet ondersteund.",
"No chapters detected": "Geen hoofdstukken gedetecteerd",
"Failed to parse the EPUB file": "EPUB-bestand kon niet worden verwerkt",
"This book format is not supported": "Dit boekformaat wordt niet ondersteund",
"Failed to import book(s): {{filenames}}": "Importeren van boek(en) mislukt: {{filenames}}",
"Book uploaded: {{title}}": "Boek geüpload: {{title}}",
"Insufficient storage quota": "Onvoldoende opslagruimte",
@@ -184,8 +184,6 @@
"Behavior": "Gedrag",
"Continuous Scroll": "Doorlopend scrollen",
"Volume Keys for Page Flip": "Volumetoetsen voor pagina omslaan",
"Clicks for Page Flip": "Klikken voor pagina omslaan",
"Swap Clicks Area": "Klikgebieden omwisselen",
"Apply": "Toepassen",
"Font": "Lettertype",
"Layout": "Opmaak",
@@ -297,7 +295,6 @@
"Disable Translation": "Vertaling uitschakelen",
"Scroll": "Scrollen",
"Overlap Pixels": "Overlap pixels",
"Click": "Klik",
"System Language": "Systemtaal",
"Security": "Beveiliging",
"Allow JavaScript": "JavaScript toestaan",
@@ -364,7 +361,6 @@
"Email:": "E-mail:",
"Plan:": "Abonnement:",
"Amount:": "Bedrag:",
"Subscription ID:": "Abonnements-ID:",
"Go to Library": "Ga naar bibliotheek",
"Need help? Contact our support team at support@readest.com": "Hulp nodig? Neem contact op met support via support@readest.com",
"Free Plan": "Gratis abonnement",
@@ -599,5 +595,66 @@
"File count: {{size}}": "Bestandsaantal: {{size}}",
"Background Read Aloud": "Achtergrond Voorlezen",
"Ready to read aloud": "Klaar om voor te lezen",
"Read Aloud": "Voorlezen"
"Read Aloud": "Voorlezen",
"Screen Brightness": "Schermhelderheid",
"Background Image": "Achtergrondafbeelding",
"Import Image": "Afbeelding importeren",
"Opacity": "Doorzichtigheid",
"Size": "Grootte",
"Cover": "Omslag",
"Contain": "Bevatten",
"{{number}} pages left in chapter": "Nog {{number}} paginas in hoofdstuk",
"Device": "Apparaat",
"E-Ink Mode": "E-Ink Modus",
"Highlight Colors": "Markeerkleuren",
"Auto Screen Brightness": "Automatische Schermhelderheid",
"Pagination": "Paginering",
"Disable Double Tap": "Schakel Dubbel Tikken Uit",
"Tap to Paginate": "Tik om te Pagineren",
"Click to Paginate": "Klik om te Pagineren",
"Tap Both Sides": "Tik op Beide Zijden",
"Click Both Sides": "Klik op Beide Zijden",
"Swap Tap Sides": "Wissel Tap Zijden",
"Swap Click Sides": "Wissel Klik Zijden",
"Source and Translated": "Bron en Vertaling",
"Translated Only": "Alleen Vertaling",
"Source Only": "Alleen Bron",
"TTS Text": "TTS Tekst",
"The book file is corrupted": "Het boekbestand is beschadigd",
"The book file is empty": "Het boekbestand is leeg",
"Failed to open the book file": "Het openen van het boekbestand is mislukt",
"On-Demand Purchase": "On-Demand Aankoop",
"Full Customization": "Volledige Aanpassing",
"Lifetime Plan": "Levenslange Abonnement",
"One-Time Payment": "Eenmalige Betaling",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Doe een eenmalige betaling om levenslange toegang te krijgen tot specifieke functies op alle apparaten. Koop specifieke functies of diensten alleen wanneer je ze nodig hebt.",
"Expand Cloud Sync Storage": "Cloud Sync Opslag Uitbreiden",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Breid je cloudopslag voor altijd uit met een eenmalige aankoop. Elke extra aankoop voegt meer ruimte toe.",
"Unlock All Customization Options": "Ontgrendel Alle Aanpassingsopties",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Ontgrendel extra thema's, lettertypen, lay-outopties en voorlezen, vertalers, cloudopslagdiensten.",
"Purchase Successful!": "Aankoop Succesvol!",
"lifetime": "levenslang",
"Thank you for your purchase! Your payment has been processed successfully.": "Bedankt voor je aankoop! Je betaling is succesvol verwerkt.",
"Order ID:": "Bestel-ID:",
"TTS Highlighting": "TTS-markering",
"Style": "Stijl",
"Underline": "Onderstrepen",
"Strikethrough": "Doorhalen",
"Squiggly": "Kronkelig",
"Outline": "Omtrek",
"Save Current Color": "Huidige kleur opslaan",
"Quick Colors": "Snelle kleuren",
"Highlighter": "Marker",
"Save Book Cover": "Opslaan boekomslag",
"Auto-save last book cover": "Automatisch opslaan laatste boekomslag",
"Back": "Terug",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Bevestigingsmail verzonden! Controleer je oude en nieuwe e-mailadres om de wijziging te bevestigen.",
"Failed to update email": "E-mail bijwerken mislukt",
"New Email": "Nieuw e-mailadres",
"Your new email": "Je nieuwe e-mailadres",
"Updating email ...": "E-mail wordt bijgewerkt...",
"Update email": "E-mail bijwerken",
"Current email": "Huidig e-mailadres",
"Update Email": "E-mail bijwerken",
"All": "Alles"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "Automatyczne importowanie przy otwieraniu pliku",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "Nie wykryto rozdziałów.",
"Failed to parse the EPUB file.": "Nie udało się przeanalizować pliku EPUB.",
"This book format is not supported.": "Ten format książki nie jest obsługiwany.",
"No chapters detected": "Nie wykryto rozdziałów",
"Failed to parse the EPUB file": "Nie udało się przeanalizować pliku EPUB",
"This book format is not supported": "Ten format książki nie jest obsługiwany",
"Unable to fetch the translation. Please log in first and try again.": "Nie można pobrać tłumaczenia. Proszę najpierw się zalogować i spróbować ponownie.",
"Group": "Grupa",
"Always on Top": "Zawsze na wierzchu",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "(z „Jak wam się podoba”, Akt II)",
"Link Color": "Kolor linku",
"Volume Keys for Page Flip": "Klucze głośności do przewracania stron",
"Clicks for Page Flip": "Kliknięcia do przewracania stron",
"Swap Clicks Area": "Zamień obszar kliknięcia",
"Screen": "Ekran",
"Orientation": "Orientacja",
"Portrait": "Portret",
@@ -299,7 +297,6 @@
"Disable Translation": "Wyłącz tłumaczenie",
"Scroll": "Przewijaj",
"Overlap Pixels": "Przesunięcie pikseli",
"Click": "Kliknij",
"System Language": "Język systemu",
"Security": "Bezpieczeństwo",
"Allow JavaScript": "Zezwól na JavaScript",
@@ -374,7 +371,6 @@
"Email:": "E-mail:",
"Plan:": "Plan:",
"Amount:": "Kwota:",
"Subscription ID:": "ID subskrypcji:",
"Go to Library": "Przejdź do biblioteki",
"Need help? Contact our support team at support@readest.com": "Potrzebujesz pomocy? Skontaktuj się z naszym zespołem wsparcia: support@readest.com",
"Free Plan": "Bezpłatny plan",
@@ -607,5 +603,66 @@
"File count: {{size}}": "Liczba plików: {{size}}",
"Background Read Aloud": "Czytanie na głos w tle",
"Ready to read aloud": "Gotowy do przeczytania na głos",
"Read Aloud": "Czytaj na głos"
"Read Aloud": "Czytaj na głos",
"Screen Brightness": "Jasność ekranu",
"Background Image": "Obraz tła",
"Import Image": "Importuj obraz",
"Opacity": "Przezroczystość",
"Size": "Rozmiar",
"Cover": "Okładka",
"Contain": "Zawierać",
"{{number}} pages left in chapter": "Pozostało {{number}} stron w rozdziale",
"Device": "Urządzenie",
"E-Ink Mode": "Tryb E-Ink",
"Highlight Colors": "Kolory wyróżnień",
"Auto Screen Brightness": "Automatyczna jasność ekranu",
"Pagination": "Stronicowanie",
"Disable Double Tap": "Wyłącz podwójne dotknięcie",
"Tap to Paginate": "Dotknij, aby stronicować",
"Click to Paginate": "Kliknij, aby stronicować",
"Tap Both Sides": "Dotknij obu stron",
"Click Both Sides": "Kliknij obu stron",
"Swap Tap Sides": "Wymień Tap Zewnętrzne",
"Swap Click Sides": "Wymień Click Zewnętrzne",
"Source and Translated": "Źródło i Tłumaczenie",
"Translated Only": "Tylko Tłumaczenie",
"Source Only": "Tylko Źródło",
"TTS Text": "Tekst TTS",
"The book file is corrupted": "Plik książki jest uszkodzony",
"The book file is empty": "Plik książki jest pusty",
"Failed to open the book file": "Nie udało się otworzyć pliku książki",
"On-Demand Purchase": "Zakup na żądanie",
"Full Customization": "Pełna Personalizacja",
"Lifetime Plan": "Plan na Całe Życie",
"One-Time Payment": "Jednorazowa Płatność",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Dokonaj jednorazowej płatności, aby cieszyć się dożywotnim dostępem do określonych funkcji na wszystkich urządzeniach. Kupuj konkretne funkcje lub usługi tylko wtedy, gdy ich potrzebujesz.",
"Expand Cloud Sync Storage": "Rozszerz Przechowywanie w Chmurze",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Rozszerz swoją przestrzeń w chmurze na zawsze dzięki jednorazowemu zakupowi. Każdy dodatkowy zakup dodaje więcej miejsca.",
"Unlock All Customization Options": "Odblokuj Wszystkie Opcje Personalizacji",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Odblokuj dodatkowe motywy, czcionki, opcje układu oraz funkcje czytania na głos, tłumacze, usługi przechowywania w chmurze.",
"Purchase Successful!": "Zakup Udany!",
"lifetime": "dożywotnio",
"Thank you for your purchase! Your payment has been processed successfully.": "Dziękujemy za zakup! Twoja płatność została pomyślnie przetworzona.",
"Order ID:": "ID zamówienia:",
"TTS Highlighting": "Wyróżnianie TTS",
"Style": "Styl",
"Underline": "Podkreślenie",
"Strikethrough": "Przekreślenie",
"Squiggly": "Falista linia",
"Outline": "Obrys",
"Save Current Color": "Zapisz bieżący kolor",
"Quick Colors": "Szybkie kolory",
"Highlighter": "Zakreślacz",
"Save Book Cover": "Zapisz okładkę książki",
"Auto-save last book cover": "Automatyczne zapisywanie ostatniej okładki książki",
"Back": "Wstecz",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "E-mail potwierdzający został wysłany! Sprawdź swój stary i nowy adres e-mail, aby potwierdzić zmianę.",
"Failed to update email": "Nie udało się zaktualizować e-maila",
"New Email": "Nowy e-mail",
"Your new email": "Twój nowy adres e-mail",
"Updating email ...": "Aktualizowanie e-maila...",
"Update email": "Zaktualizuj e-mail",
"Current email": "Obecny e-mail",
"Update Email": "Zaktualizuj e-mail",
"All": "Wszystkie"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "Importação Automática ao Abrir Arquivo",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "Nenhum capítulo detectado.",
"Failed to parse the EPUB file.": "Falha ao analisar o arquivo EPUB.",
"This book format is not supported.": "Este formato de livro não é suportado.",
"No chapters detected": "Nenhum capítulo detectado",
"Failed to parse the EPUB file": "Falha ao analisar o arquivo EPUB",
"This book format is not supported": "Este formato de livro não é suportado",
"Unable to fetch the translation. Please log in first and try again.": "Não foi possível buscar a tradução. Faça login primeiro e tente novamente.",
"Group": "Grupo",
"Always on Top": "Sempre no Topo",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "(de 'Como Gostais', Ato II)",
"Link Color": "Cor do Link",
"Volume Keys for Page Flip": "Teclas de Volume para Virar Página",
"Clicks for Page Flip": "Cliques para Virar Página",
"Swap Clicks Area": "Trocar Área de Cliques",
"Screen": "Tela",
"Orientation": "Orientação",
"Portrait": "Retrato",
@@ -298,7 +296,6 @@
"Disable Translation": "Desativar tradução",
"Scroll": "Rolar",
"Overlap Pixels": "Sobreposição de Pixels",
"Click": "Clique",
"System Language": "Idioma do Sistema",
"Security": "Segurança",
"Allow JavaScript": "Permitir JavaScript",
@@ -370,7 +367,6 @@
"Email:": "E-mail:",
"Plan:": "Plano:",
"Amount:": "Valor:",
"Subscription ID:": "ID da assinatura:",
"Go to Library": "Ir para a biblioteca",
"Need help? Contact our support team at support@readest.com": "Precisa de ajuda? Entre em contato com nossa equipe de suporte: support@readest.com",
"Free Plan": "Plano gratuito",
@@ -603,5 +599,66 @@
"File count: {{size}}": "Contagem de arquivos: {{size}}",
"Background Read Aloud": "Leitura em Segundo Plano",
"Ready to read aloud": "Pronto para leitura em voz alta",
"Read Aloud": "Ler em Voz Alta"
"Read Aloud": "Ler em Voz Alta",
"Screen Brightness": "Brilho da Tela",
"Background Image": "Imagem de Fundo",
"Import Image": "Importar Imagem",
"Opacity": "Opacidade",
"Size": "Tamanho",
"Cover": "Capa",
"Contain": "Contém",
"{{number}} pages left in chapter": "Faltam {{number}} páginas neste capítulo",
"Device": "Dispositivo",
"E-Ink Mode": "Modo E-Ink",
"Highlight Colors": "Cores de Destaque",
"Auto Screen Brightness": "Brilho Automático da Tela",
"Pagination": "Paginação",
"Disable Double Tap": "Desativar Toque Duplo",
"Tap to Paginate": "Toque para Paginar",
"Click to Paginate": "Clique para Paginar",
"Tap Both Sides": "Toque em Ambos os Lados",
"Click Both Sides": "Clique em Ambos os Lados",
"Swap Tap Sides": "Trocar Lados do Toque",
"Swap Click Sides": "Trocar Lados do Clique",
"Source and Translated": "Fonte e Traduzido",
"Translated Only": "Apenas Traduzido",
"Source Only": "Apenas Fonte",
"TTS Text": "TTS Texto",
"The book file is corrupted": "O arquivo do livro está corrompido",
"The book file is empty": "O arquivo do livro está vazio",
"Failed to open the book file": "Falha ao abrir o arquivo do livro",
"On-Demand Purchase": "Compra sob demanda",
"Full Customization": "Personalização Completa",
"Lifetime Plan": "Plano Vitalício",
"One-Time Payment": "Pagamento Único",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Faça um único pagamento para desfrutar de acesso vitalício a recursos específicos em todos os dispositivos. Compre recursos ou serviços específicos apenas quando precisar deles.",
"Expand Cloud Sync Storage": "Expandir Armazenamento de Sincronização em Nuvem",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Expanda seu armazenamento em nuvem para sempre com uma compra única. Cada compra adicional adiciona mais espaço.",
"Unlock All Customization Options": "Desbloquear Todas as Opções de Personalização",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Desbloquear temas adicionais, fontes, opções de layout e leitura em voz alta, tradutores, serviços de armazenamento em nuvem.",
"Purchase Successful!": "Compra Bem-Sucedida!",
"lifetime": "vitalício",
"Thank you for your purchase! Your payment has been processed successfully.": "Obrigado pela sua compra! Seu pagamento foi processado com sucesso.",
"Order ID:": "ID do Pedido:",
"TTS Highlighting": "Destaque TTS",
"Style": "Estilo",
"Underline": "Sublinhado",
"Strikethrough": "Tachado",
"Squiggly": "Ondulado",
"Outline": "Contorno",
"Save Current Color": "Salvar cor atual",
"Quick Colors": "Cores rápidas",
"Highlighter": "Marcador",
"Save Book Cover": "Salvar Capa do Livro",
"Auto-save last book cover": "Salvar automaticamente a última capa do livro",
"Back": "Voltar",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "E-mail de confirmação enviado! Verifique os seus endereços de e-mail antigo e novo para confirmar a alteração.",
"Failed to update email": "Falha ao atualizar o e-mail",
"New Email": "Novo e-mail",
"Your new email": "O seu novo e-mail",
"Updating email ...": "A atualizar o e-mail...",
"Update email": "Atualizar e-mail",
"Current email": "E-mail atual",
"Update Email": "Atualizar e-mail",
"All": "Todos"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "Автоматический импорт при открытии файла",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "Не обнаружено глав.",
"Failed to parse the EPUB file.": "Не удалось разобрать файл EPUB.",
"This book format is not supported.": "Этот формат книги не поддерживается.",
"No chapters detected": "Не обнаружено глав",
"Failed to parse the EPUB file": "Не удалось разобрать файл EPUB",
"This book format is not supported": "Этот формат книги не поддерживается",
"Unable to fetch the translation. Please log in first and try again.": "Не удалось получить перевод. Пожалуйста, войдите в систему и попробуйте снова.",
"Group": "Группа",
"Always on Top": "Всегда сверху",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "(из 'Как вам это понравится', Акт II)",
"Link Color": "Цвет ссылки",
"Volume Keys for Page Flip": "Клавиши громкости для перелистывания страниц",
"Clicks for Page Flip": "Клики для перелистывания страниц",
"Swap Clicks Area": "Переключить область кликов",
"Screen": "Экран",
"Orientation": "Ориентация",
"Portrait": "Портрет",
@@ -299,7 +297,6 @@
"Disable Translation": "Отключить перевод",
"Scroll": "Прокрутка",
"Overlap Pixels": "Перекрывающиеся пиксели",
"Click": "Клик",
"System Language": "Системный язык",
"Security": "Безопасность",
"Allow JavaScript": "Разрешить JavaScript",
@@ -374,7 +371,6 @@
"Email:": "Электронная почта:",
"Plan:": "План:",
"Amount:": "Сумма:",
"Subscription ID:": "ID подписки:",
"Go to Library": "Перейти в библиотеку",
"Need help? Contact our support team at support@readest.com": "Нужна помощь? Свяжитесь с нашей поддержкой: support@readest.com",
"Free Plan": "Бесплатный план",
@@ -607,5 +603,66 @@
"File count: {{size}}": "Количество файлов: {{size}}",
"Background Read Aloud": "Озвучивание в фоне",
"Ready to read aloud": "Готов к чтению вслух",
"Read Aloud": "Читать вслух"
"Read Aloud": "Читать вслух",
"Screen Brightness": "Яркость экрана",
"Background Image": "Фоновое изображение",
"Import Image": "Импортировать изображение",
"Opacity": "Непрозрачность",
"Size": "Размер",
"Cover": "Обложка",
"Contain": "Содержать",
"{{number}} pages left in chapter": "Осталось {{number}} страниц в главе",
"Device": "Устройство",
"E-Ink Mode": "E-Ink режим",
"Highlight Colors": "Цвета выделения",
"Auto Screen Brightness": "Автояркость экрана",
"Pagination": "Пагинация",
"Disable Double Tap": "Отключить двойной тап",
"Tap to Paginate": "Тапните, чтобы разбить на страницы",
"Click to Paginate": "Нажмите, чтобы разбить на страницы",
"Tap Both Sides": "Тапните обе стороны",
"Click Both Sides": "Нажмите обе стороны",
"Swap Tap Sides": "Переключить стороны касания",
"Swap Click Sides": "Переключить стороны клика",
"Source and Translated": "Источник и перевод",
"Translated Only": "Только перевод",
"Source Only": "Только источник",
"TTS Text": "TTS Текст",
"The book file is corrupted": "Файл книги поврежден",
"The book file is empty": "Файл книги пуст",
"Failed to open the book file": "Не удалось открыть файл книги",
"On-Demand Purchase": "Покупка по запросу",
"Full Customization": "Полная настройка",
"Lifetime Plan": "Пожизненный план",
"One-Time Payment": "Единовременный платеж",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Сделайте единовременный платеж, чтобы получить пожизненный доступ к определенным функциям на всех устройствах. Покупайте конкретные функции или услуги только тогда, когда они вам нужны.",
"Expand Cloud Sync Storage": "Расширить облачное хранилище",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Расширьте свое облачное хранилище навсегда с помощью единовременной покупки. Каждая дополнительная покупка добавляет больше места.",
"Unlock All Customization Options": "Разблокировать Все Опции Настройки",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Разблокировать дополнительные темы, шрифты, параметры макета и чтения вслух, переводчики, услуги облачного хранилища.",
"Purchase Successful!": "Покупка Успешна!",
"lifetime": "пожизненный",
"Thank you for your purchase! Your payment has been processed successfully.": "Спасибо за вашу покупку! Ваш платеж был успешно обработан.",
"Order ID:": "ID заказа:",
"TTS Highlighting": "Выделение TTS",
"Style": "Стиль",
"Underline": "Подчёркивание",
"Strikethrough": "Зачёркивание",
"Squiggly": "Волнистая линия",
"Outline": "Контур",
"Save Current Color": "Сохранить текущий цвет",
"Quick Colors": "Быстрые цвета",
"Highlighter": "Маркер",
"Save Book Cover": "Сохранить обложку книги",
"Auto-save last book cover": "Автоматически сохранять последнюю обложку книги",
"Back": "Назад",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Письмо с подтверждением отправлено! Пожалуйста, проверьте старый и новый адреса электронной почты, чтобы подтвердить изменение.",
"Failed to update email": "Не удалось обновить электронную почту",
"New Email": "Новый адрес электронной почты",
"Your new email": "Ваш новый адрес электронной почты",
"Updating email ...": "Обновление электронной почты ...",
"Update email": "Обновить электронную почту",
"Current email": "Текущий адрес электронной почты",
"Update Email": "Обновить электронную почту",
"All": "Все"
}
@@ -105,9 +105,9 @@
"Book Covers": "පොත් ආවරණ",
"Sort by...": "අනුව සකස් කරන්න...",
"No supported files found. Supported formats: {{formats}}": "සහාය දක්වන ගොනු හමුනොවිණි. සහාය දක්වන ආකෘති: {{formats}}",
"No chapters detected.": "කොටස් හඳුනානොගැනිණි.",
"Failed to parse the EPUB file.": "EPUB ගොනුව විග්‍රහ කිරීමට අසමත්.",
"This book format is not supported.": "මෙම පොත ආකෘතියට සහාය නොදක්වයි.",
"No chapters detected": "කොටස් හඳුනානොගැනිණි",
"Failed to parse the EPUB file": "EPUB ගොනුව විග්‍රහ කිරීමට අසමත්",
"This book format is not supported": "මෙම පොත ආකෘතියට සහාය නොදක්වයි",
"Failed to import book(s): {{filenames}}": "පොත් ආයාත කිරීමට අසමත්: {{filenames}}",
"Book uploaded: {{title}}": "පොත උඩුගත විය: {{title}}",
"Insufficient storage quota": "ප්‍රමාණවත් නොවන ගබඩා ප්‍රමාණය",
@@ -204,9 +204,6 @@
"Scrolled Mode": "අනුචලන ආකාරය",
"Continuous Scroll": "අඛණ්ඩ අනුචලනය",
"Overlap Pixels": "අතිච්ඡාදන පික්සල්",
"Click": "ක්ලික්",
"Clicks for Page Flip": "පිටු පෙරැළීම සඳහා ක්ලික්",
"Swap Clicks Area": "ක්ලික් ප්‍රදේශය මාරු කරන්න",
"Disable Double Click": "ද්විත්ව ක්ලික් අබල කරන්න",
"Volume Keys for Page Flip": "පිටු පෙරැළීම සඳහා ශබ්ද යතුර",
"Animation": "සජීවිකරණය",
@@ -379,7 +376,6 @@
"Email:": "ඊමේල්:",
"Plan:": "සැලැස්ම:",
"Amount:": "මුදල:",
"Subscription ID:": "දායකත්ව ID:",
"Go to Library": "පුස්තකාලයට යන්න",
"Need help? Contact our support team at support@readest.com": "උදව් අවශ්‍යද? support@readest.com හි අපගේ සහාය කණ්ඩායමට සම්බන්ධ වන්න",
"Free Plan": "නොමිලේ සැලැස්ම",
@@ -599,5 +595,66 @@
"File count: {{size}}": "ගොනු ගණන: {{size}}",
"Background Read Aloud": "පසුබිම් පවසා කියවීම",
"Ready to read aloud": "වාදනය කිරීමට සූදානම්",
"Read Aloud": "පවසා කියවීම"
"Read Aloud": "පවසා කියවීම",
"Screen Brightness": "තිරයේ දිදුලනුම",
"Background Image": "පසුබිම් රූපය",
"Import Image": "රූපය ආයාත කරන්න",
"Opacity": "පැහැදිලිත්වය",
"Size": "ප්‍රමාණය",
"Cover": "ආවරණය",
"Contain": "අඩංගු වන්න",
"{{number}} pages left in chapter": "පරිච්ඡේදයේ පිටු {{number}} ඉතිරිව ඇත",
"Device": "උපකරණය",
"E-Ink Mode": "E-Ink ආකාරය",
"Highlight Colors": "ඉස්මතු වර්ණ",
"Auto Screen Brightness": "ස්වයංක්‍රීය තිර දිදුලනුම",
"Pagination": "පිටුගත කිරීම",
"Disable Double Tap": "දෙවරක් ටැප් කිරීම අබල කරන්න",
"Tap to Paginate": "පිටුගත කිරීමට ටැප් කරන්න",
"Click to Paginate": "පිටුගත කිරීමට ක්ලික් කරන්න",
"Tap Both Sides": "පැත්ත දෙකම ටැප් කරන්න",
"Click Both Sides": "පැත්ත දෙකම ක්ලික් කරන්න",
"Swap Tap Sides": "පැත්ත දෙකම ටැප් කරන්න",
"Swap Click Sides": "පැත්ත දෙකම ක්ලික් කරන්න",
"Source and Translated": "මූලාශ්‍ර සහ පරිවර්තනය",
"Translated Only": "පරිවර්තනය පමණක්",
"Source Only": "මූලාශ්‍රය පමණක්",
"TTS Text": "TTS පෙළ",
"The book file is corrupted": "පොතේ ගොනුව අසමත් වී ඇත",
"The book file is empty": "පොතේ ගොනුව හිස් වේ",
"Failed to open the book file": "පොතේ ගොනුව විවෘත කිරීමට අසමත් විය",
"On-Demand Purchase": "අවශ්‍යතාවය මත මිලදී ගැනීම",
"Full Customization": "සම්පූර්ණ අභිරුචි",
"Lifetime Plan": "පොදුකාලීන සැලැස්ම",
"One-Time Payment": "එක්වර ගෙවීම",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "සියලු උපකරණ සඳහා විශේෂිත විශේෂාංග සඳහා පොදුකාලීන ප්‍රවේශය ලබා ගැනීමට එක්වර ගෙවීමක් කරන්න. ඔබට අවශ්‍ය වන විශේෂිත විශේෂාංග හෝ සේවා මිලදී ගන්න.",
"Expand Cloud Sync Storage": "කලාප සමාන්‍ය ගබඩා විශාල කරන්න",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "එක්වර මිලදී ගැනීමක් මඟින් ඔබගේ කලාප ගබඩාව සදාකාලිකව විශාල කරන්න. එක් එක් අමතර මිලදී ගැනීමක් තවත් ස්ථානයක් එක් කරයි.",
"Unlock All Customization Options": "සියලු අභිරුචි විකල්ප විවෘත කරන්න",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "අමතර තේමාවන්, ෆොන්ට්, ආකෘති විකල්ප සහ පවසා කියවීම, පරිවර්තක, කලාප ගබඩා සේවා විවෘත කරන්න.",
"Purchase Successful!": "මිලදී ගැනීම සාර්ථකයි!",
"lifetime": "පොදුකාලීන",
"Thank you for your purchase! Your payment has been processed successfully.": "ඔබේ මිලදී ගැනීමට ස්තූතියි! ඔබේ ගෙවීම සාර්ථකව සැකසිණි.",
"Order ID:": "ඇණවුම් හැඳුනුම්පත:",
"TTS Highlighting": "TTS උද්දීපනය",
"Style": "රූපය",
"Underline": "ඇටය",
"Strikethrough": "ඇඳුම",
"Squiggly": "වැලි රේඛාව",
"Outline": "පරිසීමාව",
"Save Current Color": "දැන් ඇති වර්ණය සුරකින්න",
"Quick Colors": "ක්ෂණික වර්ණ",
"Highlighter": "ඉස්මතු කරන්නා",
"Save Book Cover": "පොත් ආවරණය සුරකින්න",
"Auto-save last book cover": "අවසන් පොත් ආවරණය ස්වයංක්‍රීයව සුරකින්න",
"Back": "ආපසු",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "තහවුරු කිරීමේ විද්‍යුත් තැපැල් පණිවිඩය යවන්නා! වෙනස්කම තහවුරු කිරීම සඳහා ඔබගේ පැරණි සහ නව විද්‍යුත් තැපැල් ලිපින පරීක්ෂා කරන්න.",
"Failed to update email": "විද්‍යුත් තැපැල් යාවත්කාලීන කිරීමට අසමත් විය",
"New Email": "නව විද්‍යුත් තැපැල්",
"Your new email": "ඔබේ නව විද්‍යුත් තැපැල්",
"Updating email ...": "විද්‍යුත් තැපැල් යාවත්කාලීන කරමින් ...",
"Update email": "විද්‍යුත් තැපැල් යාවත්කාලීන කරන්න",
"Current email": "වත්මන් විද්‍යුත් තැපැල්",
"Update Email": "විද්‍යුත් තැපැල් යාවත්කාලීන කරන්න",
"All": "සියල්ල"
}
@@ -0,0 +1,660 @@
{
"Email address": "E-postadress",
"Your Password": "Ditt lösenord",
"Your email address": "Din e-postadress",
"Your password": "Ditt lösenord",
"Sign in": "Logga in",
"Signing in...": "Loggar in...",
"Sign in with {{provider}}": "Logga in med {{provider}}",
"Already have an account? Sign in": "Har du konto? Logga in",
"Create a Password": "Skapa lösenord",
"Sign up": "Registrera",
"Signing up...": "Registrerar...",
"Don't have an account? Sign up": "Inget konto? Registrera",
"Check your email for the confirmation link": "Kolla din e-post för bekräftelselänk",
"Signing in ...": "Loggar in...",
"Send a magic link email": "Skicka magisk länk",
"Check your email for the magic link": "Kolla din e-post för magisk länk",
"Send reset password instructions": "Skicka återställningsinstruktioner",
"Sending reset instructions ...": "Skickar instruktioner...",
"Forgot your password?": "Glömt lösenord?",
"Check your email for the password reset link": "Kolla din e-post för återställningslänk",
"Phone number": "Telefonnummer",
"Your phone number": "Ditt telefonnummer",
"Token": "Token",
"Your OTP token": "Din OTP-token",
"Verify token": "Verifiera token",
"Go Back": "Tillbaka",
"Sign in with Google": "Logga in med Google",
"Sign in with Apple": "Logga in med Apple",
"Sign in with GitHub": "Logga in med GitHub",
"New Password": "Nytt lösenord",
"Your new password": "Ditt nya lösenord",
"Update password": "Uppdatera lösenord",
"Updating password ...": "Uppdaterar lösenord...",
"Your password has been updated": "Ditt lösenord har uppdaterats",
"Something went wrong. Don't worry, our team has been notified and we're working on a fix.": "Något gick fel. Oroa dig inte, vårt team har meddelats och vi arbetar på en lösning.",
"Error Details:": "Feldetaljer:",
"Try Again": "Försök igen",
"Your Library": "Ditt bibliotek",
"Need help?": "Behöver du hjälp?",
"Contact Support": "Kontakta support",
"Show Book Details": "Visa bokdetaljer",
"Bookshelf": "Bokhylla",
"Import Books": "Importera böcker",
"Open": "Öppna",
"Group": "Grupp",
"Details": "Detaljer",
"Delete": "Ta bort",
"Cancel": "Avbryt",
"Confirm Deletion": "Bekräfta borttagning",
"Are you sure to delete {{count}} selected book(s)?_one": "Vill du ta bort {{count}} vald bok?",
"Are you sure to delete {{count}} selected book(s)?_other": "Vill du ta bort {{count}} valda böcker?",
"Deselect Book": "Avmarkera bok",
"Select Book": "Välj bok",
"Download Book": "Ladda ner bok",
"Upload Book": "Ladda upp bok",
"Deselect Group": "Avmarkera grupp",
"Select Group": "Välj grupp",
"Untitled Group": "Namnlös grupp",
"Group Books": "Gruppera böcker",
"Remove From Group": "Ta bort från grupp",
"Create New Group": "Skapa ny grupp",
"Save": "Spara",
"Confirm": "Bekräfta",
"From Local File": "Från lokal fil",
"Search in {{count}} Book(s)..._one": "Sök i {{count}} bok...",
"Search in {{count}} Book(s)..._other": "Sök i {{count}} böcker...",
"Search Books...": "Sök böcker...",
"Clear Search": "Rensa sökning",
"Select Books": "Välj böcker",
"Deselect": "Avmarkera",
"Select All": "Välj alla",
"View Menu": "Visa meny",
"Settings Menu": "Inställningsmeny",
"Failed to select directory": "Kunde inte välja katalog",
"The new data directory must be different from the current one.": "Den nya datakatalogen måste skilja sig från den nuvarande.",
"Migration failed: {{error}}": "Migrering misslyckades: {{error}}",
"Change Data Location": "Ändra dataplats",
"Current Data Location": "Nuvarande dataplats",
"Loading...": "Laddar...",
"File count: {{size}}": "Antal filer: {{size}}",
"Total size: {{size}}": "Total storlek: {{size}}",
"Calculating file info...": "Beräknar filinfo...",
"New Data Location": "Ny dataplats",
"Choose New Folder": "Välj ny mapp",
"Choose Different Folder": "Välj annan mapp",
"Migrating data...": "Migrerar data...",
"Copying: {{file}}": "Kopierar: {{file}}",
"{{current}} of {{total}} files": "{{current}} av {{total}} filer",
"Migration completed successfully!": "Migrering slutförd!",
"Your data has been moved to the new location. Please restart the application to complete the process.": "Din data har flyttats till den nya platsen. Starta om appen för att slutföra processen.",
"Migration failed": "Migrering misslyckades",
"Important Notice": "Viktigt meddelande",
"This will move all your app data to the new location. Make sure the destination has enough free space.": "Detta flyttar all din appdata till den nya platsen. Se till att målet har tillräckligt med utrymme.",
"Close": "Stäng",
"Restart App": "Starta om app",
"Start Migration": "Starta migrering",
"Dark Mode": "Mörkt läge",
"Light Mode": "Ljust läge",
"Auto Mode": "Auto-läge",
"Logged in as {{userDisplayName}}": "Inloggad som {{userDisplayName}}",
"Logged in": "Inloggad",
"View account details and quota": "Visa kontodetaljer och kvot",
"Account": "Konto",
"Sign In": "Logga in",
"Auto Upload Books to Cloud": "Auto-ladda upp böcker till moln",
"Auto Import on File Open": "Auto-importera vid filöppning",
"Open Last Book on Start": "Öppna senaste bok vid start",
"Check Updates on Start": "Sök uppdateringar vid start",
"Open Book in New Window": "Öppna bok i nytt fönster",
"Fullscreen": "Helskärm",
"Always on Top": "Alltid överst",
"Always Show Status Bar": "Visa alltid statusfält",
"Keep Screen Awake": "Håll skärm vaken",
"Background Read Aloud": "Läs upp i bakgrunden",
"Reload Page": "Ladda om sida",
"Settings": "Inställningar",
"Advanced Settings": "Avancerade inställningar",
"Upgrade to Readest Premium": "Uppgradera till Readest Premium",
"Download Readest": "Ladda ner Readest",
"About Readest": "Om Readest",
"Help improve Readest": "Hjälp förbättra Readest",
"Sharing anonymized statistics": "Dela anonymiserad statistik",
"List": "Lista",
"Grid": "Rutnät",
"Crop": "Beskär",
"Fit": "Anpassa",
"Title": "Titel",
"Author": "Författare",
"Format": "Format",
"Date Read": "Läsdatum",
"Date Added": "Datum tillagd",
"Ascending": "Stigande",
"Descending": "Fallande",
"Book Covers": "Bokomslag",
"Sort by...": "Sortera efter...",
"No supported files found. Supported formats: {{formats}}": "Inga filer hittades. Format som stöds: {{formats}}",
"No chapters detected": "Inga kapitel hittades",
"Failed to parse the EPUB file": "Kunde inte läsa EPUB-filen",
"This book format is not supported": "Detta bokformat stöds inte",
"Failed to import book(s): {{filenames}}": "Kunde inte importera bok(er): {{filenames}}",
"Book uploaded: {{title}}": "Bok uppladdad: {{title}}",
"Insufficient storage quota": "Otillräcklig lagringskvot",
"Failed to upload book: {{title}}": "Kunde inte ladda upp bok: {{title}}",
"Book downloaded: {{title}}": "Bok nedladdad: {{title}}",
"Failed to download book: {{title}}": "Kunde inte ladda ner bok: {{title}}",
"Book deleted: {{title}}": "Bok borttagen: {{title}}",
"Deleted cloud backup of the book: {{title}}": "Raderade molnkopia av bok: {{title}}",
"Deleted local copy of the book: {{title}}": "Raderade lokal kopia av bok: {{title}}",
"Failed to delete book: {{title}}": "Kunde inte ta bort bok: {{title}}",
"Failed to delete cloud backup of the book: {{title}}": "Kunde inte radera molnkopia av bok: {{title}}",
"Failed to delete local copy of the book: {{title}}": "Kunde inte radera lokal kopia av bok: {{title}}",
"Library Header": "Bibliotekshuvud",
"Welcome to your library. You can import your books here and read them anytime.": "Välkommen till ditt bibliotek. Du kan importera dina böcker här och läsa dem när som helst.",
"Copied to notebook": "Kopierat till anteckningsbok",
"No annotations to export": "Inga anteckningar att exportera",
"Exported from Readest": "Exporterad från Readest",
"Highlights & Annotations": "Markeringar & anteckningar",
"Untitled": "Namnlös",
"Note": "Anteckning",
"Copied to clipboard": "Kopierat",
"Copy": "Kopiera",
"Delete Highlight": "Ta bort markering",
"Highlight": "Markera",
"Annotate": "Anteckna",
"Search": "Sök",
"Dictionary": "Ordbok",
"Wikipedia": "Wikipedia",
"Translate": "Översätt",
"Speak": "Läs upp",
"Login Required": "Inloggning krävs",
"Quota Exceeded": "Kvot överskriden",
"Unable to fetch the translation. Please log in first and try again.": "Kunde inte hämta översättning. Logga in och försök igen.",
"Unable to fetch the translation. Try again later.": "Kunde inte hämta översättning. Försök senare.",
"Original Text": "Originaltext",
"Auto Detect": "Auto-identifiera",
"(detected)": "(identifierad)",
"Translated Text": "Översatt text",
"System Language": "Systemspråk",
"No translation available.": "Ingen översättning tillgänglig.",
"Translated by {{provider}}.": "Översatt av {{provider}}.",
"Remove Bookmark": "Ta bort bokmärke",
"Add Bookmark": "Lägg till bokmärke",
"Books Content": "Bokinnehåll",
"Book Content": "Bokinnehåll",
"Footer Bar": "Sidfot",
"Reading Progress": "Läsframsteg",
"Next Section": "Nästa sektion",
"Previous Section": "Föregående sektion",
"Next Page": "Nästa sida",
"Previous Page": "Föregående sida",
"Go Forward": "Framåt",
"Font Size": "Teckenstorlek",
"Page Margin": "Sidmarginal",
"Small": "Liten",
"Large": "Stor",
"Line Spacing": "Radavstånd",
"Table of Contents": "Innehåll",
"Notes": "Anteckningar",
"Font & Layout": "Typsnitt & layout",
"Jump to Location": "Hoppa till plats",
"Header Bar": "Sidhuvud",
"View Options": "Visningsalternativ",
"Sync Conflict": "Synkkonflikt",
"Sync reading progress from \"{{deviceName}}\"?": "Synka läsframsteg från \"{{deviceName}}\"?",
"another device": "annan enhet",
"Local Progress": "Lokalt framsteg",
"Remote Progress": "Fjärrframsteg",
"Failed to connect": "Kunde inte ansluta",
"Disconnected": "Frånkopplad",
"KOReader Sync Settings": "KOReader-synkinställningar",
"Sync as {{userDisplayName}}": "Synka som {{userDisplayName}}",
"Sync Server Connected": "Synkserver ansluten",
"Sync Strategy": "Synkstrategi",
"Ask on conflict": "Fråga vid konflikt",
"Always use latest": "Använd alltid senaste",
"Send changes only": "Skicka endast ändringar",
"Receive changes only": "Ta endast emot ändringar",
"Checksum Method": "Kontrollsummemetod",
"File Content (recommended)": "Filinnehåll (rekommenderas)",
"File Name": "Filnamn",
"Device Name": "Enhetsnamn",
"Connect to your KOReader Sync server.": "Anslut till din KOReader-synkserver.",
"Server URL": "Server-URL",
"Username": "Användarnamn",
"Your Username": "Ditt användarnamn",
"Password": "Lösenord",
"Connect": "Anslut",
"Notebook": "Anteckningsbok",
"Unpin Notebook": "Lossa anteckningsbok",
"Pin Notebook": "Fäst anteckningsbok",
"Hide Search Bar": "Dölj sökfält",
"Show Search Bar": "Visa sökfält",
"Resize Notebook": "Ändra storlek på anteckningsbok",
"No notes match your search": "Inga anteckningar matchar din sökning",
"Excerpts": "Utdrag",
"Add your notes here...": "Lägg till dina anteckningar här...",
"Search notes and excerpts...": "Sök anteckningar och utdrag...",
"{{time}} min left in chapter": "{{time}} min kvar i kapitlet",
"{{count}} pages left in chapter_one": "{{count}} sida kvar i kapitlet",
"{{count}} pages left in chapter_other": "{{count}} sidor kvar i kapitlet",
"On {{current}} of {{total}} page": "På {{current}} av {{total}} sidor",
"Section Title": "Sektionstitel",
"Book Cover": "Bokomslag",
"More Info": "Mer info",
"Parallel Read": "Parallell läsning",
"Disable": "Inaktivera",
"Enable": "Aktivera",
"Exit Parallel Read": "Avsluta parallell läsning",
"Enter Parallel Read": "Starta parallell läsning",
"KOReader Sync": "KOReader-synk",
"Push Progress": "Skicka framsteg",
"Pull Progress": "Hämta framsteg",
"Export Annotations": "Exportera anteckningar",
"Sort TOC by Page": "Sortera innehåll efter sida",
"Edit": "Redigera",
"Go to Library": "Gå till bibliotek",
"Book Menu": "Bokmeny",
"Unpin Sidebar": "Lossa sidofält",
"Pin Sidebar": "Fäst sidofält",
"Search...": "Sök...",
"Search Options": "Sökalternativ",
"Book": "Bok",
"Chapter": "Kapitel",
"Match Case": "Matchning av versaler",
"Match Whole Words": "Matchning av hela ord",
"Match Diacritics": "Matchning av diakritiska tecken",
"Sidebar": "Sidofält",
"Resize Sidebar": "Ändra storlek på sidofält",
"TOC": "Innehåll",
"Bookmark": "Bokmärke",
"Toggle Sidebar": "Visa/dölj sidofält",
"Toggle Translation": "Visa/dölj översättning",
"Disable Translation": "Inaktivera översättning",
"Enable Translation": "Aktivera översättning",
"Translation Disabled": "Översättning inaktiverad",
"Previous Paragraph": "Föregående stycke",
"Previous Sentence": "Föregående mening",
"Pause": "Pausa",
"Play": "Spela",
"Next Sentence": "Nästa mening",
"Next Paragraph": "Nästa stycke",
"Read Aloud": "Läs upp",
"Ready to read aloud": "Redo att läsa upp",
"TTS not supported for PDF": "Uppläsning stöds inte för PDF",
"TTS not supported for this document": "Uppläsning stöds inte för detta dokument",
"No Timeout": "Ingen timeout",
"{{value}} minute": "{{value}} minut",
"{{value}} minutes": "{{value}} minuter",
"{{value}} hour": "{{value}} timme",
"{{value}} hours": "{{value}} timmar",
"Voices for {{lang}}": "Röster för {{lang}}",
"Slow": "Långsam",
"Fast": "Snabb",
"{{engine}}: {{count}} voices_one": "{{engine}}: {{count}} röst",
"{{engine}}: {{count}} voices_other": "{{engine}}: {{count}} röster",
"Zoom Level": "Zoomnivå",
"Zoom Out": "Zooma ut",
"Reset Zoom": "Återställ zoom",
"Zoom In": "Zooma in",
"Zoom Mode": "Zoomläge",
"Single Page": "Enkel sida",
"Auto Spread": "Auto-uppslag",
"Fit Page": "Anpassa sida",
"Fit Width": "Anpassa bredd",
"Separate Cover Page": "Separat omslagssida",
"Scrolled Mode": "Rullningsläge",
"Sign in to Sync": "Logga in för synk",
"Synced at {{time}}": "Synkad {{time}}",
"Never synced": "Aldrig synkad",
"Invert Image In Dark Mode": "Invertera bild i mörkt läge",
"Reading Progress Synced": "Läsframsteg synkat",
"Page {{page}} of {{total}} ({{percentage}}%)": "Sida {{page}} av {{total}} ({{percentage}}%)",
"Current position": "Nuvarande position",
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "Cirka sida {{page}} av {{total}} ({{percentage}}%)",
"Approximately {{percentage}}%": "Cirka {{percentage}}%",
"Delete Your Account?": "Ta bort ditt konto?",
"This action cannot be undone. All your data in the cloud will be permanently deleted.": "Detta kan inte ångras. All din data i molnet raderas permanent.",
"Delete Permanently": "Ta bort permanent",
"Restore Purchase": "Återställ köp",
"Manage Subscription": "Hantera prenumeration",
"Reset Password": "Återställ lösenord",
"Sign Out": "Logga ut",
"Delete Account": "Ta bort konto",
"Upgrade to {{plan}}": "Uppgradera till {{plan}}",
"Complete Your Subscription": "Slutför din prenumeration",
"Coming Soon": "Kommer snart",
"Upgrade to Plus or Pro": "Uppgradera till Plus eller Pro",
"Current Plan": "Nuvarande plan",
"Plan Limits": "Plangränser",
"Failed to delete user. Please try again later.": "Kunde inte ta bort användare. Försök senare.",
"Failed to create checkout session": "Kunde inte skapa betalningssession",
"No purchases found to restore.": "Inga köp att återställa.",
"Failed to restore purchases.": "Kunde inte återställa köp.",
"Failed to manage subscription.": "Kunde inte hantera prenumeration.",
"Failed to load subscription plans.": "Kunde inte ladda prenumerationsplaner.",
"Loading profile...": "Laddar profil...",
"Processing your payment...": "Behandlar din betalning...",
"Please wait while we confirm your subscription.": "Vänta medan vi bekräftar din prenumeration.",
"Payment Processing": "Betalning behandlas",
"Your payment is being processed. This usually takes a few moments.": "Din betalning behandlas. Detta tar vanligtvis några ögonblick.",
"Payment Failed": "Betalning misslyckades",
"We couldn't process your subscription. Please try again or contact support if the issue persists.": "Vi kunde inte behandla din prenumeration. Försök igen eller kontakta support om problemet kvarstår.",
"Back to Profile": "Tillbaka till profil",
"Subscription Successful!": "Prenumeration lyckades!",
"Thank you for your subscription! Your payment has been processed successfully.": "Tack för din prenumeration! Din betalning har behandlats.",
"Email:": "E-post:",
"Plan:": "Plan:",
"Amount:": "Belopp:",
"Need help? Contact our support team at support@readest.com": "Behöver du hjälp? Kontakta vår support på support@readest.com",
"Free Plan": "Gratisplan",
"month": "månad",
"year": "år",
"Cross-Platform Sync": "Plattformsoberoende synk",
"Seamlessly sync your library, progress, highlights, and notes across all your devices—never lose your place again.": "Synka sömlöst ditt bibliotek, framsteg, markeringar och anteckningar mellan alla dina enheter.",
"Customizable Reading": "Anpassningsbar läsning",
"Personalize every detail with adjustable fonts, layouts, themes, and advanced display settings for the perfect reading experience.": "Anpassa varje detalj med justerbara typsnitt, layouter, teman och avancerade visningsinställningar.",
"AI Read Aloud": "AI-uppläsning",
"Enjoy hands-free reading with natural-sounding AI voices that bring your books to life.": "Njut av handsfree-läsning med naturligt ljudande AI-röster som ger dina böcker liv.",
"AI Translations": "AI-översättningar",
"Translate any text instantly with the power of Google, Azure, or DeepL—understand content in any language.": "Översätt vilken text som helst direkt med Google, Azure eller DeepL.",
"Community Support": "Community-support",
"Connect with fellow readers and get help fast in our friendly community channels.": "Anslut med andra läsare och få snabb hjälp i våra vänliga community-kanaler.",
"Cloud Sync Storage": "Molnsynklagring",
"AI Translations (per day)": "AI-översättningar (per dag)",
"Plus Plan": "Plus-plan",
"Includes All Free Plan Benefits": "Inkluderar alla gratisplanfördelar",
"Unlimited AI Read Aloud Hours": "Obegränsad AI-uppläsning",
"Listen without limits—convert as much text as you like into immersive audio.": "Lyssna obegränsat—konvertera så mycket text du vill till ljud.",
"More AI Translations": "Fler AI-översättningar",
"Unlock enhanced translation capabilities with more daily usage and advanced options.": "Lås upp förbättrad översättning med mer daglig användning och avancerade alternativ.",
"DeepL Pro Access": "DeepL Pro-åtkomst",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "Översätt upp till 100 000 tecken dagligen med den mest exakta översättningsmotorn.",
"Securely store and access your entire reading collection with up to 5 GB of cloud storage.": "Lagra säkert hela din lässamling med upp till 5 GB molnlagring.",
"Priority Support": "Prioriterad support",
"Enjoy faster responses and dedicated assistance whenever you need help.": "Få snabbare svar och dedikerad hjälp när du behöver det.",
"Pro Plan": "Pro-plan",
"Includes All Plus Plan Benefits": "Inkluderar alla Plus-planfördelar",
"Early Feature Access": "Tidig funktionsåtkomst",
"Be the first to explore new features, updates, and innovations before anyone else.": "Var först att utforska nya funktioner och uppdateringar innan andra.",
"Advanced AI Tools": "Avancerade AI-verktyg",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "Använd kraftfulla AI-verktyg för smartare läsning, översättning och innehållsupptäckt.",
"Translate up to 500,000 characters daily with the most accurate translation engine available.": "Översätt upp till 500 000 tecken dagligen med den mest exakta översättningsmotorn.",
"Securely store and access your entire reading collection with up to 20 GB of cloud storage.": "Lagra säkert hela din lässamling med upp till 20 GB molnlagring.",
"Version {{version}}": "Version {{version}}",
"Check Update": "Sök uppdatering",
"Already the latest version": "Redan senaste versionen",
"Checking for updates...": "Söker uppdateringar...",
"Error checking for updates": "Fel vid uppdateringskontroll",
"Drop to Import Books": "Släpp för att importera böcker",
"Terms of Service": "Användarvillkor",
"Privacy Policy": "Integritetspolicy",
"ON": "PÅ",
"OFF": "AV",
"Enter book title": "Ange boktitel",
"Subtitle": "Undertitel",
"Enter book subtitle": "Ange underrubrik",
"Enter author name": "Ange författarnamn",
"Series": "Serie",
"Enter series name": "Ange serienamn",
"Series Index": "Serieindex",
"Enter series index": "Ange serieindex",
"Total in Series": "Totalt i serien",
"Enter total books in series": "Ange totalt antal böcker i serien",
"Publisher": "Förlag",
"Enter publisher": "Ange förlag",
"Publication Date": "Publiceringsdatum",
"YYYY or YYYY-MM-DD": "ÅÅÅÅ eller ÅÅÅÅ-MM-DD",
"Language": "Språk",
"Identifier": "Identifierare",
"Subjects": "Ämnen",
"Fiction, Science, History": "Skönlitteratur, Vetenskap, Historia",
"Description": "Beskrivning",
"Enter book description": "Ange bokbeskrivning",
"Change cover image": "Ändra omslagsbild",
"Replace": "Ersätt",
"Remove cover image": "Ta bort omslagsbild",
"Unlock cover": "Lås upp omslag",
"Lock cover": "Lås omslag",
"Auto-Retrieve Metadata": "Auto-hämta metadata",
"Auto": "Auto",
"Auto-Retrieve": "Auto-hämta",
"Unlock all fields": "Lås upp alla fält",
"Unlock All": "Lås upp alla",
"Lock all fields": "Lås alla fält",
"Lock All": "Lås alla",
"Reset": "Återställ",
"Are you sure to delete the selected book?": "Vill du ta bort den valda boken?",
"Are you sure to delete the cloud backup of the selected book?": "Vill du ta bort molnkopian av den valda boken?",
"Are you sure to delete the local copy of the selected book?": "Vill du ta bort den lokala kopian av den valda boken?",
"Edit Metadata": "Redigera metadata",
"Book Details": "Bokdetaljer",
"Unknown": "Okänd",
"Delete Book Options": "Ta bort bokalternativ",
"Remove from Cloud & Device": "Ta bort från moln & enhet",
"Remove from Cloud Only": "Ta bort endast från moln",
"Remove from Device Only": "Ta bort endast från enhet",
"Published": "Publicerad",
"Updated": "Uppdaterad",
"Added": "Tillagd",
"File Size": "Filstorlek",
"No description available": "Ingen beskrivning tillgänglig",
"Locked": "Låst",
"Select Metadata Source": "Välj metadatakälla",
"Keep manual input": "Behåll manuell inmatning",
"Theme Mode": "Temaläge",
"Override Book Color": "Åsidosätt bokfärg",
"Theme Color": "Temafärg",
"Custom": "Anpassad",
"Code Highlighting": "Kodmarkering",
"Enable Highlighting": "Aktivera markering",
"Code Language": "Kodspråk",
"Scroll": "Rullning",
"Continuous Scroll": "Kontinuerlig rullning",
"Overlap Pixels": "Överlappande pixlar",
"Disable Double Click": "Inaktivera dubbelklick",
"Volume Keys for Page Flip": "Volymknappar för sidbyte",
"Animation": "Animation",
"Paging Animation": "Sidanimation",
"Security": "Säkerhet",
"Allow JavaScript": "Tillåt JavaScript",
"Enable only if you trust the file.": "Aktivera endast om du litar på filen.",
"Font": "Typsnitt",
"Custom Fonts": "Anpassade typsnitt",
"Cancel Delete": "Avbryt borttagning",
"Delete Font": "Ta bort typsnitt",
"Import Font": "Importera typsnitt",
"Tips": "Tips",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Format som stöds: .ttf, .otf, .woff, .woff2",
"Custom fonts can be selected from the Font Face menu": "Anpassade typsnitt kan väljas från typsnittsmeny",
"Global Settings": "Globala inställningar",
"Apply to All Books": "Tillämpa på alla böcker",
"Apply to This Book": "Tillämpa på denna bok",
"Reset Settings": "Återställ inställningar",
"Manage Custom Fonts": "Hantera anpassade typsnitt",
"System Fonts": "Systemtypsnitt",
"Serif Font": "Serif-typsnitt",
"Sans-Serif Font": "Sans-serif-typsnitt",
"Override Book Font": "Åsidosätt boktypsnitt",
"Default Font Size": "Standard teckenstorlek",
"Minimum Font Size": "Minsta teckenstorlek",
"Font Weight": "Typsnittsvikt",
"Font Family": "Typsnittsfamilj",
"Default Font": "Standardtypsnitt",
"CJK Font": "CJK-typsnitt",
"Font Face": "Typsnittssnitt",
"Monospace Font": "Monospace-typsnitt",
"Interface Language": "Gränssnittsspråk",
"Translation": "Översättning",
"Show Source Text": "Visa källtext",
"Translation Service": "Översättningstjänst",
"Translate To": "Översätt till",
"Override Book Layout": "Åsidosätt boklayout",
"Writing Mode": "Skrivläge",
"Default": "Standard",
"Horizontal Direction": "Horisontell riktning",
"Vertical Direction": "Vertikal riktning",
"RTL Direction": "RTL-riktning",
"Border Frame": "Kantram",
"Double Border": "Dubbel kant",
"Border Color": "Kantfärg",
"Paragraph": "Stycke",
"Paragraph Margin": "Styckemarginal",
"Word Spacing": "Ordavstånd",
"Letter Spacing": "Bokstavsavstånd",
"Text Indent": "Textindrag",
"Full Justification": "Full justering",
"Hyphenation": "Avstavning",
"Page": "Sida",
"Top Margin (px)": "Övre marginal (px)",
"Bottom Margin (px)": "Nedre marginal (px)",
"Left Margin (px)": "Vänster marginal (px)",
"Right Margin (px)": "Höger marginal (px)",
"Column Gap (%)": "Kolumnavstånd (%)",
"Maximum Number of Columns": "Max antal kolumner",
"Maximum Column Height": "Max kolumnhöjd",
"Maximum Column Width": "Max kolumnbredd",
"Header & Footer": "Sidhuvud & sidfot",
"Show Header": "Visa sidhuvud",
"Show Footer": "Visa sidfot",
"Show Remaining Time": "Visa återstående tid",
"Show Remaining Pages": "Visa återstående sidor",
"Show Reading Progress": "Visa läsframsteg",
"Reading Progress Style": "Läsframstegsstil",
"Page Number": "Sidnummer",
"Percentage": "Procent",
"Apply also in Scrolled Mode": "Tillämpa även i rullningsläge",
"Screen": "Skärm",
"Orientation": "Orientering",
"Portrait": "Porträtt",
"Landscape": "Landskap",
"Apply": "Tillämpa",
"Custom Content CSS": "Anpassad innehålls-CSS",
"Enter CSS for book content styling...": "Ange CSS för bokinnehållsstil...",
"Custom Reader UI CSS": "Anpassad läsar-UI CSS",
"Enter CSS for reader interface styling...": "Ange CSS för läsargränssnittsstil...",
"Decrease": "Minska",
"Increase": "Öka",
"Layout": "Layout",
"Color": "Färg",
"Behavior": "Beteende",
"Settings Panels": "Inställningspaneler",
"Reset {{settings}}": "Återställ {{settings}}",
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "Hela världen är en scen,\nOch alla män och kvinnor bara spelare;\nDe har sina utgångar och entréer,\nOch en man spelar i sin tid många roller,\nHans akter är sju åldrar.\n\n— William Shakespeare",
"(from 'As You Like It', Act II)": "(från 'Som ni behagar', Akt II)",
"Custom Theme": "Anpassat tema",
"Theme Name": "Temanamn",
"Text Color": "Textfärg",
"Background Color": "Bakgrundsfärg",
"Link Color": "Länkfärg",
"Preview": "Förhandsgranska",
"Get Help from the Readest Community": "Få hjälp från Readest-communityn",
"A new version of Readest is available!": "En ny version av Readest finns!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} finns tillgänglig (installerad version {{currentVersion}}).",
"Download and install now?": "Ladda ner och installera nu?",
"Downloading {{downloaded}} of {{contentLength}}": "Laddar ner {{downloaded}} av {{contentLength}}",
"Download finished": "Nedladdning klar",
"DOWNLOAD & INSTALL": "LADDA NER & INSTALLERA",
"Changelog": "Ändringslogg",
"Software Update": "Programuppdatering",
"What's New in Readest": "Nyheter i Readest",
"Minimize": "Minimera",
"Maximize or Restore": "Maximera eller återställ",
"Select Files": "Välj filer",
"Select Image": "Välj bild",
"Select Video": "Välj video",
"Select Audio": "Välj ljud",
"Select Fonts": "Välj typsnitt",
"Storage": "Lagring",
"{{percentage}}% of Cloud Sync Space Used.": "{{percentage}}% av molnsynkutrymme använt.",
"Translation Characters": "Översättningstecken",
"{{percentage}}% of Daily Translation Characters Used.": "{{percentage}}% av dagliga översättningstecken använt.",
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "Daglig översättningskvot nådd. Uppgradera din plan för att fortsätta använda AI-översättningar.",
"LXGW WenKai GB Screen": "LXGW WenKai GB Screen",
"LXGW WenKai TC": "LXGW WenKai TC",
"GuanKiapTsingKhai-T": "GuanKiapTsingKhai-T",
"Source Han Serif CN VF": "Source Han Serif CN VF",
"Huiwen-mincho": "Huiwen-mincho",
"KingHwa_OldSong": "KingHwa_OldSong",
"Google Books": "Google Böcker",
"Open Library": "Open Library",
"Azure Translator": "Azure Translator",
"DeepL": "DeepL",
"Google Translate": "Google Translate",
"Yandex Translate": "Yandex Translate",
"Gray": "Grå",
"Sepia": "Sepia",
"Grass": "Gräs",
"Cherry": "Körsbär",
"Sky": "Himmel",
"Solarized": "Solarized",
"Gruvbox": "Gruvbox",
"Nord": "Nord",
"Contrast": "Kontrast",
"Sunset": "Solnedgång",
"Reveal in Finder": "Visa i Finder",
"Reveal in File Explorer": "Visa i Utforskaren",
"Reveal in Folder": "Visa i mapp",
"Screen Brightness": "Skärmens ljusstyrka",
"Background Image": "Bakgrundsbild",
"Import Image": "Importera bild",
"Opacity": "Opacitet",
"Size": "Storlek",
"Cover": "Omslag",
"Contain": "Innehålla",
"{{number}} pages left in chapter": "{{number}} sidor kvar i kapitlet",
"Device": "Enhet",
"E-Ink Mode": "E-Ink-läge",
"Highlight Colors": "Markeringsfärger",
"Auto Screen Brightness": "Auto-skärmens ljusstyrka",
"Pagination": "Pagination",
"Disable Double Tap": "Inaktivera dubbeltryck",
"Tap to Paginate": "Tryck för att bläddra",
"Click to Paginate": "Klicka för att bläddra",
"Tap Both Sides": "Tryck på båda sidor",
"Click Both Sides": "Klicka på båda sidor",
"Swap Tap Sides": "Byt plats på tap-sidor",
"Swap Click Sides": "Byt plats på klick-sidor",
"Source and Translated": "Källa och översatt",
"Translated Only": "Översatt endast",
"Source Only": "Endast källa",
"TTS Text": "TTS-text",
"The book file is corrupted": "Bokfilen är skadad",
"The book file is empty": "Bokfilen är tom",
"Failed to open the book file": "Kunde inte öppna bokfilen",
"On-Demand Purchase": "Köp på begäran",
"Full Customization": "Full anpassning",
"Lifetime Plan": "Livstidsplan",
"One-Time Payment": "Engångsbetalning",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Gör en engångsbetalning för att få livstidsåtkomst till specifika funktioner på alla enheter. Köp specifika funktioner eller tjänster endast när du behöver dem.",
"Expand Cloud Sync Storage": "Utöka molnsynkroniseringslagring",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Utöka din molnlagring för alltid med ett engångsköp. Varje ytterligare köp lägger till mer utrymme.",
"Unlock All Customization Options": "Lås upp alla anpassningsalternativ",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Lås upp ytterligare teman, typsnitt, layoutalternativ och uppläsning, översättare, molnlagringstjänster.",
"Purchase Successful!": "Köp lyckades!",
"lifetime": "livstid",
"Thank you for your purchase! Your payment has been processed successfully.": "Tack för ditt köp! Din betalning har behandlats.",
"Order ID:": "Order-ID:",
"TTS Highlighting": "TTS-markering",
"Style": "Stil",
"Underline": "Understrykning",
"Strikethrough": "Genomstrykning",
"Squiggly": "Vågig linje",
"Outline": "Kontur",
"Save Current Color": "Spara aktuell färg",
"Quick Colors": "Snabba färger",
"Highlighter": "Markeringspenna",
"Save Book Cover": "Spara bokomslag",
"Auto-save last book cover": "Automatiskt spara senaste bokomslag",
"Back": "Tillbaka",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Bekräftelsemail skickat! Kontrollera din gamla och nya e-postadress för att bekräfta ändringen.",
"Failed to update email": "Misslyckades med att uppdatera e-post",
"New Email": "Ny e-post",
"Your new email": "Din nya e-post",
"Updating email ...": "Uppdaterar e-post ...",
"Update email": "Uppdatera e-post",
"Current email": "Nuvarande e-post",
"Update Email": "Uppdatera e-post",
"All": "Alla"
}
@@ -105,9 +105,9 @@
"Book Covers": "புத்தக அட்டைகள்",
"Sort by...": "வரிசைப்படுத்து...",
"No supported files found. Supported formats: {{formats}}": "ஆதரிக்கப்படும் கோப்புகள் இல்லை. ஆதரிக்கப்படும் வடிவங்கள்: {{formats}}",
"No chapters detected.": "அத்தியாயங்கள் கண்டறியப்படவில்லை.",
"Failed to parse the EPUB file.": "EPUB கோப்பை பகுப்பாய்வு செய்ய முடியவில்லை.",
"This book format is not supported.": "இந்த புத்தக வடிவம் ஆதரிக்கப்படவில்லை.",
"No chapters detected": "அத்தியாயங்கள் கண்டறியப்படவில்லை",
"Failed to parse the EPUB file": "EPUB கோப்பை பகுப்பாய்வு செய்ய முடியவில்லை",
"This book format is not supported": "இந்த புத்தக வடிவம் ஆதரிக்கப்படவில்லை",
"Failed to import book(s): {{filenames}}": "புத்தகங்களை இறக்குமதி செய்ய முடியவில்லை: {{filenames}}",
"Book uploaded: {{title}}": "புத்தகம் பதிவேற்றப்பட்டது: {{title}}",
"Insufficient storage quota": "போதுமான சேமிப்பக ஒதுக்கீடு இல்லை",
@@ -204,9 +204,6 @@
"Scrolled Mode": "உருட்டல் பயன்முறை",
"Continuous Scroll": "தொடர்ச்சியான உருட்டல்",
"Overlap Pixels": "ஒன்றுடன் ஒன்று பிக்சல்கள்",
"Click": "கிளிக்",
"Clicks for Page Flip": "பக்கம் புரட்டுவதற்கு கிளிக்கள்",
"Swap Clicks Area": "கிளிக் பகுதியை மாற்றவும்",
"Disable Double Click": "இரட்டை கிளிக்கை முடக்கவும்",
"Volume Keys for Page Flip": "பக்கம் புரட்டுவதற்கு ஒலி விசைகள்",
"Animation": "அனிமேஷன்",
@@ -379,7 +376,6 @@
"Email:": "மின்னஞ்சல்:",
"Plan:": "திட்டம்:",
"Amount:": "தொகை:",
"Subscription ID:": "சந்தா ID:",
"Go to Library": "நூலகத்திற்கு செல்லவும்",
"Need help? Contact our support team at support@readest.com": "உதவி தேவையா? support@readest.com இல் எங்கள் ஆதரவு குழுவைத் தொடர்பு கொள்ளவும்",
"Free Plan": "இலவச திட்டம்",
@@ -599,5 +595,66 @@
"File count: {{size}}": "கோப்பு எண்ணிக்கை: {{size}}",
"Background Read Aloud": "பின்னணி உரை வாசிப்பு",
"Ready to read aloud": "வாசிக்க தயாராக உள்ளது",
"Read Aloud": "உரை வாசிப்பு"
"Read Aloud": "உரை வாசிப்பு",
"Screen Brightness": "திரை பிரகாசம்",
"Background Image": "பின்னணி படம்",
"Import Image": "புகைப்படம் இறக்குமதி",
"Opacity": "பார்வை",
"Size": "அளவு",
"Cover": "மூடு",
"Contain": "அடங்கும்",
"{{number}} pages left in chapter": "அத்தியாயத்தில் {{number}} பக்கங்கள் மீதமுள்ளன",
"Device": "சாதனம்",
"E-Ink Mode": "E-Ink முறை",
"Highlight Colors": "முத்திரை நிறங்கள்",
"Auto Screen Brightness": "தானியங்கி திரை பிரகாசம்",
"Pagination": "பக்கமிடுதல்",
"Disable Double Tap": "இரட்டை தொடுதலை முடக்கு",
"Tap to Paginate": "பக்கம் மாற்ற தொடுங்கள்",
"Click to Paginate": "பக்கம் மாற்ற கிளிக் செய்யவும்",
"Tap Both Sides": "இரு பக்கங்களையும் தொடுங்கள்",
"Click Both Sides": "இரு பக்கங்களையும் கிளிக் செய்யவும்",
"Swap Tap Sides": "இரு பக்கங்களின் இடத்தை மாற்றவும்",
"Swap Click Sides": "இரு பக்கங்களின் இடத்தை மாற்றவும்",
"Source and Translated": "மூல மற்றும் மொழிபெயர்க்கப்பட்டது",
"Translated Only": "மொழிபெயர்க்கப்பட்டது மட்டும்",
"Source Only": "மூல மட்டும்",
"TTS Text": "TTS உரை",
"The book file is corrupted": "பொதுவாக, புத்தகக் கோப்பு சேதமடைந்துள்ளது",
"The book file is empty": "புத்தகக் கோப்பு காலியாக உள்ளது",
"Failed to open the book file": "புத்தகக் கோப்பைத் திறக்க முடியவில்லை",
"On-Demand Purchase": "தேவைப்படும் போது வாங்குதல்",
"Full Customization": "முழு தனிப்பயனாக்கம்",
"Lifetime Plan": "ஆயுள்திருத்தம்",
"One-Time Payment": "ஒரே முறை கட்டணம்",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "எல்லா சாதனங்களிலும் குறிப்பிட்ட அம்சங்களுக்கு ஆயுள்திருத்தம் பெற ஒரே முறை கட்டணம் செலுத்தவும். நீங்கள் தேவைப்படும் போது மட்டுமே குறிப்பிட்ட அம்சங்கள் அல்லது சேவைகளை வாங்கவும்.",
"Expand Cloud Sync Storage": "மோலச் சின்க் சேமிப்பிடத்தை விரிவாக்கவும்",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "ஒரே முறை வாங்குதலுடன் உங்கள் மேக சேமிப்பிடத்தை எப்போதும் விரிவாக்கவும். ஒவ்வொரு கூடுதல் வாங்குதலும் மேலும் இடத்தைச் சேர்க்கிறது.",
"Unlock All Customization Options": "அனைத்து தனிப்பயனாக்க விருப்பங்களைத் திறக்கவும்",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "கூடுதல் தீமைகள், எழுத்துருக்கள், வடிவமைப்பு விருப்பங்கள் மற்றும் வாசிக்க, மொழிபெயர்க்க, மேக சேமிப்பு சேவைகளைத் திறக்கவும்.",
"Purchase Successful!": "கொள்முதல் வெற்றிகரமாக முடிந்தது!",
"lifetime": "ஆயுள்திருத்தம்",
"Thank you for your purchase! Your payment has been processed successfully.": "உங்கள் வாங்குதலுக்கு நன்றி! உங்கள் கட்டணம் வெற்றிகரமாக செயல்படுத்தப்பட்டது.",
"Order ID:": "ஆர்டர் ஐடி:",
"TTS Highlighting": "TTS சிறப்பித்தல்",
"Style": "பாணி",
"Underline": "அடிக்கோடு",
"Strikethrough": "குறிக்கோடு",
"Squiggly": "சுருள் கோடு",
"Outline": "வட்டம்",
"Save Current Color": "தற்போதைய நிறத்தை சேமிக்கவும்",
"Quick Colors": "விரைவு நிறங்கள்",
"Highlighter": "ஹைலைட்டர்",
"Save Book Cover": "புத்தகக் கோப்பைச் சேமிக்கவும்",
"Auto-save last book cover": "அවසන් புத்தகக் கோப்பு காலியாக உள்ளது",
"Back": "பின் செல்ல",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "உறுதிப்பத்திர மின்னஞ்சல் அனுப்பப்பட்டுள்ளது! மாற்றத்தை உறுதிப்படுத்த உங்கள் பழைய மற்றும் புதிய மின்னஞ்சல் முகவரிகளை சரிபார்க்கவும்.",
"Failed to update email": "மின்னஞ்சல் புதுப்பிப்பில் தோல்வி",
"New Email": "புதிய மின்னஞ்சல்",
"Your new email": "உங்கள் புதிய மின்னஞ்சல்",
"Updating email ...": "மின்னஞ்சல் புதுப்பிக்கப்படுகிறது ...",
"Update email": "மின்னஞ்சல் புதுப்பிக்கவும்",
"Current email": "தற்போதைய மின்னஞ்சல்",
"Update Email": "மின்னஞ்சல் புதுப்பிக்கவும்",
"All": "அனைத்து"
}
@@ -101,9 +101,9 @@
"Book Covers": "ปกหนังสือ",
"Sort by...": "เรียงตาม...",
"No supported files found. Supported formats: {{formats}}": "ไม่พบไฟล์ที่รองรับ รูปแบบที่รองรับ: {{formats}}",
"No chapters detected.": "ไม่พบบท",
"Failed to parse the EPUB file.": "ไม่สามารถอ่านไฟล์ EPUB",
"This book format is not supported.": "ไม่รองรับรูปแบบหนังสือนี้",
"No chapters detected": "ไม่พบบท",
"Failed to parse the EPUB file": "ไม่สามารถอ่านไฟล์ EPUB",
"This book format is not supported": "ไม่รองรับรูปแบบหนังสือนี้",
"Failed to import book(s): {{filenames}}": "ไม่สามารถนำเข้าหนังสือ: {{filenames}}",
"Book uploaded: {{title}}": "อัปโหลดหนังสือแล้ว: {{title}}",
"Insufficient storage quota": "พื้นที่จัดเก็บไม่เพียงพอ",
@@ -169,9 +169,6 @@
"Scrolled Mode": "โหมดเลื่อน",
"Continuous Scroll": "เลื่อนต่อเนื่อง",
"Overlap Pixels": "พิกเซลซ้อน",
"Click": "คลิก",
"Clicks for Page Flip": "คลิกเพื่อพลิกหน้า",
"Swap Clicks Area": "สลับพื้นที่คลิก",
"Volume Keys for Page Flip": "ปุ่มเสียงเพื่อพลิกหน้า",
"Animation": "ภาพเคลื่อนไหว",
"Paging Animation": "ภาพเคลื่อนไหวพลิกหน้า",
@@ -315,7 +312,6 @@
"Email:": "อีเมล:",
"Plan:": "แผน:",
"Amount:": "จำนวนเงิน:",
"Subscription ID:": "รหัสสมาชิก:",
"Go to Library": "ไปที่คลังหนังสือ",
"Need help? Contact our support team at support@readest.com": "ต้องการความช่วยเหลือ? ติดต่อทีมสนับสนุนที่ support@readest.com",
"Free Plan": "แผนฟรี",
@@ -595,5 +591,66 @@
"File count: {{size}}": "จำนวนไฟล์: {{size}}",
"Background Read Aloud": "อ่านออกเสียงเบื้องหลัง",
"Ready to read aloud": "พร้อมที่จะอ่านออกเสียง",
"Read Aloud": "อ่านออกเสียง"
"Read Aloud": "อ่านออกเสียง",
"Screen Brightness": "ความสว่างหน้าจอ",
"Background Image": "ภาพพื้นหลัง",
"Import Image": "นำเข้าภาพ",
"Opacity": "ความโปร่งใส",
"Size": "ขนาด",
"Cover": "ปก",
"Contain": "บรรจุ",
"{{number}} pages left in chapter": "เหลือ {{number}} หน้าในบท",
"Device": "อุปกรณ์",
"E-Ink Mode": "โหมด E-Ink",
"Highlight Colors": "สีไฮไลต์",
"Auto Screen Brightness": "ความสว่างหน้าจออัตโนมัติ",
"Pagination": "การแบ่งหน้า",
"Disable Double Tap": "ปิดการแตะสองครั้ง",
"Tap to Paginate": "แตะเพื่อแบ่งหน้า",
"Click to Paginate": "คลิกเพื่อแบ่งหน้า",
"Tap Both Sides": "แตะทั้งสองด้าน",
"Click Both Sides": "คลิกทั้งสองด้าน",
"Swap Tap Sides": "เปลี่ยนด้านการแตะ",
"Swap Click Sides": "เปลี่ยนด้านการคลิก",
"Source and Translated": "ต้นฉบับและแปลแล้ว",
"Translated Only": "แปลแล้วเท่านั้น",
"Source Only": "ต้นฉบับเท่านั้น",
"TTS Text": "ข้อความ TTS",
"The book file is corrupted": "ไฟล์หนังสือเสียหาย",
"The book file is empty": "ไฟล์หนังสือว่างเปล่า",
"Failed to open the book file": "ไม่สามารถเปิดไฟล์หนังสือได้",
"On-Demand Purchase": "การซื้อเมื่อจำเป็น",
"Full Customization": "การปรับแต่งเต็มรูปแบบ",
"Lifetime Plan": "แผนการใช้งานตลอดชีพ",
"One-Time Payment": "การชำระเงินครั้งเดียว",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "ทำการชำระเงินครั้งเดียวเพื่อเพลิดเพลินกับการเข้าถึงฟีเจอร์เฉพาะตลอดชีพบนอุปกรณ์ทั้งหมด ซื้อฟีเจอร์หรือบริการเฉพาะเมื่อคุณต้องการเท่านั้น",
"Expand Cloud Sync Storage": "ขยายพื้นที่เก็บข้อมูลซิงค์คลาวด์",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "ขยายพื้นที่เก็บข้อมูลซิงค์คลาวด์ของคุณตลอดไปด้วยการซื้อครั้งเดียว การซื้อเพิ่มเติมแต่ละครั้งจะเพิ่มพื้นที่มากขึ้น",
"Unlock All Customization Options": "ปลดล็อกตัวเลือกการปรับแต่งทั้งหมด",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "ปลดล็อกธีมเพิ่มเติม ฟอนต์ ตัวเลือกการจัดรูปแบบ และฟีเจอร์การอ่านออกเสียง ตัวแปล และบริการพื้นที่เก็บข้อมูลคลาวด์",
"Purchase Successful!": "การซื้อสำเร็จ!",
"lifetime": "ตลอดชีพ",
"Thank you for your purchase! Your payment has been processed successfully.": "ขอบคุณสำหรับการซื้อ! การชำระเงินของคุณได้รับการประมวลผลเรียบร้อยแล้ว",
"Order ID:": "รหัสคำสั่งซื้อ:",
"TTS Highlighting": "การไฮไลต์ TTS",
"Style": "สไตล์",
"Underline": "ขีดเส้นใต้",
"Strikethrough": "ขีดฆ่า",
"Squiggly": "เส้นหยัก",
"Outline": "เส้นขอบ",
"Save Current Color": "บันทึกสีปัจจุบัน",
"Quick Colors": "สีด่วน",
"Highlighter": "ปากกาเน้นข้อความ",
"Save Book Cover": "บันทึกปกหนังสือ",
"Auto-save last book cover": "บันทึกปกหนังสือสุดท้ายโดยอัตโนมัติ",
"Back": "กลับ",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "ส่งอีเมลยืนยันแล้ว! กรุณาตรวจสอบอีเมลเก่าและใหม่ของคุณเพื่อยืนยันการเปลี่ยนแปลง",
"Failed to update email": "ไม่สามารถอัปเดตอีเมลได้",
"New Email": "อีเมลใหม่",
"Your new email": "อีเมลใหม่ของคุณ",
"Updating email ...": "กำลังอัปเดตอีเมล ...",
"Update email": "อัปเดตอีเมล",
"Current email": "อีเมลปัจจุบัน",
"Update Email": "อัปเดตอีเมล",
"All": "ทั้งหมด"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "Dosya Açıldığında Otomatik İçe Aktar",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "Bölüm bulunamadı.",
"Failed to parse the EPUB file.": "EPUB dosyası ayrıştırılamadı.",
"This book format is not supported.": "Bu kitap formatı desteklenmiyor.",
"No chapters detected": "Bölüm bulunamadı",
"Failed to parse the EPUB file": "EPUB dosyası ayrıştırılamadı",
"This book format is not supported": "Bu kitap formatı desteklenmiyor",
"Unable to fetch the translation. Please log in first and try again.": "Çeviri alınamıyor. Lütfen önce giriş yapın ve tekrar deneyin.",
"Group": "Grup",
"Always on Top": "Her Zaman Üstte",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "(As You Like It, 2. Perde)",
"Link Color": "Bağlantı Rengi",
"Volume Keys for Page Flip": "Sayfa Çevirme için Ses Seviyesi Tuşları",
"Clicks for Page Flip": "Sayfa Çevirme için Tıklamalar",
"Swap Clicks Area": "Tıklama Alanını Değiştir",
"Screen": "Ekran",
"Orientation": "Yönlendirme",
"Portrait": "Portre",
@@ -297,7 +295,6 @@
"Disable Translation": "Çeviriyi Devre Dışı Bırak",
"Scroll": "Kaydır",
"Overlap Pixels": "Örtüşen Pikseller",
"Click": "Klik",
"System Language": "System Dili",
"Security": "Security",
"Allow JavaScript": "JavaScript'e İzin Ver",
@@ -366,7 +363,6 @@
"Email:": "E-posta:",
"Plan:": "Plan:",
"Amount:": "Tutar:",
"Subscription ID:": "Abonelik ID'si:",
"Go to Library": "Kütüphaneye Git",
"Need help? Contact our support team at support@readest.com": "Yardım mı gerekiyor? Destek ekibimizle iletişime geçin: support@readest.com",
"Free Plan": "Ücretsiz Plan",
@@ -599,5 +595,66 @@
"File count: {{size}}": "Dosya sayısı: {{size}}",
"Background Read Aloud": "Arka Planda Sesli Okuma",
"Ready to read aloud": "Sesli okumaya hazır",
"Read Aloud": "Sesli Oku"
"Read Aloud": "Sesli Oku",
"Screen Brightness": "Ekran Parlaklığı",
"Background Image": "Arka Plan Görüntüsü",
"Import Image": "Görüntü İçe Aktar",
"Opacity": "Opaklık",
"Size": "Boyut",
"Cover": "Kapak",
"Contain": "İçerir",
"{{number}} pages left in chapter": "Bu bölümde {{number}} sayfa kaldı",
"Device": "Cihaz",
"E-Ink Mode": "E-Ink Modu",
"Highlight Colors": "Vurgu Renkleri",
"Auto Screen Brightness": "Otomatik Ekran Parlaklığı",
"Pagination": "Sayfalama",
"Disable Double Tap": "Çift Dokunmayı Devre Dışı Bırak",
"Tap to Paginate": "Sayfalamak için Dokunun",
"Click to Paginate": "Sayfalamak için Tıklayın",
"Tap Both Sides": "Her İki Tarafa Dokunun",
"Click Both Sides": "Her İki Tarafa Tıklayın",
"Swap Tap Sides": "Dokunma Taraflarını Değiştir",
"Swap Click Sides": "Tıklama Taraflarını Değiştir",
"Source and Translated": "Kaynak ve Çeviri",
"Translated Only": "Yalnızca Çeviri",
"Source Only": "Yalnızca Kaynak",
"TTS Text": "TTS Metni",
"The book file is corrupted": "Kitap dosyası bozulmuş",
"The book file is empty": "Kitap dosyası boş",
"Failed to open the book file": "Kitap dosyası açılamadı",
"On-Demand Purchase": "İhtiyaç Duyulduğunda Satın Alma",
"Full Customization": "Tam Özelleştirme",
"Lifetime Plan": "Ömür Boyu Plan",
"One-Time Payment": "Tek Seferlik Ödeme",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Tüm cihazlarda belirli özelliklere ömür boyu erişim sağlamak için tek seferlik bir ödeme yapın. Belirli özellikleri veya hizmetleri yalnızca ihtiyaç duyduğunuzda satın alın.",
"Expand Cloud Sync Storage": "Bulut Senkronizasyonu Depolama Alanını Genişlet",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Tek seferlik bir satın alma ile bulut depolama alanınızı sonsuza kadar genişletin. Her ek satın alma daha fazla alan ekler.",
"Unlock All Customization Options": "Tüm Özelleştirme Seçeneklerini Aç",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Ek tema, yazı tipi, düzen seçeneklerini ve sesli okuma, çevirmenler, bulut depolama hizmetlerini açın.",
"Purchase Successful!": "Satın Alma Başarılı!",
"lifetime": "ömür boyu",
"Thank you for your purchase! Your payment has been processed successfully.": "Satın alma işleminiz için teşekkürler! Ödemeniz başarıyla işlendi.",
"Order ID:": "Sipariş Kimliği:",
"TTS Highlighting": "TTS Vurgulama",
"Style": "Stil",
"Underline": "Altı Çizili",
"Strikethrough": "Üstü Çizili",
"Squiggly": "Dalgalı Çizgi",
"Outline": "Kontur",
"Save Current Color": "Mevcut Rengi Kaydet",
"Quick Colors": "Hızlı Renkler",
"Highlighter": "Vurgulayıcı",
"Save Book Cover": "Kitap Kapağını Kaydet",
"Auto-save last book cover": "Son kitap kapağını otomatik olarak kaydet",
"Back": "Geri",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Onay e-postası gönderildi! Değişikliği onaylamak için eski ve yeni e-posta adreslerinizi kontrol edin.",
"Failed to update email": "E-posta güncellenemedi",
"New Email": "Yeni e-posta",
"Your new email": "Yeni e-posta adresiniz",
"Updating email ...": "E-posta güncelleniyor ...",
"Update email": "E-postayı güncelle",
"Current email": "Mevcut e-posta",
"Update Email": "E-postayı güncelle",
"All": "Tümü"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "Автоматичний імпорт при відкритті файлу",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "Не виявлено жодного розділу.",
"Failed to parse the EPUB file.": "Не вдалося розібрати файл EPUB.",
"This book format is not supported.": "Цей формат книги не підтримується.",
"No chapters detected": "Не виявлено жодного розділу",
"Failed to parse the EPUB file": "Не вдалося розібрати файл EPUB",
"This book format is not supported": "Цей формат книги не підтримується",
"Unable to fetch the translation. Please log in first and try again.": "Не вдалося отримати переклад. Будь ласка, спочатку увійдіть у систему та спробуйте ще раз.",
"Group": "Група",
"Always on Top": "Завжди зверху",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "(із 'Як вам це подобається', Дія II)",
"Link Color": "Колір посилання",
"Volume Keys for Page Flip": "Перегортати сторінки клавішами гучності",
"Clicks for Page Flip": "Перегортати сторінки натисканням",
"Swap Clicks Area": "Поміняти області клацання місцями",
"Screen": "Екран",
"Orientation": "Орієнтація",
"Portrait": "Портрет",
@@ -299,7 +297,6 @@
"Disable Translation": "Вимкнути переклад",
"Scroll": "Прокрутка",
"Overlap Pixels": "Перекривати пікселі",
"Click": "Натиснути",
"System Language": "Системна мова",
"Security": "Безпека",
"Allow JavaScript": "Дозволити JavaScript",
@@ -374,7 +371,6 @@
"Email:": "Електронна пошта:",
"Plan:": "План:",
"Amount:": "Сума:",
"Subscription ID:": "ID підписки:",
"Go to Library": "Перейти до бібліотеки",
"Need help? Contact our support team at support@readest.com": "Потрібна допомога? Зверніться до служби підтримки: support@readest.com",
"Free Plan": "Безкоштовний план",
@@ -607,5 +603,66 @@
"File count: {{size}}": "Кількість файлів: {{size}}",
"Background Read Aloud": "Озвучування у фоновому режимі",
"Ready to read aloud": "Готовий до читання вголос",
"Read Aloud": "Читати вголос"
"Read Aloud": "Читати вголос",
"Screen Brightness": "Яскравість екрану",
"Background Image": "Фонове зображення",
"Import Image": "Імпортувати зображення",
"Opacity": "Непрозорість",
"Size": "Розмір",
"Cover": "Обкладинка",
"Contain": "Вміщати",
"{{number}} pages left in chapter": "Залишилось {{number}} сторінок у розділі",
"Device": "Пристрій",
"E-Ink Mode": "E-Ink режим",
"Highlight Colors": "Кольори виділення",
"Auto Screen Brightness": "Автоматична яскравість екрану",
"Pagination": "Розбиття на сторінки",
"Disable Double Tap": "Вимкнути подвійне натискання",
"Tap to Paginate": "Натисніть, щоб розбити на сторінки",
"Click to Paginate": "Клікніть, щоб розбити на сторінки",
"Tap Both Sides": "Натисніть обидві сторони",
"Click Both Sides": "Клікніть обидві сторони",
"Swap Tap Sides": "Змінити сторони дотику",
"Swap Click Sides": "Змінити сторони кліку",
"Source and Translated": "Джерело та переклад",
"Translated Only": "Тільки переклад",
"Source Only": "Тільки джерело",
"TTS Text": "Текст TTS",
"The book file is corrupted": "Файл книги пошкоджено",
"The book file is empty": "Файл книги порожній",
"Failed to open the book file": "Не вдалося відкрити файл книги",
"On-Demand Purchase": "Покупка за запитом",
"Full Customization": "Повна налаштування",
"Lifetime Plan": "План на все життя",
"One-Time Payment": "Одноразовий платіж",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Зробіть одноразовий платіж, щоб отримати довічний доступ до певних функцій на всіх пристроях. Купуйте конкретні функції або послуги лише тоді, коли вони вам потрібні.",
"Expand Cloud Sync Storage": "Розширити сховище синхронізації в хмарі",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Розширте своє хмарне сховище назавжди за допомогою одноразової покупки. Кожна додаткова покупка додає більше місця.",
"Unlock All Customization Options": "Відкрити всі параметри налаштування",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Відкрийте додаткові теми, шрифти, параметри макета та функції читання вголос, перекладачі, послуги хмарного зберігання.",
"Purchase Successful!": "Покупка успішна!",
"lifetime": "довічний",
"Thank you for your purchase! Your payment has been processed successfully.": "Дякуємо за вашу покупку! Ваш платіж успішно оброблено.",
"Order ID:": "ID замовлення:",
"TTS Highlighting": "Підсвічування TTS",
"Style": "Стиль",
"Underline": "Підкреслення",
"Strikethrough": "Закреслення",
"Squiggly": "Хвиляста лінія",
"Outline": "Контур",
"Save Current Color": "Зберегти поточний колір",
"Quick Colors": "Швидкі кольори",
"Highlighter": "Маркер",
"Save Book Cover": "Зберегти обкладинку книги",
"Auto-save last book cover": "Автоматично зберегти останню обкладинку книги",
"Back": "Назад",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Лист підтвердження надіслано! Будь ласка, перевірте стару та нову електронну адресу, щоб підтвердити зміну.",
"Failed to update email": "Не вдалося оновити електронну пошту",
"New Email": "Нова електронна пошта",
"Your new email": "Ваша нова електронна адреса",
"Updating email ...": "Оновлення електронної пошти ...",
"Update email": "Оновити електронну пошту",
"Current email": "Поточна електронна пошта",
"Update Email": "Оновити електронну пошту",
"All": "Всі"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "Tự động nhập khi mở tệp",
"LXGW WenKai GB Screen": "LXGW WenKai SC",
"LXGW WenKai TC": "LXGW WenKai TC",
"No chapters detected.": "Không phát hiện thấy chương nào.",
"Failed to parse the EPUB file.": "Không thể phân tích tệp EPUB.",
"This book format is not supported.": "Định dạng sách này không được hỗ trợ.",
"No chapters detected": "Không phát hiện thấy chương nào",
"Failed to parse the EPUB file": "Không thể phân tích tệp EPUB",
"This book format is not supported": "Định dạng sách này không được hỗ trợ",
"Unable to fetch the translation. Please log in first and try again.": "Không thể tải dịch vụ. Vui lòng đăng nhập trước và thử lại.",
"Group": "Nhóm",
"Always on Top": "Luôn ở trên cùng",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "(từ 'Như bạn thích', Hành động II)",
"Link Color": "Màu liên kết",
"Volume Keys for Page Flip": "Phím âm lượng để lật trang",
"Clicks for Page Flip": "Nhấp để lật trang",
"Swap Clicks Area": "Đổi khu vực nhấp",
"Screen": "Màn hình",
"Orientation": "Định hướng",
"Portrait": "Chân dung",
@@ -296,7 +294,6 @@
"Disable Translation": "Tắt dịch",
"Scroll": "Cuộn",
"Overlap Pixels": "Chồng lấn pixel",
"Click": "Nhấp",
"System Language": "Ngôn ngữ hệ thống",
"Security": "An ninh",
"Allow JavaScript": "Cho phép JavaScript",
@@ -362,7 +359,6 @@
"Email:": "Email:",
"Plan:": "Gói:",
"Amount:": "Số tiền:",
"Subscription ID:": "ID đăng ký:",
"Go to Library": "Đi đến Thư viện",
"Need help? Contact our support team at support@readest.com": "Cần hỗ trợ? Liên hệ nhóm hỗ trợ tại support@readest.com",
"Free Plan": "Gói miễn phí",
@@ -595,5 +591,66 @@
"File count: {{size}}": "Số lượng tệp: {{size}}",
"Background Read Aloud": "Đọc To Nền",
"Ready to read aloud": "Sẵn sàng để đọc to",
"Read Aloud": "Đọc To"
"Read Aloud": "Đọc To",
"Screen Brightness": "Độ sáng màn hình",
"Background Image": "Ảnh nền",
"Import Image": "Nhập ảnh",
"Opacity": "Độ mờ",
"Size": "Kích thước",
"Cover": "Bìa",
"Contain": "Chứa",
"{{number}} pages left in chapter": "Còn {{number}} trang trong chương",
"Device": "Thiết bị",
"E-Ink Mode": "Chế độ E-Ink",
"Highlight Colors": "Màu nổi bật",
"Auto Screen Brightness": "Độ sáng màn hình tự động",
"Pagination": "Phân trang",
"Disable Double Tap": "Vô hiệu hóa chạm hai lần",
"Tap to Paginate": "Chạm để phân trang",
"Click to Paginate": "Nhấp để phân trang",
"Tap Both Sides": "Chạm vào cả hai bên",
"Click Both Sides": "Nhấp vào cả hai bên",
"Swap Tap Sides": "Đổi bên chạm",
"Swap Click Sides": "Đổi bên nhấp",
"Source and Translated": "Nguồn và Đã dịch",
"Translated Only": "Chỉ Đã dịch",
"Source Only": "Chỉ Nguồn",
"TTS Text": "Văn bản TTS",
"The book file is corrupted": "Tệp sách bị hỏng",
"The book file is empty": "Tệp sách trống",
"Failed to open the book file": "Không thể mở tệp sách",
"On-Demand Purchase": "Mua theo yêu cầu",
"Full Customization": "Tùy chỉnh đầy đủ",
"Lifetime Plan": "Kế hoạch trọn đời",
"One-Time Payment": "Thanh toán một lần",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "Thực hiện một khoản thanh toán duy nhất để tận hưởng quyền truy cập trọn đời vào các tính năng cụ thể trên tất cả các thiết bị. Mua các tính năng hoặc dịch vụ cụ thể chỉ khi bạn cần chúng.",
"Expand Cloud Sync Storage": "Mở rộng dung lượng lưu trữ đồng bộ hóa đám mây",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "Mở rộng dung lượng lưu trữ đám mây của bạn mãi mãi với một lần mua. Mỗi lần mua thêm sẽ thêm không gian.",
"Unlock All Customization Options": "Mở khóa tất cả tùy chọn tùy chỉnh",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "Mở khóa các chủ đề, phông chữ, tùy chọn bố cục và đọc to, dịch giả, dịch vụ lưu trữ đám mây bổ sung.",
"Purchase Successful!": "Mua hàng thành công!",
"lifetime": "trọn đời",
"Thank you for your purchase! Your payment has been processed successfully.": "Cảm ơn bạn đã mua hàng! Thanh toán của bạn đã được xử lý thành công.",
"Order ID:": "Mã đơn hàng:",
"TTS Highlighting": "Đánh dấu TTS",
"Style": "Kiểu",
"Underline": "Gạch chân",
"Strikethrough": "Gạch ngang",
"Squiggly": "Gạch ngoằn ngoèo",
"Outline": "Đường viền",
"Save Current Color": "Lưu màu hiện tại",
"Quick Colors": "Màu nhanh",
"Highlighter": "Bút đánh dấu",
"Save Book Cover": "Lưu bìa sách",
"Auto-save last book cover": "Tự động lưu bìa sách cuối cùng",
"Back": "Quay lại",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "Email xác nhận đã được gửi! Vui lòng kiểm tra địa chỉ email cũ và mới của bạn để xác nhận thay đổi.",
"Failed to update email": "Cập nhật email thất bại",
"New Email": "Email mới",
"Your new email": "Email mới của bạn",
"Updating email ...": "Đang cập nhật email ...",
"Update email": "Cập nhật email",
"Current email": "Email hiện tại",
"Update Email": "Cập nhật email",
"All": "Tất cả"
}
@@ -87,7 +87,7 @@
"Show Book Details": "显示书籍详情",
"Sidebar": "侧边栏",
"Sign In": "登录",
"Sign Out": "登出",
"Sign Out": "登出账户",
"Sky": "天青",
"Slow": "慢",
"Solarized": "日晖",
@@ -221,9 +221,9 @@
"Auto Import on File Open": "打开文件时自动导入",
"LXGW WenKai GB Screen": "霞鹜文楷",
"LXGW WenKai TC": "霞鶩文楷",
"No chapters detected.": "未检测到章节",
"Failed to parse the EPUB file.": "无法解析 EPUB 文件",
"This book format is not supported.": "不支持此书籍格式",
"No chapters detected": "未检测到章节",
"Failed to parse the EPUB file": "无法解析 EPUB 文件",
"This book format is not supported": "不支持此书籍格式",
"Unable to fetch the translation. Please log in first and try again.": "无法获取翻译。请登录后重试。",
"Group": "分组",
"Always on Top": "窗口置顶",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "出自《皆大欢喜》,第二幕",
"Link Color": "链接颜色",
"Volume Keys for Page Flip": "音量键翻页",
"Clicks for Page Flip": "点击翻页",
"Swap Clicks Area": "交换点击区域",
"Screen": "屏幕",
"Orientation": "屏幕方向",
"Portrait": "竖屏",
@@ -296,7 +294,6 @@
"Disable Translation": "禁用翻译",
"Scroll": "滚动",
"Overlap Pixels": "重叠像素",
"Click": "点击",
"System Language": "系统语言",
"Security": "安全",
"Allow JavaScript": "允许 JavaScript",
@@ -362,7 +359,6 @@
"Email:": "邮箱:",
"Plan:": "套餐:",
"Amount:": "金额:",
"Subscription ID:": "订阅 ID",
"Go to Library": "前往书库",
"Need help? Contact our support team at support@readest.com": "需要帮助?请联系支持团队:support@readest.com",
"Free Plan": "免费套餐",
@@ -396,7 +392,7 @@
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "已达每日翻译配额。升级套餐以继续使用 AI 翻译。",
"Includes All Plus Plan Benefits": "包含所有 Plus 套餐功能",
"Early Feature Access": "抢先体验新功能",
"Be the first to explore new features, updates, and innovations before anyone else.": "抢先体验新功能、更新创新。",
"Be the first to explore new features, updates, and innovations before anyone else.": "抢先体验新功能、更新创新成果。",
"Advanced AI Tools": "高级 AI 工具",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "利用强大的 AI 工具实现智能阅读、翻译和内容发现。",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "使用最精准的翻译引擎每天可翻译10万字。",
@@ -595,5 +591,66 @@
"File count: {{size}}": "文件数量: {{size}}",
"Background Read Aloud": "后台语音朗读",
"Ready to read aloud": "语音朗读已就绪",
"Read Aloud": "语音朗读"
"Read Aloud": "语音朗读",
"Screen Brightness": "屏幕亮度",
"Background Image": "背景图片",
"Import Image": "导入图片",
"Opacity": "不透明度",
"Size": "大小",
"Cover": "覆盖",
"Contain": "适应",
"{{number}} pages left in chapter": "本章剩余{{number}}页",
"Device": "设备",
"E-Ink Mode": "E-Ink 模式",
"Highlight Colors": "高亮颜色",
"Auto Screen Brightness": "自动屏幕亮度",
"Pagination": "翻页",
"Disable Double Tap": "禁用双击",
"Tap to Paginate": "点击翻页",
"Click to Paginate": "点击翻页",
"Tap Both Sides": "点击两侧翻页",
"Click Both Sides": "点击两侧翻页",
"Swap Tap Sides": "交换点击区域",
"Swap Click Sides": "交换点击区域",
"Source and Translated": "原文和译文",
"Translated Only": "仅译文",
"Source Only": "仅原文",
"TTS Text": "TTS 文本",
"The book file is corrupted": "书籍文件已损坏",
"The book file is empty": "书籍文件为空",
"Failed to open the book file": "无法打开书籍文件",
"On-Demand Purchase": "按需购买",
"Full Customization": "完全自定义",
"Lifetime Plan": "终身套餐",
"One-Time Payment": "一次性付款",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "进行一次性付款以享受在所有设备上对特定功能的终身访问。仅在需要时购买特定功能或服务。",
"Expand Cloud Sync Storage": "扩展云同步存储",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "通过一次性购买,永久扩展您的云存储。每次额外购买都会增加更多空间。",
"Unlock All Customization Options": "解锁所有自定义选项",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "解锁其他主题、字体、布局选项以及朗读、翻译、云存储服务。",
"Purchase Successful!": "购买成功!",
"lifetime": "永久",
"Thank you for your purchase! Your payment has been processed successfully.": "感谢您的购买!您的付款已成功处理。",
"Order ID:": "订单号:",
"TTS Highlighting": "TTS 高亮",
"Style": "样式",
"Underline": "下划线",
"Strikethrough": "删除线",
"Squiggly": "波浪线",
"Outline": "轮廓",
"Save Current Color": "保存当前颜色",
"Quick Colors": "快速颜色",
"Highlighter": "荧光笔",
"Save Book Cover": "保存书籍封面",
"Auto-save last book cover": "自动保存最后一本书的封面",
"Back": "返回",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "确认邮件已发送!请检查您的旧邮箱和新邮箱以确认更改。",
"Failed to update email": "更新邮箱失败",
"New Email": "新邮箱",
"Your new email": "您的新邮箱",
"Updating email ...": "正在更新邮箱 ...",
"Update email": "更新邮箱",
"Current email": "当前邮箱",
"Update Email": "更新邮箱",
"All": "全部"
}
@@ -221,9 +221,9 @@
"Auto Import on File Open": "打開文件時自動導入",
"LXGW WenKai GB Screen": "霞鶩文楷 SC",
"LXGW WenKai TC": "霞鶩文楷 TC",
"No chapters detected.": "未檢測到章節",
"Failed to parse the EPUB file.": "無法解析 EPUB 文件",
"This book format is not supported.": "不支援此書籍格式",
"No chapters detected": "未檢測到章節",
"Failed to parse the EPUB file": "無法解析 EPUB 文件",
"This book format is not supported": "不支援此書籍格式",
"Unable to fetch the translation. Please log in first and try again.": "無法獲取翻譯。請先登入然後重試。",
"Group": "分組",
"Always on Top": "視窗置頂",
@@ -260,8 +260,6 @@
"(from 'As You Like It', Act II)": "(出自《皆大歡喜》,第二幕)",
"Link Color": "連結顏色",
"Volume Keys for Page Flip": "音量鍵翻頁",
"Clicks for Page Flip": "點擊翻頁",
"Swap Clicks Area": "交換點擊區域",
"Screen": "螢幕",
"Orientation": "螢幕方向",
"Portrait": "豎屏",
@@ -296,7 +294,6 @@
"Disable Translation": "停用翻譯",
"Scroll": "滾動",
"Overlap Pixels": "重疊像素",
"Click": "點擊",
"System Language": "系統語言",
"Security": "安全",
"Allow JavaScript": "允許 JavaScript",
@@ -362,7 +359,6 @@
"Email:": "電子郵件:",
"Plan:": "方案:",
"Amount:": "金額:",
"Subscription ID:": "訂閱 ID",
"Go to Library": "前往書庫",
"Need help? Contact our support team at support@readest.com": "需要協助?請聯絡支援團隊:support@readest.com",
"Free Plan": "免費方案",
@@ -396,7 +392,7 @@
"Daily translation quota reached. Upgrade your plan to continue using AI translations.": "已達每日翻譯配額。升級方案以繼續使用 AI 翻譯。",
"Includes All Plus Plan Benefits": "包含所有 Plus 方案優惠",
"Early Feature Access": "搶先體驗新功能",
"Be the first to explore new features, updates, and innovations before anyone else.": "搶先體驗新功能、更新創新。",
"Be the first to explore new features, updates, and innovations before anyone else.": "搶先體驗新功能、更新創新成果。",
"Advanced AI Tools": "進階 AI 工具",
"Harness powerful AI tools for smarter reading, translation, and content discovery.": "運用強大的 AI 工具實現智慧閱讀、翻譯和內容探索。",
"Translate up to 100,000 characters daily with the most accurate translation engine available.": "使用最精準的翻譯引擎每天翻譯多達10萬字元。",
@@ -595,5 +591,66 @@
"File count: {{size}}": "文件數量: {{size}}",
"Background Read Aloud": "背景朗讀",
"Ready to read aloud": "朗讀準備就緒",
"Read Aloud": "朗讀"
"Read Aloud": "朗讀",
"Screen Brightness": "螢幕亮度",
"Background Image": "背景圖片",
"Import Image": "匯入圖片",
"Opacity": "不透明度",
"Size": "大小",
"Cover": "覆蓋",
"Contain": "適應",
"{{number}} pages left in chapter": "本章剩餘{{number}}頁",
"Device": "設備",
"E-Ink Mode": "E-Ink 模式",
"Highlight Colors": "高亮顏色",
"Auto Screen Brightness": "自動螢幕亮度",
"Pagination": "翻頁",
"Disable Double Tap": "禁用雙擊",
"Tap to Paginate": "點擊翻頁",
"Click to Paginate": "點擊翻頁",
"Tap Both Sides": "點擊兩側翻頁",
"Click Both Sides": "點擊兩側翻頁",
"Swap Tap Sides": "交換點擊區域",
"Swap Click Sides": "交換點擊區域",
"Source and Translated": "原文和翻譯",
"Translated Only": "僅翻譯",
"Source Only": "僅原文",
"TTS Text": "TTS 文字",
"The book file is corrupted": "書籍文件已損壞",
"The book file is empty": "書籍文件為空",
"Failed to open the book file": "無法打開書籍文件",
"On-Demand Purchase": "按需購買",
"Full Customization": "完全自定義",
"Lifetime Plan": "終身方案",
"One-Time Payment": "一次性付款",
"Make a single payment to enjoy lifetime access to specific features on all devices. Purchase specific features or services only when you need them.": "進行一次性付款以享受在所有設備上對特定功能的終身訪問。僅在需要時購買特定功能或服務。",
"Expand Cloud Sync Storage": "擴展雲同步存儲",
"Expand your cloud storage forever with a one-time purchase. Each additional purchase adds more space.": "通過一次性購買,永久擴展您的雲存儲。每次額外購買都會增加更多空間。",
"Unlock All Customization Options": "解鎖所有自定義選項",
"Unlock additional themes, fonts, layout options and read aloud, translators, cloud storage services.": "解鎖其他主題、字體、佈局選項以及朗讀、翻譯、雲存儲服務。",
"Purchase Successful!": "購買成功!",
"lifetime": "永久",
"Thank you for your purchase! Your payment has been processed successfully.": "感謝您的購買!您的付款已成功處理。",
"Order ID:": "訂單編號:",
"TTS Highlighting": "TTS 高亮",
"Style": "樣式",
"Underline": "底線",
"Strikethrough": "刪除線",
"Squiggly": "波浪線",
"Outline": "輪廓",
"Save Current Color": "儲存目前顏色",
"Quick Colors": "快速顏色",
"Highlighter": "螢光筆",
"Save Book Cover": "保存書籍封面",
"Auto-save last book cover": "自動保存最後一本書的封面",
"Back": "返回",
"Confirmation email sent! Please check your old and new email addresses to confirm the change.": "確認郵件已發送!請檢查您的舊電子郵件和新電子郵件以確認更改。",
"Failed to update email": "更新電子郵件失敗",
"New Email": "新電子郵件",
"Your new email": "您的新電子郵件",
"Updating email ...": "正在更新電子郵件 ...",
"Update email": "更新電子郵件",
"Current email": "當前電子郵件",
"Update Email": "更新電子郵件",
"All": "全部"
}
+96
View File
@@ -1,5 +1,101 @@
{
"releases": {
"0.9.93": {
"date": "2025-11-20",
"notes": [
"Sync: Improved reliability for new accounts so your books sync correctly from the start",
"Library: Fixed an occasional issue where returning to the library could get stuck",
"Layout: Improved image display in paginated mode so images won't exceed the available height",
"TXT: Improved performance and stability when opening and parsing TXT files",
"Fonts: Fixed an issue where monospace fonts were not applied correctly in code blocks"
]
},
"0.9.92": {
"date": "2025-11-18",
"notes": [
"Bookshelf: Added support for nested groups to organize your books more flexibly",
"Sync: Improved reliability by automatically retrying failed book syncs",
"Sync: Improved token refresh so login sessions stay active longer on KOReader",
"E-Ink: Enhanced readability on the library page for E-Ink devices",
"UI: Storage capacity is now displayed with precise values in the user profile",
"Reader: You can now import files directly into the currently selected book group",
"Account: You can now update the email address linked to your Readest account",
"Footnotes: Added compatibility for more footnote formats"
]
},
"0.9.91": {
"date": "2025-10-29",
"notes": [
"TTS: Fixed an issue where system voices were not working on Android",
"TTS: Added options to customize the highlight style during read-aloud",
"E-Ink: Disabled all shadows to improve display clarity",
"E-Ink: Added an option to save the last book cover on Android",
"Android: Fixed a crash that occurred when toggling 'Allow Script'",
"Android: Adjusted launcher icon size for better compatibility across devices"
]
},
"0.9.90": {
"date": "2025-10-28",
"notes": [
"Sync: Faster library syncing and support for in-app one-time purchases of extra cloud storage",
"TTS: You can now choose the target language for read-aloud on translated books",
"Reader: New keyboard shortcuts make it easier to move between sections",
"Reader: Smoother scrolling for a more comfortable reading experience",
"Reader: Footnotes in definition lists are now fully supported",
"Reader: Improved search accuracy and display of search results",
"Reader: Clearer error messages when importing books",
"Reader: Improved safety when loading book content",
"Settings: You can now choose your own highlight colors",
"Settings: Added option to adjust screen brightness automatically",
"E-Ink: Better readability and clearer progress display with underlined links",
"Android: You can now store app data on an external SD card",
"iOS: Restores your last reading page if the app was closed in the background",
"Localization: Added Persian (Farsi) language support"
]
},
"0.9.88": {
"date": "2025-10-18",
"notes": [
"TTS: Fixed issue where the read-aloud indicator disappeared too quickly",
"Layout: Overriding book layout no longer affects explicitly set text alignment",
"Reader: Added localized number display for reading progress in vertical layout",
"Reader: Enhanced swipe responsiveness for smoother page transitions",
"Settings: Added an option to disable animations when using E-Ink mode"
]
},
"0.9.87": {
"date": "2025-10-16",
"notes": [
"Reader: Improved chapter detection with better chapter title recognition",
"Reader: Enhanced compatibility for parsing more footnote formats",
"Theme: Fixed auto theme switching in new reader windows",
"TTS: Improved scrolling accuracy to highlighted text during read-aloud",
"TTS: Fixed crash issues on certain Android systems"
]
},
"0.9.86": {
"date": "2025-10-14",
"notes": [
"Reader: Added support for per-book background image customization",
"Reader: Added option to override book language metadata for improved hyphenation accuracy",
"Sync: Readest sync in KOReader can now be triggered using gesture controls",
"Sync: Fixed a crash in KOReader that occurred when syncing progress without an open book",
"Sync: Improved upload performance with concurrent upload support"
]
},
"0.9.85": {
"date": "2025-10-14",
"notes": [
"TTS: Improved media session control compatibility across more Android systems",
"TTS: Made the TTS icon less distracting during read-aloud mode",
"Settings: Added an option to adjust screen brightness while reading",
"Settings: Added support for custom background images",
"PDF: Fixed an issue where scrolling and panning didnt work correctly when zoomed in",
"Android: Added adaptive launcher icon for better system integration",
"Reader: Added keyboard shortcuts for toggling bookmarks",
"Reader: Annotations are now rendered instantly when opening files"
]
},
"0.9.82": {
"date": "2025-10-01",
"notes": [
@@ -3,42 +3,10 @@
set -e
VERSION=$(jq -r '.version' package.json)
if [[ -z "$VERSION" ]]; then
echo "❌ Failed to extract version from package.json"
exit 1
fi
echo "📦 Found version: $VERSION"
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
if [[ -z "$MAJOR" || -z "$MINOR" || -z "$PATCH" ]]; then
echo "❌ Invalid version format: $VERSION"
exit 1
fi
# Convert x.y.z => x * 10000 + y * 1000 + z
VERSION_CODE=$((10#$MAJOR * 10000 + 10#$MINOR * 1000 + 10#$PATCH))
echo "🔢 Computed versionCode: $VERSION_CODE"
PROPERTIES_FILE="./src-tauri/gen/android/app/tauri.properties"
MANIFEST="./src-tauri/gen/android/app/src/main/AndroidManifest.xml"
INSTALL_PERMISSION_LINE='<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>'
STORAGE_PERMISSION_LINE='<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>'
if [[ ! -f "$PROPERTIES_FILE" ]]; then
echo "❌ File not found: $PROPERTIES_FILE"
exit 1
fi
tmpfile=$(mktemp)
sed "s/^tauri\.android\.versionName=.*/tauri.android.versionName=$VERSION/" "$PROPERTIES_FILE" | \
sed "s/^tauri\.android\.versionCode=.*/tauri.android.versionCode=$VERSION_CODE/" > "$tmpfile"
mv "$tmpfile" "$PROPERTIES_FILE"
echo "✅ Updated $PROPERTIES_FILE"
ised() {
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "$@"
@@ -71,7 +39,7 @@ if grep -q 'MANAGE_EXTERNAL_STORAGE' "$MANIFEST"; then
fi
echo "🚀 Running: pnpm tauri android build"
pnpm tauri android build
pnpm tauri android build --config src-tauri/tauri.playstore.conf.json
# --- ADD PERMISSION BACK AFTER BUILD ---
if ! grep -q 'REQUEST_INSTALL_PACKAGES' "$MANIFEST"; then
@@ -94,4 +62,5 @@ if [[ -z "$GOOGLE_PLAY_JSON_KEY_FILE" ]]; then
exit 1
fi
cd ../../
fastlane android upload_production
@@ -104,6 +104,12 @@
},
{
"url": "https://*:*"
},
{
"url": "http://*"
},
{
"url": "https://*"
}
]
},
@@ -134,6 +140,14 @@
"oauth:allow-cancel",
"sign-in-with-apple:default",
"opener:default",
{
"identifier": "opener:allow-open-url",
"allow": [
{
"url": "alipays:*"
}
]
},
"haptics:allow-vibrate",
"haptics:allow-impact-feedback",
"haptics:allow-notification-feedback",
@@ -57,9 +57,15 @@ android {
}
}
getByName("release") {
isMinifyEnabled = true
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs.getByName("signing")
}
proguardFiles(
*fileTree(".") { include("**/*.pro") }
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
.toList().toTypedArray()
)
}
}
kotlinOptions {
@@ -75,13 +81,13 @@ rust {
}
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")
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("com.google.android.material:material:1.12.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")
apply(from = "tauri.build.gradle.kts")
@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="com.android.vending.BILLING" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
@@ -115,11 +116,22 @@
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
</activity>
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<service
android:name="com.readest.native_tts.MediaForegroundService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="mediaPlayback" />
android:name="com.readest.native_tts.MediaPlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
<provider
android:name="androidx.core.content.FileProvider"
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground>
<inset
android:drawable="@mipmap/ic_launcher_foreground"
android:inset="22%"/>
</foreground>
</adaptive-icon>
@@ -0,0 +1,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>
@@ -33,6 +33,7 @@ android {
}
dependencies {
implementation("com.android.billingclient:billing-ktx:7.1.1")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.browser:browser:1.8.0")
@@ -0,0 +1,283 @@
package com.readest.native_bridge
import android.app.Activity
import android.content.Context
import android.util.Log
import com.android.billingclient.api.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.*
class BillingManager(private val activity: Activity) : PurchasesUpdatedListener {
private lateinit var billingClient: BillingClient
private val productsCache = mutableMapOf<String, ProductDetails>()
private var purchaseCallback: ((PurchaseData?) -> Unit)? = null
private val scope = CoroutineScope(Dispatchers.Main)
companion object {
private const val TAG = "BillingManager"
}
fun initialize(callback: (Boolean) -> Unit) {
billingClient = BillingClient.newBuilder(activity)
.setListener(this)
.enablePendingPurchases()
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d(TAG, "Billing client setup finished successfully")
callback(true)
} else {
Log.e(TAG, "Billing setup failed: ${billingResult.debugMessage}")
callback(false)
}
}
override fun onBillingServiceDisconnected() {
Log.w(TAG, "Billing service disconnected")
// Try to reconnect
initialize { }
}
})
}
fun fetchProducts(productIds: List<String>, callback: (List<ProductData>) -> Unit) {
if (!::billingClient.isInitialized || !billingClient.isReady) {
Log.e(TAG, "Billing client not ready")
callback(emptyList())
return
}
scope.launch {
val products = mutableListOf<ProductData>()
// Check for subscription products
val subsIds = productIds.filter {
it.contains("monthly") || it.contains("yearly") || it.contains("subscription")
}
if (subsIds.isNotEmpty()) {
fetchProductsOfType(subsIds, BillingClient.ProductType.SUBS) { subProducts ->
products.addAll(subProducts)
// Then fetch in-app products
val inAppIds = productIds - subsIds.toSet()
if (inAppIds.isNotEmpty()) {
fetchProductsOfType(inAppIds, BillingClient.ProductType.INAPP) { inAppProducts ->
products.addAll(inAppProducts)
callback(products)
}
} else {
callback(products)
}
}
} else {
// Only in-app products
fetchProductsOfType(productIds, BillingClient.ProductType.INAPP) { inAppProducts ->
products.addAll(inAppProducts)
callback(products)
}
}
}
}
private fun fetchProductsOfType(
productIds: List<String>,
productType: String,
callback: (List<ProductData>) -> Unit
) {
val productList = productIds.map { productId ->
QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(productType)
.build()
}
val params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
val products = productDetailsList.map { productDetails ->
// Cache for purchase later
productsCache[productDetails.productId] = productDetails
when (productType) {
BillingClient.ProductType.SUBS -> {
val offer = productDetails.subscriptionOfferDetails?.firstOrNull()
val pricingPhase = offer?.pricingPhases?.pricingPhaseList?.firstOrNull()
pricingPhase?.let {
ProductData(
id = productDetails.productId,
title = productDetails.title,
description = productDetails.description,
price = it.formattedPrice,
priceCurrencyCode = it.priceCurrencyCode,
priceAmountMicros = it.priceAmountMicros,
productType = "subscription"
)
}
}
BillingClient.ProductType.INAPP -> {
val oneTimeOffer = productDetails.oneTimePurchaseOfferDetails
oneTimeOffer?.let {
ProductData(
id = productDetails.productId,
title = productDetails.title,
description = productDetails.description,
price = it.formattedPrice,
priceCurrencyCode = it.priceCurrencyCode,
priceAmountMicros = it.priceAmountMicros,
productType = "consumable"
)
}
}
else -> null
}
}.filterNotNull()
callback(products)
} else {
Log.e(TAG, "Failed to fetch products: ${billingResult.debugMessage}")
callback(emptyList())
}
}
}
fun purchaseProduct(productId: String, callback: (PurchaseData?) -> Unit) {
val productDetails = productsCache[productId]
if (productDetails == null) {
Log.e(TAG, "Product not found in cache: $productId")
callback(null)
return
}
purchaseCallback = callback
val productDetailsParamsList = listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.apply {
// For subscriptions, use the first offer
productDetails.subscriptionOfferDetails?.firstOrNull()?.let { offer ->
setOfferToken(offer.offerToken)
}
}
.build()
)
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.build()
val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams)
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
Log.e(TAG, "Failed to launch billing flow: ${billingResult.debugMessage}")
callback(null)
purchaseCallback = null
}
}
fun restorePurchases(callback: (List<PurchaseData>) -> Unit) {
if (!::billingClient.isInitialized || !billingClient.isReady) {
Log.e(TAG, "Billing client not ready")
callback(emptyList())
return
}
scope.launch {
val allPurchases = mutableListOf<PurchaseData>()
// Query in-app purchases
val inappParams = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.INAPP)
.build()
billingClient.queryPurchasesAsync(inappParams) { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
allPurchases.addAll(purchases.map { purchase ->
convertToPurchaseData(purchase, "restored")
})
}
// Query subscription purchases
val subsParams = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
billingClient.queryPurchasesAsync(subsParams) { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
allPurchases.addAll(purchases.map { purchase ->
convertToPurchaseData(purchase, "restored")
})
}
callback(allPurchases)
}
}
}
}
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) {
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
purchases?.forEach { purchase ->
handlePurchase(purchase)
}
}
BillingClient.BillingResponseCode.USER_CANCELED -> {
Log.d(TAG, "Purchase cancelled by user")
purchaseCallback?.invoke(null)
purchaseCallback = null
}
else -> {
Log.e(TAG, "Purchase failed: ${billingResult.debugMessage}")
purchaseCallback?.invoke(null)
purchaseCallback = null
}
}
}
private fun handlePurchase(purchase: Purchase) {
// Acknowledge the purchase
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged) {
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d(TAG, "Purchase acknowledged")
}
}
}
val purchaseData = convertToPurchaseData(purchase, "purchased")
purchaseCallback?.invoke(purchaseData)
purchaseCallback = null
}
}
private fun convertToPurchaseData(purchase: Purchase, state: String): PurchaseData {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
return PurchaseData(
platform = "android",
productId = purchase.products.firstOrNull() ?: "",
orderId = purchase.orderId ?: purchase.purchaseToken,
purchaseToken = purchase.purchaseToken,
purchaseDate = dateFormat.format(Date(purchase.purchaseTime)),
purchaseState = state,
)
}
}
@@ -31,6 +31,7 @@ import app.tauri.annotation.InvokeArg
import app.tauri.annotation.Permission
import app.tauri.annotation.TauriPlugin
import app.tauri.plugin.JSObject
import app.tauri.plugin.JSArray
import app.tauri.plugin.Plugin
import app.tauri.plugin.Invoke
import org.json.JSONArray
@@ -38,37 +39,76 @@ import java.io.*
@InvokeArg
class AuthRequestArgs {
var authUrl: String? = null
var authUrl: String? = null
}
@InvokeArg
class CopyURIRequestArgs {
var uri: String? = null
var dst: String? = null
var uri: String? = null
var dst: String? = null
}
@InvokeArg
class InstallPackageRequestArgs {
var path: String? = null
var path: String? = null
}
@InvokeArg
class SetSystemUIVisibilityRequestArgs {
var visible: Boolean? = false
var darkMode: Boolean? = false
var visible: Boolean? = false
var darkMode: Boolean? = false
}
@InvokeArg
class InterceptKeysRequestArgs {
var volumeKeys: Boolean? = null
var backKey: Boolean? = null
var volumeKeys: Boolean? = null
var backKey: Boolean? = null
}
@InvokeArg
class LockScreenOrientationRequestArgs {
var orientation: String? = null
var orientation: String? = null
}
@InvokeArg
class SetScreenBrightnessRequestArgs {
var brightness: Double? = null // 0.0 to 1.0
}
@InvokeArg
class OpenExternalUrlArgs {
var url: String? = null
}
@InvokeArg
class FetchProductsRequestArgs {
val productIds: List<String>? = null
}
@InvokeArg
class PurchaseProductRequestArgs {
val productId: String? = null
}
data class ProductData(
val id: String,
val title: String,
val description: String,
val price: String,
val priceCurrencyCode: String?,
val priceAmountMicros: Long,
val productType: String
)
data class PurchaseData(
val productId: String,
val orderId: String,
val purchaseToken: String,
val purchaseDate: String,
val purchaseState: String,
val platform: String = "android"
)
interface KeyDownInterceptor {
fun interceptVolumeKeys(enabled: Boolean)
fun interceptBackKey(enabled: Boolean)
@@ -76,13 +116,16 @@ interface KeyDownInterceptor {
@TauriPlugin(
permissions = [
Permission(strings = [Manifest.permission.MANAGE_EXTERNAL_STORAGE], alias = "manageStorage")
Permission(strings = [Manifest.permission.MANAGE_EXTERNAL_STORAGE], alias = "manageStorage"),
]
)
class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
private val implementation = NativeBridge()
private var redirectScheme = "readest"
private var redirectHost = "auth-callback"
private val billingManager by lazy {
BillingManager(activity)
}
companion object {
var pendingInvoke: Invoke? = null
@@ -415,6 +458,165 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
invoke.resolve(ret)
}
@Command
fun get_screen_brightness(invoke: Invoke) {
val ret = JSObject()
try {
val window = activity.window
val layoutParams = window.attributes
val brightness = layoutParams.screenBrightness
if (brightness >= 0.0f) {
ret.put("brightness", brightness.toDouble())
} else {
val systemBrightness = Settings.System.getInt(
activity.contentResolver,
Settings.System.SCREEN_BRIGHTNESS
)
ret.put("brightness", systemBrightness / 255.0)
}
} catch (e: Exception) {
ret.put("error", e.message)
ret.put("brightness", -1.0)
}
invoke.resolve(ret)
}
@Command
fun set_screen_brightness(invoke: Invoke) {
val args = invoke.parseArgs(SetScreenBrightnessRequestArgs::class.java)
val ret = JSObject()
try {
val brightness = (args.brightness ?: 0.5).toFloat()
if (brightness < 0.0 || brightness > 1.0) {
invoke.reject("Brightness must be between 0.0 and 1.0")
return
}
val layoutParams = activity.window.attributes
layoutParams.screenBrightness = brightness
activity.window.attributes = layoutParams
ret.put("success", true)
} catch (e: Exception) {
ret.put("success", false)
ret.put("error", e.message)
}
invoke.resolve(ret)
}
@Command
fun iap_initialize(invoke: Invoke) {
billingManager.initialize { success ->
val result = JSObject()
result.put("success", success)
invoke.resolve(result)
}
}
@Command
fun iap_fetch_products(invoke: Invoke) {
try {
val args = invoke.parseArgs(FetchProductsRequestArgs::class.java)
val productIds = args.productIds ?: emptyList()
if (productIds.isEmpty()) {
invoke.reject("Product IDs list is empty")
return
}
billingManager.fetchProducts(productIds) { products ->
val result = JSObject()
val productsArray = JSArray()
for (product in products) {
val productObject = JSObject().apply {
put("id", product.id)
put("title", product.title)
put("description", product.description)
put("price", product.price)
put("priceCurrencyCode", product.priceCurrencyCode)
put("priceAmountMicros", product.priceAmountMicros)
put("productType", product.productType)
}
productsArray.put(productObject)
}
result.put("products", productsArray)
invoke.resolve(result)
}
} catch (e: Exception) {
invoke.reject("Failed to parse fetch products arguments: ${e.message}")
}
}
@Command
fun iap_purchase_product(invoke: Invoke) {
try {
val args = invoke.parseArgs(PurchaseProductRequestArgs::class.java)
val productId = args.productId ?: ""
if (productId.isEmpty()) {
invoke.reject("Product ID is empty")
return
}
billingManager.purchaseProduct(productId) { purchase ->
if (purchase != null) {
val result = JSObject()
val purchaseData = JSObject().apply {
put("platform", purchase.platform)
put("packageName", activity.packageName)
put("productId", purchase.productId)
put("orderId", purchase.orderId)
put("purchaseToken", purchase.purchaseToken)
put("purchaseDate", purchase.purchaseDate)
put("purchaseState", purchase.purchaseState)
}
result.put("purchase", purchaseData)
invoke.resolve(result)
} else {
invoke.reject("Purchase failed or was cancelled")
}
}
} catch (e: Exception) {
invoke.reject("Failed to parse purchase arguments: ${e.message}")
}
}
@Command
fun iap_restore_purchases(invoke: Invoke) {
billingManager.restorePurchases { purchases ->
val result = JSObject()
val purchasesArray = JSArray()
for (purchase in purchases) {
val purchaseObject = JSObject().apply {
put("platform", purchase.platform)
put("packageName", activity.packageName)
put("productId", purchase.productId)
put("orderId", purchase.orderId)
put("purchaseToken", purchase.purchaseToken)
put("purchaseDate", purchase.purchaseDate)
put("purchaseState", purchase.purchaseState)
}
purchasesArray.put(purchaseObject)
}
result.put("purchases", purchasesArray)
invoke.resolve(result)
}
}
@Command
fun get_external_sdcard_path(invoke: Invoke) {
val result = JSObject()
val externalDirs = activity.getExternalFilesDirs(null)
for (file in externalDirs) {
if (file != null && Environment.isExternalStorageRemovable(file)) {
val pathParts = file.absolutePath.split("/Android/")
if (pathParts.isNotEmpty()) {
result.put("path", pathParts[0])
invoke.resolve(result)
}
}
}
result.put("path", null)
invoke.resolve(result)
}
@Command
fun request_manage_storage_permission(invoke: Invoke) {
val ret = JSObject()
@@ -463,4 +665,21 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
}
}
}
@Command
fun open_external_url(invoke: Invoke) {
val args = invoke.parseArgs(OpenExternalUrlArgs::class.java)
val url = args.url ?: ""
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
activity.startActivity(intent)
val ret = JSObject()
ret.put("success", true)
invoke.resolve(ret)
} catch (e: Exception) {
invoke.reject("Failed to open URL: ${e.message}")
}
}
}
@@ -15,7 +15,13 @@ const COMMANDS: &[&str] = &[
"iap_restore_purchases",
"get_system_color_scheme",
"get_safe_area_insets",
"get_screen_brightness",
"set_screen_brightness",
"get_external_sdcard_path",
"open_external_url",
"request_manage_storage_permission",
"check_permissions",
"request_permissions",
"checkPermissions",
"requestPermissions",
];
@@ -49,6 +49,10 @@ class LockScreenOrientationRequestArgs: Decodable {
let orientation: String?
}
class SetScreenBrightnessRequestArgs: Decodable {
let brightness: Float?
}
struct InitializeRequest: Decodable {
let publicKey: String?
}
@@ -183,16 +187,246 @@ class VolumeKeyHandler: NSObject {
}
}
class WebViewLifecycleManager: NSObject {
private weak var webView: WKWebView?
private var originalNavigationDelegate: WKNavigationDelegate?
private var isMonitoring = false
private var lastBackgroundTime: Date?
private var backgroundTimeThreshold: TimeInterval = 180.0
func startMonitoring(webView: WKWebView) {
self.webView = webView
originalNavigationDelegate = webView.navigationDelegate
webView.navigationDelegate = self
isMonitoring = true
logger.log("WebViewLifecycleManager: Started monitoring WebView")
}
func stopMonitoring() {
isMonitoring = false
if let original = originalNavigationDelegate {
webView?.navigationDelegate = original
}
logger.log("WebViewLifecycleManager: Stopped monitoring WebView")
}
func handleAppWillEnterForeground() {
guard isMonitoring, let webView = webView else {
logger.warning(
"WebViewLifecycleManager: Cannot handle foreground - not monitoring or webView is nil")
return
}
logger.log("WebViewLifecycleManager: App entering foreground")
var timeInBackground: TimeInterval = 0
if let backgroundTime = lastBackgroundTime {
timeInBackground = Date().timeIntervalSince(backgroundTime)
logger.log("WebViewLifecycleManager: Time in background: \(timeInBackground)s")
}
// If app was backgrounded for more than threshold, check WebView health
if timeInBackground > backgroundTimeThreshold {
logger.log(
"WebViewLifecycleManager: App was backgrounded for \(timeInBackground)s, checking WebView health..."
)
checkAndRecoverWebView(webView, reason: "long_background")
} else {
// Still do a quick check after a small delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
self?.quickHealthCheck(webView)
}
}
lastBackgroundTime = nil
}
func handleAppWillResignActive() {
logger.log("WebViewLifecycleManager: App will resign active")
guard let webView = webView else { return }
webView.evaluateJavaScript("window.location.href") { result, error in
if let error = error {
logger.error("WebViewLifecycleManager: Failed to capture URL on background: \(error)")
return
}
if let urlString = result as? String {
if urlString.hasPrefix("http") || urlString.hasPrefix("tauri") {
UserDefaults.standard.set(urlString, forKey: "tauri_last_valid_url")
logger.log("WebViewLifecycleManager: Saved valid URL")
}
}
}
}
func handleAppDidEnterBackground() {
lastBackgroundTime = Date()
}
private func quickHealthCheck(_ webView: WKWebView) {
logger.log("WebViewLifecycleManager: Performing quick health check")
webView.evaluateJavaScript("window.location.href") { [weak self] result, error in
if let error = error {
logger.error("WebViewLifecycleManager: Quick health check failed: \(error)")
self?.checkAndRecoverWebView(webView, reason: "health_check_failed")
} else if let urlString = result as? String {
if urlString.contains("about:blank") || urlString.isEmpty {
logger.warning("WebViewLifecycleManager: WebView showing about:blank!")
self?.recoverWebView(webView, reason: "about_blank")
}
}
}
}
private func checkAndRecoverWebView(_ webView: WKWebView, reason: String) {
logger.log("WebViewLifecycleManager: Checking WebView health (reason: \(reason))")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
webView.evaluateJavaScript("window.location.href") { result, error in
if let error = error {
logger.error("WebViewLifecycleManager: Error checking WebView URL: \(error)")
self?.recoverWebView(webView, reason: "js_error_\(reason)")
} else if let urlString = result as? String {
logger.log("WebViewLifecycleManager: Current URL after \(reason): \(urlString)")
if urlString.contains("about:blank") || urlString.isEmpty {
logger.warning("WebViewLifecycleManager: Detected blank WebView after \(reason)")
self?.recoverWebView(webView, reason: reason)
} else {
logger.log("WebViewLifecycleManager: WebView appears healthy")
}
}
}
}
}
private func recoverWebView(_ webView: WKWebView, reason: String) {
logger.log("WebViewLifecycleManager: Recovering WebView (reason: \(reason))")
if let lastURL = UserDefaults.standard.string(forKey: "tauri_last_valid_url"),
let url = URL(string: lastURL)
{
logger.log("WebViewLifecycleManager: Reloading from saved URL: \(lastURL)")
webView.load(URLRequest(url: url))
} else {
logger.log("WebViewLifecycleManager: No saved URL, performing standard reload")
webView.reload()
}
}
}
extension WebViewLifecycleManager: WKNavigationDelegate {
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
logger.error("WebViewLifecycleManager: WebContent process TERMINATED!")
recoverWebView(webView, reason: "process_terminated")
if let original = originalNavigationDelegate,
original.responds(to: #selector(webViewWebContentProcessDidTerminate(_:)))
{
original.webViewWebContentProcessDidTerminate?(webView)
}
}
// Save successful navigation URLs
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if let url = webView.url {
let urlString = url.absoluteString
if urlString.hasPrefix("http") || urlString.hasPrefix("tauri") {
UserDefaults.standard.set(urlString, forKey: "tauri_last_valid_url")
logger.log("WebViewLifecycleManager: Saved valid URL")
}
}
if let original = originalNavigationDelegate,
original.responds(to: #selector(webView(_:didFinish:)))
{
original.webView?(webView, didFinish: navigation)
}
}
// Proxy other important navigation delegate methods
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
logger.error("WebViewLifecycleManager: Navigation failed: \(error)")
if let original = originalNavigationDelegate,
original.responds(to: #selector(webView(_:didFail:withError:)))
{
original.webView?(webView, didFail: navigation, withError: error)
}
}
func webView(
_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!,
withError error: Error
) {
logger.error("WebViewLifecycleManager: Provisional navigation failed: \(error)")
if let original = originalNavigationDelegate,
original.responds(to: #selector(webView(_:didFailProvisionalNavigation:withError:)))
{
original.webView?(webView, didFailProvisionalNavigation: navigation, withError: error)
}
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
if let original = originalNavigationDelegate,
original.responds(to: #selector(webView(_:didStartProvisionalNavigation:)))
{
original.webView?(webView, didStartProvisionalNavigation: navigation)
}
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
if let original = originalNavigationDelegate,
original.responds(to: #selector(webView(_:didCommit:)))
{
original.webView?(webView, didCommit: navigation)
}
}
func webView(
_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
if let original = originalNavigationDelegate {
original.webView?(
webView, decidePolicyFor: navigationAction, decisionHandler: decisionHandler)
} else {
decisionHandler(.allow)
}
}
func webView(
_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse,
decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void
) {
if let original = originalNavigationDelegate {
original.webView?(
webView, decidePolicyFor: navigationResponse, decisionHandler: decisionHandler)
} else {
decisionHandler(.allow)
}
}
}
class NativeBridgePlugin: Plugin {
private var webView: WKWebView?
private var authSession: ASWebAuthenticationSession?
private var currentOrientationMask: UIInterfaceOrientationMask = .all
private var originalDelegate: UIApplicationDelegate?
private var webViewLifecycleManager: WebViewLifecycleManager?
@objc public override func load(webview: WKWebView) {
self.webView = webview
logger.log("NativeBridgePlugin loaded")
webViewLifecycleManager = WebViewLifecycleManager()
webViewLifecycleManager?.startMonitoring(webView: webview)
logger.log("NativeBridgePlugin: WebView lifecycle monitoring activated")
NotificationCenter.default.addObserver(
self,
selector: #selector(appDidBecomeActive),
@@ -207,6 +441,13 @@ class NativeBridgePlugin: Plugin {
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(appWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil
)
if let app = UIApplication.value(forKey: "sharedApplication") as? UIApplication {
self.originalDelegate = app.delegate
app.delegate = self
@@ -215,6 +456,11 @@ class NativeBridgePlugin: Plugin {
}
}
@objc func appWillEnterForeground() {
logger.log("NativeBridgePlugin: App will enter foreground")
webViewLifecycleManager?.handleAppWillEnterForeground()
}
@objc func appDidBecomeActive() {
if volumeKeyHandler != nil {
activateVolumeKeyInterception()
@@ -222,9 +468,11 @@ class NativeBridgePlugin: Plugin {
}
@objc func appDidEnterBackground() {
logger.log("NativeBridgePlugin: App did enter background")
if let handler = volumeKeyHandler, handler.isIntercepting {
handler.stopInterception()
}
webViewLifecycleManager?.handleAppDidEnterBackground()
}
func activateVolumeKeyInterception() {
@@ -241,6 +489,9 @@ class NativeBridgePlugin: Plugin {
}
deinit {
webViewLifecycleManager?.stopMonitoring()
webViewLifecycleManager = nil
NotificationCenter.default.removeObserver(self)
}
@@ -539,6 +790,28 @@ class NativeBridgePlugin: Plugin {
let colorScheme = (userInterfaceStyle == .dark) ? "dark" : "light"
invoke.resolve(["colorScheme": colorScheme])
}
@objc public func get_screen_brightness(_ invoke: Invoke) {
let brightness = UIScreen.main.brightness
invoke.resolve(["brightness": brightness])
}
@objc public func set_screen_brightness(_ invoke: Invoke) {
guard let args = try? invoke.parseArgs(SetScreenBrightnessRequestArgs.self) else {
return invoke.reject("Failed to parse arguments")
}
let brightness = args.brightness ?? 0.5
if brightness < 0.0 || brightness > 1.0 {
return invoke.reject("Brightness must be between 0.0 and 1.0")
}
DispatchQueue.main.async {
UIScreen.main.brightness = CGFloat(brightness)
}
invoke.resolve(["success": true])
}
}
@_cdecl("init_plugin_native_bridge")
@@ -600,14 +873,17 @@ extension NativeBridgePlugin: UIApplicationDelegate {
}
public func applicationWillResignActive(_ application: UIApplication) {
webViewLifecycleManager?.handleAppWillResignActive()
self.originalDelegate?.applicationWillResignActive?(application)
}
public func applicationWillEnterForeground(_ application: UIApplication) {
webViewLifecycleManager?.handleAppWillEnterForeground()
self.originalDelegate?.applicationWillEnterForeground?(application)
}
public func applicationDidEnterBackground(_ application: UIApplication) {
webViewLifecycleManager?.handleAppDidEnterBackground()
self.originalDelegate?.applicationDidEnterBackground?(application)
}
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-check-permissions"
description = "Enables the check-permissions command without any pre-configured scope."
commands.allow = ["check-permissions"]
[[permission]]
identifier = "deny-check-permissions"
description = "Denies the check-permissions command without any pre-configured scope."
commands.deny = ["check-permissions"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-check-permissions"
description = "Enables the check_permissions command without any pre-configured scope."
commands.allow = ["check_permissions"]
[[permission]]
identifier = "deny-check-permissions"
description = "Denies the check_permissions command without any pre-configured scope."
commands.deny = ["check_permissions"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-get-external-sdcard-path"
description = "Enables the get_external_sdcard_path command without any pre-configured scope."
commands.allow = ["get_external_sdcard_path"]
[[permission]]
identifier = "deny-get-external-sdcard-path"
description = "Denies the get_external_sdcard_path command without any pre-configured scope."
commands.deny = ["get_external_sdcard_path"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-get-screen-brightness"
description = "Enables the get_screen_brightness command without any pre-configured scope."
commands.allow = ["get_screen_brightness"]
[[permission]]
identifier = "deny-get-screen-brightness"
description = "Denies the get_screen_brightness command without any pre-configured scope."
commands.deny = ["get_screen_brightness"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-open-external-url"
description = "Enables the open_external_url command without any pre-configured scope."
commands.allow = ["open_external_url"]
[[permission]]
identifier = "deny-open-external-url"
description = "Denies the open_external_url command without any pre-configured scope."
commands.deny = ["open_external_url"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-request-permissions"
description = "Enables the request-permissions command without any pre-configured scope."
commands.allow = ["request-permissions"]
[[permission]]
identifier = "deny-request-permissions"
description = "Denies the request-permissions command without any pre-configured scope."
commands.deny = ["request-permissions"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-request-permissions"
description = "Enables the request_permissions command without any pre-configured scope."
commands.allow = ["request_permissions"]
[[permission]]
identifier = "deny-request-permissions"
description = "Denies the request_permissions command without any pre-configured scope."
commands.deny = ["request_permissions"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-set-screen-brightness"
description = "Enables the set_screen_brightness command without any pre-configured scope."
commands.allow = ["set_screen_brightness"]
[[permission]]
identifier = "deny-set-screen-brightness"
description = "Denies the set_screen_brightness command without any pre-configured scope."
commands.deny = ["set_screen_brightness"]
@@ -20,7 +20,13 @@ Default permissions for the plugin
- `allow-iap-restore-purchases`
- `allow-get-system-color-scheme`
- `allow-get-safe-area-insets`
- `allow-get-screen-brightness`
- `allow-set-screen-brightness`
- `allow-get-external-sdcard-path`
- `allow-open-external-url`
- `allow-request-manage-storage-permission`
- `allow-check-permissions`
- `allow-request-permissions`
- `allow-checkPermissions`
- `allow-requestPermissions`
@@ -88,6 +94,32 @@ Denies the auth_with_safari command without any pre-configured scope.
<tr>
<td>
`native-bridge:allow-check-permissions`
</td>
<td>
Enables the check-permissions command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-check-permissions`
</td>
<td>
Denies the check-permissions command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-checkPermissions`
</td>
@@ -114,6 +146,32 @@ Denies the checkPermissions command without any pre-configured scope.
<tr>
<td>
`native-bridge:allow-check-permissions`
</td>
<td>
Enables the check_permissions command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-check-permissions`
</td>
<td>
Denies the check_permissions command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-copy-uri-to-path`
</td>
@@ -140,6 +198,32 @@ Denies the copy_uri_to_path command without any pre-configured scope.
<tr>
<td>
`native-bridge:allow-get-external-sdcard-path`
</td>
<td>
Enables the get_external_sdcard_path command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-get-external-sdcard-path`
</td>
<td>
Denies the get_external_sdcard_path command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-get-safe-area-insets`
</td>
@@ -166,6 +250,32 @@ Denies the get_safe_area_insets command without any pre-configured scope.
<tr>
<td>
`native-bridge:allow-get-screen-brightness`
</td>
<td>
Enables the get_screen_brightness command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-get-screen-brightness`
</td>
<td>
Denies the get_screen_brightness command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-get-status-bar-height`
</td>
@@ -426,6 +536,58 @@ Denies the lock_screen_orientation command without any pre-configured scope.
<tr>
<td>
`native-bridge:allow-open-external-url`
</td>
<td>
Enables the open_external_url command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-open-external-url`
</td>
<td>
Denies the open_external_url command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-request-permissions`
</td>
<td>
Enables the request-permissions command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-request-permissions`
</td>
<td>
Denies the request-permissions command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-requestPermissions`
</td>
@@ -478,6 +640,58 @@ Denies the request_manage_storage_permission command without any pre-configured
<tr>
<td>
`native-bridge:allow-request-permissions`
</td>
<td>
Enables the request_permissions command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-request-permissions`
</td>
<td>
Denies the request_permissions command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-set-screen-brightness`
</td>
<td>
Enables the set_screen_brightness command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-set-screen-brightness`
</td>
<td>
Denies the set_screen_brightness command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-set-system-ui-visibility`
</td>
@@ -17,7 +17,13 @@ permissions = [
"allow-iap-restore-purchases",
"allow-get-system-color-scheme",
"allow-get-safe-area-insets",
"allow-get-screen-brightness",
"allow-set-screen-brightness",
"allow-get-external-sdcard-path",
"allow-open-external-url",
"allow-request-manage-storage-permission",
"allow-check-permissions",
"allow-request-permissions",
"allow-checkPermissions",
"allow-requestPermissions",
]
@@ -318,6 +318,18 @@
"const": "deny-auth-with-safari",
"markdownDescription": "Denies the auth_with_safari command without any pre-configured scope."
},
{
"description": "Enables the check-permissions command without any pre-configured scope.",
"type": "string",
"const": "allow-check-permissions",
"markdownDescription": "Enables the check-permissions command without any pre-configured scope."
},
{
"description": "Denies the check-permissions command without any pre-configured scope.",
"type": "string",
"const": "deny-check-permissions",
"markdownDescription": "Denies the check-permissions command without any pre-configured scope."
},
{
"description": "Enables the checkPermissions command without any pre-configured scope.",
"type": "string",
@@ -330,6 +342,18 @@
"const": "deny-checkPermissions",
"markdownDescription": "Denies the checkPermissions command without any pre-configured scope."
},
{
"description": "Enables the check_permissions command without any pre-configured scope.",
"type": "string",
"const": "allow-check-permissions",
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
},
{
"description": "Denies the check_permissions command without any pre-configured scope.",
"type": "string",
"const": "deny-check-permissions",
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
},
{
"description": "Enables the copy_uri_to_path command without any pre-configured scope.",
"type": "string",
@@ -342,6 +366,18 @@
"const": "deny-copy-uri-to-path",
"markdownDescription": "Denies the copy_uri_to_path command without any pre-configured scope."
},
{
"description": "Enables the get_external_sdcard_path command without any pre-configured scope.",
"type": "string",
"const": "allow-get-external-sdcard-path",
"markdownDescription": "Enables the get_external_sdcard_path command without any pre-configured scope."
},
{
"description": "Denies the get_external_sdcard_path command without any pre-configured scope.",
"type": "string",
"const": "deny-get-external-sdcard-path",
"markdownDescription": "Denies the get_external_sdcard_path command without any pre-configured scope."
},
{
"description": "Enables the get_safe_area_insets command without any pre-configured scope.",
"type": "string",
@@ -354,6 +390,18 @@
"const": "deny-get-safe-area-insets",
"markdownDescription": "Denies the get_safe_area_insets command without any pre-configured scope."
},
{
"description": "Enables the get_screen_brightness command without any pre-configured scope.",
"type": "string",
"const": "allow-get-screen-brightness",
"markdownDescription": "Enables the get_screen_brightness command without any pre-configured scope."
},
{
"description": "Denies the get_screen_brightness command without any pre-configured scope.",
"type": "string",
"const": "deny-get-screen-brightness",
"markdownDescription": "Denies the get_screen_brightness command without any pre-configured scope."
},
{
"description": "Enables the get_status_bar_height command without any pre-configured scope.",
"type": "string",
@@ -474,6 +522,30 @@
"const": "deny-lock-screen-orientation",
"markdownDescription": "Denies the lock_screen_orientation command without any pre-configured scope."
},
{
"description": "Enables the open_external_url command without any pre-configured scope.",
"type": "string",
"const": "allow-open-external-url",
"markdownDescription": "Enables the open_external_url command without any pre-configured scope."
},
{
"description": "Denies the open_external_url command without any pre-configured scope.",
"type": "string",
"const": "deny-open-external-url",
"markdownDescription": "Denies the open_external_url command without any pre-configured scope."
},
{
"description": "Enables the request-permissions command without any pre-configured scope.",
"type": "string",
"const": "allow-request-permissions",
"markdownDescription": "Enables the request-permissions command without any pre-configured scope."
},
{
"description": "Denies the request-permissions command without any pre-configured scope.",
"type": "string",
"const": "deny-request-permissions",
"markdownDescription": "Denies the request-permissions command without any pre-configured scope."
},
{
"description": "Enables the requestPermissions command without any pre-configured scope.",
"type": "string",
@@ -498,6 +570,30 @@
"const": "deny-request-manage-storage-permission",
"markdownDescription": "Denies the request_manage_storage_permission command without any pre-configured scope."
},
{
"description": "Enables the request_permissions command without any pre-configured scope.",
"type": "string",
"const": "allow-request-permissions",
"markdownDescription": "Enables the request_permissions command without any pre-configured scope."
},
{
"description": "Denies the request_permissions command without any pre-configured scope.",
"type": "string",
"const": "deny-request-permissions",
"markdownDescription": "Denies the request_permissions command without any pre-configured scope."
},
{
"description": "Enables the set_screen_brightness command without any pre-configured scope.",
"type": "string",
"const": "allow-set-screen-brightness",
"markdownDescription": "Enables the set_screen_brightness command without any pre-configured scope."
},
{
"description": "Denies the set_screen_brightness command without any pre-configured scope.",
"type": "string",
"const": "deny-set-screen-brightness",
"markdownDescription": "Denies the set_screen_brightness command without any pre-configured scope."
},
{
"description": "Enables the set_system_ui_visibility command without any pre-configured scope.",
"type": "string",
@@ -523,10 +619,10 @@
"markdownDescription": "Denies the use_background_audio command without any pre-configured scope."
},
{
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-request-manage-storage-permission`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
"type": "string",
"const": "default",
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-open-external-url`\n- `allow-request-manage-storage-permission`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
}
]
}
@@ -127,6 +127,36 @@ pub(crate) async fn get_safe_area_insets<R: Runtime>(
app.native_bridge().get_safe_area_insets()
}
#[command]
pub(crate) async fn get_screen_brightness<R: Runtime>(
app: AppHandle<R>,
) -> Result<GetScreenBrightnessResponse> {
app.native_bridge().get_screen_brightness()
}
#[command]
pub(crate) async fn set_screen_brightness<R: Runtime>(
app: AppHandle<R>,
payload: SetScreenBrightnessRequest,
) -> Result<SetScreenBrightnessResponse> {
app.native_bridge().set_screen_brightness(payload)
}
#[command]
pub(crate) async fn get_external_sdcard_path<R: Runtime>(
app: AppHandle<R>,
) -> Result<GetExternalSDCardPathResponse> {
app.native_bridge().get_external_sdcard_path()
}
#[command]
pub(crate) async fn open_external_url<R: Runtime>(
app: AppHandle<R>,
payload: OpenExternalUrlRequest,
) -> Result<OpenExternalUrlResponse> {
app.native_bridge().open_external_url(payload)
}
#[command]
pub(crate) async fn request_manage_storage_permission<R: Runtime>(
app: AppHandle<R>,
@@ -107,6 +107,28 @@ impl<R: Runtime> NativeBridge<R> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn get_screen_brightness(&self) -> crate::Result<GetScreenBrightnessResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn set_screen_brightness(
&self,
_payload: SetScreenBrightnessRequest,
) -> crate::Result<SetScreenBrightnessResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn get_external_sdcard_path(&self) -> crate::Result<GetExternalSDCardPathResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn open_external_url(
&self,
_payload: OpenExternalUrlRequest,
) -> crate::Result<OpenExternalUrlResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn request_manage_storage_permission(
&self,
) -> crate::Result<RequestManageStoragePermissionResponse> {
@@ -53,6 +53,10 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::iap_restore_purchases,
commands::get_system_color_scheme,
commands::get_safe_area_insets,
commands::get_screen_brightness,
commands::set_screen_brightness,
commands::get_external_sdcard_path,
commands::open_external_url,
commands::request_manage_storage_permission,
])
.setup(|app, api| {
@@ -170,6 +170,44 @@ impl<R: Runtime> NativeBridge<R> {
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn get_screen_brightness(&self) -> crate::Result<GetScreenBrightnessResponse> {
self.0
.run_mobile_plugin("get_screen_brightness", ())
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn set_screen_brightness(
&self,
payload: SetScreenBrightnessRequest,
) -> crate::Result<SetScreenBrightnessResponse> {
self.0
.run_mobile_plugin("set_screen_brightness", payload)
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn get_external_sdcard_path(&self) -> crate::Result<GetExternalSDCardPathResponse> {
self.0
.run_mobile_plugin("get_external_sdcard_path", ())
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn open_external_url(
&self,
payload: OpenExternalUrlRequest,
) -> crate::Result<OpenExternalUrlResponse> {
self.0
.run_mobile_plugin("open_external_url", payload)
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn request_manage_storage_permission(
&self,
@@ -102,12 +102,15 @@ pub struct Product {
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Purchase {
pub platform: String, // "ios" or "android"
pub package_name: Option<String>,
pub product_id: String,
pub transaction_id: String,
pub transaction_id: Option<String>,
pub original_transaction_id: Option<String>,
pub order_id: Option<String>,
pub purchase_token: Option<String>,
pub purchase_date: String,
pub original_transaction_id: String,
pub purchase_state: String, // "purchased", "pending", "cancelled"
pub platform: String, // "ios" or "android"
pub purchase_state: String, // "purchased", "pending", "cancelled", "restored"
}
#[derive(Debug, Serialize, Deserialize)]
@@ -168,8 +171,47 @@ pub struct GetSafeAreaInsetsResponse {
pub right: f64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetScreenBrightnessResponse {
pub brightness: f64,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetScreenBrightnessRequest {
pub brightness: f64, // 0.0 to 1.0
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SetScreenBrightnessResponse {
pub success: bool,
pub error: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetExternalSDCardPathResponse {
pub path: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestManageStoragePermissionResponse {
pub manage_storage: String, // "granted", "denied", or "prompt"
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenExternalUrlRequest {
pub url: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenExternalUrlResponse {
pub success: bool,
pub error: Option<String>,
}
@@ -33,9 +33,8 @@ android {
}
dependencies {
implementation("androidx.media3:media3-common:1.2.0")
implementation("androidx.media3:media3-session:1.2.0")
implementation("androidx.media3:media3-exoplayer:1.2.0")
implementation("androidx.media:media:1.7.1")
implementation("androidx.media3:media3-exoplayer:1.3.1")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.7.0")
@@ -0,0 +1,269 @@
package com.readest.native_tts
import com.readest.native_tts.R
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.graphics.Bitmap
import android.media.AudioManager
import android.media.AudioManager.OnAudioFocusChangeListener
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.session.MediaButtonReceiver
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import app.tauri.plugin.JSObject
class MediaPlaybackService : MediaBrowserServiceCompat() {
private var mediaSession: MediaSessionCompat? = null
private lateinit var player: ExoPlayer
private lateinit var stateBuilder: PlaybackStateCompat.Builder
private lateinit var audioManager: AudioManager
private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
Log.i("MediaPlaybackService", "Audio focus changed: $focusChange, $player.isPlaying")
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN -> {
player.volume = 1.0f
if (!player.isPlaying) player.play()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
player.volume = 0.3f
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
if (player.isPlaying) player.pause()
}
}
}
companion object {
private const val CHANNEL_ID = "media2_playback_channel"
private const val NOTIFICATION_ID = 1002
private const val MEDIA_ROOT_ID = "media_root_id"
var pluginEventTrigger: ((String, JSObject) -> Unit)? = null
var currentTitle: String = "Read Aloud"
var currentArtist: String = "Reading your content"
var currentArtwork: Bitmap? = null
}
override fun onCreate() {
super.onCreate()
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val result = audioManager.requestAudioFocus(
afChangeListener,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d("MediaPlaybackService", "Audio focus granted")
} else {
Log.w("MediaPlaybackService", "Failed to gain audio focus")
}
player = ExoPlayer.Builder(this).build()
mediaSession = MediaSessionCompat(baseContext, "ReadestMediaSession").apply {
stateBuilder = PlaybackStateCompat.Builder().setActions(
PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PLAY_PAUSE or
PlaybackStateCompat.ACTION_PAUSE or
PlaybackStateCompat.ACTION_STOP or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
setPlaybackState(stateBuilder.build())
setCallback(SessionCallback())
setSessionToken(sessionToken)
isActive = true
}
player.addListener(object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
updatePlaybackState()
}
override fun onPlaybackStateChanged(playbackState: Int) {
updatePlaybackState()
}
})
val mediaItem = MediaItem.fromUri("asset:///silence.mp3")
player.setMediaItem(mediaItem)
player.repeatMode = Player.REPEAT_MODE_ONE
player.prepare()
player.playWhenReady = true
showNotification(PlaybackStateCompat.STATE_PLAYING)
}
private inner class SessionCallback : MediaSessionCompat.Callback() {
override fun onPlay() {
player.play()
pluginEventTrigger?.invoke("media-session-play", JSObject())
updatePlaybackState()
}
override fun onPause() {
player.pause()
pluginEventTrigger?.invoke("media-session-pause", JSObject())
updatePlaybackState()
}
override fun onSkipToNext() {
player.seekTo(0)
pluginEventTrigger?.invoke("media-session-next", JSObject())
}
override fun onSkipToPrevious() {
player.seekTo(0)
pluginEventTrigger?.invoke("media-session-previous", JSObject())
}
}
private fun updatePlaybackState() {
val state = if (player.isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED
mediaSession?.setPlaybackState(
stateBuilder.setState(state, player.currentPosition, 1f).build()
)
showNotification(state)
}
private fun showNotification(playbackState: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(CHANNEL_ID, "Media Controls", NotificationManager.IMPORTANCE_LOW)
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
startForeground(NOTIFICATION_ID, buildNotification(playbackState))
}
private fun buildNotification(playbackState: Int): Notification {
val builder = NotificationCompat.Builder(this, CHANNEL_ID).apply {
setContentTitle(currentTitle)
setContentText(currentArtist)
setLargeIcon(currentArtwork)
setContentIntent(mediaSession!!.controller.sessionActivity)
setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(this@MediaPlaybackService, PlaybackStateCompat.ACTION_STOP))
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
setSmallIcon(R.drawable.notification_icon)
addAction(
android.R.drawable.ic_media_previous,
"Previous",
MediaButtonReceiver.buildMediaButtonPendingIntent(
this@MediaPlaybackService,
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
)
)
if (playbackState == PlaybackStateCompat.STATE_PLAYING) {
addAction(
android.R.drawable.ic_media_pause,
"Pause",
MediaButtonReceiver.buildMediaButtonPendingIntent(
this@MediaPlaybackService,
PlaybackStateCompat.ACTION_PAUSE
)
)
} else {
addAction(
android.R.drawable.ic_media_play,
"Play",
MediaButtonReceiver.buildMediaButtonPendingIntent(
this@MediaPlaybackService,
PlaybackStateCompat.ACTION_PLAY
)
)
}
addAction(
android.R.drawable.ic_media_next,
"Next",
MediaButtonReceiver.buildMediaButtonPendingIntent(
this@MediaPlaybackService,
PlaybackStateCompat.ACTION_SKIP_TO_NEXT
)
)
setStyle(
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession?.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
)
}
return builder.build()
}
override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? {
return BrowserRoot(MEDIA_ROOT_ID, null)
}
override fun onLoadChildren(parentId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
result.sendResult(null)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
MediaButtonReceiver.handleIntent(mediaSession, intent)
if (intent?.action == "UPDATE_METADATA") {
currentTitle = intent.getStringExtra("title") ?: currentTitle
currentArtist = intent.getStringExtra("artist") ?: currentArtist
val newArtwork = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra("artwork", Bitmap::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra("artwork")
}
if (newArtwork != null) {
currentArtwork = newArtwork
}
val metadataBuilder = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, currentTitle)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentArtist)
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, currentArtwork)
mediaSession?.setMetadata(metadataBuilder.build())
showNotification(if (player.isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED)
} else if (intent?.action == "UPDATE_PLAYBACK_STATE") {
val isPlaying = intent.getBooleanExtra("playing", false)
val position = intent.getLongExtra("position", 0L) // in milliseconds
val duration = intent.getLongExtra("duration", 0L) // in milliseconds
if (isPlaying && !player.isPlaying) {
player.play()
} else if (!isPlaying && player.isPlaying) {
player.pause()
}
player.seekTo(position)
val state = if (isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED
mediaSession?.setPlaybackState(
stateBuilder.setState(state, position, 1f).build()
)
showNotification(state)
}
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
player.release()
mediaSession?.release()
}
}
@@ -11,13 +11,7 @@ import android.speech.tts.Voice
import android.util.Log
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.ForwardingPlayer
import androidx.media3.session.MediaSession
import androidx.media3.exoplayer.ExoPlayer
import androidx.core.content.ContextCompat
import app.tauri.annotation.Command
import app.tauri.annotation.InvokeArg
import app.tauri.annotation.Permission
@@ -92,8 +86,8 @@ class UpdateMediaSessionMetadataArgs {
@InvokeArg
class UpdateMediaSessionStateArgs {
var playing: Boolean? = null
var position: Double? = null
var duration: Double? = null
var position: Int? = null // in milliseconds
var duration: Int? = null // in milliseconds
}
@InvokeArg
@@ -106,78 +100,6 @@ class SetMediaSessionActiveArgs {
var foregroundServiceText: String? = null
}
class MediaForegroundService : Service() {
companion object {
const val NOTIFICATION_ID = 1001
const val CHANNEL_ID = "tts_media_playback_channel"
const val ACTION_START = "START_FOREGROUND_SERVICE"
const val ACTION_STOP = "STOP_FOREGROUND_SERVICE"
var pluginInstance: NativeTTSPlugin? = null
}
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> {
startForegroundService()
}
ACTION_STOP -> {
stopForegroundService()
}
}
return START_STICKY // Restart if killed
}
override fun onBind(intent: Intent?): IBinder? = null
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
NativeTTSPlugin.NOTIFICATION_TITLE,
NotificationManager.IMPORTANCE_MIN
).apply {
description = NativeTTSPlugin.NOTIFICATION_TEXT
setShowBadge(false)
setSound(null, null)
}
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
}
}
private fun startForegroundService() {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(NativeTTSPlugin.FOREGROUND_SERVICE_TITLE)
.setContentText(NativeTTSPlugin.FOREGROUND_SERVICE_TEXT)
.setSmallIcon(android.R.drawable.ic_media_play)
.setOngoing(true)
.setSilent(true)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.build()
startForeground(NOTIFICATION_ID, notification)
Log.d("MediaForegroundService", "Foreground service started")
}
private fun stopForegroundService() {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
Log.d("MediaForegroundService", "Foreground service stopped")
}
override fun onDestroy() {
super.onDestroy()
Log.d("MediaForegroundService", "Service destroyed")
}
}
@TauriPlugin(
permissions = [
Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "postNotification")
@@ -203,18 +125,8 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
private val eventChannels = ConcurrentHashMap<String, Channel<TTSMessageEvent>>()
private val speakingJobs = ConcurrentHashMap<String, Job>()
private var player: Player? = null
private var mediaSession: MediaSession? = null
private var isMediaSessionActive = false
private var isForegroundServiceRunning = false
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
init {
MediaForegroundService.pluginInstance = this
}
@Command
fun init(invoke: Invoke) {
coroutineScope.launch {
@@ -509,95 +421,6 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
}
}
@UnstableApi
private class WebViewPlayer(player: Player) : ForwardingPlayer(player) {
var eventTrigger: ((String, JSObject) -> Unit)? = null
override fun play() {
eventTrigger?.invoke("media-session-play", JSObject())
if (!this.playWhenReady) {
this.playWhenReady = true
} else {
this.playWhenReady = false
}
}
override fun pause() {
eventTrigger?.invoke("media-session-pause", JSObject())
if (!this.playWhenReady) {
this.playWhenReady = true
} else {
this.playWhenReady = false
}
}
override fun seekToNext() {
eventTrigger?.invoke("media-session-next", JSObject())
}
override fun seekToPrevious() {
eventTrigger?.invoke("media-session-previous", JSObject())
}
}
private fun initializeMediaSession() {
var basePlayer = ExoPlayer.Builder(activity).build()
val webViewPlayer = WebViewPlayer(basePlayer)
webViewPlayer.eventTrigger = { event, data -> trigger(event, data) }
player = webViewPlayer
mediaSession = MediaSession.Builder(activity, player!!)
.setCallback(object : MediaSession.Callback {
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
val builder = MediaSession.ConnectionResult.AcceptedResultBuilder(session)
builder.setAvailablePlayerCommands(
Player.Commands.Builder()
.add(Player.COMMAND_PLAY_PAUSE)
.add(Player.COMMAND_SEEK_TO_NEXT)
.add(Player.COMMAND_SEEK_TO_PREVIOUS)
.build()
)
return builder.build()
}
})
.build()
}
private fun deinitializeMediaSession() {
mediaSession?.release()
mediaSession = null
player?.release()
player = null
}
private fun startForegroundService() {
try {
val intent = Intent(activity, MediaForegroundService::class.java).apply {
action = MediaForegroundService.ACTION_START
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(intent)
} else {
activity.startService(intent)
}
Log.d(TAG, "Foreground service started")
} catch (e: Exception) {
Log.e(TAG, "Failed to start foreground service", e)
}
}
private fun stopForegroundService() {
try {
val intent = Intent(activity, MediaForegroundService::class.java).apply {
action = MediaForegroundService.ACTION_STOP
}
activity.stopService(intent)
Log.d(TAG, "Foreground service stopped")
} catch (e: Exception) {
Log.e(TAG, "Failed to stop foreground service", e)
}
}
private suspend fun loadArtworkFromUrl(urlString: String): Bitmap? {
return withContext(Dispatchers.IO) {
try {
@@ -624,50 +447,26 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
}
}
private var currentArtworkByteArray: ByteArray? = null
@Command
fun update_media_session_metadata(invoke: Invoke) {
val args = invoke.parseArgs(UpdateMediaSessionMetadataArgs::class.java)
val title = args.title ?: ""
val artist = args.artist ?: ""
val album = args.album ?: ""
val artworkUrl = args.artwork ?: ""
coroutineScope.launch {
try {
val metadataBuilder = MediaMetadata.Builder()
.setTitle(title)
.setArtist(artist)
.setAlbumTitle(album)
.apply {
if (artworkUrl.isNotEmpty()) {
loadArtworkFromUrl(artworkUrl)?.let { bitmap ->
java.io.ByteArrayOutputStream().use { stream ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
currentArtworkByteArray = stream.toByteArray()
}
}
}
currentArtworkByteArray?.let {
setArtworkData(it, MediaMetadata.PICTURE_TYPE_FRONT_COVER)
}
}
val mediaItem: MediaItem = MediaItem.Builder()
.setMediaId("silent_media")
.setUri("data:audio/wav;base64,UklGRigAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQQAAAAAAA==")
.setMediaMetadata(metadataBuilder.build())
.build()
player?.setMediaItems(listOf(mediaItem, mediaItem))
player?.prepare()
player?.playWhenReady = true
player?.setRepeatMode(Player.REPEAT_MODE_ALL)
val artworkBitmap = args.artwork?.let { loadArtworkFromUrl(it) }
val intent = Intent(activity, MediaPlaybackService::class.java).apply {
action = "UPDATE_METADATA"
putExtra("title", title)
putExtra("artist", artist)
putExtra("album", album)
putExtra("artwork", artworkBitmap)
}
activity.startService(intent)
invoke.resolve()
} catch (e: Exception) {
Log.e("NativeBridgePlugin", "Failed to update metadata: ${e.message}")
invoke.reject("Failed to update metadata: ${e.message}")
}
}
@@ -677,15 +476,17 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
fun update_media_session_state(invoke: Invoke) {
var args = invoke.parseArgs(UpdateMediaSessionStateArgs::class.java)
val isPlaying = args.playing ?: false
val position = args.position ?: 0.0
val position = args.position ?: 0
val duration = args.duration ?: 0
try {
player?.let { player ->
if (position > 0) {
player.seekTo((position * 1000).toLong())
}
player.playWhenReady = isPlaying
val intent = Intent(activity, MediaPlaybackService::class.java).apply {
action = "UPDATE_PLAYBACK_STATE"
putExtra("playing", isPlaying)
putExtra("position", position)
putExtra("duration", duration)
}
activity.startService(intent)
invoke.resolve()
} catch (e: Exception) {
invoke.reject("Failed to update playback state: ${e.message}")
@@ -696,7 +497,6 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
fun set_media_session_active(invoke: Invoke) {
var args = invoke.parseArgs(SetMediaSessionActiveArgs::class.java)
val active = args.active ?: true
val keepAppInForeground = args.keepAppInForeground ?: false
args.notificationTitle?.let { NOTIFICATION_TITLE = it }
args.notificationText?.let { NOTIFICATION_TEXT = it }
@@ -704,41 +504,26 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
args.foregroundServiceText?.let { FOREGROUND_SERVICE_TEXT = it }
try {
if (active && !isMediaSessionActive) {
initializeMediaSession()
if (keepAppInForeground) {
startForegroundService()
isForegroundServiceRunning = true
}
isMediaSessionActive = true
Log.d(TAG, "Media session activated with foreground service")
} else if (!active && isMediaSessionActive) {
deinitializeMediaSession()
if (isForegroundServiceRunning) {
stopForegroundService()
isForegroundServiceRunning = false
}
isMediaSessionActive = false
Log.d(TAG, "Media session deactivated and foreground service stopped")
val intent = Intent(activity, MediaPlaybackService::class.java)
if (active) {
MediaPlaybackService.pluginEventTrigger = { event, data -> trigger(event, data) }
MediaPlaybackService.currentTitle = FOREGROUND_SERVICE_TITLE
MediaPlaybackService.currentArtist = FOREGROUND_SERVICE_TEXT
ContextCompat.startForegroundService(activity, intent)
} else {
activity.stopService(intent)
MediaPlaybackService.pluginEventTrigger = null
}
invoke.resolve()
} catch (e: Exception) {
Log.e(TAG, "Failed to set media session active state: ${e.message}")
invoke.reject("Failed to set media session active state: ${e.message}")
}
}
fun destroy() {
try {
if (isMediaSessionActive) {
if (isForegroundServiceRunning) {
stopForegroundService()
isForegroundServiceRunning = false
}
isMediaSessionActive = false
}
deinitializeMediaSession()
val intent = Intent(activity, MediaPlaybackService::class.java)
activity.stopService(intent)
coroutineScope.cancel()
textToSpeech?.shutdown()
@@ -747,8 +532,6 @@ class NativeTTSPlugin(private val activity: Activity) : Plugin(activity) {
speakingJobs.values.forEach { it.cancel() }
speakingJobs.clear()
MediaForegroundService.pluginInstance = null
Log.d(TAG, "Plugin destroyed successfully")
} catch (e: Exception) {
Log.e(TAG, "Error during plugin destruction", e)
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

@@ -11,10 +11,12 @@ const COMMANDS: &[&str] = &[
"set_media_session_active",
"update_media_session_state",
"update_media_session_metadata",
"register_listener",
"remove_listener",
"check_permissions",
"request_permissions",
"checkPermissions",
"requestPermissions",
"registerListener",
"remove_listener",
];
fn main() {
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-check-permissions"
description = "Enables the check_permissions command without any pre-configured scope."
commands.allow = ["check_permissions"]
[[permission]]
identifier = "deny-check-permissions"
description = "Denies the check_permissions command without any pre-configured scope."
commands.deny = ["check_permissions"]
@@ -1,13 +0,0 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-registerListener"
description = "Enables the registerListener command without any pre-configured scope."
commands.allow = ["registerListener"]
[[permission]]
identifier = "deny-registerListener"
description = "Denies the registerListener command without any pre-configured scope."
commands.deny = ["registerListener"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-register-listener"
description = "Enables the register_listener command without any pre-configured scope."
commands.allow = ["register_listener"]
[[permission]]
identifier = "deny-register-listener"
description = "Denies the register_listener command without any pre-configured scope."
commands.deny = ["register_listener"]
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-request-permissions"
description = "Enables the request_permissions command without any pre-configured scope."
commands.allow = ["request_permissions"]
[[permission]]
identifier = "deny-request-permissions"
description = "Denies the request_permissions command without any pre-configured scope."
commands.deny = ["request_permissions"]
@@ -16,10 +16,12 @@ Default permissions for the plugin
- `allow-set-media-session-active`
- `allow-update-media-session-state`
- `allow-update-media-session-metadata`
- `allow-register-listener`
- `allow-remove-listener`
- `allow-check-permissions`
- `allow-request-permissions`
- `allow-checkPermissions`
- `allow-requestPermissions`
- `allow-registerListener`
- `allow-remove-listener`
## Permission Table
@@ -59,6 +61,32 @@ Denies the checkPermissions command without any pre-configured scope.
<tr>
<td>
`native-tts:allow-check-permissions`
</td>
<td>
Enables the check_permissions command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-tts:deny-check-permissions`
</td>
<td>
Denies the check_permissions command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-tts:allow-get-all-voices`
</td>
@@ -137,12 +165,12 @@ Denies the pause command without any pre-configured scope.
<tr>
<td>
`native-tts:allow-registerListener`
`native-tts:allow-register-listener`
</td>
<td>
Enables the registerListener command without any pre-configured scope.
Enables the register_listener command without any pre-configured scope.
</td>
</tr>
@@ -150,12 +178,12 @@ Enables the registerListener command without any pre-configured scope.
<tr>
<td>
`native-tts:deny-registerListener`
`native-tts:deny-register-listener`
</td>
<td>
Denies the registerListener command without any pre-configured scope.
Denies the register_listener command without any pre-configured scope.
</td>
</tr>
@@ -215,6 +243,32 @@ Denies the requestPermissions command without any pre-configured scope.
<tr>
<td>
`native-tts:allow-request-permissions`
</td>
<td>
Enables the request_permissions command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-tts:deny-request-permissions`
</td>
<td>
Denies the request_permissions command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-tts:allow-resume`
</td>
@@ -13,8 +13,10 @@ permissions = [
"allow-set-media-session-active",
"allow-update-media-session-state",
"allow-update-media-session-metadata",
"allow-register-listener",
"allow-remove-listener",
"allow-check-permissions",
"allow-request-permissions",
"allow-checkPermissions",
"allow-requestPermissions",
"allow-registerListener",
"allow-remove-listener",
]
@@ -306,6 +306,18 @@
"const": "deny-checkPermissions",
"markdownDescription": "Denies the checkPermissions command without any pre-configured scope."
},
{
"description": "Enables the check_permissions command without any pre-configured scope.",
"type": "string",
"const": "allow-check-permissions",
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
},
{
"description": "Denies the check_permissions command without any pre-configured scope.",
"type": "string",
"const": "deny-check-permissions",
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
},
{
"description": "Enables the get_all_voices command without any pre-configured scope.",
"type": "string",
@@ -343,16 +355,16 @@
"markdownDescription": "Denies the pause command without any pre-configured scope."
},
{
"description": "Enables the registerListener command without any pre-configured scope.",
"description": "Enables the register_listener command without any pre-configured scope.",
"type": "string",
"const": "allow-registerListener",
"markdownDescription": "Enables the registerListener command without any pre-configured scope."
"const": "allow-register-listener",
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
},
{
"description": "Denies the registerListener command without any pre-configured scope.",
"description": "Denies the register_listener command without any pre-configured scope.",
"type": "string",
"const": "deny-registerListener",
"markdownDescription": "Denies the registerListener command without any pre-configured scope."
"const": "deny-register-listener",
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
},
{
"description": "Enables the remove_listener command without any pre-configured scope.",
@@ -378,6 +390,18 @@
"const": "deny-requestPermissions",
"markdownDescription": "Denies the requestPermissions command without any pre-configured scope."
},
{
"description": "Enables the request_permissions command without any pre-configured scope.",
"type": "string",
"const": "allow-request-permissions",
"markdownDescription": "Enables the request_permissions command without any pre-configured scope."
},
{
"description": "Denies the request_permissions command without any pre-configured scope.",
"type": "string",
"const": "deny-request-permissions",
"markdownDescription": "Denies the request_permissions command without any pre-configured scope."
},
{
"description": "Enables the resume command without any pre-configured scope.",
"type": "string",
@@ -487,10 +511,10 @@
"markdownDescription": "Denies the update_media_session_state command without any pre-configured scope."
},
{
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-get-all-voices`\n- `allow-set-media-session-active`\n- `allow-update-media-session-state`\n- `allow-update-media-session-metadata`\n- `allow-checkPermissions`\n- `allow-requestPermissions`\n- `allow-registerListener`\n- `allow-remove-listener`",
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-get-all-voices`\n- `allow-set-media-session-active`\n- `allow-update-media-session-state`\n- `allow-update-media-session-metadata`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
"type": "string",
"const": "default",
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-get-all-voices`\n- `allow-set-media-session-active`\n- `allow-update-media-session-state`\n- `allow-update-media-session-metadata`\n- `allow-checkPermissions`\n- `allow-requestPermissions`\n- `allow-registerListener`\n- `allow-remove-listener`"
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-init`\n- `allow-speak`\n- `allow-stop`\n- `allow-pause`\n- `allow-resume`\n- `allow-set-rate`\n- `allow-set-pitch`\n- `allow-set-voice`\n- `allow-get-all-voices`\n- `allow-set-media-session-active`\n- `allow-update-media-session-state`\n- `allow-update-media-session-metadata`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-check-permissions`\n- `allow-request-permissions`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
}
]
}
+52 -1
View File
@@ -21,7 +21,11 @@ use tauri_plugin_fs::FsExt;
mod macos;
mod transfer_file;
use tauri::{command, Emitter, WebviewUrl, WebviewWindowBuilder, Window};
#[cfg(target_os = "android")]
use tauri_plugin_native_bridge::{NativeBridgeExt, OpenExternalUrlRequest};
use tauri_plugin_oauth::start;
#[cfg(not(target_os = "android"))]
use tauri_plugin_opener::OpenerExt;
use transfer_file::{download_file, upload_file};
#[cfg(desktop)]
@@ -257,9 +261,56 @@ pub fn run() {
eprintln!("Failed to initialize tauri_plugin_log: {e}");
};
let app_handle = app.handle().clone();
let win_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
.background_throttling(BackgroundThrottlingPolicy::Disabled)
.background_color(tauri::window::Color(50, 49, 48, 255));
.background_color(tauri::window::Color(50, 49, 48, 255))
.initialization_script(
r#"
window.addEventListener('DOMContentLoaded', function() {
const isTauriLocal = window.location.protocol === 'tauri:' ||
window.location.hostname === 'tauri.localhost';
const needsSafeArea = !isTauriLocal;
if (needsSafeArea && !document.getElementById('safe-area-style')) {
const style = document.createElement('style');
style.id = 'safe-area-style';
style.textContent = `
body {
padding-top: env(safe-area-inset-top) !important;
padding-bottom: env(safe-area-inset-bottom) !important;
padding-left: env(safe-area-inset-left) !important;
padding-right: env(safe-area-inset-right) !important;
}
`;
document.head.appendChild(style);
}
});
"#,
)
.on_navigation(move |url| {
if url.scheme() == "alipays" || url.scheme() == "alipay" {
let url_str = url.as_str().to_string();
#[cfg(target_os = "android")]
{
let handle = app_handle.clone();
tauri::async_runtime::spawn(async move {
match handle
.native_bridge()
.open_external_url(OpenExternalUrlRequest { url: url_str })
{
Ok(result) => println!("Result: {:?}", result),
Err(e) => eprintln!("Error: {:?}", e),
}
});
}
#[cfg(not(target_os = "android"))]
{
let _ = app_handle.opener().open_url(url_str, None::<&str>);
}
return false;
}
true
});
#[cfg(desktop)]
let win_builder = win_builder.inner_size(800.0, 600.0).resizable(true);
+3 -3
View File
@@ -15,10 +15,10 @@
"security": {
"csp": {
"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 https://translate.googleapis.com https://translate.toil.cc https://*.microsofttranslator.com https://edge.microsoft.com https://*.googleusercontent.com",
"connect-src": "'self' blob: data: asset: http://asset.localhost ipc: http://ipc.localhost http://*:* https://*:* 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 https://translate.googleapis.com https://translate.toil.cc https://*.microsofttranslator.com https://edge.microsoft.com https://*.googleusercontent.com",
"img-src": "'self' blob: data: asset: http://asset.localhost https://*",
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://chinese-fonts-cdn.netlify.app",
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://chinese-fonts-cdn.netlify.app",
"style-src": "'self' 'unsafe-inline' blob: asset: http://asset.localhost https://cdn.jsdelivr.net https://fonts.googleapis.com https://chinese-fonts-cdn.netlify.app https://cdnjs.cloudflare.com",
"font-src": "'self' blob: data: asset: http://asset.localhost tauri: https://db.onlinewebfonts.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://chinese-fonts-cdn.netlify.app https://cdnjs.cloudflare.com",
"frame-src": "'self' blob: asset: http://asset.localhost https://*.stripe.com",
"script-src": "'self' 'unsafe-inline' 'unsafe-eval' blob: asset: http://asset.localhost https://*.sentry.io https://*.posthog.com https://*.stripe.com"
},
@@ -1,5 +1,6 @@
import { describe, it, expect, vi } from 'vitest';
import { POST } from '@/app/api/apple/iap-verify/route';
import { POST as applePost } from '@/app/api/apple/iap-verify/route';
import { POST as googlePost } from '@/app/api/google/iap-verify/route';
import { NextRequest } from 'next/server';
import { setupSupabaseMocks } from '../helpers/supabase-mock';
@@ -16,7 +17,7 @@ vi.mock('@/utils/supabase', () => ({
}));
describe.skipIf(SKIP_IAP_API_TESTS)('/api/apple/iap-verify', () => {
it('should verify a valid transaction', async () => {
it('should verify a valid Apple IAP transaction', async () => {
setupSupabaseMocks();
const request = new NextRequest('http://localhost:3000/api/apple/iap-verify', {
method: 'POST',
@@ -30,7 +31,34 @@ describe.skipIf(SKIP_IAP_API_TESTS)('/api/apple/iap-verify', () => {
}),
});
const response = await POST(request);
const response = await applePost(request);
const data = await response.json();
console.log('Response:', data);
expect(response.status).toBe(200);
expect(data.purchase).toBeDefined();
});
});
describe.skipIf(SKIP_IAP_API_TESTS)('/api/google/iap-verify', () => {
it('should verify a valid Google IAP purchase', async () => {
setupSupabaseMocks();
const request = new NextRequest('http://localhost:3000/api/google/iap-verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer test-token',
},
body: JSON.stringify({
packageName: 'com.bilingify.readest',
productId: 'com.bilingify.readest.monthly.pro',
orderId: 'GPA.3388-4630-1043-97604',
purchaseToken:
'bhedilellajejnodjfcanjai.AO-J1Ow94pzeLM6e8pCJLT_tV-RnffT3HKMTcstovMNVlUTOwhx38SU5Seq3EO5qiQ0Le_VQU1ShN7nxaFQILY3UPX2nhdAKLBbekC_MxBnRf-Bpgegh_NA',
}),
});
const response = await googlePost(request);
const data = await response.json();
console.log('Response:', data);
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { vi } from 'vitest';
export const setupSupabaseMocks = async (
@@ -9,10 +8,7 @@ export const setupSupabaseMocks = async (
update: null,
insert: null,
adminUpsert: null,
adminSelect: {
data: [],
error: null,
},
adminSelect: null,
adminSelectMany: null,
adminUpdate: null,
adminInsert: null,
@@ -1,10 +1,10 @@
import { describe, it, expect, beforeAll } from 'vitest';
import { AppleIAPVerifier, createAppleIAPVerifier } from '@/libs/iap/apple/verifier';
import { AppleIAPVerifier, createAppleIAPVerifier } from '@/libs/payment/iap/apple/verifier';
const SKIP_IAP_API_TESTS = !process.env['ENABLE_IAP_API_TESTS'];
const REAL_TEST_DATA = {
validSubscription: {
transactionId: '2000000969168810',
transactionId: '2000000976418990',
originalTransactionId: '2000000968585424',
productId: 'com.bilingify.readest.monthly.plus',
},
@@ -43,7 +43,7 @@ describe.skipIf(SKIP_IAP_API_TESTS)('Apple IAP Integration Tests', () => {
expect(result.success).toBe(true);
expect(result.verified).toBe(true);
expect(result.status).toBe('active');
expect(result.status).toBe('expired');
expect(result.transactionId).toBe(transactionId);
expect(result.originalTransactionId).toBe(originalTransactionId);
expect(result.bundleId).toBe(process.env['APPLE_IAP_BUNDLE_ID']);
@@ -1,104 +1,15 @@
import { z } from 'zod';
import { NextResponse } from 'next/server';
import { getAppleIAPVerifier } from '@/libs/iap/apple/verifier';
import { createSupabaseAdminClient } from '@/utils/supabase';
import { IAPError } from '@/libs/payment/iap/types';
import { validateUserAndToken } from '@/utils/access';
import { IAPError } from '@/types/error';
import { getAppleIAPVerifier } from '@/libs/payment/iap/apple/verifier';
import { processPurchaseData, VerifiedPurchase } from '@/libs/payment/iap/apple/server';
const iapVerificationSchema = z.object({
transactionId: z.string().min(1, 'Transaction ID is required'),
originalTransactionId: z.string().min(1, 'Original Transaction ID is required'),
});
const PRODUCT_MAP: Record<string, string> = {
'com.bilingify.readest.monthly.plus': 'Plus',
'com.bilingify.readest.yearly.plus': 'Plus',
'com.bilingify.readest.monthly.pro': 'Pro',
'com.bilingify.readest.yearly.pro': 'Pro',
};
const getProductName = (productId: string) => {
return PRODUCT_MAP[productId] || productId;
};
const getProductPlan = (productId: string) => {
if (productId.includes('plus')) {
return 'plus';
} else if (productId.includes('pro')) {
return 'pro';
}
return 'free';
};
interface Purchase {
status: string;
customerEmail: string;
subscriptionId: string;
planName: string;
productId: string;
platform: string;
transactionId: string;
originalTransactionId: string;
purchaseDate?: string;
expiresDate?: string | null;
quantity: number;
environment: string;
bundleId: string;
webOrderLineItemId?: string;
subscriptionGroupIdentifier?: string;
type?: string;
revocationDate?: string | null;
revocationReason?: number | null;
}
async function updateUserSubscription(userId: string, purchase: Purchase) {
try {
const supabase = createSupabaseAdminClient();
const { data, error } = await supabase.from('apple_iap_subscriptions').upsert(
{
user_id: userId,
platform: purchase.platform,
product_id: purchase.productId,
transaction_id: purchase.transactionId,
original_transaction_id: purchase.originalTransactionId,
status: purchase.status === 'active' ? 'active' : 'expired',
purchase_date: purchase.purchaseDate,
expires_date: purchase.expiresDate,
environment: purchase.environment,
bundle_id: purchase.bundleId,
quantity: purchase.quantity || 1,
auto_renew_status: true,
web_order_line_item_id: purchase.webOrderLineItemId,
subscription_group_identifier: purchase.subscriptionGroupIdentifier,
created_at: new Date(),
updated_at: new Date(),
},
{
onConflict: 'user_id,original_transaction_id',
},
);
if (error) {
console.error('Database update error:', error);
throw new Error(`Database update failed: ${error.message}`);
}
const plan = await getProductPlan(purchase.productId);
await supabase
.from('plans')
.update({
plan: ['active', 'trialing'].includes(purchase.status) ? plan : 'free',
status: purchase.status,
})
.eq('id', userId);
return data;
} catch (error) {
console.error('Failed to update user subscription:', error);
throw error;
}
}
export async function POST(request: Request) {
const body = await request.json();
let validatedInput;
@@ -115,7 +26,7 @@ export async function POST(request: Request) {
);
}
}
const { transactionId, originalTransactionId } = validatedInput!;
const { originalTransactionId } = validatedInput!;
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
if (!user || !token) {
@@ -123,59 +34,13 @@ export async function POST(request: Request) {
}
try {
const supabase = createSupabaseAdminClient();
const { data: existingSubscription } = await supabase
.from('apple_iap_subscriptions')
.select('*')
.eq('original_transaction_id', originalTransactionId)
.single();
console.log('Existing subscription:', existingSubscription);
// Should not restore purchase for another account
if (existingSubscription && existingSubscription.user_id !== user.id) {
return NextResponse.json(
{ error: IAPError.TRANSACTION_BELONGS_TO_ANOTHER_USER },
{ status: 403 },
);
}
if (
existingSubscription &&
existingSubscription.transactionId === transactionId &&
existingSubscription.status === 'active'
) {
console.log('Transaction already verified and active');
const purchase = {
status: existingSubscription.status,
customerEmail: user.email,
subscriptionId:
existingSubscription.web_order_line_item_id ||
existingSubscription.original_transaction_id,
planName: getProductName(existingSubscription.product_id),
productId: existingSubscription.product_id,
platform: existingSubscription.platform,
transactionId: existingSubscription.transaction_id,
originalTransactionId: existingSubscription.original_transaction_id,
purchaseDate: existingSubscription.purchase_date,
expiresDate: existingSubscription.expires_date,
quantity: existingSubscription.quantity,
environment: existingSubscription.environment,
bundleId: existingSubscription.bundle_id,
};
return NextResponse.json({
purchase,
error: null,
});
}
const defaultIAPVerifier = getAppleIAPVerifier();
const verificationResult = await defaultIAPVerifier.verifyTransaction(originalTransactionId);
if (!verificationResult.success) {
console.error('Apple verification failed:', verificationResult.error);
return NextResponse.json(
{
error: verificationResult.error || IAPError.UNKNOWN_ERROR,
error: verificationResult.error || IAPError.TRANSACTION_CANNOT_BE_VERIFIED,
purchase: null,
},
{ status: 400 },
@@ -188,36 +53,15 @@ export async function POST(request: Request) {
productId: transaction.productId,
environment: transaction.environment,
});
if (transaction.environment === 'Sandbox' && process.env.NODE_ENV === 'production') {
console.warn('Sandbox transaction in production environment');
}
const purchase = {
status: verificationResult.status!,
customerEmail: user.email!,
subscriptionId: transaction.webOrderLineItemId || transaction.originalTransactionId,
planName: getProductName(transaction.productId),
productId: transaction.productId,
platform: 'ios',
transactionId: transaction.transactionId,
originalTransactionId: transaction.originalTransactionId,
purchaseDate: verificationResult.purchaseDate?.toISOString(),
expiresDate: verificationResult.expiresDate?.toISOString() || null,
quantity: transaction.quantity,
environment: transaction.environment.toLowerCase(),
bundleId: transaction.bundleId,
webOrderLineItemId: transaction.webOrderLineItemId,
subscriptionGroupIdentifier: transaction.subscriptionGroupIdentifier,
type: transaction.type,
revocationDate: verificationResult.revocationDate?.toISOString() || null,
revocationReason: verificationResult.revocationReason,
};
try {
await updateUserSubscription(user.id, purchase);
const purchase: VerifiedPurchase = await processPurchaseData(user, verificationResult);
return NextResponse.json({
purchase,
error: null,
});
} catch (dbError) {
console.error('Database update failed:', dbError);
return NextResponse.json(
{
error: IAPError.TRANSACTION_SERVICE_UNAVAILABLE,
@@ -226,11 +70,6 @@ export async function POST(request: Request) {
{ status: 500 },
);
}
return NextResponse.json({
purchase,
error: null,
});
} catch (error) {
console.error('IAP verification error:', error);
return NextResponse.json(
@@ -0,0 +1,92 @@
import { z } from 'zod';
import { NextResponse } from 'next/server';
import { validateUserAndToken } from '@/utils/access';
import { getGoogleIAPVerifier, VerifyPurchaseParams } from '@/libs/payment/iap/google/verifier';
import { processPurchaseData, VerifiedPurchase } from '@/libs/payment/iap/google/server';
import { IAPError } from '@/libs/payment/iap/types';
const iapVerificationSchema = z.object({
packageName: z.string().min(1, 'Package name is required'),
productId: z.string().min(1, 'Product ID is required'),
orderId: z.string().min(1, 'Order ID is required'),
purchaseToken: z.string().min(1, 'Purchase token is required'),
});
export async function POST(request: Request) {
const body = await request.json();
let validatedInput;
try {
validatedInput = iapVerificationSchema.parse(body);
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: 'Invalid input data',
purchase: null,
},
{ status: 400 },
);
}
}
const { orderId, purchaseToken, productId, packageName } = validatedInput!;
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
if (!user || !token) {
return NextResponse.json({ error: IAPError.NOT_AUTHENTICATED }, { status: 403 });
}
try {
const googleIAPVerifier = getGoogleIAPVerifier();
const verifyParams: VerifyPurchaseParams = { orderId, purchaseToken, productId, packageName };
const verificationResult = await googleIAPVerifier.verifyPurchase(verifyParams);
if (!verificationResult.success) {
console.error('Google verification failed:', verificationResult.error);
return NextResponse.json(
{
error: verificationResult.error || IAPError.TRANSACTION_CANNOT_BE_VERIFIED,
purchase: null,
},
{ status: 400 },
);
}
const purchaseData = verificationResult.purchaseData!;
console.log('Google verification successful:', {
orderId: purchaseData.orderId,
productId: productId,
purchaseState: purchaseData.purchaseState,
});
let purchase: VerifiedPurchase;
try {
purchase = await processPurchaseData(user, verifyParams, verificationResult);
if (verificationResult.purchaseData?.acknowledgementState === 0) {
try {
await googleIAPVerifier.acknowledgePurchase(verifyParams);
purchase.acknowledgementState = 1;
} catch (ackError) {
console.error('Failed to acknowledge purchase:', ackError);
}
}
return NextResponse.json({
purchase,
error: null,
});
} catch (dbError) {
console.error('Database update failed:', dbError);
return NextResponse.json(
{
error: IAPError.TRANSACTION_SERVICE_UNAVAILABLE,
purchase: null,
},
{ status: 500 },
);
}
} catch (error) {
console.error('IAP verification error:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : IAPError.UNKNOWN_ERROR },
{ status: 500 },
);
}
}
@@ -1,7 +1,10 @@
import Stripe from 'stripe';
import { NextResponse } from 'next/server';
import { getStripe } from '@/libs/stripe/server';
import { createOrUpdateSubscription } from '@/utils/stripe';
import {
getStripe,
createOrUpdatePayment,
createOrUpdateSubscription,
} from '@/libs/payment/stripe/server';
import { validateUserAndToken } from '@/utils/access';
export async function POST(request: Request) {
@@ -16,10 +19,11 @@ export async function POST(request: Request) {
const stripe = getStripe();
const session = await stripe.checkout.sessions.retrieve(sessionId);
if (session.payment_status === 'paid') {
const customerId = session.customer as string;
const subscriptionId = session.subscription as string;
await createOrUpdateSubscription(user.id, customerId, subscriptionId);
const customerId = session.customer as string;
if (session.payment_status === 'paid' && session.subscription) {
await createOrUpdateSubscription(user.id, customerId, session.subscription as string);
} else if (session.payment_status === 'paid' && session.payment_intent) {
await createOrUpdatePayment(user.id, customerId, sessionId);
}
return NextResponse.json({ session });
@@ -1,10 +1,15 @@
import { NextRequest, NextResponse } from 'next/server';
import { getStripe } from '@/libs/stripe/server';
import { getStripe } from '@/libs/payment/stripe/server';
import { validateUserAndToken } from '@/utils/access';
import { createSupabaseAdminClient } from '@/utils/supabase';
export async function POST(request: NextRequest) {
const { priceId, embedded = true, metadata = {} } = await request.json();
const {
priceId,
planType = 'subscription',
embedded = true,
metadata = {},
} = await request.json();
const { user, token } = await validateUserAndToken(request.headers.get('authorization'));
if (!user || !token) {
@@ -43,13 +48,12 @@ export async function POST(request: NextRequest) {
}
const stripe = getStripe();
const successUrl = `${request.headers.get('origin')}/user/subscription/success?session_id={CHECKOUT_SESSION_ID}`;
const successUrl = `${request.headers.get('origin')}/user/subscription/success?payment=stripe&session_id={CHECKOUT_SESSION_ID}`;
const returnUrl = `${request.headers.get('origin')}/user`;
const session = await stripe.checkout.sessions.create({
ui_mode: embedded ? 'embedded' : 'hosted',
customer: customerId,
mode: 'subscription',
payment_method_types: ['card'],
mode: planType === 'subscription' ? 'subscription' : 'payment',
line_items: [
{
price: priceId,
@@ -1,7 +1,7 @@
import Stripe from 'stripe';
import { NextResponse } from 'next/server';
import { getStripe } from '@/libs/stripe/server';
import { UserPlan } from '@/types/user';
import { getStripe } from '@/libs/payment/stripe/server';
import { StripeProductMetadata } from '@/types/payment';
export async function GET() {
try {
@@ -9,7 +9,6 @@ export async function GET() {
const prices = await stripe.prices.list({
expand: ['data.product'],
active: true,
type: 'recurring',
});
const plans = prices.data
@@ -19,16 +18,18 @@ export async function GET() {
})
.map((price) => {
const product = price.product as Stripe.Product & {
metadata: { plan: UserPlan };
metadata: StripeProductMetadata;
};
return {
plan: product.metadata.plan,
price_id: price.id,
productId: price.id,
price: price.unit_amount,
currency: price.currency,
interval: price.recurring?.interval,
product: price.product,
productName: product.name,
metadata: product.metadata,
price_id: price.id, // deprecated
};
});
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { getStripe } from '@/libs/stripe/server';
import { getStripe } from '@/libs/payment/stripe/server';
import { validateUserAndToken } from '@/utils/access';
import { createSupabaseAdminClient } from '@/utils/supabase';
@@ -1,7 +1,10 @@
import Stripe from 'stripe';
import { NextRequest, NextResponse } from 'next/server';
import { getStripe } from '@/libs/stripe/server';
import { createOrUpdateSubscription } from '@/utils/stripe';
import {
getStripe,
createOrUpdateSubscription,
createOrUpdatePayment,
} from '@/libs/payment/stripe/server';
import { createSupabaseAdminClient } from '@/utils/supabase';
export async function POST(request: NextRequest) {
@@ -72,17 +75,9 @@ export async function POST(request: NextRequest) {
}
async function handleSuccessfulPayment(session: Stripe.Checkout.Session, userId: string) {
const supabase = createSupabaseAdminClient();
await supabase.from('payments').insert({
user_id: userId,
stripe_checkout_id: session.id,
amount: session.amount_total,
currency: session.currency,
status: 'completed',
payment_intent: session.payment_intent,
payment_method: session.payment_method_types?.[0] || 'unknown',
created_at: new Date().toISOString(),
});
const customerId = session.customer as string;
await createOrUpdatePayment(userId, customerId, session.id);
}
async function handleSuccessfulSubscription(session: Stripe.Checkout.Session, userId: string) {
+2 -2
View File
@@ -24,7 +24,7 @@ import { start, cancel, onUrl, onInvalidUrl } from '@fabianlars/tauri-plugin-oau
import { openUrl } from '@tauri-apps/plugin-opener';
import { invoke } from '@tauri-apps/api/core';
import { handleAuthCallback } from '@/helpers/auth';
import { getUserPlan } from '@/utils/access';
import { getUserProfilePlan } from '@/utils/access';
import { getAppleIdAuth, Scope } from './utils/appleIdAuth';
import { authWithCustomTab, authWithSafari } from './utils/nativeAuth';
import WindowButtons from '@/components/WindowButtons';
@@ -173,7 +173,7 @@ export default function AuthPage() {
const type = params.get('type');
if (accessToken) {
let next = params.get('next') ?? '/';
if (getUserPlan(accessToken) === 'free') {
if (getUserProfilePlan(accessToken) === 'free') {
next = '/user';
}
handleAuthCallback({ accessToken, refreshToken, type, next, login, navigate: router.push });
@@ -56,6 +56,31 @@ export default function ResetPasswordPage() {
providers={[]}
localization={getAuthLocalization()}
/>
<button
onClick={() => router.back()}
className={`mt-6 flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2.5 text-sm transition-colors ${
isDarkMode
? 'border-gray-600 text-gray-300 hover:bg-gray-800'
: 'border-gray-300 text-gray-700 hover:bg-gray-100'
}`}
>
<svg
xmlns='http://www.w3.org/2000/svg'
className='h-4 w-4'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M15 19l-7-7 7-7'
/>
</svg>
{_('Back')}
</button>
</div>
</div>
);
@@ -0,0 +1,123 @@
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/context/AuthContext';
import { useThemeStore } from '@/store/themeStore';
import { useTranslation } from '@/hooks/useTranslation';
import { supabase } from '@/utils/supabase';
export default function UpdateEmailPage() {
const _ = useTranslation();
const router = useRouter();
const { user } = useAuth();
const { isDarkMode } = useThemeStore();
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');
useEffect(() => {
if (!user) {
router.push('/login');
}
}, [user, router]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setMessage('');
setError('');
try {
const { error: updateError } = await supabase.auth.updateUser({
email: email,
});
if (updateError) throw updateError;
setMessage(
_(
'Confirmation email sent! Please check your old and new email addresses to confirm the change.',
),
);
setEmail('');
} catch (err: any) {
setError(err.message || _('Failed to update email'));
} finally {
setLoading(false);
}
};
return (
<div className='flex min-h-screen items-center justify-center'>
<div className='w-full max-w-md p-8'>
<div className={`rounded-md p-8`}>
<form onSubmit={handleSubmit} className='space-y-6'>
<div className='space-y-1'>
<label
htmlFor='email'
className={`block text-sm font-normal ${isDarkMode ? 'text-gray-300' : 'text-gray-400'}`}
>
{_('New Email')}
</label>
<input
id='email'
type='email'
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={_('Your new email')}
required
disabled={loading}
className={`w-full rounded-md border bg-transparent px-4 py-2.5 focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 ${isDarkMode ? 'text-gray-300' : 'text-gray-400'}`}
/>
</div>
{error && <div className={`text-sm text-red-500`}>{error}</div>}
{message && <div className={`text-base-content text-sm`}>{message}</div>}
<button
type='submit'
disabled={loading || !email}
className={`w-full rounded-md bg-green-400 px-4 py-2.5 font-medium text-white transition-colors hover:bg-green-500 disabled:cursor-not-allowed`}
>
{loading ? _('Updating email ...') : _('Update email')}
</button>
<button
onClick={() => router.back()}
className={`flex w-full items-center justify-center gap-2 rounded-md border px-4 py-2.5 text-sm transition-colors ${
isDarkMode
? 'border-gray-600 text-gray-300 hover:bg-gray-800'
: 'border-gray-300 text-gray-700 hover:bg-gray-100'
}`}
>
<svg
xmlns='http://www.w3.org/2000/svg'
className='h-4 w-4'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M15 19l-7-7 7-7'
/>
</svg>
{_('Back')}
</button>
</form>
{user?.email && (
<div className={`mt-6 text-center text-sm text-gray-300`}>
{_('Current email')}: {user.email}
</div>
)}
</div>
</div>
</div>
);
}

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