feat: add options to customize the style of highlighted text of current sentence of TTS (#2358)
This commit is contained in:
@@ -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;
|
||||
+10
-4
@@ -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,7 +1,7 @@
|
||||
export type TTSGranularity = 'sentence' | 'word';
|
||||
|
||||
export type TTSHighlightOptions = {
|
||||
style: 'highlight' | 'underline' | 'squiggly' | 'outline';
|
||||
style: 'highlight' | 'underline' | 'strikethrough' | 'squiggly' | 'outline';
|
||||
color: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface ReadSettings {
|
||||
highlightStyle: HighlightStyle;
|
||||
highlightStyles: Record<HighlightStyle, HighlightColor>;
|
||||
customHighlightColors: Record<HighlightColor, string>;
|
||||
customTtsHighlightColors: string[];
|
||||
customThemes: CustomTheme[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user