layout: better organized panels in settings dialog (#1246)

This commit is contained in:
Huang Xin
2025-05-27 20:37:32 +08:00
committed by GitHub
parent 6b033f7509
commit b0cc0b6f1a
30 changed files with 447 additions and 366 deletions
@@ -59,7 +59,6 @@
"Match Whole Words": "مطابقة الكلمات الكاملة",
"Maximum Number of Columns": "العدد الأقصى للأعمدة",
"Minimum Font Size": "الحد الأدنى لحجم الخط",
"Misc": "أخرى",
"Monospace Font": "خط أحادي المسافة (Monospace)",
"More Info": "مزيد من المعلومات",
"Nord": "نورد",
@@ -305,7 +304,6 @@
"Details": "التفاصيل",
"File Size:": "حجم الملف:",
"Auto Detect": "الكشف التلقائي",
"Touch": "اللمس",
"Next Section": "القسم التالي",
"Previous Section": "القسم السابق",
"Next Page": "الصفحة التالية",
@@ -333,5 +331,8 @@
"Translation Service": "خدمة الترجمة",
"Translate To": "ترجمة إلى",
"Disable Translation": "تعطيل الترجمة",
"Overlap Margin": "هامش التداخل"
"Scroll": "تمرير",
"Overlap Pixels": "تداخل البكسلات",
"Click": "نقر",
"System Language": "لغة النظام"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "Ganze Wörter",
"Maximum Number of Columns": "Maximale Spaltenanzahl",
"Minimum Font Size": "Minimale Schriftgröße",
"Misc": "Sonstiges",
"Monospace Font": "Monospace-Schriftart",
"More Info": "Weitere Informationen",
"Nord": "Nord",
@@ -305,7 +304,6 @@
"Details": "Details",
"File Size:": "Dateigröße:",
"Auto Detect": "Automatisch erkennen",
"Touch": "Berührung",
"Next Section": "Nächster Abschnitt",
"Previous Section": "Vorheriger Abschnitt",
"Next Page": "Nächste Seite",
@@ -329,5 +327,8 @@
"Translation Service": "Übersetzungsdienst",
"Translate To": "Übersetzen nach",
"Disable Translation": "Übersetzung deaktivieren",
"Overlap Margin": "Überlappungsrand"
"Scroll": "Scrollen",
"Overlap Pixels": "Überlappungspixel",
"Click": "Tippen",
"System Language": "Systemsprache"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "Ταίριασμα ολόκληρων λέξεων",
"Maximum Number of Columns": "Μέγιστος αριθμός στηλών",
"Minimum Font Size": "Ελάχιστο μέγεθος γραμματοσειράς",
"Misc": "Διάφορα",
"Monospace Font": "Γραμματοσειρά σταθερού πλάτους",
"More Info": "Περισσότερες πληροφορίες",
"Nord": "Nord",
@@ -305,7 +304,6 @@
"Details": "Πληρ.",
"File Size:": "Μέγεθος αρχείου:",
"Auto Detect": "Αυτόματη ανίχνευση",
"Touch": "Αφή",
"Next Section": "Επόμενη ενότητα",
"Previous Section": "Προηγούμενη ενότητα",
"Next Page": "Επόμενη σελίδα",
@@ -329,5 +327,8 @@
"Translation Service": "Υπηρεσία μετάφρασης",
"Translate To": "Μετάφραση σε",
"Disable Translation": "Απενεργοποίηση μετάφρασης",
"Overlap Margin": "Περιθώριο επικάλυψης"
"Scroll": "Κύλιση",
"Overlap Pixels": "Επικάλυψη εικονοστοιχείων",
"Click": "Κλικ",
"System Language": "Γλώσσα συστήματος"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "Coincidir palabras completas",
"Maximum Number of Columns": "Número máximo de columnas",
"Minimum Font Size": "Tamaño de fuente mínimo",
"Misc": "Varios",
"Monospace Font": "Fuente monoespaciada",
"More Info": "Más información",
"Nord": "Nord",
@@ -305,7 +304,6 @@
"Details": "Detalles",
"File Size:": "Peso del archivo:",
"Auto Detect": "Detección automática",
"Touch": "Toque",
"Next Section": "Siguiente sección",
"Previous Section": "Sección anterior",
"Next Page": "Página siguiente",
@@ -330,5 +328,8 @@
"Translation Service": "Servicio de traducción",
"Translate To": "Traducir a",
"Disable Translation": "Deshabilitar traducción",
"Overlap Margin": "Margen de superposición"
"Scroll": "Desplazar",
"Overlap Pixels": "Superponer píxeles",
"Click": "Click",
"System Language": "Idioma del sistema"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "Mots entiers",
"Maximum Number of Columns": "Nombre maximal de colonnes",
"Minimum Font Size": "Taille minimale de police",
"Misc": "Divers",
"Monospace Font": "Police monospace",
"More Info": "Plus d'informations",
"Nord": "Nord",
@@ -305,7 +304,6 @@
"Details": "Détails",
"File Size:": "Taille du fichier :",
"Auto Detect": "Détection automatique",
"Touch": "Touch",
"Next Section": "Section suivante",
"Previous Section": "Section précédente",
"Next Page": "Page suivante",
@@ -330,5 +328,8 @@
"Translation Service": "Service de traduction",
"Translate To": "Traduire vers",
"Disable Translation": " désactiver la traduction",
"Overlap Margin": "Marges de chevauchement"
"Scroll": "Défilement",
"Overlap Pixels": "Pixels de chevauchement",
"Click": "Cliquer",
"System Language": "Langue du système"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "पूरे शब्द मैच करें",
"Maximum Number of Columns": "कॉलम की अधिकतम संख्या",
"Minimum Font Size": "न्यूनतम फ़ॉन्ट साइज़",
"Misc": "विविध",
"Monospace Font": "मोनोस्पेस फ़ॉन्ट",
"More Info": "अधिक जानकारी",
"Nord": "नॉर्ड",
@@ -305,7 +304,6 @@
"Details": "विवरण",
"File Size:": "फ़ाइल आकार:",
"Auto Detect": "स्वचालित पहचान",
"Touch": "स्पर्श",
"Next Section": "अगला अनुभाग",
"Previous Section": "पिछला अनुभाग",
"Next Page": "अगला पृष्ठ",
@@ -329,5 +327,8 @@
"Translation Service": "अनुवाद सेवा",
"Translate To": "अनुवाद करें",
"Disable Translation": "अनुवाद अक्षम करें",
"Overlap Margin": "ओवरलैप मार्जिन"
"Scroll": "स्क्रॉल",
"Overlap Pixels": "ओवरलैप पिक्सेल",
"Click": "क्लिक करें",
"System Language": "सिस्टम भाषा"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "Cocokkan Kata Utuh",
"Maximum Number of Columns": "Jumlah Kolom Maksimum",
"Minimum Font Size": "Ukuran Font Minimum",
"Misc": "Lain-lain",
"Monospace Font": "Font Monospace",
"More Info": "Info Lebih Lanjut",
"Nord": "Nord",
@@ -305,7 +304,6 @@
"Details": "Detail",
"File Size:": "Ukuran File:",
"Auto Detect": "Deteksi Otomatis",
"Touch": "Sentuh",
"Next Section": "Bagian Selanjutnya",
"Previous Section": "Bagian Sebelumnya",
"Next Page": "Halaman Berikutnya",
@@ -328,5 +326,8 @@
"Translation Service": "Layanan Terjemahan",
"Translate To": "Terjemahkan Ke",
"Disable Translation": "Nonaktifkan Terjemahan",
"Overlap Margin": "Margin Tumpang Tindih"
"Scroll": "Scroll",
"Overlap Pixels": "Overlap Piksel",
"Click": "Klik",
"System Language": "Bahasa Sistem"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "Parole intere",
"Maximum Number of Columns": "Numero massimo di colonne",
"Minimum Font Size": "Dimensione minima font",
"Misc": "Varie",
"Monospace Font": "Font monospazio",
"More Info": "Maggiori informazioni",
"Nord": "Nord",
@@ -305,7 +304,6 @@
"Details": "Dettagli",
"File Size:": "Dimensione file:",
"Auto Detect": "Rilevamento automatico",
"Touch": "Touch",
"Next Section": "Sezione successiva",
"Previous Section": "Sezione precedente",
"Next Page": "Pagina successiva",
@@ -330,5 +328,8 @@
"Translation Service": "Servizio di traduzione",
"Translate To": "Traduci in",
"Disable Translation": "Disabilita traduzione",
"Overlap Margin": "Margine di sovrapposizione"
"Scroll": "Scorri",
"Overlap Pixels": "Pixel di sovrapposizione",
"Click": "Fai clic",
"System Language": "Lingua di sistema"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "単語全体を一致",
"Maximum Number of Columns": "最大列数",
"Minimum Font Size": "最小フォントサイズ",
"Misc": "その他",
"Monospace Font": "等幅フォント",
"More Info": "詳細情報",
"Nord": "ノルド",
@@ -305,7 +304,6 @@
"Details": "詳細",
"File Size:": "ファイルサイズ:",
"Auto Detect": "自動検出",
"Touch": "タッチ",
"Next Section": "次のセクション",
"Previous Section": "前のセクション",
"Next Page": "次のページ",
@@ -328,5 +326,8 @@
"Translation Service": "翻訳サービス",
"Translate To": "翻訳先",
"Disable Translation": "翻訳を無効にする",
"Overlap Margin": "オーバーラップマージン"
"Scroll": "スクロール",
"Overlap Pixels": "重なりピクセル",
"Click": "クリック",
"System Language": "システム言語"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "전체 단어 일치",
"Maximum Number of Columns": "최대 열 수",
"Minimum Font Size": "최소 글꼴 크기",
"Misc": "기타",
"Monospace Font": "고정폭 글꼴",
"More Info": "추가 정보",
"Nord": "노드",
@@ -305,7 +304,6 @@
"Details": "세부정보",
"File Size:": "파일 크기:",
"Auto Detect": "자동 감지",
"Touch": "터치",
"Next Section": "다음 섹션",
"Previous Section": "이전 섹션",
"Next Page": "다음 페이지",
@@ -328,5 +326,8 @@
"Translation Service": "번역 서비스",
"Translate To": "번역 대상 언어",
"Disable Translation": "번역 비활성화",
"Overlap Margin": "겹침 여백"
"Scroll": "스크롤",
"Overlap Pixels": "겹치는 픽셀",
"Click": "클릭",
"System Language": "시스템 언어"
}
@@ -197,7 +197,6 @@
"Font": "Lettertype",
"Layout": "Opmaak",
"Color": "Kleur",
"Misc": "Overig",
"(from 'As You Like It', Act II)": "(uit 'As You Like It', Akte II)",
"Custom Theme": "Aangepast thema",
"Theme Name": "Themanaam",
@@ -305,7 +304,6 @@
"Reveal in Folder": "Tonen in map",
"Dont have an account? Sign up": "Nog geen account? Registreer u",
"All the world's a stage,\nAnd all the men and women merely players;\nThey have their exits and their entrances,\nAnd one man in his time plays many parts,\nHis acts being seven ages.\n\n— William Shakespeare": "De hele wereld is een toneel,\nEn alle mannen en vrouwen slechts spelers;\nZij hebben hun uitgangen en hun entrees,\nEn één man speelt in zijn tijd vele rollen,\nZijn daden zijnde zeven leeftijden.\n\n— William Shakespeare",
"Touch": "Raak",
"Next Section": "Volgende sectie",
"Previous Section": "Vorige sectie",
"Next Page": "Volgende pagina",
@@ -329,5 +327,8 @@
"Translation Service": "Vertaalservice",
"Translate To": "Vertalen naar",
"Disable Translation": "Vertaling uitschakelen",
"Overlap Margin": "Overlap marge"
"Scroll": "Scrollen",
"Overlap Pixels": "Overlap pixels",
"Click": "Klik",
"System Language": "Systemtaal"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "Uwzględnij całe słowa",
"Maximum Number of Columns": "Maksymalna liczba kolumn",
"Minimum Font Size": "Minimalny rozmiar czcionki",
"Misc": "Różne",
"Monospace Font": "Czcionka o stałej szerokości",
"More Info": "Więcej informacji",
"Nord": "Północny",
@@ -305,7 +304,6 @@
"Details": "Szczegóły",
"File Size:": "Rozmiar pliku:",
"Auto Detect": "Automatyczne wykrywanie",
"Touch": "Dotyk",
"Next Section": "Następna sekcja",
"Previous Section": "Poprzednia sekcja",
"Next Page": "Następna strona",
@@ -331,5 +329,8 @@
"Translation Service": "Usługa tłumaczenia",
"Translate To": "Przetłumacz na",
"Disable Translation": "Wyłącz tłumaczenie",
"Overlap Margin": "Margines nakładania"
"Scroll": "Przewijaj",
"Overlap Pixels": "Przesunięcie pikseli",
"Click": "Kliknij",
"System Language": "Język systemu"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "Corresponder Palavras Inteiras",
"Maximum Number of Columns": "Número Máximo de Colunas",
"Minimum Font Size": "Tamanho Mínimo da Fonte",
"Misc": "Diversos",
"Monospace Font": "Fonte Monoespaçada",
"More Info": "Mais Informações",
"Nord": "Nord",
@@ -305,7 +304,6 @@
"Details": "Detalhes",
"File Size:": "Tamanho do arquivo:",
"Auto Detect": "Detectar Automaticamente",
"Touch": "Toque",
"Next Section": "Próxima seção",
"Previous Section": "Seção anterior",
"Next Page": "Próxima página",
@@ -330,5 +328,8 @@
"Translation Service": "Serviço de tradução",
"Translate To": "Traduzir para",
"Disable Translation": "Desativar tradução",
"Overlap Margin": "Margem de Sobreposição"
"Scroll": "Rolar",
"Overlap Pixels": "Sobreposição de Pixels",
"Click": "Clique",
"System Language": "Idioma do Sistema"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "Только целые слова",
"Maximum Number of Columns": "Максимальное количество колонок",
"Minimum Font Size": "Минимальный размер шрифта",
"Misc": "Разное",
"Monospace Font": "Моноширинный шрифт",
"More Info": "Подробнее",
"Nord": "Nord",
@@ -305,7 +304,6 @@
"Details": "Инфо",
"File Size:": "Размер файла:",
"Auto Detect": "Автоопределение",
"Touch": "Сенсорный экран",
"Next Section": "Следующий раздел",
"Previous Section": "Предыдущий раздел",
"Next Page": "Следующая страница",
@@ -331,5 +329,8 @@
"Translation Service": "Сервис перевода",
"Translate To": "Перевести на",
"Disable Translation": "Отключить перевод",
"Overlap Margin": "Поле перекрытия"
"Scroll": "Прокрутка",
"Overlap Pixels": "Перекрывающиеся пиксели",
"Click": "Клик",
"System Language": "Системный язык"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "Tam Kelimeleri Eşleştir",
"Maximum Number of Columns": "Maksimum Sütun Sayısı",
"Minimum Font Size": "Minimum Yazı Boyutu",
"Misc": "Diğer",
"Monospace Font": "Eş Aralıklı Yazı Tipi",
"More Info": "Daha Fazla Bilgi",
"Nord": "Nord",
@@ -305,7 +304,6 @@
"Details": "Detaylar",
"File Size:": "Dosya Boyutu:",
"Auto Detect": "Algıla",
"Touch": "Dokunmatik",
"Next Section": "Sonraki Bölüm",
"Previous Section": "Önceki Bölüm",
"Next Page": "Sonraki Sayfa",
@@ -329,5 +327,8 @@
"Translation Service": "Çeviri Hizmeti",
"Translate To": "Şuna Çevir",
"Disable Translation": "Çeviriyi Devre Dışı Bırak",
"Overlap Margin": "Çakışma Kenar Boşluğu"
"Scroll": "Kaydır",
"Overlap Pixels": "Örtüşen Pikseller",
"Click": "Klik",
"System Language": "System Dili"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "Шукати цілі слова",
"Maximum Number of Columns": "Максимальна кількість колонок",
"Minimum Font Size": "Мінімальний розмір шрифту",
"Misc": "Різне",
"Monospace Font": "Моноширинний шрифт",
"More Info": "Більше інформації",
"Nord": "Північний",
@@ -305,7 +304,6 @@
"Details": "Деталі",
"File Size:": "Розмір файлу:",
"Auto Detect": "Авто виявлення",
"Touch": "Дотик",
"Next Section": "Наступний розділ",
"Previous Section": "Попередній розділ",
"Next Page": "Наступна сторінка",
@@ -331,5 +329,8 @@
"Translation Service": "Служба перекладу",
"Translate To": "Перекласти на",
"Disable Translation": "Вимкнути переклад",
"Overlap Margin": "Відступ перекриття"
"Scroll": "Прокрутка",
"Overlap Pixels": "Пікселі перекриття",
"Click": "Клацнути",
"System Language": "Системна мова"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "Khớp toàn bộ từ",
"Maximum Number of Columns": "Số cột tối đa",
"Minimum Font Size": "Cỡ chữ tối thiểu",
"Misc": "Khác",
"Monospace Font": "Phông chữ đơn cách",
"More Info": "Thông tin thêm",
"Nord": "Nord",
@@ -305,7 +304,6 @@
"Details": "Chi tiết",
"File Size:": "Kích thước tệp:",
"Auto Detect": "Phát hiện tự động",
"Touch": "Chạm",
"Next Section": "Phần tiếp theo",
"Previous Section": "Phần trước",
"Next Page": "Trang tiếp theo",
@@ -328,5 +326,8 @@
"Translation Service": "Dịch vụ dịch thuật",
"Translate To": "Dịch sang",
"Disable Translation": "Tắt dịch",
"Overlap Margin": "Độ chồng lề"
"Scroll": "Cuộn",
"Overlap Pixels": "Chồng lấn pixel",
"Click": "Nhấp",
"System Language": "Ngôn ngữ hệ thống"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "匹配整个单词",
"Maximum Number of Columns": "分栏数",
"Minimum Font Size": "最小字号",
"Misc": "杂项",
"Monospace Font": "等宽字体",
"More Info": "更多信息",
"Nord": "极光",
@@ -305,7 +304,6 @@
"Details": "详情",
"File Size:": "文件大小",
"Auto Detect": "自动检测",
"Touch": "触控",
"Next Section": "下一章",
"Previous Section": "上一章",
"Next Page": "下一页",
@@ -328,5 +326,8 @@
"Translation Service": "翻译服务",
"Translate To": "翻译到",
"Disable Translation": "禁用翻译",
"Overlap Margin": "重叠边距"
"Scroll": "滚动",
"Overlap Pixels": "重叠像素",
"Click": "点击",
"System Language": "系统语言"
}
@@ -59,7 +59,6 @@
"Match Whole Words": "匹配整個單詞",
"Maximum Number of Columns": "分欄數",
"Minimum Font Size": "最小字號",
"Misc": "雜項",
"Monospace Font": "等寬字體",
"More Info": "更多信息",
"Nord": "極光",
@@ -305,7 +304,6 @@
"Details": "詳情",
"File Size:": "檔案大小",
"Auto Detect": "自動檢測",
"Touch": "觸控",
"Next Section": "下一章",
"Previous Section": "上一章",
"Next Page": "下一頁",
@@ -328,5 +326,8 @@
"Translation Service": "翻譯服務",
"Translate To": "翻譯至",
"Disable Translation": "停用翻譯",
"Overlap Margin": "重疊邊距"
"Scroll": "滾動",
"Overlap Pixels": "重疊像素",
"Click": "點擊",
"System Language": "系統語言"
}
@@ -129,8 +129,8 @@ const HeaderBar: React.FC<HeaderBarProps> = ({
</div>
<div className='bg-base-100 z-20 ml-auto flex h-full items-center space-x-4'>
<SettingsToggler />
<TranslationToggler bookKey={bookKey} />
<SettingsToggler />
<NotebookToggler bookKey={bookKey} />
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom dropdown-end'
@@ -1,16 +1,21 @@
import React, { useEffect, useState } from 'react';
import { RiTranslateAi } from 'react-icons/ri';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useBookDataStore } from '@/store/bookDataStore';
import { saveViewSettings } from '../utils/viewSettingsHelper';
import { isSameLang } from '@/utils/lang';
import Button from '@/components/Button';
import { useEnv } from '@/context/EnvContext';
const TranslationToggler = ({ bookKey }: { bookKey: string }) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { getBookData } = useBookDataStore();
const { getViewSettings, setViewSettings, setHoveredBookKey } = useReaderStore();
const bookData = getBookData(bookKey);
const viewSettings = getViewSettings(bookKey)!;
const [translationEnabled, setTranslationEnabled] = useState(viewSettings.translationEnabled!);
@@ -28,6 +33,9 @@ const TranslationToggler = ({ bookKey }: { bookKey: string }) => {
icon={
<RiTranslateAi className={translationEnabled ? 'text-blue-500' : 'text-base-content'} />
}
disabled={
!bookData || isSameLang(bookData.book?.primaryLanguage, viewSettings.translateTargetLang!)
}
onClick={() => setTranslationEnabled(!translationEnabled)}
tooltip={translationEnabled ? _('Disable Translation') : _('Enable Translation')}
tooltipDirection='bottom'
@@ -21,6 +21,7 @@ const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [volumeKeysToFlip, setVolumeKeysToFlip] = useState(viewSettings.volumeKeysToFlip!);
const [isDisableClick, setIsDisableClick] = useState(viewSettings.disableClick!);
const [swapClickArea, setSwapClickArea] = useState(viewSettings.swapClickArea!);
const [animated, setAnimated] = useState(viewSettings.animated!);
useEffect(() => {
if (isScrolledMode === viewSettings.scrolled) return;
@@ -67,10 +68,20 @@ const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [swapClickArea]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'animated', animated, false, false);
if (animated) {
getView(bookKey)?.renderer.setAttribute('animated', '');
} else {
getView(bookKey)?.renderer.removeAttribute('animated');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [animated]);
return (
<div className='my-4 w-full space-y-6'>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Touch')}</h2>
<h2 className='mb-2 font-medium'>{_('Scroll')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item'>
@@ -92,7 +103,7 @@ const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
/>
</div>
<NumberInput
label={_('Overlap Margin')}
label={_('Overlap Pixels')}
value={scrollingOverlap}
onChange={setScrollingOverlap}
disabled={!viewSettings.scrolled}
@@ -100,17 +111,14 @@ const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
max={200}
step={10}
/>
{appService?.isMobileApp && (
<div className='config-item'>
<span className=''>{_('Volume Keys for Page Flip')}</span>
<input
type='checkbox'
className='toggle'
checked={volumeKeysToFlip}
onChange={() => setVolumeKeysToFlip(!volumeKeysToFlip)}
/>
</div>
)}
</div>
</div>
</div>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Click')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200'>
<div className='config-item'>
<span className=''>{_('Clicks for Page Flip')}</span>
<input
@@ -130,6 +138,34 @@ const ControlPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
onChange={() => setSwapClickArea(!swapClickArea)}
/>
</div>
{appService?.isMobileApp && (
<div className='config-item'>
<span className=''>{_('Volume Keys for Page Flip')}</span>
<input
type='checkbox'
className='toggle'
checked={volumeKeysToFlip}
onChange={() => setVolumeKeysToFlip(!volumeKeysToFlip)}
/>
</div>
)}
</div>
</div>
</div>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Animation')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item'>
<span className=''>{_('Paging Animation')}</span>
<input
type='checkbox'
className='toggle'
checked={animated}
onChange={() => setAnimated(!animated)}
/>
</div>
</div>
</div>
</div>
@@ -1,3 +1,4 @@
import clsx from 'clsx';
import React from 'react';
import { MdCheck } from 'react-icons/md';
import { useSettingsStore } from '@/store/settingsStore';
@@ -21,7 +22,10 @@ const DialogMenu: React.FC<DialogMenuProps> = ({ toggleDropdown }) => {
return (
<div
tabIndex={0}
className='dropdown-content dropdown-right no-triangle border-base-200 z-20 mt-1 border shadow-2xl'
className={clsx(
'dropdown-content dropdown-right no-triangle border-base-200 z-20 mt-1 border shadow-2xl',
'text-base sm:text-sm',
)}
>
<button
className='hover:bg-base-200 text-base-content flex w-full items-center justify-between rounded-md p-1'
@@ -226,6 +226,16 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
return (
<div className='my-4 w-full space-y-6'>
<div className='flex items-center justify-between'>
<h2 className=''>{_('Override Book Font')}</h2>
<input
type='checkbox'
className='toggle'
checked={overrideFont}
onChange={() => setOverrideFont(!overrideFont)}
/>
</div>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Font Size')}</h2>
<div className='card border-base-200 border shadow'>
@@ -288,16 +298,6 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
onSelect={setDefaultCJKFont}
/>
)}
<div className='config-item'>
<span className=''>{_('Override Book Font')}</span>
<input
type='checkbox'
className='toggle'
checked={overrideFont}
onChange={() => setOverrideFont(!overrideFont)}
/>
</div>
</div>
</div>
</div>
@@ -0,0 +1,151 @@
import clsx from 'clsx';
import i18n from 'i18next';
import React, { useEffect, useState } from 'react';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { getTranslators } from '@/services/translators';
import { TRANSLATED_LANGS } from '@/services/constants';
import DropDown from './DropDown';
const LangPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
const { envConfig } = useEnv();
const { getViewSettings, setViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const [translationEnabled, setTranslationEnabled] = useState(viewSettings.translationEnabled!);
const [translationProvider, setTranslationProvider] = useState(viewSettings.translationProvider!);
const [translateTargetLang, setTranslateTargetLang] = useState(viewSettings.translateTargetLang!);
const getCurrentUILangOption = () => {
const uiLanguage = viewSettings.uiLanguage;
return {
option: uiLanguage,
label:
uiLanguage === ''
? _('Auto')
: TRANSLATED_LANGS[uiLanguage as keyof typeof TRANSLATED_LANGS],
};
};
const getLangOptions = () => {
const langs = TRANSLATED_LANGS as Record<string, string>;
const options = Object.entries(langs).map(([option, label]) => ({ option, label }));
options.sort((a, b) => a.label.localeCompare(b.label));
options.unshift({ option: '', label: _('System Language') });
return options;
};
const handleSelectUILang = (option: string) => {
saveViewSettings(envConfig, bookKey, 'uiLanguage', option, false, false);
i18n.changeLanguage(option ? option : navigator.language);
};
const getTranslationProviderOptions = () => {
const translators = getTranslators();
const availableProviders = translators.map((t) => {
return { option: t.name, label: t.label };
});
return availableProviders;
};
const getCurrentTranslationProviderOption = () => {
const option = translationProvider;
const availableProviders = getTranslationProviderOptions();
return availableProviders.find((p) => p.option === option) || availableProviders[0]!;
};
const handleSelectTranslationProvider = (option: string) => {
setTranslationProvider(option);
saveViewSettings(envConfig, bookKey, 'translationProvider', option, false, false);
viewSettings.translationProvider = option;
setViewSettings(bookKey, { ...viewSettings });
};
const getCurrentTargetLangOption = () => {
const option = translateTargetLang;
const availableOptions = getLangOptions();
return availableOptions.find((o) => o.option === option) || availableOptions[0]!;
};
const handleSelectTargetLang = (option: string) => {
setTranslateTargetLang(option);
saveViewSettings(envConfig, bookKey, 'translateTargetLang', option, false, false);
viewSettings.translateTargetLang = option;
setViewSettings(bookKey, { ...viewSettings });
};
useEffect(() => {
if (translationEnabled === viewSettings.translationEnabled) return;
saveViewSettings(envConfig, bookKey, 'translationEnabled', translationEnabled, true, false);
viewSettings.translationEnabled = translationEnabled;
setViewSettings(bookKey, { ...viewSettings });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [translationEnabled]);
return (
<div className={clsx('my-4 w-full space-y-6')}>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Language')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item'>
<span className=''>{_('Interface Language')}</span>
<DropDown
options={getLangOptions()}
selected={getCurrentUILangOption()}
onSelect={handleSelectUILang}
className='dropdown-bottom'
listClassName='!max-h-60'
/>
</div>
</div>
</div>
</div>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Translation')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200'>
<div className='config-item'>
<span className=''>{_('Enable Translation')}</span>
<input
type='checkbox'
className='toggle'
checked={translationEnabled}
onChange={() => setTranslationEnabled(!translationEnabled)}
/>
</div>
<div className='config-item'>
<span className=''>{_('Translation Service')}</span>
<DropDown
selected={getCurrentTranslationProviderOption()}
options={getTranslationProviderOptions()}
onSelect={handleSelectTranslationProvider}
disabled={!translationEnabled}
className='dropdown-top'
/>
</div>
<div className='config-item'>
<span className=''>{_('Translate To')}</span>
<DropDown
options={getLangOptions()}
selected={getCurrentTargetLangOption()}
onSelect={handleSelectTargetLang}
disabled={!translationEnabled}
className='dropdown-top'
listClassName='!max-h-60'
/>
</div>
</div>
</div>
</div>
</div>
);
};
export default LangPanel;
@@ -247,46 +247,53 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
return (
<div className='my-4 w-full space-y-6'>
<div className='flex items-center justify-between'>
<h2 className=''>{_('Override Book Layout')}</h2>
<input
type='checkbox'
className='toggle'
checked={overrideLayout}
onChange={() => setOverrideLayout(!overrideLayout)}
/>
</div>
{mightBeRTLBook && (
<div className='w-full'>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</div>
</div>
@@ -390,15 +397,6 @@ const LayoutPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
onChange={() => setHyphenation(!hyphenation)}
/>
</div>
<div className='config-item'>
<span className=''>{_('Override Book Layout')}</span>
<input
type='checkbox'
className='toggle'
checked={overrideLayout}
onChange={() => setOverrideLayout(!overrideLayout)}
/>
</div>
</div>
</div>
</div>
@@ -1,6 +1,5 @@
import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import i18n from 'i18next';
import { IoPhoneLandscapeOutline, IoPhonePortraitOutline } from 'react-icons/io5';
import { MdOutlineScreenRotation } from 'react-icons/md';
import { useEnv } from '@/context/EnvContext';
@@ -10,11 +9,8 @@ import { useTranslation } from '@/hooks/useTranslation';
import { getStyles } from '@/utils/style';
import { lockScreenOrientation } from '@/utils/bridge';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
import { getTranslators } from '@/services/translators';
import { TRANSLATED_LANGS } from '@/services/constants';
import cssbeautify from 'cssbeautify';
import cssValidate from '@/utils/css';
import DropDown from './DropDown';
const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
@@ -23,13 +19,9 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const { getView, getViewSettings, setViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const [animated, setAnimated] = useState(viewSettings.animated!);
const [draftStylesheet, setDraftStylesheet] = useState(viewSettings.userStylesheet!);
const [draftStylesheetSaved, setDraftStylesheetSaved] = useState(true);
const [screenOrientation, setScreenOrientation] = useState(viewSettings.screenOrientation!);
const [translationEnabled, setTranslationEnabled] = useState(viewSettings.translationEnabled!);
const [translationProvider, setTranslationProvider] = useState(viewSettings.translationProvider!);
const [translateTargetLang, setTranslateTargetLang] = useState(viewSettings.translateTargetLang!);
const [error, setError] = useState<string | null>(null);
const [inputFocusInAndroid, setInputFocusInAndroid] = useState(false);
@@ -101,74 +93,6 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
}
};
const getCurrentUILangOption = () => {
const uiLanguage = viewSettings.uiLanguage;
return {
option: uiLanguage,
label:
uiLanguage === ''
? _('Auto')
: TRANSLATED_LANGS[uiLanguage as keyof typeof TRANSLATED_LANGS],
};
};
const getLangOptions = () => {
const langs = TRANSLATED_LANGS as Record<string, string>;
const options = Object.entries(langs).map(([option, label]) => ({ option, label }));
options.sort((a, b) => a.label.localeCompare(b.label));
options.unshift({ option: '', label: _('Auto') });
return options;
};
const handleSelectUILang = (option: string) => {
saveViewSettings(envConfig, bookKey, 'uiLanguage', option, false, false);
i18n.changeLanguage(option ? option : navigator.language);
};
const getTranslationProviderOptions = () => {
const translators = getTranslators();
const availableProviders = translators.map((t) => {
return { option: t.name, label: t.label };
});
return availableProviders;
};
const getCurrentTranslationProviderOption = () => {
const option = translationProvider;
const availableProviders = getTranslationProviderOptions();
return availableProviders.find((p) => p.option === option) || availableProviders[0]!;
};
const handleSelectTranslationProvider = (option: string) => {
setTranslationProvider(option);
saveViewSettings(envConfig, bookKey, 'translationProvider', option, false, false);
viewSettings.translationProvider = option;
setViewSettings(bookKey, { ...viewSettings });
};
const getCurrentTargetLangOption = () => {
const option = translateTargetLang;
const availableOptions = getLangOptions();
return availableOptions.find((o) => o.option === option) || availableOptions[0]!;
};
const handleSelectTargetLang = (option: string) => {
setTranslateTargetLang(option);
saveViewSettings(envConfig, bookKey, 'translateTargetLang', option, false, false);
viewSettings.translateTargetLang = option;
setViewSettings(bookKey, { ...viewSettings });
};
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'animated', animated, false, false);
if (animated) {
getView(bookKey)?.renderer.setAttribute('animated', '');
} else {
getView(bookKey)?.renderer.removeAttribute('animated');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [animated]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'screenOrientation', screenOrientation, false, false);
if (appService?.isMobileApp) {
@@ -177,14 +101,6 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [screenOrientation]);
useEffect(() => {
if (translationEnabled === viewSettings.translationEnabled) return;
saveViewSettings(envConfig, bookKey, 'translationEnabled', translationEnabled, true, false);
viewSettings.translationEnabled = translationEnabled;
setViewSettings(bookKey, { ...viewSettings });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [translationEnabled]);
return (
<div
className={clsx(
@@ -192,79 +108,6 @@ const MiscPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
inputFocusInAndroid && 'h-[50%] overflow-y-auto pb-[200px]',
)}
>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Language')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item'>
<span className=''>{_('Interface Language')}</span>
<DropDown
options={getLangOptions()}
selected={getCurrentUILangOption()}
onSelect={handleSelectUILang}
/>
</div>
</div>
</div>
</div>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Translation')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200'>
<div className='config-item'>
<span className=''>{_('Enable Translation')}</span>
<input
type='checkbox'
className='toggle'
checked={translationEnabled}
onChange={() => setTranslationEnabled(!translationEnabled)}
/>
</div>
<div className='config-item'>
<span className=''>{_('Translation Service')}</span>
<DropDown
selected={getCurrentTranslationProviderOption()}
options={getTranslationProviderOptions()}
onSelect={handleSelectTranslationProvider}
disabled={!translationEnabled}
className='dropdown-top'
/>
</div>
<div className='config-item'>
<span className=''>{_('Translate To')}</span>
<DropDown
options={getLangOptions()}
selected={getCurrentTargetLangOption()}
onSelect={handleSelectTargetLang}
disabled={!translationEnabled}
className='dropdown-top'
listClassName='!max-h-60'
/>
</div>
</div>
</div>
</div>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Animation')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200 divide-y'>
<div className='config-item'>
<span className=''>{_('Paging Animation')}</span>
<input
type='checkbox'
className='toggle'
checked={animated}
onChange={() => setAnimated(!animated)}
/>
</div>
</div>
</div>
</div>
{appService?.isMobileApp && (
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Screen')}</h2>
@@ -5,7 +5,7 @@ import { useEnv } from '@/context/EnvContext';
import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { RiFontSize } from 'react-icons/ri';
import { RiDashboardLine } from 'react-icons/ri';
import { RiDashboardLine, RiTranslate } from 'react-icons/ri';
import { VscSymbolColor } from 'react-icons/vsc';
import { PiDotsThreeVerticalBold } from 'react-icons/pi';
import { LiaHandPointerSolid } from 'react-icons/lia';
@@ -19,9 +19,10 @@ import Dropdown from '@/components/Dropdown';
import Dialog from '@/components/Dialog';
import DialogMenu from './DialogMenu';
import ControlPanel from './ControlPanel';
import LangPanel from './LangPanel';
import MiscPanel from './MiscPanel';
type SettingsPanelType = 'Font' | 'Layout' | 'Color' | 'Control' | 'Misc';
type SettingsPanelType = 'Font' | 'Layout' | 'Color' | 'Control' | 'Language' | 'Custom';
type TabConfig = {
tab: SettingsPanelType;
@@ -34,10 +35,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
const { appService } = useEnv();
const [isRtl] = useState(() => getDirFromUILanguage() === 'rtl');
const tabsRef = useRef<HTMLDivElement | null>(null);
const [showTabLabels, setShowTabLabels] = useState(false);
const [activePanel, setActivePanel] = useState<SettingsPanelType>(
(localStorage.getItem('lastConfigPanel') || 'Font') as SettingsPanelType,
);
const [showAllTabLabels, setShowAllTabLabels] = useState(false);
const { setFontLayoutSettingsDialogOpen } = useSettingsStore();
const tabConfig = [
@@ -62,12 +60,25 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
label: _('Behavior'),
},
{
tab: 'Misc',
tab: 'Language',
icon: RiTranslate,
label: _('Language'),
},
{
tab: 'Custom',
icon: IoAccessibilityOutline,
label: _('Misc'),
label: _('Custom'),
},
] as TabConfig[];
const [activePanel, setActivePanel] = useState<SettingsPanelType>(() => {
const lastPanel = localStorage.getItem('lastConfigPanel');
if (lastPanel && tabConfig.some((tab) => tab.tab === lastPanel)) {
return lastPanel as SettingsPanelType;
}
return 'Font' as SettingsPanelType;
});
const handleSetActivePanel = (tab: SettingsPanelType) => {
setActivePanel(tab);
localStorage.setItem('lastConfigPanel', tab);
@@ -82,7 +93,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
if (!container) return;
const checkButtonWidths = () => {
const threshold = (container.clientWidth - 64) * 0.25;
const threshold = (container.clientWidth - 64) * 0.22;
const hideLabel = Array.from(container.querySelectorAll('button')).some((button) => {
const labelSpan = button.querySelector('span');
const labelText = labelSpan?.textContent || '';
@@ -100,7 +111,7 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
document.body.removeChild(clone);
return fullWidth > threshold;
});
setShowTabLabels(!hideLabel);
setShowAllTabLabels(!hideLabel);
};
checkButtonWidths();
@@ -120,83 +131,82 @@ const SettingsDialog: React.FC<{ bookKey: string; config: BookConfig }> = ({ boo
}, []);
return (
<>
<Dialog
isOpen={true}
onClose={handleClose}
className='modal-open'
boxClassName={clsx('sm:min-w-[520px]', appService?.isMobile && 'sm:max-w-[90%] sm:w-3/4')}
snapHeight={appService?.isMobile ? 0.7 : undefined}
header={
<div className='flex w-full items-center justify-between'>
<Dialog
isOpen={true}
onClose={handleClose}
className='modal-open'
boxClassName={clsx('sm:min-w-[520px]', appService?.isMobile && 'sm:max-w-[90%] sm:w-3/4')}
snapHeight={appService?.isMobile ? 0.7 : undefined}
header={
<div className='flex w-full items-center justify-between'>
<button
tabIndex={-1}
onClick={handleClose}
className={
'btn btn-ghost btn-circle flex h-8 min-h-8 w-8 hover:bg-transparent focus:outline-none sm:hidden'
}
>
{isRtl ? <MdArrowForwardIos /> : <MdArrowBackIosNew />}
</button>
<div
ref={tabsRef}
className={clsx(
'dialog-tabs ms-2 flex h-10 w-full items-center sm:ms-0',
showAllTabLabels ? 'gap-1' : 'gap-2',
)}
>
{tabConfig.map(({ tab, icon: Icon, label }) => (
<button
key={tab}
data-tab={tab}
className={clsx(
'btn btn-ghost text-base-content btn-sm',
showAllTabLabels ? 'gap-0 px-1.5' : 'gap-1 px-2',
activePanel === tab ? 'btn-active' : '',
)}
onClick={() => handleSetActivePanel(tab)}
>
<Icon className='mr-0' />
<span
className={clsx('label', !(showAllTabLabels || activePanel === tab) && 'hidden')}
>
{label}
</span>
</button>
))}
</div>
<div className='flex h-full items-center justify-end gap-x-2'>
<Dropdown
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 />}
>
<DialogMenu />
</Dropdown>
<button
tabIndex={-1}
onClick={handleClose}
className={
'btn btn-ghost btn-circle flex h-8 min-h-8 w-8 hover:bg-transparent focus:outline-none sm:hidden'
'bg-base-300/65 btn btn-ghost btn-circle hidden h-6 min-h-6 w-6 p-0 sm:flex'
}
>
{isRtl ? <MdArrowForwardIos /> : <MdArrowBackIosNew />}
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'>
<path
fill='currentColor'
d='M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z'
/>
</svg>
</button>
<div
ref={tabsRef}
className={clsx(
'dialog-tabs ms-2 flex h-10 w-full items-center',
showTabLabels ? 'gap-2' : 'gap-4',
)}
>
{tabConfig.map(({ tab, icon: Icon, label }) => (
<button
key={tab}
data-tab={tab}
className={clsx(
'btn btn-ghost text-base-content btn-sm px-1',
activePanel === tab ? 'btn-active' : '',
)}
onClick={() => handleSetActivePanel(tab)}
>
<Icon className='mr-0' />
<span className={clsx('label', !showTabLabels && 'hidden')}>{label}</span>
</button>
))}
</div>
<div className='flex h-full items-center justify-end gap-x-2'>
<Dropdown
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 />}
>
<DialogMenu />
</Dropdown>
<button
onClick={handleClose}
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>
</button>
</div>
</div>
}
>
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
{activePanel === 'Control' && <ControlPanel bookKey={bookKey} />}
{activePanel === 'Misc' && <MiscPanel bookKey={bookKey} />}
</Dialog>
</>
</div>
}
>
{activePanel === 'Font' && <FontPanel bookKey={bookKey} />}
{activePanel === 'Layout' && <LayoutPanel bookKey={bookKey} />}
{activePanel === 'Color' && <ColorPanel bookKey={bookKey} />}
{activePanel === 'Control' && <ControlPanel bookKey={bookKey} />}
{activePanel === 'Language' && <LangPanel bookKey={bookKey} />}
{activePanel === 'Custom' && <MiscPanel bookKey={bookKey} />}
</Dialog>
);
};
+1 -1
View File
@@ -35,7 +35,7 @@ const Button: React.FC<ButtonProps> = ({
className={clsx(
'btn btn-ghost h-8 min-h-8 w-8 p-0',
appService?.isMobileApp && 'hover:bg-transparent',
disabled && 'btn-disabled !bg-transparent',
disabled && 'btn-disabled !bg-transparent opacity-50',
className,
)}
onClick={disabled ? undefined : onClick}
+12
View File
@@ -39,3 +39,15 @@ export const localeToLang = (locale: string): string => {
}
return mapping[locale] || locale.split('-')[0]!.toLowerCase();
};
export const normalizedLangCode = (lang: string | null | undefined): string => {
if (!lang) return '';
return lang.split('-')[0]!.toLowerCase();
};
export const isSameLang = (lang1?: string | null, lang2?: string | null): boolean => {
if (!lang1 || !lang2) return false;
const normalizedLang1 = normalizedLangCode(lang1);
const normalizedLang2 = normalizedLangCode(lang2);
return normalizedLang1 === normalizedLang2;
};