Add listen button
This commit is contained in:
parent
32dd96d03c
commit
c162de041d
|
@ -33,6 +33,9 @@
|
||||||
"copiedLabel": {
|
"copiedLabel": {
|
||||||
"message": "Copied."
|
"message": "Copied."
|
||||||
},
|
},
|
||||||
|
"listenLabel": {
|
||||||
|
"message": "Listen"
|
||||||
|
},
|
||||||
"targetLangLabel": {
|
"targetLangLabel": {
|
||||||
"message": "Target language"
|
"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 React, { Component } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
|
import ListenButton from "./ListenButton";
|
||||||
import "../styles/inputArea.scss";
|
import "../styles/inputArea.scss";
|
||||||
|
|
||||||
export default class InputArea extends Component {
|
export default class InputArea extends Component {
|
||||||
|
@ -16,7 +17,9 @@ export default class InputArea extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps) {
|
shouldComponentUpdate(nextProps) {
|
||||||
const shouldUpdate = this.props.inputText !== nextProps.inputText;
|
const shouldUpdate =
|
||||||
|
this.props.inputText !== nextProps.inputText ||
|
||||||
|
this.props.sourceLang !== nextProps.sourceLang;
|
||||||
return shouldUpdate;
|
return shouldUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,16 +28,20 @@ export default class InputArea extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { inputText, sourceLang } = this.props;
|
||||||
return (
|
return (
|
||||||
<div id="inputArea">
|
<div id="inputArea">
|
||||||
<textarea
|
<textarea
|
||||||
value={this.props.inputText}
|
value={inputText}
|
||||||
ref="textarea"
|
ref="textarea"
|
||||||
placeholder={browser.i18n.getMessage("initialTextArea")}
|
placeholder={browser.i18n.getMessage("initialTextArea")}
|
||||||
onChange={this.handleInputText}
|
onChange={this.handleInputText}
|
||||||
autoFocus
|
autoFocus
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
|
<div className="listen">
|
||||||
|
{sourceLang && <ListenButton text={inputText} lang={sourceLang} />}
|
||||||
|
</div>
|
||||||
</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: "",
|
inputText: "",
|
||||||
resultText: "",
|
resultText: "",
|
||||||
candidateText: "",
|
candidateText: "",
|
||||||
|
sourceLang: "",
|
||||||
statusText: "OK",
|
statusText: "OK",
|
||||||
tabUrl: "",
|
tabUrl: "",
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
|
@ -93,7 +94,8 @@ export default class PopupPage extends Component {
|
||||||
this.setState({
|
this.setState({
|
||||||
resultText: result.resultText,
|
resultText: result.resultText,
|
||||||
candidateText: result.candidateText,
|
candidateText: result.candidateText,
|
||||||
statusText: result.statusText
|
statusText: result.statusText,
|
||||||
|
sourceLang: result.sourceLanguage
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
@ -143,7 +145,11 @@ export default class PopupPage extends Component {
|
||||||
isEnabledOnPage={this.state.isEnabledOnPage}
|
isEnabledOnPage={this.state.isEnabledOnPage}
|
||||||
isConnected={this.state.isConnected}
|
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 />
|
<hr />
|
||||||
<ResultArea
|
<ResultArea
|
||||||
inputText={this.state.inputText}
|
inputText={this.state.inputText}
|
||||||
|
|
|
@ -3,8 +3,9 @@ import browser from "webextension-polyfill";
|
||||||
import getErrorMessage from "src/common/getErrorMessage";
|
import getErrorMessage from "src/common/getErrorMessage";
|
||||||
import { getSettings } from "src/settings/settings";
|
import { getSettings } from "src/settings/settings";
|
||||||
import openUrl from "src/common/openUrl";
|
import openUrl from "src/common/openUrl";
|
||||||
|
import CopyButton from "./CopyButton";
|
||||||
|
import ListenButton from "./ListenButton";
|
||||||
import "../styles/ResultArea.scss";
|
import "../styles/ResultArea.scss";
|
||||||
import MediaButtons from "./MediaButtons";
|
|
||||||
|
|
||||||
const splitLine = text => {
|
const splitLine = text => {
|
||||||
const regex = /(\n)/g;
|
const regex = /(\n)/g;
|
||||||
|
@ -12,7 +13,7 @@ const splitLine = text => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default props => {
|
export default props => {
|
||||||
const { resultText, candidateText, statusText } = props;
|
const { resultText, candidateText, statusText, targetLang } = props;
|
||||||
const isError = statusText !== "OK";
|
const isError = statusText !== "OK";
|
||||||
const shouldShowCandidate = getSettings("ifShowCandidate");
|
const shouldShowCandidate = getSettings("ifShowCandidate");
|
||||||
|
|
||||||
|
@ -33,7 +34,10 @@ export default props => {
|
||||||
<a onClick={handleLinkClick}>{browser.i18n.getMessage("openInGoogleLabel")}</a>
|
<a onClick={handleLinkClick}>{browser.i18n.getMessage("openInGoogleLabel")}</a>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<MediaButtons resultText={resultText} />
|
<div className="mediaButtons">
|
||||||
|
<CopyButton text={resultText} />
|
||||||
|
<ListenButton text={resultText} lang={targetLang} />
|
||||||
|
</div>
|
||||||
</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 {
|
#inputArea {
|
||||||
|
position: relative;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
resize: none;
|
resize: none;
|
||||||
|
@ -23,4 +25,11 @@
|
||||||
textarea:focus {
|
textarea:focus {
|
||||||
border-color: var(--highlight);
|
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;
|
overflow-y: auto;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
background-color: var(--main-bg);
|
background-color: var(--main-bg);
|
||||||
margin: 10px;
|
margin: 10px 10px 3px;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
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