Initial implementation of TTS with Web Speech API (#120)
* Initial implementation of TTS with Web Speech API * Add translations for annotator tools
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
"About Readest": "Über Readest",
|
||||
"Add your notes here...": "Fügen Sie hier Ihre Notizen hinzu...",
|
||||
"Animation": "Animation",
|
||||
"Annotate": "Annotieren",
|
||||
"Apply": "Anwenden",
|
||||
"Are you sure to delete the selected books?": "Möchten Sie die ausgewählten Bücher wirklich löschen?",
|
||||
"Auto Mode": "Automatischer Modus",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "Farbe",
|
||||
"Confirm": "Bestätigen",
|
||||
"Confirm Deletion": "Löschen bestätigen",
|
||||
"Copied to notebook": "In Notizbuch kopiert",
|
||||
"Copy": "Kopieren",
|
||||
"Custom CSS": "Benutzerdefiniertes CSS",
|
||||
"Dark Mode": "Dunkelmodus",
|
||||
"Default": "Standard",
|
||||
"Default Font": "Standardschriftart",
|
||||
"Default Font Size": "Standard-Schriftgröße",
|
||||
"Delete": "Löschen",
|
||||
"Delete Highlight": "Hervorhebung löschen",
|
||||
"Dictionary": "Wörterbuch",
|
||||
"Disable Click-to-Flip": "Klick zum Blättern deaktivieren",
|
||||
"Download Readest": "Readest herunterladen",
|
||||
"Edit": "Bearbeiten",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "Grasgrün",
|
||||
"Gray": "Grau",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Hervorheben",
|
||||
"Horizontal Direction": "Horizontale Richtung",
|
||||
"Hyphenation": "Silbentrennung",
|
||||
"Identifier:": "Kennung:",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "Serifenlose Schriftart",
|
||||
"Save": "Speichern",
|
||||
"Scrolled Mode": "Scroll-Modus",
|
||||
"Search": "Suchen",
|
||||
"Search books...": "Bücher suchen...",
|
||||
"Search...": "Suchen...",
|
||||
"Select Book": "Buch auswählen",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "Abmelden",
|
||||
"Sky": "Himmelblau",
|
||||
"Solarized": "Solarisiert",
|
||||
"Speak": "Sprechen",
|
||||
"Subjects:": "Themen:",
|
||||
"System Fonts": "System-Schriftarten",
|
||||
"Theme Color": "Themenfarbe",
|
||||
"Theme Mode": "Themenmodus",
|
||||
"Translate": "Übersetzen",
|
||||
"Translated Text": "Übersetzter Text",
|
||||
"Unknown": "Unbekannt",
|
||||
"Untitled": "Ohne Titel",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "Version {{version}}",
|
||||
"Vertical Direction": "Vertikale Richtung",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Willkommen in Ihrer Bibliothek. Sie können hier Ihre Bücher importieren und jederzeit lesen.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Schreibmodus",
|
||||
"Your Library": "Ihre Bibliothek"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"About Readest": "Acerca de Readest",
|
||||
"Add your notes here...": "Añade tus notas aquí...",
|
||||
"Animation": "Animación",
|
||||
"Annotate": "Anotar",
|
||||
"Apply": "Aplicar",
|
||||
"Are you sure to delete the selected books?": "¿Estás seguro de eliminar los libros seleccionados?",
|
||||
"Auto Mode": "Modo automático",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "Color",
|
||||
"Confirm": "Confirmar",
|
||||
"Confirm Deletion": "Confirmar eliminación",
|
||||
"Copied to notebook": "Copiado al cuaderno",
|
||||
"Copy": "Copiar",
|
||||
"Custom CSS": "CSS personalizado",
|
||||
"Dark Mode": "Modo oscuro",
|
||||
"Default": "Predeterminado",
|
||||
"Default Font": "Fuente predeterminada",
|
||||
"Default Font Size": "Tamaño de fuente predeterminado",
|
||||
"Delete": "Eliminar",
|
||||
"Delete Highlight": "Eliminar resaltado",
|
||||
"Dictionary": "Diccionario",
|
||||
"Disable Click-to-Flip": "Desactivar clic para voltear",
|
||||
"Download Readest": "Descargar Readest",
|
||||
"Edit": "Editar",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "Hierba",
|
||||
"Gray": "Gris",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Resaltar",
|
||||
"Horizontal Direction": "Dirección horizontal",
|
||||
"Hyphenation": "Guionización",
|
||||
"Identifier:": "Identificador:",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "Fuente sans-serif",
|
||||
"Save": "Guardar",
|
||||
"Scrolled Mode": "Modo desplazamiento",
|
||||
"Search": "Buscar",
|
||||
"Search books...": "Buscar libros...",
|
||||
"Search...": "Buscar...",
|
||||
"Select Book": "Seleccionar libro",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "Cerrar sesión",
|
||||
"Sky": "Cielo",
|
||||
"Solarized": "Solarizado",
|
||||
"Speak": "Leer",
|
||||
"Subjects:": "Temas:",
|
||||
"System Fonts": "Fuentes del sistema",
|
||||
"Theme Color": "Color del tema",
|
||||
"Theme Mode": "Modo del tema",
|
||||
"Translate": "Traducir",
|
||||
"Translated Text": "Texto traducido",
|
||||
"Unknown": "Desconocido",
|
||||
"Untitled": "Sin título",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "Versión {{version}}",
|
||||
"Vertical Direction": "Dirección vertical",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenido a tu biblioteca. Puedes importar tus libros aquí y leerlos en cualquier momento.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Modo de escritura",
|
||||
"Your Library": "Tu biblioteca"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"About Readest": "À propos de Readest",
|
||||
"Add your notes here...": "Ajoutez vos notes ici...",
|
||||
"Animation": "Animation",
|
||||
"Annotate": "Annoter",
|
||||
"Apply": "Appliquer",
|
||||
"Are you sure to delete the selected books?": "Êtes-vous sûr de vouloir supprimer les livres sélectionnés ?",
|
||||
"Auto Mode": "Mode automatique",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "Couleur",
|
||||
"Confirm": "Confirmer",
|
||||
"Confirm Deletion": "Confirmer la suppression",
|
||||
"Copied to notebook": "Copié dans le carnet de notes",
|
||||
"Copy": "Copier",
|
||||
"Custom CSS": "CSS personnalisé",
|
||||
"Dark Mode": "Mode sombre",
|
||||
"Default": "Par défaut",
|
||||
"Default Font": "Police par défaut",
|
||||
"Default Font Size": "Taille de police par défaut",
|
||||
"Delete": "Supprimer",
|
||||
"Delete Highlight": "Supprimer le surlignage",
|
||||
"Dictionary": "Dictionnaire",
|
||||
"Disable Click-to-Flip": "Désactiver le clic pour tourner",
|
||||
"Download Readest": "Télécharger Readest",
|
||||
"Edit": "Modifier",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "Herbe",
|
||||
"Gray": "Gris",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Surligner",
|
||||
"Horizontal Direction": "Direction horizontale",
|
||||
"Hyphenation": "Césure",
|
||||
"Identifier:": "Identifiant :",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "Police sans empattement",
|
||||
"Save": "Enregistrer",
|
||||
"Scrolled Mode": "Mode défilement",
|
||||
"Search": "Rechercher",
|
||||
"Search books...": "Rechercher des livres...",
|
||||
"Search...": "Rechercher...",
|
||||
"Select Book": "Sélectionner un livre",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "Se déconnecter",
|
||||
"Sky": "Ciel",
|
||||
"Solarized": "Solarisé",
|
||||
"Speak": "Lire",
|
||||
"Subjects:": "Sujets :",
|
||||
"System Fonts": "Polices système",
|
||||
"Theme Color": "Couleur du thème",
|
||||
"Theme Mode": "Mode du thème",
|
||||
"Translate": "Traduire",
|
||||
"Translated Text": "Texte traduit",
|
||||
"Unknown": "Inconnu",
|
||||
"Untitled": "Sans titre",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "Version {{version}}",
|
||||
"Vertical Direction": "Direction verticale",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bienvenue dans votre bibliothèque. Vous pouvez importer vos livres ici et les lire à tout moment.",
|
||||
"Wikipedia": "Wikipédia",
|
||||
"Writing Mode": "Mode écriture",
|
||||
"Your Library": "Votre bibliothèque"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"About Readest": "Tentang Readest",
|
||||
"Add your notes here...": "Tambahkan catatan Anda di sini...",
|
||||
"Animation": "Animasi",
|
||||
"Annotate": "Anotasi",
|
||||
"Apply": "Terapkan",
|
||||
"Are you sure to delete the selected books?": "Apakah Anda yakin ingin menghapus buku yang dipilih?",
|
||||
"Auto Mode": "Mode Otomatis",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "Warna",
|
||||
"Confirm": "Konfirmasi",
|
||||
"Confirm Deletion": "Konfirmasi Penghapusan",
|
||||
"Copied to notebook": "Disalin ke buku catatan",
|
||||
"Copy": "Salin",
|
||||
"Custom CSS": "CSS Kustom",
|
||||
"Dark Mode": "Mode Gelap",
|
||||
"Default": "Default",
|
||||
"Default Font": "Font Default",
|
||||
"Default Font Size": "Ukuran Font Default",
|
||||
"Delete": "Hapus",
|
||||
"Delete Highlight": "Hapus Sorotan",
|
||||
"Dictionary": "Kamus",
|
||||
"Disable Click-to-Flip": "Nonaktifkan Klik untuk Membalik",
|
||||
"Download Readest": "Unduh Readest",
|
||||
"Edit": "Edit",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "Rumput",
|
||||
"Gray": "Abu-abu",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Sorot",
|
||||
"Horizontal Direction": "Arah Horizontal",
|
||||
"Hyphenation": "Pemenggalan Kata",
|
||||
"Identifier:": "Pengenal:",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "Font Sans-Serif",
|
||||
"Save": "Simpan",
|
||||
"Scrolled Mode": "Mode Gulir",
|
||||
"Search": "Cari",
|
||||
"Search books...": "Cari buku...",
|
||||
"Search...": "Cari...",
|
||||
"Select Book": "Pilih Buku",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "Keluar",
|
||||
"Sky": "Langit",
|
||||
"Solarized": "Solarized",
|
||||
"Speak": "Bicara",
|
||||
"Subjects:": "Subjek:",
|
||||
"System Fonts": "Font Sistem",
|
||||
"Theme Color": "Warna Tema",
|
||||
"Theme Mode": "Mode Tema",
|
||||
"Translate": "Terjemahkan",
|
||||
"Translated Text": "Teks Terjemahan",
|
||||
"Unknown": "Tidak Diketahui",
|
||||
"Untitled": "Tanpa Judul",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "Versi {{version}}",
|
||||
"Vertical Direction": "Arah Vertikal",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Selamat datang di perpustakaan Anda. Anda dapat mengimpor buku-buku Anda di sini dan membacanya kapan saja.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Mode Menulis",
|
||||
"Your Library": "Perpustakaan Anda"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"About Readest": "Informazioni su Readest",
|
||||
"Add your notes here...": "Aggiungi qui le tue note...",
|
||||
"Animation": "Animazione",
|
||||
"Annotate": "Annota",
|
||||
"Apply": "Applica",
|
||||
"Are you sure to delete the selected books?": "Sei sicuro di voler eliminare i libri selezionati?",
|
||||
"Auto Mode": "Modalità automatica",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "Colore",
|
||||
"Confirm": "Conferma",
|
||||
"Confirm Deletion": "Conferma eliminazione",
|
||||
"Copied to notebook": "Copiato nel quaderno",
|
||||
"Copy": "Copia",
|
||||
"Custom CSS": "CSS personalizzato",
|
||||
"Dark Mode": "Modalità scura",
|
||||
"Default": "Predefinito",
|
||||
"Default Font": "Font predefinito",
|
||||
"Default Font Size": "Dimensione font predefinita",
|
||||
"Delete": "Elimina",
|
||||
"Delete Highlight": "Elimina evidenziazione",
|
||||
"Dictionary": "Dizionario",
|
||||
"Disable Click-to-Flip": "Disattiva click per voltare pagina",
|
||||
"Download Readest": "Scarica Readest",
|
||||
"Edit": "Modifica",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "Erba",
|
||||
"Gray": "Grigio",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Evidenzia",
|
||||
"Horizontal Direction": "Direzione orizzontale",
|
||||
"Hyphenation": "Sillabazione",
|
||||
"Identifier:": "Identificatore:",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "Font sans-serif",
|
||||
"Save": "Salva",
|
||||
"Scrolled Mode": "Modalità scorrimento",
|
||||
"Search": "Cerca",
|
||||
"Search books...": "Cerca libri...",
|
||||
"Search...": "Cerca...",
|
||||
"Select Book": "Seleziona libro",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "Esci",
|
||||
"Sky": "Cielo",
|
||||
"Solarized": "Solarizzato",
|
||||
"Speak": "Leggi",
|
||||
"Subjects:": "Argomenti:",
|
||||
"System Fonts": "Font di sistema",
|
||||
"Theme Color": "Colore tema",
|
||||
"Theme Mode": "Modalità tema",
|
||||
"Translate": "Traduci",
|
||||
"Translated Text": "Testo tradotto",
|
||||
"Unknown": "Sconosciuto",
|
||||
"Untitled": "Senza titolo",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "Versione {{version}}",
|
||||
"Vertical Direction": "Direzione verticale",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Benvenuto nella tua biblioteca. Puoi importare i tuoi libri qui e leggerli in qualsiasi momento.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Modalità scrittura",
|
||||
"Your Library": "La tua biblioteca"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"About Readest": "Readestについて",
|
||||
"Add your notes here...": "ここにメモを追加...",
|
||||
"Animation": "アニメーション",
|
||||
"Annotate": "注釈",
|
||||
"Apply": "適用",
|
||||
"Are you sure to delete the selected books?": "選択した書籍を削除してもよろしいですか?",
|
||||
"Auto Mode": "自動モード",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "色",
|
||||
"Confirm": "確認",
|
||||
"Confirm Deletion": "削除の確認",
|
||||
"Copied to notebook": "ノートブックにコピー",
|
||||
"Copy": "コピー",
|
||||
"Custom CSS": "カスタムCSS",
|
||||
"Dark Mode": "ダークモード",
|
||||
"Default": "デフォルト",
|
||||
"Default Font": "デフォルトフォント",
|
||||
"Default Font Size": "デフォルトフォントサイズ",
|
||||
"Delete": "削除",
|
||||
"Delete Highlight": "ハイライトを削除",
|
||||
"Dictionary": "辞書",
|
||||
"Disable Click-to-Flip": "クリックめくりを無効化",
|
||||
"Download Readest": "Readestをダウンロード",
|
||||
"Edit": "編集",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "グリーン",
|
||||
"Gray": "グレー",
|
||||
"Gruvbox": "グルーブボックス",
|
||||
"Highlight": "ハイライト",
|
||||
"Horizontal Direction": "横書",
|
||||
"Hyphenation": "ハイフネーション",
|
||||
"Identifier:": "識別子:",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "ゴシック体",
|
||||
"Save": "保存",
|
||||
"Scrolled Mode": "スクロールモード",
|
||||
"Search": "検索",
|
||||
"Search books...": "書籍を検索...",
|
||||
"Search...": "検索...",
|
||||
"Select Book": "書籍を選択",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "サインアウト",
|
||||
"Sky": "スカイ",
|
||||
"Solarized": "ソーラライズド",
|
||||
"Speak": "読み上げ",
|
||||
"Subjects:": "主題:",
|
||||
"System Fonts": "システムフォント",
|
||||
"Theme Color": "テーマカラー",
|
||||
"Theme Mode": "テーマモード",
|
||||
"Translate": "翻訳",
|
||||
"Translated Text": "翻訳されたテキスト",
|
||||
"Unknown": "不明",
|
||||
"Untitled": "無題",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "バージョン {{version}}",
|
||||
"Vertical Direction": "縦書",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "ライブラリーへようこそ。ここに書籍をインポートして、いつでも読むことができます。",
|
||||
"Wikipedia": "ウィキペディア",
|
||||
"Writing Mode": "書き込みモード",
|
||||
"Your Library": "ライブラリー"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"About Readest": "Readest 정보",
|
||||
"Add your notes here...": "여기에 메모를 추가하세요...",
|
||||
"Animation": "애니메이션",
|
||||
"Annotate": "주석",
|
||||
"Apply": "적용",
|
||||
"Are you sure to delete the selected books?": "선택한 책을 삭제하시겠습니까?",
|
||||
"Auto Mode": "자동 모드",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "색상",
|
||||
"Confirm": "확인",
|
||||
"Confirm Deletion": "삭제 확인",
|
||||
"Copied to notebook": "노트북에 복사됨",
|
||||
"Copy": "복사",
|
||||
"Custom CSS": "사용자 정의 CSS",
|
||||
"Dark Mode": "다크 모드",
|
||||
"Default": "기본값",
|
||||
"Default Font": "기본 글꼴",
|
||||
"Default Font Size": "기본 글꼴 크기",
|
||||
"Delete": "삭제",
|
||||
"Delete Highlight": "하이라이트 삭제",
|
||||
"Dictionary": "사전",
|
||||
"Disable Click-to-Flip": "클릭 넘기기 비활성화",
|
||||
"Download Readest": "Readest 다운로드",
|
||||
"Edit": "편집",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "잔디색",
|
||||
"Gray": "회색",
|
||||
"Gruvbox": "그러브박스",
|
||||
"Highlight": "하이라이트",
|
||||
"Horizontal Direction": "수평 방향",
|
||||
"Hyphenation": "하이픈 넣기",
|
||||
"Identifier:": "식별자:",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "산세리프체",
|
||||
"Save": "저장",
|
||||
"Scrolled Mode": "스크롤 모드",
|
||||
"Search": "검색",
|
||||
"Search books...": "책 검색...",
|
||||
"Search...": "검색...",
|
||||
"Select Book": "책 선택",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "로그아웃",
|
||||
"Sky": "하늘색",
|
||||
"Solarized": "솔라라이즈드",
|
||||
"Speak": "말하기",
|
||||
"Subjects:": "주제:",
|
||||
"System Fonts": "시스템 글꼴",
|
||||
"Theme Color": "테마 색상",
|
||||
"Theme Mode": "테마 모드",
|
||||
"Translate": "번역",
|
||||
"Translated Text": "번역된 텍스트",
|
||||
"Unknown": "알 수 없음",
|
||||
"Untitled": "제목 없음",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "버전 {{version}}",
|
||||
"Vertical Direction": "수직 방향",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "내 서재에 오신 것을 환영합니다. 여기에서 책을 가져와 언제든지 읽을 수 있습니다.",
|
||||
"Wikipedia": "위키피디아",
|
||||
"Writing Mode": "글쓰기 모드",
|
||||
"Your Library": "내 서재"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"About Readest": "Sobre o Readest",
|
||||
"Add your notes here...": "Adicione suas notas aqui...",
|
||||
"Animation": "Animação",
|
||||
"Annotate": "Anotar",
|
||||
"Apply": "Aplicar",
|
||||
"Are you sure to delete the selected books?": "Tem certeza de que deseja excluir os livros selecionados?",
|
||||
"Auto Mode": "Modo Automático",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "Cor",
|
||||
"Confirm": "Confirmar",
|
||||
"Confirm Deletion": "Confirmar Exclusão",
|
||||
"Copied to notebook": "Copiado para o caderno",
|
||||
"Copy": "Copiar",
|
||||
"Custom CSS": "CSS Personalizado",
|
||||
"Dark Mode": "Modo Escuro",
|
||||
"Default": "Padrão",
|
||||
"Default Font": "Fonte Padrão",
|
||||
"Default Font Size": "Tamanho da Fonte Padrão",
|
||||
"Delete": "Excluir",
|
||||
"Delete Highlight": "Excluir Destaque",
|
||||
"Dictionary": "Dicionário",
|
||||
"Disable Click-to-Flip": "Desativar Clique para Virar",
|
||||
"Download Readest": "Baixar Readest",
|
||||
"Edit": "Editar",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "Grama",
|
||||
"Gray": "Cinza",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Destaque",
|
||||
"Horizontal Direction": "Direção Horizontal",
|
||||
"Hyphenation": "Hifenização",
|
||||
"Identifier:": "Identificador:",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "Fonte Sans-Serif",
|
||||
"Save": "Salvar",
|
||||
"Scrolled Mode": "Modo de Rolagem",
|
||||
"Search": "Buscar",
|
||||
"Search books...": "Procurar livros...",
|
||||
"Search...": "Pesquisar...",
|
||||
"Select Book": "Selecionar Livro",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "Sair",
|
||||
"Sky": "Céu",
|
||||
"Solarized": "Solarizado",
|
||||
"Speak": "Falar",
|
||||
"Subjects:": "Assuntos:",
|
||||
"System Fonts": "Fontes do Sistema",
|
||||
"Theme Color": "Cor do Tema",
|
||||
"Theme Mode": "Modo do Tema",
|
||||
"Translate": "Traduzir",
|
||||
"Translated Text": "Texto Traduzido",
|
||||
"Unknown": "Desconhecido",
|
||||
"Untitled": "Sem Título",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "Versão {{version}}",
|
||||
"Vertical Direction": "Direção Vertical",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Bem-vindo à sua biblioteca. Você pode importar seus livros aqui e lê-los a qualquer momento.",
|
||||
"Wikipedia": "Wikipédia",
|
||||
"Writing Mode": "Modo de Escrita",
|
||||
"Your Library": "Sua Biblioteca"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"About Readest": "О Readest",
|
||||
"Add your notes here...": "Добавьте свои заметки здесь...",
|
||||
"Animation": "Анимация",
|
||||
"Annotate": "Аннотация",
|
||||
"Apply": "Применить",
|
||||
"Are you sure to delete the selected books?": "Вы уверены, что хотите удалить выбранные книги?",
|
||||
"Auto Mode": "Автоматический режим",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "Цвет",
|
||||
"Confirm": "Подтвердить",
|
||||
"Confirm Deletion": "Подтвердить удаление",
|
||||
"Copied to notebook": "Скопировано в блокнот",
|
||||
"Copy": "Копировать",
|
||||
"Custom CSS": "Пользовательский CSS",
|
||||
"Dark Mode": "Тёмная тема",
|
||||
"Default": "По умолчанию",
|
||||
"Default Font": "Шрифт по умолчанию",
|
||||
"Default Font Size": "Размер шрифта по умолчанию",
|
||||
"Delete": "Удалить",
|
||||
"Delete Highlight": "Удалить выделение",
|
||||
"Dictionary": "Словарь",
|
||||
"Disable Click-to-Flip": "Отключить переворот по клику",
|
||||
"Download Readest": "Скачать Readest",
|
||||
"Edit": "Редактировать",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "Трава",
|
||||
"Gray": "Серый",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Выделить",
|
||||
"Horizontal Direction": "Горизонтальное направление",
|
||||
"Hyphenation": "Перенос слов",
|
||||
"Identifier:": "Идентификатор:",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "Шрифт без засечек",
|
||||
"Save": "Сохранить",
|
||||
"Scrolled Mode": "Режим прокрутки",
|
||||
"Search": "Поиск",
|
||||
"Search books...": "Поиск книг...",
|
||||
"Search...": "Поиск...",
|
||||
"Select Book": "Выбрать книгу",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "Выйти",
|
||||
"Sky": "Небесный",
|
||||
"Solarized": "Солнечный",
|
||||
"Speak": "Произнести",
|
||||
"Subjects:": "Темы:",
|
||||
"System Fonts": "Системные шрифты",
|
||||
"Theme Color": "Цвет темы",
|
||||
"Theme Mode": "Режим темы",
|
||||
"Translate": "Перевести",
|
||||
"Translated Text": "Переведённый текст",
|
||||
"Unknown": "Неизвестно",
|
||||
"Untitled": "Без названия",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "Версия {{version}}",
|
||||
"Vertical Direction": "Вертикальное направление",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Добро пожаловать в вашу библиотеку. Вы можете импортировать сюда книги и читать их в любое время.",
|
||||
"Wikipedia": "Википедия",
|
||||
"Writing Mode": "Режим записи",
|
||||
"Your Library": "Ваша библиотека"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"About Readest": "Readest Hakkında",
|
||||
"Add your notes here...": "Notlarınızı buraya ekleyin...",
|
||||
"Animation": "Animasyon",
|
||||
"Annotate": "Not Ekle",
|
||||
"Apply": "Uygula",
|
||||
"Are you sure to delete the selected books?": "Seçili kitapları silmek istediğinizden emin misiniz?",
|
||||
"Auto Mode": "Otomatik Mod",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "Renk",
|
||||
"Confirm": "Onayla",
|
||||
"Confirm Deletion": "Silmeyi Onayla",
|
||||
"Copied to notebook": "Not defterine kopyalandı",
|
||||
"Copy": "Kopyala",
|
||||
"Custom CSS": "Özel CSS",
|
||||
"Dark Mode": "Karanlık Mod",
|
||||
"Default": "Varsayılan",
|
||||
"Default Font": "Varsayılan Yazı Tipi",
|
||||
"Default Font Size": "Varsayılan Yazı Boyutu",
|
||||
"Delete": "Sil",
|
||||
"Delete Highlight": "Vurguyu Sil",
|
||||
"Dictionary": "Sözlük",
|
||||
"Disable Click-to-Flip": "Tıkla-Çevir'i Devre Dışı Bırak",
|
||||
"Download Readest": "Readest'i İndir",
|
||||
"Edit": "Düzenle",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "Çimen",
|
||||
"Gray": "Gri",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Vurgula",
|
||||
"Horizontal Direction": "Yatay Yön",
|
||||
"Hyphenation": "Heceleme",
|
||||
"Identifier:": "Tanımlayıcı:",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "Sans-Serif Yazı Tipi",
|
||||
"Save": "Kaydet",
|
||||
"Scrolled Mode": "Kaydırma Modu",
|
||||
"Search": "Ara",
|
||||
"Search books...": "Kitap ara...",
|
||||
"Search...": "Ara...",
|
||||
"Select Book": "Kitap Seç",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "Çıkış Yap",
|
||||
"Sky": "Gökyüzü",
|
||||
"Solarized": "Solarized",
|
||||
"Speak": "Konuş",
|
||||
"Subjects:": "Konular:",
|
||||
"System Fonts": "Sistem Yazı Tipleri",
|
||||
"Theme Color": "Tema Rengi",
|
||||
"Theme Mode": "Tema Modu",
|
||||
"Translate": "Çevir",
|
||||
"Translated Text": "Çevrilen Metin",
|
||||
"Unknown": "Bilinmiyor",
|
||||
"Untitled": "Başlıksız",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "Sürüm {{version}}",
|
||||
"Vertical Direction": "Dikey Yön",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Kütüphanenize hoş geldiniz. Buradan kitaplarınızı içe aktarabilir ve istediğiniz zaman okuyabilirsiniz.",
|
||||
"Wikipedia": "Vikipedi",
|
||||
"Writing Mode": "Yazma Modu",
|
||||
"Your Library": "Kütüphaneniz"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"About Readest": "Về Readest",
|
||||
"Add your notes here...": "Thêm ghi chú của bạn vào đây...",
|
||||
"Animation": "Hiệu ứng động",
|
||||
"Annotate": "Chú thích",
|
||||
"Apply": "Áp dụng",
|
||||
"Are you sure to delete the selected books?": "Bạn có chắc chắn muốn xóa các sách đã chọn?",
|
||||
"Auto Mode": "Chế độ tự động",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "Màu sắc",
|
||||
"Confirm": "Xác nhận",
|
||||
"Confirm Deletion": "Xác nhận xóa",
|
||||
"Copied to notebook": "Đã sao chép vào sổ ghi chú",
|
||||
"Copy": "Sao chép",
|
||||
"Custom CSS": "CSS tùy chỉnh",
|
||||
"Dark Mode": "Chế độ tối",
|
||||
"Default": "Mặc định",
|
||||
"Default Font": "Phông chữ mặc định",
|
||||
"Default Font Size": "Cỡ chữ mặc định",
|
||||
"Delete": "Xóa",
|
||||
"Delete Highlight": "Xóa văn bản được đánh dấu",
|
||||
"Dictionary": "Từ điển",
|
||||
"Disable Click-to-Flip": "Tắt tính năng nhấp để lật trang",
|
||||
"Download Readest": "Tải Readest",
|
||||
"Edit": "Chỉnh sửa",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "Xanh cỏ",
|
||||
"Gray": "Xám",
|
||||
"Gruvbox": "Gruvbox",
|
||||
"Highlight": "Đánh dấu",
|
||||
"Horizontal Direction": "Hướng ngang",
|
||||
"Hyphenation": "Gạch nối từ",
|
||||
"Identifier:": "Định danh:",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "Phông chữ không chân",
|
||||
"Save": "Lưu",
|
||||
"Scrolled Mode": "Chế độ cuộn",
|
||||
"Search": "Tìm kiếm",
|
||||
"Search books...": "Tìm kiếm sách...",
|
||||
"Search...": "Tìm kiếm...",
|
||||
"Select Book": "Chọn sách",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "Đăng xuất",
|
||||
"Sky": "Xanh trời",
|
||||
"Solarized": "Solarized",
|
||||
"Speak": "Đọc",
|
||||
"Subjects:": "Chủ đề:",
|
||||
"System Fonts": "Phông chữ hệ thống",
|
||||
"Theme Color": "Màu chủ đề",
|
||||
"Theme Mode": "Chế độ chủ đề",
|
||||
"Translate": "Dịch",
|
||||
"Translated Text": "Văn bản dịch",
|
||||
"Unknown": "Không xác định",
|
||||
"Untitled": "Không có tiêu đề",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "Phiên bản {{version}}",
|
||||
"Vertical Direction": "Hướng dọc",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "Chào mừng đến với thư viện của bạn. Bạn có thể nhập sách vào đây và đọc bất cứ lúc nào.",
|
||||
"Wikipedia": "Wikipedia",
|
||||
"Writing Mode": "Chế độ viết",
|
||||
"Your Library": "Thư viện của bạn"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"About Readest": "关于 Readest",
|
||||
"Add your notes here...": "在这里添加您的笔记...",
|
||||
"Animation": "动画",
|
||||
"Annotate": "笔记",
|
||||
"Apply": "应用",
|
||||
"Are you sure to delete the selected books?": "您确定要删除所选书籍吗?",
|
||||
"Auto Mode": "自动主题",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "颜色",
|
||||
"Confirm": "确认",
|
||||
"Confirm Deletion": "确认删除",
|
||||
"Copied to notebook": "已复制到笔记本",
|
||||
"Copy": "复制",
|
||||
"Custom CSS": "自定义 CSS",
|
||||
"Dark Mode": "深色主题",
|
||||
"Default": "默认",
|
||||
"Default Font": "默认字体",
|
||||
"Default Font Size": "默认字号",
|
||||
"Delete": "删除",
|
||||
"Delete Highlight": "删除划线",
|
||||
"Dictionary": "词典",
|
||||
"Disable Click-to-Flip": "禁用点击翻页",
|
||||
"Download Readest": "下载 Readest",
|
||||
"Edit": "编辑",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "青草",
|
||||
"Gray": "素雅",
|
||||
"Gruvbox": "暖橘",
|
||||
"Highlight": "划线",
|
||||
"Horizontal Direction": "横排",
|
||||
"Hyphenation": "断字",
|
||||
"Identifier:": "识别码",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "无衬线字体",
|
||||
"Save": "保存",
|
||||
"Scrolled Mode": "滚动模式",
|
||||
"Search": "搜索",
|
||||
"Search books...": "搜索书籍...",
|
||||
"Search...": "搜索...",
|
||||
"Select Book": "选择书籍",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "登出",
|
||||
"Sky": "天青",
|
||||
"Solarized": "日晖",
|
||||
"Speak": "朗读",
|
||||
"Subjects:": "主题",
|
||||
"System Fonts": "系统字体",
|
||||
"Theme Color": "主题颜色",
|
||||
"Theme Mode": "主题模式",
|
||||
"Translate": "翻译",
|
||||
"Translated Text": "译文",
|
||||
"Unknown": "未知",
|
||||
"Untitled": "无标题",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "版本 {{version}}",
|
||||
"Vertical Direction": "竖排",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "书库空空如也,您可以导入您的书籍并随时阅读。",
|
||||
"Wikipedia": "维基百科",
|
||||
"Writing Mode": "排版模式",
|
||||
"Your Library": "书库"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"About Readest": "關於 Readest",
|
||||
"Add your notes here...": "在這裡添加您的筆記...",
|
||||
"Animation": "動畫",
|
||||
"Annotate": "筆記",
|
||||
"Apply": "應用",
|
||||
"Are you sure to delete the selected books?": "您確定要刪除所選書籍嗎?",
|
||||
"Auto Mode": "自動主題",
|
||||
@@ -16,12 +17,16 @@
|
||||
"Color": "顏色",
|
||||
"Confirm": "確認",
|
||||
"Confirm Deletion": "確認刪除",
|
||||
"Copied to notebook": "已複製到筆記本",
|
||||
"Copy": "複製",
|
||||
"Custom CSS": "自定義 CSS",
|
||||
"Dark Mode": "深色主題",
|
||||
"Default": "預設",
|
||||
"Default Font": "預設字體",
|
||||
"Default Font Size": "預設字號",
|
||||
"Delete": "刪除",
|
||||
"Delete Highlight": "刪除劃線",
|
||||
"Dictionary": "詞典",
|
||||
"Disable Click-to-Flip": "禁用點擊翻頁",
|
||||
"Download Readest": "下載 Readest",
|
||||
"Edit": "編輯",
|
||||
@@ -44,6 +49,7 @@
|
||||
"Grass": "青草",
|
||||
"Gray": "素雅",
|
||||
"Gruvbox": "暖橘",
|
||||
"Highlight": "劃線",
|
||||
"Horizontal Direction": "橫排",
|
||||
"Hyphenation": "斷字",
|
||||
"Identifier:": "識別碼",
|
||||
@@ -89,6 +95,7 @@
|
||||
"Sans-Serif Font": "無襯線字體",
|
||||
"Save": "保存",
|
||||
"Scrolled Mode": "滾動模式",
|
||||
"Search": "搜索",
|
||||
"Search books...": "搜索書籍...",
|
||||
"Search...": "搜索...",
|
||||
"Select Book": "選擇書籍",
|
||||
@@ -102,10 +109,12 @@
|
||||
"Sign Out": "登出",
|
||||
"Sky": "天青",
|
||||
"Solarized": "日暉",
|
||||
"Speak": "朗讀",
|
||||
"Subjects:": "主題",
|
||||
"System Fonts": "系統字體",
|
||||
"Theme Color": "主題顏色",
|
||||
"Theme Mode": "主題模式",
|
||||
"Translate": "翻譯",
|
||||
"Translated Text": "譯文",
|
||||
"Unknown": "未知",
|
||||
"Untitled": "無標題",
|
||||
@@ -114,6 +123,7 @@
|
||||
"Version {{version}}": "版本 {{version}}",
|
||||
"Vertical Direction": "豎排",
|
||||
"Welcome to your library. You can import your books here and read them anytime.": "書庫空空如也,您可以導入您的書籍並隨時閱讀。",
|
||||
"Wikipedia": "維基百科",
|
||||
"Writing Mode": "排版模式",
|
||||
"Your Library": "書庫"
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import FoliateViewer from './FoliateViewer';
|
||||
import getGridTemplate from '@/utils/grid';
|
||||
import SectionInfo from './SectionInfo';
|
||||
@@ -11,7 +12,6 @@ import PageInfoView from './PageInfo';
|
||||
import Ribbon from './Ribbon';
|
||||
import SettingsDialog from './settings/SettingsDialog';
|
||||
import Annotator from './annotator/Annotator';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import FootnotePopup from './FootnotePopup';
|
||||
|
||||
interface BooksGridProps {
|
||||
@@ -49,7 +49,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
|
||||
<div
|
||||
id={`gridcell-${bookKey}`}
|
||||
key={bookKey}
|
||||
className='relative h-full w-full rounded-window overflow-hidden'
|
||||
className='rounded-window relative h-full w-full overflow-hidden'
|
||||
>
|
||||
{isBookmarked && <Ribbon width={marginGap} />}
|
||||
<HeaderBar
|
||||
|
||||
@@ -26,6 +26,7 @@ import Spinner from '@/components/Spinner';
|
||||
import SideBar from './sidebar/SideBar';
|
||||
import Notebook from './notebook/Notebook';
|
||||
import BooksGrid from './BooksGrid';
|
||||
import TTSControl from './tts/TTSControl';
|
||||
|
||||
const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ ids, settings }) => {
|
||||
const router = useRouter();
|
||||
@@ -154,6 +155,7 @@ const ReaderContent: React.FC<{ ids?: string; settings: SystemSettings }> = ({ i
|
||||
<div className='flex h-screen'>
|
||||
<SideBar onGoToLibrary={handleCloseBooksToLibrary} />
|
||||
<BooksGrid bookKeys={bookKeys} onCloseBook={handleCloseBook} />
|
||||
<TTSControl />
|
||||
<Notebook />
|
||||
{showDetailsBook && (
|
||||
<BookDetailModal
|
||||
|
||||
@@ -6,7 +6,8 @@ import { FaWikipediaW } from 'react-icons/fa';
|
||||
import { BsPencilSquare } from 'react-icons/bs';
|
||||
import { RiDeleteBinLine } from 'react-icons/ri';
|
||||
import { BsTranslate } from 'react-icons/bs';
|
||||
import { SiDeepl } from 'react-icons/si';
|
||||
import { TbHexagonLetterD } from 'react-icons/tb';
|
||||
import { FaHeadphones } from 'react-icons/fa6';
|
||||
|
||||
import * as CFI from 'foliate-js/epubcfi.js';
|
||||
import { Overlayer } from 'foliate-js/overlayer.js';
|
||||
@@ -17,6 +18,7 @@ import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
import { useFoliateEvents } from '../../hooks/useFoliateEvents';
|
||||
import { useNotesSync } from '../../hooks/useNotesSync';
|
||||
import { getPopupPosition, getPosition, Position, TextSelection } from '@/utils/sel';
|
||||
@@ -28,6 +30,7 @@ import WikipediaPopup from './WikipediaPopup';
|
||||
import DeepLPopup from './DeepLPopup';
|
||||
|
||||
const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const _ = useTranslation();
|
||||
const { envConfig } = useEnv();
|
||||
const { settings } = useSettingsStore();
|
||||
const { getConfig, saveConfig, getBookData, updateBooknotes } = useBookDataStore();
|
||||
@@ -222,7 +225,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!selection || !selection.text) return;
|
||||
setToastMessage('Copied to notebook');
|
||||
setToastMessage(_('Copied to notebook'));
|
||||
|
||||
const { booknotes: annotations = [] } = config;
|
||||
if (selection) navigator.clipboard.writeText(selection.text);
|
||||
@@ -334,19 +337,29 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
setShowDeepLPopup(true);
|
||||
};
|
||||
|
||||
const handleSpeakText = async () => {
|
||||
if (!selection || !selection.text) return;
|
||||
setShowAnnotPopup(false);
|
||||
|
||||
await view!.initTTS();
|
||||
const ssml = view!.tts.from(selection.range)!;
|
||||
eventDispatcher.dispatch('speak', { bookKey, ssml });
|
||||
};
|
||||
|
||||
const selectionAnnotated = selection?.annotated;
|
||||
const buttons = [
|
||||
{ tooltipText: 'Copy', Icon: FiCopy, onClick: handleCopy },
|
||||
{ tooltipText: _('Copy'), Icon: FiCopy, onClick: handleCopy },
|
||||
{
|
||||
tooltipText: selectionAnnotated ? 'Delete Highlight' : 'Highlight',
|
||||
tooltipText: selectionAnnotated ? _('Delete Highlight') : _('Highlight'),
|
||||
Icon: selectionAnnotated ? RiDeleteBinLine : PiHighlighterFill,
|
||||
onClick: handleHighlight,
|
||||
},
|
||||
{ tooltipText: 'Annotate', Icon: BsPencilSquare, onClick: handleAnnotate },
|
||||
{ tooltipText: 'Search', Icon: FiSearch, onClick: handleSearch },
|
||||
{ tooltipText: 'Dictionary', Icon: BsTranslate, onClick: handleDictionary },
|
||||
{ tooltipText: 'Translate', Icon: SiDeepl, onClick: handleTranslation },
|
||||
{ tooltipText: 'Wikipedia', Icon: FaWikipediaW, onClick: handleWikipedia },
|
||||
{ tooltipText: _('Annotate'), Icon: BsPencilSquare, onClick: handleAnnotate },
|
||||
{ tooltipText: _('Search'), Icon: FiSearch, onClick: handleSearch },
|
||||
{ tooltipText: _('Dictionary'), Icon: TbHexagonLetterD, onClick: handleDictionary },
|
||||
{ tooltipText: _('Translate'), Icon: BsTranslate, onClick: handleTranslation },
|
||||
{ tooltipText: _('Wikipedia'), Icon: FaWikipediaW, onClick: handleWikipedia },
|
||||
{ tooltipText: _('Speak'), Icon: FaHeadphones, onClick: handleSpeakText },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -51,7 +51,8 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'h-4 w-4 p-0 text-center leading-none',
|
||||
'w-4 p-0 text-center leading-none',
|
||||
style === 'highlight' ? 'h-4' : 'h-5',
|
||||
style === 'highlight' &&
|
||||
(selectedStyle === 'highlight' ? `bg-${selectedColor}-400` : `bg-gray-300`),
|
||||
(style === 'underline' || style === 'squiggly') &&
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { TTSController } from '@/services/tts/TTSController';
|
||||
import { WebSpeechClient } from '@/services/tts';
|
||||
import { getPopupPosition, Position } from '@/utils/sel';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import Popup from '@/components/Popup';
|
||||
import TTSPanel from './TTSPanel';
|
||||
import TTSIcon from './TTSIcon';
|
||||
|
||||
const POPUP_WIDTH = 260;
|
||||
const POPUP_HEIGHT = 80;
|
||||
const POPUP_PADDING = 10;
|
||||
|
||||
const TTSControl = () => {
|
||||
const { getView } = useReaderStore();
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [showPanel, setShowPanel] = useState(false);
|
||||
const [panelPosition, setPanelPosition] = useState<Position>();
|
||||
const [trianglePosition, setTrianglePosition] = useState<Position>();
|
||||
|
||||
const iconRef = useRef<HTMLDivElement>(null);
|
||||
const ttsControllerRef = useRef<TTSController | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (ttsControllerRef.current) {
|
||||
ttsControllerRef.current.kill();
|
||||
ttsControllerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
eventDispatcher.on('speak', handleSpeak);
|
||||
return () => {
|
||||
eventDispatcher.off('speak', handleSpeak);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleSpeak = async (event: CustomEvent) => {
|
||||
const { bookKey, ssml } = event.detail;
|
||||
const view = getView(bookKey);
|
||||
if (!view) return;
|
||||
|
||||
const ttsClient = new WebSpeechClient();
|
||||
const ttsController = new TTSController(ttsClient, view);
|
||||
ttsControllerRef.current = ttsController;
|
||||
ttsControllerRef.current.speak(ssml);
|
||||
setIsPlaying(true);
|
||||
};
|
||||
|
||||
const handleTogglePlay = async () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (!ttsController) return;
|
||||
|
||||
if (isPlaying) {
|
||||
ttsController.pause();
|
||||
setIsPlaying(false);
|
||||
} else {
|
||||
await ttsController.start();
|
||||
setIsPlaying(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackward = async () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
await ttsController.backward();
|
||||
}
|
||||
};
|
||||
|
||||
const handleForward = async () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
await ttsController.forward();
|
||||
}
|
||||
};
|
||||
|
||||
const handleStop = async () => {
|
||||
const ttsController = ttsControllerRef.current;
|
||||
if (ttsController) {
|
||||
ttsController.stop();
|
||||
ttsControllerRef.current = null;
|
||||
setIsPlaying(false);
|
||||
setShowPanel(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePanelPosition = () => {
|
||||
if (iconRef.current) {
|
||||
const rect = iconRef.current.getBoundingClientRect();
|
||||
const windowRect = document.documentElement.getBoundingClientRect();
|
||||
|
||||
const trianglePos = {
|
||||
dir: 'up',
|
||||
point: { x: rect.left + rect.width / 2, y: rect.top - 12 },
|
||||
} as Position;
|
||||
|
||||
const popupPos = getPopupPosition(
|
||||
trianglePos,
|
||||
windowRect,
|
||||
POPUP_WIDTH,
|
||||
POPUP_HEIGHT,
|
||||
POPUP_PADDING,
|
||||
);
|
||||
|
||||
setPanelPosition(popupPos);
|
||||
setTrianglePosition(trianglePos);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePopup = () => {
|
||||
updatePanelPosition();
|
||||
setShowPanel((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ttsControllerRef.current && (
|
||||
<div ref={iconRef} className='absolute bottom-12 right-6 h-12 w-12'>
|
||||
<TTSIcon isPlaying={isPlaying} onClick={togglePopup} />
|
||||
</div>
|
||||
)}
|
||||
{showPanel && panelPosition && trianglePosition && (
|
||||
<Popup
|
||||
width={POPUP_WIDTH}
|
||||
height={POPUP_HEIGHT}
|
||||
position={panelPosition}
|
||||
trianglePosition={trianglePosition}
|
||||
className='bg-base-200 absolute flex shadow-lg'
|
||||
>
|
||||
<TTSPanel
|
||||
isPlaying={isPlaying}
|
||||
onTogglePlay={handleTogglePlay}
|
||||
onBackward={handleBackward}
|
||||
onForward={handleForward}
|
||||
onStop={handleStop}
|
||||
/>
|
||||
</Popup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TTSControl;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
|
||||
type TTSIconProps = {
|
||||
isPlaying: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
const TTSIcon: React.FC<TTSIconProps> = ({ isPlaying, onClick }) => {
|
||||
const bars = [1, 2, 3, 4];
|
||||
|
||||
return (
|
||||
<div className='relative h-full w-full cursor-pointer' onClick={onClick}>
|
||||
<div className='absolute inset-0 overflow-hidden rounded-full bg-gradient-to-r from-blue-500 via-emerald-500 to-violet-500'>
|
||||
<div
|
||||
className='absolute -inset-full bg-gradient-to-r from-blue-500 via-emerald-500 to-violet-500'
|
||||
style={{
|
||||
animation: isPlaying ? 'moveGradient 2s alternate infinite' : 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='absolute inset-0 flex items-center justify-center'>
|
||||
<style>{`
|
||||
@keyframes moveGradient {
|
||||
0% { transform: translate(0, 0); }
|
||||
100% { transform: translate(25%, 25%); }
|
||||
}
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: scaleY(1); }
|
||||
50% { transform: scaleY(0.6); }
|
||||
}
|
||||
`}</style>
|
||||
<div className='flex items-end space-x-1'>
|
||||
{bars.map((bar) => (
|
||||
<div
|
||||
key={bar}
|
||||
className='w-1 rounded-t bg-white'
|
||||
style={{
|
||||
height: '16px',
|
||||
animationName: isPlaying ? 'bounce' : 'none',
|
||||
animationDuration: isPlaying ? `${1 + bar * 0.1}s` : '0s',
|
||||
animationTimingFunction: 'ease-in-out',
|
||||
animationIterationCount: 'infinite',
|
||||
animationDelay: `${bar * 0.1}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TTSIcon;
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { MdPlayCircle, MdPauseCircle, MdFastRewind, MdFastForward, MdStop } from 'react-icons/md';
|
||||
|
||||
type TTSPanelProps = {
|
||||
isPlaying: boolean;
|
||||
onTogglePlay: () => void;
|
||||
onBackward: () => void;
|
||||
onForward: () => void;
|
||||
onStop: () => void;
|
||||
};
|
||||
|
||||
const TTSPanel = ({ isPlaying, onTogglePlay, onBackward, onForward, onStop }: TTSPanelProps) => {
|
||||
return (
|
||||
<div className='flex w-full items-center justify-center rounded-2xl p-4'>
|
||||
<div className='flex items-center justify-between space-x-4'>
|
||||
<button onClick={onBackward} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
<MdFastRewind size={32} />
|
||||
</button>
|
||||
<button onClick={onTogglePlay} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
{isPlaying ? (
|
||||
<MdPauseCircle size={48} className='fill-primary' />
|
||||
) : (
|
||||
<MdPlayCircle size={48} className='fill-primary' />
|
||||
)}
|
||||
</button>
|
||||
<button onClick={onForward} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
<MdFastForward size={32} />
|
||||
</button>
|
||||
<button onClick={onStop} className='hover:bg-base-200/75 rounded-full p-1'>
|
||||
<MdStop size={32} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TTSPanel;
|
||||
@@ -0,0 +1,17 @@
|
||||
export type TTSMessageCode = 'boundary' | 'error' | 'end';
|
||||
|
||||
export interface TTSMessageEvent {
|
||||
code: TTSMessageCode;
|
||||
message?: string;
|
||||
mark?: string;
|
||||
}
|
||||
|
||||
export interface TTSClient {
|
||||
init(): Promise<void>;
|
||||
speak(ssml: string): Promise<AsyncIterable<TTSMessageEvent>>;
|
||||
pause(): Promise<void>;
|
||||
resume(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
setRate(rate: number): Promise<void>;
|
||||
setPitch(pitch: number): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { FoliateView } from '@/types/view';
|
||||
import { TTSClient, TTSMessageCode } from './TTSClient';
|
||||
|
||||
type TTSState = 'stopped' | 'playing' | 'paused';
|
||||
|
||||
export class TTSController extends EventTarget {
|
||||
state: TTSState = 'stopped';
|
||||
ttsClient: TTSClient;
|
||||
view: FoliateView;
|
||||
#nossmlCnt: number = 0;
|
||||
|
||||
constructor(ttsClient: TTSClient, view: FoliateView) {
|
||||
super();
|
||||
this.ttsClient = ttsClient;
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
async #init() {
|
||||
await this.ttsClient.stop();
|
||||
await this.view.initTTS();
|
||||
}
|
||||
|
||||
async #speak(ssml: string | undefined | Promise<string>) {
|
||||
console.log('TTS speak');
|
||||
this.state = 'playing';
|
||||
ssml = await ssml;
|
||||
if (!ssml) {
|
||||
this.#nossmlCnt++;
|
||||
// FIXME: in case we are at the end of the book, need a better way to handle this
|
||||
if (this.#nossmlCnt < 100) {
|
||||
await this.view.next(1);
|
||||
this.forward();
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
this.#nossmlCnt = 0;
|
||||
}
|
||||
|
||||
const iter = await this.ttsClient.speak(ssml);
|
||||
let lastCode: TTSMessageCode = 'boundary';
|
||||
for await (const { code, mark } of iter) {
|
||||
if (mark) {
|
||||
this.view.tts.setMark(mark);
|
||||
}
|
||||
lastCode = code;
|
||||
}
|
||||
|
||||
if (lastCode === 'end') {
|
||||
this.forward();
|
||||
} else {
|
||||
this.state = 'paused';
|
||||
}
|
||||
}
|
||||
|
||||
async speak(ssml: string | Promise<string>) {
|
||||
await this.#init();
|
||||
this.#speak(ssml).catch((e) => this.error(e));
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.state !== 'playing') {
|
||||
this.start();
|
||||
} else {
|
||||
this.pause();
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.#init();
|
||||
const resumeOrStart = this.state === 'paused' ? this.view.tts.resume() : this.view.tts.start();
|
||||
return this.#speak(resumeOrStart);
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.state = 'paused';
|
||||
this.ttsClient.stop().catch((e) => this.error(e));
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.state = 'stopped';
|
||||
this.ttsClient.stop().catch((e) => this.error(e));
|
||||
}
|
||||
|
||||
// goto previous sentence
|
||||
async backward() {
|
||||
this.#init();
|
||||
if (this.state === 'playing') {
|
||||
this.#speak(this.view.tts.prev());
|
||||
} else {
|
||||
this.state = 'paused';
|
||||
this.view.tts.prev(true);
|
||||
}
|
||||
}
|
||||
|
||||
// goto next sentence
|
||||
async forward() {
|
||||
await this.#init();
|
||||
if (this.state === 'playing') {
|
||||
this.#speak(this.view.tts.next());
|
||||
} else {
|
||||
this.state = 'paused';
|
||||
this.view.tts.next(true);
|
||||
}
|
||||
}
|
||||
|
||||
error(e: unknown) {
|
||||
console.error(e);
|
||||
this.state = 'stopped';
|
||||
}
|
||||
|
||||
kill() {
|
||||
this.state = 'stopped';
|
||||
this.ttsClient.stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { TTSClient, TTSMessageEvent } from './TTSClient';
|
||||
import { AsyncQueue } from '@/utils/queue';
|
||||
|
||||
interface TTSBoundaryEvent {
|
||||
type: 'boundary' | 'end' | 'error';
|
||||
speaking: boolean;
|
||||
name?: string;
|
||||
mark?: string;
|
||||
charIndex?: number;
|
||||
charLength?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
type TTSMark = {
|
||||
offset: number;
|
||||
name: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
function getXmlLang(ssml: string): string | null {
|
||||
const match = ssml.match(/xml:lang\s*=\s*"([^"]+)"/);
|
||||
return match ? match[1]! : null;
|
||||
}
|
||||
|
||||
function parseSSMLMarks(ssml: string) {
|
||||
ssml = ssml.replace(/<speak[^>]*>/i, '');
|
||||
ssml = ssml.replace(/<\/speak>/i, '');
|
||||
|
||||
const markRegex = /<mark\s+name="([^"]+)"\s*\/>/g;
|
||||
let plainText = '';
|
||||
const marks: TTSMark[] = [];
|
||||
|
||||
let match;
|
||||
while ((match = markRegex.exec(ssml)) !== null) {
|
||||
const markTagEndIndex = markRegex.lastIndex;
|
||||
const nextMarkIndex = ssml.indexOf('<mark', markTagEndIndex);
|
||||
const nextChunk = ssml.slice(
|
||||
markTagEndIndex,
|
||||
nextMarkIndex !== -1 ? nextMarkIndex : ssml.length,
|
||||
);
|
||||
const cleanedChunk = nextChunk.replace(/<[^>]+>/g, '').trimStart();
|
||||
plainText += cleanedChunk;
|
||||
|
||||
const offset = plainText.length - cleanedChunk.length;
|
||||
const markName = match[1]!;
|
||||
marks.push({ offset, name: markName, text: cleanedChunk });
|
||||
}
|
||||
|
||||
return { plainText, marks };
|
||||
}
|
||||
|
||||
function findMark(charIndex: number, marks: TTSMark[]) {
|
||||
let left = 0;
|
||||
let right = marks.length - 1;
|
||||
let result: TTSMark | null = null;
|
||||
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
const mark = marks[mid]!;
|
||||
|
||||
if (mark.offset <= charIndex) {
|
||||
result = mark;
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function* speakWithBoundary(ssml: string) {
|
||||
const lang = getXmlLang(ssml);
|
||||
const { plainText, marks } = parseSSMLMarks(ssml);
|
||||
// console.log('text', plainText, marks);
|
||||
|
||||
const synth = window.speechSynthesis;
|
||||
const utterance = new SpeechSynthesisUtterance(plainText);
|
||||
|
||||
if (lang) {
|
||||
utterance.lang = lang;
|
||||
}
|
||||
|
||||
const queue = new AsyncQueue<TTSBoundaryEvent>();
|
||||
|
||||
utterance.onboundary = (event: SpeechSynthesisEvent) => {
|
||||
const mark = findMark(event.charIndex, marks);
|
||||
// console.log('boundary', mark);
|
||||
queue.enqueue({
|
||||
type: 'boundary',
|
||||
speaking: true,
|
||||
name: event.name,
|
||||
mark: mark?.name ?? '',
|
||||
charIndex: event.charIndex,
|
||||
charLength: event.charLength,
|
||||
});
|
||||
};
|
||||
|
||||
utterance.onend = () => {
|
||||
queue.enqueue({ type: 'end', speaking: false });
|
||||
queue.finish();
|
||||
};
|
||||
|
||||
utterance.onerror = (event) => {
|
||||
queue.enqueue({ type: 'error', speaking: false, error: event.error });
|
||||
queue.finish();
|
||||
};
|
||||
|
||||
synth.speak(utterance);
|
||||
|
||||
while (true) {
|
||||
const ev = await queue.dequeue();
|
||||
if (ev === null) {
|
||||
break;
|
||||
}
|
||||
yield ev;
|
||||
}
|
||||
}
|
||||
|
||||
export class WebSpeechClient implements TTSClient {
|
||||
#utterance: SpeechSynthesisUtterance | null = null;
|
||||
#synth = window.speechSynthesis;
|
||||
|
||||
async init() {
|
||||
if (!this.#synth) {
|
||||
throw new Error('Web Speech API not supported in this browser');
|
||||
}
|
||||
}
|
||||
|
||||
async speak(ssml: string) {
|
||||
await this.stop();
|
||||
|
||||
const generator = (async function* () {
|
||||
for await (const ev of speakWithBoundary(ssml)) {
|
||||
if (ev.type === 'boundary') {
|
||||
yield {
|
||||
code: 'boundary',
|
||||
mark: ev.mark ?? '',
|
||||
message: `${ev.name} ${ev.charIndex}/${ev.charLength}`,
|
||||
} as TTSMessageEvent;
|
||||
} else if (ev.type === 'error') {
|
||||
yield { code: 'error', message: ev.error } as TTSMessageEvent;
|
||||
} else if (ev.type === 'end') {
|
||||
yield { code: 'end', message: 'Speech finished' } as TTSMessageEvent;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return generator;
|
||||
}
|
||||
|
||||
async pause() {
|
||||
this.#synth.pause();
|
||||
}
|
||||
|
||||
async resume() {
|
||||
this.#synth.resume();
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.#synth.cancel();
|
||||
}
|
||||
|
||||
async setRate(rate: number) {
|
||||
// The Web Speech API uses utterance.rate in [0.1 .. 10],
|
||||
// you might need to scale your “rate” to match.
|
||||
if (this.#utterance) this.#utterance.rate = rate / 10;
|
||||
}
|
||||
|
||||
async setPitch(pitch: number) {
|
||||
// The Web Speech API uses pitch in [0 .. 2].
|
||||
if (this.#utterance) this.#utterance.pitch = pitch / 50;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './TTSClient';
|
||||
export * from './WebSpeechClient';
|
||||
export * from './TTSController';
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BookDoc } from '@/libs/document';
|
||||
import { BookNote, BookSearchConfig, BookSearchResult } from '@/types/book';
|
||||
import { TTS } from 'foliate-js/tts.js';
|
||||
|
||||
export interface FoliateView extends HTMLElement {
|
||||
open: (book: BookDoc) => Promise<void>;
|
||||
@@ -17,6 +18,8 @@ export interface FoliateView extends HTMLElement {
|
||||
clearSearch: () => void;
|
||||
select: (target: string | number | { fraction: number }) => void;
|
||||
deselect: () => void;
|
||||
initTTS: () => Promise<void>;
|
||||
tts: TTS;
|
||||
history: {
|
||||
canGoBack: boolean;
|
||||
canGoForward: boolean;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
export class AsyncQueue<T> {
|
||||
private queue: T[] = [];
|
||||
private resolves: ((value: void) => void)[] = [];
|
||||
private done = false;
|
||||
|
||||
enqueue(item: T) {
|
||||
this.queue.push(item);
|
||||
if (this.resolves.length > 0) {
|
||||
const resolve = this.resolves.shift();
|
||||
resolve?.();
|
||||
}
|
||||
}
|
||||
|
||||
finish() {
|
||||
this.done = true;
|
||||
while (this.resolves.length > 0) {
|
||||
const resolve = this.resolves.shift();
|
||||
resolve?.();
|
||||
}
|
||||
}
|
||||
|
||||
async dequeue(): Promise<T | null> {
|
||||
if (this.queue.length > 0) {
|
||||
return this.queue.shift()!;
|
||||
}
|
||||
if (this.done) {
|
||||
return null;
|
||||
}
|
||||
return new Promise<T | null>((resolve) => {
|
||||
this.resolves.push(() => {
|
||||
if (this.queue.length > 0) {
|
||||
resolve(this.queue.shift()!);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
+1
-1
Submodule packages/foliate-js updated: fcc449c089...aa5f950b1f
Reference in New Issue
Block a user