Language selection

This commit is contained in:
a 2020-08-13 10:49:43 +02:00
parent a5acab5810
commit 243e9d30f0
10 changed files with 210 additions and 5 deletions

View file

@ -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": {

View file

@ -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);

View file

@ -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(),
});
})(); })();

View file

@ -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

View file

@ -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;

View 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>

View 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>

View file

@ -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
View file

@ -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;
} }

View file

@ -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"