"
39 | ],
40 | "js": [
41 | "content.js"
42 | ]
43 | }
44 | ],
45 |
46 | "icons": {
47 | "16": "Fluentcards16.png",
48 | "48": "Fluentcards48.png",
49 | "128": "Fluentcards128.png",
50 | "256": "Fluentcards256.png"
51 | },
52 |
53 | "manifest_version": 2
54 | }
55 |
--------------------------------------------------------------------------------
/src/content/services/words-api.js:
--------------------------------------------------------------------------------
1 | import fetchJson from './fetch';
2 |
3 | /**
4 | * Download a definition of a word
5 | *
6 | * @param {string} word
7 | * @returns {promise}
8 | */
9 | export default function getDefinition(word, lang, targetLang) {
10 | if (lang !== 'en' || targetLang !== 'en') {
11 | return Promise.reject(new Error('Unsupported language'));
12 | }
13 |
14 | return fetchJson('wordsApi', word)
15 | .then(data => ({
16 | def: data.results
17 | .reduce((acc, next) => {
18 | const prev = acc[acc.length - 1];
19 |
20 | if (prev && prev.partOfSpeech === next.partOfSpeech) {
21 | prev.definition.push(next.definition);
22 | } else {
23 | next.definition = [ next.definition ];
24 | acc.push(next);
25 | }
26 |
27 | return acc;
28 | }, [])
29 | .map(result => ({
30 | text: data.word,
31 | ts: data.pronunciation ?
32 | data.pronunciation[result.partOfSpeech] || data.pronunciation.all :
33 | '',
34 | tr: result.definition.map(text => ({ text })),
35 | pos: result.partOfSpeech
36 | }))
37 | }));
38 | };
39 |
--------------------------------------------------------------------------------
/src/options/index.js:
--------------------------------------------------------------------------------
1 | import userOptions from '../common/services/user-options';
2 |
3 | // Save options to the storage
4 | function saveOptions(form) {
5 | const data = {
6 | targetLanguage: form.elements.targetLanguage.value,
7 | sourceLanguage: form.elements.sourceLanguage.value
8 | };
9 |
10 | userOptions.set(data).then(() => {
11 | // Update status to let user know options were saved.
12 | const status = document.getElementById('status');
13 | status.textContent = 'Options saved.';
14 |
15 | setTimeout(() => {
16 | status.textContent = '';
17 | }, 750);
18 | });
19 | }
20 |
21 | // Restore the form state using the preferences stored in the storage.
22 | function restoreOptions(form) {
23 | userOptions.get().then(options => {
24 | form.elements.targetLanguage.value = options.targetLanguage;
25 | form.elements.sourceLanguage.value = options.sourceLanguage;
26 | });
27 | }
28 |
29 | // Init
30 | document.addEventListener('DOMContentLoaded', () => {
31 | let form = document.getElementById('options-form');
32 |
33 | restoreOptions(form);
34 |
35 | form.addEventListener('submit', (e) => {
36 | e.preventDefault();
37 | saveOptions(form);
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/src/content/components/Card/Card.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import Def from '../Def/Def.jsx';
3 | import styles from './Card.css';
4 |
5 | const maxDefinitions = 2;
6 |
7 | export default class Card extends PureComponent {
8 | render() {
9 | const defData = this.props.data.data.def
10 | .slice(0, maxDefinitions);
11 |
12 | const showPos = defData.some((item, index) => {
13 | const prev = defData[index - 1]
14 | if (!prev) return false;
15 | return item.text === prev.text && item.pos !== prev.pos;
16 | });
17 |
18 | const items = defData
19 | .map((def, i) => {
20 | return (
21 |
22 | );
23 | });
24 |
25 | return (
26 |
27 | { items }
28 |
29 | { this.props.children }
30 |
31 |
38 |
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/content/components/Def/Def.css:
--------------------------------------------------------------------------------
1 | .def {
2 | margin-bottom: 1.5em;
3 | position: relative;
4 | }
5 |
6 | .speak {
7 | position: absolute;
8 | z-index: 1;
9 | right: 0;
10 | top: 0;
11 | }
12 |
13 | .word {
14 | font-size: 1.5em;
15 | padding-right: 20px;
16 | }
17 |
18 | .extra {
19 | color: #888;
20 | font-weight: 100;
21 | font-size: 0.7em;
22 | color: #aaa;
23 | padding-left: 0.5em;
24 | }
25 |
26 | .gen {
27 | font-style: italic;
28 | }
29 |
30 | .ts {
31 | font-size: 14px;
32 | color: #888;
33 | padding-left: 0.5em;
34 | }
35 |
36 | .tsBlock {
37 | padding-top: 2px;
38 | }
39 |
40 | .tsBlock .ts {
41 | padding-left: 0;
42 | }
43 |
44 | .definitions {
45 | margin: 1em 0 0;
46 | }
47 |
48 | .definitions div {
49 | display: list-item;
50 | list-style-type: disc;
51 | margin-left: 1.3em;
52 | margin-bottom: 0.5em;
53 | }
54 |
55 | .pos {
56 | text-transform: lowercase;
57 | color: #888;
58 | font-weight: 100;
59 | font-size: 0.5em;
60 | margin-left: 0.5em;
61 | vertical-align: super;
62 | }
63 |
64 | .examples {
65 | margin: 1em 0 0;
66 | font-style: italic;
67 | }
68 |
69 | .examples span {
70 | color: #888;
71 | font-weight: 100;
72 | font-style: normal;
73 | }
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fluentcards",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@babel/core": "7.5.5",
7 | "babel-loader": "8.0.6",
8 | "babel-plugin-named-asset-import": "^0.3.3",
9 | "babel-preset-react-app": "^9.0.1",
10 | "css-loader": "2.1.1",
11 | "html-webpack-plugin": "4.0.0-beta.5",
12 | "lodash": "^4.17.15",
13 | "mini-css-extract-plugin": "0.5.0",
14 | "optimize-css-assets-webpack-plugin": "5.0.3",
15 | "react": "^16.9.0",
16 | "react-dom": "^16.9.0",
17 | "style-loader": "^1.0.0",
18 | "textarea-caret": "^3.1.0",
19 | "webpack": "4.39.1",
20 | "webpack-cli": "^3.3.8",
21 | "webpack-dev-server": "3.2.1"
22 | },
23 | "scripts": {
24 | "start": "webpack --progress --colors --watch",
25 | "build": "NODE_ENV='production' webpack -p; ./bin/static-files.sh"
26 | },
27 | "browserslist": {
28 | "production": [
29 | ">0.2%",
30 | "not dead",
31 | "not op_mini all"
32 | ],
33 | "development": [
34 | "last 1 chrome version",
35 | "last 1 firefox version",
36 | "last 1 safari version"
37 | ]
38 | },
39 | "babel": {
40 | "presets": [
41 | "react-app"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/content/components/SpeakButton/SpeakButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import speak from '../../services/speech.js';
3 | import styles from './SpeakButton.css';
4 |
5 |
6 | export default class SpeakButton extends PureComponent {
7 | constructor() {
8 | super();
9 |
10 | this.onClick = (e) => {
11 | e.preventDefault();
12 | e.stopPropagation();
13 | speak(this.props.word, this.props.lang);
14 | };
15 | }
16 |
17 | render() {
18 | return (
19 |
20 |
28 |
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | module.exports = {
5 | mode: process.env.NODE_ENV,
6 |
7 | devtool: process.env.NODE_ENV === 'production' ? false : 'source-map',
8 | cache: true,
9 |
10 | entry: {
11 | content: [ path.resolve(__dirname, 'src/content/index.js') ],
12 | background: [ path.resolve(__dirname, 'src/background/index.js') ],
13 | popup: [ path.resolve(__dirname, 'src/popup/index.js') ],
14 | options: [ path.resolve(__dirname, 'src/options/index.js') ]
15 | },
16 |
17 | output: {
18 | path: path.resolve(__dirname, 'dist'),
19 | filename: '[name].js'
20 | },
21 |
22 | plugins: [
23 | new webpack.DefinePlugin({
24 | 'process.env': {
25 | NODE_ENV: JSON.stringify('production')
26 | }
27 | })
28 | ],
29 |
30 | module: {
31 | rules: [
32 | {
33 | test: /.jsx?$/,
34 | loader: 'babel-loader',
35 | include: [
36 | path.resolve(__dirname, 'src')
37 | ]
38 | },
39 | {
40 | test: /\.css$/,
41 | use: [
42 | 'style-loader',
43 | { loader: 'css-loader', options: { modules: true } }
44 | ]
45 | }
46 | ]
47 | }
48 | };
49 |
--------------------------------------------------------------------------------
/src/content/components/SaveButton/SaveButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import lookupsStore from '../../services/lookups-store';
3 | import styles from './SaveButton.css';
4 |
5 |
6 | export default class SaveButton extends PureComponent {
7 | constructor() {
8 | super();
9 |
10 | this.state = {
11 | saving: false,
12 | saved: false
13 | };
14 |
15 | this._onClick = () => this.saveWord();
16 | }
17 |
18 | saveWord() {
19 | this.setState({ saving: true });
20 |
21 | // Save the lookup in the storage
22 | lookupsStore
23 | .saveOne(this.props.word, this.props.context, this.props.data)
24 | .then(() => {
25 | this.setState({ saving: false, saved: true });
26 | })
27 | .catch(() => {
28 | this.setState({ saving: false });
29 | });
30 | }
31 |
32 | componentWillMount() {
33 | if (this.props.saveOnMount) {
34 | this.saveWord();
35 | }
36 | }
37 |
38 | render() {
39 | return (
40 |
41 |
44 |
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/popup/components/DomainToggle/DomainToggle.css:
--------------------------------------------------------------------------------
1 | .toggle {
2 | padding-right: 2em;
3 | }
4 |
5 | input[type="checkbox"] {
6 | position: absolute;
7 | margin: 8px 0 0 16px;
8 | }
9 | input[type="checkbox"] + label {
10 | position: relative;
11 | padding: 5px 0 0 50px;
12 | }
13 | input[type="checkbox"] + label:before {
14 | content: "";
15 | position: absolute;
16 | display: block;
17 | left: 0;
18 | top: 0;
19 | width: 40px; /* x*5 */
20 | height: 24px; /* x*3 */
21 | border-radius: 16px; /* x*2 */
22 | background: #fff;
23 | border: 1px solid #d9d9d9;
24 | transition: all 0.3s;
25 | }
26 | input[type="checkbox"] + label:after {
27 | content: '';
28 | position: absolute;
29 | display: block;
30 | left: 0px;
31 | top: 0px;
32 | width: 24px; /* x*3 */
33 | height: 24px; /* x*3 */
34 | border-radius: 16px; /* x*2 */
35 | background: #fff;
36 | border: 1px solid #d9d9d9;
37 | transition: all 0.3s;
38 | }
39 | input[type="checkbox"] + label:hover:after {
40 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
41 | }
42 | input[type="checkbox"]:checked + label:after {
43 | margin-left: 16px;
44 | }
45 | input[type="checkbox"]:checked + label:before {
46 | background: #333;
47 | }
48 |
49 | input[type="checkbox"] + label>span:before {
50 | content: 'Off ';
51 | }
52 |
53 | input[type="checkbox"]:checked + label>span:before {
54 | content: 'Enabled ';
55 | }
56 |
--------------------------------------------------------------------------------
/src/background/index.js:
--------------------------------------------------------------------------------
1 | import storage from '../common/services/storage';
2 | import config from '../common/config';
3 |
4 | // Export words
5 | chrome.runtime.onMessage.addListener(msg => {
6 | switch (msg && msg.event) {
7 | case 'exportCards':
8 | return chrome.tabs.create({ url: 'https://fluentcards.com/vocab' });
9 | }
10 | });
11 |
12 | // Saved words counter
13 | function updateCount() {
14 | storage.getAll().then(data => {
15 | const count = Object.keys(data)
16 | .map(Number)
17 | .filter(key => !isNaN(Number(key)))
18 | .length;
19 |
20 | return chrome.browserAction.setBadgeText({ text: String(count) });
21 | });
22 | }
23 | updateCount();
24 | chrome.storage.onChanged.addListener(updateCount);
25 | chrome.browserAction.setBadgeBackgroundColor({ color: '#aaa' });
26 |
27 |
28 | // Create a context menu item to save the selection
29 | chrome.contextMenus.create({
30 | title: 'Add to Fluentcards', contexts: [ 'selection' ],
31 | onclick: (info, tab) => {
32 | chrome.tabs.sendMessage(tab.id, { event: 'saveSelection' });
33 | }
34 | });
35 |
36 | // Make requests on content script's behalf
37 | chrome.runtime.onMessage.addListener(
38 | (request, sender, sendResponse) => {
39 | if (request.api) {
40 | fetch(config.urls[request.api] + request.params)
41 | .then(response => response.json())
42 | .then(data => sendResponse(data))
43 | .catch(error => sendResponse(error));
44 | return true;
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/src/popup/components/OptionsButton/OptionsButton.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './OptionsButton.css';
3 |
4 |
5 | function openOptions() {
6 | chrome.runtime.openOptionsPage();
7 | }
8 |
9 | export default function render() {
10 | return (
11 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/src/content/components/Main/Main.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import userOptions from '../../../common/services/user-options'
3 | import lookup from '../../services/lookup.js';
4 | import Card from '../Card/Card.jsx';
5 | import SaveButton from '../SaveButton/SaveButton.jsx';
6 | import styles from './Main.css';
7 |
8 | const options = userOptions.getDefaults();
9 | userOptions.get().then(data => Object.assign(options, data));
10 |
11 | export default class Main extends PureComponent {
12 | constructor() {
13 | super();
14 |
15 | this.state = {
16 | data: null
17 | };
18 |
19 | this._onLoad = result => this.onLoad(result);
20 | }
21 |
22 | onLoad(result) {
23 | this.props.onLoad();
24 |
25 | if (!result) return;
26 |
27 | // Display the definition
28 | this.setState({ data: result });
29 | }
30 |
31 | componentDidMount() {
32 | lookup(this.props.word, options.targetLanguage, options.sourceLanguage)
33 | .then(data => this._onLoad(data))
34 | .catch(() => this._onLoad());
35 | }
36 |
37 | componentWillUnmount() {
38 | this._onLoad = () => null;
39 | }
40 |
41 | render() {
42 | return (
43 |
44 | { this.state.data ? (
45 |
46 |
51 |
52 | ) : null }
53 |
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/content/index.js:
--------------------------------------------------------------------------------
1 | import { debounce } from 'lodash';
2 | import { isValidSelection } from './services/text-utils.js';
3 | import { exportCards } from './services/export.js';
4 | import storage from '../common/services/storage.js';
5 | import Popup from './components/Popup/Popup.jsx';
6 |
7 |
8 | function createPopup(selection, shouldSave = false) {
9 | return new Popup(selection, shouldSave);
10 | }
11 |
12 | function initEvents() {
13 | let isDoubleClick = false;
14 | let popup = null;
15 |
16 | const reset = () => {
17 | if (popup) {
18 | popup.remove();
19 | popup = null;
20 | }
21 | };
22 |
23 | document.addEventListener('dblclick', () => {
24 | isDoubleClick = true;
25 | });
26 |
27 | document.addEventListener('selectionchange', debounce(() => {
28 | reset();
29 |
30 | if (!isDoubleClick) return;
31 |
32 | const selection = window.getSelection();
33 | if (!isValidSelection(selection.toString())) return;
34 |
35 | popup = createPopup(selection);
36 | isDoubleClick = false;
37 | }, 200));
38 |
39 | // To avoid showing the definition when the user double-clicks
40 | // to copy the selection
41 | document.addEventListener('keydown', () => {
42 | if (popup && popup.isDismissable) reset();
43 | });
44 |
45 | // Save the selection from the context menu
46 | chrome.runtime.onMessage.addListener(msg => {
47 | if (msg && msg.event === 'saveSelection') {
48 | popup = createPopup(window.getSelection(), true);
49 | }
50 | });
51 | }
52 |
53 | function isDomainEnabled() {
54 | return storage.get(window.location.hostname)
55 | .then(domain => domain == null ? true : domain);
56 | }
57 |
58 | function init() {
59 | exportCards();
60 |
61 | isDomainEnabled().then(isEnabled => {
62 | if (isEnabled) initEvents();
63 | });
64 | }
65 |
66 | init();
67 |
--------------------------------------------------------------------------------
/src/popup/components/Root/Root.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import storage from '../../../common/services/storage';
3 | import userOptions from '../../../common/services/user-options';
4 | import Logo from '../Logo/Logo.jsx';
5 | import OptionsButton from '../OptionsButton/OptionsButton.jsx';
6 | import DomainToggle from '../DomainToggle/DomainToggle.jsx';
7 | import ExportButton from '../ExportButton/ExportButton.jsx';
8 | import styles from './Root.css';
9 |
10 | const langs = {
11 | es: 'Spanish',
12 | fr: 'French',
13 | de: 'German'
14 | };
15 |
16 | export default class Root extends PureComponent {
17 | constructor() {
18 | super();
19 |
20 | this.state = {
21 | hasItems: 0,
22 | userLang: ''
23 | };
24 | }
25 |
26 | checkItems() {
27 | return storage.getAll().then((data) => {
28 | return Object.keys(data).some(key => !isNaN(Number(key)));
29 | });
30 | }
31 |
32 | componentDidMount() {
33 | this.checkItems().then(hasItems => {
34 | this.setState({ hasItems });
35 | });
36 |
37 | userOptions.get().then(options => {
38 | this.setState({ userLang: options.sourceLanguage });
39 | });
40 | }
41 |
42 | render() {
43 | const targetLang = langs[this.state.userLang] || 'a foreign language';
44 |
45 | return (
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | Double-click on any word on the page to look it up in the dictionary.
58 |
59 |
60 | { this.state.hasItems ? (
61 |
62 |
63 |
64 | ) : null }
65 |
66 |
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/popup/components/DomainToggle/DomainToggle.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import storage from '../../../common/services/storage';
3 | import styles from './DomainToggle.css';
4 |
5 |
6 | function getDomain() {
7 | return new Promise((resolve, reject) => {
8 | chrome.tabs.query({
9 | active: true,
10 | currentWindow: true
11 | }, (tabs) => {
12 | if (!tabs[0]) {
13 | reject('No active tab');
14 | return;
15 | }
16 |
17 | const url = tabs[0].url;
18 | const link = document.createElement('a');
19 | link.href = url;
20 | const domain = link.hostname;
21 |
22 | resolve(domain);
23 | });
24 | });
25 | }
26 |
27 | function isDomainEnabled(domain) {
28 | return storage.get(domain)
29 | .then(data => (!data ? true : data.isEnabled));
30 | }
31 |
32 | function toggleSite(domain, isEnabled) {
33 | storage.set(domain, { isEnabled });
34 | }
35 |
36 | export default class DomainToggle extends PureComponent {
37 | constructor() {
38 | super();
39 |
40 | this.state = {
41 | domain: '',
42 | isEnabled: true
43 | };
44 |
45 | this.onChange = e => {
46 | const isEnabled = e.target.checked;
47 | toggleSite(this.state.domain, isEnabled);
48 | this.setState({ isEnabled });
49 | };
50 | }
51 |
52 | componentDidMount() {
53 | getDomain()
54 | .then(domain => {
55 | isDomainEnabled(domain).then(isEnabled => {
56 | this.setState({ domain, isEnabled });
57 | });
58 | });
59 | }
60 |
61 | render() {
62 | const domain = this.state.domain.replace(/^www\./, '');
63 |
64 | return (
65 |
66 |
70 |
73 |
74 | )
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/content/services/yandex-dictionary.js:
--------------------------------------------------------------------------------
1 | import fetchJson from './fetch';
2 |
3 | const apiKeys = [
4 | 'ZGljdC4xLjEuMjAxNTA4MTdUMDgxMTAzWi43YWM4YTUzODk0OTFjYTE1LjkxNjQwNjQwNzEyM2Y2MDlmZDBiZjkzYzEyMjE5MGQ1NmFmNjM1OWM=',
5 | 'ZGljdC4xLjEuMjAxNDA4MTBUMTgwODQyWi40YzA1ZmEyMzkyOWQ4OTFiLjA5Y2QzOTUyZDQ4Njk2YzYzOWIxNjRhNzcxZjY5NDU2N2IwNGJkZWY=',
6 | 'ZGljdC4xLjEuMjAxNDExMjJUMTIwMzA2Wi40ZTQ2NzY1ZGQyMDYwMTBhLjNlNGExYjE4MmRmNWQ4OTJmZDc0ZGQzZTQ0ZjM4OWIwZTVhZWVhMjQ='
7 | ];
8 |
9 | const endpoint = 'https://dictionary.yandex.net/api/v1/dicservice.json/lookup?&flags=4';
10 |
11 | // eslint-disable-next-line
12 | const langs = [ 'be-be','be-ru','bg-ru','cs-en','cs-ru','da-en','da-ru','de-de','de-en','de-ru','de-tr','el-en','el-ru','en-cs','en-da','en-de','en-el','en-en','en-es','en-et','en-fi','en-fr','en-it','en-lt','en-lv','en-nl','en-no','en-pt','en-ru','en-sk','en-sv','en-tr','en-uk','es-en','es-es','es-ru','et-en','et-ru','fi-en','fi-ru','fr-en','fr-fr','fr-ru','it-en','it-it','it-ru','lt-en','lt-ru','lv-en','lv-ru','nl-en','nl-ru','no-en','no-ru','pl-ru','pt-en','pt-ru','ru-be','ru-bg','ru-cs','ru-da','ru-de','ru-el','ru-en','ru-es','ru-et','ru-fi','ru-fr','ru-it','ru-lt','ru-lv','ru-nl','ru-no','ru-pl','ru-pt','ru-ru','ru-sk','ru-sv','ru-tr','ru-tt','ru-uk','sk-en','sk-ru','sv-en','sv-ru','tr-de','tr-en','tr-ru','tt-ru','uk-en','uk-ru','uk-uk' ];
13 |
14 | const defaultLang = 'en';
15 |
16 | /**
17 | * Download a dictionary definition of a word
18 | *
19 | * @param {string} text
20 | * @param {string} lang
21 | * @param {string} targetLang
22 | * @returns {promise}
23 | */
24 | export default function yandexDefine(text, lang, targetLang) {
25 | let langPair = `${ lang }-${ targetLang }`;
26 |
27 | if (!langs.includes(langPair)) {
28 | langPair = `${ defaultLang }-${ targetLang }`;
29 | }
30 |
31 | if (!langs.includes(langPair)) {
32 | langPair = `${ defaultLang }-${ defaultLang }`;
33 | }
34 |
35 | if (!langs.includes(langPair)) {
36 | return Promise.reject('Missing language pair');
37 | }
38 |
39 | const params = [
40 | 'key=' + atob(apiKeys[~~(Math.random() * apiKeys.length)]),
41 | 'lang=' + langPair,
42 | 'text=' + encodeURIComponent(text)
43 | ].join('&');
44 |
45 | return fetchJson('dictionaryApi', params)
46 | .then(data => {
47 | if (data && data.def && data.def.length) return data;
48 |
49 | throw new Error('No data');
50 | });
51 | }
52 |
--------------------------------------------------------------------------------
/src/content/components/Def/Def.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import { getArticle, splitWords } from '../../services/text-utils.js';
3 | import SpeakButton from '../SpeakButton/SpeakButton.jsx';
4 | import styles from './Def.css';
5 |
6 | const maxTrs = 3;
7 | const maxLongTrs = 2;
8 |
9 | export default class Def extends PureComponent {
10 | renderHeading() {
11 | const data = this.props.data;
12 |
13 | const pos = this.props.showPos && data.pos ? (
14 | { data.pos }
15 | ) : '';
16 |
17 | let word = data.text;
18 | const article = getArticle(data, this.props.lang);
19 | if (article) word = article + ' ' + word;
20 |
21 | const extra = (data.fl || data.num || data.gen) ? (
22 |
23 | { data.fl || '' }
24 | { data.fl && (data.num || data.gen) ? ', ' : '' }
25 | { data.num || data.gen || '' }
26 |
27 | ) : '';
28 |
29 | const transcription = data.ts ? (
30 | { `${ data.ts }` }
31 | ) : '';
32 |
33 | return (
34 |
35 |
36 | { word }
37 |
38 | { pos }
39 |
40 | { extra || transcription }
41 |
42 |
43 | { extra ? (
44 |
{ transcription }
45 | ) : '' }
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | render() {
55 | const data = this.props.data;
56 | const trs = (data.tr || []).slice(0, maxTrs);
57 |
58 | const trTexts = trs.map(item => item.text).filter(Boolean);
59 | const list = trTexts.some(tr => splitWords(tr).length > 5) ?
60 | trTexts.slice(0, maxLongTrs).map((tr, i) => (
61 | { tr }
62 | )) : trTexts.join('; ');
63 |
64 | const trExamples = [];
65 | trs.forEach(tr => {
66 | tr.ex && tr.ex.forEach(ex => trExamples.push(ex.text));
67 | });
68 | const examplesList = trExamples.slice(0, maxLongTrs).join('; ');
69 |
70 | return (
71 |
72 | { this.renderHeading() }
73 |
74 |
{ list }
75 |
76 | { examplesList ? (
77 |
78 | E. g.
79 | { examplesList }
80 |
81 | ) : '' }
82 |
83 | );
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/content/components/Popup/Popup.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import getCaretCoordinates from 'textarea-caret';
4 | import { getContext } from '../../services/text-utils.js';
5 | import Main from '../Main/Main.jsx';
6 |
7 |
8 | function extractWord(sel) {
9 | return sel.toString();
10 | }
11 |
12 | function extractContext(sel, word) {
13 | return getContext(word, (sel.focusNode.parentNode || sel.focusNode).textContent);
14 | }
15 |
16 | function getPosition(sel) {
17 | const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
18 | const scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
19 | const range = sel.getRangeAt(0);
20 | const isInInput = sel.anchorNode.contains(document.activeElement) &&
21 | [ 'textarea', 'input' ].includes(document.activeElement.tagName.toLowerCase());
22 | const position = {};
23 |
24 | if (isInInput) {
25 | const input = document.activeElement;
26 | const coordsStart = getCaretCoordinates(input, input.selectionStart);
27 | const coordsEnd = getCaretCoordinates(input, input.selectionEnd);
28 | const bbox = input.getBoundingClientRect();
29 | position.left = bbox.left + coordsStart.left;
30 | position.right = bbox.left + coordsEnd.left;
31 | position.top = bbox.top + coordsStart.top;
32 | position.bottom = bbox.top + coordsStart.top + 20;
33 | } else {
34 | const startBbox = range.getBoundingClientRect();
35 | position.left = startBbox.left;
36 |
37 | // Collapse the selection range horizontally to align to the right
38 | // NB: do this after caching the original bbox
39 | const endRange = range.cloneRange();
40 | endRange.collapse();
41 | const endBbox = endRange.getBoundingClientRect();
42 | position.right = endBbox.left;
43 | position.top = endBbox.top;
44 | position.bottom = endBbox.bottom;
45 | }
46 |
47 | return {
48 | left: Math.round(position.left + scrollLeft),
49 | right: Math.round(position.right + scrollLeft),
50 | top: Math.round(position.top + scrollTop),
51 | bottom: Math.round(position.bottom + scrollTop)
52 | };
53 | }
54 |
55 | function createDiv({ left, right, top, bottom }) {
56 | const padding = 3;
57 | const div = document.createElement('div');
58 |
59 | div.style.position = 'absolute';
60 | div.style.zIndex = '1000';
61 | div.style.left = `${ left }px`;
62 | div.style.top = `${ top - padding }px`;
63 | div.style.width = `${ (right - left) }px`;
64 | div.style.paddingTop = `${ padding + (bottom - top) }px`;
65 |
66 | document.body.appendChild(div);
67 |
68 | return div;
69 | }
70 |
71 | export default class Popup {
72 | constructor(sel, shouldSave = false) {
73 | const maxWait = 100;
74 | const start = Date.now();
75 | const word = extractWord(sel);
76 | const context = extractContext(sel, word);
77 | const pos = getPosition(sel);
78 |
79 | this.isDismissable = true;
80 | const onLoad = () => this.isDismissable = (Date.now() - start) <= maxWait;
81 |
82 | this.div = createDiv(pos);
83 |
84 | this.div.addEventListener('mouseleave', () => {
85 | if (this.isDismissable) this.remove();
86 | });
87 |
88 | ReactDOM.render(
89 | ,
90 | this.div
91 | );
92 | }
93 |
94 | remove() {
95 | if (!this.div) return;
96 |
97 | ReactDOM.unmountComponentAtNode(this.div);
98 | this.div.remove();
99 | this.div = null;
100 | }
101 | }
102 |
103 |
--------------------------------------------------------------------------------
/src/options/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Fluentcards Options
5 |
6 |
32 |
33 |
34 |
35 |
122 |
123 |
124 | Powered by
125 | Yandex.Dictionary
126 | and
127 | WordsAPI
128 |
129 |
130 |
131 |
132 |
133 |
134 |
--------------------------------------------------------------------------------