Add listen button
This commit is contained in:
parent
32dd96d03c
commit
c162de041d
|
@ -33,6 +33,9 @@
|
|||
"copiedLabel": {
|
||||
"message": "Copied."
|
||||
},
|
||||
"listenLabel": {
|
||||
"message": "Listen"
|
||||
},
|
||||
"targetLangLabel": {
|
||||
"message": "Target language"
|
||||
},
|
||||
|
|
38
src/popup/components/CopyButton.js
Normal file
38
src/popup/components/CopyButton.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
32
src/popup/components/ListenButton.js
Normal file
32
src/popup/components/ListenButton.js
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
3
src/popup/icons/speaker.svg
Normal file
3
src/popup/icons/speaker.svg
Normal 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 |
27
src/popup/styles/CopyButton.scss
Normal file
27
src/popup/styles/CopyButton.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
15
src/popup/styles/ListenButton.scss
Normal file
15
src/popup/styles/ListenButton.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue