feat: add options to customize the style of highlighted text of current sentence of TTS (#2358)

This commit is contained in:
Huang Xin
2025-10-29 17:17:36 +08:00
committed by GitHub
parent c7ba44a91e
commit 1d87dcdb7f
41 changed files with 887 additions and 301 deletions
@@ -651,5 +651,13 @@
"Purchase Successful!": "تمت عملية الشراء بنجاح!",
"lifetime": "مدى الحياة",
"Thank you for your purchase! Your payment has been processed successfully.": "شكرًا لشرائك! تم معالجة الدفع بنجاح.",
"Order ID:": "معرف الطلب:"
"Order ID:": "معرف الطلب:",
"TTS Highlighting": "تمييز تحويل النص إلى كلام",
"Style": "النمط",
"Underline": "تسطير",
"Strikethrough": "يتوسطه خط",
"Squiggly": "متعرج",
"Outline": "مخطط",
"Save Current Color": "حفظ اللون الحالي",
"Quick Colors": "ألوان سريعة"
}
@@ -635,5 +635,13 @@
"Purchase Successful!": "ক্রয় সফল!",
"lifetime": "জীবনকাল",
"Thank you for your purchase! Your payment has been processed successfully.": "আপনার ক্রয়ের জন্য ধন্যবাদ! আপনার পেমেন্ট সফলভাবে প্রক্রিয়া করা হয়েছে।",
"Order ID:": "অর্ডার আইডি:"
"Order ID:": "অর্ডার আইডি:",
"TTS Highlighting": "টিটিএস হাইলাইটিং",
"Style": "শৈলী",
"Underline": "আন্ডারলাইন",
"Strikethrough": "কাটা লেখা",
"Squiggly": "ঢেউখেলানো দাগ",
"Outline": "বাহিরের রেখা",
"Save Current Color": "বর্তমান রঙ সংরক্ষণ করুন",
"Quick Colors": "দ্রুত রংসমূহ"
}
@@ -631,5 +631,13 @@
"Purchase Successful!": "ক্রয় সফল!",
"lifetime": "জীবনকাল",
"Thank you for your purchase! Your payment has been processed successfully.": "আপনার ক্রয়ের জন্য ধন্যবাদ! আপনার পেমেন্ট সফলভাবে প্রক্রিয়াকৃত হয়েছে।",
"Order ID:": "অর্ডার আইডি:"
"Order ID:": "অর্ডার আইডি:",
"TTS Highlighting": "ཡིག་འབྲུ་སྦྱོར་བའི་གསལ་བཤད།",
"Style": "བཟོ་ལྟ།",
"Underline": "འོག་ཐིག",
"Strikethrough": "འཐེན་ཐིག",
"Squiggly": "འཁྱོག་ཐིག",
"Outline": "མཐའ་ཐིག",
"Save Current Color": "མིག་སྔའི་ཚོན་གཏན་འཁེལ་བྱེད།",
"Quick Colors": "མྱུར་ཚོན།"
}
@@ -635,5 +635,13 @@
"Purchase Successful!": "Kauf erfolgreich!",
"lifetime": "lebenslang",
"Thank you for your purchase! Your payment has been processed successfully.": "Danke für Ihren Kauf! Ihre Zahlung wurde erfolgreich verarbeitet.",
"Order ID:": "Bestell-ID:"
"Order ID:": "Bestell-ID:",
"TTS Highlighting": "TTS-Hervorhebung",
"Style": "Stil",
"Underline": "Unterstreichen",
"Strikethrough": "Durchgestrichen",
"Squiggly": "Wellig",
"Outline": "Umriss",
"Save Current Color": "Aktuelle Farbe speichern",
"Quick Colors": "Schnellfarben"
}
@@ -635,5 +635,13 @@
"Purchase Successful!": "Η αγορά ήταν επιτυχής!",
"lifetime": "διαρκής",
"Thank you for your purchase! Your payment has been processed successfully.": "Σας ευχαριστούμε για την αγορά σας! Η πληρωμή σας έχει επεξεργαστεί με επιτυχία.",
"Order ID:": "Αριθμός παραγγελίας:"
"Order ID:": "Αριθμός παραγγελίας:",
"TTS Highlighting": "Επισήμανση TTS",
"Style": "Στυλ",
"Underline": "Υπογράμμιση",
"Strikethrough": "Διαγραμμένο",
"Squiggly": "Κυματιστό",
"Outline": "Περίγραμμα",
"Save Current Color": "Αποθήκευση τρέχουσας χρώματος",
"Quick Colors": "Γρήγορα χρώματα"
}
@@ -639,5 +639,13 @@
"Purchase Successful!": "¡Compra Exitosa!",
"lifetime": "de por vida",
"Thank you for your purchase! Your payment has been processed successfully.": "¡Gracias por tu compra! Tu pago se ha procesado correctamente.",
"Order ID:": "ID de pedido:"
"Order ID:": "ID de pedido:",
"TTS Highlighting": "TTS Resaltado",
"Style": "Estilo",
"Underline": "Subrayado",
"Strikethrough": "Tachado",
"Squiggly": "Ondulado",
"Outline": "Contorno",
"Save Current Color": "Guardar Color Actual",
"Quick Colors": "Colores Rápidos"
}
@@ -635,5 +635,13 @@
"Purchase Successful!": "خرید با موفقیت انجام شد!",
"lifetime": "مادام‌العمر",
"Thank you for your purchase! Your payment has been processed successfully.": "از خرید شما متشکریم! پرداخت شما با موفقیت انجام شد.",
"Order ID:": "شناسه سفارش:"
"Order ID:": "شناسه سفارش:",
"TTS Highlighting": "هایلایت TTS",
"Style": "سبک",
"Underline": "خط زیرین",
"Strikethrough": "خط‌خورده",
"Squiggly": "خط‌موج‌دار",
"Outline": "خط‌کشی",
"Save Current Color": "ذخیره رنگ فعلی",
"Quick Colors": "رنگ‌های سریع"
}
@@ -639,5 +639,13 @@
"Purchase Successful!": "Achat Réussi!",
"lifetime": "à vie",
"Thank you for your purchase! Your payment has been processed successfully.": "Merci pour votre achat ! Votre paiement a été traité avec succès.",
"Order ID:": "ID de commande :"
"Order ID:": "ID de commande :",
"TTS Highlighting": "Surbrillance TTS",
"Style": "Style",
"Underline": "Souligner",
"Strikethrough": "Barré",
"Squiggly": "Ondulé",
"Outline": "Contour",
"Save Current Color": "Enregistrer la Couleur Actuelle",
"Quick Colors": "Couleurs Rapides"
}
@@ -635,5 +635,13 @@
"Purchase Successful!": "खरीदारी सफल!",
"lifetime": "जीवनकाल",
"Thank you for your purchase! Your payment has been processed successfully.": "आपकी खरीदारी के लिए धन्यवाद! आपका भुगतान सफलतापूर्वक संसाधित हो गया है।",
"Order ID:": "ऑर्डर आईडी:"
"Order ID:": "ऑर्डर आईडी:",
"TTS Highlighting": "TTS हाइलाइटिंग",
"Style": "शैली",
"Underline": "रेखांकित",
"Strikethrough": "रेखांकित करें",
"Squiggly": "स्क्विगली",
"Outline": "आउटलाइन",
"Save Current Color": "वर्तमान रंग सहेजें",
"Quick Colors": "त्वरित रंग"
}
@@ -631,5 +631,13 @@
"Purchase Successful!": "Pembelian Berhasil!",
"lifetime": "seumur hidup",
"Thank you for your purchase! Your payment has been processed successfully.": "Terima kasih atas pembelian Anda! Pembayaran Anda telah berhasil diproses.",
"Order ID:": "ID Pesanan:"
"Order ID:": "ID Pesanan:",
"TTS Highlighting": "TTS Menyorot",
"Style": "Gaya",
"Underline": "Garisan Bawah",
"Strikethrough": "Garis Tengah",
"Squiggly": "Bergelombang",
"Outline": "Garis Besar",
"Save Current Color": "Simpan Warna Saat Ini",
"Quick Colors": "Warna Cepat"
}
@@ -639,5 +639,13 @@
"Purchase Successful!": "Acquisto Riuscito!",
"lifetime": "a vita",
"Thank you for your purchase! Your payment has been processed successfully.": "Grazie per il tuo acquisto! Il pagamento è stato elaborato con successo.",
"Order ID:": "ID Ordine:"
"Order ID:": "ID Ordine:",
"TTS Highlighting": "Evidenziazione TTS",
"Style": "Stile",
"Underline": "Sottolineato",
"Strikethrough": "Barrato",
"Squiggly": "Ondulato",
"Outline": "Contorno",
"Save Current Color": "Salva Colore Corrente",
"Quick Colors": "Colori Veloci"
}
@@ -631,5 +631,13 @@
"Purchase Successful!": "購入成功!",
"lifetime": "ライフタイム",
"Thank you for your purchase! Your payment has been processed successfully.": "ご購入ありがとうございます!お支払いが正常に処理されました。",
"Order ID:": "購入ID"
"Order ID:": "購入ID",
"TTS Highlighting": "TTSハイライト",
"Style": "スタイル",
"Underline": "下線",
"Strikethrough": "取り消し線",
"Squiggly": "波線",
"Outline": "アウトライン",
"Save Current Color": "現在の色を保存",
"Quick Colors": "クイックカラー"
}
@@ -631,5 +631,13 @@
"Purchase Successful!": "구매 성공!",
"lifetime": "평생",
"Thank you for your purchase! Your payment has been processed successfully.": "구매해주셔서 감사합니다! 결제가 성공적으로 처리되었습니다.",
"Order ID:": "주문 ID:"
"Order ID:": "주문 ID:",
"TTS Highlighting": "TTS 하이라이팅",
"Style": "스타일",
"Underline": "밑줄",
"Strikethrough": "취소선",
"Squiggly": "물결선",
"Outline": "윤곽선",
"Save Current Color": "현재 색상 저장",
"Quick Colors": "빠른 색상"
}
@@ -635,5 +635,13 @@
"Purchase Successful!": "Aankoop Succesvol!",
"lifetime": "levenslang",
"Thank you for your purchase! Your payment has been processed successfully.": "Bedankt voor je aankoop! Je betaling is succesvol verwerkt.",
"Order ID:": "Bestel-ID:"
"Order ID:": "Bestel-ID:",
"TTS Highlighting": "TTS-markering",
"Style": "Stijl",
"Underline": "Onderstrepen",
"Strikethrough": "Doorhalen",
"Squiggly": "Kronkelig",
"Outline": "Omtrek",
"Save Current Color": "Huidige kleur opslaan",
"Quick Colors": "Snelle kleuren"
}
@@ -643,5 +643,13 @@
"Purchase Successful!": "Zakup Udany!",
"lifetime": "dożywotnio",
"Thank you for your purchase! Your payment has been processed successfully.": "Dziękujemy za zakup! Twoja płatność została pomyślnie przetworzona.",
"Order ID:": "ID zamówienia:"
"Order ID:": "ID zamówienia:",
"TTS Highlighting": "Wyróżnianie TTS",
"Style": "Styl",
"Underline": "Podkreślenie",
"Strikethrough": "Przekreślenie",
"Squiggly": "Falista linia",
"Outline": "Obrys",
"Save Current Color": "Zapisz bieżący kolor",
"Quick Colors": "Szybkie kolory"
}
@@ -639,5 +639,13 @@
"Purchase Successful!": "Compra Bem-Sucedida!",
"lifetime": "vitalício",
"Thank you for your purchase! Your payment has been processed successfully.": "Obrigado pela sua compra! Seu pagamento foi processado com sucesso.",
"Order ID:": "ID do Pedido:"
"Order ID:": "ID do Pedido:",
"TTS Highlighting": "Destaque TTS",
"Style": "Estilo",
"Underline": "Sublinhado",
"Strikethrough": "Tachado",
"Squiggly": "Ondulado",
"Outline": "Contorno",
"Save Current Color": "Salvar cor atual",
"Quick Colors": "Cores rápidas"
}
@@ -643,5 +643,13 @@
"Purchase Successful!": "Покупка Успешна!",
"lifetime": "пожизненный",
"Thank you for your purchase! Your payment has been processed successfully.": "Спасибо за вашу покупку! Ваш платеж был успешно обработан.",
"Order ID:": "ID заказа:"
"Order ID:": "ID заказа:",
"TTS Highlighting": "Выделение TTS",
"Style": "Стиль",
"Underline": "Подчёркивание",
"Strikethrough": "Зачёркивание",
"Squiggly": "Волнистая линия",
"Outline": "Контур",
"Save Current Color": "Сохранить текущий цвет",
"Quick Colors": "Быстрые цвета"
}
@@ -635,5 +635,13 @@
"Purchase Successful!": "මිලදී ගැනීම සාර්ථකයි!",
"lifetime": "පොදුකාලීන",
"Thank you for your purchase! Your payment has been processed successfully.": "ඔබේ මිලදී ගැනීමට ස්තූතියි! ඔබේ ගෙවීම සාර්ථකව සැකසිණි.",
"Order ID:": "ඇණවුම් හැඳුනුම්පත:"
"Order ID:": "ඇණවුම් හැඳුනුම්පත:",
"TTS Highlighting": "TTS උද්දීපනය",
"Style": "රූපය",
"Underline": "ඇටය",
"Strikethrough": "ඇඳුම",
"Squiggly": "වැලි රේඛාව",
"Outline": "පරිසීමාව",
"Save Current Color": "දැන් ඇති වර්ණය සුරකින්න",
"Quick Colors": "ක්ෂණික වර්ණ"
}
@@ -635,5 +635,13 @@
"Purchase Successful!": "Köp lyckades!",
"lifetime": "livstid",
"Thank you for your purchase! Your payment has been processed successfully.": "Tack för ditt köp! Din betalning har behandlats.",
"Order ID:": "Order-ID:"
"Order ID:": "Order-ID:",
"TTS Highlighting": "TTS-markering",
"Style": "Stil",
"Underline": "Understrykning",
"Strikethrough": "Genomstrykning",
"Squiggly": "Vågig linje",
"Outline": "Kontur",
"Save Current Color": "Spara aktuell färg",
"Quick Colors": "Snabba färger"
}
@@ -635,5 +635,13 @@
"Purchase Successful!": "கொள்முதல் வெற்றிகரமாக முடிந்தது!",
"lifetime": "ஆயுள்திருத்தம்",
"Thank you for your purchase! Your payment has been processed successfully.": "உங்கள் வாங்குதலுக்கு நன்றி! உங்கள் கட்டணம் வெற்றிகரமாக செயல்படுத்தப்பட்டது.",
"Order ID:": "ஆர்டர் ஐடி:"
"Order ID:": "ஆர்டர் ஐடி:",
"TTS Highlighting": "TTS சிறப்பித்தல்",
"Style": "பாணி",
"Underline": "அடிக்கோடு",
"Strikethrough": "குறிக்கோடு",
"Squiggly": "சுருள் கோடு",
"Outline": "வட்டம்",
"Save Current Color": "தற்போதைய நிறத்தை சேமிக்கவும்",
"Quick Colors": "விரைவு நிறங்கள்"
}
@@ -631,5 +631,13 @@
"Purchase Successful!": "การซื้อสำเร็จ!",
"lifetime": "ตลอดชีพ",
"Thank you for your purchase! Your payment has been processed successfully.": "ขอบคุณสำหรับการซื้อ! การชำระเงินของคุณได้รับการประมวลผลเรียบร้อยแล้ว",
"Order ID:": "รหัสคำสั่งซื้อ:"
"Order ID:": "รหัสคำสั่งซื้อ:",
"TTS Highlighting": "การไฮไลต์ TTS",
"Style": "สไตล์",
"Underline": "ขีดเส้นใต้",
"Strikethrough": "ขีดฆ่า",
"Squiggly": "เส้นหยัก",
"Outline": "เส้นขอบ",
"Save Current Color": "บันทึกสีปัจจุบัน",
"Quick Colors": "สีด่วน"
}
@@ -635,5 +635,13 @@
"Purchase Successful!": "Satın Alma Başarılı!",
"lifetime": "ömür boyu",
"Thank you for your purchase! Your payment has been processed successfully.": "Satın alma işleminiz için teşekkürler! Ödemeniz başarıyla işlendi.",
"Order ID:": "Sipariş Kimliği:"
"Order ID:": "Sipariş Kimliği:",
"TTS Highlighting": "TTS Vurgulama",
"Style": "Stil",
"Underline": "Altı Çizili",
"Strikethrough": "Üstü Çizili",
"Squiggly": "Dalgalı Çizgi",
"Outline": "Kontur",
"Save Current Color": "Mevcut Rengi Kaydet",
"Quick Colors": "Hızlı Renkler"
}
@@ -643,5 +643,13 @@
"Purchase Successful!": "Покупка успішна!",
"lifetime": "довічний",
"Thank you for your purchase! Your payment has been processed successfully.": "Дякуємо за вашу покупку! Ваш платіж успішно оброблено.",
"Order ID:": "ID замовлення:"
"Order ID:": "ID замовлення:",
"TTS Highlighting": "Підсвічування TTS",
"Style": "Стиль",
"Underline": "Підкреслення",
"Strikethrough": "Закреслення",
"Squiggly": "Хвиляста лінія",
"Outline": "Контур",
"Save Current Color": "Зберегти поточний колір",
"Quick Colors": "Швидкі кольори"
}
@@ -631,5 +631,13 @@
"Purchase Successful!": "Mua hàng thành công!",
"lifetime": "trọn đời",
"Thank you for your purchase! Your payment has been processed successfully.": "Cảm ơn bạn đã mua hàng! Thanh toán của bạn đã được xử lý thành công.",
"Order ID:": "Mã đơn hàng:"
"Order ID:": "Mã đơn hàng:",
"TTS Highlighting": "Đánh dấu TTS",
"Style": "Kiểu",
"Underline": "Gạch chân",
"Strikethrough": "Gạch ngang",
"Squiggly": "Gạch ngoằn ngoèo",
"Outline": "Đường viền",
"Save Current Color": "Lưu màu hiện tại",
"Quick Colors": "Màu nhanh"
}
@@ -631,5 +631,13 @@
"Purchase Successful!": "购买成功!",
"lifetime": "永久",
"Thank you for your purchase! Your payment has been processed successfully.": "感谢您的购买!您的付款已成功处理。",
"Order ID:": "订单号:"
"Order ID:": "订单号:",
"TTS Highlighting": "TTS 高亮",
"Style": "样式",
"Underline": "下划线",
"Strikethrough": "删除线",
"Squiggly": "波浪线",
"Outline": "轮廓",
"Save Current Color": "保存当前颜色",
"Quick Colors": "快速颜色"
}
@@ -631,5 +631,13 @@
"Purchase Successful!": "購買成功!",
"lifetime": "永久",
"Thank you for your purchase! Your payment has been processed successfully.": "感謝您的購買!您的付款已成功處理。",
"Order ID:": "訂單編號:"
"Order ID:": "訂單編號:",
"TTS Highlighting": "TTS 高亮",
"Style": "樣式",
"Underline": "底線",
"Strikethrough": "刪除線",
"Squiggly": "波浪線",
"Outline": "輪廓",
"Save Current Color": "儲存目前顏色",
"Quick Colors": "快速顏色"
}
@@ -323,7 +323,7 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
setShowIndicator(true);
const ttsController = new TTSController(appService, view);
await ttsController.init();
await ttsController.initViewTTS();
await ttsController.initViewTTS(viewSettings.ttsHighlightOptions);
const ssml = view.tts?.from(ttsFromRange);
if (ssml) {
const lang = parseSSMLLang(ssml, primaryLang) || 'en';
@@ -625,6 +625,13 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
};
}, [hoveredBookKey]);
useEffect(() => {
const ttsHighlightOptions = viewSettings?.ttsHighlightOptions;
if (ttsControllerRef.current && ttsHighlightOptions) {
ttsControllerRef.current.initViewTTS(ttsHighlightOptions);
}
}, [viewSettings?.ttsHighlightOptions]);
return (
<>
{showPanel && <Overlay onDismiss={handleDismissPopup} />}
@@ -1,9 +1,4 @@
import React, { useState, useEffect } from 'react';
import { MdOutlineLightMode, MdOutlineDarkMode, MdClose, MdAdd } from 'react-icons/md';
import { MdRadioButtonUnchecked, MdRadioButtonChecked } from 'react-icons/md';
import { CgColorPicker } from 'react-icons/cg';
import { TbSunMoon } from 'react-icons/tb';
import { PiPlus } from 'react-icons/pi';
import {
applyCustomTheme,
CustomTheme,
@@ -17,48 +12,55 @@ import { useThemeStore } from '@/store/themeStore';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useResetViewSettings } from '@/hooks/useResetSettings';
import { useCustomTextureStore } from '@/store/customTextureStore';
import { saveViewSettings } from '@/helpers/settings';
import { CODE_LANGUAGES, CodeLanguage, manageSyntaxHighlighting } from '@/utils/highlightjs';
import { manageSyntaxHighlighting } from '@/utils/highlightjs';
import { SettingsPanelPanelProp } from './SettingsDialog';
import { useFileSelector } from '@/hooks/useFileSelector';
import { PREDEFINED_TEXTURES } from '@/styles/textures';
import { HighlightColor } from '@/types/book';
import { HIGHLIGHT_COLOR_HEX } from '@/services/constants';
import Select from '@/components/Select';
import ThemeEditor from './ThemeEditor';
import ColorInput from './ColorInput';
import ThemeEditor from './color/ThemeEditor';
import ThemeModeSelector from './color/ThemeModeSelector';
import ThemeColorSelector from './color/ThemeColorSelector';
import BackgroundTextureSelector from './color/BackgroundTextureSelector';
import HighlightColorsEditor from './color/HighlightColorsEditor';
import TTSHighlightStyleEditor, { TTSHighlightStyle } from './color/TTSHighlightStyleEditor';
import CodeHighlightingSettings from './color/CodeHighlightingSettings';
const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset }) => {
const _ = useTranslation();
const { themeMode, themeColor, isDarkMode, setThemeMode, setThemeColor, saveCustomTheme } =
useThemeStore();
const { envConfig, appService } = useEnv();
const { settings, setSettings } = useSettingsStore();
const { settings, setSettings, saveSettings } = useSettingsStore();
const { getView, getViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey) || settings.globalViewSettings;
const [invertImgColorInDark, setInvertImgColorInDark] = useState(
viewSettings.invertImgColorInDark,
);
const iconSize16 = useResponsiveSize(16);
const iconSize24 = useResponsiveSize(24);
const [editTheme, setEditTheme] = useState<CustomTheme | null>(null);
const [customThemes, setCustomThemes] = useState<Theme[]>([]);
const [showCustomThemeEditor, setShowCustomThemeEditor] = useState(false);
const [overrideColor, setOverrideColor] = useState(viewSettings.overrideColor);
const [codeHighlighting, setcodeHighlighting] = useState(viewSettings.codeHighlighting);
const [codeLanguage, setCodeLanguage] = useState(viewSettings.codeLanguage);
const [selectedTextureId, setSelectedTextureId] = useState(viewSettings.backgroundTextureId);
const [backgroundOpacity, setBackgroundOpacity] = useState(viewSettings.backgroundOpacity);
const [backgroundSize, setBackgroundSize] = useState(viewSettings.backgroundSize);
const [ttsHighlightStyle, setTtsHighlightStyle] = useState(
viewSettings.ttsHighlightOptions.style,
);
const [ttsHighlightColor, setTtsHighlightColor] = useState(
viewSettings.ttsHighlightOptions.color,
);
const [customHighlightColors, setCustomHighlightColors] = useState(
settings.globalReadSettings.customHighlightColors,
);
const [customTtsHighlightColors, setCustomTtsHighlightColors] = useState(
settings.globalReadSettings.customTtsHighlightColors || [],
);
const {
textures: customTextures,
@@ -70,7 +72,6 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
saveCustomTextures,
} = useCustomTextureStore();
const resetToDefaults = useResetViewSettings();
const { selectFiles } = useFileSelector(appService, _);
const handleReset = () => {
@@ -110,7 +111,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
}, [overrideColor]);
useEffect(() => {
let update = false; // check if we need to update syntax highlighting
let update = false;
if (codeHighlighting !== viewSettings.codeHighlighting) {
saveViewSettings(envConfig, bookKey, 'codeHighlighting', codeHighlighting);
update = true;
@@ -172,7 +173,6 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
const handleSaveCustomTheme = (customTheme: CustomTheme) => {
applyCustomTheme(customTheme);
saveCustomTheme(envConfig, settings, customTheme);
setSettings({ ...settings });
setThemeColor(customTheme.name);
setShowCustomThemeEditor(false);
@@ -180,7 +180,6 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
const handleDeleteCustomTheme = (customTheme: CustomTheme) => {
saveCustomTheme(envConfig, settings, customTheme, true);
setSettings({ ...settings });
setThemeColor('default');
setShowCustomThemeEditor(false);
@@ -224,7 +223,35 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
saveCustomTextures(envConfig);
};
const allTextures = [...PREDEFINED_TEXTURES, ...customTextures.filter((t) => !t.deletedAt)];
const handleHighlightColorsChange = (colors: typeof customHighlightColors) => {
setCustomHighlightColors(colors);
settings.globalReadSettings.customHighlightColors = colors;
setSettings(settings);
saveSettings(envConfig, settings);
};
const handleTTSStyleChange = (style: TTSHighlightStyle) => {
setTtsHighlightStyle(style);
saveViewSettings(envConfig, bookKey, 'ttsHighlightOptions', {
style,
color: ttsHighlightColor,
});
};
const handleTTSColorChange = (color: string) => {
setTtsHighlightColor(color);
saveViewSettings(envConfig, bookKey, 'ttsHighlightOptions', {
style: ttsHighlightStyle,
color,
});
};
const handleCustomTtsColorsChange = (colors: string[]) => {
setCustomTtsHighlightColors(colors);
settings.globalReadSettings.customTtsHighlightColors = colors;
setSettings(settings);
saveSettings(envConfig, settings);
};
return (
<div className='my-4 w-full space-y-6'>
@@ -237,32 +264,7 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
/>
) : (
<>
<div className='flex items-center justify-between'>
<h2 className='font-medium'>{_('Theme Mode')}</h2>
<div className='flex gap-4'>
<button
title={_('Auto Mode')}
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setThemeMode('auto')}
>
<TbSunMoon />
</button>
<button
title={_('Light Mode')}
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setThemeMode('light')}
>
<MdOutlineLightMode />
</button>
<button
title={_('Dark Mode')}
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
onClick={() => setThemeMode('dark')}
>
<MdOutlineDarkMode />
</button>
</div>
</div>
<ThemeModeSelector themeMode={themeMode} onThemeModeChange={setThemeMode} />
<div className='flex items-center justify-between'>
<h2 className='font-medium'>{_('Invert Image In Dark Mode')}</h2>
@@ -285,226 +287,48 @@ const ColorPanel: React.FC<SettingsPanelPanelProp> = ({ bookKey, onRegisterReset
/>
</div>
<div>
<h2 className='mb-2 font-medium'>{_('Theme Color')}</h2>
<div className='grid grid-cols-3 gap-4'>
{themes.concat(customThemes).map(({ name, label, colors, isCustomizale }) => (
<button
key={name}
tabIndex={0}
onClick={() => setThemeColor(name)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
setThemeColor(name);
}
e.stopPropagation();
}}
className={`relative flex cursor-pointer flex-col items-center justify-center rounded-lg p-4 shadow-md ${
themeColor === name ? 'ring-2 ring-indigo-500 ring-offset-2' : ''
}`}
style={{
backgroundColor: isDarkMode
? colors.dark['base-100']
: colors.light['base-100'],
color: isDarkMode ? colors.dark['base-content'] : colors.light['base-content'],
}}
>
<input
aria-label={_(label)}
type='radio'
name='theme'
value={name}
checked={themeColor === name}
onChange={() => setThemeColor(name)}
className='hidden'
/>
{themeColor === name ? (
<MdRadioButtonChecked size={iconSize24} />
) : (
<MdRadioButtonUnchecked size={iconSize24} />
)}
<span>{_(label)}</span>
{isCustomizale && themeColor === name && (
<button onClick={() => handleEditTheme(name)}>
<CgColorPicker size={iconSize16} className='absolute right-2 top-2' />
</button>
)}
</button>
))}
<button
className={`relative flex cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed p-4 shadow-md`}
onClick={() => setShowCustomThemeEditor(true)}
>
<PiPlus size={iconSize24} />
<span>{_('Custom')}</span>
</button>
</div>
</div>
<ThemeColorSelector
themes={themes.concat(customThemes)}
themeColor={themeColor}
isDarkMode={isDarkMode}
onThemeColorChange={setThemeColor}
onEditTheme={handleEditTheme}
onCreateTheme={() => setShowCustomThemeEditor(true)}
/>
<div>
<h2 className='mb-2 font-medium'>{_('Background Image')}</h2>
<div className='mb-4 grid grid-cols-2 gap-4'>
{allTextures.map((texture) => (
<button
key={texture.id}
onClick={() => setSelectedTextureId(texture.id)}
className={`bg-base-100 relative flex flex-col items-center justify-center rounded-lg border-2 p-4 shadow-md transition-all ${
selectedTextureId === texture.id
? 'ring-2 ring-indigo-500 ring-offset-2'
: 'border-base-300'
}`}
style={{
backgroundImage: texture.loaded
? `url("${texture.blobUrl || texture.url}")`
: 'none',
backgroundSize: 'cover',
backgroundPosition: 'top',
minHeight: '80px',
}}
>
{selectedTextureId === texture.id && (
<MdRadioButtonChecked
size={iconSize24}
className='absolute right-2 top-2 rounded-full bg-white text-indigo-500'
/>
)}
{!PREDEFINED_TEXTURES.find((t) => t.id === texture.id) && (
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteCustomTexture(texture.id);
}}
className='absolute left-2 top-2 rounded-full bg-red-500 p-1 text-white transition-colors hover:bg-red-600'
title={_('Delete')}
>
<MdClose size={16} />
</button>
)}
</button>
))}
<BackgroundTextureSelector
predefinedTextures={PREDEFINED_TEXTURES}
customTextures={customTextures.filter((t) => !t.deletedAt)}
selectedTextureId={selectedTextureId}
backgroundOpacity={backgroundOpacity}
backgroundSize={backgroundSize}
onTextureSelect={setSelectedTextureId}
onOpacityChange={setBackgroundOpacity}
onSizeChange={setBackgroundSize}
onImportImage={handleImportImage}
onDeleteTexture={handleDeleteCustomTexture}
/>
{/* Custom Image Upload */}
<div
className={`border-base-300 relative flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-4 shadow-md transition-all`}
style={{ minHeight: '80px' }}
>
<button
className='card-body flex cursor-pointer items-center justify-center p-2 text-center'
onClick={handleImportImage}
>
<div className='flex items-center gap-2'>
<div className='flex items-center justify-center'>
<MdAdd className='text-primary/85 group-hover:text-primary h-6 w-6' />
</div>
<div className='text-primary/85 group-hover:text-primary line-clamp-1 font-medium'>
{_('Import Image')}
</div>
</div>
</button>
</div>
</div>
<HighlightColorsEditor
customHighlightColors={customHighlightColors}
onChange={handleHighlightColorsChange}
/>
{/* Background Image Settings */}
{selectedTextureId !== 'none' && (
<div className='card border-base-200 bg-base-100 space-y-4 border p-4 shadow'>
<div className='flex items-center justify-between'>
<span className='text-sm font-medium'>{_('Opacity')}</span>
<div className='flex items-center gap-2'>
<input
type='range'
min='0'
max='1'
step='0.05'
value={backgroundOpacity}
onChange={(e) => setBackgroundOpacity(parseFloat(e.target.value))}
className='range range-sm w-32'
/>
<span className='w-12 text-right text-sm'>
{Math.round(backgroundOpacity * 100)}%
</span>
</div>
</div>
<TTSHighlightStyleEditor
style={ttsHighlightStyle}
color={ttsHighlightColor}
customColors={customTtsHighlightColors}
onStyleChange={handleTTSStyleChange}
onColorChange={handleTTSColorChange}
onCustomColorsChange={handleCustomTtsColorsChange}
/>
<div className='flex items-center justify-between'>
<span className='text-sm font-medium'>{_('Size')}</span>
<Select
value={backgroundSize}
onChange={(e) => setBackgroundSize(e.target.value)}
options={[
{ value: 'auto', label: _('Auto') },
{ value: 'cover', label: _('Cover') },
{ value: 'contain', label: _('Contain') },
]}
/>
</div>
</div>
)}
</div>
<div>
<h2 className='mb-2 font-medium'>{_('Highlight Colors')}</h2>
<div className='card border-base-200 bg-base-100 overflow-visible border p-4 shadow'>
<div className='grid grid-cols-3 gap-3 sm:grid-cols-5'>
{(['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[]).map(
(color, index, array) => {
const position =
index === 0 ? 'left' : index === array.length - 1 ? 'right' : 'center';
return (
<div key={color} className='flex flex-col items-center gap-2'>
<div
className='border-base-300 h-8 w-8 rounded-full border-2 shadow-sm'
style={{ backgroundColor: customHighlightColors[color] }}
/>
<ColorInput
label=''
value={customHighlightColors[color]}
compact={true}
pickerPosition={position}
onChange={(value: string) => {
customHighlightColors[color] = value;
setCustomHighlightColors({ ...customHighlightColors });
settings.globalReadSettings.customHighlightColors =
customHighlightColors;
setSettings(settings);
}}
/>
</div>
);
},
)}
</div>
</div>
</div>
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Code Highlighting')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200'>
<div className='config-item'>
<span className=''>{_('Enable Highlighting')}</span>
<input
type='checkbox'
className='toggle'
checked={codeHighlighting}
onChange={() => setcodeHighlighting(!codeHighlighting)}
/>
</div>
<div className='config-item'>
<span className=''>{_('Code Language')}</span>
<Select
value={codeLanguage}
onChange={(event) => setCodeLanguage(event.target.value as CodeLanguage)}
options={CODE_LANGUAGES.map((lang) => ({
value: lang,
label: lang === 'auto-detect' ? _('Auto') : lang,
}))}
disabled={!codeHighlighting}
/>
</div>
</div>
</div>
</div>
<CodeHighlightingSettings
codeHighlighting={codeHighlighting}
codeLanguage={codeLanguage}
onToggle={setcodeHighlighting}
onLanguageChange={setCodeLanguage}
/>
</>
)}
</div>
@@ -0,0 +1,145 @@
import React from 'react';
import { MdRadioButtonChecked, MdClose, MdAdd } from 'react-icons/md';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import Select from '@/components/Select';
interface Texture {
id: string;
url?: string;
blobUrl?: string;
loaded?: boolean;
}
interface BackgroundTextureSelectorProps {
predefinedTextures: Texture[];
customTextures: Texture[];
selectedTextureId: string;
backgroundOpacity: number;
backgroundSize: string;
onTextureSelect: (id: string) => void;
onOpacityChange: (opacity: number) => void;
onSizeChange: (size: string) => void;
onImportImage: () => void;
onDeleteTexture: (id: string) => void;
}
const BackgroundTextureSelector: React.FC<BackgroundTextureSelectorProps> = ({
predefinedTextures,
customTextures,
selectedTextureId,
backgroundOpacity,
backgroundSize,
onTextureSelect,
onOpacityChange,
onSizeChange,
onImportImage,
onDeleteTexture,
}) => {
const _ = useTranslation();
const iconSize24 = useResponsiveSize(24);
const allTextures = [...predefinedTextures, ...customTextures];
return (
<div>
<h2 className='mb-2 font-medium'>{_('Background Image')}</h2>
<div className='mb-4 grid grid-cols-2 gap-4'>
{allTextures.map((texture) => (
<button
key={texture.id}
onClick={() => onTextureSelect(texture.id)}
className={`bg-base-100 relative flex flex-col items-center justify-center rounded-lg border-2 p-4 shadow-md transition-all ${
selectedTextureId === texture.id
? 'ring-2 ring-indigo-500 ring-offset-2'
: 'border-base-300'
}`}
style={{
backgroundImage: texture.loaded ? `url("${texture.blobUrl || texture.url}")` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'top',
minHeight: '80px',
}}
>
{selectedTextureId === texture.id && (
<MdRadioButtonChecked
size={iconSize24}
className='absolute right-2 top-2 rounded-full bg-white text-indigo-500'
/>
)}
{!predefinedTextures.find((t) => t.id === texture.id) && (
<button
onClick={(e) => {
e.stopPropagation();
onDeleteTexture(texture.id);
}}
className='absolute left-2 top-2 rounded-full bg-red-500 p-1 text-white transition-colors hover:bg-red-600'
title={_('Delete')}
>
<MdClose size={16} />
</button>
)}
</button>
))}
{/* Custom Image Upload */}
<div
className='border-base-300 relative flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed p-4 shadow-md transition-all'
style={{ minHeight: '80px' }}
>
<button
className='card-body flex cursor-pointer items-center justify-center p-2 text-center'
onClick={onImportImage}
>
<div className='flex items-center gap-2'>
<div className='flex items-center justify-center'>
<MdAdd className='text-primary/85 group-hover:text-primary h-6 w-6' />
</div>
<div className='text-primary/85 group-hover:text-primary line-clamp-1 font-medium'>
{_('Import Image')}
</div>
</div>
</button>
</div>
</div>
{/* Background Image Settings */}
{selectedTextureId !== 'none' && (
<div className='card border-base-200 bg-base-100 space-y-4 border p-4 shadow'>
<div className='flex items-center justify-between'>
<span className='text-sm font-medium'>{_('Opacity')}</span>
<div className='flex items-center gap-2'>
<input
type='range'
min='0'
max='1'
step='0.05'
value={backgroundOpacity}
onChange={(e) => onOpacityChange(parseFloat(e.target.value))}
className='range range-sm w-32'
/>
<span className='w-12 text-right text-sm'>
{Math.round(backgroundOpacity * 100)}%
</span>
</div>
</div>
<div className='flex items-center justify-between'>
<span className='text-sm font-medium'>{_('Size')}</span>
<Select
value={backgroundSize}
onChange={(e) => onSizeChange(e.target.value)}
options={[
{ value: 'auto', label: _('Auto') },
{ value: 'cover', label: _('Cover') },
{ value: 'contain', label: _('Contain') },
]}
/>
</div>
</div>
)}
</div>
);
};
export default BackgroundTextureSelector;
@@ -0,0 +1,54 @@
import React from 'react';
import { CODE_LANGUAGES, CodeLanguage } from '@/utils/highlightjs';
import { useTranslation } from '@/hooks/useTranslation';
import Select from '@/components/Select';
interface CodeHighlightingSettingsProps {
codeHighlighting: boolean;
codeLanguage: string;
onToggle: (enabled: boolean) => void;
onLanguageChange: (language: CodeLanguage) => void;
}
const CodeHighlightingSettings: React.FC<CodeHighlightingSettingsProps> = ({
codeHighlighting,
codeLanguage,
onToggle,
onLanguageChange,
}) => {
const _ = useTranslation();
return (
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('Code Highlighting')}</h2>
<div className='card border-base-200 bg-base-100 border shadow'>
<div className='divide-base-200'>
<div className='config-item'>
<span>{_('Enable Highlighting')}</span>
<input
type='checkbox'
className='toggle'
checked={codeHighlighting}
onChange={() => onToggle(!codeHighlighting)}
/>
</div>
<div className='config-item'>
<span>{_('Code Language')}</span>
<Select
value={codeLanguage}
onChange={(event) => onLanguageChange(event.target.value as CodeLanguage)}
options={CODE_LANGUAGES.map((lang) => ({
value: lang,
label: lang === 'auto-detect' ? _('Auto') : lang,
}))}
disabled={!codeHighlighting}
/>
</div>
</div>
</div>
</div>
);
};
export default CodeHighlightingSettings;
@@ -9,7 +9,13 @@ type ColorInputProps = {
pickerPosition?: 'left' | 'center' | 'right';
};
const ColorInput: React.FC<ColorInputProps> = ({ label, value, onChange, compact = false, pickerPosition = 'left' }) => {
const ColorInput: React.FC<ColorInputProps> = ({
label,
value,
onChange,
compact = false,
pickerPosition = 'left',
}) => {
const [isOpen, setIsOpen] = useState(false);
const pickerRef = useRef<HTMLDivElement>(null);
@@ -53,9 +59,9 @@ const ColorInput: React.FC<ColorInputProps> = ({ label, value, onChange, compact
/>
{isOpen && (
<div
ref={pickerRef}
className={`absolute top-full z-50 mt-1 ${getPickerPositionClass()}`}
<div
ref={pickerRef}
className={`absolute top-full z-50 py-1 ${getPickerPositionClass()}`}
>
<SketchPicker
width='200px'
@@ -0,0 +1,54 @@
import React from 'react';
import { HighlightColor } from '@/types/book';
import { useTranslation } from '@/hooks/useTranslation';
import ColorInput from './ColorInput';
interface HighlightColorsEditorProps {
customHighlightColors: Record<HighlightColor, string>;
onChange: (colors: Record<HighlightColor, string>) => void;
}
const HighlightColorsEditor: React.FC<HighlightColorsEditorProps> = ({
customHighlightColors,
onChange,
}) => {
const _ = useTranslation();
const handleColorChange = (color: HighlightColor, value: string) => {
const updated = { ...customHighlightColors, [color]: value };
onChange(updated);
};
return (
<div>
<h2 className='mb-2 font-medium'>{_('Highlight Colors')}</h2>
<div className='card border-base-200 bg-base-100 overflow-visible border p-4 shadow'>
<div className='grid grid-cols-3 gap-3 sm:grid-cols-5'>
{(['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[]).map(
(color, index, array) => {
const position =
index === 0 ? 'left' : index === array.length - 1 ? 'right' : 'center';
return (
<div key={color} className='flex flex-col items-center gap-2'>
<div
className='border-base-300 h-8 w-8 rounded-full border-2 shadow-sm'
style={{ backgroundColor: customHighlightColors[color] }}
/>
<ColorInput
label=''
value={customHighlightColors[color]}
compact={true}
pickerPosition={position}
onChange={(value: string) => handleColorChange(color, value)}
/>
</div>
);
},
)}
</div>
</div>
</div>
);
};
export default HighlightColorsEditor;
@@ -0,0 +1,147 @@
import React from 'react';
import { MdClose } from 'react-icons/md';
import { useTranslation } from '@/hooks/useTranslation';
import Select from '@/components/Select';
import ColorInput from './ColorInput';
export type TTSHighlightStyle =
| 'highlight'
| 'underline'
| 'strikethrough'
| 'squiggly'
| 'outline';
interface TTSHighlightStyleEditorProps {
style: TTSHighlightStyle;
color: string;
customColors: string[];
onStyleChange: (style: TTSHighlightStyle) => void;
onColorChange: (color: string) => void;
onCustomColorsChange: (colors: string[]) => void;
}
const TTSHighlightStyleEditor: React.FC<TTSHighlightStyleEditorProps> = ({
style,
color,
customColors,
onStyleChange,
onColorChange,
onCustomColorsChange,
}) => {
const _ = useTranslation();
const defaultQuickColors = [
{ color: '#FFD700', label: 'Gold' },
{ color: '#00CED1', label: 'Cyan' },
{ color: '#FF69B4', label: 'Pink' },
{ color: '#90EE90', label: 'Green' },
{ color: '#FFA500', label: 'Orange' },
];
const handleAddCustomColor = () => {
if (!customColors.includes(color) && !defaultQuickColors.some((c) => c.color === color)) {
const updatedColors = [...customColors, color];
onCustomColorsChange(updatedColors);
}
};
const handleDeleteCustomColor = (colorToDelete: string) => {
const updatedColors = customColors.filter((c) => c !== colorToDelete);
onCustomColorsChange(updatedColors);
};
return (
<div className='w-full'>
<h2 className='mb-2 font-medium'>{_('TTS Highlighting')}</h2>
<div className='card border-base-200 bg-base-100 border p-4 shadow'>
<div className='space-y-4'>
<div className='flex items-center justify-between'>
<span className='text-sm font-medium'>{_('Style')}</span>
<Select
value={style}
onChange={(e) => onStyleChange(e.target.value as TTSHighlightStyle)}
options={[
{ value: 'highlight', label: _('Highlight') },
{ value: 'underline', label: _('Underline') },
{ value: 'strikethrough', label: _('Strikethrough') },
{ value: 'squiggly', label: _('Squiggly') },
{ value: 'outline', label: _('Outline') },
]}
/>
</div>
<div className='flex items-center justify-between'>
<span className='text-sm font-medium'>{_('Color')}</span>
<div className='flex items-center gap-2'>
<div
className='border-base-300 pointer-events-none h-8 w-8 cursor-pointer rounded-full border-2 shadow-sm'
style={{ backgroundColor: color }}
/>
<ColorInput
label=''
value={color}
compact={true}
pickerPosition='center'
onChange={onColorChange}
/>
<button
onClick={handleAddCustomColor}
disabled={
customColors.includes(color) || defaultQuickColors.some((c) => c.color === color)
}
className='btn btn-ghost btn-sm gap-1 bg-transparent disabled:bg-transparent'
title={_('Save Current Color')}
>
<span className='text-xs'>{_('Save')}</span>
</button>
</div>
</div>
<div className='border-base-200 flex justify-between border-t pt-4'>
<div className='flex items-center'>
<span className='text-sm font-medium'>{_('Quick Colors')}</span>
</div>
<div className='flex max-w-[70%] flex-wrap justify-end gap-2'>
{defaultQuickColors.map(({ color: quickColor }) => (
<button
key={quickColor}
onClick={() => onColorChange(quickColor)}
className={`h-8 w-8 rounded-full border-2 transition-all hover:scale-110 ${
color === quickColor
? 'ring-2 ring-indigo-500 ring-offset-2'
: 'border-base-300'
}`}
style={{ backgroundColor: quickColor }}
/>
))}
{customColors.map((customColor) => (
<div key={customColor} className='group relative'>
<button
onClick={() => onColorChange(customColor)}
className={`h-8 w-8 rounded-full border-2 transition-all hover:scale-110 ${
color === customColor
? 'ring-2 ring-indigo-500 ring-offset-2'
: 'border-base-300'
}`}
style={{ backgroundColor: customColor }}
/>
<button
onClick={() => handleDeleteCustomColor(customColor)}
className='absolute -right-1 -top-1 rounded-full bg-red-500 p-0.5 text-white opacity-0 transition-opacity hover:opacity-100 group-hover:opacity-100'
title={_('Delete')}
>
<MdClose size={12} />
</button>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
};
export default TTSHighlightStyleEditor;
@@ -0,0 +1,87 @@
import React from 'react';
import { CgColorPicker } from 'react-icons/cg';
import { MdRadioButtonUnchecked, MdRadioButtonChecked } from 'react-icons/md';
import { PiPlus } from 'react-icons/pi';
import { Theme } from '@/styles/themes';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
interface ThemeColorSelectorProps {
themes: Theme[];
themeColor: string;
isDarkMode: boolean;
onThemeColorChange: (name: string) => void;
onEditTheme: (name: string) => void;
onCreateTheme: () => void;
}
const ThemeColorSelector: React.FC<ThemeColorSelectorProps> = ({
themes,
themeColor,
isDarkMode,
onThemeColorChange,
onEditTheme,
onCreateTheme,
}) => {
const _ = useTranslation();
const iconSize16 = useResponsiveSize(16);
const iconSize24 = useResponsiveSize(24);
return (
<div>
<h2 className='mb-2 font-medium'>{_('Theme Color')}</h2>
<div className='grid grid-cols-3 gap-4'>
{themes.map(({ name, label, colors, isCustomizale }) => (
<button
key={name}
tabIndex={0}
onClick={() => onThemeColorChange(name)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onThemeColorChange(name);
}
e.stopPropagation();
}}
className={`relative flex cursor-pointer flex-col items-center justify-center rounded-lg p-4 shadow-md ${
themeColor === name ? 'ring-2 ring-indigo-500 ring-offset-2' : ''
}`}
style={{
backgroundColor: isDarkMode ? colors.dark['base-100'] : colors.light['base-100'],
color: isDarkMode ? colors.dark['base-content'] : colors.light['base-content'],
}}
>
<input
aria-label={_(label)}
type='radio'
name='theme'
value={name}
checked={themeColor === name}
onChange={() => onThemeColorChange(name)}
className='hidden'
/>
{themeColor === name ? (
<MdRadioButtonChecked size={iconSize24} />
) : (
<MdRadioButtonUnchecked size={iconSize24} />
)}
<span>{_(label)}</span>
{isCustomizale && themeColor === name && (
<button onClick={() => onEditTheme(name)}>
<CgColorPicker size={iconSize16} className='absolute right-2 top-2' />
</button>
)}
</button>
))}
<button
className='relative flex cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed p-4 shadow-md'
onClick={onCreateTheme}
>
<PiPlus size={iconSize24} />
<span>{_('Custom')}</span>
</button>
</div>
</div>
);
};
export default ThemeColorSelector;
@@ -0,0 +1,44 @@
import React from 'react';
import { MdOutlineLightMode, MdOutlineDarkMode } from 'react-icons/md';
import { TbSunMoon } from 'react-icons/tb';
import { useTranslation } from '@/hooks/useTranslation';
interface ThemeModeSelectorProps {
themeMode: 'auto' | 'light' | 'dark';
onThemeModeChange: (mode: 'auto' | 'light' | 'dark') => void;
}
const ThemeModeSelector: React.FC<ThemeModeSelectorProps> = ({ themeMode, onThemeModeChange }) => {
const _ = useTranslation();
return (
<div className='flex items-center justify-between'>
<h2 className='font-medium'>{_('Theme Mode')}</h2>
<div className='flex gap-4'>
<button
title={_('Auto Mode')}
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'auto' ? 'btn-active bg-base-300' : ''}`}
onClick={() => onThemeModeChange('auto')}
>
<TbSunMoon />
</button>
<button
title={_('Light Mode')}
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'light' ? 'btn-active bg-base-300' : ''}`}
onClick={() => onThemeModeChange('light')}
>
<MdOutlineLightMode />
</button>
<button
title={_('Dark Mode')}
className={`btn btn-ghost btn-circle btn-sm ${themeMode === 'dark' ? 'btn-active bg-base-300' : ''}`}
onClick={() => onThemeModeChange('dark')}
>
<MdOutlineDarkMode />
</button>
</div>
</div>
);
};
export default ThemeModeSelector;
@@ -104,6 +104,7 @@ export const DEFAULT_READSETTINGS: ReadSettings = {
squiggly: 'blue',
},
customHighlightColors: HIGHLIGHT_COLOR_HEX,
customTtsHighlightColors: [],
};
export const DEFAULT_MOBILE_READSETTINGS: Partial<ReadSettings> = {
@@ -219,6 +220,7 @@ export const DEFAULT_TTS_CONFIG: TTSConfig = {
ttsVoice: '',
ttsLocation: '',
showTTSBar: false,
ttsHighlightOptions: { style: 'highlight', color: '#808080' },
};
export const DEFAULT_TRANSLATOR_CONFIG: TranslatorConfig = {
@@ -41,6 +41,8 @@ export class TTSController extends EventTarget {
ttsNativeVoices: TTSVoice[] = [];
ttsTargetLang: string = '';
options: TTSHighlightOptions = { style: 'highlight', color: 'gray' };
constructor(appService: AppService | null, view: FoliateView) {
super();
this.ttsWebClient = new WebSpeechClient(this);
@@ -80,10 +82,10 @@ export class TTSController extends EventTarget {
this.ttsEdgeVoices = await this.ttsEdgeClient.getAllVoices();
}
#getHighlighter(options: TTSHighlightOptions) {
#getHighlighter() {
return (range: Range) => {
const { overlayer } = this.view.renderer.getContents()[0] as { overlayer: Overlayer };
const { style, color } = options;
const { style, color } = this.options;
overlayer?.remove(HIGHLIGHT_KEY);
overlayer?.add(HIGHLIGHT_KEY, range, Overlayer[style], { color });
};
@@ -94,20 +96,23 @@ export class TTSController extends EventTarget {
overlayer?.remove(HIGHLIGHT_KEY);
}
async initViewTTS() {
async initViewTTS(options?: TTSHighlightOptions) {
if (options) {
this.options.style = options.style;
this.options.color = options.color;
}
let granularity: TTSGranularity = this.view.language.isCJK ? 'sentence' : 'word';
const supportedGranularities = this.ttsClient.getGranularities();
if (!supportedGranularities.includes(granularity)) {
granularity = supportedGranularities[0]!;
}
const highlightOptions: TTSHighlightOptions = { style: 'highlight', color: 'gray' };
await this.view.initTTS(
granularity,
createRejectFilter({
tags: ['rt', 'sup'],
contents: [{ tag: 'a', content: /^\d+$/ }],
}),
this.#getHighlighter(highlightOptions),
this.#getHighlighter(),
);
}
+1 -1
View File
@@ -1,7 +1,7 @@
export type TTSGranularity = 'sentence' | 'word';
export type TTSHighlightOptions = {
style: 'highlight' | 'underline' | 'squiggly' | 'outline';
style: 'highlight' | 'underline' | 'strikethrough' | 'squiggly' | 'outline';
color: string;
};
+2
View File
@@ -1,4 +1,5 @@
import { BookMetadata } from '@/libs/document';
import { TTSHighlightOptions } from '@/services/tts/types';
export type BookFormat = 'EPUB' | 'PDF' | 'MOBI' | 'AZW' | 'AZW3' | 'CBZ' | 'FB2' | 'FBZ';
export type BookNoteType = 'bookmark' | 'annotation' | 'excerpt';
@@ -175,6 +176,7 @@ export interface TTSConfig {
ttsVoice: string;
ttsLocation: string;
showTTSBar: boolean;
ttsHighlightOptions: TTSHighlightOptions;
}
export interface TranslatorConfig {
+1
View File
@@ -23,6 +23,7 @@ export interface ReadSettings {
highlightStyle: HighlightStyle;
highlightStyles: Record<HighlightStyle, HighlightColor>;
customHighlightColors: Record<HighlightColor, string>;
customTtsHighlightColors: string[];
customThemes: CustomTheme[];
}