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",
|
||||
"main": "webpack.config.js",
|
||||
"dependencies": {
|
||||
"svelte-select": "^3.11.0",
|
||||
"tippy.js": "^6.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -9,6 +9,7 @@ let com = new Communicator();
|
|||
const scraper = new GTranslateScraper();
|
||||
com.translateCallback = (toTranslate, sender) =>
|
||||
scraper.translate(toTranslate, sender.tab?.id);
|
||||
com.getLanguagesCallback = () => scraper.languages;
|
||||
|
||||
const db = new Flashcards();
|
||||
com.addFlashcardCallback = (c) => db.addFlashcard(c);
|
||||
|
|
|
@ -1,5 +1,19 @@
|
|||
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 () => {
|
||||
if (window.location.host != "translate.google.com" || window.hasRun) return;
|
||||
window.hasRun = true;
|
||||
|
@ -8,6 +22,8 @@ import { browser } from "webextension-polyfill-ts";
|
|||
name: "gtranslate_scraper_conn",
|
||||
});
|
||||
|
||||
const sendMessage = (m: Message) => conn.postMessage(m);
|
||||
|
||||
const src = <HTMLTextAreaElement>document.querySelector("textarea#source");
|
||||
|
||||
const result = <HTMLDivElement>(
|
||||
|
@ -32,11 +48,30 @@ import { browser } from "webextension-polyfill-ts";
|
|||
});
|
||||
|
||||
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) => {
|
||||
if (src.value == to_translate && !isTranslating()) sendTranslation();
|
||||
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 content_script from "./gtranslate_content_script.ts?file";
|
||||
import { Message, MessageKinds } from "./gtranslate_content_script";
|
||||
|
||||
interface TranslationRequest {
|
||||
tabID?: number;
|
||||
|
@ -12,13 +13,25 @@ export class GTranslateScraper {
|
|||
private conn?: Runtime.Port;
|
||||
private current?: TranslationRequest;
|
||||
private queue: Array<TranslationRequest> = [];
|
||||
languages: Promise<Array<Language>>;
|
||||
|
||||
constructor() {
|
||||
let languagesResolve: (value: Array<Language>) => void;
|
||||
this.languages = new Promise((resolv) => (languagesResolve = resolv));
|
||||
|
||||
browser.runtime.onConnect.addListener((p) => {
|
||||
if (p.name != "gtranslate_scraper_conn") return;
|
||||
this.conn = 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
|
||||
|
|
|
@ -16,6 +16,9 @@ export class Communicator {
|
|||
value: Flashcard,
|
||||
sender: Runtime.MessageSender
|
||||
) => Promise<void>;
|
||||
getLanguagesCallback?: (
|
||||
sender: Runtime.MessageSender
|
||||
) => Promise<Array<Language>>;
|
||||
|
||||
constructor() {
|
||||
browser.runtime.onMessage.addListener(
|
||||
|
@ -32,6 +35,8 @@ export class Communicator {
|
|||
return this.addFlashcardCallback!(c.card, s);
|
||||
case commandKinds.removeFlashcard:
|
||||
return this.removeFlashcardCallback!(c.card, s);
|
||||
case commandKinds.getLanguages:
|
||||
return this.getLanguagesCallback!(s);
|
||||
default:
|
||||
console.warn(`Unimplemented command ${c}`);
|
||||
}
|
||||
|
@ -63,6 +68,10 @@ export class Communicator {
|
|||
commandKind: commandKinds.removeFlashcard,
|
||||
card: card,
|
||||
});
|
||||
static getLanguages = (): Promise<Array<Language>> =>
|
||||
sendMessage({
|
||||
commandKind: commandKinds.getLanguages,
|
||||
});
|
||||
}
|
||||
|
||||
const sendMessage = (m: command) => browser.runtime.sendMessage(m);
|
||||
|
@ -73,6 +82,7 @@ export enum commandKinds {
|
|||
translate = "translate",
|
||||
addFlashcard = "addFlashcard",
|
||||
removeFlashcard = "removeFlashcard",
|
||||
getLanguages = "getLanguages",
|
||||
}
|
||||
interface setEnabled {
|
||||
commandKind: commandKinds.setEnabled;
|
||||
|
@ -98,9 +108,14 @@ interface removeFlashcard {
|
|||
card: Flashcard;
|
||||
}
|
||||
|
||||
interface getLanguages {
|
||||
commandKind: commandKinds.getLanguages;
|
||||
}
|
||||
|
||||
export type command =
|
||||
| setEnabled
|
||||
| getEnabled
|
||||
| translate
|
||||
| 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">
|
||||
import { Communicator } from "../../communication";
|
||||
import LanguageSelection from "./LanguageSelection.svelte";
|
||||
import Select from "svelte-select";
|
||||
|
||||
let isON = false;
|
||||
|
||||
|
@ -7,6 +9,8 @@
|
|||
con.setEnabledCallback = (v) => (isON = v);
|
||||
Communicator.getEnabled().then((v) => (isON = v));
|
||||
|
||||
const langPromise = Communicator.getLanguages();
|
||||
|
||||
const handleClick = () => {
|
||||
isON = !isON;
|
||||
Communicator.setEnabled(isON);
|
||||
|
@ -15,8 +19,9 @@
|
|||
|
||||
<style lang="scss">
|
||||
#container {
|
||||
display: flex;
|
||||
min-width: 320px;
|
||||
//display: flex;
|
||||
width: 387px;
|
||||
height: 600px;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
|
@ -25,4 +30,14 @@
|
|||
|
||||
<div id="container">
|
||||
<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>
|
||||
|
|
5
src/types.d.ts
vendored
5
src/types.d.ts
vendored
|
@ -16,6 +16,11 @@ declare interface Flashcard {
|
|||
exported: boolean;
|
||||
}
|
||||
|
||||
declare interface Language {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
declare interface Window {
|
||||
hasRun: any;
|
||||
}
|
||||
|
|
|
@ -6174,6 +6174,11 @@ svelte-preprocess@~3.9.11:
|
|||
detect-indent "^6.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@*:
|
||||
version "0.1.77"
|
||||
resolved "https://registry.yarnpkg.com/svelte2tsx/-/svelte2tsx-0.1.77.tgz#7bd5f1b2c5de0bdd39c66e02e4951621f3b9d60e"
|
||||
|
|
Loading…
Reference in a new issue