Add listen button

This commit is contained in:
sienori 2020-05-01 23:35:48 +09:00
parent 32dd96d03c
commit c162de041d
13 changed files with 162 additions and 81 deletions

View file

@ -33,6 +33,9 @@
"copiedLabel": {
"message": "Copied."
},
"listenLabel": {
"message": "Listen"
},
"targetLangLabel": {
"message": "Target language"
},

View file

@ -0,0 +1,38 @@
import React, { Component } from "react";
import browser from "webextension-polyfill";
import { CopyToClipboard } from "react-copy-to-clipboard";
import CopyIcon from "../icons/copy.svg";
import "../styles/CopyButton.scss";
export default class CopyButton extends Component {
constructor(props) {
super(props);
this.state = { isCopied: false };
}
handleCopy = () => {
this.setState({ isCopied: true });
};
componentWillReceiveProps(nextProps) {
if (this.props.text !== nextProps.text) this.setState({ isCopied: false });
}
render() {
const { text } = this.props;
if (!text) return null;
return (
<div className="copy">
{this.state.isCopied && (
<span className="copiedText">{browser.i18n.getMessage("copiedLabel")}</span>
)}
<CopyToClipboard text={text} onCopy={this.handleCopy}>
<button className="copyButton" title={browser.i18n.getMessage("copyLabel")}>
<CopyIcon />
</button>
</CopyToClipboard>
</div>
);
}
}

View file

@ -1,6 +1,7 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import browser from "webextension-polyfill";
import ListenButton from "./ListenButton";
import "../styles/inputArea.scss";
export default class InputArea extends Component {
@ -16,7 +17,9 @@ export default class InputArea extends Component {
};
shouldComponentUpdate(nextProps) {
const shouldUpdate = this.props.inputText !== nextProps.inputText;
const shouldUpdate =
this.props.inputText !== nextProps.inputText ||
this.props.sourceLang !== nextProps.sourceLang;
return shouldUpdate;
}
@ -25,16 +28,20 @@ export default class InputArea extends Component {
};
render() {
const { inputText, sourceLang } = this.props;
return (
<div id="inputArea">
<textarea
value={this.props.inputText}
value={inputText}
ref="textarea"
placeholder={browser.i18n.getMessage("initialTextArea")}
onChange={this.handleInputText}
autoFocus
spellCheck={false}
/>
<div className="listen">
{sourceLang && <ListenButton text={inputText} lang={sourceLang} />}
</div>
</div>
);
}

View file

@ -0,0 +1,32 @@
import React from "react";
import browser from "webextension-polyfill";
import log from "loglevel";
import SpeakerIcon from "../icons/speaker.svg";
import "../styles/ListenButton.scss";
const logDir = "popup/AudioButton";
const playAudio = async (text, lang) => {
const url = `https://translate.google.com/translate_tts?client=tw-ob&q=${encodeURIComponent(
text
)}&tl=${lang}`;
const audio = new Audio(url);
audio.load();
await audio.play().catch(e => log.error(logDir, "playAudio()", e, url));
};
export default props => {
const { text, lang } = props;
const canListen = text && text.length < 200;
if (!canListen) return null;
return (
<button
className="listenButton"
onClick={() => playAudio(text, lang)}
title={browser.i18n.getMessage("listenLabel")}
>
<SpeakerIcon />
</button>
);
};

View file

@ -1,40 +0,0 @@
import React, { Component } from "react";
import browser from "webextension-polyfill";
import { CopyToClipboard } from "react-copy-to-clipboard";
import CopyIcon from "../icons/copy.svg";
import "../styles/MediaButtons.scss";
export default class CopyButton extends Component {
constructor(props) {
super(props);
this.state = { isCopied: false };
}
handleCopy = () => {
this.setState({ isCopied: true });
};
componentWillReceiveProps(nextProps) {
if (this.props.resultText !== nextProps.resultText) this.setState({ isCopied: false });
}
render() {
const { resultText } = this.props;
return (
resultText && (
<div className="mediaButtons">
<div className="copy">
{this.state.isCopied && (
<span className="copiedText">{browser.i18n.getMessage("copiedLabel")}</span>
)}
<CopyToClipboard text={resultText} onCopy={this.handleCopy}>
<button className="copyButton" title={browser.i18n.getMessage("copyLabel")}>
<CopyIcon />
</button>
</CopyToClipboard>
</div>
</div>
)
);
}
}

View file

@ -39,6 +39,7 @@ export default class PopupPage extends Component {
inputText: "",
resultText: "",
candidateText: "",
sourceLang: "",
statusText: "OK",
tabUrl: "",
isConnected: true,
@ -93,7 +94,8 @@ export default class PopupPage extends Component {
this.setState({
resultText: result.resultText,
candidateText: result.candidateText,
statusText: result.statusText
statusText: result.statusText,
sourceLang: result.sourceLanguage
});
return result;
};
@ -143,7 +145,11 @@ export default class PopupPage extends Component {
isEnabledOnPage={this.state.isEnabledOnPage}
isConnected={this.state.isConnected}
/>
<InputArea inputText={this.state.inputText} handleInputText={this.handleInputText} />
<InputArea
inputText={this.state.inputText}
handleInputText={this.handleInputText}
sourceLang={this.state.sourceLang}
/>
<hr />
<ResultArea
inputText={this.state.inputText}

View file

@ -3,8 +3,9 @@ import browser from "webextension-polyfill";
import getErrorMessage from "src/common/getErrorMessage";
import { getSettings } from "src/settings/settings";
import openUrl from "src/common/openUrl";
import CopyButton from "./CopyButton";
import ListenButton from "./ListenButton";
import "../styles/ResultArea.scss";
import MediaButtons from "./MediaButtons";
const splitLine = text => {
const regex = /(\n)/g;
@ -12,7 +13,7 @@ const splitLine = text => {
};
export default props => {
const { resultText, candidateText, statusText } = props;
const { resultText, candidateText, statusText, targetLang } = props;
const isError = statusText !== "OK";
const shouldShowCandidate = getSettings("ifShowCandidate");
@ -33,7 +34,10 @@ export default props => {
<a onClick={handleLinkClick}>{browser.i18n.getMessage("openInGoogleLabel")}</a>
</p>
)}
<MediaButtons resultText={resultText} />
<div className="mediaButtons">
<CopyButton text={resultText} />
<ListenButton text={resultText} lang={targetLang} />
</div>
</div>
);
};

View file

@ -0,0 +1,3 @@
<svg viewBox="2 2 20 20">
<path d="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z" />
</svg>

After

Width:  |  Height:  |  Size: 265 B

View file

@ -0,0 +1,27 @@
.copy {
display: flex;
justify-content: center;
.copiedText {
color: var(--sub-text);
font-size: 12px;
margin-right: 5px;
-moz-user-select: none;
-webkit-user-select: none;
}
.copyButton {
padding: 0;
border: 0;
background-color: var(--main-bg);
cursor: pointer;
svg {
height: 16px;
width: 16px;
fill: var(--sub-text);
&:hover {
fill: var(--highlight);
}
}
}
}

View file

@ -1,5 +1,7 @@
#inputArea {
position: relative;
margin: 10px;
textarea {
font: inherit;
resize: none;
@ -23,4 +25,11 @@
textarea:focus {
border-color: var(--highlight);
}
.listen {
position: absolute;
height: 16px;
right: 5px;
bottom: 5px;
}
}

View file

@ -0,0 +1,15 @@
.listenButton {
padding: 0;
border: 0;
background-color: rgba(0, 0, 0, 0);
cursor: pointer;
svg {
height: 16px;
width: 16px;
fill: var(--sub-text);
&:hover {
fill: var(--highlight);
}
}
}

View file

@ -1,33 +0,0 @@
.mediaButtons {
display: flex;
flex-direction: row;
justify-content: flex-end;
.copy {
display: flex;
justify-content: center;
.copiedText {
color: var(--sub-text);
font-size: 12px;
margin-right: 5px;
-moz-user-select: none;
-webkit-user-select: none;
}
.copyButton {
padding: 0;
border: 0;
background-color: var(--main-bg);
cursor: pointer;
svg {
height: 16px;
width: 16px;
fill: var(--sub-text);
&:hover {
fill: var(--highlight);
}
}
}
}
}

View file

@ -4,7 +4,7 @@
overflow-y: auto;
word-wrap: break-word;
background-color: var(--main-bg);
margin: 10px;
margin: 10px 10px 3px;
p {
margin: 0;
@ -35,4 +35,14 @@
}
}
}
.mediaButtons {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-right: 5px;
& > * {
margin-left: 10px;
}
}
}