diff --git a/apps/readest-app/public/locales/ar/translation.json b/apps/readest-app/public/locales/ar/translation.json index 83c007cd..f10dfc92 100644 --- a/apps/readest-app/public/locales/ar/translation.json +++ b/apps/readest-app/public/locales/ar/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "هذا تنسيق الكتاب غير مدعوم.", "Unable to fetch the translation. Please log in first and try again.": "تعذر جلب الترجمة. يرجى تسجيل الدخول أولاً ثم المحاولة مرة أخرى.", "Group": "تجميع", - "Always on Top": "دائمًا في المقدمة" + "Always on Top": "دائمًا في المقدمة", + "No Timeout": "لا مهلة", + "{{value}} minute": "{{value}} دقيقة", + "{{value}} minutes": "{{value}} دقائق", + "{{value}} hour": "{{value}} ساعة", + "{{value}} hours": "{{value}} ساعات" } diff --git a/apps/readest-app/public/locales/de/translation.json b/apps/readest-app/public/locales/de/translation.json index b066200a..42db8237 100644 --- a/apps/readest-app/public/locales/de/translation.json +++ b/apps/readest-app/public/locales/de/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "Dieses Buchformat wird nicht unterstützt.", "Unable to fetch the translation. Please log in first and try again.": "Übersetzung konnte nicht abgerufen werden. Bitte zuerst anmelden und erneut versuchen.", "Group": "Gruppieren", - "Always on Top": "Immer im Vordergrund" + "Always on Top": "Immer im Vordergrund", + "No Timeout": "Kein Timeout", + "{{value}} minute": "{{value}} Minute", + "{{value}} minutes": "{{value}} Minuten", + "{{value}} hour": "{{value}} Stunde", + "{{value}} hours": "{{value}} Stunden" } diff --git a/apps/readest-app/public/locales/el/translation.json b/apps/readest-app/public/locales/el/translation.json index 22892735..06453df1 100644 --- a/apps/readest-app/public/locales/el/translation.json +++ b/apps/readest-app/public/locales/el/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "Αυτή η μορφή βιβλίου δεν υποστηρίζεται.", "Unable to fetch the translation. Please log in first and try again.": "Αδυναμία λήψης της μετάφρασης. Παρακαλώ συνδεθείτε πρώτα και δοκιμάστε ξανά.", "Group": "Ομάδα", - "Always on Top": "Πάντα στην κορυφή" + "Always on Top": "Πάντα στην κορυφή", + "No Timeout": "Χωρίς χρονικό όριο", + "{{value}} minute": "{{value}} λεπτό", + "{{value}} minutes": "{{value}} λεπτά", + "{{value}} hour": "{{value}} ώρα", + "{{value}} hours": "{{value}} ώρες" } diff --git a/apps/readest-app/public/locales/es/translation.json b/apps/readest-app/public/locales/es/translation.json index e6a6cc5d..94ce6d59 100644 --- a/apps/readest-app/public/locales/es/translation.json +++ b/apps/readest-app/public/locales/es/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "Este formato de libro no es compatible.", "Unable to fetch the translation. Please log in first and try again.": "No se puede obtener la traducción. Por favor, inicia sesión primero e inténtalo de nuevo.", "Group": "Grupo", - "Always on Top": "Siempre en la parte superior" + "Always on Top": "Siempre en la parte superior", + "No Timeout": "Sin tiempo de espera", + "{{value}} minute": "{{value}} minuto", + "{{value}} minutes": "{{value}} minutos", + "{{value}} hour": "{{value}} hora", + "{{value}} hours": "{{value}} horas" } diff --git a/apps/readest-app/public/locales/fr/translation.json b/apps/readest-app/public/locales/fr/translation.json index 7f9ab99f..813b5260 100644 --- a/apps/readest-app/public/locales/fr/translation.json +++ b/apps/readest-app/public/locales/fr/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "Ce format de livre n'est pas pris en charge.", "Unable to fetch the translation. Please log in first and try again.": "Impossible de récupérer la traduction. Veuillez d'abord vous connecter et réessayer.", "Group": "Groupe", - "Always on Top": "Toujours au-dessus" + "Always on Top": "Toujours au-dessus", + "No Timeout": "Aucun délai d'attente", + "{{value}} minute": "{{value}} minute", + "{{value}} minutes": "{{value}} minutes", + "{{value}} hour": "{{value}} heure", + "{{value}} hours": "{{value}} heures" } diff --git a/apps/readest-app/public/locales/hi/translation.json b/apps/readest-app/public/locales/hi/translation.json index 45c1c6e4..31f6f3e2 100644 --- a/apps/readest-app/public/locales/hi/translation.json +++ b/apps/readest-app/public/locales/hi/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "यह पुस्तक प्रारूप समर्थित नहीं है।", "Unable to fetch the translation. Please log in first and try again.": "अनुवाद प्राप्त करने में असमर्थ। कृपया पहले लॉग इन करें और पुनः प्रयास करें।", "Group": "समूह", - "Always on Top": "सर्वोच्च पर हमेशा" + "Always on Top": "सर्वोच्च पर हमेशा", + "No Timeout": "कोई समय समाप्त नहीं", + "{{value}} minute": "{{value}} मिनट", + "{{value}} minutes": "{{value}} मिनट", + "{{value}} hour": "{{value}} घंटा", + "{{value}} hours": "{{value}} घंटे" } diff --git a/apps/readest-app/public/locales/id/translation.json b/apps/readest-app/public/locales/id/translation.json index c8d8e5cd..c16db3a6 100644 --- a/apps/readest-app/public/locales/id/translation.json +++ b/apps/readest-app/public/locales/id/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "Format buku ini tidak didukung.", "Unable to fetch the translation. Please log in first and try again.": "Tidak dapat mengambil terjemahan. Silakan masuk terlebih dahulu dan coba lagi.", "Group": "Grup", - "Always on Top": "Selalu di Atas" + "Always on Top": "Selalu di Atas", + "No Timeout": "Tidak Ada Waktu Habis", + "{{value}} minute": "{{value}} menit", + "{{value}} minutes": "{{value}} menit", + "{{value}} hour": "{{value}} jam", + "{{value}} hours": "{{value}} jam" } diff --git a/apps/readest-app/public/locales/it/translation.json b/apps/readest-app/public/locales/it/translation.json index 8585889c..cd026081 100644 --- a/apps/readest-app/public/locales/it/translation.json +++ b/apps/readest-app/public/locales/it/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "Questo formato di libro non è supportato.", "Unable to fetch the translation. Please log in first and try again.": "Impossibile recuperare la traduzione. Accedi prima e riprova.", "Group": "Gruppo", - "Always on Top": "Sempre in primo piano" + "Always on Top": "Sempre in primo piano", + "No Timeout": "Nessun timeout", + "{{value}} minute": "{{value}} minuto", + "{{value}} minutes": "{{value}} minuti", + "{{value}} hour": "{{value}} ora", + "{{value}} hours": "{{value}} ore" } diff --git a/apps/readest-app/public/locales/ja/translation.json b/apps/readest-app/public/locales/ja/translation.json index 7eaf2230..fd7252f3 100644 --- a/apps/readest-app/public/locales/ja/translation.json +++ b/apps/readest-app/public/locales/ja/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "この書籍形式はサポートされていません。", "Unable to fetch the translation. Please log in first and try again.": "翻訳を取得できません。まずログインして再試行してください。", "Group": "グループ", - "Always on Top": "常に最前面" + "Always on Top": "常に最前面", + "No Timeout": "タイムアウトなし", + "{{value}} minute": "{{value}}分", + "{{value}} minutes": "{{value}}分", + "{{value}} hour": "{{value}}時間", + "{{value}} hours": "{{value}}時間" } diff --git a/apps/readest-app/public/locales/ko/translation.json b/apps/readest-app/public/locales/ko/translation.json index e32cef52..b2394257 100644 --- a/apps/readest-app/public/locales/ko/translation.json +++ b/apps/readest-app/public/locales/ko/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "이 책 형식은 지원되지 않습니다.", "Unable to fetch the translation. Please log in first and try again.": "번역을 가져올 수 없습니다. 먼저 로그인하고 다시 시도하세요.", "Group": "그룹", - "Always on Top": "항상 위에" + "Always on Top": "항상 위에", + "No Timeout": "타임아웃 없음", + "{{value}} minute": "{{value}} 분", + "{{value}} minutes": "{{value}} 분", + "{{value}} hour": "{{value}} 시간", + "{{value}} hours": "{{value}} 시간" } diff --git a/apps/readest-app/public/locales/pl/translation.json b/apps/readest-app/public/locales/pl/translation.json index 2041262a..af9a7404 100644 --- a/apps/readest-app/public/locales/pl/translation.json +++ b/apps/readest-app/public/locales/pl/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "Ten format książki nie jest obsługiwany.", "Unable to fetch the translation. Please log in first and try again.": "Nie można pobrać tłumaczenia. Proszę najpierw się zalogować i spróbować ponownie.", "Group": "Grupa", - "Always on Top": "Zawsze na wierzchu" + "Always on Top": "Zawsze na wierzchu", + "No Timeout": "Brak limitu czasu", + "{{value}} minute": "{{value}} minuta", + "{{value}} minutes": "{{value}} minut", + "{{value}} hour": "{{value}} godzina", + "{{value}} hours": "{{value}} godziny" } diff --git a/apps/readest-app/public/locales/pt/translation.json b/apps/readest-app/public/locales/pt/translation.json index e910f0c6..1b6f6700 100644 --- a/apps/readest-app/public/locales/pt/translation.json +++ b/apps/readest-app/public/locales/pt/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "Este formato de livro não é suportado.", "Unable to fetch the translation. Please log in first and try again.": "Não foi possível buscar a tradução. Faça login primeiro e tente novamente.", "Group": "Grupo", - "Always on Top": "Sempre no Topo" + "Always on Top": "Sempre no Topo", + "No Timeout": "Sem Tempo Limite", + "{{value}} minute": "{{value}} minuto", + "{{value}} minutes": "{{value}} minutos", + "{{value}} hour": "{{value}} hora", + "{{value}} hours": "{{value}} horas" } diff --git a/apps/readest-app/public/locales/ru/translation.json b/apps/readest-app/public/locales/ru/translation.json index 952c9880..7a80b85f 100644 --- a/apps/readest-app/public/locales/ru/translation.json +++ b/apps/readest-app/public/locales/ru/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "Этот формат книги не поддерживается.", "Unable to fetch the translation. Please log in first and try again.": "Не удалось получить перевод. Пожалуйста, войдите в систему и попробуйте снова.", "Group": "Группа", - "Always on Top": "Всегда сверху" + "Always on Top": "Всегда сверху", + "No Timeout": "Нет таймаута", + "{{value}} minute": "{{value}} минута", + "{{value}} minutes": "{{value}} минуты", + "{{value}} hour": "{{value}} час", + "{{value}} hours": "{{value}} часа" } diff --git a/apps/readest-app/public/locales/tr/translation.json b/apps/readest-app/public/locales/tr/translation.json index 3978a11d..871daf65 100644 --- a/apps/readest-app/public/locales/tr/translation.json +++ b/apps/readest-app/public/locales/tr/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "Bu kitap formatı desteklenmiyor.", "Unable to fetch the translation. Please log in first and try again.": "Çeviri alınamıyor. Lütfen önce giriş yapın ve tekrar deneyin.", "Group": "Grup", - "Always on Top": "Her Zaman Üstte" + "Always on Top": "Her Zaman Üstte", + "No Timeout": "Zaman Aşımı Yok", + "{{value}} minute": "{{value}} dakika", + "{{value}} minutes": "{{value}} dakika", + "{{value}} hour": "{{value}} saat", + "{{value}} hours": "{{value}} saat" } diff --git a/apps/readest-app/public/locales/uk/translation.json b/apps/readest-app/public/locales/uk/translation.json index fd06735d..120cb493 100644 --- a/apps/readest-app/public/locales/uk/translation.json +++ b/apps/readest-app/public/locales/uk/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "Цей формат книги не підтримується.", "Unable to fetch the translation. Please log in first and try again.": "Не вдалося отримати переклад. Будь ласка, спочатку увійдіть у систему та спробуйте ще раз.", "Group": "Група", - "Always on Top": "Завжди зверху" + "Always on Top": "Завжди зверху", + "No Timeout": "Без тайм-ауту", + "{{value}} minute": "{{value}} хвилина", + "{{value}} minutes": "{{value}} хвилин", + "{{value}} hour": "{{value}} година", + "{{value}} hours": "{{value}} години" } diff --git a/apps/readest-app/public/locales/vi/translation.json b/apps/readest-app/public/locales/vi/translation.json index e492bdcf..ba69d395 100644 --- a/apps/readest-app/public/locales/vi/translation.json +++ b/apps/readest-app/public/locales/vi/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "Định dạng sách này không được hỗ trợ.", "Unable to fetch the translation. Please log in first and try again.": "Không thể tải dịch vụ. Vui lòng đăng nhập trước và thử lại.", "Group": "Nhóm", - "Always on Top": "Luôn ở trên cùng" + "Always on Top": "Luôn ở trên cùng", + "No Timeout": "Không có thời gian chờ", + "{{value}} minute": "{{value}} phút", + "{{value}} minutes": "{{value}} phút", + "{{value}} hour": "{{value}} giờ", + "{{value}} hours": "{{value}} giờ" } diff --git a/apps/readest-app/public/locales/zh-CN/translation.json b/apps/readest-app/public/locales/zh-CN/translation.json index 5b5f3d55..d0cfc5d6 100644 --- a/apps/readest-app/public/locales/zh-CN/translation.json +++ b/apps/readest-app/public/locales/zh-CN/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "不支持此书籍格式。", "Unable to fetch the translation. Please log in first and try again.": "无法获取翻译。请登录后重试。", "Group": "分组", - "Always on Top": "窗口置顶" + "Always on Top": "窗口置顶", + "No Timeout": "关闭定时", + "{{value}} minute": "{{value}}分钟", + "{{value}} minutes": "{{value}}分钟", + "{{value}} hour": "{{value}}小时", + "{{value}} hours": "{{value}}小时" } diff --git a/apps/readest-app/public/locales/zh-TW/translation.json b/apps/readest-app/public/locales/zh-TW/translation.json index 6ccafd4c..512bad6d 100644 --- a/apps/readest-app/public/locales/zh-TW/translation.json +++ b/apps/readest-app/public/locales/zh-TW/translation.json @@ -266,5 +266,10 @@ "This book format is not supported.": "不支援此書籍格式。", "Unable to fetch the translation. Please log in first and try again.": "無法獲取翻譯。請先登入然後重試。", "Group": "分組", - "Always on Top": "視窗置頂" + "Always on Top": "視窗置頂", + "No Timeout": "關閉定時", + "{{value}} minute": "{{value}}分鐘", + "{{value}} minutes": "{{value}}分鐘", + "{{value}} hour": "{{value}}小時", + "{{value}} hours": "{{value}}小時" } diff --git a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx index ef3e9b1a..4712630c 100644 --- a/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx +++ b/apps/readest-app/src/app/reader/components/tts/TTSControl.tsx @@ -34,6 +34,10 @@ const TTSControl = () => { const [panelPosition, setPanelPosition] = useState(); const [trianglePosition, setTrianglePosition] = useState(); + const [timeoutOption, setTimeoutOption] = useState(0); + const [timeoutTimestamp, setTimeoutTimestamp] = useState(0); + const [timeoutFunc, setTimeoutFunc] = useState | null>(null); + const popupWidth = useResponsiveSize(POPUP_WIDTH); const popupHeight = useResponsiveSize(POPUP_HEIGHT); const popupPadding = useResponsiveSize(POPUP_PADDING); @@ -142,11 +146,8 @@ const TTSControl = () => { } }; - const handleTTSStop = async (event: CustomEvent) => { - const { bookKey: stopBookKey } = event.detail; - if (bookKey === stopBookKey) { - handleStop(); - } + const handleTTSStop = async () => { + handleStop(); }; const handleQueryIsSpeaking = () => { @@ -257,6 +258,23 @@ const TTSControl = () => { return ''; }; + const handleSelectTimeout = (value: number) => { + setTimeoutOption(value); + if (timeoutFunc) { + clearTimeout(timeoutFunc); + } + if (value > 0) { + setTimeoutFunc( + setTimeout(() => { + handleStop(); + }, value * 1000), + ); + setTimeoutTimestamp(Date.now() + value * 1000); + } else { + setTimeoutTimestamp(0); + } + }; + const updatePanelPosition = () => { if (iconRef.current) { const rect = iconRef.current.getBoundingClientRect(); @@ -323,14 +341,16 @@ const TTSControl = () => { bookKey={bookKey} ttsLang={ttsLang} isPlaying={isPlaying} + timeoutOption={timeoutOption} + timeoutTimestamp={timeoutTimestamp} onTogglePlay={handleTogglePlay} onBackward={handleBackward} onForward={handleForward} - onStop={handleStop} onSetRate={handleSetRate} onGetVoices={handleGetVoices} onSetVoice={handleSetVoice} onGetVoiceId={handleGetVoiceId} + onSelectTimeout={handleSelectTimeout} /> )} diff --git a/apps/readest-app/src/app/reader/components/tts/TTSPanel.tsx b/apps/readest-app/src/app/reader/components/tts/TTSPanel.tsx index 366662fa..6b17820b 100644 --- a/apps/readest-app/src/app/reader/components/tts/TTSPanel.tsx +++ b/apps/readest-app/src/app/reader/components/tts/TTSPanel.tsx @@ -1,12 +1,12 @@ import clsx from 'clsx'; import React, { useState, ChangeEvent, useEffect } from 'react'; -import { MdPlayCircle, MdPauseCircle, MdFastRewind, MdFastForward, MdStop } from 'react-icons/md'; +import { MdPlayCircle, MdPauseCircle, MdFastRewind, MdFastForward, MdAlarm } from 'react-icons/md'; import { RiVoiceAiFill } from 'react-icons/ri'; import { MdCheck } from 'react-icons/md'; import { TTSVoice } from '@/services/tts'; import { useEnv } from '@/context/EnvContext'; import { useReaderStore } from '@/store/readerStore'; -import { useTranslation } from '@/hooks/useTranslation'; +import { TranslationFunc, useTranslation } from '@/hooks/useTranslation'; import { useSettingsStore } from '@/store/settingsStore'; import { useDefaultIconSize, useResponsiveSize } from '@/hooks/useResponsiveSize'; @@ -14,28 +14,104 @@ type TTSPanelProps = { bookKey: string; ttsLang: string; isPlaying: boolean; + timeoutOption: number; + timeoutTimestamp: number; onTogglePlay: () => void; onBackward: () => void; onForward: () => void; - onStop: () => void; onSetRate: (rate: number) => void; onGetVoices: (lang: string) => Promise; onSetVoice: (voice: string) => void; onGetVoiceId: () => string; + onSelectTimeout: (value: number) => void; +}; + +const getTTSTimeoutOptions = (_: TranslationFunc) => { + return [ + { + label: _('No Timeout'), + value: 0, + }, + { + label: _('{{value}} minute', { value: 1 }), + value: 60, + }, + { + label: _('{{value}} minutes', { value: 3 }), + value: 180, + }, + { + label: _('{{value}} minutes', { value: 5 }), + value: 300, + }, + { + label: _('{{value}} minutes', { value: 10 }), + value: 600, + }, + { + label: _('{{value}} minutes', { value: 20 }), + value: 1200, + }, + { + label: _('{{value}} minutes', { value: 30 }), + value: 1800, + }, + { + label: _('{{value}} minutes', { value: 45 }), + value: 2700, + }, + { + label: _('{{value}} hour', { value: 1 }), + value: 3600, + }, + { + label: _('{{value}} hours', { value: 2 }), + value: 7200, + }, + { + label: _('{{value}} hours', { value: 3 }), + value: 10800, + }, + { + label: _('{{value}} hours', { value: 4 }), + value: 14400, + }, + { + label: _('{{value}} hours', { value: 6 }), + value: 21600, + }, + { + label: _('{{value}} hours', { value: 8 }), + value: 28800, + }, + ]; +}; + +const getCountdownTime = (timeout: number) => { + const now = Date.now(); + if (timeout > now) { + const remainingTime = Math.floor((timeout - now) / 1000); + const minutes = Math.floor(remainingTime / 3600) * 60 + Math.floor((remainingTime % 3600) / 60); + const seconds = remainingTime % 60; + return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; + } + return ''; }; const TTSPanel = ({ bookKey, ttsLang, isPlaying, + timeoutOption, + timeoutTimestamp, onTogglePlay, onBackward, onForward, - onStop, onSetRate, onGetVoices, onSetVoice, onGetVoiceId, + onSelectTimeout, }: TTSPanelProps) => { const _ = useTranslation(); const { envConfig } = useEnv(); @@ -47,6 +123,10 @@ const TTSPanel = ({ const [rate, setRate] = useState(viewSettings?.ttsRate ?? 1.0); const [selectedVoice, setSelectedVoice] = useState(viewSettings?.ttsVoice ?? ''); + const [timeoutCountdown, setTimeoutCountdown] = useState(() => { + return getCountdownTime(timeoutTimestamp); + }); + const defaultIconSize = useDefaultIconSize(); const iconSize32 = useResponsiveSize(32); const iconSize48 = useResponsiveSize(48); @@ -72,6 +152,23 @@ const TTSPanel = ({ setViewSettings(bookKey, viewSettings); }; + const updateTimeout = (timeout: number) => { + const now = Date.now(); + if (timeout > 0 && timeout < now) { + onSelectTimeout(0); + setTimeoutCountdown(''); + } else if (timeout > 0) { + setTimeoutCountdown(getCountdownTime(timeout)); + } + }; + + useEffect(() => { + setTimeout(() => { + updateTimeout(timeoutTimestamp); + }, 1000); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [timeoutTimestamp, timeoutCountdown]); + useEffect(() => { const voiceId = onGetVoiceId(); setSelectedVoice(voiceId); @@ -87,6 +184,8 @@ const TTSPanel = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [ttsLang]); + const timeoutOptions = getTTSTimeoutOptions(_); + return (
@@ -132,9 +231,40 @@ const TTSPanel = ({ - +
+ +
    + {timeoutOptions.map((option, index) => ( +
  • onSelectTimeout(option.value)}> +
    + + {timeoutOption === option.value && } + + {option.label} +
    +
  • + ))} +
+
+