a11y: add screen reader support (#2040)

Tested with VoiceOver on iOS and macOS
This commit is contained in:
Huang Xin
2025-09-15 04:00:42 +08:00
committed by GitHub
parent b1fb477359
commit bb34b1ba59
55 changed files with 870 additions and 344 deletions
@@ -81,7 +81,6 @@
"Search...": "بحث...",
"Select Book": "تحديد الكتاب",
"Select Books": "تحديد الكتب",
"Select Multiple Books": "تحديد عدة كتب",
"Sepia": "بني داكن",
"Serif Font": "خط مذيل (Serif)",
"Show Book Details": "عرض تفاصيل الكتاب",
@@ -541,8 +540,23 @@
"Separate Cover Page": "صفحة غلاف منفصلة",
"Resize Notebook": "تغيير حجم المفكرة",
"Resize Sidebar": "تغيير حجم الشريط الجانبي",
"Menu": "القائمة",
"Dismiss": "رفض",
"Get Help from the Readest Community": "الحصول على المساعدة من مجتمع ريديست",
"Remove cover image": "إزالة صورة الغلاف"
"Remove cover image": "إزالة صورة الغلاف",
"Bookshelf": "رف الكتب",
"View Menu": "عرض القائمة",
"Settings Menu": "إعدادات القائمة",
"View account details and quota": "عرض تفاصيل الحساب والحصة المخصصة",
"Library Header": "رأس المكتبة",
"Books Grid": "شبكة الكتب",
"Book Content": "محتوى الكتاب",
"Footer Bar": "شريط التذييل",
"Header Bar": "شريط الرأس",
"View Options": "خيارات العرض",
"Progress Information": "معلومات التقدم",
"Book Menu": "قائمة الكتاب",
"Search Options": "خيارات البحث",
"Close": "إغلاق",
"Delete Book Options": "خيارات حذف الكتاب",
"ON": "تشغيل",
"OFF": "إيقاف"
}
@@ -66,7 +66,6 @@
"Search Books...": "বই খুঁজুন...",
"Clear Search": "খোঁজ পরিষ্কার করুন",
"Import Books": "বই আমদানি করুন",
"Select Multiple Books": "একাধিক বই নির্বাচন করুন",
"Select Books": "বই নির্বাচন করুন",
"Deselect": "অনির্বাচিত করুন",
"Select All": "সব নির্বাচন করুন",
@@ -525,8 +524,23 @@
"Separate Cover Page": "আলাদা কভার পৃষ্ঠা",
"Resize Notebook": "নোটবুকের আকার পরিবর্তন",
"Resize Sidebar": "সাইডবারের আকার পরিবর্তন",
"Menu": "মেনু",
"Dismiss": "বাতিল",
"Get Help from the Readest Community": "রিডেস্ট কমিউনিটি থেকে সাহায্য নিন",
"Remove cover image": "মলাট ছবি সরান"
"Remove cover image": "মলাট ছবি সরান",
"Bookshelf": "বইয়ের তাক",
"View Menu": "মেনু দেখুন",
"Settings Menu": "সেটিংস মেনু",
"View account details and quota": "অ্যাকাউন্টের বিবরণ এবং কোটার তথ্য দেখুন",
"Library Header": "লাইব্রেরি হেডার",
"Books Grid": "বইয়ের গ্রিড",
"Book Content": "বইয়ের বিষয়বস্তু",
"Footer Bar": "ফুটার বার",
"Header Bar": "হেডার বার",
"View Options": "দেখার অপশন",
"Progress Information": "অগ্রগতির তথ্য",
"Book Menu": "বইয়ের মেনু",
"Search Options": "অনুসন্ধানের অপশন",
"Close": "বন্ধ করুন",
"Delete Book Options": "বই মুছে ফেলার অপশন",
"ON": "চালু",
"OFF": "বন্ধ"
}
@@ -82,7 +82,6 @@
"Search...": "འཚོལ་བཤེར།...",
"Select Book": "དཔེ་དེབ་འདེམས་པ།",
"Select Books": "དཔེ་དེབ་འདེམས་པ།",
"Select Multiple Books": "དཔེ་དེབ་མང་པོ་འདེམས་པ།",
"Sepia": "རྙིང་པའི་ཉམས་འགྱུར།",
"Serif Font": "མཐའ་ཐིག་ཡོད་པའི་ཡིག་གཟུགས།",
"Show Book Details": "དཔེ་དེབ་ཀྱི་གནས་ཚུལ་རྒྱས་པ་མངོན་པ།",
@@ -521,8 +520,23 @@
"Separate Cover Page": "སྟོང་ངོས་གི་ཤོག་དཀར་པ།",
"Resize Notebook": "དེབ་སྣོད་གཅོད་སྒྲིག",
"Resize Sidebar": "གཡས་སྒོ་གཅོད་སྒྲིག",
"Menu": "སྒེའུ་ཁུང་།",
"Dismiss": "བརྟེན་སྒྲིག",
"Get Help from the Readest Community": "Readest མཛའ་བརྩེའི་རོགས་རམ་ལོངས་སུ་རོགས་རམ་ཐོབ།",
"Remove cover image": "སྟོང་ངོས་ཀྱི་པར་རིས་ཕྱིར་འཐེན།"
"Remove cover image": "སྟོང་ངོས་ཀྱི་པར་རིས་ཕྱིར་འཐེན།",
"Bookshelf": "དེབ་སྡེབས་",
"View Menu": "མ་ལག་བལྟ་བ",
"Settings Menu": "སྒྲིག་སྟངས་མ་ལག",
"View account details and quota": "རྩིས་ཐོའི་རྒྱུ་ཆ་དང་ཚད་བརྡ་བལྟ་བ",
"Library Header": "དེབ་མཛོད་མགོ་ཡིག",
"Books Grid": "དེབ་ཀྱི་དཔེ་རིས",
"Book Content": "དེབ་ནང་དོན",
"Footer Bar": "མཇུག་སྒམ་ཕྱོགས",
"Header Bar": "མགོ་སྒམ་ཕྱོགས",
"View Options": "བལྟ་བའི་གདམ་ཁ",
"Progress Information": "རྒྱུན་ལས་བརྡ་དོན",
"Book Menu": "དེབ་ཀྱི་མ་ལག",
"Search Options": "འཚོལ་བའི་གདམ་ཁ",
"Close": "ཁ་རྒྱག",
"Delete Book Options": "དེབ་བཏོན་གཏང་གི་གདམ་ཁ",
"ON": "འབྱུང་ཁུངས",
"OFF": "བརྗེ་བ"
}
@@ -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",
@@ -525,8 +524,23 @@
"Separate Cover Page": "Titelseite separat anzeigen",
"Resize Notebook": "Notizbuchgröße ändern",
"Resize Sidebar": "Seitenleisten-Größe ändern",
"Menu": "Menü",
"Dismiss": "Schließen",
"Get Help from the Readest Community": "Hilfe von der Readest-Community erhalten",
"Remove cover image": "Coverbild entfernen"
"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",
"Books Grid": "Bücherübersicht",
"Book Content": "Buchinhalt",
"Footer Bar": "Fußzeile",
"Header Bar": "Kopfzeile",
"View Options": "Ansichtsoptionen",
"Progress Information": "Fortschrittsinformationen",
"Book Menu": "Buchmenü",
"Search Options": "Suchoptionen",
"Close": "Schließen",
"Delete Book Options": "Buch-Löschoptionen",
"ON": "EIN",
"OFF": "AUS"
}
@@ -82,7 +82,6 @@
"Search...": "Αναζήτηση...",
"Select Book": "Επιλογή βιβλίου",
"Select Books": "Επιλογή βιβλίων",
"Select Multiple Books": "Επιλογή πολλαπλών βιβλίων",
"Sepia": "Σέπια",
"Serif Font": "Γραμματοσειρά Serif",
"Show Book Details": "Εμφάνιση λεπτομερειών βιβλίου",
@@ -525,8 +524,23 @@
"Separate Cover Page": "Ξεχωριστή σελίδα εξωφύλλου",
"Resize Notebook": "Αλλαγή μεγέθους σημειωματάριου",
"Resize Sidebar": "Αλλαγή μεγέθους πλαϊνής γραμμής",
"Menu": "Μενού",
"Dismiss": "Απόρριψη",
"Get Help from the Readest Community": "Λάβετε βοήθεια από την κοινότητα Readest",
"Remove cover image": "Αφαίρεση εικόνας εξωφύλλου"
"Remove cover image": "Αφαίρεση εικόνας εξωφύλλου",
"Bookshelf": "Ράφι βιβλίων",
"View Menu": "Προβολή μενού",
"Settings Menu": "Μενού ρυθμίσεων",
"View account details and quota": "Προβολή στοιχείων λογαριασμού και ορίου",
"Library Header": "Κεφαλίδα βιβλιοθήκης",
"Books Grid": "Πλέγμα βιβλίων",
"Book Content": "Περιεχόμενο βιβλίου",
"Footer Bar": "Γραμμή υποσέλιδου",
"Header Bar": "Γραμμή κεφαλίδας",
"View Options": "Επιλογές προβολής",
"Progress Information": "Πληροφορίες προόδου",
"Book Menu": "Μενού βιβλίου",
"Search Options": "Επιλογές αναζήτησης",
"Close": "Κλείσιμο",
"Delete Book Options": "Επιλογές διαγραφής βιβλίου",
"ON": "ΕΝΕΡΓΟΠΟΙΗΣΗ",
"OFF": "ΑΠΕΝΕΡΓΟΠΟΙΗΣΗ"
}
@@ -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",
@@ -529,8 +528,23 @@
"Separate Cover Page": "Portada separada",
"Resize Notebook": "Redimensionar cuaderno",
"Resize Sidebar": "Redimensionar barra lateral",
"Menu": "Menú",
"Dismiss": "Descartar",
"Get Help from the Readest Community": "Obtén ayuda de la comunidad de Readest",
"Remove cover image": "Eliminar imagen de portada"
"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",
"Books Grid": "Cuadrícula de Libros",
"Book Content": "Contenido del Libro",
"Footer Bar": "Barra de Pie de Página",
"Header Bar": "Barra de Encabezado",
"View Options": "Opciones de Vista",
"Progress Information": "Información de Progreso",
"Book Menu": "Menú de Libro",
"Search Options": "Opciones de Búsqueda",
"Close": "Cerrar",
"Delete Book Options": "Opciones de Eliminar Libro",
"ON": "ENCENDIDO",
"OFF": "APAGADO"
}
@@ -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",
@@ -529,8 +528,23 @@
"Separate Cover Page": "Page de couverture séparée",
"Resize Notebook": "Redimensionner le Carnet",
"Resize Sidebar": "Redimensionner la Barre Latérale",
"Menu": "Menu",
"Dismiss": "Fermer",
"Get Help from the Readest Community": "Obtenir de l'aide de la communauté Readest",
"Remove cover image": "Supprimer l'image de couverture"
"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",
"Books Grid": "Grille de livres",
"Book Content": "Contenu du livre",
"Footer Bar": "Barre de pied de page",
"Header Bar": "Barre d'en-tête",
"View Options": "Options d'affichage",
"Progress Information": "Informations sur la progression",
"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"
}
@@ -82,7 +82,6 @@
"Search...": "खोजें...",
"Select Book": "पुस्तक चुनें",
"Select Books": "पुस्तकें चुनें",
"Select Multiple Books": "कई पुस्तकें चुनें",
"Sepia": "सेपिया",
"Serif Font": "सेरिफ फ़ॉन्ट",
"Show Book Details": "पुस्तक विवरण दिखाएं",
@@ -525,8 +524,23 @@
"Separate Cover Page": "अलग कवर पृष्ठ",
"Resize Notebook": "नोटबुक का आकार बदलें",
"Resize Sidebar": "साइडबार का आकार बदलें",
"Menu": "मेनू",
"Dismiss": "खारिज करें",
"Get Help from the Readest Community": "Readest समुदाय से मदद लें",
"Remove cover image": "कवर इमेज हटाएं"
"Remove cover image": "कवर इमेज हटाएं",
"Bookshelf": "पुस्तक शेल्फ",
"View Menu": "मेनू देखें",
"Settings Menu": "सेटिंग्स मेनू",
"View account details and quota": "खाता विवरण और कोटा देखें",
"Library Header": "पुस्तकालय शीर्षक",
"Books Grid": "पुस्तकों की ग्रिड",
"Book Content": "पुस्तक की सामग्री",
"Footer Bar": "फुटर बार",
"Header Bar": "हेडर बार",
"View Options": "देखने के विकल्प",
"Progress Information": "प्रगति जानकारी",
"Book Menu": "पुस्तक मेनू",
"Search Options": "खोज विकल्प",
"Close": "बंद करें",
"Delete Book Options": "पुस्तक हटाने के विकल्प",
"ON": "चालू",
"OFF": "बंद"
}
@@ -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",
@@ -521,8 +520,23 @@
"Separate Cover Page": "Halaman Sampul Terpisah",
"Resize Notebook": "Ubah Ukuran Buku Catatan",
"Resize Sidebar": "Ubah Ukuran Bilah Samping",
"Menu": "Menu",
"Dismiss": "Tutup",
"Get Help from the Readest Community": "Dapatkan Bantuan dari Komunitas Readest",
"Remove cover image": "Hapus gambar sampul"
"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",
"Books Grid": "Grid Buku",
"Book Content": "Isi Buku",
"Footer Bar": "Bilah Footer",
"Header Bar": "Bilah Header",
"View Options": "Opsi Tampilan",
"Progress Information": "Informasi Kemajuan",
"Book Menu": "Menu Buku",
"Search Options": "Opsi Pencarian",
"Close": "Tutup",
"Delete Book Options": "Opsi Hapus Buku",
"ON": "AKTIF",
"OFF": "TIDAK AKTIF"
}
@@ -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",
@@ -529,8 +528,23 @@
"Separate Cover Page": "Pagina di copertina separata",
"Resize Notebook": "Ridimensiona quaderno",
"Resize Sidebar": "Ridimensiona barra laterale",
"Menu": "Menu",
"Dismiss": "Chiudi",
"Get Help from the Readest Community": "Ottieni aiuto dalla community di Readest",
"Remove cover image": "Rimuovi immagine di copertina"
"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",
"Books Grid": "Griglia dei libri",
"Book Content": "Contenuto del libro",
"Footer Bar": "Barra piè di pagina",
"Header Bar": "Barra intestazione",
"View Options": "Opzioni di visualizzazione",
"Progress Information": "Informazioni sul progresso",
"Book Menu": "Menu libro",
"Search Options": "Opzioni di ricerca",
"Close": "Chiudi",
"Delete Book Options": "Opzioni di eliminazione libro",
"ON": "ATTIVO",
"OFF": "DISATTIVO"
}
@@ -82,7 +82,6 @@
"Search...": "検索...",
"Select Book": "書籍を選択",
"Select Books": "書籍を選択",
"Select Multiple Books": "複数の書籍を選択",
"Sepia": "セピア",
"Serif Font": "セリフ体",
"Show Book Details": "書籍の詳細を表示",
@@ -521,8 +520,23 @@
"Separate Cover Page": "別の表紙ページ",
"Resize Notebook": "ノートのサイズ変更",
"Resize Sidebar": "サイドバーのサイズ変更",
"Menu": "メニュー",
"Dismiss": "閉じる",
"Get Help from the Readest Community": "Readestコミュニティからヘルプを得る",
"Remove cover image": "表紙画像を削除"
"Remove cover image": "表紙画像を削除",
"Bookshelf": "本棚",
"View Menu": "メニューを表示",
"Settings Menu": "設定メニュー",
"View account details and quota": "アカウントの詳細とクォータを表示",
"Library Header": "ライブラリヘッダー",
"Books Grid": "本のグリッド",
"Book Content": "本の内容",
"Footer Bar": "フッターバー",
"Header Bar": "ヘッダーバー",
"View Options": "表示オプション",
"Progress Information": "進捗情報",
"Book Menu": "本のメニュー",
"Search Options": "検索オプション",
"Close": "閉じる",
"Delete Book Options": "本の削除オプション",
"ON": "オン",
"OFF": "オフ"
}
@@ -82,7 +82,6 @@
"Search...": "검색...",
"Select Book": "책 선택",
"Select Books": "책 선택",
"Select Multiple Books": "여러 책 선택",
"Sepia": "세피아",
"Serif Font": "세리프체",
"Show Book Details": "책 세부 정보 표시",
@@ -521,8 +520,23 @@
"Separate Cover Page": "별도의 표지 페이지",
"Resize Notebook": "노트북 크기 조정",
"Resize Sidebar": "사이드바 크기 조정",
"Menu": "메뉴",
"Dismiss": "닫기",
"Get Help from the Readest Community": "Readest 커뮤니티에서 도움 받기",
"Remove cover image": "표지 이미지 제거"
"Remove cover image": "표지 이미지 제거",
"Bookshelf": "책장",
"View Menu": "메뉴 보기",
"Settings Menu": "설정 메뉴",
"View account details and quota": "계정 세부정보 및 할당량 보기",
"Library Header": "라이브러리 헤더",
"Books Grid": "도서 그리드",
"Book Content": "도서 내용",
"Footer Bar": "푸터 바",
"Header Bar": "헤더 바",
"View Options": "보기 옵션",
"Progress Information": "진행 정보",
"Book Menu": "도서 메뉴",
"Search Options": "검색 옵션",
"Close": "닫기",
"Delete Book Options": "도서 삭제 옵션",
"ON": "켜기",
"OFF": "끄기"
}
@@ -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",
@@ -525,8 +524,23 @@
"Separate Cover Page": "Afzonderlijke omslagpagina",
"Resize Notebook": "Formaat Notitieblok Aanpassen",
"Resize Sidebar": "Formaat Zijbalk Aanpassen",
"Menu": "Menu",
"Dismiss": "Sluiten",
"Get Help from the Readest Community": "Hulp krijgen van de Readest Community",
"Remove cover image": "Omslagafbeelding verwijderen"
"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",
"Books Grid": "Boekrooster",
"Book Content": "Boekinhoud",
"Footer Bar": "Voettekstbalk",
"Header Bar": "Koptekstbalk",
"View Options": "Weergaveopties",
"Progress Information": "Voortgangsinformatie",
"Book Menu": "Boekmenu",
"Search Options": "Zoekopties",
"Close": "Sluiten",
"Delete Book Options": "Boekverwijderopties",
"ON": "AAN",
"OFF": "UIT"
}
@@ -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",
@@ -533,8 +532,23 @@
"Separate Cover Page": "Osobna strona okładki",
"Resize Notebook": "Zmień rozmiar notatnika",
"Resize Sidebar": "Zmień rozmiar paska bocznego",
"Menu": "Menu",
"Dismiss": "Odrzuć",
"Get Help from the Readest Community": "Uzyskaj pomoc od społeczności Readest",
"Remove cover image": "Usuń obraz okładki"
"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",
"Books Grid": "Siatka książek",
"Book Content": "Zawartość książki",
"Footer Bar": "Pasek stopki",
"Header Bar": "Pasek nagłówka",
"View Options": "Opcje widoku",
"Progress Information": "Informacje o postępie",
"Book Menu": "Menu książki",
"Search Options": "Opcje wyszukiwania",
"Close": "Zamknij",
"Delete Book Options": "Opcje usuwania książki",
"ON": "WŁĄCZONE",
"OFF": "WYŁĄCZONE"
}
@@ -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",
@@ -529,8 +528,23 @@
"Separate Cover Page": "Páginas de Capa Separadas",
"Resize Notebook": "Redimensionar Caderno",
"Resize Sidebar": "Redimensionar Barra Lateral",
"Menu": "Menu",
"Dismiss": "Dispensar",
"Get Help from the Readest Community": "Obter ajuda da comunidade Readest",
"Remove cover image": "Remover imagem da capa"
"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",
"Books Grid": "Grade de Livros",
"Book Content": "Conteúdo do Livro",
"Footer Bar": "Barra de Rodapé",
"Header Bar": "Barra de Cabeçalho",
"View Options": "Opções de Visualização",
"Progress Information": "Informações de Progresso",
"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"
}
@@ -82,7 +82,6 @@
"Search...": "Поиск...",
"Select Book": "Выбрать книгу",
"Select Books": "Выбрать книги",
"Select Multiple Books": "Выбрать несколько книг",
"Sepia": "Сепия",
"Serif Font": "Шрифт с засечками",
"Show Book Details": "Показать детали книги",
@@ -533,8 +532,23 @@
"Separate Cover Page": "Отдельная страница обложки",
"Resize Notebook": "Изменить размер блокнота",
"Resize Sidebar": "Изменить размер боковой панели",
"Menu": "Меню",
"Dismiss": "Закрыть",
"Get Help from the Readest Community": "Получить помощь от сообщества Readest",
"Remove cover image": "Удалить изображение обложки"
"Remove cover image": "Удалить изображение обложки",
"Bookshelf": "Книжная полка",
"View Menu": "Просмотр меню",
"Settings Menu": "Меню настроек",
"View account details and quota": "Просмотр данных аккаунта и квоты",
"Library Header": "Заголовок библиотеки",
"Books Grid": "Сетка книг",
"Book Content": "Содержание книги",
"Footer Bar": "Нижняя панель",
"Header Bar": "Верхняя панель",
"View Options": "Параметры просмотра",
"Progress Information": "Информация о прогрессе",
"Book Menu": "Меню книги",
"Search Options": "Параметры поиска",
"Close": "Закрыть",
"Delete Book Options": "Параметры удаления книги",
"ON": "ВКЛ",
"OFF": "ВЫКЛ"
}
@@ -66,7 +66,6 @@
"Search Books...": "පොත් සොයන්න...",
"Clear Search": "සෙවීම මකන්න",
"Import Books": "පොත් ආයාත කරන්න",
"Select Multiple Books": "පොත් කිහිපයක් තෝරන්න",
"Select Books": "පොත් තෝරන්න",
"Deselect": "අනිශ්චිත කරන්න",
"Select All": "සියල්ල තෝරන්න",
@@ -525,8 +524,23 @@
"Separate Cover Page": "වෙන් වූ ආවරණ පිටුව",
"Resize Notebook": "සටහන් පොතේ ප්‍රමාණය වෙනස් කරන්න",
"Resize Sidebar": "පැති තීරුවේ ප්‍රමාණය වෙනස් කරන්න",
"Menu": "මෙනුව",
"Dismiss": "අත්හැරීම",
"Get Help from the Readest Community": "Readest ප්‍රජාවෙන් උදව් ලබා ගන්න",
"Remove cover image": "ආවරණ රූපය ඉවත් කරන්න"
"Remove cover image": "ආවරණ රූපය ඉවත් කරන්න",
"Bookshelf": "පොත් රාක්කය",
"View Menu": "මෙනුව බලන්න",
"Settings Menu": "සැකසුම් මෙනුව",
"View account details and quota": "ගිණුම් විස්තර සහ කොටස බලන්න",
"Library Header": "පුස්තකාල ශීර්ෂය",
"Books Grid": "පොත් ජාලය",
"Book Content": "පොතේ අන්තර්ගතය",
"Footer Bar": "පාදක තීරුව",
"Header Bar": "ශීර්ෂ තීරුව",
"View Options": "දැක්ම විකල්ප",
"Progress Information": "ප්‍රගති තොරතුරු",
"Book Menu": "පොත් මෙනුව",
"Search Options": "සෙවුම් විකල්ප",
"Close": "වසන්න",
"Delete Book Options": "පොත මකන විකල්ප",
"ON": "ඇක්ටිව්",
"OFF": "ඩිසැක්ටිව්"
}
@@ -66,7 +66,6 @@
"Search Books...": "புத்தகங்களைத் தேடவும்...",
"Clear Search": "தேடலை அழிக்கவும்",
"Import Books": "புத்தகங்களை இறக்குமதி செய்யவும்",
"Select Multiple Books": "பல புத்தகங்களை தேர்வு செய்யவும்",
"Select Books": "புத்தகங்களை தேர்வு செய்யவும்",
"Deselect": "தேர்வுநீக்கவும்",
"Select All": "அனைத்தையும் தேர்வு செய்யவும்",
@@ -525,8 +524,23 @@
"Separate Cover Page": "தனித்த அட்டை பக்கம்",
"Resize Notebook": "குறிப்பேட்டை மீட்டமைக்கவும்",
"Resize Sidebar": "பக்கப்பட்டியை மீட்டமைக்கவும்",
"Menu": "மெனு",
"Dismiss": "நிராகரிக்கவும்",
"Get Help from the Readest Community": "Readest சமூகத்திலிருந்து உதவி பெறவும்",
"Remove cover image": "அட்டை படத்தை நீக்கவும்"
"Remove cover image": "அட்டை படத்தை நீக்கவும்",
"Bookshelf": "புத்தக அலமாரி",
"View Menu": "மெனுவை பார்க்கவும்",
"Settings Menu": "அமைப்புகள் மெனு",
"View account details and quota": "கணக்கு விவரங்கள் மற்றும் ஒதுக்கீட்டை பார்க்கவும்",
"Library Header": "நூலக தலைப்பு",
"Books Grid": "புத்தக கட்டமைப்பு",
"Book Content": "புத்தக உள்ளடக்கம்",
"Footer Bar": "அடிக்குறிப்பு பட்டை",
"Header Bar": "தலைப்பு பட்டை",
"View Options": "காணும் விருப்பங்கள்",
"Progress Information": "முன்னேற்ற தகவல்",
"Book Menu": "புத்தக மெனு",
"Search Options": "தேடல் விருப்பங்கள்",
"Close": "மூடு",
"Delete Book Options": "புத்தகத்தை நீக்கும் விருப்பங்கள்",
"ON": "செயல்படுத்தவும்",
"OFF": "நிறுத்தவும்"
}
@@ -63,7 +63,6 @@
"Search Books...": "ค้นหาหนังสือ...",
"Clear Search": "ล้างการค้นหา",
"Import Books": "นำเข้าหนังสือ",
"Select Multiple Books": "เลือกหลายเล่ม",
"Select Books": "เลือกหนังสือ",
"Deselect": "ยกเลิกเลือก",
"Select All": "เลือกทั้งหมด",
@@ -521,8 +520,23 @@
"Separate Cover Page": "หน้าปกแยกต่างหาก",
"Resize Notebook": "ปรับขนาดสมุดบันทึก",
"Resize Sidebar": "ปรับขนาดแถบข้าง",
"Menu": "เมนู",
"Dismiss": "ปิด",
"Get Help from the Readest Community": "รับความช่วยเหลือจากชุมชน Readest",
"Remove cover image": "ลบรูปปก"
"Remove cover image": "ลบรูปปก",
"Bookshelf": "ชั้นวางหนังสือ",
"View Menu": "ดูเมนู",
"Settings Menu": "เมนูการตั้งค่า",
"View account details and quota": "ดูรายละเอียดบัญชีและโควต้า",
"Library Header": "ส่วนหัวของห้องสมุด",
"Books Grid": "ตารางหนังสือ",
"Book Content": "เนื้อหาหนังสือ",
"Footer Bar": "แถบด้านล่าง",
"Header Bar": "แถบส่วนหัว",
"View Options": "ตัวเลือกการดู",
"Progress Information": "ข้อมูลความคืบหน้า",
"Book Menu": "เมนูหนังสือ",
"Search Options": "ตัวเลือกการค้นหา",
"Close": "ปิด",
"Delete Book Options": "ตัวเลือกการลบหนังสือ",
"ON": "เปิด",
"OFF": "ปิด"
}
@@ -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",
@@ -525,8 +524,23 @@
"Separate Cover Page": "Ayrı Kapak Sayfası",
"Resize Notebook": "Defteri Yeniden Boyutlandır",
"Resize Sidebar": "Kenar Çubuğunu Yeniden Boyutlandır",
"Menu": "Menü",
"Dismiss": "Kapat",
"Get Help from the Readest Community": "Readest Topluluğundan Yardım Alın",
"Remove cover image": "Kapak resmini kaldır"
"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ığı",
"Books Grid": "Kitap Izgarası",
"Book Content": "Kitap İçeriği",
"Footer Bar": "Alt Çubuk",
"Header Bar": "Üst Çubuk",
"View Options": "Görünüm Seçenekleri",
"Progress Information": "İlerleme Bilgisi",
"Book Menu": "Kitap Menüsü",
"Search Options": "Arama Seçenekleri",
"Close": "Kapat",
"Delete Book Options": "Kitap Silme Seçenekleri",
"ON": "AÇIK",
"OFF": "KAPALI"
}
@@ -82,7 +82,6 @@
"Search...": "Пошук...",
"Select Book": "Вибрати книгу",
"Select Books": "Вибрати книги",
"Select Multiple Books": "Вибрати кілька книг",
"Sepia": "Сепія",
"Serif Font": "Шрифт із засічками",
"Show Book Details": "Показати деталі книги",
@@ -533,8 +532,23 @@
"Separate Cover Page": "Окрема сторінка обкладинки",
"Resize Notebook": "Змінити розмір нотаток",
"Resize Sidebar": "Змінити розмір бічної панелі",
"Menu": "Меню",
"Dismiss": "Відхилити",
"Get Help from the Readest Community": "Отримати допомогу від спільноти Readest",
"Remove cover image": "Видалити зображення обкладинки"
"Remove cover image": "Видалити зображення обкладинки",
"Bookshelf": "Книжкова полиця",
"View Menu": "Переглянути меню",
"Settings Menu": "Меню налаштувань",
"View account details and quota": "Переглянути деталі акаунта та квоту",
"Library Header": "Заголовок бібліотеки",
"Books Grid": "Сітка книг",
"Book Content": "Зміст книги",
"Footer Bar": "Нижня панель",
"Header Bar": "Верхня панель",
"View Options": "Параметри перегляду",
"Progress Information": "Інформація про прогрес",
"Book Menu": "Меню книги",
"Search Options": "Параметри пошуку",
"Close": "Закрити",
"Delete Book Options": "Параметри видалення книги",
"ON": "УВІМКНЕННЯ",
"OFF": "ВИМКНЕННЯ"
}
@@ -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",
@@ -521,8 +520,23 @@
"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",
"Menu": "Menu",
"Dismiss": "Đóng",
"Get Help from the Readest Community": "Nhận Trợ Giúp từ Cộng Đồng Readest",
"Remove cover image": "Xóa ảnh bìa"
"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",
"Books Grid": "Lưới sách",
"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",
"Progress Information": "Thông tin tiến độ",
"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"
}
@@ -82,7 +82,6 @@
"Search...": "搜索...",
"Select Book": "选择书籍",
"Select Books": "选择书籍",
"Select Multiple Books": "选择多本书籍",
"Sepia": "旧韵",
"Serif Font": "衬线字体",
"Show Book Details": "显示书籍详情",
@@ -521,8 +520,23 @@
"Separate Cover Page": "单独封面页",
"Resize Notebook": "调整笔记本大小",
"Resize Sidebar": "调整侧边栏大小",
"Menu": "菜单",
"Dismiss": "关闭",
"Get Help from the Readest Community": "从 Readest 社区获取帮助",
"Remove cover image": "移除封面图片"
"Remove cover image": "移除封面图片",
"Bookshelf": "书架",
"View Menu": "查看菜单",
"Settings Menu": "设置菜单",
"View account details and quota": "查看账户详情和配额",
"Library Header": "图书馆标题",
"Books Grid": "图书网格",
"Book Content": "书籍内容",
"Footer Bar": "底部栏",
"Header Bar": "顶部栏",
"View Options": "查看选项",
"Progress Information": "进度信息",
"Book Menu": "书籍菜单",
"Search Options": "搜索选项",
"Close": "关闭",
"Delete Book Options": "删除书籍选项",
"ON": "开启",
"OFF": "关闭"
}
@@ -82,7 +82,6 @@
"Search...": "搜索...",
"Select Book": "選擇書籍",
"Select Books": "選擇書籍",
"Select Multiple Books": "選擇多本書籍",
"Sepia": "舊韻",
"Serif Font": "襯線字體",
"Show Book Details": "顯示書籍詳情",
@@ -521,8 +520,23 @@
"Separate Cover Page": "單獨封面頁",
"Resize Notebook": "調整筆記本大小",
"Resize Sidebar": "調整側邊欄大小",
"Menu": "選單",
"Dismiss": "關閉",
"Get Help from the Readest Community": "從 Readest 社群獲取幫助",
"Remove cover image": "移除封面圖片"
"Remove cover image": "移除封面圖片",
"Bookshelf": "書架",
"View Menu": "檢視選單",
"Settings Menu": "設定選單",
"View account details and quota": "檢視帳戶詳細資料與配額",
"Library Header": "圖書館標題",
"Books Grid": "書籍網格",
"Book Content": "書籍內容",
"Footer Bar": "頁腳欄",
"Header Bar": "標頭欄",
"View Options": "檢視選項",
"Progress Information": "進度資訊",
"Book Menu": "書籍選單",
"Search Options": "搜尋選項",
"Close": "關閉",
"Delete Book Options": "刪除書籍選項",
"ON": "開啟",
"OFF": "關閉"
}
+1
View File
@@ -363,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')}
>
@@ -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,6 +41,7 @@ const BookItem: React.FC<BookItemProps> = ({
handleBookDownload,
showBookDetailsModal,
}) => {
const _ = useTranslation();
const router = useRouter();
const { user } = useAuth();
const { appService } = useEnv();
@@ -115,6 +117,7 @@ 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)}
@@ -9,6 +9,7 @@ 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';
@@ -76,6 +77,8 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const { setSelectedBooks, getSelectedBooks, toggleSelectedBook } = useLibraryStore();
const allBookshelfItems = generateBookshelfItems(libraryBooks);
const autofocusRef = useAutoFocus<HTMLDivElement>();
useEffect(() => {
if (isImportingBook.current) return;
isImportingBook.current = true;
@@ -316,12 +319,16 @@ const Bookshelf: React.FC<BookshelfProps> = ({
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='list'
aria-label={_('Bookshelf')}
>
{filteredBookshelfItems.map((item) => (
<BookshelfItem
@@ -347,6 +354,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
))}
{viewMode === 'grid' && !navBooksGroup && allBookshelfItems.length > 0 && (
<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',
@@ -322,6 +322,8 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
appService?.isMobileApp && 'no-context-menu',
pressing && mode === 'grid' ? 'scale-95' : 'scale-100',
)}
role='group'
aria-label={'format' in item ? item.title : item.name}
style={{
transition: 'transform 0.2s',
}}
@@ -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>
@@ -26,7 +26,11 @@ const ReadingProgress: React.FC<ReadingProgressProps> = memo(
}
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;
@@ -165,9 +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
<Menu
label={_('Settings Menu')}
className={clsx(
'settings-menu dropdown-content no-triangle border-base-100',
'z-20 mt-2 max-w-[90vw] shadow-2xl',
@@ -181,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} />
@@ -201,27 +209,27 @@ 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}
/>
)}
@@ -229,39 +237,29 @@ const SettingsMenu: React.FC<SettingsMenuProps> = ({ setIsDropdownOpen }) => {
{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}
/>
@@ -274,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,13 +92,16 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
};
return (
<div 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)}
/>
))}
@@ -114,7 +117,7 @@ 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)}
/>
))}
@@ -130,7 +133,7 @@ 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)}
/>
))}
@@ -140,11 +143,11 @@ const ViewMenu: React.FC<ViewMenuProps> = ({ setIsDropdownOpen }) => {
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>
);
};
+9 -1
View File
@@ -627,13 +627,20 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
return (
<div
ref={pageRef}
role='main'
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?.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}
@@ -652,6 +659,7 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
(libraryBooks.some((book) => !book.deletedAt) ? (
<OverlayScrollbarsComponent
defer
aria-label=''
ref={osRef}
className='flex-grow'
options={{ scrollbars: { autoHide: 'scroll' } }}
@@ -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 Grid')}
>
{bookKeys.map((bookKey, index) => {
const bookData = getBookData(bookKey);
@@ -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,
@@ -62,6 +64,7 @@ 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();
@@ -81,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);
@@ -410,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,7 +173,8 @@ const FooterBar: React.FC<FooterBarProps> = ({
return (
<>
<div
role='button'
role='none'
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
className={clsx(
'absolute bottom-0 left-0 z-10 hidden w-full sm:flex sm:h-[52px]',
@@ -185,6 +186,8 @@ 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-10 flex w-full flex-col',
'sm:h-[52px] sm:justify-center',
@@ -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 {
@@ -105,7 +107,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
}}
>
<div
role='button'
role='none'
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
className={clsx('absolute top-0 z-10 h-11 w-full')}
onFocus={() => !appService?.isMobile && setHoveredBookKey(bookKey)}
@@ -124,6 +127,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`,
@@ -154,25 +159,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>
);
};
@@ -65,6 +65,8 @@ const ProgressInfoView: React.FC<PageInfoProps> = ({
isVertical ? 'writing-vertical-rl' : 'w-full',
isScrolled && !isVertical && 'bg-base-100',
)}
role='group'
aria-label={_('Progress Information')}
style={
isVertical
? {
@@ -51,6 +51,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
isVertical ? 'writing-vertical-rl max-h-[85%]' : 'top-0 h-[44px]',
isScrolled && !isVertical && 'bg-base-100',
)}
role='contentinfo'
style={
isVertical
? {
@@ -68,7 +69,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
}
}
>
<h2
<span
className={clsx(
'text-neutral-content text-center font-sans text-xs font-light',
isVertical ? '' : 'line-clamp-1',
@@ -76,7 +77,7 @@ const SectionInfo: React.FC<SectionInfoProps> = ({
)}
>
{section || ''}
</h2>
</span>
</div>
</>
);
@@ -211,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}`,
@@ -212,6 +212,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 />}
@@ -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,
@@ -75,6 +77,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',
@@ -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',
@@ -216,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}`,
@@ -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')}
>
+12 -28
View File
@@ -11,39 +11,23 @@ interface ButtonProps {
className?: string;
}
const Button: React.FC<ButtonProps> = ({
icon,
onClick,
disabled = false,
tooltip,
tooltipDirection = 'top',
className,
}) => {
const Button: React.FC<ButtonProps> = ({ icon, onClick, disabled = false, tooltip, 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 && 'btn-disabled !bg-transparent opacity-50',
className,
)}
data-tip={tooltip}
title={tooltip}
aria-label={tooltip}
onClick={disabled ? undefined : onClick}
disabled={disabled}
>
<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>
);
};
+24 -3
View File
@@ -4,6 +4,7 @@ import { MdArrowBackIosNew, MdArrowForwardIos } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
import { useDrag } from '@/hooks/useDrag';
import { useThemeStore } from '@/store/themeStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useDeviceControlStore } from '@/store/deviceStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { impactFeedback } from '@tauri-apps/plugin-haptics';
@@ -41,12 +42,14 @@ const Dialog: React.FC<DialogProps> = ({
contentClassName,
onClose,
}) => {
const _ = useTranslation();
const { appService } = useEnv();
const { systemUIVisible, statusBarHeight, safeAreaInsets } = useThemeStore();
const { acquireBackKeyInterception, releaseBackKeyInterception } = useDeviceControlStore();
const [isFullHeightInMobile, setIsFullHeightInMobile] = useState(!snapHeight);
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
const dialogRef = useRef<HTMLDialogElement>(null);
const previousActiveElementRef = useRef<HTMLElement | null>(null);
const iconSize22 = useResponsiveSize(22);
const isMobile = window.innerWidth < 640 || window.innerHeight < 640;
@@ -63,14 +66,30 @@ const Dialog: React.FC<DialogProps> = ({
};
useEffect(() => {
if (!isOpen) return;
if (!isOpen) {
if (previousActiveElementRef.current) {
previousActiveElementRef.current.focus();
previousActiveElementRef.current = null;
}
return;
}
previousActiveElementRef.current = document.activeElement as HTMLElement;
setIsFullHeightInMobile(!snapHeight && isMobile);
window.addEventListener('keydown', handleKeyDown);
if (appService?.isAndroidApp) {
acquireBackKeyInterception();
eventDispatcher.onSync('native-key-down', handleKeyDown);
}
const timer = setTimeout(() => {
if (dialogRef.current) {
dialogRef.current.focus();
}
}, 100);
return () => {
clearTimeout(timer);
window.removeEventListener('keydown', handleKeyDown);
if (appService?.isAndroidApp) {
releaseBackKeyInterception();
@@ -155,7 +174,9 @@ const Dialog: React.FC<DialogProps> = ({
<dialog
ref={dialogRef}
id={id ?? 'dialog'}
tabIndex={-1}
open={isOpen}
aria-hidden={!isOpen}
className={clsx(
'modal sm:min-w-90 z-50 h-full w-full !items-start !bg-transparent sm:w-full sm:!items-center',
className,
@@ -204,7 +225,7 @@ const Dialog: React.FC<DialogProps> = ({
) : (
<div className='flex h-11 w-full items-center justify-between'>
<button
tabIndex={-1}
aria-label={_('Close')}
onClick={onClose}
className={
'btn btn-ghost btn-circle flex h-8 min-h-8 w-8 hover:bg-transparent focus:outline-none sm:hidden'
@@ -220,7 +241,7 @@ const Dialog: React.FC<DialogProps> = ({
<span className='line-clamp-1 text-center font-bold'>{title ?? ''}</span>
</div>
<button
tabIndex={-1}
aria-label={_('Close')}
onClick={onClose}
className={
'bg-base-300/65 btn btn-ghost btn-circle ml-auto hidden h-6 min-h-6 w-6 focus:outline-none sm:flex'
+50 -18
View File
@@ -1,10 +1,10 @@
import clsx from 'clsx';
import React, { useState, isValidElement, ReactElement, ReactNode } from 'react';
import { useTranslation } from '@/hooks/useTranslation';
import React, { useState, isValidElement, ReactElement, ReactNode, useRef } from 'react';
import { Overlay } from './Overlay';
import MenuItem from './MenuItem';
interface DropdownProps {
label: string;
className?: string;
menuClassName?: string;
buttonClassName?: string;
@@ -52,6 +52,7 @@ const enhanceMenuItems = (
};
const Dropdown: React.FC<DropdownProps> = ({
label,
className,
menuClassName,
buttonClassName,
@@ -59,14 +60,45 @@ const Dropdown: React.FC<DropdownProps> = ({
children,
onToggle,
}) => {
const _ = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [isFocused, setIsFocused] = useState(false);
const [isPointerDown, setIsPointerDown] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const toggleDropdown = () => {
console.error('Toggling dropdown, current state:', isOpen);
const newIsOpen = !isOpen;
setIsDropdownOpen(newIsOpen);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
if (!isOpen) setIsDropdownOpen(true);
e.stopPropagation();
} else if (e.key === 'Escape' && isOpen) {
setIsDropdownOpen(false);
e.stopPropagation();
}
};
const handleFocus = () => {
if (!isOpen && !isPointerDown) {
setIsDropdownOpen(true);
}
setIsFocused(true);
};
const handleBlur = (e: React.FocusEvent) => {
if (!containerRef.current) return;
const relatedTarget = e.relatedTarget;
if (relatedTarget && !containerRef.current.contains(relatedTarget)) {
console.log('Dropdown lost focus, closing menu');
setIsFocused(false);
setIsDropdownOpen(false);
}
};
const setIsDropdownOpen = (isOpen: boolean) => {
setIsOpen(isOpen);
onToggle?.(isOpen);
@@ -86,28 +118,28 @@ const Dropdown: React.FC<DropdownProps> = ({
<div className='dropdown-container flex'>
{isOpen && <Overlay onDismiss={() => setIsDropdownOpen(false)} />}
<div
ref={containerRef}
role='menu'
tabIndex={0}
role='button'
aria-label={_('Menu')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
if (!isOpen) toggleDropdown();
} else if (e.key === 'Escape' && isOpen) {
toggleDropdown();
} else {
e.stopPropagation();
}
}}
className={clsx('dropdown', className)}
aria-label={label}
title={label}
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onPointerDown={() => setIsPointerDown(true)}
onPointerUp={() => setIsPointerDown(false)}
className={clsx('dropdown flex flex-col', className)}
>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
role='none'
className={clsx('dropdown-toggle', buttonClassName, isFocused && 'bg-base-300/50')}
onClick={toggleDropdown}
className={clsx('dropdown-toggle', buttonClassName, isOpen && 'bg-base-300/50')}
>
{toggleButton}
</div>
{isOpen && childrenWithToggle}
<div role='none' className={clsx('flex items-center justify-center', !isOpen && 'hidden')}>
{isOpen && childrenWithToggle}
</div>
</div>
</div>
);
+31
View File
@@ -0,0 +1,31 @@
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
interface MenuProps {
children: React.ReactNode;
label: string;
className?: string;
}
const Menu: React.FC<MenuProps> = ({ children, className }) => {
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setTimeout(() => {
if (menuRef.current) {
const firstItem = menuRef.current.querySelector('[role="menuitem"]');
if (firstItem) {
(firstItem as HTMLElement).focus();
}
}
}, 200);
}, []);
return (
<div ref={menuRef} role='none' className={clsx(className)}>
{children}
</div>
);
};
export default Menu;
+53 -27
View File
@@ -1,10 +1,13 @@
import clsx from 'clsx';
import React from 'react';
import { IconType } from 'react-icons';
import { MdCheck } from 'react-icons/md';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
interface MenuItemProps {
label: string;
toggled?: boolean;
description?: string;
tooltip?: string;
buttonClass?: string;
@@ -12,7 +15,7 @@ interface MenuItemProps {
shortcut?: string;
disabled?: boolean;
noIcon?: boolean;
transient?: boolean; // For transient items the dropdown will close on click
transient?: boolean;
Icon?: React.ReactNode | IconType;
children?: React.ReactNode;
onClick?: () => void;
@@ -21,6 +24,7 @@ interface MenuItemProps {
const MenuItem: React.FC<MenuItemProps> = ({
label,
toggled,
description,
tooltip,
buttonClass,
@@ -34,36 +38,30 @@ const MenuItem: React.FC<MenuItemProps> = ({
onClick,
setIsDropdownOpen,
}) => {
const _ = useTranslation();
const iconSize = useResponsiveSize(16);
const menuButton = (
<button
tabIndex={0}
className={clsx(
'hover:bg-base-300 text-base-content flex w-full flex-col items-center justify-center rounded-md p-1 py-[10px]',
disabled && 'btn-disabled text-gray-400',
buttonClass,
)}
aria-label={label}
data-tip={tooltip ? tooltip : ''}
onClick={() => {
onClick?.();
if (transient) {
setIsDropdownOpen?.(false);
}
}}
disabled={disabled}
>
const IconType = Icon || (toggled !== undefined ? (toggled ? MdCheck : undefined) : undefined);
const handleClick = () => {
onClick?.();
if (transient) {
setIsDropdownOpen?.(false);
}
};
const buttonContent = (
<>
<div className='flex w-full items-center justify-between'>
<div className='flex min-w-0 items-center'>
{!noIcon && (
<span style={{ minWidth: `${iconSize}px` }}>
{typeof Icon === 'function' ? (
<Icon
{typeof IconType === 'function' ? (
<IconType
className={disabled ? 'text-gray-400' : 'text-base-content'}
size={iconSize}
/>
) : (
Icon
IconType
)}
</span>
)}
@@ -96,22 +94,50 @@ const MenuItem: React.FC<MenuItemProps> = ({
</span>
)}
</div>
</button>
</>
);
if (children) {
return (
<ul className='menu rounded-box m-0 p-0'>
<li>
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
<ul className='menu rounded-box m-0 p-0' role='menuitem' tabIndex={-1}>
<li aria-label={label}>
<details>
<summary className='hover:bg-base-300 p-0 pr-3'>{menuButton}</summary>
<summary
className={clsx(
'hover:bg-base-300 text-base-content cursor-pointer rounded-md p-1 py-[10px] pr-3',
disabled && 'btn-disabled cursor-not-allowed text-gray-400',
buttonClass,
)}
title={tooltip ? tooltip : ''}
>
{buttonContent}
</summary>
{children}
</details>
</li>
</ul>
);
}
return menuButton;
return (
<button
role={disabled ? 'none' : 'menuitem'}
aria-label={toggled !== undefined ? `${label} - ${toggled ? _('ON') : _('OFF')}` : undefined}
aria-live={toggled === undefined ? 'polite' : 'off'}
tabIndex={disabled ? -1 : 0}
className={clsx(
'hover:bg-base-300 text-base-content flex w-full flex-col items-center justify-center rounded-md p-1 py-[10px]',
disabled && 'btn-disabled text-gray-400',
buttonClass,
)}
title={tooltip ? tooltip : ''}
onClick={handleClick}
disabled={disabled}
>
{buttonContent}
</button>
);
};
export default MenuItem;
+2 -6
View File
@@ -1,6 +1,5 @@
import clsx from 'clsx';
import React from 'react';
import { useTranslation } from '@/hooks/useTranslation';
interface OverlayProps {
onDismiss: () => void;
@@ -8,9 +7,7 @@ interface OverlayProps {
className?: string;
}
export const Overlay: React.FC<OverlayProps> = ({ onDismiss, dismissLabel, className }) => {
const _ = useTranslation();
export const Overlay: React.FC<OverlayProps> = ({ onDismiss, className }) => {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape' || e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
@@ -21,9 +18,8 @@ export const Overlay: React.FC<OverlayProps> = ({ onDismiss, dismissLabel, class
return (
<div
className={clsx('overlay fixed inset-0 cursor-default', className)}
role='button'
role='none'
tabIndex={-1}
aria-label={dismissLabel || _('Dismiss')}
onClick={onDismiss}
onContextMenu={onDismiss}
onKeyDown={handleKeyDown}
@@ -70,6 +70,7 @@ const BookDetailView: React.FC<BookDetailViewProps> = ({
)}
{onDelete && (
<Dropdown
label={_('Delete Book Options')}
className='dropdown-bottom flex justify-center'
buttonClassName='btn btn-ghost h-8 min-h-8 w-8 p-0'
toggleButton={<MdOutlineDelete className='fill-red-500' />}
@@ -0,0 +1,35 @@
import { useEffect, useRef } from 'react';
interface UseAutoFocusOptions {
ref?: React.RefObject<HTMLElement | null>;
delay?: number;
condition?: boolean;
preventScroll?: boolean;
}
export const useAutoFocus = <T extends HTMLElement = HTMLDivElement>(
options: UseAutoFocusOptions = {},
) => {
const { delay = 0, condition = true, preventScroll = false, ref } = options;
const localRef = useRef<T>(null);
const focusRef = ref || localRef;
useEffect(() => {
if (condition && focusRef.current) {
const focusElement = () => {
focusRef.current?.focus({ preventScroll });
};
if (delay > 0) {
const timer = setTimeout(focusElement, delay);
return () => clearTimeout(timer);
} else {
focusElement();
}
}
return;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [condition, delay, preventScroll]);
return focusRef as React.RefObject<T>;
};