Let's add speech
This commit is contained in:
parent
33b55c937b
commit
92d6fe7ebe
4 changed files with 93 additions and 30 deletions
|
@ -104,6 +104,7 @@ export const ICONS = {
|
||||||
cloud: () => import('@iconify-icons/mingcute/cloud-line'),
|
cloud: () => import('@iconify-icons/mingcute/cloud-line'),
|
||||||
month: () => import('@iconify-icons/mingcute/calendar-month-line'),
|
month: () => import('@iconify-icons/mingcute/calendar-month-line'),
|
||||||
media: () => import('@iconify-icons/mingcute/photo-album-line'),
|
media: () => import('@iconify-icons/mingcute/photo-album-line'),
|
||||||
|
speak: () => import('@iconify-icons/mingcute/radar-line'),
|
||||||
};
|
};
|
||||||
|
|
||||||
function Icon({
|
function Icon({
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
import getTranslateTargetLanguage from '../utils/get-translate-target-language';
|
||||||
import localeMatch from '../utils/locale-match';
|
import localeMatch from '../utils/locale-match';
|
||||||
|
import { speak, supportsTTS } from '../utils/speech';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
|
||||||
import Icon from './icon';
|
import Icon from './icon';
|
||||||
|
@ -51,6 +52,16 @@ export default function MediaAltModal({ alt, lang, onClose }) {
|
||||||
<Icon icon="translate" />
|
<Icon icon="translate" />
|
||||||
<span>Translate</span>
|
<span>Translate</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{supportsTTS && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
speak(alt, lang);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="speak" />
|
||||||
|
<span>Speak</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</Menu2>
|
</Menu2>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -63,6 +63,7 @@ import Media from './media';
|
||||||
import { isMediaCaptionLong } from './media';
|
import { isMediaCaptionLong } from './media';
|
||||||
import MenuLink from './menu-link';
|
import MenuLink from './menu-link';
|
||||||
import RelativeTime from './relative-time';
|
import RelativeTime from './relative-time';
|
||||||
|
import { speak, supportsTTS } from '../utils/speech';
|
||||||
import TranslationBlock from './translation-block';
|
import TranslationBlock from './translation-block';
|
||||||
|
|
||||||
const SHOW_COMMENT_COUNT_LIMIT = 280;
|
const SHOW_COMMENT_COUNT_LIMIT = 280;
|
||||||
|
@ -90,6 +91,26 @@ const isIOS =
|
||||||
|
|
||||||
const REACTIONS_LIMIT = 80;
|
const REACTIONS_LIMIT = 80;
|
||||||
|
|
||||||
|
function getPollText(poll) {
|
||||||
|
if (!poll?.options?.length) return '';
|
||||||
|
return `📊:\n${poll.options
|
||||||
|
.map(
|
||||||
|
(option) =>
|
||||||
|
`- ${option.title}${
|
||||||
|
option.votesCount >= 0 ? ` (${option.votesCount})` : ''
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
.join('\n')}`;
|
||||||
|
}
|
||||||
|
function getPostText(status) {
|
||||||
|
const { spoilerText, content, poll } = status;
|
||||||
|
return (
|
||||||
|
(spoilerText ? `${spoilerText}\n\n` : '') +
|
||||||
|
getHTMLText(content) +
|
||||||
|
getPollText(poll)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Status({
|
function Status({
|
||||||
statusID,
|
statusID,
|
||||||
status,
|
status,
|
||||||
|
@ -782,6 +803,7 @@ function Status({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{enableTranslate ? (
|
{enableTranslate ? (
|
||||||
|
<div class={supportsTTS ? 'menu-horizontal' : ''}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={forceTranslate}
|
disabled={forceTranslate}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -791,14 +813,43 @@ function Status({
|
||||||
<Icon icon="translate" />
|
<Icon icon="translate" />
|
||||||
<span>Translate</span>
|
<span>Translate</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{supportsTTS && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
const postText = getPostText(status);
|
||||||
|
if (postText) {
|
||||||
|
speak(postText, language);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="speak" />
|
||||||
|
<span>Speak</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
(!language || differentLanguage) && (
|
(!language || differentLanguage) && (
|
||||||
|
<div class={supportsTTS ? 'menu-horizontal' : ''}>
|
||||||
<MenuLink
|
<MenuLink
|
||||||
to={`${instance ? `/${instance}` : ''}/s/${id}?translate=1`}
|
to={`${instance ? `/${instance}` : ''}/s/${id}?translate=1`}
|
||||||
>
|
>
|
||||||
<Icon icon="translate" />
|
<Icon icon="translate" />
|
||||||
<span>Translate</span>
|
<span>Translate</span>
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
|
{supportsTTS && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
const postText = getPostText(status);
|
||||||
|
if (postText) {
|
||||||
|
speak(postText, language);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="speak" />
|
||||||
|
<span>Speak</span>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{((!isSizeLarge && sameInstance) || enableTranslate) && <MenuDivider />}
|
{((!isSizeLarge && sameInstance) || enableTranslate) && <MenuDivider />}
|
||||||
|
@ -1578,22 +1629,7 @@ function Status({
|
||||||
forceTranslate={forceTranslate || inlineTranslate}
|
forceTranslate={forceTranslate || inlineTranslate}
|
||||||
mini={!isSizeLarge && !withinContext}
|
mini={!isSizeLarge && !withinContext}
|
||||||
sourceLanguage={language}
|
sourceLanguage={language}
|
||||||
text={
|
text={getPostText(status)}
|
||||||
(spoilerText ? `${spoilerText}\n\n` : '') +
|
|
||||||
getHTMLText(content) +
|
|
||||||
(poll?.options?.length
|
|
||||||
? `\n\nPoll:\n${poll.options
|
|
||||||
.map(
|
|
||||||
(option) =>
|
|
||||||
`- ${option.title}${
|
|
||||||
option.votesCount >= 0
|
|
||||||
? ` (${option.votesCount})`
|
|
||||||
: ''
|
|
||||||
}`,
|
|
||||||
)
|
|
||||||
.join('\n')}`
|
|
||||||
: '')
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!spoilerText && sensitive && !!mediaAttachments.length && (
|
{!spoilerText && sensitive && !!mediaAttachments.length && (
|
||||||
|
|
15
src/utils/speech.js
Normal file
15
src/utils/speech.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export const supportsTTS = 'speechSynthesis' in window;
|
||||||
|
|
||||||
|
export function speak(text, lang) {
|
||||||
|
if (!supportsTTS) return;
|
||||||
|
try {
|
||||||
|
if (speechSynthesis.speaking) {
|
||||||
|
speechSynthesis.cancel();
|
||||||
|
}
|
||||||
|
const utterance = new SpeechSynthesisUtterance(text);
|
||||||
|
if (lang) utterance.lang = lang;
|
||||||
|
speechSynthesis.speak(utterance);
|
||||||
|
} catch (e) {
|
||||||
|
alert(e);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue