From b71b246601551f10bc91293141ca2f002e82c205 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Wed, 1 Apr 2026 12:35:26 +0800 Subject: [PATCH] feat(settings): add TTS settings tab and highlight opacity, closes #3661 (#3712) Add a new TTS tab in settings with media metadata update frequency control (sentence/paragraph/chapter) to reduce Bluetooth notification spam, and move TTS highlight settings from Color tab to the new TTS tab. Also add a highlight opacity setting with live preview in the Color tab. Co-authored-by: Claude Opus 4.6 (1M context) --- .../public/locales/ar/translation.json | 9 +- .../public/locales/bn/translation.json | 9 +- .../public/locales/bo/translation.json | 9 +- .../public/locales/de/translation.json | 9 +- .../public/locales/el/translation.json | 9 +- .../public/locales/es/translation.json | 9 +- .../public/locales/fa/translation.json | 9 +- .../public/locales/fr/translation.json | 9 +- .../public/locales/he/translation.json | 9 +- .../public/locales/hi/translation.json | 9 +- .../public/locales/id/translation.json | 9 +- .../public/locales/it/translation.json | 9 +- .../public/locales/ja/translation.json | 9 +- .../public/locales/ko/translation.json | 9 +- .../public/locales/ms/translation.json | 9 +- .../public/locales/nl/translation.json | 9 +- .../public/locales/pl/translation.json | 9 +- .../public/locales/pt/translation.json | 9 +- .../public/locales/ru/translation.json | 9 +- .../public/locales/si/translation.json | 9 +- .../public/locales/sl/translation.json | 9 +- .../public/locales/sv/translation.json | 9 +- .../public/locales/ta/translation.json | 9 +- .../public/locales/th/translation.json | 9 +- .../public/locales/tr/translation.json | 9 +- .../public/locales/uk/translation.json | 9 +- .../public/locales/vi/translation.json | 9 +- .../public/locales/zh-CN/translation.json | 9 +- .../public/locales/zh-TW/translation.json | 9 +- .../src/__tests__/utils/tts-metadata.test.ts | 135 +++++++++++++ .../src/app/reader/hooks/useTTSControl.ts | 33 +++- .../app/reader/hooks/useTTSMediaSession.ts | 19 +- .../src/components/settings/ColorPanel.tsx | 54 ++---- .../components/settings/SettingsDialog.tsx | 18 +- .../src/components/settings/TTSPanel.tsx | 116 +++++++++++ .../settings/color/HighlightColorsEditor.tsx | 182 ++++++++++-------- apps/readest-app/src/hooks/useTheme.ts | 7 +- .../src/services/commandRegistry.ts | 11 +- apps/readest-app/src/services/constants.ts | 2 + apps/readest-app/src/services/tts/types.ts | 2 + apps/readest-app/src/types/book.ts | 3 + apps/readest-app/src/utils/ttsMetadata.ts | 53 +++++ 42 files changed, 729 insertions(+), 167 deletions(-) create mode 100644 apps/readest-app/src/__tests__/utils/tts-metadata.test.ts create mode 100644 apps/readest-app/src/components/settings/TTSPanel.tsx create mode 100644 apps/readest-app/src/utils/ttsMetadata.ts diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 791094b7..e0ce1a6a 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -1196,5 +1196,12 @@ "Text to Speech": "تحويل النص إلى كلام", "Zoom": "تكبير", "Window": "نافذة", - "Your Bookshelf": "رف الكتب الخاص بك" + "Your Bookshelf": "رف الكتب الخاص بك", + "TTS": "تحويل النص إلى كلام", + "Media Info": "معلومات الوسائط", + "Update Frequency": "تردد التحديث", + "Every Sentence": "كل جملة", + "Every Paragraph": "كل فقرة", + "Every Chapter": "كل فصل", + "TTS Media Info Update Frequency": "تردد تحديث معلومات وسائط TTS" } diff --git a/apps/readest-app/public/locales/bn/translation.json b/apps/readest-app/public/locales/bn/translation.json index 7610dfad..080b967e 100644 --- a/apps/readest-app/public/locales/bn/translation.json +++ b/apps/readest-app/public/locales/bn/translation.json @@ -1144,5 +1144,12 @@ "Text to Speech": "পাঠ থেকে বাক্য", "Zoom": "জুম", "Window": "উইন্ডো", - "Your Bookshelf": "আপনার বইয়ের তাক" + "Your Bookshelf": "আপনার বইয়ের তাক", + "TTS": "টেক্সট টু স্পিচ", + "Media Info": "মিডিয়া তথ্য", + "Update Frequency": "আপডেটের ফ্রিকোয়েন্সি", + "Every Sentence": "প্রতিটি বাক্য", + "Every Paragraph": "প্রতিটি অনুচ্ছেদ", + "Every Chapter": "প্রতিটি অধ্যায়", + "TTS Media Info Update Frequency": "TTS মিডিয়া তথ্য আপডেট ফ্রিকোয়েন্সি" } diff --git a/apps/readest-app/public/locales/bo/translation.json b/apps/readest-app/public/locales/bo/translation.json index a2ab8676..50fcd46a 100644 --- a/apps/readest-app/public/locales/bo/translation.json +++ b/apps/readest-app/public/locales/bo/translation.json @@ -1131,5 +1131,12 @@ "Text to Speech": "ཡི་གེ་ནས་སྒྲ།", "Zoom": "ཆེ་རུ་གཏོང་།", "Window": "སྒེའུ་ཁུང་།", - "Your Bookshelf": "ཁྱོད་ཀྱི་དཔེ་མཛོད།" + "Your Bookshelf": "ཁྱོད་ཀྱི་དཔེ་མཛོད།", + "TTS": "ཡི་གེ་ནས་སྒྲ", + "Media Info": "སྨྱན་བྱད་ཆ་འཕྲིན", + "Update Frequency": "གསར་སྒྱུར་ཐེངས་གྲངས", + "Every Sentence": "ཚིག་གྲུབ་རེ་རེ", + "Every Paragraph": "དུམ་མཚམས་རེ་རེ", + "Every Chapter": "ལེའུ་རེ་རེ", + "TTS Media Info Update Frequency": "TTS སྨྱན་བྱད་ཆ་འཕྲིན་གསར་སྒྱུར་ཐེངས་གྲངས" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index f7adadd1..3c9357e6 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -1144,5 +1144,12 @@ "Text to Speech": "Sprachausgabe", "Zoom": "Zoom", "Window": "Fenster", - "Your Bookshelf": "Dein Bücherregal" + "Your Bookshelf": "Dein Bücherregal", + "TTS": "Sprachausgabe", + "Media Info": "Medieninfo", + "Update Frequency": "Aktualisierungshäufigkeit", + "Every Sentence": "Jeden Satz", + "Every Paragraph": "Jeden Absatz", + "Every Chapter": "Jedes Kapitel", + "TTS Media Info Update Frequency": "TTS-Medieninfo-Aktualisierungshäufigkeit" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 25a908a3..cb907501 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -1144,5 +1144,12 @@ "Text to Speech": "Κείμενο σε ομιλία", "Zoom": "Ζουμ", "Window": "Παράθυρο", - "Your Bookshelf": "Η βιβλιοθήκη σου" + "Your Bookshelf": "Η βιβλιοθήκη σου", + "TTS": "Εκφώνηση", + "Media Info": "Πληροφορίες μέσων", + "Update Frequency": "Συχνότητα ενημέρωσης", + "Every Sentence": "Κάθε πρόταση", + "Every Paragraph": "Κάθε παράγραφο", + "Every Chapter": "Κάθε κεφάλαιο", + "TTS Media Info Update Frequency": "Συχνότητα ενημέρωσης πληροφοριών μέσων TTS" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index b6ad02ee..e803e062 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -1157,5 +1157,12 @@ "Text to Speech": "Texto a voz", "Zoom": "Zoom", "Window": "Ventana", - "Your Bookshelf": "Tu estantería" + "Your Bookshelf": "Tu estantería", + "TTS": "Lectura en voz alta", + "Media Info": "Info multimedia", + "Update Frequency": "Frecuencia de actualización", + "Every Sentence": "Cada oración", + "Every Paragraph": "Cada párrafo", + "Every Chapter": "Cada capítulo", + "TTS Media Info Update Frequency": "Frecuencia de actualización de info multimedia TTS" } diff --git a/apps/readest-app/public/locales/fa/translation.json b/apps/readest-app/public/locales/fa/translation.json index 99c73174..501668c0 100644 --- a/apps/readest-app/public/locales/fa/translation.json +++ b/apps/readest-app/public/locales/fa/translation.json @@ -1144,5 +1144,12 @@ "Text to Speech": "تبدیل متن به گفتار", "Zoom": "بزرگ‌نمایی", "Window": "پنجره", - "Your Bookshelf": "قفسه کتاب شما" + "Your Bookshelf": "قفسه کتاب شما", + "TTS": "تبدیل متن به گفتار", + "Media Info": "اطلاعات رسانه", + "Update Frequency": "تناوب به‌روزرسانی", + "Every Sentence": "هر جمله", + "Every Paragraph": "هر پاراگراف", + "Every Chapter": "هر فصل", + "TTS Media Info Update Frequency": "تناوب به‌روزرسانی اطلاعات رسانه TTS" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 0e5f495e..c9df7cd4 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -1157,5 +1157,12 @@ "Text to Speech": "Synthèse vocale", "Zoom": "Zoom", "Window": "Fenêtre", - "Your Bookshelf": "Votre bibliothèque" + "Your Bookshelf": "Votre bibliothèque", + "TTS": "Synthèse vocale", + "Media Info": "Infos média", + "Update Frequency": "Fréquence de mise à jour", + "Every Sentence": "Chaque phrase", + "Every Paragraph": "Chaque paragraphe", + "Every Chapter": "Chaque chapitre", + "TTS Media Info Update Frequency": "Fréquence de mise à jour des infos média TTS" } diff --git a/apps/readest-app/public/locales/he/translation.json b/apps/readest-app/public/locales/he/translation.json index 2910d91a..ec997b4f 100644 --- a/apps/readest-app/public/locales/he/translation.json +++ b/apps/readest-app/public/locales/he/translation.json @@ -1157,5 +1157,12 @@ "Text to Speech": "טקסט לדיבור", "Zoom": "זום", "Window": "חלון", - "Your Bookshelf": "מדף הספרים שלך" + "Your Bookshelf": "מדף הספרים שלך", + "TTS": "הקראה", + "Media Info": "פרטי מדיה", + "Update Frequency": "תדירות עדכון", + "Every Sentence": "כל משפט", + "Every Paragraph": "כל פסקה", + "Every Chapter": "כל פרק", + "TTS Media Info Update Frequency": "תדירות עדכון פרטי מדיה TTS" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 9a645831..65e70bef 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -1144,5 +1144,12 @@ "Text to Speech": "पाठ से वाक्", "Zoom": "ज़ूम", "Window": "विंडो", - "Your Bookshelf": "आपकी किताबों की अलमारी" + "Your Bookshelf": "आपकी किताबों की अलमारी", + "TTS": "पाठ से वाक्", + "Media Info": "मीडिया जानकारी", + "Update Frequency": "अपडेट आवृत्ति", + "Every Sentence": "हर वाक्य", + "Every Paragraph": "हर पैराग्राफ", + "Every Chapter": "हर अध्याय", + "TTS Media Info Update Frequency": "TTS मीडिया जानकारी अपडेट आवृत्ति" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index 53cfd129..bf5bc420 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -1131,5 +1131,12 @@ "Text to Speech": "Teks ke Suara", "Zoom": "Zoom", "Window": "Jendela", - "Your Bookshelf": "Rak Buku Anda" + "Your Bookshelf": "Rak Buku Anda", + "TTS": "Teks ke Suara", + "Media Info": "Info Media", + "Update Frequency": "Frekuensi Pembaruan", + "Every Sentence": "Setiap Kalimat", + "Every Paragraph": "Setiap Paragraf", + "Every Chapter": "Setiap Bab", + "TTS Media Info Update Frequency": "Frekuensi Pembaruan Info Media TTS" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index fecd714a..4409cf8d 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -1157,5 +1157,12 @@ "Text to Speech": "Sintesi vocale", "Zoom": "Zoom", "Window": "Finestra", - "Your Bookshelf": "La tua libreria" + "Your Bookshelf": "La tua libreria", + "TTS": "Sintesi vocale", + "Media Info": "Info multimediali", + "Update Frequency": "Frequenza aggiornamento", + "Every Sentence": "Ogni frase", + "Every Paragraph": "Ogni paragrafo", + "Every Chapter": "Ogni capitolo", + "TTS Media Info Update Frequency": "Frequenza aggiornamento info multimediali TTS" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 21a7b0aa..d5a882cd 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -1131,5 +1131,12 @@ "Text to Speech": "テキスト読み上げ", "Zoom": "ズーム", "Window": "ウィンドウ", - "Your Bookshelf": "あなたの本棚" + "Your Bookshelf": "あなたの本棚", + "TTS": "読み上げ", + "Media Info": "メディア情報", + "Update Frequency": "更新頻度", + "Every Sentence": "文ごと", + "Every Paragraph": "段落ごと", + "Every Chapter": "章ごと", + "TTS Media Info Update Frequency": "TTSメディア情報の更新頻度" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index ae522251..b3eb410d 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -1131,5 +1131,12 @@ "Text to Speech": "텍스트 음성 변환", "Zoom": "확대/축소", "Window": "창", - "Your Bookshelf": "내 책장" + "Your Bookshelf": "내 책장", + "TTS": "음성 읽기", + "Media Info": "미디어 정보", + "Update Frequency": "업데이트 빈도", + "Every Sentence": "문장마다", + "Every Paragraph": "단락마다", + "Every Chapter": "챕터마다", + "TTS Media Info Update Frequency": "TTS 미디어 정보 업데이트 빈도" } diff --git a/apps/readest-app/public/locales/ms/translation.json b/apps/readest-app/public/locales/ms/translation.json index d332a1c9..d2ffa377 100644 --- a/apps/readest-app/public/locales/ms/translation.json +++ b/apps/readest-app/public/locales/ms/translation.json @@ -1131,5 +1131,12 @@ "Text to Speech": "Teks ke Pertuturan", "Zoom": "Zum", "Window": "Tetingkap", - "Your Bookshelf": "Rak Buku Anda" + "Your Bookshelf": "Rak Buku Anda", + "TTS": "Teks ke Suara", + "Media Info": "Info Media", + "Update Frequency": "Kekerapan Kemas Kini", + "Every Sentence": "Setiap Ayat", + "Every Paragraph": "Setiap Perenggan", + "Every Chapter": "Setiap Bab", + "TTS Media Info Update Frequency": "Kekerapan Kemas Kini Info Media TTS" } diff --git a/apps/readest-app/public/locales/nl/translation.json b/apps/readest-app/public/locales/nl/translation.json index d3721cbf..d0cffbd2 100644 --- a/apps/readest-app/public/locales/nl/translation.json +++ b/apps/readest-app/public/locales/nl/translation.json @@ -1144,5 +1144,12 @@ "Text to Speech": "Tekst naar spraak", "Zoom": "Zoom", "Window": "Venster", - "Your Bookshelf": "Jouw boekenkast" + "Your Bookshelf": "Jouw boekenkast", + "TTS": "Spraak", + "Media Info": "Media-info", + "Update Frequency": "Updatefrequentie", + "Every Sentence": "Elke zin", + "Every Paragraph": "Elke alinea", + "Every Chapter": "Elk hoofdstuk", + "TTS Media Info Update Frequency": "TTS media-info updatefrequentie" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index dced94f3..3fbb8102 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -1170,5 +1170,12 @@ "Text to Speech": "Tekst na mowę", "Zoom": "Powiększenie", "Window": "Okno", - "Your Bookshelf": "Twoja półka z książkami" + "Your Bookshelf": "Twoja półka z książkami", + "TTS": "Czytanie na głos", + "Media Info": "Informacje o mediach", + "Update Frequency": "Częstotliwość aktualizacji", + "Every Sentence": "Co zdanie", + "Every Paragraph": "Co akapit", + "Every Chapter": "Co rozdział", + "TTS Media Info Update Frequency": "Częstotliwość aktualizacji informacji o mediach TTS" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index f2dbf266..92c216e5 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -1157,5 +1157,12 @@ "Text to Speech": "Texto para fala", "Zoom": "Zoom", "Window": "Janela", - "Your Bookshelf": "Sua estante" + "Your Bookshelf": "Sua estante", + "TTS": "Leitura em voz alta", + "Media Info": "Info de mídia", + "Update Frequency": "Frequência de atualização", + "Every Sentence": "A cada frase", + "Every Paragraph": "A cada parágrafo", + "Every Chapter": "A cada capítulo", + "TTS Media Info Update Frequency": "Frequência de atualização de info de mídia TTS" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 9d9af38a..f3ac4c42 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -1170,5 +1170,12 @@ "Text to Speech": "Озвучивание", "Zoom": "Масштаб", "Window": "Окно", - "Your Bookshelf": "Ваша книжная полка" + "Your Bookshelf": "Ваша книжная полка", + "TTS": "Озвучивание", + "Media Info": "Медиа-информация", + "Update Frequency": "Частота обновления", + "Every Sentence": "Каждое предложение", + "Every Paragraph": "Каждый абзац", + "Every Chapter": "Каждая глава", + "TTS Media Info Update Frequency": "Частота обновления медиа-информации TTS" } diff --git a/apps/readest-app/public/locales/si/translation.json b/apps/readest-app/public/locales/si/translation.json index a042d8d2..0076360b 100644 --- a/apps/readest-app/public/locales/si/translation.json +++ b/apps/readest-app/public/locales/si/translation.json @@ -1144,5 +1144,12 @@ "Text to Speech": "පෙළ කථනය", "Zoom": "විශාලනය", "Window": "කවුළුව", - "Your Bookshelf": "ඔබේ පොත් රාක්කය" + "Your Bookshelf": "ඔබේ පොත් රාක්කය", + "TTS": "කියවීම", + "Media Info": "මාධ්‍ය තොරතුරු", + "Update Frequency": "යාවත්කාලීන සංඛ්‍යාතය", + "Every Sentence": "සෑම වාක්‍යයක්ම", + "Every Paragraph": "සෑම ඡේදයක්ම", + "Every Chapter": "සෑම පරිච්ඡේදයක්ම", + "TTS Media Info Update Frequency": "TTS මාධ්‍ය තොරතුරු යාවත්කාලීන සංඛ්‍යාතය" } diff --git a/apps/readest-app/public/locales/sl/translation.json b/apps/readest-app/public/locales/sl/translation.json index a9f15d98..a019879a 100644 --- a/apps/readest-app/public/locales/sl/translation.json +++ b/apps/readest-app/public/locales/sl/translation.json @@ -1170,5 +1170,12 @@ "Text to Speech": "Besedilo v govor", "Zoom": "Povečava", "Window": "Okno", - "Your Bookshelf": "Vaša knjižna polica" + "Your Bookshelf": "Vaša knjižna polica", + "TTS": "Branje na glas", + "Media Info": "Podatki o medijih", + "Update Frequency": "Pogostost posodabljanja", + "Every Sentence": "Vsak stavek", + "Every Paragraph": "Vsak odstavek", + "Every Chapter": "Vsako poglavje", + "TTS Media Info Update Frequency": "Pogostost posodabljanja podatkov o medijih TTS" } diff --git a/apps/readest-app/public/locales/sv/translation.json b/apps/readest-app/public/locales/sv/translation.json index a4ea44cf..948748c5 100644 --- a/apps/readest-app/public/locales/sv/translation.json +++ b/apps/readest-app/public/locales/sv/translation.json @@ -1144,5 +1144,12 @@ "Text to Speech": "Text till tal", "Zoom": "Zoom", "Window": "Fönster", - "Your Bookshelf": "Din bokhylla" + "Your Bookshelf": "Din bokhylla", + "TTS": "Uppläsning", + "Media Info": "Medieinformation", + "Update Frequency": "Uppdateringsfrekvens", + "Every Sentence": "Varje mening", + "Every Paragraph": "Varje stycke", + "Every Chapter": "Varje kapitel", + "TTS Media Info Update Frequency": "Uppdateringsfrekvens för TTS-medieinformation" } diff --git a/apps/readest-app/public/locales/ta/translation.json b/apps/readest-app/public/locales/ta/translation.json index 72aafd2f..a487b053 100644 --- a/apps/readest-app/public/locales/ta/translation.json +++ b/apps/readest-app/public/locales/ta/translation.json @@ -1144,5 +1144,12 @@ "Text to Speech": "உரையிலிருந்து பேச்சு", "Zoom": "பெரிதாக்கு", "Window": "சாளரம்", - "Your Bookshelf": "உங்கள் புத்தக அலமாரி" + "Your Bookshelf": "உங்கள் புத்தக அலமாரி", + "TTS": "உரை வாசிப்பு", + "Media Info": "ஊடக தகவல்", + "Update Frequency": "புதுப்பிப்பு அதிர்வெண்", + "Every Sentence": "ஒவ்வொரு வாக்கியமும்", + "Every Paragraph": "ஒவ்வொரு பத்தியும்", + "Every Chapter": "ஒவ்வொரு அத்தியாயமும்", + "TTS Media Info Update Frequency": "TTS ஊடக தகவல் புதுப்பிப்பு அதிர்வெண்" } diff --git a/apps/readest-app/public/locales/th/translation.json b/apps/readest-app/public/locales/th/translation.json index c0c5a6c9..8da211b9 100644 --- a/apps/readest-app/public/locales/th/translation.json +++ b/apps/readest-app/public/locales/th/translation.json @@ -1131,5 +1131,12 @@ "Text to Speech": "อ่านออกเสียง", "Zoom": "ซูม", "Window": "หน้าต่าง", - "Your Bookshelf": "ชั้นหนังสือของคุณ" + "Your Bookshelf": "ชั้นหนังสือของคุณ", + "TTS": "อ่านออกเสียง", + "Media Info": "ข้อมูลสื่อ", + "Update Frequency": "ความถี่ในการอัปเดต", + "Every Sentence": "ทุกประโยค", + "Every Paragraph": "ทุกย่อหน้า", + "Every Chapter": "ทุกบท", + "TTS Media Info Update Frequency": "ความถี่ในการอัปเดตข้อมูลสื่อ TTS" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index ec4d79af..5d792de2 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -1144,5 +1144,12 @@ "Text to Speech": "Metinden konuşmaya", "Zoom": "Yakınlaştırma", "Window": "Pencere", - "Your Bookshelf": "Kitaplığınız" + "Your Bookshelf": "Kitaplığınız", + "TTS": "Sesli Okuma", + "Media Info": "Medya Bilgisi", + "Update Frequency": "Güncelleme Sıklığı", + "Every Sentence": "Her Cümle", + "Every Paragraph": "Her Paragraf", + "Every Chapter": "Her Bölüm", + "TTS Media Info Update Frequency": "TTS Medya Bilgisi Güncelleme Sıklığı" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index 6239e644..e2ab16e4 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -1170,5 +1170,12 @@ "Text to Speech": "Озвучення", "Zoom": "Масштаб", "Window": "Вікно", - "Your Bookshelf": "Ваша книжкова полиця" + "Your Bookshelf": "Ваша книжкова полиця", + "TTS": "Озвучування", + "Media Info": "Медіа-інформація", + "Update Frequency": "Частота оновлення", + "Every Sentence": "Кожне речення", + "Every Paragraph": "Кожен абзац", + "Every Chapter": "Кожен розділ", + "TTS Media Info Update Frequency": "Частота оновлення медіа-інформації TTS" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index b711d8ba..ede64115 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -1131,5 +1131,12 @@ "Text to Speech": "Chuyển văn bản thành giọng nói", "Zoom": "Thu phóng", "Window": "Cửa sổ", - "Your Bookshelf": "Tủ sách của bạn" + "Your Bookshelf": "Tủ sách của bạn", + "TTS": "Đọc văn bản", + "Media Info": "Thông tin phương tiện", + "Update Frequency": "Tần suất cập nhật", + "Every Sentence": "Mỗi câu", + "Every Paragraph": "Mỗi đoạn văn", + "Every Chapter": "Mỗi chương", + "TTS Media Info Update Frequency": "Tần suất cập nhật thông tin phương tiện TTS" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index e29eb1c9..e1af5abb 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -1131,5 +1131,12 @@ "Text to Speech": "文字转语音", "Zoom": "缩放", "Window": "窗口", - "Your Bookshelf": "你的书架" + "Your Bookshelf": "你的书架", + "TTS": "语音朗读", + "Media Info": "媒体信息", + "Update Frequency": "更新频率", + "Every Sentence": "每句", + "Every Paragraph": "每段", + "Every Chapter": "每章", + "TTS Media Info Update Frequency": "TTS 媒体信息更新频率" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 72e25b01..f8562704 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -1131,5 +1131,12 @@ "Text to Speech": "文字轉語音", "Zoom": "縮放", "Window": "視窗", - "Your Bookshelf": "你的書架" + "Your Bookshelf": "你的書架", + "TTS": "語音朗讀", + "Media Info": "媒體資訊", + "Update Frequency": "更新頻率", + "Every Sentence": "每句", + "Every Paragraph": "每段", + "Every Chapter": "每章", + "TTS Media Info Update Frequency": "TTS 媒體資訊更新頻率" } diff --git a/apps/readest-app/src/__tests__/utils/tts-metadata.test.ts b/apps/readest-app/src/__tests__/utils/tts-metadata.test.ts new file mode 100644 index 00000000..1aa68854 --- /dev/null +++ b/apps/readest-app/src/__tests__/utils/tts-metadata.test.ts @@ -0,0 +1,135 @@ +import { describe, it, expect } from 'vitest'; +import { buildTTSMediaMetadata } from '@/utils/ttsMetadata'; + +describe('buildTTSMediaMetadata', () => { + const baseOptions = { + markText: 'The quick brown fox jumps over the lazy dog.', + markName: '0', + sectionLabel: 'Chapter 1: Introduction', + title: 'Alice in Wonderland', + author: 'Lewis Carroll', + }; + + describe('sentence mode (default)', () => { + it('uses mark text as title', () => { + const result = buildTTSMediaMetadata({ ...baseOptions, ttsMediaMetadata: 'sentence' }); + expect(result.title).toBe(baseOptions.markText); + }); + + it('uses section label as artist', () => { + const result = buildTTSMediaMetadata({ ...baseOptions, ttsMediaMetadata: 'sentence' }); + expect(result.artist).toBe(baseOptions.sectionLabel); + }); + + it('uses author as album', () => { + const result = buildTTSMediaMetadata({ ...baseOptions, ttsMediaMetadata: 'sentence' }); + expect(result.album).toBe(baseOptions.author); + }); + + it('falls back to book title when section label is empty', () => { + const result = buildTTSMediaMetadata({ + ...baseOptions, + sectionLabel: '', + ttsMediaMetadata: 'sentence', + }); + expect(result.artist).toBe(baseOptions.title); + }); + + it('always updates regardless of mark name', () => { + expect( + buildTTSMediaMetadata({ ...baseOptions, markName: '0', ttsMediaMetadata: 'sentence' }) + .shouldUpdate, + ).toBe(true); + expect( + buildTTSMediaMetadata({ ...baseOptions, markName: '3', ttsMediaMetadata: 'sentence' }) + .shouldUpdate, + ).toBe(true); + }); + }); + + describe('paragraph mode', () => { + it('uses same mapping as sentence mode', () => { + const sentence = buildTTSMediaMetadata({ ...baseOptions, ttsMediaMetadata: 'sentence' }); + const paragraph = buildTTSMediaMetadata({ + ...baseOptions, + markName: '0', + ttsMediaMetadata: 'paragraph', + }); + expect(paragraph.title).toBe(sentence.title); + expect(paragraph.artist).toBe(sentence.artist); + expect(paragraph.album).toBe(sentence.album); + }); + + it('updates on first sentence of a block (markName "0")', () => { + const result = buildTTSMediaMetadata({ + ...baseOptions, + markName: '0', + ttsMediaMetadata: 'paragraph', + }); + expect(result.shouldUpdate).toBe(true); + }); + + it('skips update for subsequent sentences in the same block', () => { + expect( + buildTTSMediaMetadata({ ...baseOptions, markName: '1', ttsMediaMetadata: 'paragraph' }) + .shouldUpdate, + ).toBe(false); + expect( + buildTTSMediaMetadata({ ...baseOptions, markName: '2', ttsMediaMetadata: 'paragraph' }) + .shouldUpdate, + ).toBe(false); + }); + }); + + describe('chapter mode', () => { + it('uses section label as title', () => { + const result = buildTTSMediaMetadata({ ...baseOptions, ttsMediaMetadata: 'chapter' }); + expect(result.title).toBe(baseOptions.sectionLabel); + }); + + it('uses author as artist', () => { + const result = buildTTSMediaMetadata({ ...baseOptions, ttsMediaMetadata: 'chapter' }); + expect(result.artist).toBe(baseOptions.author); + }); + + it('uses book title as album', () => { + const result = buildTTSMediaMetadata({ ...baseOptions, ttsMediaMetadata: 'chapter' }); + expect(result.album).toBe(baseOptions.title); + }); + + it('falls back to book title when section label is empty', () => { + const result = buildTTSMediaMetadata({ + ...baseOptions, + sectionLabel: '', + ttsMediaMetadata: 'chapter', + }); + expect(result.title).toBe(baseOptions.title); + }); + + it('updates when section label differs from previous', () => { + const result = buildTTSMediaMetadata({ + ...baseOptions, + ttsMediaMetadata: 'chapter', + previousSectionLabel: 'Prologue', + }); + expect(result.shouldUpdate).toBe(true); + }); + + it('skips update when section label matches previous', () => { + const result = buildTTSMediaMetadata({ + ...baseOptions, + ttsMediaMetadata: 'chapter', + previousSectionLabel: 'Chapter 1: Introduction', + }); + expect(result.shouldUpdate).toBe(false); + }); + + it('updates when no previous section label', () => { + const result = buildTTSMediaMetadata({ + ...baseOptions, + ttsMediaMetadata: 'chapter', + }); + expect(result.shouldUpdate).toBe(true); + }); + }); +}); diff --git a/apps/readest-app/src/app/reader/hooks/useTTSControl.ts b/apps/readest-app/src/app/reader/hooks/useTTSControl.ts index b166685d..b213bc18 100644 --- a/apps/readest-app/src/app/reader/hooks/useTTSControl.ts +++ b/apps/readest-app/src/app/reader/hooks/useTTSControl.ts @@ -15,6 +15,7 @@ import { genSSMLRaw, parseSSMLLang } from '@/utils/ssml'; import { throttle } from '@/utils/throttle'; import { isCfiInLocation } from '@/utils/cfi'; import { getLocale } from '@/utils/misc'; +import { buildTTSMediaMetadata } from '@/utils/ttsMetadata'; import { invokeUseBackgroundAudio } from '@/utils/bridge'; import { estimateTTSTime } from '@/utils/ttsTime'; import { useTTSMediaSession } from './useTTSMediaSession'; @@ -47,6 +48,7 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp const followingTTSLocationRef = useRef(true); const sectionChangingTimestampRef = useRef(0); + const previousSectionLabelRef = useRef(undefined); const ttsControllerRef = useRef(null); const [ttsController, setTtsController] = useState(null); const [ttsClientsInited, setTtsClientsInitialized] = useState(false); @@ -134,23 +136,39 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp const handleSpeakMark = (e: Event) => { const progress = getProgress(bookKey); + const viewSettings = getViewSettings(bookKey); const { sectionLabel } = progress || {}; const mark = (e as CustomEvent).detail; + const ttsMediaMetadata = viewSettings?.ttsMediaMetadata ?? 'sentence'; - if (mediaSessionRef.current) { + const metadata = buildTTSMediaMetadata({ + markText: mark?.text || '', + markName: mark?.name || '', + sectionLabel: sectionLabel || '', + title, + author, + ttsMediaMetadata, + previousSectionLabel: previousSectionLabelRef.current, + }); + + if (ttsMediaMetadata === 'chapter') { + previousSectionLabelRef.current = sectionLabel; + } + + if (metadata.shouldUpdate && mediaSessionRef.current) { const mediaSession = mediaSessionRef.current; if (mediaSession instanceof TauriMediaSession) { mediaSession.updateMetadata({ - title: mark?.text || '', - artist: sectionLabel || title, - album: author, + title: metadata.title, + artist: metadata.artist, + album: metadata.album, artwork: '', }); } else { mediaSession.metadata = new MediaMetadata({ - title: mark?.text || '', - artist: sectionLabel || title, - album: author, + title: metadata.title, + artist: metadata.artist, + album: metadata.album, artwork: [{ src: coverImageUrl || '/icon.png', sizes: '512x512', type: 'image/png' }], }); } @@ -386,6 +404,7 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp setShowIndicator(false); setShowBackToCurrentTTSLocation(false); } + previousSectionLabelRef.current = undefined; if (appService?.isIOSApp) { await invokeUseBackgroundAudio({ enabled: false }); } diff --git a/apps/readest-app/src/app/reader/hooks/useTTSMediaSession.ts b/apps/readest-app/src/app/reader/hooks/useTTSMediaSession.ts index b059afd3..efd328c3 100644 --- a/apps/readest-app/src/app/reader/hooks/useTTSMediaSession.ts +++ b/apps/readest-app/src/app/reader/hooks/useTTSMediaSession.ts @@ -2,6 +2,7 @@ import { useRef } from 'react'; import { useSettingsStore } from '@/store/settingsStore'; import { useBookDataStore } from '@/store/bookDataStore'; import { useReaderStore } from '@/store/readerStore'; +import { buildTTSMediaMetadata } from '@/utils/ttsMetadata'; import { useTranslation } from '@/hooks/useTranslation'; import { SILENCE_DATA } from '@/services/tts'; import { getMediaSession, TauriMediaSession } from '@/libs/mediaSession'; @@ -15,7 +16,7 @@ export const useTTSMediaSession = ({ bookKey }: UseTTSMediaSessionProps) => { const _ = useTranslation(); const { settings } = useSettingsStore(); const { getBookData } = useBookDataStore(); - const { getProgress } = useReaderStore(); + const { getProgress, getViewSettings } = useReaderStore(); const mediaSessionRef = useRef(null); const unblockerAudioRef = useRef(null); @@ -60,9 +61,11 @@ export const useTTSMediaSession = ({ bookKey }: UseTTSMediaSessionProps) => { if (mediaSession instanceof TauriMediaSession) { const bookData = getBookData(bookKey); const progress = getProgress(bookKey); + const viewSettings = getViewSettings(bookKey); if (!bookData || !bookData.book) return; const { title, author, coverImageUrl } = bookData.book; const { sectionLabel } = progress || {}; + const ttsMediaMetadataMode = viewSettings?.ttsMediaMetadata ?? 'sentence'; let artworkImage = '/icon.png'; try { @@ -79,10 +82,18 @@ export const useTTSMediaSession = ({ bookKey }: UseTTSMediaSessionProps) => { foregroundServiceTitle: _('Read Aloud'), foregroundServiceText: _('Ready to read aloud'), }); + const metadata = buildTTSMediaMetadata({ + markText: title, + markName: '0', + sectionLabel: sectionLabel || '', + title, + author, + ttsMediaMetadata: ttsMediaMetadataMode, + }); mediaSession.updateMetadata({ - title: title, - artist: sectionLabel || title, - album: author, + title: metadata.title, + artist: metadata.artist, + album: metadata.album, artwork: artworkImage, }); } diff --git a/apps/readest-app/src/components/settings/ColorPanel.tsx b/apps/readest-app/src/components/settings/ColorPanel.tsx index e63b328e..72054ed1 100644 --- a/apps/readest-app/src/components/settings/ColorPanel.tsx +++ b/apps/readest-app/src/components/settings/ColorPanel.tsx @@ -25,7 +25,6 @@ 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'; import ReadingRulerSettings from './color/ReadingRulerSettings'; @@ -50,18 +49,10 @@ const ColorPanel: React.FC = ({ bookKey, onRegisterReset 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 [highlightOpacity, setHighlightOpacity] = useState(viewSettings.highlightOpacity ?? 0.3); const [customHighlightColors, setCustomHighlightColors] = useState( settings.globalReadSettings.customHighlightColors, ); - const [customTtsHighlightColors, setCustomTtsHighlightColors] = useState( - settings.globalReadSettings.customTtsHighlightColors || [], - ); const [userHighlightColors, setUserHighlightColors] = useState( settings.globalReadSettings.userHighlightColors || [], ); @@ -87,6 +78,7 @@ const ColorPanel: React.FC = ({ bookKey, onRegisterReset resetToDefaults({ overrideColor: setOverrideColor, invertImgColorInDark: setInvertImgColorInDark, + highlightOpacity: setHighlightOpacity, codeHighlighting: setcodeHighlighting, codeLanguage: setCodeLanguage, readingRulerEnabled: setReadingRulerEnabled, @@ -123,6 +115,12 @@ const ColorPanel: React.FC = ({ bookKey, onRegisterReset // eslint-disable-next-line react-hooks/exhaustive-deps }, [overrideColor]); + useEffect(() => { + if (highlightOpacity === viewSettings.highlightOpacity) return; + saveViewSettings(envConfig, bookKey, 'highlightOpacity', highlightOpacity); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [highlightOpacity]); + useEffect(() => { let update = false; if (codeHighlighting !== viewSettings.codeHighlighting) { @@ -263,29 +261,6 @@ const ColorPanel: React.FC = ({ bookKey, onRegisterReset 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); - }; - const handleUserHighlightColorsChange = (colors: string[]) => { setUserHighlightColors(colors); settings.globalReadSettings.userHighlightColors = colors; @@ -364,21 +339,14 @@ const ColorPanel: React.FC = ({ bookKey, onRegisterReset - - = ({ bookKey }) => { icon: RiTranslate, label: _('Language'), }, + { + tab: 'TTS', + icon: PiSpeakerHigh, + label: _('TTS'), + }, { tab: 'AI', icon: PiRobot, @@ -133,6 +140,7 @@ const SettingsDialog: React.FC<{ bookKey: string }> = ({ bookKey }) => { Layout: null, Color: null, Control: null, + TTS: null, Language: null, AI: null, Custom: null, @@ -165,6 +173,7 @@ const SettingsDialog: React.FC<{ bookKey: string }> = ({ bookKey }) => { layout: 'Layout', color: 'Color', control: 'Control', + tts: 'TTS', language: 'Language', ai: 'AI', custom: 'Custom', @@ -200,7 +209,7 @@ const SettingsDialog: React.FC<{ bookKey: string }> = ({ bookKey }) => { if (!container) return; const checkButtonWidths = () => { - const threshold = (container.clientWidth - 64) * 0.22; + const threshold = (container.clientWidth - 64) / tabConfig.filter((t) => !t.disabled).length; const hideLabel = Array.from(container.querySelectorAll('button')).some((button) => { const labelSpan = button.querySelector('span'); const labelText = labelSpan?.textContent || ''; @@ -373,6 +382,9 @@ const SettingsDialog: React.FC<{ bookKey: string }> = ({ bookKey }) => { onRegisterReset={(fn) => registerResetFunction('Control', fn)} /> )} + {activePanel === 'TTS' && ( + registerResetFunction('TTS', fn)} /> + )} {activePanel === 'Language' && ( = ({ bookKey, onRegisterReset }) => { + const _ = useTranslation(); + const { envConfig } = useEnv(); + const { getViewSettings } = useReaderStore(); + const { settings, setSettings, saveSettings } = useSettingsStore(); + const viewSettings = getViewSettings(bookKey) || settings.globalViewSettings; + + const [ttsMediaMetadata, setTtsMediaMetadata] = useState( + viewSettings.ttsMediaMetadata ?? 'sentence', + ); + const [ttsHighlightStyle, setTtsHighlightStyle] = useState( + viewSettings.ttsHighlightOptions.style, + ); + const [ttsHighlightColor, setTtsHighlightColor] = useState( + viewSettings.ttsHighlightOptions.color, + ); + const [customTtsHighlightColors, setCustomTtsHighlightColors] = useState( + settings.globalReadSettings.customTtsHighlightColors || [], + ); + + const resetToDefaults = useResetViewSettings(); + + const handleReset = () => { + resetToDefaults({ + ttsMediaMetadata: setTtsMediaMetadata as React.Dispatch>, + }); + }; + + useEffect(() => { + onRegisterReset(handleReset); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (ttsMediaMetadata === viewSettings.ttsMediaMetadata) return; + saveViewSettings(envConfig, bookKey, 'ttsMediaMetadata', ttsMediaMetadata, false, false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ttsMediaMetadata]); + + 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); + }; + + const handleMediaMetadataChange = (event: React.ChangeEvent) => { + setTtsMediaMetadata(event.target.value as TTSMediaMetadataMode); + }; + + return ( +
+ + +
+

{_('Media Info')}

+
+
+
+
+ {_('Update Frequency')} +
+