Language selection
This commit is contained in:
parent
a5acab5810
commit
243e9d30f0
10 changed files with 210 additions and 5 deletions
|
@ -4,6 +4,7 @@
|
||||||
"description": "Lingo is a small browser extension to help you learn languages",
|
"description": "Lingo is a small browser extension to help you learn languages",
|
||||||
"main": "webpack.config.js",
|
"main": "webpack.config.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"svelte-select": "^3.11.0",
|
||||||
"tippy.js": "^6.2.5"
|
"tippy.js": "^6.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -9,6 +9,7 @@ let com = new Communicator();
|
||||||
const scraper = new GTranslateScraper();
|
const scraper = new GTranslateScraper();
|
||||||
com.translateCallback = (toTranslate, sender) =>
|
com.translateCallback = (toTranslate, sender) =>
|
||||||
scraper.translate(toTranslate, sender.tab?.id);
|
scraper.translate(toTranslate, sender.tab?.id);
|
||||||
|
com.getLanguagesCallback = () => scraper.languages;
|
||||||
|
|
||||||
const db = new Flashcards();
|
const db = new Flashcards();
|
||||||
com.addFlashcardCallback = (c) => db.addFlashcard(c);
|
com.addFlashcardCallback = (c) => db.addFlashcard(c);
|
||||||
|
|
|
@ -1,5 +1,19 @@
|
||||||
import { browser } from "webextension-polyfill-ts";
|
import { browser } from "webextension-polyfill-ts";
|
||||||
|
|
||||||
|
export enum MessageKinds {
|
||||||
|
translationFinished = "translation",
|
||||||
|
languageList = "languages",
|
||||||
|
}
|
||||||
|
interface TranslationMessage {
|
||||||
|
messageKind: MessageKinds.translationFinished;
|
||||||
|
translation: Translation;
|
||||||
|
}
|
||||||
|
interface LanguageListMessage {
|
||||||
|
messageKind: MessageKinds.languageList;
|
||||||
|
languages: Array<Language>;
|
||||||
|
}
|
||||||
|
export type Message = TranslationMessage | LanguageListMessage;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
if (window.location.host != "translate.google.com" || window.hasRun) return;
|
if (window.location.host != "translate.google.com" || window.hasRun) return;
|
||||||
window.hasRun = true;
|
window.hasRun = true;
|
||||||
|
@ -8,6 +22,8 @@ import { browser } from "webextension-polyfill-ts";
|
||||||
name: "gtranslate_scraper_conn",
|
name: "gtranslate_scraper_conn",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sendMessage = (m: Message) => conn.postMessage(m);
|
||||||
|
|
||||||
const src = <HTMLTextAreaElement>document.querySelector("textarea#source");
|
const src = <HTMLTextAreaElement>document.querySelector("textarea#source");
|
||||||
|
|
||||||
const result = <HTMLDivElement>(
|
const result = <HTMLDivElement>(
|
||||||
|
@ -32,11 +48,30 @@ import { browser } from "webextension-polyfill-ts";
|
||||||
});
|
});
|
||||||
|
|
||||||
const sendTranslation = () => {
|
const sendTranslation = () => {
|
||||||
conn.postMessage({ src: src.value, result: result.innerText.trim() });
|
sendMessage({
|
||||||
|
messageKind: MessageKinds.translationFinished,
|
||||||
|
translation: { src: src.value, result: result.innerText.trim() },
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
conn.onMessage.addListener((to_translate: string) => {
|
conn.onMessage.addListener((to_translate: string) => {
|
||||||
if (src.value == to_translate && !isTranslating()) sendTranslation();
|
if (src.value == to_translate && !isTranslating()) sendTranslation();
|
||||||
else src.value = to_translate;
|
else src.value = to_translate;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getLanguages = () => {
|
||||||
|
const langs = document.querySelectorAll(
|
||||||
|
"div.language_list_section:nth-child(3) > .language_list_item_wrapper"
|
||||||
|
);
|
||||||
|
return Array.from(langs).map((l) => {
|
||||||
|
const code = l.classList[1].split("-").pop();
|
||||||
|
const name = l.querySelector(".language_list_item_language_name")!
|
||||||
|
.textContent;
|
||||||
|
return { code: code!, name: name! };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
sendMessage({
|
||||||
|
messageKind: MessageKinds.languageList,
|
||||||
|
languages: getLanguages(),
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { browser, WebRequest, Runtime } from "webextension-polyfill-ts";
|
import { browser, WebRequest, Runtime } from "webextension-polyfill-ts";
|
||||||
import content_script from "./gtranslate_content_script.ts?file";
|
import content_script from "./gtranslate_content_script.ts?file";
|
||||||
|
import { Message, MessageKinds } from "./gtranslate_content_script";
|
||||||
|
|
||||||
interface TranslationRequest {
|
interface TranslationRequest {
|
||||||
tabID?: number;
|
tabID?: number;
|
||||||
|
@ -12,13 +13,25 @@ export class GTranslateScraper {
|
||||||
private conn?: Runtime.Port;
|
private conn?: Runtime.Port;
|
||||||
private current?: TranslationRequest;
|
private current?: TranslationRequest;
|
||||||
private queue: Array<TranslationRequest> = [];
|
private queue: Array<TranslationRequest> = [];
|
||||||
|
languages: Promise<Array<Language>>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
let languagesResolve: (value: Array<Language>) => void;
|
||||||
|
this.languages = new Promise((resolv) => (languagesResolve = resolv));
|
||||||
|
|
||||||
browser.runtime.onConnect.addListener((p) => {
|
browser.runtime.onConnect.addListener((p) => {
|
||||||
if (p.name != "gtranslate_scraper_conn") return;
|
if (p.name != "gtranslate_scraper_conn") return;
|
||||||
this.conn = p;
|
this.conn = p;
|
||||||
this.processFromQueue(p);
|
this.processFromQueue(p);
|
||||||
p.onMessage.addListener((m) => this.onTranslationRecieved(m));
|
p.onMessage.addListener((m: Message) => {
|
||||||
|
switch (m.messageKind) {
|
||||||
|
case MessageKinds.languageList:
|
||||||
|
languagesResolve(m.languages);
|
||||||
|
return;
|
||||||
|
case MessageKinds.translationFinished:
|
||||||
|
this.onTranslationRecieved(m.translation);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//This allows us to create the google translate iframe by removing the x-frame-options header
|
//This allows us to create the google translate iframe by removing the x-frame-options header
|
||||||
|
|
|
@ -16,6 +16,9 @@ export class Communicator {
|
||||||
value: Flashcard,
|
value: Flashcard,
|
||||||
sender: Runtime.MessageSender
|
sender: Runtime.MessageSender
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
getLanguagesCallback?: (
|
||||||
|
sender: Runtime.MessageSender
|
||||||
|
) => Promise<Array<Language>>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
browser.runtime.onMessage.addListener(
|
browser.runtime.onMessage.addListener(
|
||||||
|
@ -32,6 +35,8 @@ export class Communicator {
|
||||||
return this.addFlashcardCallback!(c.card, s);
|
return this.addFlashcardCallback!(c.card, s);
|
||||||
case commandKinds.removeFlashcard:
|
case commandKinds.removeFlashcard:
|
||||||
return this.removeFlashcardCallback!(c.card, s);
|
return this.removeFlashcardCallback!(c.card, s);
|
||||||
|
case commandKinds.getLanguages:
|
||||||
|
return this.getLanguagesCallback!(s);
|
||||||
default:
|
default:
|
||||||
console.warn(`Unimplemented command ${c}`);
|
console.warn(`Unimplemented command ${c}`);
|
||||||
}
|
}
|
||||||
|
@ -63,6 +68,10 @@ export class Communicator {
|
||||||
commandKind: commandKinds.removeFlashcard,
|
commandKind: commandKinds.removeFlashcard,
|
||||||
card: card,
|
card: card,
|
||||||
});
|
});
|
||||||
|
static getLanguages = (): Promise<Array<Language>> =>
|
||||||
|
sendMessage({
|
||||||
|
commandKind: commandKinds.getLanguages,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendMessage = (m: command) => browser.runtime.sendMessage(m);
|
const sendMessage = (m: command) => browser.runtime.sendMessage(m);
|
||||||
|
@ -73,6 +82,7 @@ export enum commandKinds {
|
||||||
translate = "translate",
|
translate = "translate",
|
||||||
addFlashcard = "addFlashcard",
|
addFlashcard = "addFlashcard",
|
||||||
removeFlashcard = "removeFlashcard",
|
removeFlashcard = "removeFlashcard",
|
||||||
|
getLanguages = "getLanguages",
|
||||||
}
|
}
|
||||||
interface setEnabled {
|
interface setEnabled {
|
||||||
commandKind: commandKinds.setEnabled;
|
commandKind: commandKinds.setEnabled;
|
||||||
|
@ -98,9 +108,14 @@ interface removeFlashcard {
|
||||||
card: Flashcard;
|
card: Flashcard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface getLanguages {
|
||||||
|
commandKind: commandKinds.getLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
export type command =
|
export type command =
|
||||||
| setEnabled
|
| setEnabled
|
||||||
| getEnabled
|
| getEnabled
|
||||||
| translate
|
| translate
|
||||||
| addFlashcard
|
| addFlashcard
|
||||||
| removeFlashcard;
|
| removeFlashcard
|
||||||
|
| getLanguages;
|
||||||
|
|
36
src/frontend/popup/LanguageOption.svelte
Normal file
36
src/frontend/popup/LanguageOption.svelte
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
export let language: Language;
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
const clicked = () => {
|
||||||
|
dispatch("languageSelected", {
|
||||||
|
language: language,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.code == "Enter") {
|
||||||
|
clicked();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
border: solid gray 1px;
|
||||||
|
display: block;
|
||||||
|
padding: 1em 0.5ch;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus,
|
||||||
|
button:hover {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<button on:click={clicked} on:keydown={onKeyDown}>{language.name}</button>
|
||||||
|
|
79
src/frontend/popup/LanguageSelection.svelte
Normal file
79
src/frontend/popup/LanguageSelection.svelte
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import LanguageOption from "./LanguageOption.svelte";
|
||||||
|
import { slide } from "svelte/transition";
|
||||||
|
|
||||||
|
export let languages: Array<Language>;
|
||||||
|
export let default_language: Language;
|
||||||
|
|
||||||
|
const onBlur = (e: FocusEvent) => {
|
||||||
|
//Selected one of the child elements, possibly with tabindex
|
||||||
|
if (
|
||||||
|
e.relatedTarget instanceof HTMLElement &&
|
||||||
|
lang_list.contains(e.relatedTarget)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
focused = false;
|
||||||
|
};
|
||||||
|
setTimeout(() => console.log(window.innerWidth, window.innerHeight), 1000);
|
||||||
|
|
||||||
|
const languageSelectedEvent = (e: any) => {
|
||||||
|
const lang = e.detail?.language;
|
||||||
|
if (lang) {
|
||||||
|
current_lang = lang;
|
||||||
|
}
|
||||||
|
input.focus();
|
||||||
|
input.blur();
|
||||||
|
};
|
||||||
|
|
||||||
|
let langSearch = "";
|
||||||
|
$: if (!focused) langSearch = "";
|
||||||
|
$: searched_langs = languages.filter((l) =>
|
||||||
|
l.name.toLowerCase().startsWith(langSearch.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
let input: HTMLInputElement;
|
||||||
|
let lang_list: HTMLDivElement;
|
||||||
|
let current_lang = default_language;
|
||||||
|
let focused = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.langs {
|
||||||
|
max-height: 60vh;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 999;
|
||||||
|
background-color: lightpink;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<input
|
||||||
|
placeholder={current_lang.name}
|
||||||
|
bind:this={input}
|
||||||
|
bind:value={langSearch}
|
||||||
|
on:focus={() => (focused = true)}
|
||||||
|
on:blur={onBlur} />
|
||||||
|
{#if focused}
|
||||||
|
<div
|
||||||
|
aria-label="Language list"
|
||||||
|
bind:this={lang_list}
|
||||||
|
class="langs"
|
||||||
|
transition:slide={{ duration: 600 }}>
|
||||||
|
{#each searched_langs as l}
|
||||||
|
<LanguageOption
|
||||||
|
language={l}
|
||||||
|
on:languageSelected={languageSelectedEvent} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
|
@ -1,5 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Communicator } from "../../communication";
|
import { Communicator } from "../../communication";
|
||||||
|
import LanguageSelection from "./LanguageSelection.svelte";
|
||||||
|
import Select from "svelte-select";
|
||||||
|
|
||||||
let isON = false;
|
let isON = false;
|
||||||
|
|
||||||
|
@ -7,6 +9,8 @@
|
||||||
con.setEnabledCallback = (v) => (isON = v);
|
con.setEnabledCallback = (v) => (isON = v);
|
||||||
Communicator.getEnabled().then((v) => (isON = v));
|
Communicator.getEnabled().then((v) => (isON = v));
|
||||||
|
|
||||||
|
const langPromise = Communicator.getLanguages();
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
isON = !isON;
|
isON = !isON;
|
||||||
Communicator.setEnabled(isON);
|
Communicator.setEnabled(isON);
|
||||||
|
@ -15,8 +19,9 @@
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#container {
|
#container {
|
||||||
display: flex;
|
//display: flex;
|
||||||
min-width: 320px;
|
width: 387px;
|
||||||
|
height: 600px;
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -25,4 +30,14 @@
|
||||||
|
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<button on:click={handleClick}>{isON ? 'ON' : 'OFF'}</button>
|
<button on:click={handleClick}>{isON ? 'ON' : 'OFF'}</button>
|
||||||
|
{#await langPromise then langs}
|
||||||
|
Translate from
|
||||||
|
<LanguageSelection
|
||||||
|
languages={langs}
|
||||||
|
default_language={langs.find((l) => l.code == 'de') || langs[0]} />
|
||||||
|
To
|
||||||
|
<LanguageSelection
|
||||||
|
languages={langs}
|
||||||
|
default_language={langs.find((l) => l.code == 'en') || langs[0]} />
|
||||||
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
5
src/types.d.ts
vendored
5
src/types.d.ts
vendored
|
@ -16,6 +16,11 @@ declare interface Flashcard {
|
||||||
exported: boolean;
|
exported: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare interface Language {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
declare interface Window {
|
declare interface Window {
|
||||||
hasRun: any;
|
hasRun: any;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6174,6 +6174,11 @@ svelte-preprocess@~3.9.11:
|
||||||
detect-indent "^6.0.0"
|
detect-indent "^6.0.0"
|
||||||
strip-indent "^3.0.0"
|
strip-indent "^3.0.0"
|
||||||
|
|
||||||
|
svelte-select@^3.11.0:
|
||||||
|
version "3.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/svelte-select/-/svelte-select-3.11.0.tgz#81edf80f7ce53cf7bdd7bf86072388a64f094fc4"
|
||||||
|
integrity sha512-uravHZUwHxqh6tK1ITnHtYxKwyy7mEywChEhe4Kb5OBSgDdLnKBQmYJ1Sr6RklV+8Rg3pDzWpEBegy+/00pFkw==
|
||||||
|
|
||||||
svelte2tsx@*:
|
svelte2tsx@*:
|
||||||
version "0.1.77"
|
version "0.1.77"
|
||||||
resolved "https://registry.yarnpkg.com/svelte2tsx/-/svelte2tsx-0.1.77.tgz#7bd5f1b2c5de0bdd39c66e02e4951621f3b9d60e"
|
resolved "https://registry.yarnpkg.com/svelte2tsx/-/svelte2tsx-0.1.77.tgz#7bd5f1b2c5de0bdd39c66e02e4951621f3b9d60e"
|
||||||
|
|
Loading…
Reference in a new issue