Compare commits

..

44 Commits

Author SHA1 Message Date
Huang Xin 519e222883 release(hotfix): version 0.9.81 (#2082) 2025-09-19 13:20:48 +02:00
Huang Xin 488a242787 css: default to lower font-family specificity than the styles defined inside the ebook, closes #2070 (#2081) 2025-09-19 13:10:37 +02:00
StepanSad 523a03d212 Update translation.json (#2078) 2025-09-19 12:04:22 +02:00
Huang Xin d49f3442e8 fix(layout): scroll in the dropdown menu in landscape mode, closes #2072 (#2076) 2025-09-19 11:50:03 +02:00
Huang Xin 48dc570809 fix: handle invalid bookkey for viewState, closes #2073 (#2074) 2025-09-19 11:09:32 +02:00
Huang Xin 509320813f tts: support media session in desktop apps (#2071) 2025-09-19 10:22:39 +02:00
Huang Xin 54d7e73acb fix: disable swipe up to toggle action bar for PDF zoomed in (#2068) 2025-09-18 19:46:47 +02:00
Huang Xin 1bc0eb3a4b release: version 0.9.80 (#2067) 2025-09-18 18:45:12 +02:00
Huang Xin c0bf2d40dd fix: jump to current item in virtualized TOC list, closes #2055 (#2066) 2025-09-18 18:38:09 +02:00
Huang Xin e30a39f9cb fix(config): prevent occasional errors when saving progress and configs (#2065) 2025-09-18 16:56:44 +02:00
Huang Xin 9727e4e9b8 fix(metadata): handle invalid language code and title when importing, closes #2056 and closes #2058 (#2064) 2025-09-18 16:47:02 +02:00
Huang Xin 3380912cdb feat(kosync): use metadata hash to aggregate different versions of the same book (#2063) 2025-09-18 14:51:26 +02:00
Huang Xin 91a5454b46 feat(sync): aggregate books with metadata hash for progress and annotation sync (#2062) 2025-09-18 14:05:16 +02:00
Huang Xin e0c5acf4c8 chore: check file status before deleting cloud files (#2053) 2025-09-17 04:54:32 +02:00
Huang Xin 7e062be0d0 doc: update accessibility support in README (#2051) 2025-09-16 18:06:10 +02:00
Huang Xin b23876c2b9 a11y: support NVDA screen reader on Windows and Orca screen reader on Linux (#2050) 2025-09-16 17:38:21 +02:00
Huang Xin e9a152683e fix(layout): show focus ring only for keyboard navigation (#2046) 2025-09-15 21:13:52 +02:00
Huang Xin 34f1af727f a11y: accessibility for mobile platforms (#2044) 2025-09-15 18:45:45 +02:00
Huang Xin dfc24f3803 fix(layout): fix break word for titles in book detail, closes #2039 (#2043) 2025-09-15 09:21:35 +02:00
Huang Xin ec233f7b37 fix(sync): resolve issue where invalid tokens were occasionally used with the storage API (#2041) 2025-09-14 22:08:22 +02:00
Huang Xin bb34b1ba59 a11y: add screen reader support (#2040)
Tested with VoiceOver on iOS and macOS
2025-09-14 22:00:42 +02:00
Daniil Leontev b1fb477359 fb2: respect elements nested in stanza (#2038)
Updates foliate-js submodule to include necessary changes. Fixes #2027
2025-09-14 17:03:53 +02:00
StepanSad 3c877a19fc Update translation.json (#2032) 2025-09-14 08:49:02 +08:00
Huang Xin b47baa6b74 fix: add delete confirmation for book deletion from contextmenu, closes #2013 (#2029) 2025-09-12 18:27:07 +02:00
Huang Xin 14c85cbadf feat(metadata): add an option to remove cover image, closes #2020 (#2028) 2025-09-12 17:10:25 +02:00
Huang Xin a88b9139f8 fix: occasionally failed double click detection on the header bar on Windows, closes #2015 (#2026) 2025-09-12 13:36:56 +02:00
Huang Xin 251a4b52c0 fix: support dragging window on touch screen, closes #2010 (#2025) 2025-09-12 11:43:49 +02:00
Huang Xin 1aaeaf5ebe feat: add support links in the about window (#2024) 2025-09-12 08:03:35 +02:00
Huang Xin 9fd152d727 refactor(a11y): add basic lint for accessibility (#2021) 2025-09-11 18:57:04 +02:00
Huang Xin 2571ceede4 fix: also apply zoom shortcuts to PDFs (#2016) 2025-09-10 17:32:23 +02:00
Huang Xin 932cfa3b9f feat: show groups in list mode in the library page, closes #978 (#2009) 2025-09-09 18:18:54 +02:00
Huang Xin 01fde8573c fonts: support variable fonts when importing ttf/otf font files, closes #2003 (#2007) 2025-09-09 11:25:27 +02:00
Huang Xin d95656b37b fix(tts): compensate audio fade-in for resumed play on iOS and macOS, closes #2005 (#2006) 2025-09-09 08:29:39 +02:00
Huang Xin 210eda0340 fix: pagination with volume keys now has right direction for RTL books, closes #1996 (#2004) 2025-09-09 05:36:45 +02:00
Huang Xin 0d8b877d73 fix: don't show rounded window in maximized or fullscreen mode on (#2001)
Linux, closes #1998
2025-09-08 19:32:15 +02:00
dependabot[bot] 79d39b2295 chore(deps): bump the github-actions group with 2 updates (#1999) 2025-09-08 12:44:45 +08:00
Huang Xin e03b0af58a feat: swipe to paginate on fixed layout books, closes #1028 (#1995) 2025-09-07 16:14:34 +02:00
Huang Xin 71c2143cba fix: avoid pagination until book view is inited, closes #1983 (#1994) 2025-09-07 14:47:35 +02:00
Huang Xin 70a7f5be19 fix: use transparent background for rounded window on Linux, closes #1982 (#1991) 2025-09-07 10:38:27 +02:00
Huang Xin d2242423f5 fix: correctly parse filenames for woff/woff2 font files on Android, closes #1981 (#1990) 2025-09-07 07:04:48 +02:00
Huang Xin 782e9c8b20 fix: restore scale factor (zoom level) for PDFs, closes #1985 (#1989) 2025-09-07 06:12:57 +02:00
Huang Xin 55b2c678a4 fix(kosync): handle non-standard XPath syntax from Koreader, closes #1968 (#1988) 2025-09-07 06:05:27 +02:00
Huang Xin 48d754c184 fix(layout): prevent layout shift on Android browsers, closes #1979 (#1980) 2025-09-06 18:13:02 +02:00
Huang Xin ff0443ace7 compat: fixed zoom level handling on iOS and macOS, closes #1784 (#1978) 2025-09-06 16:07:06 +02:00
156 changed files with 3423 additions and 1095 deletions
+6 -1
View File
@@ -48,7 +48,7 @@ jobs:
version: 10.14.0
- name: setup node
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 22
cache: pnpm
@@ -72,6 +72,11 @@ jobs:
run: |
pnpm test -- --watch=false
- name: run lint
working-directory: apps/readest-app
run: |
pnpm lint
- name: build the web App
if: matrix.config.platform == 'web'
working-directory: apps/readest-app
+5 -5
View File
@@ -19,12 +19,12 @@ jobs:
steps:
- uses: actions/checkout@v5
- name: setup node
uses: actions/setup-node@v4
uses: actions/setup-node@v5
- name: get version
run: echo "PACKAGE_VERSION=$(node -p "require('./apps/readest-app/package.json').version")" >> $GITHUB_ENV
- name: get release
id: get-release
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const { data } = await github.rest.repos.getLatestRelease({
@@ -35,7 +35,7 @@ jobs:
core.setOutput('release_tag', data.tag_name);
- name: get release notes
id: get-release-notes
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
@@ -57,7 +57,7 @@ jobs:
steps:
- name: update release
id: update-release
uses: actions/github-script@v7
uses: actions/github-script@v8
env:
release_id: ${{ needs.get-release.outputs.release_id }}
release_tag: ${{ needs.get-release.outputs.release_tag }}
@@ -135,7 +135,7 @@ jobs:
version: 10.14.0
- name: setup node
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 22
cache: pnpm
+17 -18
View File
@@ -45,23 +45,23 @@
<div align="left">✅ Implemented</div>
| **Feature** | **Description** | **Status** |
| --------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------- |
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, TXT, PDF (experimental) | ✅ |
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ |
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience. | ✅ |
| **Excerpt Text for Note-Taking** | Easily excerpt text from books for detailed notes and analysis. | ✅ |
| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ |
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ |
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ |
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
| **Translate with DeepL** | From a single sentence to the entire book—translate instantly with DeepL. | ✅ |
| **Translate with Yandex** | Instantly translate text or books using Yandex Translate. | ✅ |
| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ |
| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ |
| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ |
| **Feature** | **Description** | **Status** |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------- |
| **Multi-Format Support** | Support EPUB, MOBI, KF8 (AZW3), FB2, CBZ, TXT, PDF (experimental) | ✅ |
| **Scroll/Page View Modes** | Switch between scrolling or paginated reading modes. | ✅ |
| **Full-Text Search** | Search across the entire book to find relevant sections. | ✅ |
| **Annotations and Highlighting** | Add highlights, bookmarks, and notes to enhance your reading experience. | ✅ |
| **Excerpt Text for Note-Taking** | Easily excerpt text from books for detailed notes and analysis. | ✅ |
| **Dictionary/Wikipedia Lookup** | Instantly look up words and terms when reading. | ✅ |
| **[Parallel Read][link-parallel-read]** | Read two books or documents simultaneously in a split-screen view. | ✅ |
| **Customize Font and Layout** | Adjust font, layout, theme mode, and theme colors for a personalized experience. | ✅ |
| **File Association and Open With** | Quickly open files in Readest in your file browser with one-click. | ✅ |
| **Sync across Platforms** | Synchronize book files, reading progress, notes, and bookmarks across all supported platforms. | ✅ |
| **Accessibility** | Provides full keyboard navigation and supports for screen readers such as VoiceOver, TalkBack, NVDA, and Orca. | ✅ |
| **Translate with DeepL and Yandex** | From a single sentence to the entire book—translate instantly. | ✅ |
| **Text-to-Speech (TTS) Support** | Enjoy smooth, multilingual narration—even within a single book. | ✅ |
| **Library Management** | Organize, sort, and manage your entire ebook library. | ✅ |
| **Code Syntax Highlighting** | Read software manuals with rich coloring of code examples. | ✅ |
## Planned Features
@@ -72,7 +72,6 @@
| ------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------ |
| [**Sync with Koreader**][link-kosync-wiki] | Synchronize reading progress, notes, and bookmarks with [Koreader][link-koreader] devices. | 🛠 |
| **AI-Powered Summarization** | Generate summaries of books or chapters using AI for quick insights. | 🛠 |
| **Keyboard Navigation** | Implement vimium-style keybindings for book navigation. | 🔄 |
| **Support OPDS/Calibre** | Integrate OPDS/Calibre to access online libraries and catalogs. | 🔄 |
| **Audiobook Support** | Extend functionality to play and manage audiobooks. | 🔄 |
| **Handwriting Annotations** | Add support for handwriting annotations using a pen on compatible devices. | 🔄 |
+20 -8
View File
@@ -1,13 +1,25 @@
import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
import path from 'node:path';
import js from '@eslint/js';
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
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [...compat.extends("next/core-web-vitals", "next/typescript")];
export default [
...compat.extends('next/core-web-vitals', 'next/typescript'),
{
plugins: {
'jsx-a11y': jsxA11y,
},
rules: {
...jsxA11y.configs.recommended.rules,
},
},
];
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.78",
"version": "0.9.81",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -125,6 +125,7 @@
"dotenv-cli": "^7.4.4",
"eslint": "^9.16.0",
"eslint-config-next": "15.0.3",
"eslint-plugin-jsx-a11y": "^6.10.2",
"i18next-scanner": "^4.6.0",
"jsdom": "^26.1.0",
"mkdirp": "^3.0.1",
@@ -81,7 +81,6 @@
"Search...": "بحث...",
"Select Book": "تحديد الكتاب",
"Select Books": "تحديد الكتب",
"Select Multiple Books": "تحديد عدة كتب",
"Sepia": "بني داكن",
"Serif Font": "خط مذيل (Serif)",
"Show Book Details": "عرض تفاصيل الكتاب",
@@ -117,6 +116,7 @@
"Book Details": "تفاصيل الكتاب",
"From Local File": "من ملف محلي",
"TOC": "جدول المحتويات",
"Table of Contents": "جدول المحتويات",
"Book uploaded: {{title}}": "تم تحميل الكتاب: {{title}}",
"Failed to upload book: {{title}}": "فشل تحميل الكتاب: {{title}}",
"Book downloaded: {{title}}": "تم تنزيل الكتاب: {{title}}",
@@ -344,7 +344,6 @@
"Left Margin (px)": "الهامش الأيسر (بكسل)",
"Column Gap (%)": "تباعد الأعمدة (%)",
"Always Show Status Bar": "دائمًا إظهار شريط الحالة",
"Translation Not Available": "الترجمة غير متاحة",
"Custom Content CSS": "CSS مخصص للمحتوى",
"Enter CSS for book content styling...": "أدخل CSS لتنسيق محتوى الكتاب...",
"Custom Reader UI CSS": "CSS مخصص لواجهة القارئ",
@@ -538,5 +537,47 @@
"Play": "تشغيل",
"Next Sentence": "الجملة التالية",
"Next Paragraph": "الفقرة التالية",
"Separate Cover Page": "صفحة غلاف منفصلة"
"Separate Cover Page": "صفحة غلاف منفصلة",
"Resize Notebook": "تغيير حجم المفكرة",
"Resize Sidebar": "تغيير حجم الشريط الجانبي",
"Get Help from the Readest Community": "الحصول على المساعدة من مجتمع ريديست",
"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": "تكبير أو استعادة"
}
@@ -66,7 +66,6 @@
"Search Books...": "বই খুঁজুন...",
"Clear Search": "খোঁজ পরিষ্কার করুন",
"Import Books": "বই আমদানি করুন",
"Select Multiple Books": "একাধিক বই নির্বাচন করুন",
"Select Books": "বই নির্বাচন করুন",
"Deselect": "অনির্বাচিত করুন",
"Select All": "সব নির্বাচন করুন",
@@ -324,9 +323,9 @@
"Match Whole Words": "পূর্ণ শব্দ মিলান",
"Match Diacritics": "ডায়াক্রিটিক্স মিলান",
"TOC": "সূচিপত্র",
"Table of Contents": "সূচিপত্র",
"Sidebar": "সাইডবার",
"Disable Translation": "অনুবাদ নিষ্ক্রিয়",
"Translation Not Available": "অনুবাদ উপলব্ধ নয়",
"TTS not supported for PDF": "PDF এর জন্য TTS সমর্থিত নয়",
"TTS not supported for this document": "এই ডকুমেন্টের জন্য TTS সমর্থিত নয়",
"No Timeout": "কোন টাইমআউট নেই",
@@ -522,5 +521,47 @@
"Play": "চালান",
"Next Sentence": "পরবর্তী বাক্য",
"Next Paragraph": "পরবর্তী অনুচ্ছেদ",
"Separate Cover Page": "আলাদা কভার পৃষ্ঠা"
"Separate Cover Page": "আলাদা কভার পৃষ্ঠা",
"Resize Notebook": "নোটবুকের আকার পরিবর্তন",
"Resize Sidebar": "সাইডবারের আকার পরিবর্তন",
"Get Help from the Readest Community": "রিডেস্ট কমিউনিটি থেকে সাহায্য নিন",
"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": "বৃহৎ করুন বা পুনরুদ্ধার করুন"
}
@@ -82,7 +82,6 @@
"Search...": "འཚོལ་བཤེར།...",
"Select Book": "དཔེ་དེབ་འདེམས་པ།",
"Select Books": "དཔེ་དེབ་འདེམས་པ།",
"Select Multiple Books": "དཔེ་དེབ་མང་པོ་འདེམས་པ།",
"Sepia": "རྙིང་པའི་ཉམས་འགྱུར།",
"Serif Font": "མཐའ་ཐིག་ཡོད་པའི་ཡིག་གཟུགས།",
"Show Book Details": "དཔེ་དེབ་ཀྱི་གནས་ཚུལ་རྒྱས་པ་མངོན་པ།",
@@ -118,6 +117,7 @@
"Book Details": "དཔེ་དེབ་ཀྱི་གནས་ཚུལ་རྒྱས་པ།",
"From Local File": "ས་གནས་ཀྱི་ཡིག་ཆ་ནས་ནང་འདྲེན།",
"TOC": "དཀར་ཆག",
"Table of Contents": "དཀར་ཆག",
"Book uploaded: {{title}}": "དཔེ་དེབ་སྤྲད་ཟིན། {{title}}",
"Failed to upload book: {{title}}": "དཔེ་དེབ་སྤྲད་མ་ཐུབ། {{title}}",
"Book downloaded: {{title}}": "དཔེ་དེབ་ཕབ་ལེན་བྱས་ཟིན། {{title}}",
@@ -329,7 +329,6 @@
"Left Margin (px)": "གཡོན་ངོས་ཀྱི་བར་ཐག",
"Column Gap (%)": "གྲལ་ཐིག་བར་གྱི་བར་ཐག",
"Always Show Status Bar": "རྟག་ཏུ་གནས་ཚུལ་གྱི་གྲལ་ཐིག་མངོན་པ།",
"Translation Not Available": "སྒྱུར་བཅོས་སྤྱོད་ཐུབ་མེད།",
"Custom Content CSS": "རང་འགུལ་གྱི་ནང་དོན་ CSS",
"Enter CSS for book content styling...": "དཔེ་དེབ་ཀྱི་ནང་དོན་གྱི་རྣམ་པའི་ CSS ནང་འཇུག་བྱོས།...",
"Custom Reader UI CSS": "རང་འགུལ་གྱི་མཐུད་ངོས་ CSS",
@@ -518,5 +517,47 @@
"Play": "གཏོང་བ།",
"Next Sentence": "རྗེས་ཀྱི་ཚིག་གྲུབ།",
"Next Paragraph": "རྗེས་ཀྱི་དོན་ཚན།",
"Separate Cover Page": "སྟོང་ངོས་གི་ཤོག་དཀར་པ།"
"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": "བཀྲ་ཤིས་བཏང་བ།"
}
@@ -81,7 +81,6 @@
"Search...": "Suchen...",
"Select Book": "Buch auswählen",
"Select Books": "Bücher auswählen",
"Select Multiple Books": "Mehrere Bücher auswählen",
"Sepia": "Sepia",
"Serif Font": "Serifenschrift",
"Show Book Details": "Buchdetails anzeigen",
@@ -117,6 +116,7 @@
"Book Details": "Buchdetails",
"From Local File": "Aus lokaler Datei",
"TOC": "Inhaltsverzeichnis",
"Table of Contents": "Inhaltsverzeichnis",
"Book uploaded: {{title}}": "Buch hochgeladen: {{title}}",
"Failed to upload book: {{title}}": "Fehler beim Hochladen des Buches: {{title}}",
"Book downloaded: {{title}}": "Buch heruntergeladen: {{title}}",
@@ -332,7 +332,6 @@
"Left Margin (px)": "Linker Rand (px)",
"Column Gap (%)": "Spaltenabstand (%)",
"Always Show Status Bar": "Statusleiste immer anzeigen",
"Translation Not Available": "Übersetzung nicht verfügbar",
"Custom Content CSS": "Benutzerdefiniertes Inhalts-CSS",
"Enter CSS for book content styling...": "CSS für die Buchinhaltsgestaltung eingeben...",
"Custom Reader UI CSS": "Benutzerdefiniertes CSS für die Leseroberfläche",
@@ -522,5 +521,47 @@
"Play": "Wiedergabe",
"Next Sentence": "Nächster Satz",
"Next Paragraph": "Nächster Absatz",
"Separate Cover Page": "Titelseite separat anzeigen"
"Separate Cover Page": "Titelseite separat anzeigen",
"Resize Notebook": "Notizbuchgröße ändern",
"Resize Sidebar": "Seitenleisten-Größe ändern",
"Get Help from the Readest Community": "Hilfe von der Readest-Community erhalten",
"Remove cover image": "Coverbild entfernen",
"Bookshelf": "Bücherregal",
"View Menu": "Menü anzeigen",
"Settings Menu": "Einstellungsmenü",
"View account details and quota": "Kontodetails und Kontingent anzeigen",
"Library Header": "Bibliothekskopf",
"Book Content": "Buchinhalt",
"Footer Bar": "Fußzeile",
"Header Bar": "Kopfzeile",
"View Options": "Ansichtsoptionen",
"Book Menu": "Buchmenü",
"Search Options": "Suchoptionen",
"Close": "Schließen",
"Delete Book Options": "Buch-Löschoptionen",
"ON": "EIN",
"OFF": "AUS",
"Reading Progress": "Lesefortschritt",
"Page Margin": "Seitenrand",
"Remove Bookmark": "Lesezeichen entfernen",
"Add Bookmark": "Lesezeichen hinzufügen",
"Books Content": "Buchinhalt",
"Jump to Location": "Zu Standort springen",
"Unpin Notebook": "Notizbuch lösen",
"Pin Notebook": "Notizbuch anheften",
"Hide Search Bar": "Suchleiste ausblenden",
"Show Search Bar": "Suchleiste einblenden",
"On {{current}} of {{total}} page": "Auf Seite {{current}} von {{total}}",
"Section Title": "Abschnittsüberschrift",
"Decrease": "Verringern",
"Increase": "Erhöhen",
"Settings Panels": "Einstellungsfenster",
"Settings": "Einstellungen",
"Unpin Sidebar": "Seitenleiste lösen",
"Pin Sidebar": "Seitenleiste anheften",
"Toggle Sidebar": "Seitenleiste umschalten",
"Toggle Translation": "Übersetzung umschalten",
"Translation Disabled": "Übersetzung deaktiviert",
"Minimize": "Minimieren",
"Maximize or Restore": "Maximieren oder Wiederherstellen"
}
@@ -82,7 +82,6 @@
"Search...": "Αναζήτηση...",
"Select Book": "Επιλογή βιβλίου",
"Select Books": "Επιλογή βιβλίων",
"Select Multiple Books": "Επιλογή πολλαπλών βιβλίων",
"Sepia": "Σέπια",
"Serif Font": "Γραμματοσειρά Serif",
"Show Book Details": "Εμφάνιση λεπτομερειών βιβλίου",
@@ -118,6 +117,7 @@
"Book Details": "Λεπτομέρειες βιβλίου",
"From Local File": "Από τοπικό αρχείο",
"TOC": "Πίνακας περιεχομένων",
"Table of Contents": "Πίνακας περιεχομένων",
"Book uploaded: {{title}}": "Το βιβλίο με τίτλο {{title}} ανέβηκε",
"Failed to upload book: {{title}}": "Αποτυχία ανέβασμα βιβλίου: {{title}}",
"Book downloaded: {{title}}": "Το βιβλίο με τίτλο {{title}} κατέβηκε",
@@ -333,7 +333,6 @@
"Left Margin (px)": "Αριστερό περιθώριο (px)",
"Column Gap (%)": "Απόσταση στηλών (%)",
"Always Show Status Bar": "Πάντα εμφάνιση γραμμής κατάστασης",
"Translation Not Available": "Η μετάφραση δεν είναι διαθέσιμη",
"Custom Content CSS": "Προσαρμοσμένο CSS περιεχομένου",
"Enter CSS for book content styling...": "Εισαγάγετε CSS για στυλ περιεχομένου βιβλίου...",
"Custom Reader UI CSS": "Προσαρμοσμένο CSS διεπαφής αναγνώστη",
@@ -522,5 +521,47 @@
"Play": "Αναπαραγωγή",
"Next Sentence": "Επόμενη πρόταση",
"Next Paragraph": "Επόμενη παράγραφος",
"Separate Cover Page": "Ξεχωριστή σελίδα εξωφύλλου"
"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": "Μεγιστοποίηση ή Επαναφορά"
}
@@ -109,7 +109,6 @@
"Search...": "Buscar...",
"Select Book": "Seleccionar libro",
"Select Books": "Seleccionar libros",
"Select Multiple Books": "Seleccionar varios libros",
"Sepia": "Sepia",
"Serif Font": "Fuente serif",
"Show Book Details": "Mostrar detalles del libro",
@@ -145,6 +144,7 @@
"Book Details": "Detalles del libro",
"From Local File": "Desde archivo local",
"TOC": "Índice",
"Table of Contents": "Índice",
"Book uploaded: {{title}}": "Libro subido: {{title}}",
"Failed to upload book: {{title}}": "Error al subir libro: {{title}}",
"Book downloaded: {{title}}": "Libro descargado: {{title}}",
@@ -363,7 +363,6 @@
"Left Margin (px)": "Margen izquierdo (px)",
"Column Gap (%)": "Espacio entre columnas (%)",
"Always Show Status Bar": "Mostrar siempre la barra de estado",
"Translation Not Available": "Traducción no disponible",
"Custom Content CSS": "CSS del contenido",
"Enter CSS for book content styling...": "Introduce CSS para el contenido del libro...",
"Custom Reader UI CSS": "CSS de la interfaz",
@@ -526,5 +525,47 @@
"Play": "Reproducir",
"Next Sentence": "Siguiente oración",
"Next Paragraph": "Siguiente párrafo",
"Separate Cover Page": "Portada separada"
"Separate Cover Page": "Portada separada",
"Resize Notebook": "Redimensionar cuaderno",
"Resize Sidebar": "Redimensionar barra lateral",
"Get Help from the Readest Community": "Obtén ayuda de la comunidad de Readest",
"Remove cover image": "Eliminar imagen de portada",
"Bookshelf": "Estantería",
"View Menu": "Menú de Vista",
"Settings Menu": "Menú de Configuración",
"View account details and quota": "Ver detalles de la cuenta y cuota",
"Library Header": "Encabezado de Biblioteca",
"Book Content": "Contenido del Libro",
"Footer Bar": "Barra de Pie de Página",
"Header Bar": "Barra de Encabezado",
"View Options": "Opciones de Vista",
"Book Menu": "Menú de Libro",
"Search Options": "Opciones de Búsqueda",
"Close": "Cerrar",
"Delete Book Options": "Opciones de Eliminar Libro",
"ON": "ENCENDIDO",
"OFF": "APAGADO",
"Reading Progress": "Progreso de Lectura",
"Page Margin": "Márgenes de Página",
"Remove Bookmark": "Eliminar Marcador",
"Add Bookmark": "Agregar Marcador",
"Books Content": "Contenido de Libros",
"Jump to Location": "Saltar a Ubicación",
"Unpin Notebook": "Desanclar Cuaderno",
"Pin Notebook": "Anclar Cuaderno",
"Hide Search Bar": "Ocultar Barra de Búsqueda",
"Show Search Bar": "Mostrar Barra de Búsqueda",
"On {{current}} of {{total}} page": "En {{current}} de {{total}} página",
"Section Title": "Título de Sección",
"Decrease": "Disminuir",
"Increase": "Aumentar",
"Settings Panels": "Paneles de Configuración",
"Settings": "Configuraciones",
"Unpin Sidebar": "Desanclar Barra Lateral",
"Pin Sidebar": "Anclar Barra Lateral",
"Toggle Sidebar": "Alternar Barra Lateral",
"Toggle Translation": "Alternar Traducción",
"Translation Disabled": "Traducción Deshabilitada",
"Minimize": "Minimizar",
"Maximize or Restore": "Maximizar o Restaurar"
}
@@ -82,7 +82,6 @@
"Search...": "Rechercher...",
"Select Book": "Sélectionner un livre",
"Select Books": "Sélectionner des livres",
"Select Multiple Books": "Sélectionner plusieurs livres",
"Sepia": "Sépia",
"Serif Font": "Police avec empattement",
"Show Book Details": "Afficher les détails du livre",
@@ -118,6 +117,7 @@
"Book Details": "Détails du livre",
"From Local File": "Depuis un fichier local",
"TOC": "Table des matières",
"Table of Contents": "Table des matières",
"Book uploaded: {{title}}": "Livre téléchargé : {{title}}",
"Failed to upload book: {{title}}": "Échec du téléchargement du livre : {{title}}",
"Book downloaded: {{title}}": "Livre téléchargé : {{title}}",
@@ -335,7 +335,6 @@
"Left Margin (px)": "Marge gauche (px)",
"Column Gap (%)": "Espacement des colonnes (%)",
"Always Show Status Bar": "Afficher toujours la barre d'état",
"Translation Not Available": "Traduction non disponible",
"Custom Content CSS": "CSS du contenu",
"Enter CSS for book content styling...": "Saisissez le CSS pour le contenu du livre...",
"Custom Reader UI CSS": "CSS de linterface",
@@ -526,5 +525,47 @@
"Play": "Lecture",
"Next Sentence": "Phrase suivante",
"Next Paragraph": "Paragraphe suivant",
"Separate Cover Page": "Page de couverture séparée"
"Separate Cover Page": "Page de couverture séparée",
"Resize Notebook": "Redimensionner le Carnet",
"Resize Sidebar": "Redimensionner la Barre Latérale",
"Get Help from the Readest Community": "Obtenir de l'aide de la communauté Readest",
"Remove cover image": "Supprimer l'image de couverture",
"Bookshelf": "Étagère à livres",
"View Menu": "Menu d'affichage",
"Settings Menu": "Menu des paramètres",
"View account details and quota": "Voir les détails du compte et le quota",
"Library Header": "En-tête de la bibliothèque",
"Book Content": "Contenu du livre",
"Footer Bar": "Barre de pied de page",
"Header Bar": "Barre d'en-tête",
"View Options": "Options d'affichage",
"Book Menu": "Menu du livre",
"Search Options": "Options de recherche",
"Close": "Fermer",
"Delete Book Options": "Options de suppression du livre",
"ON": "ACTIVER",
"OFF": "DÉSACTIVER",
"Reading Progress": "Progression de lecture",
"Page Margin": "Marge de page",
"Remove Bookmark": "Supprimer le Marque-page",
"Add Bookmark": "Ajouter un Marque-page",
"Books Content": "Contenu des Livres",
"Jump to Location": "Aller à l'Emplacement",
"Unpin Notebook": "Détacher le Carnet",
"Pin Notebook": "Épingler le Carnet",
"Hide Search Bar": "Masquer la Barre de Recherche",
"Show Search Bar": "Afficher la Barre de Recherche",
"On {{current}} of {{total}} page": "Sur {{current}} de {{total}} page",
"Section Title": "Titre de Section",
"Decrease": "Diminuer",
"Increase": "Augmenter",
"Settings Panels": "Panneaux de Configuration",
"Settings": "Paramètres",
"Unpin Sidebar": "Détacher la Barre Latérale",
"Pin Sidebar": "Épingler la Barre Latérale",
"Toggle Sidebar": "Alternar la Barre Latérale",
"Toggle Translation": "Alternar Traduction",
"Translation Disabled": "Traduction Désactivée",
"Minimize": "Minimiser",
"Maximize or Restore": "Maximiser ou Restaurer"
}
@@ -82,7 +82,6 @@
"Search...": "खोजें...",
"Select Book": "पुस्तक चुनें",
"Select Books": "पुस्तकें चुनें",
"Select Multiple Books": "कई पुस्तकें चुनें",
"Sepia": "सेपिया",
"Serif Font": "सेरिफ फ़ॉन्ट",
"Show Book Details": "पुस्तक विवरण दिखाएं",
@@ -118,6 +117,7 @@
"Book Details": "पुस्तक विवरण",
"From Local File": "स्थानीय फ़ाइल से",
"TOC": "सामग्री",
"Table of Contents": "सामग्री",
"Book uploaded: {{title}}": "पुस्तक अपलोड की गई: {{title}}",
"Failed to upload book: {{title}}": "पुस्तक अपलोड करने में विफल: {{title}}",
"Book downloaded: {{title}}": "पुस्तक डाउनलोड की गई: {{title}}",
@@ -333,7 +333,6 @@
"Left Margin (px)": "बायां हाशिया (px)",
"Column Gap (%)": "स्तंभों के बीच की दूरी (%)",
"Always Show Status Bar": "स्थिति पट्टी हमेशा दिखाएं",
"Translation Not Available": "अनुवाद उपलब्ध नहीं है",
"Custom Content CSS": "सामग्री का CSS",
"Enter CSS for book content styling...": "पुस्तक सामग्री के लिए CSS दर्ज करें...",
"Custom Reader UI CSS": "इंटरफ़ेस का CSS",
@@ -522,5 +521,47 @@
"Play": "चलाएं",
"Next Sentence": "अगला वाक्य",
"Next Paragraph": "अगला पैराग्राफ",
"Separate Cover Page": "अलग कवर पृष्ठ"
"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": "अधिकतम या पुनर्स्थापित करें"
}
@@ -82,7 +82,6 @@
"Search...": "Cari...",
"Select Book": "Pilih Buku",
"Select Books": "Pilih buku",
"Select Multiple Books": "Pilih beberapa buku",
"Sepia": "Sepia",
"Serif Font": "Font Serif",
"Show Book Details": "Tampilkan Detail Buku",
@@ -118,6 +117,7 @@
"Book Details": "Detail Buku",
"From Local File": "Dari file lokal",
"TOC": "Daftar Isi",
"Table of Contents": "Daftar Isi",
"Book uploaded: {{title}}": "Buku diunggah: {{title}}",
"Failed to upload book: {{title}}": "Gagal mengunggah buku: {{title}}",
"Book downloaded: {{title}}": "Buku diunduh: {{title}}",
@@ -330,7 +330,6 @@
"Left Margin (px)": "Margin Kiri (px)",
"Column Gap (%)": "Jarak Antar Kolom (%)",
"Always Show Status Bar": "Selalu Tampilkan Bilah Status",
"Translation Not Available": "Terjemahan Tidak Tersedia",
"Custom Content CSS": "CSS konten",
"Enter CSS for book content styling...": "Masukkan CSS untuk gaya konten buku...",
"Custom Reader UI CSS": "CSS antarmuka",
@@ -518,5 +517,47 @@
"Play": "Putar",
"Next Sentence": "Kalimat Berikutnya",
"Next Paragraph": "Paragraf Berikutnya",
"Separate Cover Page": "Halaman Sampul Terpisah"
"Separate Cover Page": "Halaman Sampul Terpisah",
"Resize Notebook": "Ubah Ukuran Buku Catatan",
"Resize Sidebar": "Ubah Ukuran Bilah Samping",
"Get Help from the Readest Community": "Dapatkan Bantuan dari Komunitas Readest",
"Remove cover image": "Hapus gambar sampul",
"Bookshelf": "Rak Buku",
"View Menu": "Lihat Menu",
"Settings Menu": "Menu Pengaturan",
"View account details and quota": "Lihat detail akun dan kuota",
"Library Header": "Header Perpustakaan",
"Book Content": "Isi Buku",
"Footer Bar": "Bilah Footer",
"Header Bar": "Bilah Header",
"View Options": "Opsi Tampilan",
"Book Menu": "Menu Buku",
"Search Options": "Opsi Pencarian",
"Close": "Tutup",
"Delete Book Options": "Opsi Hapus Buku",
"ON": "AKTIF",
"OFF": "TIDAK AKTIF",
"Reading Progress": "Progres Membaca",
"Page Margin": "Margin Halaman",
"Remove Bookmark": "Hapus Penanda",
"Add Bookmark": "Tambahkan Penanda",
"Books Content": "Isi Buku",
"Jump to Location": "Lompat ke Lokasi",
"Unpin Notebook": "Batal Pin Notebook",
"Pin Notebook": "Pin Notebook",
"Hide Search Bar": "Sembunyikan Bilah Pencarian",
"Show Search Bar": "Tampilkan Bilah Pencarian",
"On {{current}} of {{total}} page": "Pada {{current}} dari {{total}} halaman",
"Section Title": "Judul Bagian",
"Decrease": "Kecilkan",
"Increase": "Besarakan",
"Settings Panels": "Panel Pengaturan",
"Settings": "Pengaturan",
"Unpin Sidebar": "Batal Pin Bilah Samping",
"Pin Sidebar": "Pin Bilah Samping",
"Toggle Sidebar": "Toggel Bilah Samping",
"Toggle Translation": "Toggel Terjemahan",
"Translation Disabled": "Terjemahan Dinonaktifkan",
"Minimize": "Minimalkan",
"Maximize or Restore": "Maksimalkan atau Pulihkan"
}
@@ -82,7 +82,6 @@
"Search...": "Cerca...",
"Select Book": "Seleziona libro",
"Select Books": "Seleziona libri",
"Select Multiple Books": "Seleziona più libri",
"Sepia": "Seppia",
"Serif Font": "Font serif",
"Show Book Details": "Mostra dettagli libro",
@@ -118,6 +117,7 @@
"Book Details": "Dettagli libro",
"From Local File": "Da file locale",
"TOC": "Sommario",
"Table of Contents": "Sommario",
"Book uploaded: {{title}}": "Libro caricato: {{title}}",
"Failed to upload book: {{title}}": "Caricamento libro non riuscito: {{title}}",
"Book downloaded: {{title}}": "Libro scaricato: {{title}}",
@@ -336,7 +336,6 @@
"Left Margin (px)": "Margine Sinistro (px)",
"Column Gap (%)": "Spazio tra Colonne (%)",
"Always Show Status Bar": "Mostra sempre la barra di stato",
"Translation Not Available": "Traduzione non disponibile",
"Custom Content CSS": "CSS contenuto",
"Enter CSS for book content styling...": "Inserisci il CSS per il contenuto del libro...",
"Custom Reader UI CSS": "CSS interfaccia",
@@ -526,5 +525,47 @@
"Play": "Riproduci",
"Next Sentence": "Frase successiva",
"Next Paragraph": "Paragrafo successivo",
"Separate Cover Page": "Pagina di copertina separata"
"Separate Cover Page": "Pagina di copertina separata",
"Resize Notebook": "Ridimensiona quaderno",
"Resize Sidebar": "Ridimensiona barra laterale",
"Get Help from the Readest Community": "Ottieni aiuto dalla community di Readest",
"Remove cover image": "Rimuovi immagine di copertina",
"Bookshelf": "Scaffale dei libri",
"View Menu": "Visualizza menu",
"Settings Menu": "Menu impostazioni",
"View account details and quota": "Visualizza dettagli account e quota",
"Library Header": "Intestazione biblioteca",
"Book Content": "Contenuto del libro",
"Footer Bar": "Barra piè di pagina",
"Header Bar": "Barra intestazione",
"View Options": "Opzioni di visualizzazione",
"Book Menu": "Menu libro",
"Search Options": "Opzioni di ricerca",
"Close": "Chiudi",
"Delete Book Options": "Opzioni di eliminazione libro",
"ON": "ATTIVO",
"OFF": "DISATTIVO",
"Reading Progress": "Progresso di lettura",
"Page Margin": "Margine di pagina",
"Remove Bookmark": "Rimuovi Segnalibro",
"Add Bookmark": "Aggiungi Segnalibro",
"Books Content": "Contenuto dei Libri",
"Jump to Location": "Salta alla Posizione",
"Unpin Notebook": "Sblocca Quaderno",
"Pin Notebook": "Blocca Quaderno",
"Hide Search Bar": "Nascondi Barra di Ricerca",
"Show Search Bar": "Mostra Barra di Ricerca",
"On {{current}} of {{total}} page": "A pagina {{current}} di {{total}}",
"Section Title": "Titolo Sezione",
"Decrease": "Diminuisci",
"Increase": "Aumenta",
"Settings Panels": "Pannelli di Impostazione",
"Settings": "Impostazioni",
"Unpin Sidebar": "Sblocca Bilah Samping",
"Pin Sidebar": "Blocca Bilah Samping",
"Toggle Sidebar": "Toggel Bilah Samping",
"Toggle Translation": "Toggel Terjemahan",
"Translation Disabled": "Terjemahan Dinonaktifkan",
"Minimize": "Minimalkan",
"Maximize or Restore": "Maksimalkan atau Pulihkan"
}
@@ -82,7 +82,6 @@
"Search...": "検索...",
"Select Book": "書籍を選択",
"Select Books": "書籍を選択",
"Select Multiple Books": "複数の書籍を選択",
"Sepia": "セピア",
"Serif Font": "セリフ体",
"Show Book Details": "書籍の詳細を表示",
@@ -118,6 +117,7 @@
"Book Details": "書籍の詳細",
"From Local File": "ローカルファイルから",
"TOC": "目次",
"Table of Contents": "目次",
"Book uploaded: {{title}}": "書籍がアップロードされました:{{title}}",
"Failed to upload book: {{title}}": "書籍のアップロードに失敗しました:{{title}}",
"Book downloaded: {{title}}": "書籍がダウンロードされました:{{title}}",
@@ -330,7 +330,6 @@
"Left Margin (px)": "左マージン (px)",
"Column Gap (%)": "列間の間隔 (%)",
"Always Show Status Bar": "ステータスバーを常に表示",
"Translation Not Available": "翻訳は利用できません",
"Custom Content CSS": "コンテンツCSS",
"Enter CSS for book content styling...": "本のコンテンツ用CSSを入力...",
"Custom Reader UI CSS": "リーダーUIのCSS",
@@ -518,5 +517,47 @@
"Play": "再生",
"Next Sentence": "次の文",
"Next Paragraph": "次の段落",
"Separate Cover Page": "別の表紙ページ"
"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": "最大化または復元"
}
@@ -82,7 +82,6 @@
"Search...": "검색...",
"Select Book": "책 선택",
"Select Books": "책 선택",
"Select Multiple Books": "여러 책 선택",
"Sepia": "세피아",
"Serif Font": "세리프체",
"Show Book Details": "책 세부 정보 표시",
@@ -118,6 +117,7 @@
"Book Details": "책 세부 정보",
"From Local File": "로컬 파일에서",
"TOC": "목차",
"Table of Contents": "목차",
"Book uploaded: {{title}}": "책 업로드됨: {{title}}",
"Failed to upload book: {{title}}": "책 업로드 실패: {{title}}",
"Book downloaded: {{title}}": "책 다운로드됨: {{title}}",
@@ -330,7 +330,6 @@
"Left Margin (px)": "왼쪽 여백 (px)",
"Column Gap (%)": "열 간격 (%)",
"Always Show Status Bar": "상태 표시줄 항상 표시",
"Translation Not Available": "번역을 사용할 수 없음",
"Custom Content CSS": "콘텐츠 CSS",
"Enter CSS for book content styling...": "도서 콘텐츠 스타일을 위한 CSS 입력...",
"Custom Reader UI CSS": "리더 UI CSS",
@@ -518,5 +517,47 @@
"Play": "재생",
"Next Sentence": "다음 문장",
"Next Paragraph": "다음 단락",
"Separate Cover Page": "별도의 표지 페이지"
"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": "최대화 또는 복원"
}
@@ -55,7 +55,6 @@
"Search Books...": "Boeken zoeken...",
"Clear Search": "Zoekopdracht wissen",
"Import Books": "Boeken importeren",
"Select Multiple Books": "Meerdere boeken selecteren",
"Select Books": "Boeken selecteren",
"Logged in as {{userDisplayName}}": "Ingelogd als {{userDisplayName}}",
"Logged in": "Ingelogd",
@@ -211,6 +210,7 @@
"Match Whole Words": "Hele woorden",
"Match Diacritics": "Diakritische tekens",
"TOC": "Inhoudsopgave",
"Table of Contents": "Inhoudsopgave",
"Sidebar": "Zijbalk",
"TTS not supported for PDF": "TTS niet ondersteund voor PDF",
"No Timeout": "Geen time-out",
@@ -333,7 +333,6 @@
"Left Margin (px)": "Linker marge (px)",
"Column Gap (%)": "Kolomafstand (%)",
"Always Show Status Bar": "Statusbalk altijd weergeven",
"Translation Not Available": "Vertaling niet beschikbaar",
"Custom Content CSS": "Aangepaste inhoud-CSS",
"Enter CSS for book content styling...": "Voer CSS in voor de opmaak van de boekinhoud...",
"Custom Reader UI CSS": "Aangepaste interface-CSS",
@@ -522,5 +521,47 @@
"Play": "Afspelen",
"Next Sentence": "Volgende zin",
"Next Paragraph": "Volgende alinea",
"Separate Cover Page": "Afzonderlijke omslagpagina"
"Separate Cover Page": "Afzonderlijke omslagpagina",
"Resize Notebook": "Formaat Notitieblok Aanpassen",
"Resize Sidebar": "Formaat Zijbalk Aanpassen",
"Get Help from the Readest Community": "Hulp krijgen van de Readest Community",
"Remove cover image": "Omslagafbeelding verwijderen",
"Bookshelf": "Boekenkast",
"View Menu": "Menu bekijken",
"Settings Menu": "Instellingenmenu",
"View account details and quota": "Accountgegevens en quotum bekijken",
"Library Header": "Bibliotheekkop",
"Book Content": "Boekinhoud",
"Footer Bar": "Voettekstbalk",
"Header Bar": "Koptekstbalk",
"View Options": "Weergaveopties",
"Book Menu": "Boekmenu",
"Search Options": "Zoekopties",
"Close": "Sluiten",
"Delete Book Options": "Boekverwijderopties",
"ON": "AAN",
"OFF": "UIT",
"Reading Progress": "Leesvoortgang",
"Page Margin": "Pagina Marges",
"Remove Bookmark": "Bladwijzer Verwijderen",
"Add Bookmark": "Bladwijzer Toevoegen",
"Books Content": "Boekinhoud",
"Jump to Location": "Ga naar Locatie",
"Unpin Notebook": "Notitieblok Losmaken",
"Pin Notebook": "Notitieblok Vastmaken",
"Hide Search Bar": "Verberg Zoekbalk",
"Show Search Bar": "Toon Zoekbalk",
"On {{current}} of {{total}} page": "Op {{current}} van {{total}} pagina",
"Section Title": "Sectietitel",
"Decrease": "Verkleinen",
"Increase": "Vergroten",
"Settings Panels": "Instellingenpanelen",
"Settings": "Instellingen",
"Unpin Sidebar": "Zijbalk Losmaken",
"Pin Sidebar": "Zijbalk Vastmaken",
"Toggle Sidebar": "Zijbalk Wisselen",
"Toggle Translation": "Vertaling Wisselen",
"Translation Disabled": "Vertaling Uitgeschakeld",
"Minimize": "Minimaliseren",
"Maximize or Restore": "Maximaliseren of Herstellen"
}
@@ -82,7 +82,6 @@
"Search...": "Szukaj...",
"Select Book": "Wybierz książkę",
"Select Books": "Wybierz książki",
"Select Multiple Books": "Wybierz wiele książek",
"Sepia": "Sepia",
"Serif Font": "Czcionka szeryfowa",
"Show Book Details": "Pokaż szczegóły książki",
@@ -118,6 +117,7 @@
"Book Details": "Szczegóły książki",
"From Local File": "Z pliku lokalnego",
"TOC": "Spis treści",
"Table of Contents": "Spis treści",
"Book uploaded: {{title}}": "Książka przesłana: {{title}}",
"Failed to upload book: {{title}}": "Nie udało się przesłać książki: {{title}}",
"Book downloaded: {{title}}": "Książka pobrana: {{title}}",
@@ -339,7 +339,6 @@
"Left Margin (px)": "Lewy margines (px)",
"Column Gap (%)": "Odstęp między kolumnami (%)",
"Always Show Status Bar": "Zawsze pokazuj pasek stanu",
"Translation Not Available": "Brak dostępnego tłumaczenia",
"Custom Content CSS": "CSS treści",
"Enter CSS for book content styling...": "Wprowadź CSS dla stylu treści książki...",
"Custom Reader UI CSS": "CSS interfejsu",
@@ -530,5 +529,47 @@
"Play": "Odtwórz",
"Next Sentence": "Następne zdanie",
"Next Paragraph": "Następny akapit",
"Separate Cover Page": "Osobna strona okładki"
"Separate Cover Page": "Osobna strona okładki",
"Resize Notebook": "Zmień rozmiar notatnika",
"Resize Sidebar": "Zmień rozmiar paska bocznego",
"Get Help from the Readest Community": "Uzyskaj pomoc od społeczności Readest",
"Remove cover image": "Usuń obraz okładki",
"Bookshelf": "Półka na książki",
"View Menu": "Pokaż menu",
"Settings Menu": "Menu ustawień",
"View account details and quota": "Zobacz szczegóły konta i limit",
"Library Header": "Nagłówek biblioteki",
"Book Content": "Zawartość książki",
"Footer Bar": "Pasek stopki",
"Header Bar": "Pasek nagłówka",
"View Options": "Opcje widoku",
"Book Menu": "Menu książki",
"Search Options": "Opcje wyszukiwania",
"Close": "Zamknij",
"Delete Book Options": "Opcje usuwania książki",
"ON": "WŁĄCZONE",
"OFF": "WYŁĄCZONE",
"Reading Progress": "Postęp czytania",
"Page Margin": "Margines strony",
"Remove Bookmark": "Usuń zakładkę",
"Add Bookmark": "Dodaj zakładkę",
"Books Content": "Zawartość książek",
"Jump to Location": "Przejdź do lokalizacji",
"Unpin Notebook": "Odczep notatnik",
"Pin Notebook": "Przypnij notatnik",
"Hide Search Bar": "Ukryj pasek wyszukiwania",
"Show Search Bar": "Pokaż pasek wyszukiwania",
"On {{current}} of {{total}} page": "Na {{current}} z {{total}} strony",
"Section Title": "Tytuł sekcji",
"Decrease": "Zmniejsz",
"Increase": "Zwiększ",
"Settings Panels": "Panele ustawień",
"Settings": "Ustawienia",
"Unpin Sidebar": "Odczep pasek boczny",
"Pin Sidebar": "Przypnij pasek boczny",
"Toggle Sidebar": "Włącz/Wyłącz pasek boczny",
"Toggle Translation": "Włącz/Wyłącz tłumaczenie",
"Translation Disabled": "Tłumaczenie wyłączone",
"Minimize": "Minimalizuj",
"Maximize or Restore": "Maksymalizuj lub Przywróć"
}
@@ -82,7 +82,6 @@
"Search...": "Pesquisar...",
"Select Book": "Selecionar Livro",
"Select Books": "Selecionar livros",
"Select Multiple Books": "Selecionar múltiplos livros",
"Sepia": "Sépia",
"Serif Font": "Fonte Serif",
"Show Book Details": "Mostrar Detalhes do Livro",
@@ -118,6 +117,7 @@
"Book Details": "Detalhes do Livro",
"From Local File": "Do arquivo local",
"TOC": "Índice",
"Table of Contents": "Índice",
"Book uploaded: {{title}}": "Livro enviado: {{title}}",
"Failed to upload book: {{title}}": "Falha ao enviar livro: {{title}}",
"Book downloaded: {{title}}": "Livro baixado: {{title}}",
@@ -336,7 +336,6 @@
"Left Margin (px)": "Margem esquerda (px)",
"Column Gap (%)": "Espaço entre colunas (%)",
"Always Show Status Bar": "Exibir sempre a barra de status",
"Translation Not Available": "Tradução não disponível",
"Custom Content CSS": "CSS do conteúdo",
"Enter CSS for book content styling...": "Insira o CSS para o estilo do conteúdo do livro...",
"Custom Reader UI CSS": "CSS da interface",
@@ -526,5 +525,47 @@
"Play": "Reproduzir",
"Next Sentence": "Próxima frase",
"Next Paragraph": "Próximo parágrafo",
"Separate Cover Page": "Páginas de Capa Separadas"
"Separate Cover Page": "Páginas de Capa Separadas",
"Resize Notebook": "Redimensionar Caderno",
"Resize Sidebar": "Redimensionar Barra Lateral",
"Get Help from the Readest Community": "Obter ajuda da comunidade Readest",
"Remove cover image": "Remover imagem da capa",
"Bookshelf": "Estante de Livros",
"View Menu": "Ver Menu",
"Settings Menu": "Menu de Configurações",
"View account details and quota": "Ver detalhes da conta e cota",
"Library Header": "Cabeçalho da Biblioteca",
"Book Content": "Conteúdo do Livro",
"Footer Bar": "Barra de Rodapé",
"Header Bar": "Barra de Cabeçalho",
"View Options": "Opções de Visualização",
"Book Menu": "Menu do Livro",
"Search Options": "Opções de Pesquisa",
"Close": "Fechar",
"Delete Book Options": "Opções de Exclusão de Livro",
"ON": "LIGADO",
"OFF": "DESLIGADO",
"Reading Progress": "Progresso de Leitura",
"Page Margin": "Margem da Página",
"Remove Bookmark": "Remover Marcador",
"Add Bookmark": "Adicionar Marcador",
"Books Content": "Conteúdo dos Livros",
"Jump to Location": "Ir para Localização",
"Unpin Notebook": "Desafixar Caderno",
"Pin Notebook": "Fixar Caderno",
"Hide Search Bar": "Ocultar Barra de Pesquisa",
"Show Search Bar": "Mostrar Barra de Pesquisa",
"On {{current}} of {{total}} page": "Na {{current}} de {{total}} página",
"Section Title": "Título da Seção",
"Decrease": "Diminuir",
"Increase": "Aumentar",
"Settings Panels": "Painéis de Configurações",
"Settings": "Configurações",
"Unpin Sidebar": "Desafixar Barra Lateral",
"Pin Sidebar": "Fixar Barra Lateral",
"Toggle Sidebar": "Alternar Barra Lateral",
"Toggle Translation": "Alternar Tradução",
"Translation Disabled": "Tradução Desativada",
"Minimize": "Minimizar",
"Maximize or Restore": "Maximizar ou Restaurar"
}
@@ -82,7 +82,6 @@
"Search...": "Поиск...",
"Select Book": "Выбрать книгу",
"Select Books": "Выбрать книги",
"Select Multiple Books": "Выбрать несколько книг",
"Sepia": "Сепия",
"Serif Font": "Шрифт с засечками",
"Show Book Details": "Показать детали книги",
@@ -118,6 +117,7 @@
"Book Details": "Детали книги",
"From Local File": "Из локального файла",
"TOC": "Содержание",
"Table of Contents": "Содержание",
"Book uploaded: {{title}}": "Книга загружена: {{title}}",
"Failed to upload book: {{title}}": "Не удалось загрузить книгу: {{title}}",
"Book downloaded: {{title}}": "Книга скачана: {{title}}",
@@ -339,7 +339,6 @@
"Left Margin (px)": "Левый отступ (пикс)",
"Column Gap (%)": "Промежуток между колонками (%)",
"Always Show Status Bar": "Всегда показывать панель состояния",
"Translation Not Available": "Перевод недоступен",
"Custom Content CSS": "CSS содержимого",
"Enter CSS for book content styling...": "Введите CSS для оформления содержимого книги...",
"Custom Reader UI CSS": "CSS интерфейса",
@@ -530,5 +529,47 @@
"Play": "Воспроизвести",
"Next Sentence": "Следующее предложение",
"Next Paragraph": "Следующий абзац",
"Separate Cover Page": "Отдельная страница обложки"
"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": "Максимизировать или восстановить"
}
@@ -66,7 +66,6 @@
"Search Books...": "පොත් සොයන්න...",
"Clear Search": "සෙවීම මකන්න",
"Import Books": "පොත් ආයාත කරන්න",
"Select Multiple Books": "පොත් කිහිපයක් තෝරන්න",
"Select Books": "පොත් තෝරන්න",
"Deselect": "අනිශ්චිත කරන්න",
"Select All": "සියල්ල තෝරන්න",
@@ -324,9 +323,9 @@
"Match Whole Words": "සම්පූර්ණ වචන ගැළපීම",
"Match Diacritics": "ධ්වනි ලකුණු ගැළපීම",
"TOC": "සූචිය",
"Table of Contents": "සූචිය",
"Sidebar": "පැති තීරුව",
"Disable Translation": "පරිවර්තනය අබල කරන්න",
"Translation Not Available": "පරිවර්තනය ලබා ගත නොහැක",
"TTS not supported for PDF": "PDF සඳහා TTS සහාය නොදක්වයි",
"TTS not supported for this document": "මෙම ලේඛනය සඳහා TTS සහාය නොදක්වයි",
"No Timeout": "කාල සීමාවක් නැත",
@@ -522,5 +521,47 @@
"Play": "වාදනය කරන්න",
"Next Sentence": "මීළඟ වාක්‍යය",
"Next Paragraph": "මීළඟ ඡේදය",
"Separate Cover Page": "වෙන් වූ ආවරණ පිටුව"
"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": "ඉහළ නැංවීම හෝ ප්‍රතිසංස්කරණය"
}
@@ -66,7 +66,6 @@
"Search Books...": "புத்தகங்களைத் தேடவும்...",
"Clear Search": "தேடலை அழிக்கவும்",
"Import Books": "புத்தகங்களை இறக்குமதி செய்யவும்",
"Select Multiple Books": "பல புத்தகங்களை தேர்வு செய்யவும்",
"Select Books": "புத்தகங்களை தேர்வு செய்யவும்",
"Deselect": "தேர்வுநீக்கவும்",
"Select All": "அனைத்தையும் தேர்வு செய்யவும்",
@@ -324,9 +323,9 @@
"Match Whole Words": "முழு வார்த்தைகள் பொருத்தம்",
"Match Diacritics": "உச்சரிப்பு குறிகள் பொருத்தம்",
"TOC": "உள்ளடக்கம்",
"Table of Contents": "உள்ளடக்கம்",
"Sidebar": "பக்கப்பட்டி",
"Disable Translation": "மொழிபெயர்ப்பை முடக்கவும்",
"Translation Not Available": "மொழிபெயர்ப்பு கிடைக்கவில்லை",
"TTS not supported for PDF": "PDF க்கு TTS ஆதரிக்கப்படவில்லை",
"TTS not supported for this document": "இந்த ஆவணத்திற்கு TTS ஆதரிக்கப்படவில்லை",
"No Timeout": "நேர வரம்பு இல்லை",
@@ -522,5 +521,47 @@
"Play": "இயக்கு",
"Next Sentence": "அடுத்த வாக்கியம்",
"Next Paragraph": "அடுத்த பத்தி",
"Separate Cover Page": "தனித்த அட்டை பக்கம்"
"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": "அதிகரிக்கவும் அல்லது மீட்டமைக்கவும்"
}
@@ -63,7 +63,6 @@
"Search Books...": "ค้นหาหนังสือ...",
"Clear Search": "ล้างการค้นหา",
"Import Books": "นำเข้าหนังสือ",
"Select Multiple Books": "เลือกหลายเล่ม",
"Select Books": "เลือกหนังสือ",
"Deselect": "ยกเลิกเลือก",
"Select All": "เลือกทั้งหมด",
@@ -274,9 +273,9 @@
"Match Whole Words": "ตรงทั้งคำ",
"Match Diacritics": "ตรงตามเครื่องหมายวรรณยุกต์",
"TOC": "สารบัญ",
"Table of Contents": "สารบัญ",
"Sidebar": "แถบข้าง",
"Disable Translation": "ปิดการแปล",
"Translation Not Available": "ไม่มีการแปล",
"TTS not supported for PDF": "ไม่รองรับ TTS สำหรับ PDF",
"No Timeout": "ไม่จำกัดเวลา",
"{{value}} minute": "{{value}} นาที",
@@ -518,5 +517,47 @@
"Play": "เล่น",
"Next Sentence": "ประโยคถัดไป",
"Next Paragraph": "ย่อหน้าถัดไป",
"Separate Cover Page": "หน้าปกแยกต่างหาก"
"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": "ขยายหรือคืนค่า"
}
@@ -82,7 +82,6 @@
"Search...": "Ara...",
"Select Book": "Kitap Seç",
"Select Books": "Kitapları seç",
"Select Multiple Books": "Birden fazla kitap seç",
"Sepia": "Sepya",
"Serif Font": "Serif Yazı Tipi",
"Show Book Details": "Kitap Detaylarını Göster",
@@ -118,6 +117,7 @@
"Book Details": "Kitap Detayları",
"From Local File": "Yerel dosyadan",
"TOC": "İçindekiler",
"Table of Contents": "İçindekiler",
"Book uploaded: {{title}}": "Kitap yüklendi: {{title}}",
"Failed to upload book: {{title}}": "Kitap yüklenemedi: {{title}}",
"Book downloaded: {{title}}": "Kitap indirildi: {{title}}",
@@ -333,7 +333,6 @@
"Left Margin (px)": "Sol Kenar Boşluğu (px)",
"Column Gap (%)": "Sütun Aralığı (%)",
"Always Show Status Bar": "Durum Çubuğunu Her Zaman Göster",
"Translation Not Available": "Çeviri Mevcut Değil",
"Custom Content CSS": "Özel İçerik CSS'si",
"Enter CSS for book content styling...": "Kitap içeriği stili için CSS girin...",
"Custom Reader UI CSS": "Özel Okuyucu Arayüzü CSS'si",
@@ -522,5 +521,47 @@
"Play": "Oynat",
"Next Sentence": "Sonraki Cümle",
"Next Paragraph": "Sonraki Paragraf",
"Separate Cover Page": "Ayrı Kapak Sayfası"
"Separate Cover Page": "Ayrı Kapak Sayfası",
"Resize Notebook": "Defteri Yeniden Boyutlandır",
"Resize Sidebar": "Kenar Çubuğunu Yeniden Boyutlandır",
"Get Help from the Readest Community": "Readest Topluluğundan Yardım Alın",
"Remove cover image": "Kapak resmini kaldır",
"Bookshelf": "Kitaplık",
"View Menu": "Menüyü Görüntüle",
"Settings Menu": "Ayarlar Menüsü",
"View account details and quota": "Hesap detaylarını ve kotayı görüntüle",
"Library Header": "Kütüphane Başlığı",
"Book Content": "Kitap İçeriği",
"Footer Bar": "Alt Çubuk",
"Header Bar": "Üst Çubuk",
"View Options": "Görünüm Seçenekleri",
"Book Menu": "Kitap Menüsü",
"Search Options": "Arama Seçenekleri",
"Close": "Kapat",
"Delete Book Options": "Kitap Silme Seçenekleri",
"ON": "AÇIK",
"OFF": "KAPALI",
"Reading Progress": "Okuma İlerlemesi",
"Page Margin": "Sayfa Kenar Boşluğu",
"Remove Bookmark": "Yer İşareti Kaldır",
"Add Bookmark": "Yer İşareti Ekle",
"Books Content": "Kitap İçeriği",
"Jump to Location": "Konuma Atlama",
"Unpin Notebook": "Defteri Sabitlemeyi Kaldır",
"Pin Notebook": "Defteri Sabitle",
"Hide Search Bar": "Arama Çubuğunu Gizle",
"Show Search Bar": "Arama Çubuğunu Göster",
"On {{current}} of {{total}} page": "{{current}} / {{total}} sayfada",
"Section Title": "Bölüm Başlığı",
"Decrease": "Azalt",
"Increase": "Artır",
"Settings Panels": "Ayarlar Panelleri",
"Settings": "Ayarlar",
"Unpin Sidebar": "Kenar Çubuğunu Sabitlemeyi Kaldır",
"Pin Sidebar": "Kenar Çubuğunu Sabitle",
"Toggle Sidebar": "Kenar Çubuğunu Değiştir",
"Toggle Translation": "Çeviriyi Değiştir",
"Translation Disabled": "Çeviri Devre Dışı",
"Minimize": "Küçült",
"Maximize or Restore": "Büyüt veya Geri Yükle"
}
@@ -32,11 +32,11 @@
"Fast": "Швидко",
"Font": "Шрифт",
"Font & Layout": "Шрифт та макет",
"Font Face": "Накреслення шрифту",
"Font Face": "Налаштування шрифту",
"Font Family": "Сімейство шрифтів",
"Font Size": "Розмір шрифту",
"Full Justification": "Повне вирівнювання",
"Global Settings": "Глобальні налаштування",
"Global Settings": "Ґлобальні налаштування",
"Go Back": "Назад",
"Go Forward": "Вперед",
"Grass": "Трав'яний",
@@ -82,7 +82,6 @@
"Search...": "Пошук...",
"Select Book": "Вибрати книгу",
"Select Books": "Вибрати книги",
"Select Multiple Books": "Вибрати кілька книг",
"Sepia": "Сепія",
"Serif Font": "Шрифт із засічками",
"Show Book Details": "Показати деталі книги",
@@ -114,10 +113,11 @@
"Apply to This Book": "Застосувати лише до цієї книги",
"Unable to fetch the translation. Try again later.": "Не вдалося отримати переклад. Спробуйте пізніше.",
"Check Update": "Перевірити оновлення",
"Already the latest version": "У вас вже встановлена остання версія",
"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}}",
@@ -237,7 +237,7 @@
"Header & Footer": "Заголовок і нижній колонтитул",
"Apply also in Scrolled Mode": "Застосувати також у прокрученому режимі",
"A new version of Readest is available!": "Доступна нова версія Readest!",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Readest {{newVersion}} доступна (встановлено {{currentVersion}}).",
"Readest {{newVersion}} is available (installed version {{currentVersion}}).": "Нова версія Readest {{newVersion}} доступна. (Поточна {{currentVersion}}).",
"Download and install now?": "Завантажити та встановити зараз?",
"Downloading {{downloaded}} of {{contentLength}}": "Завантаженно {{downloaded}} з {{contentLength}}",
"Download finished": "Завантаження завершено",
@@ -339,7 +339,6 @@
"Left Margin (px)": "Лівий відступ (px)",
"Column Gap (%)": "Проміжок між стовпцями (%)",
"Always Show Status Bar": "Завжди показувати панель стану",
"Translation Not Available": "Переклад недоступний",
"Custom Content CSS": "Користуацький CSS вмісту",
"Enter CSS for book content styling...": "Введіть CSS для стилізації вмісту книги...",
"Custom Reader UI CSS": "CSS інтерфейсу",
@@ -490,7 +489,7 @@
"File Content (recommended)": "Вміст файлу (рекомендується)",
"File Name": "Ім'я файлу",
"Device Name": "Ім'я пристрою",
"Connect to your KOReader Sync server.": "Підключіться до свого сервера синхронізації KOReader.",
"Connect to your KOReader Sync server.": "Під'єдайте свій сервер синхронізації KOReader.",
"Server URL": "URL сервера",
"Username": "Ім'я користувача",
"Your Username": "Ваше ім'я користувача",
@@ -500,8 +499,8 @@
"Sync Conflict": "Конфлікт синхронізації",
"Sync reading progress from \"{{deviceName}}\"?": "Синхронізувати прогрес читання із \"{{deviceName}}\"?",
"another device": "інший пристрій",
"Local Progress": "Локальний прогрес",
"Remote Progress": "Прогрес у хмарі",
"Local Progress": "Локальний проґрес",
"Remote Progress": "Проґрес у хмарі",
"Page {{page}} of {{total}} ({{percentage}}%)": "Сторінка {{page}} з {{total}} ({{percentage}}%)",
"Current position": "Поточна позиція",
"Approximately page {{page}} of {{total}} ({{percentage}}%)": "Приблизно сторінка {{page}} із {{total}} ({{percentage}}%)",
@@ -509,26 +508,68 @@
"Failed to connect": "Не вдалося під'єднатися",
"Sync Server Connected": "Сервер синхронізації під'єднано",
"Sync as {{userDisplayName}}": "Синхронізувати як {{userDisplayName}}",
"Custom Fonts": "Користувацькі Шрифти",
"Cancel Delete": "Скасувати Видалення",
"Import Font": "Імпортувати Шрифт",
"Delete Font": "Видалити Шрифт",
"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": "Вибрати Шрифти",
"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": "Отримати прогрес",
"Push Progress": "Надіслати проґрес",
"Pull Progress": "Отримати проґрес",
"Previous Paragraph": "Попередній абзац",
"Previous Sentence": "Попереднє речення",
"Pause": "Пауза",
"Play": "Відтворити",
"Next Sentence": "Наступне речення",
"Next Paragraph": "Наступний абзац",
"Separate Cover Page": "Окрема сторінка обкладинки"
"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": "Збільшити або відновити"
}
@@ -82,7 +82,6 @@
"Search...": "Tìm kiếm...",
"Select Book": "Chọn sách",
"Select Books": "Chọn sách",
"Select Multiple Books": "Chọn nhiều sách",
"Sepia": "Nâu cổ",
"Serif Font": "Phông chữ có chân",
"Show Book Details": "Hiển thị chi tiết sách",
@@ -118,6 +117,7 @@
"Book Details": "Chi tiết sách",
"From Local File": "Từ tệp địa phương",
"TOC": "Mục lục",
"Table of Contents": "Mục lục",
"Book uploaded: {{title}}": "Sách đã tải lên: {{title}}",
"Failed to upload book: {{title}}": "Không thể tải lên sách: {{title}}",
"Book downloaded: {{title}}": "Sách đã tải về: {{title}}",
@@ -330,7 +330,6 @@
"Left Margin (px)": "Lề trái (px)",
"Column Gap (%)": "Khoảng cách cột (%)",
"Always Show Status Bar": "Luôn hiển thị thanh trạng thái",
"Translation Not Available": "Không có bản dịch",
"Custom Content CSS": "CSS nội dung",
"Enter CSS for book content styling...": "Nhập CSS cho phần nội dung sách...",
"Custom Reader UI CSS": "CSS giao diện đọc",
@@ -518,5 +517,47 @@
"Play": "Phát",
"Next Sentence": "Câu tiếp theo",
"Next Paragraph": "Đoạn tiếp theo",
"Separate Cover Page": "Trang Bìa Riêng"
"Separate Cover Page": "Trang Bìa Riêng",
"Resize Notebook": "Thay Đổi Kích Thước Sổ Ghi Chép",
"Resize Sidebar": "Thay Đổi Kích Thước Thanh Bên",
"Get Help from the Readest Community": "Nhận Trợ Giúp từ Cộng Đồng Readest",
"Remove cover image": "Xóa ảnh bìa",
"Bookshelf": "Kệ sách",
"View Menu": "Xem menu",
"Settings Menu": "Menu cài đặt",
"View account details and quota": "Xem chi tiết tài khoản và hạn mức",
"Library Header": "Tiêu đề thư viện",
"Book Content": "Nội dung sách",
"Footer Bar": "Thanh chân trang",
"Header Bar": "Thanh đầu trang",
"View Options": "Tùy chọn xem",
"Book Menu": "Menu sách",
"Search Options": "Tùy chọn tìm kiếm",
"Close": "Đóng",
"Delete Book Options": "Tùy chọn xóa sách",
"ON": "BẬT",
"OFF": "TẮT",
"Reading Progress": "Tiến độ đọc",
"Page Margin": "Lề trang",
"Remove Bookmark": "Xóa đánh dấu",
"Add Bookmark": "Thêm đánh dấu",
"Books Content": "Nội dung sách",
"Jump to Location": "Nhảy đến vị trí",
"Unpin Notebook": "Bỏ ghim sổ tay",
"Pin Notebook": "Ghim sổ tay",
"Hide Search Bar": "Ẩn thanh tìm kiếm",
"Show Search Bar": "Hiện thanh tìm kiếm",
"On {{current}} of {{total}} page": "Trên {{current}} của {{total}} trang",
"Section Title": "Tiêu đề phần",
"Decrease": "Giảm",
"Increase": "Tăng",
"Settings Panels": "Các bảng điều khiển cài đặt",
"Settings": "Cài đặt",
"Unpin Sidebar": "Bỏ ghim thanh bên",
"Pin Sidebar": "Ghim thanh bên",
"Toggle Sidebar": "Chuyển đổi thanh bên",
"Toggle Translation": "Chuyển đổi dịch thuật",
"Translation Disabled": "Dịch thuật đã tắt",
"Minimize": "Thu nhỏ",
"Maximize or Restore": "Tối đa hóa hoặc Khôi phục"
}
@@ -82,7 +82,6 @@
"Search...": "搜索...",
"Select Book": "选择书籍",
"Select Books": "选择书籍",
"Select Multiple Books": "选择多本书籍",
"Sepia": "旧韵",
"Serif Font": "衬线字体",
"Show Book Details": "显示书籍详情",
@@ -118,6 +117,7 @@
"Book Details": "书籍详情",
"From Local File": "从本地文件导入",
"TOC": "目录",
"Table of Contents": "目录",
"Book uploaded: {{title}}": "书籍已上传:{{title}}",
"Failed to upload book: {{title}}": "上传书籍失败:{{title}}",
"Book downloaded: {{title}}": "书籍已下载:{{title}}",
@@ -330,7 +330,6 @@
"Left Margin (px)": "左边距",
"Column Gap (%)": "列间距",
"Always Show Status Bar": "始终显示状态栏",
"Translation Not Available": "翻译不可用",
"Custom Content CSS": "自定义内容 CSS",
"Enter CSS for book content styling...": "输入书籍内容样式的CSS…",
"Custom Reader UI CSS": "自定义界面 CSS",
@@ -518,5 +517,47 @@
"Play": "播放",
"Next Sentence": "下一句",
"Next Paragraph": "下一段",
"Separate Cover Page": "单独封面页"
"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": "最大化或还原"
}
@@ -82,7 +82,6 @@
"Search...": "搜索...",
"Select Book": "選擇書籍",
"Select Books": "選擇書籍",
"Select Multiple Books": "選擇多本書籍",
"Sepia": "舊韻",
"Serif Font": "襯線字體",
"Show Book Details": "顯示書籍詳情",
@@ -118,6 +117,7 @@
"Book Details": "書籍詳情",
"From Local File": "從本地文件導入",
"TOC": "目錄",
"Table of Contents": "目錄",
"Book uploaded: {{title}}": "書籍已上傳:{{title}}",
"Failed to upload book: {{title}}": "書籍上傳失敗:{{title}}",
"Book downloaded: {{title}}": "書籍已下載:{{title}}",
@@ -330,7 +330,6 @@
"Left Margin (px)": "左邊距",
"Column Gap (%)": "欄間距",
"Always Show Status Bar": "始終顯示狀態欄",
"Translation Not Available": "翻譯不可用",
"Custom Content CSS": "自訂內容 CSS",
"Enter CSS for book content styling...": "輸入書籍內容樣式的CSS…",
"Custom Reader UI CSS": "自訂介面 CSS",
@@ -518,5 +517,47 @@
"Play": "播放",
"Next Sentence": "下一句",
"Next Paragraph": "下一段",
"Separate Cover Page": "單獨封面頁"
"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": "最大化或還原"
}
+21
View File
@@ -1,5 +1,26 @@
{
"releases": {
"0.9.81": {
"date": "2025-09-19",
"notes": [
"TTS: Added desktop common media control support when TTS is playing",
"PDF: Disabled swipe-up gesture for toggling the action bar when zoomed in",
"Fonts: Fixed an issue where embedded fonts in ebooks were occasionally not applied",
"Reader: Fixed an issue where newly created highlights sometimes would not appear",
"Reader: Enabled scrolling in the menu when the menu is too long"
]
},
"0.9.80": {
"date": "2025-09-19",
"notes": [
"Accessibility: Added support for popular screen readers on all platforms, making books accessible to readers with visual impairments",
"Sync: Progress is now synced across different versions of the same book by aggregated metadata",
"Library: Groups are now displayed in list view for easier organization",
"Library: Added delete confirmation when removing books from the context menu",
"Fonts: Added support for fonts with subfamilies when importing TTF/OTF font files",
"Layout: Prevented layout shift in Android when the virtual keyboard appears"
]
},
"0.9.78": {
"date": "2025-09-06",
"notes": [
+3 -1
View File
@@ -268,7 +268,9 @@ pub fn run() {
}
#[cfg(target_os = "linux")]
{
builder = builder.transparent(true);
builder = builder
.transparent(true)
.background_color(tauri::window::Color(0, 0, 0, 0));
}
builder
+3 -3
View File
@@ -67,7 +67,7 @@ export default function AuthPage() {
const router = useRouter();
const { login } = useAuth();
const { envConfig, appService } = useEnv();
const { isDarkMode, safeAreaInsets } = useThemeStore();
const { isDarkMode, safeAreaInsets, isRoundedWindow } = useThemeStore();
const { isTrafficLightVisible } = useTrafficLightStore();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [port, setPort] = useState<number | null>(null);
@@ -346,8 +346,7 @@ export default function AuthPage() {
className={clsx(
'bg-base-100 inset-0 flex select-none flex-col items-center overflow-hidden',
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
appService?.isLinuxApp && 'window-border',
appService?.hasRoundedWindow && 'rounded-window',
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
)}
>
<div
@@ -364,6 +363,7 @@ export default function AuthPage() {
)}
>
<button
aria-label={_('Go Back')}
onClick={handleGoBack}
className={clsx('btn btn-ghost h-12 min-h-12 w-12 p-0 sm:h-8 sm:min-h-8 sm:w-8')}
>
@@ -46,7 +46,7 @@ export default function ResetPasswordPage() {
return (
<div className='flex min-h-screen items-center justify-center'>
<div className='w-full max-w-md'>
<div className='w-full max-w-md p-8'>
<Auth
supabaseClient={supabase}
view='update_password'
+1 -12
View File
@@ -41,20 +41,9 @@ export const viewport = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<html lang='en'>
<head>
<title>{title}</title>
<script
dangerouslySetInnerHTML={{
__html: `
const themeMode = localStorage.getItem('themeMode');
const themeColor = localStorage.getItem('themeColor');
if (themeMode && themeColor) {
document.documentElement.setAttribute('data-theme', \`\${themeColor}-\${themeMode}\`);
}
`,
}}
/>
<meta
name='viewport'
content='minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover'
@@ -10,6 +10,7 @@ import { Book } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useAuth } from '@/context/AuthContext';
import { useRouter } from 'next/navigation';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
import { navigateToLogin } from '@/utils/nav';
@@ -40,24 +41,22 @@ const BookItem: React.FC<BookItemProps> = ({
handleBookDownload,
showBookDetailsModal,
}) => {
const _ = useTranslation();
const router = useRouter();
const { user } = useAuth();
const { appService } = useEnv();
const iconSize15 = useResponsiveSize(15);
const stopEvent = (e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault();
e.stopPropagation();
};
return (
<div
role='none'
className={clsx(
'book-item flex',
mode === 'grid' && 'h-full flex-col justify-end',
mode === 'list' && 'h-28 flex-row gap-4 overflow-hidden',
appService?.hasContextMenu ? 'cursor-pointer' : '',
)}
onClick={(e) => e.stopPropagation()}
>
<div
className={clsx(
@@ -115,13 +114,12 @@ const BookItem: React.FC<BookItemProps> = ({
<div className='flex items-center justify-center gap-x-2'>
{!appService?.isMobile && (
<button
aria-label={_('Show Book Details')}
className='show-detail-button -m-2 p-2 sm:opacity-0 sm:group-hover:opacity-100'
onPointerDown={(e) => stopEvent(e)}
onPointerUp={(e) => stopEvent(e)}
onPointerMove={(e) => stopEvent(e)}
onPointerCancel={(e) => stopEvent(e)}
onPointerLeave={(e) => stopEvent(e)}
onClick={() => showBookDetailsModal(book)}
onPointerDown={(e) => e.stopPropagation()}
onClick={() => {
showBookDetailsModal(book);
}}
>
<div className='pt-[2px] sm:pt-[1px]'>
<LiaInfoCircleSolid size={iconSize15} />
@@ -146,11 +144,7 @@ const BookItem: React.FC<BookItemProps> = ({
(!book.uploadedAt || (book.uploadedAt && !book.downloadedAt)) && (
<button
className='show-cloud-button -m-2 p-2'
onPointerDown={(e) => stopEvent(e)}
onPointerUp={(e) => stopEvent(e)}
onPointerMove={(e) => stopEvent(e)}
onPointerCancel={(e) => stopEvent(e)}
onPointerLeave={(e) => stopEvent(e)}
onPointerDown={(e) => e.stopPropagation()}
onClick={() => {
if (!user) {
navigateToLogin(router);
@@ -9,17 +9,19 @@ import { Book, BooksGroup } from '@/types/book';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useAutoFocus } from '@/hooks/useAutoFocus';
import { useSettingsStore } from '@/store/settingsStore';
import { useLibraryStore } from '@/store/libraryStore';
import { useTranslation } from '@/hooks/useTranslation';
import { navigateToLibrary, navigateToReader, showReaderWindow } from '@/utils/nav';
import { formatAuthors, formatTitle } from '@/utils/book';
import { eventDispatcher } from '@/utils/event';
import { isMd5 } from '@/utils/md5';
import Alert from '@/components/Alert';
import Spinner from '@/components/Spinner';
import ModalPortal from '@/components/ModalPortal';
import BookshelfItem, { generateGridItems, generateListItems } from './BookshelfItem';
import BookshelfItem, { generateBookshelfItems } from './BookshelfItem';
import GroupingModal from './GroupingModal';
interface BookshelfProps {
@@ -57,6 +59,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const { safeAreaInsets } = useThemeStore();
const [loading, setLoading] = useState(false);
const [showSelectModeActions, setShowSelectModeActions] = useState(false);
const [bookIdsToDelete, setBookIdsToDelete] = useState<string[]>([]);
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
const [showGroupingModal, setShowGroupingModal] = useState(false);
const [queryTerm, setQueryTerm] = useState<string | null>(null);
@@ -72,8 +75,9 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const { setCurrentBookshelf, setLibrary } = useLibraryStore();
const { setSelectedBooks, getSelectedBooks, toggleSelectedBook } = useLibraryStore();
const allBookshelfItems =
viewMode === 'grid' ? generateGridItems(libraryBooks) : generateListItems(libraryBooks);
const allBookshelfItems = generateBookshelfItems(libraryBooks);
const autofocusRef = useAutoFocus<HTMLDivElement>();
useEffect(() => {
if (isImportingBook.current) return;
@@ -190,8 +194,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const getBooksToDelete = () => {
const booksToDelete: Book[] = [];
const selectedBooks = getSelectedBooks();
selectedBooks.forEach((id) => {
bookIdsToDelete.forEach((id) => {
for (const book of libraryBooks.filter((book) => book.hash === id || book.groupId === id)) {
if (book && !book.deletedAt) {
booksToDelete.push(book);
@@ -211,6 +214,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
};
const deleteSelectedBooks = () => {
setBookIdsToDelete(getSelectedBooks());
setShowSelectModeActions(false);
setShowDeleteAlert(true);
};
@@ -220,6 +224,13 @@ const Bookshelf: React.FC<BookshelfProps> = ({
setShowGroupingModal(true);
};
const handleDeleteBooksIntent = (event: CustomEvent) => {
const { ids } = event.detail;
setBookIdsToDelete(ids);
setShowSelectModeActions(false);
setShowDeleteAlert(true);
};
const bookFilter = (item: Book, queryTerm: string) => {
if (item.deletedAt) return false;
const searchTerm = new RegExp(queryTerm, 'i');
@@ -296,17 +307,28 @@ const Bookshelf: React.FC<BookshelfProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSelectMode, isSelectAll, isSelectNone]);
useEffect(() => {
eventDispatcher.on('delete-books', handleDeleteBooksIntent);
return () => {
eventDispatcher.off('delete-books', handleDeleteBooksIntent);
};
}, []);
const selectedBooks = getSelectedBooks();
return (
<div className='bookshelf'>
<div
ref={autofocusRef}
tabIndex={-1}
className={clsx(
'bookshelf-items transform-wrapper',
'bookshelf-items transform-wrapper focus:outline-none',
viewMode === 'grid' && 'grid flex-1 grid-cols-3 gap-x-4 px-4 sm:gap-x-0 sm:px-2',
viewMode === 'grid' && 'sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12',
viewMode === 'list' && 'flex flex-col',
)}
role='main'
aria-label={_('Bookshelf')}
>
{filteredBookshelfItems.map((item) => (
<BookshelfItem
@@ -331,16 +353,16 @@ const Bookshelf: React.FC<BookshelfProps> = ({
/>
))}
{viewMode === 'grid' && !navBooksGroup && allBookshelfItems.length > 0 && (
<div
<button
aria-label={_('Import Books')}
className={clsx(
'border-1 bg-base-100 hover:bg-base-300/50 flex items-center justify-center',
'mx-0 my-4 aspect-[28/41] sm:mx-4',
)}
role='button'
onClick={handleImportBooks}
>
<PiPlus className='size-10' color='gray' />
</div>
</button>
)}
</div>
{loading && (
@@ -9,6 +9,7 @@ import { useTranslation } from '@/hooks/useTranslation';
import { useLongPress } from '@/hooks/useLongPress';
import { Menu, MenuItem } from '@tauri-apps/api/menu';
import { revealItemInDir } from '@tauri-apps/plugin-opener';
import { eventDispatcher } from '@/utils/event';
import { getOSPlatform } from '@/utils/misc';
import { throttle } from '@/utils/throttle';
import { LibraryCoverFitType, LibraryViewModeType } from '@/types/settings';
@@ -20,7 +21,7 @@ import GroupItem from './GroupItem';
export type BookshelfItem = Book | BooksGroup;
export const generateGridItems = (books: Book[]): (Book | BooksGroup)[] => {
export const generateBookshelfItems = (books: Book[]): (Book | BooksGroup)[] => {
const groups: BooksGroup[] = books.reduce((acc: BooksGroup[], book: Book) => {
if (book.deletedAt) return acc;
book.groupId = book.groupId || BOOK_UNGROUPED_ID;
@@ -49,10 +50,6 @@ export const generateGridItems = (books: Book[]): (Book | BooksGroup)[] => {
return [...ungroupedBooks, ...groupedBooks].sort((a, b) => b.updatedAt - a.updatedAt);
};
export const generateListItems = (books: Book[]): (Book | BooksGroup)[] => {
return books.filter((book) => !book.deletedAt).sort((a, b) => b.updatedAt - a.updatedAt);
};
export const generateGroupsList = (items: Book[]): BookGroupType[] => {
return items
.sort((a, b) => b.updatedAt - a.updatedAt)
@@ -98,7 +95,6 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
toggleSelection,
handleBookUpload,
handleBookDownload,
handleBookDelete,
handleSetSelectMode,
handleShowDetailsBook,
}) => {
@@ -216,7 +212,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
const deleteBookMenuItem = await MenuItem.new({
text: _('Delete'),
action: async () => {
await handleBookDelete(book);
eventDispatcher.dispatch('delete-books', { ids: [book.hash] });
},
});
const menu = await Menu.new();
@@ -245,9 +241,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
const deleteGroupMenuItem = await MenuItem.new({
text: _('Delete'),
action: async () => {
for (const book of group.books) {
await handleBookDelete(book);
}
eventDispatcher.dispatch('delete-books', { ids: [group.id] });
},
});
const menu = await Menu.new();
@@ -318,19 +312,34 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
[isSelectMode, handleSelectItem, handleOpenItem, handleContextMenu],
);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleOpenItem();
}
if (e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10')) {
e.preventDefault();
handleContextMenu();
}
};
return (
<div className={clsx(mode === 'list' && 'sm:hover:bg-base-300/50 px-4 sm:px-6')}>
<div
className={clsx(
'group',
'visible-focus-inset-2 group',
mode === 'grid' && 'sm:hover:bg-base-300/50 flex h-full flex-col px-0 py-4 sm:px-4',
mode === 'list' && 'border-base-300 flex flex-col border-b py-2',
appService?.isMobileApp && 'no-context-menu',
pressing ? (mode === 'grid' ? 'scale-95' : 'scale-98') : 'scale-100',
pressing && mode === 'grid' ? 'scale-95' : 'scale-100',
)}
role='button'
tabIndex={0}
aria-label={'format' in item ? item.title : item.name}
style={{
transition: 'transform 0.2s',
}}
onKeyDown={handleKeyDown}
{...handlers}
>
<div className='flex-grow'>
@@ -347,7 +356,12 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
showBookDetailsModal={showBookDetailsModal}
/>
) : (
<GroupItem group={item} isSelectMode={isSelectMode} groupSelected={itemSelected} />
<GroupItem
mode={mode}
group={item}
isSelectMode={isSelectMode}
groupSelected={itemSelected}
/>
)}
</div>
</div>
@@ -1,19 +1,99 @@
import clsx from 'clsx';
import { MdCheckCircle, MdCheckCircleOutline } from 'react-icons/md';
import { useEffect, useRef, useState } from 'react';
import { MdCheckCircle, MdCheckCircleOutline, MdChevronRight, MdChevronLeft } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { BooksGroup } from '@/types/book';
import { LibraryViewModeType } from '@/types/settings';
import BookCover from '@/components/BookCover';
interface GroupItemProps {
mode: LibraryViewModeType;
group: BooksGroup;
isSelectMode: boolean;
groupSelected: boolean;
}
const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, groupSelected }) => {
const GroupItem: React.FC<GroupItemProps> = ({ mode, group, isSelectMode, groupSelected }) => {
const { appService } = useEnv();
const iconSize15 = useResponsiveSize(15);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showLeftArrow, setShowLeftArrow] = useState(false);
const [showRightArrow, setShowRightArrow] = useState(false);
const checkScrollArrows = () => {
if (mode === 'list' && scrollContainerRef.current) {
const container = scrollContainerRef.current;
const hasOverflow = container.scrollWidth > container.clientWidth;
if (hasOverflow) {
const isAtStart = container.scrollLeft <= 5;
const isAtEnd = container.scrollLeft >= container.scrollWidth - container.clientWidth - 5;
setShowLeftArrow(!isAtStart);
setShowRightArrow(!isAtEnd);
} else {
setShowLeftArrow(false);
setShowRightArrow(false);
}
} else {
setShowLeftArrow(false);
setShowRightArrow(false);
}
};
useEffect(() => {
checkScrollArrows();
if (mode === 'list' && scrollContainerRef.current) {
const container = scrollContainerRef.current;
setTimeout(() => {
container.style.transform = 'translateZ(0)';
container.scrollLeft = 0;
}, 0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mode, group.books.length, scrollContainerRef.current]);
const handleScroll = () => {
checkScrollArrows();
};
const scrollLeft = () => {
if (scrollContainerRef.current) {
const container = scrollContainerRef.current;
const scrollAmount = container.clientWidth * 0.5;
const currentScroll = container.scrollLeft;
const targetScroll = Math.max(0, currentScroll - scrollAmount);
container.scrollTo({ left: targetScroll, behavior: 'smooth' });
}
};
const scrollRight = () => {
if (scrollContainerRef.current) {
const container = scrollContainerRef.current;
const scrollAmount = container.clientWidth * 0.5;
const currentScroll = container.scrollLeft;
const maxScroll = container.scrollWidth - container.clientWidth;
const targetScroll = Math.min(maxScroll, currentScroll + scrollAmount);
container.scrollTo({ left: targetScroll, behavior: 'smooth' });
}
};
const stopEvent = (e: React.MouseEvent | React.TouchEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleLeftArrowClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
scrollLeft();
};
const handleRightArrowClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
scrollRight();
};
return (
<div
@@ -22,14 +102,99 @@ const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, groupSelecte
appService?.hasContextMenu ? 'cursor-pointer' : '',
)}
>
<div className='bg-base-100 relative flex aspect-[28/41] items-center justify-center overflow-hidden p-2 shadow-md'>
<div className='grid w-full grid-cols-2 grid-rows-2 gap-1 overflow-hidden'>
{group.books.slice(0, 4).map((book) => (
<div key={book.hash} className='relative aspect-[28/41] h-full w-full'>
<BookCover book={book} isPreview />
<div
className={clsx(
'relative flex overflow-hidden',
mode === 'grid' && 'bg-base-100 aspect-[28/41] items-center justify-center p-2 shadow-md',
mode === 'list' && 'h-32 items-center justify-start gap-4 py-2',
)}
>
<div
className={clsx(
mode === 'grid' && 'flex h-full w-full',
mode === 'list' && 'relative min-w-0 max-w-[85%]',
)}
>
<div
ref={mode === 'list' ? scrollContainerRef : undefined}
className={clsx(
mode === 'grid' && 'grid w-full grid-cols-2 grid-rows-2 gap-1 overflow-hidden',
mode === 'list' && 'flex h-28 gap-2 overflow-x-auto overflow-y-hidden',
)}
style={
mode === 'list'
? {
scrollbarWidth: 'none',
msOverflowStyle: 'none',
WebkitOverflowScrolling: 'touch',
transform: 'translateZ(0)',
willChange: 'scroll-position',
}
: undefined
}
onScroll={mode === 'list' ? handleScroll : undefined}
>
{group.books.slice(0, mode === 'grid' ? 4 : undefined).map((book) => (
<div
key={book.hash}
className={clsx(
'relative aspect-[28/41] h-full',
mode === 'grid' && 'w-full',
mode === 'list' && 'flex-shrink-0',
)}
>
<BookCover book={book} isPreview />
</div>
))}
</div>
{mode === 'list' && showLeftArrow && (
<div className='absolute left-[-0.5px] top-0 h-full w-12'>
<div className='from-base-200/85 via-base-200/20 absolute inset-0 bg-gradient-to-r to-transparent'></div>
<button
onClick={handleLeftArrowClick}
onPointerDown={(e) => stopEvent(e)}
onPointerUp={(e) => stopEvent(e)}
onPointerMove={(e) => stopEvent(e)}
onPointerCancel={(e) => stopEvent(e)}
onPointerLeave={(e) => stopEvent(e)}
className='absolute left-2 top-1/2 -translate-y-1/2 transition-all duration-200 hover:scale-110'
>
<div className='bg-base-100 border-base-content/10 hover:border-base-content/30 rounded-full border p-1 shadow-sm transition-colors duration-200'>
<MdChevronLeft
size={16}
className='text-base-content/50 hover:text-base-content/70'
/>
</div>
</button>
</div>
))}
)}
{mode === 'list' && showRightArrow && (
<div className='absolute right-[-0.5px] top-0 h-full w-12'>
<div className='from-base-200/85 via-base-200/20 absolute inset-0 bg-gradient-to-l to-transparent'></div>
<button
onClick={handleRightArrowClick}
onPointerDown={(e) => stopEvent(e)}
onPointerUp={(e) => stopEvent(e)}
onPointerMove={(e) => stopEvent(e)}
onPointerCancel={(e) => stopEvent(e)}
onPointerLeave={(e) => stopEvent(e)}
className='absolute right-2 top-1/2 -translate-y-1/2 transition-all duration-200 hover:scale-110'
>
<div className='bg-base-100 border-base-content/10 hover:border-base-content/30 rounded-full border p-1 shadow-sm transition-colors duration-200'>
<MdChevronRight
size={16}
className='text-base-content/50 hover:text-base-content/70'
/>
</div>
</button>
</div>
)}
</div>
{mode === 'list' && (
<div className='text-base-content/75 w-28 min-w-24 max-w-40 overflow-hidden text-ellipsis text-base font-semibold'>
{group.name}
</div>
)}
{groupSelected && (
<div className='absolute inset-0 bg-black opacity-30 transition-opacity duration-300'></div>
)}
@@ -43,14 +208,16 @@ const GroupItem: React.FC<GroupItemProps> = ({ group, isSelectMode, groupSelecte
</div>
)}
</div>
<div className={clsx('flex w-full flex-col pt-2')}>
<div className='min-w-0 flex-1'>
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-xs font-semibold'>
{group.name}
</h4>
{mode === 'grid' && (
<div className={clsx('flex w-full flex-col pt-2')}>
<div className='min-w-0 flex-1'>
<h4 className='block overflow-hidden text-ellipsis whitespace-nowrap text-xs font-semibold'>
{group.name}
</h4>
</div>
<div className='placeholder' style={{ height: `${iconSize15}px` }}></div>
</div>
<div className='placeholder' style={{ height: `${iconSize15}px` }}></div>
</div>
)}
</div>
);
};
@@ -150,29 +150,26 @@ const GroupingModal: React.FC<GroupingModalProps> = ({
<h2 className='text-center text-lg font-bold'>{_('Group Books')}</h2>
<div className={clsx('mt-4 grid grid-cols-1 gap-2 text-base md:grid-cols-2')}>
{isSelectedBooksHasGroup && (
<div
<button
onClick={handleRemoveFromGroup}
role='button'
className='flex items-center space-x-2 p-2 text-blue-500'
>
<HiOutlineFolderRemove size={iconSize} />
<span>{_('Remove From Group')}</span>
</div>
</button>
)}
<div
<button
onClick={handleCreateGroup}
role='button'
className='flex items-center space-x-2 p-2 text-blue-500'
>
<HiOutlineFolderAdd size={iconSize} />
<span>{_('Create New Group')}</span>
</div>
</button>
</div>
{showInput && (
<div className='mt-4 flex items-center gap-2'>
<input
type='text'
autoFocus
ref={editorRef}
value={editGroupName}
onChange={(e) => setEditGroupName(e.target.value)}
@@ -2,6 +2,7 @@ import clsx from 'clsx';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import MenuItem from '@/components/MenuItem';
import Menu from '@/components/Menu';
interface ImportMenuProps {
setIsDropdownOpen?: (open: boolean) => void;
@@ -18,15 +19,15 @@ const ImportMenu: React.FC<ImportMenuProps> = ({ setIsDropdownOpen, onImportBook
};
return (
<ul
tabIndex={-1}
<Menu
label={_('Import Books')}
className={clsx(
'dropdown-content bg-base-100 menu rounded-box z-[1] mt-3 w-52 p-2 shadow',
'dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow',
appService?.isMobile ? 'no-triangle' : 'dropdown-center',
)}
>
<MenuItem label={_('From Local File')} onClick={handleImportBooks} />
</ul>
</Menu>
);
};
@@ -172,34 +172,27 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
)}
<span className='bg-base-content/50 mx-2 h-4 w-[0.5px]'></span>
<Dropdown
label={_('Import Books')}
className={clsx(
'exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center',
appService?.isMobile ? 'dropdown-end' : 'dropdown-center',
)}
buttonClassName='p-0 h-6 min-h-6 w-6 flex items-center justify-center'
toggleButton={
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Import Books')}>
<PiPlus className='m-0.5 h-5 w-5' />
</div>
}
toggleButton={<PiPlus role='none' className='m-0.5 h-5 w-5' />}
>
<ImportMenu onImportBooks={onImportBooks} />
</Dropdown>
{appService?.isMobile ? null : (
<button
onClick={onToggleSelectMode}
aria-label={_('Select Multiple Books')}
aria-label={_('Select Books')}
title={_('Select Books')}
className='h-6'
>
<div
className='lg:tooltip lg:tooltip-bottom cursor-pointer'
data-tip={_('Select Books')}
>
<PiSelectionAllDuotone
role='button'
className={`h-6 w-6 ${isSelectMode ? 'fill-gray-400' : 'fill-gray-500'}`}
/>
</div>
<PiSelectionAllDuotone
role='button'
className={`h-6 w-6 ${isSelectMode ? 'fill-gray-400' : 'fill-gray-500'}`}
/>
</button>
)}
</div>
@@ -224,16 +217,18 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
) : (
<div className='flex h-full items-center gap-x-2 sm:gap-x-4'>
<Dropdown
label={_('View Menu')}
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeCircle size={iconSize18} />}
toggleButton={<PiDotsThreeCircle role='none' size={iconSize18} />}
>
<ViewMenu />
</Dropdown>
<Dropdown
label={_('Settings Menu')}
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<MdOutlineMenu size={iconSize18} />}
toggleButton={<MdOutlineMenu role='none' size={iconSize18} />}
>
<SettingsMenu />
</Dropdown>
@@ -21,12 +21,16 @@ const ReadingProgress: React.FC<ReadingProgressProps> = memo(
({ book }) => {
const progressPercentage = useMemo(() => getProgressPercentage(book), [book]);
if (progressPercentage === null) {
if (progressPercentage === null || Number.isNaN(progressPercentage)) {
return null;
}
return (
<div className='text-neutral-content/70 flex justify-between text-xs'>
<div
className='text-neutral-content/70 flex justify-between text-xs'
role='status'
aria-label={`${progressPercentage}%`}
>
<span>{progressPercentage}%</span>
</div>
);
@@ -3,7 +3,6 @@ import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { PiUserCircle } from 'react-icons/pi';
import { PiUserCircleCheck } from 'react-icons/pi';
import { MdCheck } from 'react-icons/md';
import { TbSunMoon } from 'react-icons/tb';
import { BiMoon, BiSun } from 'react-icons/bi';
@@ -23,6 +22,7 @@ import { optInTelemetry, optOutTelemetry } from '@/utils/telemetry';
import UserAvatar from '@/components/UserAvatar';
import MenuItem from '@/components/MenuItem';
import Quota from '@/components/Quota';
import Menu from '@/components/Menu';
interface SettingsMenuProps {
setIsDropdownOpen?: (isOpen: boolean) => void;
@@ -104,7 +104,6 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
setSettings(settings);
saveSettings(envConfig, settings);
setIsAlwaysShowStatusBar(settings.alwaysShowStatusBar);
setIsDropdownOpen?.(false);
};
const toggleAutoUploadBooks = () => {
@@ -166,10 +165,16 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
const avatarUrl = user?.user_metadata?.['picture'] || user?.user_metadata?.['avatar_url'];
const userFullName = user?.user_metadata?.['full_name'];
const userDisplayName = userFullName ? userFullName.split(' ')[0] : null;
const themeModeLabel =
themeMode === 'dark'
? _('Dark Mode')
: themeMode === 'light'
? _('Light Mode')
: _('Auto Mode');
return (
<div
tabIndex={0}
<Menu
label={_('Settings Menu')}
className={clsx(
'settings-menu dropdown-content no-triangle border-base-100',
'z-20 mt-2 max-w-[90vw] shadow-2xl',
@@ -183,6 +188,7 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
: _('Logged in')
}
labelClass='!max-w-40'
aria-label={_('View account details and quota')}
Icon={
avatarUrl ? (
<UserAvatar url={avatarUrl} size={iconSize} DefaultIcon={PiUserCircleCheck} />
@@ -191,10 +197,10 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
)
}
>
<ul>
<div onClick={handleUserProfile} className='cursor-pointer'>
<ul className='flex flex-col'>
<button onClick={handleUserProfile} className='w-full'>
<Quota quotas={quotas} labelClassName='h-10 pl-3 pr-2' />
</div>
</button>
<MenuItem label={_('Account')} noIcon onClick={handleUserProfile} />
</ul>
</MenuItem>
@@ -203,71 +209,61 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
)}
<MenuItem
label={_('Auto Upload Books to Cloud')}
Icon={isAutoUpload ? MdCheck : undefined}
toggled={isAutoUpload}
onClick={toggleAutoUploadBooks}
/>
{isTauriAppPlatform() && !appService?.isMobile && (
<MenuItem
label={_('Auto Import on File Open')}
Icon={isAutoImportBooksOnOpen ? MdCheck : undefined}
toggled={isAutoImportBooksOnOpen}
onClick={toggleAutoImportBooksOnOpen}
/>
)}
{isTauriAppPlatform() && (
<MenuItem
label={_('Open Last Book on Start')}
Icon={isOpenLastBooks ? MdCheck : undefined}
toggled={isOpenLastBooks}
onClick={toggleOpenLastBooks}
/>
)}
{appService?.hasUpdater && (
<MenuItem
label={_('Check Updates on Start')}
Icon={isAutoCheckUpdates ? MdCheck : undefined}
toggled={isAutoCheckUpdates}
onClick={toggleAutoCheckUpdates}
/>
)}
<hr className='border-base-200 my-1' />
<hr aria-hidden='true' className='border-base-200 my-1' />
{appService?.hasWindow && (
<MenuItem
label={_('Open Book in New Window')}
Icon={settings.openBookInNewWindow ? MdCheck : undefined}
toggled={settings.openBookInNewWindow}
onClick={toggleOpenInNewWindow}
/>
)}
{appService?.hasWindow && <MenuItem label={_('Fullscreen')} onClick={handleFullScreen} />}
{appService?.hasWindow && (
<MenuItem
label={_('Always on Top')}
Icon={isAlwaysOnTop ? MdCheck : undefined}
onClick={toggleAlwaysOnTop}
/>
<MenuItem label={_('Always on Top')} toggled={isAlwaysOnTop} onClick={toggleAlwaysOnTop} />
)}
{appService?.isMobileApp && (
<MenuItem
label={_('Always Show Status Bar')}
Icon={isAlwaysShowStatusBar ? MdCheck : undefined}
toggled={isAlwaysShowStatusBar}
onClick={toggleAlwaysShowStatusBar}
/>
)}
<MenuItem
label={_('Keep Screen Awake')}
Icon={isScreenWakeLock ? MdCheck : undefined}
toggled={isScreenWakeLock}
onClick={toggleScreenWakeLock}
/>
<MenuItem label={_('Reload Page')} onClick={handleReloadPage} />
<MenuItem
label={
themeMode === 'dark'
? _('Dark Mode')
: themeMode === 'light'
? _('Light Mode')
: _('Auto Mode')
}
label={themeModeLabel}
Icon={themeMode === 'dark' ? BiMoon : themeMode === 'light' ? BiSun : TbSunMoon}
onClick={cycleThemeMode}
/>
<hr className='border-base-200 my-1' />
<hr aria-hidden='true' className='border-base-200 my-1' />
{user && userPlan === 'free' && !appService?.isIOSApp && (
<MenuItem label={_('Upgrade to Readest Premium')} onClick={handleUpgrade} />
)}
@@ -276,10 +272,10 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
<MenuItem
label={_('Help improve Readest')}
description={isTelemetryEnabled ? _('Sharing anonymized statistics') : ''}
Icon={isTelemetryEnabled ? MdCheck : undefined}
toggled={isTelemetryEnabled}
onClick={toggleTelemetry}
/>
</div>
</Menu>
);
};
@@ -1,12 +1,12 @@
import React from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { MdCheck } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { LibraryCoverFitType, LibrarySortByType, LibraryViewModeType } from '@/types/settings';
import { navigateToLibrary } from '@/utils/nav';
import MenuItem from '@/components/MenuItem';
import Menu from '@/components/Menu';
interface ViewMenuProps {
setIsDropdownOpen?: (isOpen: boolean) => void;
@@ -92,20 +92,20 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
};
return (
<div
tabIndex={0}
className='settings-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'
<Menu
label={_('View Menu')}
className='view-menu dropdown-content no-triangle border-base-100 z-20 mt-2 shadow-2xl'
>
{viewOptions.map((option) => (
<MenuItem
key={option.value}
label={option.label}
buttonClass='h-8'
Icon={viewMode === option.value ? MdCheck : undefined}
toggled={viewMode === option.value}
onClick={() => handleSetViewMode(option.value as LibraryViewModeType)}
/>
))}
<hr className='border-base-200 my-1' />
<hr aria-hidden='true' className='border-base-200 my-1' />
<MenuItem
label={_('Book Covers')}
buttonClass='h-8'
@@ -117,11 +117,11 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
key={option.value}
label={option.label}
buttonClass='h-8'
Icon={coverFit === option.value ? MdCheck : undefined}
toggled={coverFit === option.value}
onClick={() => handleToggleCropCovers(option.value as LibraryCoverFitType)}
/>
))}
<hr className='border-base-200 my-1' />
<hr aria-hidden='true' className='border-base-200 my-1' />
<MenuItem
label={_('Sort by...')}
buttonClass='h-8'
@@ -133,21 +133,21 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
key={option.value}
label={option.label}
buttonClass='h-8'
Icon={sortBy === option.value ? MdCheck : undefined}
toggled={sortBy === option.value}
onClick={() => handleSetSortBy(option.value as LibrarySortByType)}
/>
))}
<hr className='border-base-200 my-1' />
<hr aria-hidden='true' className='border-base-200 my-1' />
{sortingOptions.map((option) => (
<MenuItem
key={option.value.toString()}
label={option.label}
buttonClass='h-8'
Icon={isAscending === option.value ? MdCheck : undefined}
toggled={isAscending === option.value}
onClick={() => handleSetSortAscending(option.value)}
/>
))}
</div>
</Menu>
);
};
+12 -6
View File
@@ -74,7 +74,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
} = useLibraryStore();
const _ = useTranslation();
const { selectFiles } = useFileSelector(appService, _);
const { safeAreaInsets: insets } = useThemeStore();
const { safeAreaInsets: insets, isRoundedWindow } = useThemeStore();
const { settings, setSettings, saveSettings } = useSettingsStore();
const [loading, setLoading] = useState(false);
const isInitiating = useRef(false);
@@ -619,7 +619,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
};
if (!appService || !insets || checkOpenWithBooks || checkLastOpenBooks) {
return <div className='bg-base-200 h-[100vh]' />;
return <div className={clsx('h-[100vh]', !appService?.isLinuxApp && 'bg-base-200')} />;
}
const showBookshelf = libraryLoaded || libraryBooks.length > 0;
@@ -627,14 +627,19 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
return (
<div
ref={pageRef}
aria-label='Your Library'
className={clsx(
'library-page bg-base-200 text-base-content flex select-none flex-col overflow-hidden',
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
appService?.isLinuxApp && 'window-border',
appService?.hasRoundedWindow && 'rounded-window',
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
)}
>
<div className='top-0 z-40 w-full'>
<div
className='top-0 z-40 w-full'
role='banner'
tabIndex={-1}
aria-label={_('Library Header')}
>
<LibraryHeader
isSelectMode={isSelectMode}
isSelectAll={isSelectAll}
@@ -653,6 +658,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
(libraryBooks.some((book) => !book.deletedAt) ? (
<OverlayScrollbarsComponent
defer
aria-label=''
ref={osRef}
className='flex-grow'
options={{ scrollbars: { autoHide: 'scroll' } }}
@@ -730,7 +736,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
const LibraryPage = () => {
return (
<Suspense fallback={<div className='bg-base-200 h-[100vh]' />}>
<Suspense fallback={<div className='h-[100vh]' />}>
<LibraryPageWithSearchParams />
</Suspense>
);
@@ -103,8 +103,7 @@ const BookmarkToggler: React.FC<BookmarkTogglerProps> = ({ bookKey }) => {
)
}
onClick={toggleBookmark}
tooltip={_('Bookmark')}
tooltipDirection='bottom'
label={isBookmarked ? _('Remove Bookmark') : _('Add Bookmark')}
></Button>
);
};
@@ -7,6 +7,7 @@ import { useSettingsStore } from '@/store/settingsStore';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useTranslation } from '@/hooks/useTranslation';
import { getGridTemplate, getInsetEdges } from '@/utils/grid';
import { getViewInsets } from '@/utils/insets';
import FoliateViewer from './FoliateViewer';
@@ -27,6 +28,7 @@ interface BooksGridProps {
}
const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { getConfig, getBookData } = useBookDataStore();
const { getProgress, getViewState, getViewSettings } = useReaderStore();
@@ -75,6 +77,8 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
gridTemplateColumns: gridTemplate.columns,
gridTemplateRows: gridTemplate.rows,
}}
role='main'
aria-label={_('Books Content')}
>
{bookKeys.map((bookKey, index) => {
const bookData = getBookData(bookKey);
@@ -181,7 +185,6 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
{showFooter && (
<ProgressInfoView
bookKey={bookKey}
bookFormat={book.format}
section={section}
pageinfo={pageinfo}
timeinfo={timeinfo}
@@ -15,6 +15,8 @@ import { usePagination } from '../hooks/usePagination';
import { useFoliateEvents } from '../hooks/useFoliateEvents';
import { useProgressSync } from '../hooks/useProgressSync';
import { useProgressAutoSave } from '../hooks/useProgressAutoSave';
import { useAutoFocus } from '@/hooks/useAutoFocus';
import { useTranslation } from '@/hooks/useTranslation';
import { useKOSync } from '../hooks/useKOSync';
import {
applyFixedlayoutStyles,
@@ -45,6 +47,7 @@ import { lockScreenOrientation } from '@/utils/bridge';
import { useTextTranslation } from '../hooks/useTextTranslation';
import { manageSyntaxHighlighting } from '@/utils/highlightjs';
import { getViewInsets } from '@/utils/insets';
import { removeTabIndex } from '@/utils/a11y';
import Spinner from '@/components/Spinner';
import KOSyncConflictResolver from './KOSyncResolver';
@@ -61,11 +64,12 @@ const FoliateViewer: React.FC<{
gridInsets: Insets;
contentInsets: Insets;
}> = ({ bookKey, bookDoc, config, gridInsets, contentInsets: insets }) => {
const _ = useTranslation();
const { appService, envConfig } = useEnv();
const { themeCode, isDarkMode } = useThemeStore();
const { settings } = useSettingsStore();
const { loadCustomFonts, getLoadedFonts } = useCustomFontStore();
const { getView, setView: setFoliateView, setProgress } = useReaderStore();
const { getView, setView: setFoliateView, setViewInited, setProgress } = useReaderStore();
const { getViewState, getViewSettings, setViewSettings } = useReaderStore();
const { getParallels } = useParallelViewStore();
const { getBookData } = useBookDataStore();
@@ -80,6 +84,8 @@ const FoliateViewer: React.FC<{
const [loading, setLoading] = useState(false);
const docLoaded = useRef(false);
useAutoFocus<HTMLDivElement>({ ref: containerRef });
useEffect(() => {
const timer = setTimeout(() => setToastMessage(''), 2000);
return () => clearTimeout(timer);
@@ -160,6 +166,8 @@ const FoliateViewer: React.FC<{
applyImageStyle(detail.doc);
removeTabIndex(detail.doc);
// Inline scripts in tauri platforms are not executed by default
if (viewSettings.allowScript && isTauriAppPlatform()) {
evalInlineScripts(detail.doc);
@@ -222,7 +230,7 @@ const FoliateViewer: React.FC<{
const { handlePageFlip, handleContinuousScroll } = usePagination(bookKey, viewRef, containerRef);
const mouseHandlers = useMouseEvent(bookKey, handlePageFlip, handleContinuousScroll);
const touchHandlers = useTouchEvent(bookKey, handleContinuousScroll);
const touchHandlers = useTouchEvent(bookKey, handlePageFlip, handleContinuousScroll);
useFoliateEvents(viewRef.current, {
onLoad: docLoadHandler,
@@ -300,6 +308,7 @@ const FoliateViewer: React.FC<{
if (bookDoc?.rendition?.layout === 'pre-paginated') {
view.renderer.setAttribute('zoom', viewSettings.zoomMode);
view.renderer.setAttribute('spread', viewSettings.spreadMode);
view.renderer.setAttribute('scale-factor', viewSettings.zoomLevel);
} else {
view.renderer.setAttribute('max-column-count', maxColumnCount);
view.renderer.setAttribute('max-inline-size', `${maxInlineSize}px`);
@@ -313,6 +322,7 @@ const FoliateViewer: React.FC<{
} else {
await view.goToFraction(0);
}
setViewInited(bookKey, true);
};
openBook();
@@ -405,7 +415,10 @@ const FoliateViewer: React.FC<{
<>
<div
ref={containerRef}
className='foliate-viewer h-[100%] w-[100%]'
tabIndex={-1}
role='document'
aria-label={_('Book Content')}
className='foliate-viewer h-[100%] w-[100%] focus:outline-none'
{...mouseHandlers}
{...touchHandlers}
/>
@@ -173,6 +173,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
return (
<>
<div
role='none'
className={clsx(
'absolute bottom-0 left-0 z-10 hidden w-full sm:flex sm:h-[52px]',
// show scroll bar when vertical and scrolled in desktop
@@ -182,8 +183,10 @@ const FooterBar: React.FC<FooterBarProps> = ({
onTouchStart={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
/>
<div
role='group'
aria-label={_('Footer Bar')}
className={clsx(
'footer-bar shadow-xs bottom-0 z-30 flex w-full flex-col',
'footer-bar shadow-xs bottom-0 z-10 flex w-full flex-col',
'sm:h-[52px] sm:justify-center',
'sm:bg-base-100 border-base-300/50 border-t sm:border-none',
'transition-[opacity,transform] duration-300',
@@ -199,8 +202,8 @@ const FooterBar: React.FC<FooterBarProps> = ({
: `pointer-events-none translate-y-full opacity-0 sm:translate-y-0`,
)}
dir={viewSettings?.rtl ? 'rtl' : 'ltr'}
onFocus={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
onMouseLeave={() => window.innerWidth >= 640 && setHoveredBookKey('')}
aria-hidden={!isVisible}
>
{/* Mobile footer bar */}
<div
@@ -216,6 +219,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
>
<div className='flex w-full items-center justify-between gap-x-6'>
<Slider
label={_('Reading Progress')}
heightPx={sliderHeight}
bubbleLabel={`${Math.round(progressFraction * 100)}%`}
initialValue={progressValid ? progressFraction * 100 : 0}
@@ -226,34 +230,34 @@ const FooterBar: React.FC<FooterBarProps> = ({
<Button
icon={viewSettings?.rtl ? <RiArrowRightDoubleLine /> : <RiArrowLeftDoubleLine />}
onClick={viewSettings?.rtl ? handleGoNextSection : handleGoPrevSection}
tooltip={viewSettings?.rtl ? _('Next Section') : _('Previous Section')}
label={viewSettings?.rtl ? _('Next Section') : _('Previous Section')}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowRightSLine /> : <RiArrowLeftSLine />}
onClick={viewSettings?.rtl ? handleGoNextPage : handleGoPrevPage}
tooltip={viewSettings?.rtl ? _('Next Page') : _('Previous Page')}
label={viewSettings?.rtl ? _('Next Page') : _('Previous Page')}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
onClick={handleGoBack}
tooltip={_('Go Back')}
label={_('Go Back')}
disabled={!view?.history.canGoBack}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowGoBackLine /> : <RiArrowGoForwardLine />}
onClick={handleGoForward}
tooltip={_('Go Forward')}
label={_('Go Forward')}
disabled={!view?.history.canGoForward}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowLeftSLine /> : <RiArrowRightSLine />}
onClick={viewSettings?.rtl ? handleGoPrevPage : handleGoNextPage}
tooltip={viewSettings?.rtl ? _('Previous Page') : _('Next Page')}
label={viewSettings?.rtl ? _('Previous Page') : _('Next Page')}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowLeftDoubleLine /> : <RiArrowRightDoubleLine />}
onClick={viewSettings?.rtl ? handleGoPrevSection : handleGoNextSection}
tooltip={viewSettings?.rtl ? _('Previous Section') : _('Next Section')}
label={viewSettings?.rtl ? _('Previous Section') : _('Next Section')}
/>
</div>
</div>
@@ -269,6 +273,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
}}
>
<Slider
label={_('Font Size')}
initialValue={viewSettings?.defaultFontSize ?? 16}
bubbleLabel={`${viewSettings?.defaultFontSize ?? 16}`}
minLabel='A'
@@ -281,6 +286,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
/>
<div className='flex w-full items-center justify-between gap-x-6'>
<Slider
label={_('Page Margin')}
initialValue={getMarginProgressValue(
viewSettings?.marginTopPx ?? 44,
viewSettings?.gapPercent ?? 5,
@@ -292,6 +298,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
onChange={handleMarginChange}
/>
<Slider
label={_('Line Spacing')}
initialValue={(viewSettings?.lineHeight ?? 1.6) * 10}
bubbleElement={<RxLineHeight size={marginIconSize} />}
minLabel={_('Small')}
@@ -311,15 +318,22 @@ const FooterBar: React.FC<FooterBarProps> = ({
}}
>
<Button
label={_('Table of Contents')}
icon={<TOCIcon size={tocIconSize} className='' />}
onClick={() => handleSetActionTab('toc')}
/>
<Button icon={<NoteIcon className='' />} onClick={() => handleSetActionTab('note')} />
<Button
label={_('Notes')}
icon={<NoteIcon className='' />}
onClick={() => handleSetActionTab('note')}
/>
<Button
label={_('Reading Progress')}
icon={<SliderIcon className={clsx(actionTab === 'progress' && 'text-blue-500')} />}
onClick={() => handleSetActionTab('progress')}
/>
<Button
label={_('Font & Layout')}
icon={
<FontIcon
size={fontIconSize}
@@ -329,6 +343,7 @@ const FooterBar: React.FC<FooterBarProps> = ({
onClick={() => handleSetActionTab('font')}
/>
<Button
label={_('Speak')}
icon={<TTSIcon className={ttsEnabled ? 'text-blue-500' : ''} />}
onClick={() => handleSetActionTab('tts')}
/>
@@ -343,33 +358,40 @@ const FooterBar: React.FC<FooterBarProps> = ({
<Button
icon={viewSettings?.rtl ? <RiArrowRightDoubleLine /> : <RiArrowLeftDoubleLine />}
onClick={viewSettings?.rtl ? handleGoNextSection : handleGoPrevSection}
tooltip={viewSettings?.rtl ? _('Next Section') : _('Previous Section')}
label={viewSettings?.rtl ? _('Next Section') : _('Previous Section')}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowRightSLine /> : <RiArrowLeftSLine />}
onClick={viewSettings?.rtl ? handleGoNextPage : handleGoPrevPage}
tooltip={viewSettings?.rtl ? _('Next Page') : _('Previous Page')}
label={viewSettings?.rtl ? _('Next Page') : _('Previous Page')}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowGoForwardLine /> : <RiArrowGoBackLine />}
onClick={handleGoBack}
tooltip={_('Go Back')}
label={_('Go Back')}
disabled={!view?.history.canGoBack}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowGoBackLine /> : <RiArrowGoForwardLine />}
onClick={handleGoForward}
tooltip={_('Go Forward')}
label={_('Go Forward')}
disabled={!view?.history.canGoForward}
/>
<span className='mx-2 text-center text-sm'>
{progressValid ? `${Math.round(progressFraction * 100)}%` : ''}
</span>
{progressValid && (
<span
title={_('Reading Progress')}
aria-label={_('Reading Progress') + ': ' + `${Math.round(progressFraction * 100)}%`}
className='mx-2 text-center text-sm'
>
<span aria-hidden='true'>{`${Math.round(progressFraction * 100)}%`}</span>
</span>
)}
<input
type='range'
className='text-base-content mx-2 min-w-0 flex-1'
min={0}
max={100}
aria-label={_('Jump to Location')}
value={progressValid ? progressFraction * 100 : 0}
onChange={(e) =>
handleProgressChange(parseInt((e.target as HTMLInputElement).value, 10))
@@ -378,17 +400,17 @@ const FooterBar: React.FC<FooterBarProps> = ({
<Button
icon={<FaHeadphones className={ttsEnabled ? 'text-blue-500' : ''} />}
onClick={handleSpeakText}
tooltip={_('Speak')}
label={_('Speak')}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowLeftSLine /> : <RiArrowRightSLine />}
onClick={viewSettings?.rtl ? handleGoPrevPage : handleGoNextPage}
tooltip={viewSettings?.rtl ? _('Previous Page') : _('Next Page')}
label={viewSettings?.rtl ? _('Previous Page') : _('Next Page')}
/>
<Button
icon={viewSettings?.rtl ? <RiArrowLeftDoubleLine /> : <RiArrowRightDoubleLine />}
onClick={viewSettings?.rtl ? handleGoPrevSection : handleGoNextSection}
tooltip={viewSettings?.rtl ? _('Previous Section') : _('Next Section')}
label={viewSettings?.rtl ? _('Previous Section') : _('Next Section')}
/>
</div>
</div>
@@ -11,6 +11,7 @@ import { mountAdditionalFonts } from '@/styles/fonts';
import { eventDispatcher } from '@/utils/event';
import { FoliateView } from '@/types/view';
import { isCJKLang } from '@/utils/lang';
import { Overlay } from '@/components/Overlay';
import Popup from '@/components/Popup';
interface FootnotePopupProps {
@@ -226,13 +227,7 @@ const FootnotePopup: React.FC<FootnotePopupProps> = ({ bookKey, bookDoc }) => {
return (
<div>
{showPopup && (
<div
className='fixed inset-0'
onClick={handleDismissPopup}
onContextMenu={handleDismissPopup}
/>
)}
{showPopup && <Overlay onDismiss={handleDismissPopup} />}
<Popup
width={responsiveWidth}
height={responsiveHeight}
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
import { Insets } from '@/types/misc';
@@ -7,6 +7,7 @@ import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import WindowButtons from '@/components/WindowButtons';
@@ -37,6 +38,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
onCloseBook,
onSetSettingsDialogOpen,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const headerRef = useRef<HTMLDivElement>(null);
const {
@@ -83,6 +85,16 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appService, isSideBarVisible, hoveredBookKey]);
// Check if mouse is outside header area to avoid false positive event of MouseLeave when clicking inside header on Windows
const isMouseOutsideHeader = useCallback((clientX: number, clientY: number) => {
if (!headerRef.current) return true;
const rect = headerRef.current.getBoundingClientRect();
return (
clientX <= rect.left || clientX >= rect.right || clientY <= rect.top || clientY >= rect.bottom
);
}, []);
const isHeaderVisible = hoveredBookKey === bookKey || isDropdownOpen;
const trafficLightInHeader =
appService?.hasTrafficLight && !trafficLightInFullscreen && !isSideBarVisible && isTopLeft;
@@ -95,6 +107,7 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
}}
>
<div
role='none'
className={clsx('absolute top-0 z-10 h-11 w-full')}
onMouseEnter={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
onTouchStart={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
@@ -111,6 +124,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
/>
<div
ref={headerRef}
role='group'
aria-label={_('Header Bar')}
className={clsx(
`header-bar bg-base-100 absolute top-0 z-10 flex h-11 w-full items-center pr-4`,
`shadow-xs transition-[opacity,margin-top] duration-300`,
@@ -126,7 +141,12 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
? `${Math.max(gridInsets.top, statusBarHeight)}px`
: `${gridInsets.top}px`,
}}
onMouseLeave={() => !appService?.isMobile && setHoveredBookKey('')}
onFocus={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
onMouseLeave={(e) => {
if (!appService?.isMobile && isMouseOutsideHeader(e.clientX, e.clientY)) {
setHoveredBookKey('');
}
}}
>
<div className='bg-base-100 sidebar-bookmark-toggler z-20 flex h-full items-center gap-x-4 pe-2'>
<div className='hidden sm:flex'>
@@ -137,25 +157,29 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
</div>
<div
role='contentinfo'
aria-label={_('Title') + ' - ' + bookTitle}
className={clsx(
'header-title z-15 bg-base-100 pointer-events-none hidden flex-1 items-center justify-center sm:flex',
!windowButtonVisible && 'absolute inset-0',
)}
>
<h2
<div
aria-hidden='true'
className={clsx(
'line-clamp-1 text-center text-xs font-semibold',
!windowButtonVisible && 'max-w-[50%]',
)}
>
{bookTitle}
</h2>
</div>
</div>
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4 ps-2'>
<SettingsToggler />
<NotebookToggler bookKey={bookKey} />
<Dropdown
label={_('View Options')}
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<PiDotsThreeVerticalBold size={iconSize16} />}
@@ -215,126 +215,128 @@ export const KOSyncSettingsWindow: React.FC = () => {
title={_('KOReader Sync Settings')}
boxClassName='sm:!min-w-[520px] sm:h-auto'
>
<div className='mb-4 mt-0 flex flex-col gap-4 p-2 sm:p-4'>
{isConfigured ? (
<>
<div className='text-center'>
<p className='text-base-content/80 text-sm'>
{_('Sync as {{userDisplayName}}', {
userDisplayName: settings.kosync.username,
})}
</p>
</div>
<div className='flex h-14 items-center justify-between'>
<span className='text-base-content/80'>{_('Sync Server Connected')}</span>
<input
type='checkbox'
className='toggle'
checked={settings.kosync.enabled}
onChange={() => handleDisconnect()}
/>
</div>
<div className='form-control w-full'>
<label className='label py-1'>
<span className='label-text font-medium'>{_('Sync Strategy')}</span>
</label>
<StyledSelect
value={settings.kosync.strategy}
onChange={handleStrategyChange}
options={[
{ value: 'prompt', label: _('Ask on conflict') },
{ value: 'silent', label: _('Always use latest') },
{ value: 'send', label: _('Send changes only') },
{ value: 'receive', label: _('Receive changes only') },
]}
/>
</div>
<div className='form-control w-full'>
<label className='label py-1'>
<span className='label-text font-medium'>{_('Checksum Method')}</span>
</label>
<StyledSelect
value={settings.kosync.checksumMethod}
onChange={handleChecksumMethodChange}
options={[
{ value: 'binary', label: _('File Content (recommended)') },
{ value: 'filename', label: _('File Name'), disabled: true },
]}
/>
</div>
<div className='form-control w-full'>
<label className='label py-1'>
<span className='label-text font-medium'>{_('Device Name')}</span>
</label>
<input
type='text'
placeholder={osName ? `Readest (${osName})` : 'Readest'}
className='input input-bordered h-12 w-full focus:outline-none focus:ring-0'
value={deviceName}
onChange={handleDeviceNameChange}
/>
</div>
</>
) : (
<>
<p className='text-base-content/70 text-center text-sm'>
{_('Connect to your KOReader Sync server.')}
</p>
<div className='form-control w-full'>
<label className='label py-1'>
<span className='label-text font-medium'>{_('Server URL')}</span>
</label>
<input
type='text'
placeholder='https://koreader.sync.server'
className='input input-bordered h-12 w-full focus:outline-none focus:ring-0'
spellCheck='false'
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</div>
<form className='flex flex-col gap-4'>
{isOpen && (
<div className='mb-4 mt-0 flex flex-col gap-4 p-2 sm:p-4'>
{isConfigured ? (
<>
<div className='text-center'>
<p className='text-base-content/80 text-sm'>
{_('Sync as {{userDisplayName}}', {
userDisplayName: settings.kosync.username,
})}
</p>
</div>
<div className='flex h-14 items-center justify-between'>
<span className='text-base-content/80'>{_('Sync Server Connected')}</span>
<input
type='checkbox'
className='toggle'
checked={settings.kosync.enabled}
onChange={() => handleDisconnect()}
/>
</div>
<div className='form-control w-full'>
<label className='label py-1'>
<span className='label-text font-medium'>{_('Username')}</span>
<span className='label-text font-medium'>{_('Sync Strategy')}</span>
</label>
<StyledSelect
value={settings.kosync.strategy}
onChange={handleStrategyChange}
options={[
{ value: 'prompt', label: _('Ask on conflict') },
{ value: 'silent', label: _('Always use latest') },
{ value: 'send', label: _('Send changes only') },
{ value: 'receive', label: _('Receive changes only') },
]}
/>
</div>
<div className='form-control w-full'>
<label className='label py-1'>
<span className='label-text font-medium'>{_('Checksum Method')}</span>
</label>
<StyledSelect
value={settings.kosync.checksumMethod}
onChange={handleChecksumMethodChange}
options={[
{ value: 'binary', label: _('File Content (recommended)') },
{ value: 'filename', label: _('File Name'), disabled: true },
]}
/>
</div>
<div className='form-control w-full'>
<label className='label py-1'>
<span className='label-text font-medium'>{_('Device Name')}</span>
</label>
<input
type='text'
placeholder={_('Your Username')}
placeholder={osName ? `Readest (${osName})` : 'Readest'}
className='input input-bordered h-12 w-full focus:outline-none focus:ring-0'
spellCheck='false'
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete='username'
value={deviceName}
onChange={handleDeviceNameChange}
/>
</div>
</>
) : (
<>
<p className='text-base-content/70 text-center text-sm'>
{_('Connect to your KOReader Sync server.')}
</p>
<div className='form-control w-full'>
<label className='label py-1'>
<span className='label-text font-medium'>{_('Password')}</span>
<span className='label-text font-medium'>{_('Server URL')}</span>
</label>
<input
type='password'
placeholder={_('Your Password')}
type='text'
placeholder='https://koreader.sync.server'
className='input input-bordered h-12 w-full focus:outline-none focus:ring-0'
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete='current-password'
spellCheck='false'
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</div>
</form>
<button
className='btn btn-primary mt-2 h-12 min-h-12 w-full'
onClick={handleConnect}
disabled={isConnecting || !url || !username || !password}
>
{isConnecting ? <span className='loading loading-spinner'></span> : _('Connect')}
</button>
{connectionStatus && (
<div className='text-error h-4 text-center text-sm'>{connectionStatus}</div>
)}
</>
)}
</div>
<form className='flex flex-col gap-4'>
<div className='form-control w-full'>
<label className='label py-1'>
<span className='label-text font-medium'>{_('Username')}</span>
</label>
<input
type='text'
placeholder={_('Your Username')}
className='input input-bordered h-12 w-full focus:outline-none focus:ring-0'
spellCheck='false'
value={username}
onChange={(e) => setUsername(e.target.value)}
autoComplete='username'
/>
</div>
<div className='form-control w-full'>
<label className='label py-1'>
<span className='label-text font-medium'>{_('Password')}</span>
</label>
<input
type='password'
placeholder={_('Your Password')}
className='input input-bordered h-12 w-full focus:outline-none focus:ring-0'
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete='current-password'
/>
</div>
</form>
<button
className='btn btn-primary mt-2 h-12 min-h-12 w-full'
onClick={handleConnect}
disabled={isConnecting || !url || !username || !password}
>
{isConnecting ? <span className='loading loading-spinner'></span> : _('Connect')}
</button>
{connectionStatus && (
<div className='text-error h-4 text-center text-sm'>{connectionStatus}</div>
)}
</>
)}
</div>
)}
</Dialog>
);
};
@@ -42,8 +42,7 @@ const NotebookToggler: React.FC<NotebookTogglerProps> = ({ bookKey }) => {
)
}
onClick={handleToggleSidebar}
tooltip={_('Notebook')}
tooltipDirection='bottom'
label={_('Notebook')}
></Button>
);
};
@@ -5,11 +5,11 @@ import { PageInfo, TimeInfo } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useBookDataStore } from '@/store/bookDataStore';
import { formatReadingProgress } from '@/utils/progress';
interface PageInfoProps {
bookKey: string;
bookFormat: string;
section?: PageInfo;
pageinfo?: PageInfo;
timeinfo?: TimeInfo;
@@ -20,7 +20,6 @@ interface PageInfoProps {
const ProgressInfoView: React.FC<PageInfoProps> = ({
bookKey,
bookFormat,
section,
pageinfo,
timeinfo,
@@ -30,8 +29,10 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const { getBookData } = useBookDataStore();
const { getView, getViewSettings } = useReaderStore();
const view = getView(bookKey);
const bookData = getBookData(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const showDoubleBorder = viewSettings.vertical && viewSettings.doubleBorder;
@@ -46,9 +47,10 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
: '{current} / {total}'
: '{percent}%';
const progressInfo = ['PDF', 'CBZ'].includes(bookFormat)
? formatReadingProgress(section?.current, section?.total, formatTemplate)
: formatReadingProgress(pageinfo?.current, pageinfo?.total, formatTemplate);
const pageProgress = bookData?.isFixedLayout ? section : pageinfo;
const progressInfo = bookData?.isFixedLayout
? formatReadingProgress(pageProgress?.current, pageProgress?.total, formatTemplate)
: formatReadingProgress(pageProgress?.current, pageProgress?.total, formatTemplate);
const timeLeft = timeinfo
? _('{{time}} min left in chapter', { time: Math.round(timeinfo.section) })
@@ -65,6 +67,18 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
isVertical ? 'writing-vertical-rl' : 'w-full',
isScrolled && !isVertical && 'bg-base-100',
)}
aria-label={[
pageProgress
? _('On {{current}} of {{total}} page', {
current: pageProgress.current + 1,
total: pageProgress.total,
})
: '',
timeLeft,
pageLeft,
]
.filter(Boolean)
.join(', ')}
style={
isVertical
? {
@@ -83,6 +97,7 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
}
>
<div
aria-hidden='true'
className={clsx(
'flex items-center justify-center',
isVertical ? 'h-full' : 'h-[52px] w-full',
@@ -39,12 +39,12 @@ Z-Index Layering Guide:
Floats above the content but below global dialogs.
40 TTS Bar
Mini controls for TTS playback on top of the TTS Control.
30 Footbar (TTS Control)
Persistent bottom controls and the TTS icon/panel.
30 TTS Control
Persistent TTS icon/panel.
20 Menu / Sidebar / Notebook (Pinned)
Docked navigation or note views.
10 Headerbar / Ribbon
Top toolbar and ribbon elements.
10 Headerbar / Footbar / Ribbon
Top toolbar, bottom footbar and ribbon elements.
0 Base Content
Main reading area or background content.
*/
@@ -57,7 +57,8 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
const { settings, setSettings } = useSettingsStore();
const { isSideBarVisible, getIsSideBarVisible, setSideBarVisible } = useSidebarStore();
const { isNotebookVisible, getIsNotebookVisible, setNotebookVisible } = useNotebookStore();
const { isDarkMode, systemUIAlwaysHidden, showSystemUI, dismissSystemUI } = useThemeStore();
const { isDarkMode, systemUIAlwaysHidden, isRoundedWindow } = useThemeStore();
const { showSystemUI, dismissSystemUI } = useThemeStore();
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
const [libraryLoaded, setLibraryLoaded] = useState(false);
const isInitiating = useRef(false);
@@ -142,8 +143,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
className={clsx(
`reader-page bg-base-100 text-base-content select-none overflow-hidden`,
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
appService?.isLinuxApp && 'window-border',
appService?.hasRoundedWindow && 'rounded-window',
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
)}
>
<Suspense fallback={<div className='h-[100vh]'></div>}>
@@ -155,7 +155,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
</Suspense>
</div>
) : (
<div className='bg-base-100 h-[100vh]'></div>
<div className={clsx('h-[100vh]', !appService?.isLinuxApp && 'bg-base-100')}></div>
);
};
@@ -4,6 +4,7 @@ import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
interface SectionInfoProps {
bookKey: string;
@@ -26,6 +27,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
contentInsets,
gridInsets,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const { hoveredBookKey } = useReaderStore();
const { systemUIVisible, statusBarHeight } = useThemeStore();
@@ -51,6 +53,8 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
isVertical ? 'writing-vertical-rl max-h-[85%]' : 'top-0 h-[44px]',
isScrolled && !isVertical && 'bg-base-100',
)}
role='banner'
aria-label={section ? _('Section Title') + `: ${section}` : ''}
style={
isVertical
? {
@@ -68,7 +72,8 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
}
}
>
<h2
<span
aria-hidden='true'
className={clsx(
'text-neutral-content text-center font-sans text-xs font-light',
isVertical ? '' : 'line-clamp-1',
@@ -76,7 +81,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
)}
>
{section || ''}
</h2>
</span>
</div>
</>
);
@@ -18,8 +18,7 @@ const SettingsToggler = () => {
<Button
icon={<RiFontSize className='text-base-content' />}
onClick={handleToggleSettings}
tooltip={_('Font & Layout')}
tooltipDirection='bottom'
label={_('Font & Layout')}
></Button>
);
};
@@ -33,8 +33,7 @@ const SidebarToggler: React.FC<SidebarTogglerProps> = ({ bookKey }) => {
)
}
onClick={handleToggleSidebar}
tooltip={_('Sidebar')}
tooltipDirection='bottom'
label={_('Toggle Sidebar')}
/>
);
};
@@ -45,16 +45,16 @@ const TranslationToggler = ({ bookKey }: { bookKey: string }) => {
icon={
<RiTranslateAi className={translationEnabled ? 'text-blue-500' : 'text-base-content'} />
}
aria-label={_('Toggle Translation')}
disabled={!translationAvailable}
onClick={() => setTranslationEnabled(!translationEnabled)}
tooltip={
label={
translationAvailable
? translationEnabled
? _('Disable Translation')
: _('Enable Translation')
: _('Translation Not Available')
: _('Translation Disabled')
}
tooltipDirection='bottom'
></Button>
);
};
@@ -41,10 +41,11 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
const { user } = useAuth();
const { envConfig, appService } = useEnv();
const { getConfig, getBookData } = useBookDataStore();
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const { getView, getViewSettings, getViewState, setViewSettings } = useReaderStore();
const config = getConfig(bookKey)!;
const bookData = getBookData(bookKey)!;
const viewSettings = getViewSettings(bookKey)!;
const viewState = getViewState(bookKey);
const { themeMode, isDarkMode, setThemeMode } = useThemeStore();
const [isScrolledMode, setScrolledMode] = useState(viewSettings!.scrolled);
@@ -146,7 +147,6 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
return (
<div
tabIndex={0}
className={clsx(
'view-menu dropdown-content dropdown-right no-triangle z-20 mt-1 border',
'bgcolor-base-200 border-base-200 shadow-2xl',
@@ -235,7 +235,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
/>
</>
)}
<hr className='border-base-300 my-1' />
<hr aria-hidden='true' className='border-base-300 my-1' />
<MenuItem label={_('Font & Layout')} shortcut='Shift+F' onClick={openFontLayoutMenu} />
@@ -247,7 +247,7 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
disabled={bookData.bookDoc?.rendition?.layout === 'pre-paginated'}
/>
<hr className='border-base-300 my-1' />
<hr aria-hidden='true' className='border-base-300 my-1' />
<MenuItem
label={
@@ -260,10 +260,11 @@ const ViewMenu: React.FC<ViewMenuProps> = ({
: _('Never synced')
}
Icon={user ? MdSync : MdSyncProblem}
iconClassName={user && viewState?.syncing ? 'animate-reverse-spin' : ''}
onClick={handleSync}
/>
<hr className='border-base-300 my-1' />
<hr aria-hidden='true' className='border-base-300 my-1' />
{appService?.hasWindow && <MenuItem label={_('Fullscreen')} onClick={handleFullScreen} />}
<MenuItem
@@ -25,6 +25,7 @@ const NotebookHeader: React.FC<{
</div>
<div className='flex w-full items-center gap-x-4'>
<button
title={isPinned ? _('Unpin Notebook') : _('Pin Notebook')}
onClick={handleTogglePin}
className={clsx(
'btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 sm:flex',
@@ -34,6 +35,7 @@ const NotebookHeader: React.FC<{
{isPinned ? <MdPushPin size={iconSize14} /> : <MdOutlinePushPin size={iconSize14} />}
</button>
<button
title={_('Close')}
onClick={handleClose}
className={'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent sm:hidden'}
>
@@ -42,6 +44,7 @@ const NotebookHeader: React.FC<{
</div>
<div className='flex items-center justify-end gap-x-4'>
<button
title={isSearchBarVisible ? _('Hide Search Bar') : _('Show Search Bar')}
onClick={handleToggleSearchBar}
className={clsx('btn btn-ghost h-8 min-h-8 w-8 p-0', isSearchBarVisible && 'bg-base-300')}
>
@@ -109,7 +109,6 @@ const NoteEditor: React.FC<NoteEditorProps> = ({ onSave, onEdit }) => {
onSave={handleSaveNote}
onEscape={handleEscape}
placeholder={_('Add your notes here...')}
autoFocus={true}
spellCheck={false}
/>
</div>
@@ -9,12 +9,13 @@ import { useNotebookStore } from '@/store/notebookStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useThemeStore } from '@/store/themeStore';
import { useEnv } from '@/context/EnvContext';
import { useDrag } from '@/hooks/useDrag';
import { DragKey, useDrag } from '@/hooks/useDrag';
import { TextSelection } from '@/utils/sel';
import { BookNote } from '@/types/book';
import { uniqueId } from '@/utils/misc';
import { eventDispatcher } from '@/utils/event';
import { getBookDirFromLanguage } from '@/utils/book';
import { Overlay } from '@/components/Overlay';
import BooknoteItem from '../sidebar/BooknoteItem';
import NotebookHeader from './Header';
import NoteEditor from './NoteEditor';
@@ -33,7 +34,8 @@ const Notebook: React.FC = ({}) => {
const { notebookNewAnnotation, notebookEditAnnotation, setNotebookPin } = useNotebookStore();
const { getBookData, getConfig, saveConfig, updateBooknotes } = useBookDataStore();
const { getView, getViewSettings } = useReaderStore();
const { setNotebookWidth, setNotebookVisible, toggleNotebookPin } = useNotebookStore();
const { getNotebookWidth, setNotebookWidth, setNotebookVisible, toggleNotebookPin } =
useNotebookStore();
const { setNotebookNewAnnotation, setNotebookEditAnnotation } = useNotebookStore();
const [isSearchBarVisible, setIsSearchBarVisible] = useState(false);
@@ -79,9 +81,7 @@ const Notebook: React.FC = ({}) => {
settings.globalReadSettings.isNotebookPinned = !isNotebookPinned;
};
const handleClickOverlay = (event: React.MouseEvent) => {
event.preventDefault();
event.stopPropagation();
const handleClickOverlay = () => {
setNotebookVisible(false);
setNotebookNewAnnotation(null);
setNotebookEditAnnotation(null);
@@ -138,7 +138,19 @@ const Notebook: React.FC = ({}) => {
handleNotebookResize(`${Math.round(newWidth * 10000) / 100}%`);
};
const { handleDragStart } = useDrag(onDragMove);
const onDragKeyDown = (data: { key: DragKey; step: number }) => {
const currentWidth = parseFloat(getNotebookWidth()) / 100;
let newWidth = currentWidth;
if (data.key === 'ArrowLeft') {
newWidth = Math.max(MIN_NOTEBOOK_WIDTH, currentWidth + data.step);
} else if (data.key === 'ArrowRight') {
newWidth = Math.min(MAX_NOTEBOOK_WIDTH, currentWidth - data.step);
}
handleNotebookResize(`${Math.round(newWidth * 10000) / 100}%`);
};
const { handleDragStart, handleDragKeyDown } = useDrag(onDragMove, onDragKeyDown);
const config = getConfig(sideBarBookKey);
const { booknotes: allNotes = [] } = config || {};
@@ -189,7 +201,7 @@ const Notebook: React.FC = ({}) => {
return isNotebookVisible ? (
<>
{!isNotebookPinned && (
<div className='overlay fixed inset-0 z-[45] bg-black/20' onClick={handleClickOverlay} />
<Overlay className='z-[45] bg-black/20' onDismiss={handleClickOverlay} />
)}
<div
className={clsx(
@@ -199,6 +211,8 @@ const Notebook: React.FC = ({}) => {
appService?.hasRoundedWindow && 'rounded-window-top-right rounded-window-bottom-right',
isNotebookPinned ? 'z-20' : 'z-[45] shadow-2xl',
)}
role='group'
aria-label={_('Notebook')}
dir={viewSettings?.rtl && languageDir === 'rtl' ? 'rtl' : 'ltr'}
style={{
width: `${notebookWidth}`,
@@ -216,9 +230,15 @@ const Notebook: React.FC = ({}) => {
}
`}</style>
<div
className='drag-bar absolute left-0 top-0 -m-2 h-full w-0.5 cursor-col-resize bg-transparent p-2'
className='drag-bar absolute -left-2 top-0 h-full w-0.5 cursor-col-resize bg-transparent p-2'
role='slider'
tabIndex={0}
aria-label={_('Resize Notebook')}
aria-orientation='horizontal'
aria-valuenow={parseFloat(notebookWidth)}
onMouseDown={handleDragStart}
onTouchStart={handleDragStart}
onKeyDown={handleDragKeyDown}
/>
<div className='flex-shrink-0'>
<NotebookHeader
@@ -263,7 +283,13 @@ const Notebook: React.FC = ({}) => {
{filteredExcerptNotes.map((item, index) => (
<li key={`${index}-${item.id}`} className='my-2'>
<div
role='button'
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Backspace' || e.key === 'Delete') {
handleEditNote(item, true);
}
}}
className='collapse-arrow border-base-300 bg-base-100 collapse border'
>
<div
@@ -283,9 +309,11 @@ const Notebook: React.FC = ({}) => {
<div className='collapse-content font-size-xs select-text px-3 pb-0'>
<p className='hyphens-auto text-justify'>{item.text}</p>
<div className='flex justify-end' dir='ltr'>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions*/}
<div
className='font-size-xs cursor-pointer align-bottom text-red-500 hover:text-red-600'
onClick={handleEditNote.bind(null, item, true)}
aria-label={_('Delete')}
>
{_('Delete')}
</div>
@@ -33,8 +33,8 @@ const ColorInput: React.FC<ColorInputProps> = ({ label, value, onChange }) => {
<div className='mb-3'>
<label className='mb-1 block text-sm font-medium'>{label}</label>
<div className='flex items-center'>
<div
className='border-base-200 relative mr-2 flex h-7 w-8 cursor-pointer items-center justify-center overflow-hidden rounded border'
<button
className='border-base-200/75 relative mr-2 flex h-7 w-8 cursor-pointer items-center justify-center overflow-hidden rounded border'
style={{ backgroundColor: value }}
onClick={() => setIsOpen(!isOpen)}
/>
@@ -44,7 +44,7 @@ const ColorInput: React.FC<ColorInputProps> = ({ label, value, onChange }) => {
value={value}
spellCheck={false}
onChange={(e) => onChange(e.target.value)}
className='bg-base-100 text-base-content border-base-200 min-w-4 max-w-36 flex-1 rounded border p-1 font-mono text-sm'
className='bg-base-100 text-base-content border-base-200/75 min-w-4 max-w-36 flex-1 rounded border p-1 font-mono text-sm'
/>
</div>
@@ -148,30 +148,27 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
<div className='flex items-center justify-between'>
<h2 className='font-medium'>{_('Theme Mode')}</h2>
<div className='flex gap-4'>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Auto Mode')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setThemeMode('auto')}
>
<TbSunMoon />
</button>
</div>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Light Mode')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setThemeMode('light')}
>
<MdOutlineLightMode />
</button>
</div>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Dark Mode')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setThemeMode('dark')}
>
<MdOutlineDarkMode />
</button>
</div>
<button
title={_('Auto Mode')}
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setThemeMode('auto')}
>
<TbSunMoon />
</button>
<button
title={_('Light Mode')}
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setThemeMode('light')}
>
<MdOutlineLightMode />
</button>
<button
title={_('Dark Mode')}
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setThemeMode('dark')}
>
<MdOutlineDarkMode />
</button>
</div>
</div>
@@ -200,8 +197,16 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
<h2 className='mb-2 font-medium'>{_('Theme Color')}</h2>
<div className='grid grid-cols-3 gap-4'>
{themes.concat(customThemes).map(({ name, label, colors, isCustomizale }) => (
<label
<button
key={name}
tabIndex={0}
onClick={() => setThemeColor(name)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
setThemeColor(name);
}
e.stopPropagation();
}}
className={`relative flex cursor-pointer flex-col items-center justify-center rounded-lg p-4 shadow-md ${
themeColor === name ? 'ring-2 ring-indigo-500 ring-offset-2' : ''
}`}
@@ -213,6 +218,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
}}
>
<input
aria-label={_(label)}
type='radio'
name='theme'
value={name}
@@ -231,15 +237,15 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
<CgColorPicker size={iconSize16} className='absolute right-2 top-2' />
</button>
)}
</label>
</button>
))}
<label
<button
className={`relative flex cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed p-4 shadow-md`}
onClick={() => setShowCustomThemeEditor(true)}
>
<PiPlus size={iconSize24} />
<span>{_('Custom')}</span>
</label>
</button>
</div>
</div>
@@ -56,7 +56,8 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
let fontFile: File;
if (selectedFile.path) {
const filePath = selectedFile.path;
fontPath = getFilename(filePath);
const fileobj = await appService!.fs.openFile(filePath, 'None');
fontPath = fileobj.name || getFilename(filePath);
await appService!.fs.copyFile(filePath, fontPath, 'Fonts');
fontFile = await appService!.fs.openFile(fontPath, 'Fonts');
} else if (selectedFile.file) {
@@ -73,6 +74,7 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
family: fontInfo.family,
style: fontInfo.style,
weight: fontInfo.weight,
variable: fontInfo.variable,
});
console.log('Added custom font:', customFont);
if (customFont && !customFont.error) {
@@ -139,9 +141,9 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
<div className='breadcrumbs py-1'>
<ul>
<li>
<a className='font-semibold' onClick={onBack}>
<button className='font-semibold' onClick={onBack}>
{_('Font')}
</a>
</button>
</li>
<li className='font-medium'>{_('Custom Fonts')}</li>
</ul>
@@ -166,7 +168,7 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
<div className='grid grid-cols-2 gap-4'>
<div className='card border-primary/50 hover:border-primary/75 group h-12 border-2 transition-colors'>
<div
<button
className='card-body flex cursor-pointer items-center justify-center p-2 text-center'
onClick={handleImportFont}
>
@@ -178,11 +180,12 @@ const CustomFonts: React.FC<CustomFontsProps> = ({ bookKey, onBack }) => {
{_('Import Font')}
</div>
</div>
</div>
</button>
</div>
{availableFamilies.map((family) => (
<div
role='none'
key={family.name}
className={clsx(
'card h-12 border shadow-sm',
@@ -42,7 +42,6 @@ const DialogMenu: React.FC<DialogMenuProps> = ({
return (
<div
tabIndex={0}
className={clsx(
'dropdown-content dropdown-right no-triangle border-base-200 z-20 mt-1 border shadow-2xl',
'text-base sm:text-sm',
@@ -1,63 +0,0 @@
import clsx from 'clsx';
import React from 'react';
import { FiChevronDown } from 'react-icons/fi';
import { MdCheck } from 'react-icons/md';
import { useDefaultIconSize, useResponsiveSize } from '@/hooks/useResponsiveSize';
interface DropDownProps {
selected: { option: string; label: string };
options: { option: string; label: string }[];
onSelect: (option: string) => void;
disabled?: boolean;
className?: string;
listClassName?: string;
}
const DropDown: React.FC<DropDownProps> = ({
selected,
options,
onSelect,
className,
listClassName,
disabled = false,
}) => {
const iconSize16 = useResponsiveSize(16);
const defaultIconSize = useDefaultIconSize();
return (
<div className={clsx('dropdown dropdown-bottom', className)}>
<button
tabIndex={0}
className={clsx(
'btn btn-sm flex items-center gap-1 px-[20px] font-normal normal-case',
disabled && 'btn-disabled',
)}
onClick={(e) => e.currentTarget.focus()}
>
<span>{selected.label}</span>
<FiChevronDown size={iconSize16} />
</button>
<ul
tabIndex={0}
className={clsx(
'dropdown-content bgcolor-base-200 no-triangle menu rounded-box absolute z-[1] shadow',
'menu-vertical right-[-32px] mt-2 inline max-h-80 w-44 overflow-y-scroll sm:right-0',
listClassName,
)}
>
{options.map(({ option, label }) => (
<li key={option} onClick={() => onSelect(option)}>
<div className='flex items-center px-0'>
<span style={{ minWidth: `${defaultIconSize}px` }}>
{selected.option === option && <MdCheck className='text-base-content' />}
</span>
<span>{label || option}</span>
</div>
</li>
))}
</ul>
</div>
);
};
export default DropDown;
@@ -34,8 +34,25 @@ const FontItem: React.FC<FontItemProps> = ({ index, style, data }) => {
const option = options[index]!;
return (
<li className='px-2' key={option.option} style={style} onClick={() => onSelect(option.option)}>
<div className='flex w-full items-center overflow-hidden !px-0 text-sm'>
<li
role='option'
aria-selected={selected === option.option}
className='px-2'
key={option.option}
style={style}
>
<div
role='button'
tabIndex={0}
onClick={() => onSelect(option.option)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onSelect(option.option);
}
}}
aria-label={option.label || option.option}
className='flex w-full items-center overflow-hidden !px-0 text-sm'
>
<span style={{ minWidth: `${iconSize16}px` }}>
{selected === option.option && (
<MdCheck className='text-base-content' size={iconSize16} />
@@ -115,6 +132,7 @@ const FontDropdown: React.FC<DropdownProps> = ({
</div>
</button>
<ul
role='listbox'
tabIndex={0}
className={clsx(
'dropdown-content bgcolor-base-200 no-triangle menu rounded-box absolute z-[1] mt-4 shadow',
@@ -145,6 +163,7 @@ const FontDropdown: React.FC<DropdownProps> = ({
<span>{_('System Fonts')}</span>
</div>
<ul
role='listbox'
tabIndex={0}
className={clsx(
'dropdown-content bgcolor-base-200 menu rounded-box relative z-[1] shadow',
@@ -382,41 +382,37 @@ const LayoutPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterRese
<div className='flex items-center justify-between'>
<h2 className='font-medium'>{_('Writing Mode')}</h2>
<div className='flex gap-4'>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Default')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${writingMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('auto')}
>
<MdOutlineAutoMode />
</button>
</div>
<button
title={_('Default')}
className={`btn btn-ghost btn-circle btn-sm ${writingMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('auto')}
>
<MdOutlineAutoMode />
</button>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Horizontal Direction')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${writingMode === 'horizontal-tb' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('horizontal-tb')}
>
<MdOutlineTextRotationNone />
</button>
</div>
<button
title={_('Horizontal Direction')}
className={`btn btn-ghost btn-circle btn-sm ${writingMode === 'horizontal-tb' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('horizontal-tb')}
>
<MdOutlineTextRotationNone />
</button>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('Vertical Direction')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${writingMode === 'vertical-rl' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('vertical-rl')}
>
<MdTextRotateVertical />
</button>
</div>
<button
title={_('Vertical Direction')}
className={`btn btn-ghost btn-circle btn-sm ${writingMode === 'vertical-rl' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('vertical-rl')}
>
<MdTextRotateVertical />
</button>
<div className='lg:tooltip lg:tooltip-bottom' data-tip={_('RTL Direction')}>
<button
className={`btn btn-ghost btn-circle btn-sm ${writingMode === 'horizontal-rl' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('horizontal-rl')}
>
<TbTextDirectionRtl />
</button>
</div>
<button
title={_('RTL Direction')}
className={`btn btn-ghost btn-circle btn-sm ${writingMode === 'horizontal-rl' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setWritingMode('horizontal-rl')}
>
<TbTextDirectionRtl />
</button>
</div>
</div>
)}
@@ -149,7 +149,9 @@ const MiscPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
textareaRef: React.RefObject<HTMLTextAreaElement>,
) => (
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_(title)}</h2>
<h2 className='mb-2 font-medium' aria-label={_(title)}>
{_(title)}
</h2>
<div
className={`card border-base-200 bg-base-100 border shadow ${error ? 'border-red-500' : ''}`}
>
@@ -1,6 +1,7 @@
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import { FiMinus, FiPlus } from 'react-icons/fi';
import { useTranslation } from '@/hooks/useTranslation';
interface NumberInputProps {
className?: string;
@@ -23,6 +24,7 @@ const NumberInput: React.FC<NumberInputProps> = ({
step,
disabled,
}) => {
const _ = useTranslation();
const [localValue, setLocalValue] = useState(value);
const numberStep = step || 1;
@@ -72,19 +74,29 @@ const NumberInput: React.FC<NumberInputProps> = ({
<input
type='text'
inputMode='decimal'
disabled={disabled}
value={localValue}
onChange={handleChange}
onBlur={handleOnBlur}
className='input input-ghost settings-content text-base-content w-16 max-w-xs rounded border-0 bg-transparent py-1 pe-3 ps-1 text-right !outline-none'
className={clsx(
'input input-ghost settings-content text-base-content w-16 max-w-xs rounded border-0 bg-transparent py-1 pe-3 ps-1 text-right !outline-none',
disabled && 'input-disabled cursor-not-allowed disabled:bg-transparent',
)}
onFocus={(e) => e.target.select()}
/>
<button
tabIndex={disabled ? -1 : 0}
disabled={value <= min || disabled}
aria-label={_('Decrease')}
onClick={decrement}
className={`btn btn-circle btn-sm ${value <= min || disabled ? 'btn-disabled !bg-opacity-5' : ''}`}
>
<FiMinus className='h-4 w-4' />
</button>
<button
tabIndex={disabled ? -1 : 0}
disabled={value >= max || disabled}
aria-label={_('Increase')}
onClick={increment}
className={`btn btn-circle btn-sm ${value >= max || disabled ? 'btn-disabled !bg-opacity-5' : ''}`}
>
@@ -3,6 +3,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { BookConfig } from '@/types/book';
import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useTranslation } from '@/hooks/useTranslation';
import { RiFontSize } from 'react-icons/ri';
import { RiDashboardLine, RiTranslate } from 'react-icons/ri';
@@ -10,7 +11,7 @@ import { VscSymbolColor } from 'react-icons/vsc';
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
import { LiaHandPointerSolid } from 'react-icons/lia';
import { IoAccessibilityOutline } from 'react-icons/io5';
import { MdArrowBackIosNew, MdArrowForwardIos } from 'react-icons/md';
import { MdArrowBackIosNew, MdArrowForwardIos, MdClose } from 'react-icons/md';
import { getDirFromUILanguage } from '@/utils/rtl';
import FontPanel from './FontPanel';
import LayoutPanel from './LayoutPanel';
@@ -37,6 +38,7 @@ type TabConfig = {
const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ bookKey }) => {
const _ = useTranslation();
const { appService } = useEnv();
const closeIconSize = useResponsiveSize(16);
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
const tabsRef = useRef<HTMLDivElement | null>(null);
const [showAllTabLabels, setShowAllTabLabels] = useState(false);
@@ -177,6 +179,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
<div className='flex w-full flex-row items-center justify-between'>
<button
tabIndex={-1}
aria-label={_('Close')}
onClick={handleClose}
className={
'btn btn-ghost btn-circle flex h-8 min-h-8 w-8 hover:bg-transparent focus:outline-none sm:hidden'
@@ -186,12 +189,16 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
</button>
<div
ref={tabsRef}
role='group'
aria-label={_('Settings Panels') + ' - ' + (currentPanel?.label || '')}
className={clsx('dialog-tabs ms-1 flex h-10 w-full items-center gap-1 sm:ms-0')}
>
{tabConfig.map(({ tab, icon: Icon, label }) => (
<button
key={tab}
data-tab={tab}
tabIndex={0}
title={label}
className={clsx(
'btn btn-ghost text-base-content btn-sm gap-1 px-2',
activePanel === tab ? 'btn-active' : '',
@@ -212,6 +219,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
</div>
<div className='flex h-full items-center justify-end gap-x-2'>
<Dropdown
label={_('Settings Menu')}
className='dropdown-bottom dropdown-end'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0 flex items-center justify-center'
toggleButton={<PiDotsThreeVerticalBold />}
@@ -228,60 +236,56 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
</Dropdown>
<button
onClick={handleClose}
aria-label={_('Close')}
className={
'bg-base-300/65 btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 p-0 sm:flex'
}
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='1em'
height='1em'
viewBox='0 0 24 24'
>
<path
fill='currentColor'
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
/>
</svg>
<MdClose size={closeIconSize} />
</button>
</div>
</div>
</div>
}
>
{activePanel === 'Font' && (
<FontPanel bookKey={bookKey} onRegisterReset={(fn) => registerResetFunction('Font', fn)} />
)}
{activePanel === 'Layout' && (
<LayoutPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Layout', fn)}
/>
)}
{activePanel === 'Color' && (
<ColorPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Color', fn)}
/>
)}
{activePanel === 'Control' && (
<ControlPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Control', fn)}
/>
)}
{activePanel === 'Language' && (
<LangPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Language', fn)}
/>
)}
{activePanel === 'Custom' && (
<MiscPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Custom', fn)}
/>
)}
<div role='group' aria-label={`${_(currentPanel?.label || '')} - ${_('Settings')}`}>
{activePanel === 'Font' && (
<FontPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Font', fn)}
/>
)}
{activePanel === 'Layout' && (
<LayoutPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Layout', fn)}
/>
)}
{activePanel === 'Color' && (
<ColorPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Color', fn)}
/>
)}
{activePanel === 'Control' && (
<ControlPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Control', fn)}
/>
)}
{activePanel === 'Language' && (
<LangPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Language', fn)}
/>
)}
{activePanel === 'Custom' && (
<MiscPanel
bookKey={bookKey}
onRegisterReset={(fn) => registerResetFunction('Custom', fn)}
/>
)}
</div>
</Dialog>
);
};
@@ -41,8 +41,9 @@ const BookCard = ({ book }: { book: Book }) => {
<button
className='btn btn-ghost hover:bg-base-300 h-6 min-h-6 w-6 rounded-full p-0 transition-colors'
aria-label={_('More Info')}
onClick={showBookDetails}
>
<MdInfoOutline size={iconSize18} className='fill-base-content' onClick={showBookDetails} />
<MdInfoOutline size={iconSize18} className='fill-base-content' />
</button>
</div>
);
@@ -14,6 +14,7 @@ import { isWebAppPlatform } from '@/services/environment';
import { eventDispatcher } from '@/utils/event';
import { DOWNLOAD_READEST_URL } from '@/services/constants';
import { setKOSyncSettingsWindowVisible } from '@/app/reader/components/KOSyncSettings';
import { FIXED_LAYOUT_FORMATS } from '@/types/book';
import useBooksManager from '../../hooks/useBooksManager';
import MenuItem from '@/components/MenuItem';
@@ -85,11 +86,8 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
setIsDropdownOpen?.(false);
};
const isWebApp = isWebAppPlatform();
return (
<div
tabIndex={0}
className={clsx('book-menu dropdown-content border-base-100 z-20 shadow-2xl', menuClassName)}
>
<MenuItem
@@ -107,7 +105,7 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
>
<ul className='max-h-60 overflow-y-auto'>
{getVisibleLibrary()
.filter((book) => book.format !== 'PDF' && book.format !== 'CBZ')
.filter((book) => !FIXED_LAYOUT_FORMATS.has(book.format))
.filter((book) => !!book.downloadedAt)
.slice(0, 20)
.map((book) => (
@@ -149,7 +147,7 @@ const BookMenu: React.FC<BookMenuProps> = ({ menuClassName, setIsDropdownOpen })
/>
<MenuItem label={_('Reload Page')} shortcut='Shift+R' onClick={handleReloadPage} />
<hr className='border-base-200 my-1' />
{isWebApp && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
{isWebAppPlatform() && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
<MenuItem label={_('About Readest')} onClick={showAboutReadest} />
</div>
);
@@ -38,7 +38,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
const progress = getProgress(bookKey);
const { isCurrent, viewRef } = useScrollToItem(cfi, progress);
const handleClickItem = (event: React.MouseEvent) => {
const handleClickItem = (event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault();
eventDispatcher.dispatch('navigate', { bookKey, cfi });
@@ -108,7 +108,6 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
onChange={(value) => (editorDraftRef.current = value)}
onSave={handleSaveBookmark}
onEscape={() => setInlineEditMode(false)}
autoFocus={true}
spellCheck={false}
/>
</div>
@@ -124,14 +123,25 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
return (
<li
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role='button'
ref={viewRef}
className={clsx(
'border-base-300 content group relative my-2 cursor-pointer rounded-lg p-2',
isCurrent ? 'bg-base-300/85 hover:bg-base-300' : 'hover:bg-base-300/55 bg-base-100',
isCurrent
? 'bg-base-300/85 hover:bg-base-300 focus:bg-base-300'
: 'hover:bg-base-300/55 focus:bg-base-300/55 bg-base-100',
'transition-all duration-300 ease-in-out',
)}
tabIndex={0}
onClick={handleClickItem}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClickItem(e);
} else {
e.stopPropagation();
}
}}
>
<div
className={clsx('min-h-4 p-0 transition-all duration-300 ease-in-out')}
@@ -175,17 +185,20 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
</div>
</div>
</div>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
className={clsx(
'max-h-0 overflow-hidden p-0',
'transition-[max-height] duration-300 ease-in-out',
'group-hover:max-h-8 group-hover:overflow-visible',
'group-focus-within:max-h-8 group-focus-within:overflow-visible',
)}
style={
{
'--bottom-override': 0,
} as React.CSSProperties
}
// This is needed to prevent the parent onClick from being triggered
onClick={(e) => e.stopPropagation()}
>
<div className='flex cursor-default items-center justify-between'>
@@ -199,7 +212,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
<TextButton
onClick={item.type === 'bookmark' ? editBookmark : editNote.bind(null, item)}
variant='primary'
className='opacity-0 transition duration-300 ease-in-out group-hover:opacity-100'
className='opacity-0 transition duration-300 ease-in-out group-focus-within:opacity-100 group-hover:opacity-100'
>
{_('Edit')}
</TextButton>
@@ -208,7 +221,7 @@ const BooknoteItem: React.FC<BooknoteItemProps> = ({ bookKey, item }) => {
<TextButton
onClick={deleteNote.bind(null, item)}
variant='danger'
className='opacity-0 transition duration-300 ease-in-out group-hover:opacity-100'
className='opacity-0 transition duration-300 ease-in-out group-focus-within:opacity-100 group-hover:opacity-100'
>
{_('Delete')}
</TextButton>
@@ -73,7 +73,7 @@ const SidebarContent: React.FC<{
})}
>
{targetTab === 'toc' && bookDoc.toc && (
<TOCView toc={bookDoc.toc} bookKey={sideBarBookKey} />
<TOCView toc={bookDoc.toc} sections={bookDoc.sections} bookKey={sideBarBookKey} />
)}
{targetTab === 'annotations' && (
<BooknoteView type='annotation' toc={bookDoc.toc ?? []} bookKey={sideBarBookKey} />
@@ -5,6 +5,7 @@ import { FiSearch } from 'react-icons/fi';
import { MdOutlineMenu, MdOutlinePushPin, MdPushPin } from 'react-icons/md';
import { MdArrowBackIosNew } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import Dropdown from '@/components/Dropdown';
@@ -18,6 +19,7 @@ const SidebarHeader: React.FC<{
onTogglePin: () => void;
onToggleSearchBar: () => void;
}> = ({ isPinned, isSearchBarVisible, onGoToLibrary, onClose, onTogglePin, onToggleSearchBar }) => {
const _ = useTranslation();
const { appService } = useEnv();
const {
isTrafficLightVisible,
@@ -52,12 +54,14 @@ const SidebarHeader: React.FC<{
>
<div className='flex items-center gap-x-8'>
<button
title={_('Close')}
onClick={onClose}
className={'btn btn-ghost btn-circle flex h-6 min-h-6 w-6 hover:bg-transparent sm:hidden'}
>
<MdArrowBackIosNew size={iconSize22} />
</button>
<button
title={_('Go to Library')}
className='btn btn-ghost hidden h-8 min-h-8 w-8 p-0 sm:flex'
onClick={onGoToLibrary}
>
@@ -66,6 +70,7 @@ const SidebarHeader: React.FC<{
</div>
<div className='flex min-w-24 max-w-32 items-center justify-between sm:size-[70%]'>
<button
title={isSearchBarVisible ? _('Hide Search Bar') : _('Show Search Bar')}
onClick={onToggleSearchBar}
className={clsx(
'btn btn-ghost left-0 h-8 min-h-8 w-8 p-0',
@@ -75,6 +80,7 @@ const SidebarHeader: React.FC<{
<FiSearch size={iconSize18} className='text-base-content' />
</button>
<Dropdown
label={_('Book Menu')}
className={clsx(
window.innerWidth < 640 && 'dropdown-end',
'dropdown-bottom flex justify-center',
@@ -87,6 +93,7 @@ const SidebarHeader: React.FC<{
</Dropdown>
<div className='right-0 hidden h-8 w-8 items-center justify-center sm:flex'>
<button
title={isPinned ? _('Unpin Sidebar') : _('Pin Sidebar')}
onClick={onTogglePin}
className={clsx(
'sidebar-pin-btn btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 sm:flex',
@@ -206,6 +206,7 @@ const SearchBar: React.FC<SearchBarProps> = ({
<div className='bg-base-300 flex h-8 w-8 items-center rounded-r-lg'>
<Dropdown
label={_('Search Options')}
className={clsx(
window.innerWidth < 640 && 'dropdown-end',
'dropdown-bottom flex justify-center',
@@ -46,7 +46,6 @@ const SearchOptions: React.FC<SearchOptionsProps> = ({
return (
<div
tabIndex={0}
className={clsx(
'book-menu dropdown-content dropdown-center border-base-200 z-20 w-56 border shadow-2xl',
menuClassName,
@@ -23,12 +23,22 @@ const SearchResultItem: React.FC<SearchResultItemProps> = ({
return (
<li
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role='button'
ref={viewRef}
className={clsx(
'my-2 cursor-pointer rounded-lg p-2 text-sm',
isCurrent ? 'bg-base-300 hover:bg-gray-300/70' : 'hover:bg-base-300 bg-base-100',
)}
tabIndex={0}
onClick={() => onSelectResult(cfi)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onSelectResult(cfi);
} else {
e.stopPropagation();
}
}}
>
<div className='line-clamp-3'>
<span className=''>{excerpt.pre}</span>
@@ -1,24 +1,26 @@
import clsx from 'clsx';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { impactFeedback } from '@tauri-apps/plugin-haptics';
import { useSettingsStore } from '@/store/settingsStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { useTranslation } from '@/hooks/useTranslation';
import { BookSearchResult } from '@/types/book';
import { eventDispatcher } from '@/utils/event';
import { getBookDirFromLanguage } from '@/utils/book';
import { useEnv } from '@/context/EnvContext';
import { useDrag } from '@/hooks/useDrag';
import { DragKey, useDrag } from '@/hooks/useDrag';
import { useThemeStore } from '@/store/themeStore';
import { Overlay } from '@/components/Overlay';
import useShortcuts from '@/hooks/useShortcuts';
import SidebarHeader from './Header';
import SidebarContent from './Content';
import BookCard from './BookCard';
import useSidebar from '../../hooks/useSidebar';
import SearchBar from './SearchBar';
import SearchResults from './SearchResults';
import useShortcuts from '@/hooks/useShortcuts';
const MIN_SIDEBAR_WIDTH = 0.05;
const MAX_SIDEBAR_WIDTH = 0.45;
@@ -28,6 +30,7 @@ const VELOCITY_THRESHOLD = 0.5;
const SideBar: React.FC<{
onGoToLibrary: () => void;
}> = ({ onGoToLibrary }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { updateAppTheme, safeAreaInsets } = useThemeStore();
const { settings } = useSettingsStore();
@@ -37,11 +40,13 @@ const SideBar: React.FC<{
const [isSearchBarVisible, setIsSearchBarVisible] = useState(false);
const [searchResults, setSearchResults] = useState<BookSearchResult[] | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const sidebarHeight = useRef(1.0);
const isMobile = window.innerWidth < 640;
const {
sideBarWidth,
isSideBarPinned,
isSideBarVisible,
getSideBarWidth,
setSideBarVisible,
handleSideBarResize,
handleSideBarTogglePin,
@@ -89,6 +94,7 @@ const SideBar: React.FC<{
const heightFraction = data.clientY / window.innerHeight;
const newTop = Math.max(0.0, Math.min(1, heightFraction));
sidebarHeight.current = newTop;
const sidebar = document.querySelector('.sidebar-container') as HTMLElement;
const overlay = document.querySelector('.overlay') as HTMLElement;
@@ -135,11 +141,29 @@ const SideBar: React.FC<{
handleSideBarResize(`${Math.round(newWidth * 10000) / 100}%`);
};
const handleHorizontalDragKeyDown = (data: { key: DragKey; step: number }) => {
const currentWidth = parseFloat(getSideBarWidth()) / 100;
let newWidth = currentWidth;
if (data.key === 'ArrowLeft') {
newWidth = Math.max(MIN_SIDEBAR_WIDTH, currentWidth - data.step);
} else if (data.key === 'ArrowRight') {
newWidth = Math.min(MAX_SIDEBAR_WIDTH, currentWidth + data.step);
}
handleSideBarResize(`${Math.round(newWidth * 10000) / 100}%`);
};
const handleVerticalDragKeyDown = () => {};
const { handleDragStart: handleVerticalDragStart } = useDrag(
handleVerticalDragMove,
handleVerticalDragKeyDown,
handleVerticalDragEnd,
);
const { handleDragStart: handleHorizontalDragStart } = useDrag(handleHorizontalDragMove);
const { handleDragStart: handleHorizontalDragStart, handleDragKeyDown } = useDrag(
handleHorizontalDragMove,
handleHorizontalDragKeyDown,
);
const handleClickOverlay = () => {
setSideBarVisible(false);
@@ -182,10 +206,7 @@ const SideBar: React.FC<{
return isSideBarVisible ? (
<>
{!isSideBarPinned && (
<div
className='overlay fixed inset-0 z-[45] bg-black/50 sm:bg-black/20'
onClick={handleClickOverlay}
/>
<Overlay className='z-[45] bg-black/50 sm:bg-black/20' onDismiss={handleClickOverlay} />
)}
<div
className={clsx(
@@ -195,6 +216,8 @@ const SideBar: React.FC<{
appService?.hasRoundedWindow && 'rounded-window-top-left rounded-window-bottom-left',
isSideBarPinned ? 'z-20' : 'z-[45] shadow-2xl',
)}
role='group'
aria-label={_('Sidebar')}
dir={viewSettings?.rtl && languageDir === 'rtl' ? 'rtl' : 'ltr'}
style={{
width: `${sideBarWidth}`,
@@ -222,6 +245,11 @@ const SideBar: React.FC<{
<div className='flex-shrink-0'>
{isMobile && (
<div
role='slider'
tabIndex={0}
aria-label={_('Resize Sidebar')}
aria-orientation='vertical'
aria-valuenow={sidebarHeight.current}
className='drag-handle flex h-10 w-full cursor-row-resize items-center justify-center'
onMouseDown={handleVerticalDragStart}
onTouchStart={handleVerticalDragStart}
@@ -265,11 +293,17 @@ const SideBar: React.FC<{
)}
<div
className={clsx(
'drag-bar absolute right-0 top-0 -m-2 h-full w-0.5 cursor-col-resize bg-transparent p-2',
'drag-bar absolute -right-2 top-0 h-full w-0.5 cursor-col-resize bg-transparent p-1',
isMobile && 'hidden',
)}
role='slider'
tabIndex={0}
aria-label={_('Resize Sidebar')}
aria-orientation='horizontal'
aria-valuenow={parseFloat(sideBarWidth)}
onMouseDown={handleHorizontalDragStart}
onTouchStart={handleHorizontalDragStart}
onKeyDown={handleDragKeyDown}
></div>
</div>
</>
@@ -49,7 +49,7 @@ const TOCItemView = React.memo<{
);
const handleClickItem = useCallback(
(event: React.MouseEvent) => {
(event: React.MouseEvent | React.KeyboardEvent) => {
event.preventDefault();
onItemClick(item);
},
@@ -58,9 +58,10 @@ const TOCItemView = React.memo<{
return (
<div
tabIndex={0}
role='treeitem'
tabIndex={-1}
onClick={item.href ? handleClickItem : undefined}
onKeyDown={item.href ? (e) => e.key === 'Enter' && handleClickItem(e) : undefined}
aria-expanded={flatItem.isExpanded ? 'true' : 'false'}
aria-selected={isActive ? 'true' : 'false'}
data-href={item.href ? getContentMd5(item.href) : undefined}
@@ -76,8 +77,11 @@ const TOCItemView = React.memo<{
}}
>
{item.subitems && (
<div
<button
onClick={handleToggleExpand}
onKeyDown={(e) => {
e.stopPropagation();
}}
className='inline-block cursor-pointer'
style={{
padding: '12px',
@@ -85,7 +89,7 @@ const TOCItemView = React.memo<{
}}
>
{createExpanderIcon(flatItem.isExpanded || false)}
</div>
</button>
)}
<div
className='ms-2 truncate text-ellipsis'
@@ -3,7 +3,7 @@ import { FixedSizeList as VirtualList } from 'react-window';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import 'overlayscrollbars/overlayscrollbars.css';
import { TOCItem } from '@/libs/document';
import { SectionItem, TOCItem } from '@/libs/document';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useSidebarStore } from '@/store/sidebarStore';
@@ -39,7 +39,8 @@ const useFlattenedTOC = (toc: TOCItem[], expandedItems: Set<string>) => {
const TOCView: React.FC<{
bookKey: string;
toc: TOCItem[];
}> = ({ bookKey, toc }) => {
sections?: SectionItem[];
}> = ({ bookKey, toc, sections }) => {
const { appService } = useEnv();
const { getView, getProgress, getViewState, getViewSettings } = useReaderStore();
const { sideBarBookKey, isSideBarVisible } = useSidebarStore();
@@ -182,8 +183,7 @@ const TOCView: React.FC<{
(activeItem as HTMLElement).setAttribute('aria-current', 'page');
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeHref]);
}, [flatItems, activeHref]);
const virtualItemSize = useMemo(() => {
return window.innerWidth >= 640 && !viewSettings?.translationEnabled ? 37 : 57;
@@ -214,12 +214,12 @@ const TOCView: React.FC<{
useEffect(() => {
if (flatItems.length > 0) {
setTimeout(scrollToActiveItem, appService?.isAndroidApp ? 300 : 0);
setTimeout(scrollToActiveItem, appService?.isAndroidApp ? 300 : 100);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scrollToActiveItem]);
}, [progress]);
return flatItems.length > 256 ? (
return sections && sections.length > 256 ? (
<div
className='virtual-list rounded pt-2'
data-overlayscrollbars-initialize=''
@@ -36,12 +36,22 @@ const TabNavigation: React.FC<{
{tabs.map((tab) => (
<div
key={tab}
className='lg:tooltip lg:tooltip-top z-20 m-1.5 flex-1 cursor-pointer rounded-md p-2'
data-tip={
tabIndex={0}
role='button'
className='z-20 m-1.5 flex-1 cursor-pointer rounded-md p-2'
onClick={() => onTabChange(tab)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onTabChange(tab);
}
}}
title={tab === 'toc' ? _('TOC') : tab === 'annotations' ? _('Annotate') : _('Bookmark')}
aria-label={
tab === 'toc' ? _('TOC') : tab === 'annotations' ? _('Annotate') : _('Bookmark')
}
>
<div className={clsx('flex h-6 items-center')} onClick={() => onTabChange(tab)}>
<div className={clsx('m-0 flex h-6 items-center p-0')}>
{tab === 'toc' ? (
<TOCIcon className='mx-auto' />
) : tab === 'annotations' ? (
@@ -14,6 +14,7 @@ import { throttle } from '@/utils/throttle';
import { invokeUseBackgroundAudio } from '@/utils/bridge';
import { CFI } from '@/libs/document';
import { Insets } from '@/types/misc';
import { Overlay } from '@/components/Overlay';
import Popup from '@/components/Popup';
import TTSPanel from './TTSPanel';
import TTSIcon from './TTSIcon';
@@ -120,7 +121,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
const progress = getProgress(bookKey);
const { sectionLabel } = progress || {};
const mark = (e as CustomEvent<TTSMark>).detail;
if (appService?.isMobileApp && 'mediaSession' in navigator) {
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: mark?.text || '',
artist: sectionLabel || title,
@@ -438,10 +439,18 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
});
navigator.mediaSession.setActionHandler('seekforward', () => {
handleForward();
handleForward(true);
});
navigator.mediaSession.setActionHandler('seekbackward', () => {
handleBackward(true);
});
navigator.mediaSession.setActionHandler('nexttrack', () => {
handleForward();
});
navigator.mediaSession.setActionHandler('previoustrack', () => {
handleBackward();
});
}
@@ -464,13 +473,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
return (
<>
{showPanel && (
<div
className='fixed inset-0'
onClick={handleDismissPopup}
onContextMenu={handleDismissPopup}
/>
)}
{showPanel && <Overlay onDismiss={handleDismissPopup} />}
{showIndicator && (
<div
ref={iconRef}
@@ -11,7 +11,7 @@ const TTSIcon: React.FC<TTSIconProps> = ({ isPlaying, ttsInited, onClick }) => {
const bars = [1, 2, 3, 4];
return (
<div
<button
className={clsx(
'relative h-full w-full rounded-full',
ttsInited ? 'cursor-pointer' : 'cursor-not-allowed',
@@ -59,7 +59,7 @@ const TTSIcon: React.FC<TTSIconProps> = ({ isPlaying, ttsInited, onClick }) => {
))}
</div>
</div>
</div>
</button>
);
};
@@ -281,6 +281,7 @@ const TTSPanel = ({
)}
</button>
<ul
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
className={clsx(
'dropdown-content bgcolor-base-200 no-triangle menu menu-vertical rounded-box absolute right-0 z-[1] shadow',
@@ -288,6 +289,7 @@ const TTSPanel = ({
)}
>
{timeoutOptions.map((option, index) => (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
<li
key={`${index}-${option.value}`}
onClick={() => onSelectTimeout(bookKey, option.value)}
@@ -317,6 +319,7 @@ const TTSPanel = ({
<RiVoiceAiFill size={iconSize32} />
</button>
<ul
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
className={clsx(
'dropdown-content bgcolor-base-200 no-triangle menu menu-vertical rounded-box absolute right-0 z-[1] shadow',
@@ -338,6 +341,7 @@ const TTSPanel = ({
</span>
</div>
{voiceGroup.voices.map((voice, voiceIndex) => (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
<li
key={`${index}-${voiceGroup.id}-${voiceIndex}`}
onClick={() => !voice.disabled && handleSelectVoice(voice.id, voice.lang)}
@@ -3,6 +3,7 @@ import { useNotebookStore } from '@/store/notebookStore';
import { isTauriAppPlatform } from '@/services/environment';
import { useSidebarStore } from '@/store/sidebarStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { getStyles } from '@/utils/style';
import { tauriHandleToggleFullScreen, tauriQuitApp } from '@/utils/window';
import { eventDispatcher } from '@/utils/event';
@@ -20,6 +21,7 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
const { getView, getViewState, getViewSettings, setViewSettings } = useReaderStore();
const { toggleSideBar, setSideBarBookKey } = useSidebarStore();
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
const { getBookData } = useBookDataStore();
const { toggleNotebook } = useNotebookStore();
const { getNextBookKey } = useBooksManager();
const viewSettings = getViewSettings(sideBarBookKey ?? '');
@@ -104,36 +106,36 @@ const useBookShortcuts = ({ sideBarBookKey, bookKeys }: UseBookShortcutsProps) =
eventDispatcher.dispatch('search', { term: '' });
};
const zoomIn = () => {
const applyZoomLevel = (zoomLevel: number) => {
if (!sideBarBookKey) return;
const view = getView(sideBarBookKey);
if (!view?.renderer?.setStyles) return;
const bookData = getBookData(sideBarBookKey);
const viewSettings = getViewSettings(sideBarBookKey)!;
const zoomLevel = viewSettings!.zoomLevel + ZOOM_STEP;
viewSettings!.zoomLevel = Math.min(zoomLevel, MAX_ZOOM_LEVEL);
viewSettings!.zoomLevel = zoomLevel;
setViewSettings(sideBarBookKey, viewSettings!);
view?.renderer.setStyles?.(getStyles(viewSettings!));
if (bookData?.bookDoc?.rendition?.layout === 'pre-paginated') {
view?.renderer.setAttribute('scale-factor', zoomLevel);
}
};
const zoomIn = () => {
if (!sideBarBookKey) return;
const viewSettings = getViewSettings(sideBarBookKey)!;
const zoomLevel = viewSettings!.zoomLevel + ZOOM_STEP;
applyZoomLevel(Math.min(zoomLevel, MAX_ZOOM_LEVEL));
};
const zoomOut = () => {
if (!sideBarBookKey) return;
const view = getView(sideBarBookKey);
if (!view?.renderer?.setStyles) return;
const viewSettings = getViewSettings(sideBarBookKey)!;
const zoomLevel = viewSettings!.zoomLevel - ZOOM_STEP;
viewSettings!.zoomLevel = Math.max(zoomLevel, MIN_ZOOM_LEVEL);
setViewSettings(sideBarBookKey, viewSettings!);
view?.renderer.setStyles?.(getStyles(viewSettings!));
applyZoomLevel(Math.max(zoomLevel, MIN_ZOOM_LEVEL));
};
const resetZoom = () => {
if (!sideBarBookKey) return;
const view = getView(sideBarBookKey);
if (!view?.renderer?.setStyles) return;
const viewSettings = getViewSettings(sideBarBookKey)!;
viewSettings!.zoomLevel = 100;
setViewSettings(sideBarBookKey, viewSettings!);
view?.renderer.setStyles?.(getStyles(viewSettings!));
applyZoomLevel(100);
};
const toggleTTS = () => {
@@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { debounce } from '@/utils/debounce';
import { ScrollSource } from './usePagination';
@@ -58,8 +59,10 @@ interface IframeTouchEvent {
export const useTouchEvent = (
bookKey: string,
handlePageFlip: (msg: CustomEvent) => void,
handleContinuousScroll: (source: ScrollSource, delta: number, threshold: number) => void,
) => {
const { getBookData } = useBookDataStore();
const { hoveredBookKey, setHoveredBookKey, getViewSettings } = useReaderStore();
let touchStart: IframeTouch | null = null;
@@ -103,6 +106,7 @@ export const useTouchEvent = (
const windowWidth = window.innerWidth;
if (touchEnd) {
const viewSettings = getViewSettings(bookKey)!;
const bookData = getBookData(bookKey)!;
const deltaY = touchEnd.screenY - touchStart.screenY;
const deltaX = touchEnd.screenX - touchStart.screenX;
// also check for deltaX to prevent swipe page turn from triggering the toggle
@@ -112,7 +116,11 @@ export const useTouchEvent = (
Math.abs(deltaX) < windowWidth * 0.3
) {
// swipe up to toggle the header bar and the footer bar, only for horizontal page mode
if (!viewSettings!.scrolled && !viewSettings!.vertical) {
if (
!viewSettings!.scrolled && // not scrolled
!viewSettings!.vertical && // not vertical
(!bookData.isFixedLayout || viewSettings.zoomLevel <= 100) // for fixed layout, not when zoomed in
) {
setHoveredBookKey(hoveredBookKey ? null : bookKey);
}
} else {
@@ -120,6 +128,18 @@ export const useTouchEvent = (
setHoveredBookKey(null);
}
}
handlePageFlip(
new CustomEvent('touch-swipe', {
detail: {
deltaX,
deltaY,
startX: touchStart.screenX,
startY: touchStart.screenY,
endX: touchEnd.screenX,
endY: touchEnd.screenY,
},
}),
);
handleContinuousScroll('touch', deltaY, 30);
}
@@ -9,20 +9,24 @@ import { debounce } from '@/utils/debounce';
export const useNotesSync = (bookKey: string) => {
const { user } = useAuth();
const { syncedNotes, syncNotes, lastSyncedAtNotes } = useSync(bookKey);
const { getConfig, setConfig } = useBookDataStore();
const { getConfig, setConfig, getBookData } = useBookDataStore();
const config = getConfig(bookKey);
const bookHash = bookKey.split('-')[0]!;
const getNewNotes = () => {
const config = getConfig(bookKey);
if (!config?.location || !user) return [];
const book = getBookData(bookKey)?.book;
if (!config?.location || !book || !user) return [];
const metaHash = book.metaHash;
const bookNotes = config.booknotes ?? [];
const newNotes = bookNotes.filter(
(note) => lastSyncedAtNotes < note.updatedAt || lastSyncedAtNotes < (note.deletedAt ?? 0),
);
newNotes.forEach((note) => {
note.bookHash = bookHash;
note.metaHash = metaHash;
});
return newNotes;
};
@@ -30,8 +34,9 @@ export const useNotesSync = (bookKey: string) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const handleAutoSync = useCallback(
debounce(() => {
const book = getBookData(bookKey)?.book;
const newNotes = getNewNotes();
syncNotes(newNotes, bookHash, 'both');
syncNotes(newNotes, bookHash, book?.metaHash, 'both');
}, SYNC_NOTES_INTERVAL_SEC * 1000),
[syncNotes],
);
@@ -40,7 +45,7 @@ export const useNotesSync = (bookKey: string) => {
if (!config?.location || !user) return;
handleAutoSync();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config, handleAutoSync]);
}, [config?.booknotes, handleAutoSync]);
useEffect(() => {
const processNewNote = (note: BookNote) => {
@@ -60,7 +65,10 @@ export const useNotesSync = (bookKey: string) => {
return note;
};
if (syncedNotes?.length && config) {
const newNotes = syncedNotes.filter((note) => note.bookHash === bookHash);
const book = getBookData(bookKey)?.book;
const newNotes = syncedNotes.filter(
(note) => note.bookHash === bookHash || note.metaHash === book?.metaHash,
);
if (!newNotes.length) return;
const oldNotes = config.booknotes ?? [];
const mergedNotes = [
@@ -3,6 +3,7 @@ import { useEnv } from '@/context/EnvContext';
import { FoliateView } from '@/types/view';
import { ViewSettings } from '@/types/book';
import { useReaderStore } from '@/store/readerStore';
import { useBookDataStore } from '@/store/bookDataStore';
import { useDeviceControlStore } from '@/store/deviceStore';
import { eventDispatcher } from '@/utils/event';
import { isTauriAppPlatform } from '@/services/environment';
@@ -10,25 +11,33 @@ import { tauriGetWindowLogicalPosition } from '@/utils/window';
export type ScrollSource = 'touch' | 'mouse';
type PaginationSide = 'left' | 'right' | 'up' | 'down';
const swapLeftRight = (side: PaginationSide) => {
if (side === 'left') return 'right';
if (side === 'right') return 'left';
return side;
};
export const viewPagination = (
view: FoliateView | null,
viewSettings: ViewSettings | null | undefined,
side: 'left' | 'right',
side: PaginationSide,
) => {
if (!view || !viewSettings) return;
const renderer = view.renderer;
if (view.book.dir === 'rtl') {
side = swapLeftRight(side);
}
if (renderer.scrolled) {
if (view.book.dir === 'rtl') {
side = side === 'left' ? 'right' : 'left';
}
const { size } = renderer;
const showHeader = viewSettings.showHeader && viewSettings.showBarsOnScroll;
const showFooter = viewSettings.showFooter && viewSettings.showBarsOnScroll;
const scrollingOverlap = viewSettings.scrollingOverlap;
const distance = size - scrollingOverlap - (showHeader ? 44 : 0) - (showFooter ? 44 : 0);
return side === 'left' ? view.prev(distance) : view.next(distance);
return side === 'left' || side === 'up' ? view.prev(distance) : view.next(distance);
} else {
return side === 'left' ? view.goLeft() : view.goRight();
return side === 'left' || side === 'up' ? view.prev() : view.next();
}
};
@@ -38,13 +47,18 @@ export const usePagination = (
containerRef: React.RefObject<HTMLDivElement>,
) => {
const { appService } = useEnv();
const { getViewSettings } = useReaderStore();
const { getBookData } = useBookDataStore();
const { getViewSettings, getViewState } = useReaderStore();
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
const { acquireVolumeKeyInterception, releaseVolumeKeyInterception } = useDeviceControlStore();
const handlePageFlip = async (
msg: MessageEvent | CustomEvent | React.MouseEvent<HTMLDivElement, MouseEvent>,
) => {
const viewState = getViewState(bookKey);
const bookData = getBookData(bookKey);
if (!viewState?.inited || !bookData) return;
if (msg instanceof MessageEvent) {
if (msg.data && msg.data.bookKey === bookKey) {
const viewSettings = getViewSettings(bookKey)!;
@@ -118,14 +132,27 @@ export const usePagination = (
}
}
} else if (msg instanceof CustomEvent) {
const { keyName } = msg.detail;
const viewSettings = getViewSettings(bookKey);
if (viewSettings?.volumeKeysToFlip) {
if (msg.type === 'native-key-down' && viewSettings?.volumeKeysToFlip) {
const { keyName } = msg.detail;
setHoveredBookKey('');
if (keyName === 'VolumeUp') {
viewPagination(viewRef.current, viewSettings, 'left');
viewPagination(viewRef.current, viewSettings, 'up');
} else if (keyName === 'VolumeDown') {
viewPagination(viewRef.current, viewSettings, 'right');
viewPagination(viewRef.current, viewSettings, 'down');
}
} else if (
msg.type === 'touch-swipe' &&
bookData.bookDoc?.rendition?.layout === 'pre-paginated' &&
viewSettings?.zoomLevel === 100
) {
const { deltaX, deltaY } = msg.detail;
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 30) {
if (deltaX > 0) {
viewPagination(viewRef.current, viewSettings, 'left');
} else {
viewPagination(viewRef.current, viewSettings, 'right');
}
}
}
} else {
@@ -147,6 +174,10 @@ export const usePagination = (
const handleContinuousScroll = (mode: ScrollSource, scrollDelta: number, threshold: number) => {
const renderer = viewRef.current?.renderer;
const viewSettings = getViewSettings(bookKey)!;
const bookData = getBookData(bookKey)!;
// Currently continuous scroll is not supported in pre-paginated layout
if (bookData.bookDoc?.rendition?.layout === 'pre-paginated') return;
if (renderer && viewSettings.scrolled && viewSettings.continuousScroll) {
const doScroll = () => {
// may have overscroll where the start is greater than 0
@@ -13,10 +13,12 @@ export const useProgressAutoSave = (bookKey: string) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const saveBookConfig = useCallback(
throttle(async () => {
const config = getConfig(bookKey)!;
const settings = useSettingsStore.getState().settings;
await saveConfig(envConfig, bookKey, config, settings);
throttle(() => {
setTimeout(async () => {
const config = getConfig(bookKey)!;
const settings = useSettingsStore.getState().settings;
await saveConfig(envConfig, bookKey, config, settings);
}, 5000);
}, 10000),
[],
);
@@ -16,31 +16,34 @@ import { getCFIFromXPointer, getXPointerFromCFI, normalizeProgressXPointer } fro
export const useProgressSync = (bookKey: string) => {
const _ = useTranslation();
const { getConfig, setConfig, getBookData } = useBookDataStore();
const { getView, getProgress } = useReaderStore();
const { getView, getProgress, setHoveredBookKey } = useReaderStore();
const { settings } = useSettingsStore();
const { syncedConfigs, syncConfigs } = useSync(bookKey);
const { user } = useAuth();
const config = getConfig(bookKey);
const progress = getProgress(bookKey);
const configPulled = useRef(false);
const hasPulledConfigOnce = useRef(false);
const pushConfig = (bookKey: string, config: BookConfig | null) => {
if (!config || !user) return;
const pushConfig = async (bookKey: string, config: BookConfig | null) => {
const book = getBookData(bookKey)?.book;
if (!config || !book || !user) return;
const bookHash = bookKey.split('-')[0]!;
const newConfig = { ...config, bookHash };
const metaHash = book.metaHash;
const newConfig = { ...config, bookHash, metaHash };
const compressedConfig = JSON.parse(
serializeConfig(newConfig, settings.globalViewSettings, DEFAULT_BOOK_SEARCH_CONFIG),
);
delete compressedConfig.booknotes;
syncConfigs([compressedConfig], bookHash, 'push');
await syncConfigs([compressedConfig], bookHash, metaHash, 'push');
};
const pullConfig = (bookKey: string) => {
if (!user) return;
const pullConfig = async (bookKey: string) => {
const book = getBookData(bookKey)?.book;
if (!user || !book) return;
const bookHash = bookKey.split('-')[0]!;
syncConfigs([], bookHash, 'pull');
const metaHash = book.metaHash;
await syncConfigs([], bookHash, metaHash, 'pull');
};
const syncConfig = async () => {
@@ -66,10 +69,11 @@ export const useProgressSync = (bookKey: string) => {
}
};
const handleSyncBookProgress = (event: CustomEvent) => {
const handleSyncBookProgress = async (event: CustomEvent) => {
const { bookKey: syncBookKey } = event.detail;
if (syncBookKey === bookKey) {
syncConfig();
configPulled.current = false;
await pullConfig(bookKey);
}
};
@@ -95,7 +99,7 @@ export const useProgressSync = (bookKey: string) => {
if (!progress?.location || !user) return;
handleAutoSync();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [progress]);
}, [progress?.location]);
// Pull: pull progress once when the book is opened
useEffect(() => {
@@ -105,9 +109,16 @@ export const useProgressSync = (bookKey: string) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [progress]);
const applyRemoteProgress = useCallback(async () => {
if (!syncedConfigs || syncedConfigs.length === 0) return;
const syncedConfig = syncedConfigs.filter((c) => c.bookHash === bookKey.split('-')[0])[0];
const applyRemoteProgress = async (syncedConfigs: BookConfig[]) => {
const config = getConfig(bookKey);
const book = getBookData(bookKey)?.book;
if (!syncedConfigs || syncedConfigs.length === 0 || !config || !book) return;
const bookHash = bookKey.split('-')[0]!;
const metaHash = book.metaHash;
const syncedConfig = syncedConfigs.filter(
(c) => c.bookHash === bookHash || c.metaHash === metaHash,
)[0];
if (syncedConfig) {
const configCFI = config?.location;
let remoteCFILocation = syncedConfig.location;
@@ -132,6 +143,7 @@ export const useProgressSync = (bookKey: string) => {
if (CFI.compare(configCFI, remoteCFILocation) < 0) {
if (view) {
view.goTo(remoteCFILocation);
setHoveredBookKey(null);
eventDispatcher.dispatch('hint', {
bookKey,
message: _('Reading Progress Synced'),
@@ -140,17 +152,16 @@ export const useProgressSync = (bookKey: string) => {
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [syncedConfigs, config?.location]);
};
// Pull: proccess the pulled progress
useEffect(() => {
if (!configPulled.current && syncedConfigs) {
configPulled.current = true;
applyRemoteProgress().catch((error) => {
applyRemoteProgress(syncedConfigs).catch((error) => {
console.error('Failed to apply remote progress', error);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [applyRemoteProgress]);
}, [syncedConfigs]);
};
@@ -8,6 +8,7 @@ const useSidebar = (initialWidth: string, isPinned: boolean) => {
sideBarWidth,
isSideBarVisible,
isSideBarPinned,
getSideBarWidth,
setSideBarWidth,
setSideBarVisible,
setSideBarPin,
@@ -37,6 +38,7 @@ const useSidebar = (initialWidth: string, isPinned: boolean) => {
sideBarWidth,
isSideBarPinned,
isSideBarVisible,
getSideBarWidth,
handleSideBarResize,
handleSideBarTogglePin,
setSideBarVisible,
@@ -2,6 +2,7 @@ import clsx from 'clsx';
import { useRef } from 'react';
import { IoArrowBack } from 'react-icons/io5';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import WindowButtons from '@/components/WindowButtons';
@@ -10,6 +11,7 @@ interface ProfileHeaderProps {
}
const ProfileHeader: React.FC<ProfileHeaderProps> = ({ onGoBack }) => {
const _ = useTranslation();
const { appService } = useEnv();
const { isTrafficLightVisible } = useTrafficLightStore();
const headerRef = useRef<HTMLDivElement>(null);
@@ -23,6 +25,7 @@ const ProfileHeader: React.FC<ProfileHeaderProps> = ({ onGoBack }) => {
)}
>
<button
aria-label={_('Go Back')}
onClick={onGoBack}
className={clsx('btn btn-ghost h-12 min-h-12 w-12 p-0 sm:h-8 sm:min-h-8 sm:w-8')}
>
@@ -48,7 +48,7 @@ const PlanCard: React.FC<PlanCardProps> = ({
</div>
</div>
<div className='mb-6 space-y-3' onClick={() => onSelectPlan(index)}>
<div role='none' className='mb-6 space-y-3' onClick={() => onSelectPlan(index)}>
{plan.features.map((feature, featureIndex) => (
<div key={featureIndex} className='flex flex-col'>
<div className='flex items-center gap-2'>
@@ -64,7 +64,11 @@ const PlanCard: React.FC<PlanCardProps> = ({
))}
</div>
<div className='mb-6 rounded-lg bg-white/50 p-4' onClick={() => onSelectPlan(index)}>
<div
role='none'
className='mb-6 rounded-lg bg-white/50 p-4'
onClick={() => onSelectPlan(index)}
>
<h5 className='mb-3 font-semibold'>{_('Plan Limits')}</h5>
<div className='space-y-2'>
{Object.entries(plan.limits).map(([key, value]) => (
+2 -3
View File
@@ -59,7 +59,7 @@ const ProfilePage = () => {
const { envConfig, appService } = useEnv();
const { token, user, logout } = useAuth();
const { settings, setSettings, saveSettings } = useSettingsStore();
const { safeAreaInsets } = useThemeStore();
const { safeAreaInsets, isRoundedWindow } = useThemeStore();
const [loading, setLoading] = useState(false);
const [availablePlans, setAvailablePlans] = useState<AvailablePlan[]>([]);
@@ -342,8 +342,7 @@ const ProfilePage = () => {
className={clsx(
'bg-base-100 inset-0 select-none overflow-hidden',
appService?.isIOSApp ? 'h-[100vh]' : 'h-dvh',
appService?.isLinuxApp && 'window-border',
appService?.hasRoundedWindow && 'rounded-window',
appService?.hasRoundedWindow && isRoundedWindow && 'window-border rounded-window',
)}
>
<div
+63 -57
View File
@@ -5,6 +5,7 @@ import { useTranslation } from '@/hooks/useTranslation';
import { checkForAppUpdates, checkAppReleaseNotes } from '@/helpers/updater';
import { parseWebViewVersion } from '@/utils/ua';
import { getAppVersion } from '@/utils/version';
import SupportLinks from './SupportLinks';
import LegalLinks from './LegalLinks';
import Dialog from './Dialog';
import Link from './Link';
@@ -83,69 +84,74 @@ export const AboutWindow = () => {
isOpen={isOpen}
title={_('About Readest')}
onClose={handleClose}
boxClassName='sm:!w-96 sm:h-auto'
boxClassName='sm:!w-[480px] sm:!max-w-screen-sm sm:h-auto'
>
<div className='about-content flex h-full flex-col items-center justify-center'>
<div className='flex flex-col items-center gap-2 px-8'>
<div className='mb-2 mt-8'>
<Image src='/icon.png' alt='App Logo' className='h-20 w-20' width={64} height={64} />
{isOpen && (
<div className='about-content flex h-full flex-col items-center justify-center'>
<div className='flex flex-col items-center gap-2 px-8'>
<div className='mb-2 mt-8'>
<Image src='/icon.png' alt='App Logo' className='h-20 w-20' width={64} height={64} />
</div>
<div className='flex select-text flex-col items-center'>
<h2 className='mb-2 text-2xl font-bold'>Readest</h2>
<p className='text-neutral-content text-center text-sm'>
{_('Version {{version}}', { version: getAppVersion() })} {`(${browserInfo})`}
</p>
</div>
<div className='my-1 h-5'>
{!updateStatus && (
<button
className='badge badge-primary cursor-pointer p-2'
onClick={appService?.hasUpdater ? handleCheckUpdate : handleShowRecentUpdates}
>
{_('Check Update')}
</button>
)}
{updateStatus === 'updated' && (
<p className='text-neutral-content mt-2 text-xs'>
{_('Already the latest version')}
</p>
)}
{updateStatus === 'checking' && (
<p className='text-neutral-content mt-2 text-xs'>{_('Checking for updates...')}</p>
)}
{updateStatus === 'error' && (
<p className='text-error mt-2 text-xs'>{_('Error checking for updates')}</p>
)}
</div>
</div>
<div className='flex select-text flex-col items-center'>
<h2 className='mb-2 text-2xl font-bold'>Readest</h2>
<p className='text-neutral-content text-center text-sm'>
{_('Version {{version}}', { version: getAppVersion() })} {`(${browserInfo})`}
<div className='divider py-16 sm:py-2'></div>
<div className='flex flex-col items-center gap-2 px-4 text-center' dir='ltr'>
<p className='text-neutral-content text-sm'>
© {new Date().getFullYear()} Bilingify LLC. All rights reserved.
</p>
</div>
<div className='h-5'>
{!updateStatus && (
<span
className='badge badge-primary cursor-pointer p-2'
onClick={appService?.hasUpdater ? handleCheckUpdate : handleShowRecentUpdates}
<p className='text-neutral-content text-xs'>
This software is licensed under the{' '}
<Link
href='https://www.gnu.org/licenses/agpl-3.0.html'
className='text-blue-500 underline'
>
{_('Check Update')}
</span>
)}
{updateStatus === 'updated' && (
<p className='text-neutral-content mt-2 text-xs'>{_('Already the latest version')}</p>
)}
{updateStatus === 'checking' && (
<p className='text-neutral-content mt-2 text-xs'>{_('Checking for updates...')}</p>
)}
{updateStatus === 'error' && (
<p className='text-error mt-2 text-xs'>{_('Error checking for updates')}</p>
)}
GNU Affero General Public License v3.0
</Link>
. You are free to use, modify, and distribute this software under the terms of the
AGPL v3 license. Please see the license for more details.
</p>
<p className='text-neutral-content text-xs'>
Source code is available at{' '}
<Link href='https://github.com/readest/readest' className='text-blue-500 underline'>
GitHub
</Link>
.
</p>
<LegalLinks />
<SupportLinks />
</div>
</div>
<div className='divider py-16 sm:py-2'></div>
<div className='flex flex-col items-center px-4 text-center' dir='ltr'>
<p className='text-neutral-content text-sm'>
© {new Date().getFullYear()} Bilingify LLC. All rights reserved.
</p>
<p className='text-neutral-content mt-2 text-xs'>
This software is licensed under the{' '}
<Link
href='https://www.gnu.org/licenses/agpl-3.0.html'
className='text-blue-500 underline'
>
GNU Affero General Public License v3.0
</Link>
. You are free to use, modify, and distribute this software under the terms of the AGPL
v3 license. Please see the license for more details.
</p>
<p className='text-neutral-content my-2 text-xs'>
Source code is available at{' '}
<Link href='https://github.com/readest/readest' className='text-blue-500 underline'>
GitHub
</Link>
.
</p>
<LegalLinks />
</div>
</div>
)}
</Dialog>
);
};
+12 -30
View File
@@ -6,44 +6,26 @@ interface ButtonProps {
icon: React.ReactNode;
onClick: () => void;
disabled?: boolean;
tooltip?: string;
tooltipDirection?: 'top' | 'bottom' | 'left' | 'right';
label?: string;
className?: string;
}
const Button: React.FC<ButtonProps> = ({
icon,
onClick,
disabled = false,
tooltip,
tooltipDirection = 'top',
className,
}) => {
const Button: React.FC<ButtonProps> = ({ icon, onClick, disabled = false, label, className }) => {
const { appService } = useEnv();
return (
<div
<button
className={clsx(
'lg:tooltip z-50 h-8 min-h-8 w-8',
tooltip && `lg:tooltip-${tooltipDirection}`,
{
'tooltip-hidden': !tooltip,
},
'btn btn-ghost h-8 min-h-8 w-8 p-0',
appService?.isMobileApp && 'hover:bg-transparent',
disabled && 'cursor-default !bg-transparent opacity-50',
className,
)}
data-tip={tooltip}
title={label}
aria-label={label}
onClick={disabled ? undefined : onClick}
>
<button
className={clsx(
'btn btn-ghost h-8 min-h-8 w-8 p-0',
appService?.isMobileApp && 'hover:bg-transparent',
disabled && 'btn-disabled !bg-transparent opacity-50',
className,
)}
onClick={disabled ? undefined : onClick}
disabled={disabled}
>
{icon}
</button>
</div>
{icon}
</button>
);
};

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