feat(tts): support going previous/next by sentence in bottom TTS bar, closes #1816 (#1967)

This commit is contained in:
Huang Xin
2025-09-05 16:34:30 +08:00
committed by GitHub
parent 64007b1188
commit 5724d2eeec
29 changed files with 230 additions and 43 deletions
@@ -531,5 +531,11 @@
"Select Fonts": "حدد الخطوط",
"Supported font formats: .ttf, .otf, .woff, .woff2": "تنسيقات الخط المدعومة: .ttf، .otf، .woff، .woff2",
"Push Progress": "تقدم الدفع",
"Pull Progress": "تقدم السحب"
"Pull Progress": "تقدم السحب",
"Previous Paragraph": "الفقرة السابقة",
"Previous Sentence": "الجملة السابقة",
"Pause": "إيقاف مؤقت",
"Play": "تشغيل",
"Next Sentence": "الجملة التالية",
"Next Paragraph": "الفقرة التالية"
}
@@ -515,5 +515,11 @@
"Sunset": "সূর্যাস্ত",
"Reveal in Finder": "ফাইন্ডারে দেখান",
"Reveal in File Explorer": "ফাইল এক্সপ্লোরারে দেখান",
"Reveal in Folder": "ফোল্ডারে দেখান"
"Reveal in Folder": "ফোল্ডারে দেখান",
"Previous Paragraph": "পূর্ববর্তী অনুচ্ছেদ",
"Previous Sentence": "পূর্ববর্তী বাক্য",
"Pause": "বিরতি",
"Play": "চালান",
"Next Sentence": "পরবর্তী বাক্য",
"Next Paragraph": "পরবর্তী অনুচ্ছেদ"
}
@@ -511,5 +511,11 @@
"Select Fonts": "ཡིག་གཟུགས་འདེམས་པ།",
"Supported font formats: .ttf, .otf, .woff, .woff2": "ཡིག་གཟུགས་འདེམས་པ་བྱས་ནས་བསྐྱར་བརྗེ་བ། ཡིག་གཟུགས་ཀྱི་སྒྲ་སྐད། .ttf, .otf, .woff, .woff2",
"Push Progress": "འདེགས་པའི་འཕེལ་རིམ།",
"Pull Progress": "འདྲུད་པའི་འཕེལ་རིམ།"
"Pull Progress": "འདྲུད་པའི་འཕེལ་རིམ།",
"Previous Paragraph": "སྔོན་གྱི་དོན་ཚན།",
"Previous Sentence": "སྔོན་གྱི་ཚིག་གྲུབ།",
"Pause": "སྐར་མ་འཇོག",
"Play": "གཏོང་བ།",
"Next Sentence": "རྗེས་ཀྱི་ཚིག་གྲུབ།",
"Next Paragraph": "རྗེས་ཀྱི་དོན་ཚན།"
}
@@ -515,5 +515,11 @@
"Select Fonts": "Schriftarten auswählen",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Unterstützte Schriftartenformate: .ttf, .otf, .woff, .woff2",
"Push Progress": "Fortschritt senden",
"Pull Progress": "Fortschritt empfangen"
"Pull Progress": "Fortschritt empfangen",
"Previous Paragraph": "Vorheriger Absatz",
"Previous Sentence": "Vorheriger Satz",
"Pause": "Pause",
"Play": "Wiedergabe",
"Next Sentence": "Nächster Satz",
"Next Paragraph": "Nächster Absatz"
}
@@ -515,5 +515,11 @@
"Select Fonts": "Επιλογή Γραμματοσειρών",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Υποστηριζόμενες μορφές γραμματοσειρών: .ttf, .otf, .woff, .woff2",
"Push Progress": "Αποστολή προόδου",
"Pull Progress": "Λήψη προόδου"
"Pull Progress": "Λήψη προόδου",
"Previous Paragraph": "Προηγούμενη παράγραφος",
"Previous Sentence": "Προηγούμενη πρόταση",
"Pause": "Παύση",
"Play": "Αναπαραγωγή",
"Next Sentence": "Επόμενη πρόταση",
"Next Paragraph": "Επόμενη παράγραφος"
}
@@ -519,5 +519,11 @@
"Select Fonts": "Seleccionar Fuentes",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formatos de fuente compatibles: .ttf, .otf, .woff, .woff2",
"Push Progress": "Empujar progreso",
"Pull Progress": "Tirar progreso"
"Pull Progress": "Tirar progreso",
"Previous Paragraph": "Párrafo anterior",
"Previous Sentence": "Oración anterior",
"Pause": "Pausar",
"Play": "Reproducir",
"Next Sentence": "Siguiente oración",
"Next Paragraph": "Siguiente párrafo"
}
@@ -519,5 +519,11 @@
"Select Fonts": "Sélectionner des Polices",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formats de police pris en charge : .ttf, .otf, .woff, .woff2",
"Push Progress": "Pousser la Progression",
"Pull Progress": "Tirer la Progression"
"Pull Progress": "Tirer la Progression",
"Previous Paragraph": "Paragraphe précédent",
"Previous Sentence": "Phrase précédente",
"Pause": "Pause",
"Play": "Lecture",
"Next Sentence": "Phrase suivante",
"Next Paragraph": "Paragraphe suivant"
}
@@ -515,5 +515,11 @@
"Select Fonts": "फ़ॉन्ट चुनें",
"Supported font formats: .ttf, .otf, .woff, .woff2": "समर्थित फ़ॉन्ट प्रारूप: .ttf, .otf, .woff, .woff2",
"Push Progress": "पुश प्रगति",
"Pull Progress": "पुल प्रगति"
"Pull Progress": "पुल प्रगति",
"Previous Paragraph": "पिछला पैराग्राफ",
"Previous Sentence": "पिछला वाक्य",
"Pause": "रोकें",
"Play": "चलाएं",
"Next Sentence": "अगला वाक्य",
"Next Paragraph": "अगला पैराग्राफ"
}
@@ -511,5 +511,11 @@
"Select Fonts": "Pilih Font",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Format font yang didukung: .ttf, .otf, .woff, .woff2",
"Push Progress": "Dorong Progres",
"Pull Progress": "Tarik Progres"
"Pull Progress": "Tarik Progres",
"Previous Paragraph": "Paragraf Sebelumnya",
"Previous Sentence": "Kalimat Sebelumnya",
"Pause": "Jeda",
"Play": "Putar",
"Next Sentence": "Kalimat Berikutnya",
"Next Paragraph": "Paragraf Berikutnya"
}
@@ -519,5 +519,11 @@
"Select Fonts": "Seleziona Font",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formati di font supportati: .ttf, .otf, .woff, .woff2",
"Push Progress": "Progresso push",
"Pull Progress": "Progresso pull"
"Pull Progress": "Progresso pull",
"Previous Paragraph": "Paragrafo precedente",
"Previous Sentence": "Frase precedente",
"Pause": "Pausa",
"Play": "Riproduci",
"Next Sentence": "Frase successiva",
"Next Paragraph": "Paragrafo successivo"
}
@@ -511,5 +511,11 @@
"Select Fonts": "フォントを選択",
"Supported font formats: .ttf, .otf, .woff, .woff2": "サポートされているフォント形式:.ttf, .otf, .woff, .woff2",
"Push Progress": "進捗をプッシュ",
"Pull Progress": "進捗をプル"
"Pull Progress": "進捗をプル",
"Previous Paragraph": "前の段落",
"Previous Sentence": "前の文",
"Pause": "一時停止",
"Play": "再生",
"Next Sentence": "次の文",
"Next Paragraph": "次の段落"
}
@@ -511,5 +511,11 @@
"Select Fonts": "글꼴 선택",
"Supported font formats: .ttf, .otf, .woff, .woff2": "지원되는 글꼴 형식: .ttf, .otf, .woff, .woff2",
"Push Progress": "진행 상황 푸시",
"Pull Progress": "진행 상황 끌어오기"
"Pull Progress": "진행 상황 끌어오기",
"Previous Paragraph": "이전 단락",
"Previous Sentence": "이전 문장",
"Pause": "일시정지",
"Play": "재생",
"Next Sentence": "다음 문장",
"Next Paragraph": "다음 단락"
}
@@ -515,5 +515,11 @@
"Select Fonts": "Lettertypen Selecteren",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Ondersteunde lettertypeformaten: .ttf, .otf, .woff, .woff2",
"Push Progress": "Voortgang pushen",
"Pull Progress": "Voortgang pullen"
"Pull Progress": "Voortgang pullen",
"Previous Paragraph": "Vorige alinea",
"Previous Sentence": "Vorige zin",
"Pause": "Pauzeren",
"Play": "Afspelen",
"Next Sentence": "Volgende zin",
"Next Paragraph": "Volgende alinea"
}
@@ -523,5 +523,11 @@
"Select Fonts": "Wybierz Czcionki",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Obsługiwane formaty czcionek: .ttf, .otf, .woff, .woff2",
"Push Progress": "Wyślij postęp",
"Pull Progress": "Pobierz postęp"
"Pull Progress": "Pobierz postęp",
"Previous Paragraph": "Poprzedni akapit",
"Previous Sentence": "Poprzednie zdanie",
"Pause": "Pauza",
"Play": "Odtwórz",
"Next Sentence": "Następne zdanie",
"Next Paragraph": "Następny akapit"
}
@@ -519,5 +519,11 @@
"Select Fonts": "Selecionar Fontes",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Formatos de fonte suportados: .ttf, .otf, .woff, .woff2",
"Push Progress": "Enviar Progresso",
"Pull Progress": "Obter Progresso"
"Pull Progress": "Obter Progresso",
"Previous Paragraph": "Parágrafo anterior",
"Previous Sentence": "Frase anterior",
"Pause": "Pausar",
"Play": "Reproduzir",
"Next Sentence": "Próxima frase",
"Next Paragraph": "Próximo parágrafo"
}
@@ -523,5 +523,11 @@
"Select Fonts": "Выбрать Шрифты",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Поддерживаемые форматы шрифтов: .ttf, .otf, .woff, .woff2",
"Push Progress": "Отправить прогресс",
"Pull Progress": "Получить прогресс"
"Pull Progress": "Получить прогресс",
"Previous Paragraph": "Предыдущий абзац",
"Previous Sentence": "Предыдущее предложение",
"Pause": "Пауза",
"Play": "Воспроизвести",
"Next Sentence": "Следующее предложение",
"Next Paragraph": "Следующий абзац"
}
@@ -515,5 +515,11 @@
"Sunset": "හිරු බැස්ම",
"Reveal in Finder": "Finder හි පෙන්වන්න",
"Reveal in File Explorer": "File Explorer හි පෙන්වන්න",
"Reveal in Folder": "ෆෝල්ඩරයේ පෙන්වන්න"
"Reveal in Folder": "ෆෝල්ඩරයේ පෙන්වන්න",
"Previous Paragraph": "කලින් ඡේදය",
"Previous Sentence": "කලින් වාක්‍යය",
"Pause": "විරාමය",
"Play": "වාදනය කරන්න",
"Next Sentence": "මීළඟ වාක්‍යය",
"Next Paragraph": "මීළඟ ඡේදය"
}
@@ -515,5 +515,11 @@
"Sunset": "சூரிய அஸ்தமனம்",
"Reveal in Finder": "Finder இல் காட்டவும்",
"Reveal in File Explorer": "File Explorer இல் காட்டவும்",
"Reveal in Folder": "கோப்புறையில் காட்டவும்"
"Reveal in Folder": "கோப்புறையில் காட்டவும்",
"Previous Paragraph": "முந்தைய பத்தி",
"Previous Sentence": "முந்தைய வாக்கியம்",
"Pause": "இடைநிறுத்தம்",
"Play": "இயக்கு",
"Next Sentence": "அடுத்த வாக்கியம்",
"Next Paragraph": "அடுத்த பத்தி"
}
@@ -511,5 +511,11 @@
"Select Fonts": "เลือกฟอนต์",
"Supported font formats: .ttf, .otf, .woff, .woff2": "รูปแบบฟอนต์ที่รองรับ: .ttf, .otf, .woff, .woff2",
"Push Progress": "ส่งความคืบหน้า",
"Pull Progress": "ดึงความคืบหน้า"
"Pull Progress": "ดึงความคืบหน้า",
"Previous Paragraph": "ย่อหน้าก่อนหน้า",
"Previous Sentence": "ประโยคก่อนหน้า",
"Pause": "หยุดชั่วคราว",
"Play": "เล่น",
"Next Sentence": "ประโยคถัดไป",
"Next Paragraph": "ย่อหน้าถัดไป"
}
@@ -515,5 +515,11 @@
"Select Fonts": "Yazı Tiplerini Seç",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Desteklenen yazı tipi formatları: .ttf, .otf, .woff, .woff2",
"Push Progress": "İlerlemeyi Gönder",
"Pull Progress": "İlerlemeyi Al"
"Pull Progress": "İlerlemeyi Al",
"Previous Paragraph": "Önceki Paragraf",
"Previous Sentence": "Önceki Cümle",
"Pause": "Duraklat",
"Play": "Oynat",
"Next Sentence": "Sonraki Cümle",
"Next Paragraph": "Sonraki Paragraf"
}
@@ -523,5 +523,11 @@
"Select Fonts": "Вибрати Шрифти",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Підтримувані формати шрифтів: .ttf, .otf, .woff, .woff2",
"Push Progress": "Надіслати прогрес",
"Pull Progress": "Отримати прогрес"
"Pull Progress": "Отримати прогрес",
"Previous Paragraph": "Попередній абзац",
"Previous Sentence": "Попереднє речення",
"Pause": "Пауза",
"Play": "Відтворити",
"Next Sentence": "Наступне речення",
"Next Paragraph": "Наступний абзац"
}
@@ -511,5 +511,11 @@
"Select Fonts": "Chọn Phông Chữ",
"Supported font formats: .ttf, .otf, .woff, .woff2": "Định dạng phông chữ được hỗ trợ: .ttf, .otf, .woff, .woff2",
"Push Progress": "Đẩy tiến độ",
"Pull Progress": "Kéo tiến độ"
"Pull Progress": "Kéo tiến độ",
"Previous Paragraph": "Đoạn trước",
"Previous Sentence": "Câu trước",
"Pause": "Tạm dừng",
"Play": "Phát",
"Next Sentence": "Câu tiếp theo",
"Next Paragraph": "Đoạn tiếp theo"
}
@@ -511,5 +511,11 @@
"Select Fonts": "选择字体",
"Supported font formats: .ttf, .otf, .woff, .woff2": "支持的字体格式:.ttf, .otf, .woff, .woff2",
"Push Progress": "推送阅读进度",
"Pull Progress": "拉取阅读进度"
"Pull Progress": "拉取阅读进度",
"Previous Paragraph": "上一段",
"Previous Sentence": "上一句",
"Pause": "暂停",
"Play": "播放",
"Next Sentence": "下一句",
"Next Paragraph": "下一段"
}
@@ -511,5 +511,11 @@
"Select Fonts": "選擇字型",
"Supported font formats: .ttf, .otf, .woff, .woff2": "支援的字型格式:.ttf, .otf, .woff, .woff2",
"Push Progress": "推送閱讀進度",
"Pull Progress": "拉取閱讀進度"
"Pull Progress": "拉取閱讀進度",
"Previous Paragraph": "上一段",
"Previous Sentence": "上一句",
"Pause": "暫停",
"Play": "播放",
"Next Sentence": "下一句",
"Next Paragraph": "下一段"
}
@@ -1,17 +1,25 @@
import clsx from 'clsx';
import React from 'react';
import { MdPlayArrow, MdOutlinePause, MdFastRewind, MdFastForward } from 'react-icons/md';
import {
MdPlayArrow,
MdOutlinePause,
MdFastRewind,
MdFastForward,
MdSkipPrevious,
MdSkipNext,
} from 'react-icons/md';
import { Insets } from '@/types/misc';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useTranslation } from '@/hooks/useTranslation';
type TTSBarProps = {
bookKey: string;
isPlaying: boolean;
onTogglePlay: () => void;
onBackward: () => void;
onForward: () => void;
onBackward: (byMark: boolean) => void;
onForward: (byMark: boolean) => void;
gridInsets: Insets;
};
@@ -23,6 +31,7 @@ const TTSBar = ({
onForward,
gridInsets,
}: TTSBarProps) => {
const _ = useTranslation();
const { appService } = useEnv();
const { hoveredBookKey, setHoveredBookKey } = useReaderStore();
const iconSize32 = useResponsiveSize(30);
@@ -44,20 +53,42 @@ const TTSBar = ({
>
<div className='text-base-content flex h-[52px] items-center space-x-2 px-2'>
<button
onClick={onBackward}
onClick={onBackward.bind(null, false)}
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
title={_('Previous Paragraph')}
aria-label={_('Previous Paragraph')}
>
<MdFastRewind size={iconSize32} />
</button>
<button
onClick={onBackward.bind(null, true)}
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
title={_('Previous Sentence')}
aria-label={_('Previous Sentence')}
>
<MdSkipPrevious size={iconSize32} />
</button>
<button
onClick={onTogglePlay}
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
title={isPlaying ? _('Pause') : _('Play')}
aria-label={isPlaying ? _('Pause') : _('Play')}
>
{isPlaying ? <MdOutlinePause size={iconSize48} /> : <MdPlayArrow size={iconSize48} />}
</button>
<button
onClick={onForward}
onClick={onForward.bind(null, true)}
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
title={_('Next Sentence')}
aria-label={_('Next Sentence')}
>
<MdSkipNext size={iconSize32} />
</button>
<button
onClick={onForward.bind(null, false)}
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
title={_('Next Paragraph')}
aria-label={_('Next Paragraph')}
>
<MdFastForward size={iconSize32} />
</button>
@@ -260,17 +260,17 @@ const TTSControl: React.FC<TTSControlProps> = ({ bookKey, gridInsets }) => {
}
}, [isPlaying, isPaused]);
const handleBackward = useCallback(async () => {
const handleBackward = useCallback(async (byMark = false) => {
const ttsController = ttsControllerRef.current;
if (ttsController) {
await ttsController.backward();
await ttsController.backward(byMark);
}
}, []);
const handleForward = useCallback(async () => {
const handleForward = useCallback(async (byMark = false) => {
const ttsController = ttsControllerRef.current;
if (ttsController) {
await ttsController.forward();
await ttsController.forward(byMark);
}
}, []);
@@ -237,12 +237,16 @@ const TTSPanel = ({
<button
onClick={onBackward}
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
title={_('Previous Paragraph')}
aria-label={_('Previous Paragraph')}
>
<MdFastRewind size={iconSize32} />
</button>
<button
onClick={onTogglePlay}
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
title={isPlaying ? _('Pause') : _('Play')}
aria-label={isPlaying ? _('Pause') : _('Play')}
>
{isPlaying ? (
<MdPauseCircle size={iconSize48} className='fill-primary' />
@@ -253,6 +257,8 @@ const TTSPanel = ({
<button
onClick={onForward}
className='rounded-full p-1 transition-transform duration-200 hover:scale-105'
title={_('Next Paragraph')}
aria-label={_('Next Paragraph')}
>
<MdFastForward size={iconSize32} />
</button>
@@ -271,30 +271,36 @@ export class TTSController extends EventTarget {
this.state = 'stopped';
}
// goto previous paragraph
async backward() {
// goto previous mark/paragraph
async backward(byMark = false) {
await this.initViewTTS();
if (this.state === 'playing') {
await this.stop();
this.#speak(this.view.tts?.prev());
if (byMark) this.#speak(this.view.tts?.prevMark());
else this.#speak(this.view.tts?.prev());
} else {
await this.stop();
this.state = 'backward-paused';
this.view.tts?.prev(true);
if (byMark) this.view.tts?.prevMark(true);
else this.view.tts?.prev(true);
}
}
// goto next paragraph
async forward() {
// goto next mark/paragraph
async forward(byMark = false) {
await this.initViewTTS();
if (this.state === 'playing') {
await this.stop();
this.#speak(this.view.tts?.next());
this.preloadNextSSML();
if (byMark) this.#speak(this.view.tts?.nextMark());
else {
this.#speak(this.view.tts?.next());
this.preloadNextSSML();
}
} else {
await this.stop();
this.state = 'forward-paused';
this.view.tts?.next(true);
if (byMark) this.view.tts?.nextMark(true);
else this.view.tts?.next(true);
}
}