Language changes

This commit is contained in:
a 2020-08-16 16:11:51 +02:00
parent b7ff7d1ada
commit 74fe885fd0
14 changed files with 288 additions and 182 deletions

View file

@ -1 +1,5 @@
{ "useTabs": true, "semi": true } {
"useTabs": true,
"semi": true,
"svelteSortOrder": "scripts-markup-styles"
}

View file

@ -7,14 +7,35 @@ import content_script from "../frontend/content_script/content_script.ts?file";
let com = new Communicator(); let com = new Communicator();
const scraper = new GTranslateScraper(); const scraper = new GTranslateScraper();
com.translateCallback = (toTranslate, sender) => com.translateCallback = async (toTranslate, sender) =>
scraper.translate(toTranslate, sender.tab?.id); scraper.translate(toTranslate, await getCurrentLanguages(), sender.tab?.id);
com.getLanguagesCallback = () => scraper.languages; com.getLanguagesCallback = () => scraper.languages;
const db = new Flashcards(); const db = new Flashcards();
com.addFlashcardCallback = (c) => db.addFlashcard(c); com.addFlashcardCallback = (c) => db.addFlashcard(c);
com.removeFlashcardCallback = (c) => db.removeFlashcard(c); com.removeFlashcardCallback = (c) => db.removeFlashcard(c);
const getCurrentLanguages = async () => {
const langs = await scraper.languages;
let srcLangCode =
(await browser.storage.local.get("srcLang"))["srcLang"] ?? "de";
let dstLangCode =
(await browser.storage.local.get("dstLang"))["dstLang"] ?? "en";
const srcLang = langs.filter((l) => l.code == srcLangCode).pop()!;
const dstLang = langs.filter((l) => l.code == dstLangCode).pop()!;
return { srcLang: srcLang, dstLang: dstLang };
};
const setCurrentLanguages = async (l: Partial<LanguagePair>) => {
if (l.srcLang) await browser.storage.local.set({ srcLang: l.srcLang.code });
if (l.dstLang) await browser.storage.local.set({ dstLang: l.dstLang.code });
};
com.getCurrentLanguagesCallback = getCurrentLanguages;
com.setCurrentLanguagesCallback = setCurrentLanguages;
const getTabID = async (s: Runtime.MessageSender): Promise<number> => { const getTabID = async (s: Runtime.MessageSender): Promise<number> => {
if (s.tab?.id) return s.tab.id; if (s.tab?.id) return s.tab.id;
let tabs = await browser.tabs.query({ let tabs = await browser.tabs.query({

View file

@ -1,29 +1,33 @@
import { newPromise } from "../utils";
export class Flashcards { export class Flashcards {
db!: IDBDatabase; db: Promise<IDBDatabase>;
constructor() { constructor() {
let [promise, resolve, reject] = newPromise<IDBDatabase>();
this.db = promise;
const req = indexedDB.open("FlashCardDB", 1); const req = indexedDB.open("FlashCardDB", 1);
req.onupgradeneeded = () => { req.onupgradeneeded = () => {
let objectStore = req.result.createObjectStore("flashcards", { let objectStore = req.result.createObjectStore("flashcards", {
keyPath: "id", keyPath: "id",
autoIncrement: true, autoIncrement: true,
}); });
objectStore.createIndex("src", "src", { unique: true }); objectStore.createIndex("src", "src");
objectStore.createIndex("result", "result"); objectStore.createIndex("result", "result");
objectStore.createIndex("dateAdded", "dateAdded"); objectStore.createIndex("dateAdded", "dateAdded");
objectStore.createIndex("exported", "exported"); objectStore.createIndex("exported", "exported");
objectStore.createIndex("srcLang", "languages.srcLang.code");
objectStore.createIndex("dstLang", "languages.dstLang.code");
}; };
req.onsuccess = () => { req.onsuccess = () => resolve(req.result);
this.db = req.result; req.onerror = () =>
}; reject(
req.onerror = () => {
throw new Error(
`Error initializing the database backend, ${req.error}. This shouldn't ever happen` `Error initializing the database backend, ${req.error}. This shouldn't ever happen`
); );
};
} }
addFlashcard(t: Translation | Flashcard): Promise<Flashcard> { async addFlashcard(t: Translation | Flashcard): Promise<Flashcard> {
let card: Flashcard; let card: Flashcard;
if ("dateAdded" in t && "exported" in t) card = t; if ("dateAdded" in t && "exported" in t) card = t;
else { else {
@ -34,41 +38,41 @@ export class Flashcards {
}; };
} }
let req = this.db let req = (await this.db)
.transaction(["flashcards"], "readwrite") .transaction(["flashcards"], "readwrite")
.objectStore("flashcards") .objectStore("flashcards")
.add(card); .add(card);
return new Promise((resolve, reject) => { let [promise, resolve, reject] = newPromise<Flashcard>();
req.onsuccess = () => { req.onsuccess = () => {
card.id = req.result; card.id = req.result;
resolve(card); resolve(card);
}; };
req.onerror = () => reject(req.error); req.onerror = () => reject(req.error);
}); return promise;
} }
removeFlashcard(card: Flashcard): Promise<void> { async removeFlashcard(card: Flashcard): Promise<void> {
if (card.id) { if (!card.id) return Promise.reject("Undefined ID");
let req = this.db let req = (await this.db)
.transaction(["flashcards"], "readwrite") .transaction(["flashcards"], "readwrite")
.objectStore("flashcards") .objectStore("flashcards")
.delete(card.id); .delete(card.id);
return new Promise((resolve, reject) => {
req.onsuccess = () => resolve(); let [promise, resolve, reject] = newPromise<void>();
req.onerror = () => reject(req.error); req.onsuccess = () => resolve();
}); req.onerror = () => reject(req.error);
} else return Promise.reject("Undefined ID"); return promise;
} }
async getAllCards(): Promise<Array<Flashcard>> { async getAllCards(): Promise<Array<Flashcard>> {
let req = this.db let req = (await this.db)
.transaction(["flashcards"], "readonly") .transaction(["flashcards"], "readonly")
.objectStore("flashcards") .objectStore("flashcards")
.getAll(); .getAll();
return new Promise((resolve, reject) => { let [promise, resolve, reject] = newPromise<Flashcard[]>();
req.onsuccess = () => resolve(req.result); req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error); req.onerror = () => reject(req.error);
}); return promise;
} }
} }

View file

@ -4,11 +4,11 @@ export enum MessageKinds {
translationFinished = "translation", translationFinished = "translation",
languageList = "languages", languageList = "languages",
} }
interface TranslationMessage { export interface TranslationMessage {
messageKind: MessageKinds.translationFinished; messageKind: MessageKinds.translationFinished;
translation: Translation; translation: Translation;
} }
interface LanguageListMessage { export interface LanguageListMessage {
messageKind: MessageKinds.languageList; messageKind: MessageKinds.languageList;
languages: Array<Language>; languages: Array<Language>;
} }
@ -25,11 +25,17 @@ export type Message = TranslationMessage | LanguageListMessage;
const sendMessage = (m: Message) => conn.postMessage(m); 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>(
document.querySelector("div.results-container") document.querySelector("div.results-container")
); );
const [srcLang, dstLang] = Array.from(
document.querySelectorAll(".jfk-button-checked")
).map((d) => ({
code: d.getAttribute("value")!,
name: d.textContent!,
}));
const isTranslating = () => result.classList.contains("translating"); const isTranslating = () => result.classList.contains("translating");
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
@ -50,7 +56,14 @@ export type Message = TranslationMessage | LanguageListMessage;
const sendTranslation = () => { const sendTranslation = () => {
sendMessage({ sendMessage({
messageKind: MessageKinds.translationFinished, messageKind: MessageKinds.translationFinished,
translation: { src: src.value, result: result.innerText.trim() }, translation: {
src: src.value,
result: result.innerText.trim(),
languages: {
srcLang: srcLang,
dstLang: dstLang,
},
},
}); });
}; };

View file

@ -1,23 +1,26 @@
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"; import { Message, MessageKinds } from "./gtranslate_content_script";
import { newPromise } from "../utils";
interface TranslationRequest { interface TranslationRequest {
tabID?: number; tabID?: number;
resolveFn: (value: Translation) => void; resolveFn: (value: Translation) => void;
rejectFn: (reason: String) => void; rejectFn: (reason: String) => void;
toTranslate: string; toTranslate: string;
lang: LanguagePair;
} }
export class GTranslateScraper { export class GTranslateScraper {
private conn?: Runtime.Port; private conn?: Runtime.Port;
private current?: TranslationRequest;
private queue: Array<TranslationRequest> = []; private queue: Array<TranslationRequest> = [];
private iframe = <HTMLIFrameElement>document.createElement("iframe");
languages: Promise<Array<Language>>; languages: Promise<Array<Language>>;
lang: LanguagePair | undefined;
constructor() { constructor() {
let languagesResolve: (value: Array<Language>) => void; let languagesResolve: (value: Array<Language>) => void;
this.languages = new Promise((resolv) => (languagesResolve = resolv)); [this.languages, languagesResolve] = newPromise<Array<Language>>();
browser.runtime.onConnect.addListener((p) => { browser.runtime.onConnect.addListener((p) => {
if (p.name != "gtranslate_scraper_conn") return; if (p.name != "gtranslate_scraper_conn") return;
@ -33,6 +36,30 @@ export class GTranslateScraper {
} }
}); });
}); });
this.loadIframe("de", "en"); //Sample languages so we can get the language list
}
private loadIframe(srcLangCode: string, dstLangCode: string) {
if (!document.body.contains(this.iframe))
document.body.appendChild(this.iframe);
this.conn = undefined;
//Registers a temp content script, because we cannot inject scripts into iframes created on the background html page, because they have no tabId
browser.contentScripts
.register({
matches: ["<all_urls>"],
allFrames: true,
js: [
{
file: content_script,
},
],
runAt: "document_end",
})
.then((r) => {
this.iframe.addEventListener("load", r.unregister, { once: true });
this.iframe.src = `https://translate.google.com/?op=translate&sl=${srcLangCode}&tl=${dstLangCode}`;
});
//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
browser.webRequest.onHeadersReceived.addListener( browser.webRequest.onHeadersReceived.addListener(
@ -43,71 +70,59 @@ export class GTranslateScraper {
}, },
["blocking", "responseHeaders"] ["blocking", "responseHeaders"]
); );
let iframe = <HTMLIFrameElement>document.createElement("iframe");
//TODO make the language customizable
iframe.src = "https://translate.google.com/?op=translate&sl=de&tl=en";
//Registers a temp content script, because we cannot inject scripts into iframes created on the background html page, because they have no tabId
const js = {
file: content_script,
};
browser.contentScripts
.register({
matches: ["<all_urls>"],
allFrames: true,
js: [js],
runAt: "document_end",
})
.then((r) => {
iframe.addEventListener("load", r.unregister);
document.body.appendChild(iframe);
});
} }
private onTranslationRecieved(message: Translation) {
if (this.current) this.current.resolveFn(message); private onTranslationRecieved(trans: Translation) {
this.current = undefined; const current = this.queue.pop();
if (current) current.resolveFn(trans);
this.processFromQueue(this.conn!); this.processFromQueue(this.conn!);
} }
private processTranslationRequest(
request: TranslationRequest,
conn: Runtime.Port
) {
this.current = request;
conn.postMessage(request.toTranslate);
}
private processFromQueue(conn: Runtime.Port) { private processFromQueue(conn: Runtime.Port) {
const next = this.queue.pop(); const request = this.queue.slice(-1).pop();
if (next) this.processTranslationRequest(next, conn); if (!request) return;
if (this.lang != request.lang) {
this.lang = request.lang;
this.loadIframe(this.lang.srcLang.code, this.lang.dstLang.code);
} else conn.postMessage(request.toTranslate);
} }
translate(to_translate: string, tabID?: number): Promise<Translation> { translate(
if (to_translate == "") return Promise.resolve({ src: "", result: "" }); toTranslate: string,
langs: LanguagePair,
tabID?: number
): Promise<Translation> {
toTranslate = toTranslate.trim();
if (toTranslate == "")
return Promise.resolve({
src: "",
result: "",
languages: langs,
});
const p = new Promise((resolve: (value: Translation) => void, reject) => { let [promise, resolve, reject] = newPromise<Translation>();
const req = {
toTranslate: to_translate, const req = {
resolveFn: resolve, toTranslate: toTranslate,
rejectFn: reject, resolveFn: resolve,
tabID: tabID, rejectFn: reject,
}; tabID: tabID,
if (this.current || !this.conn) { lang: langs,
//Remove the requests from the same tab };
if (tabID) //Remove the requests from the same tab
this.queue = this.queue.filter((r) => { if (tabID)
if (r.tabID !== tabID) { this.queue = this.queue.filter((r) => {
r.rejectFn("Got another request from the same tab"); if (r.tabID !== tabID) {
return true; r.rejectFn("Got another request from the same tab");
} return true;
}); }
this.queue.push(req); });
} else { this.queue.push(req);
this.processTranslationRequest(req, this.conn); if (this.queue.length == 1 && this.conn) this.processFromQueue(this.conn);
}
}); return promise;
return p;
} }
} }

View file

@ -19,31 +19,36 @@ export class Communicator {
getLanguagesCallback?: ( getLanguagesCallback?: (
sender: Runtime.MessageSender sender: Runtime.MessageSender
) => Promise<Array<Language>>; ) => Promise<Array<Language>>;
getCurrentLanguagesCallback?: (
sender: Runtime.MessageSender
) => Promise<LanguagePair>;
setCurrentLanguagesCallback?: (
value: Partial<LanguagePair>,
sender: Runtime.MessageSender
) => void;
constructor() { constructor() {
browser.runtime.onMessage.addListener( browser.runtime.onMessage.addListener(
(c: command, s: Runtime.MessageSender) => { (c: command, s: Runtime.MessageSender) => {
try { switch (c.commandKind) {
switch (c.commandKind) { case commandKinds.setEnabled:
case commandKinds.setEnabled: return this.setEnabledCallback!(c.value, s);
return this.setEnabledCallback!(c.value, s); case commandKinds.getEnabled:
case commandKinds.getEnabled: return this.getEnabledCallback!(s);
return this.getEnabledCallback!(s); case commandKinds.translate:
case commandKinds.translate: return this.translateCallback!(c.toTranslate, s);
return this.translateCallback!(c.toTranslate, s); case commandKinds.addFlashcard:
case commandKinds.addFlashcard: 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:
case commandKinds.getLanguages: return this.getLanguagesCallback!(s);
return this.getLanguagesCallback!(s); case commandKinds.getCurrentLanguages:
default: return this.getCurrentLanguagesCallback!(s);
console.warn(`Unimplemented command ${c}`); case commandKinds.setCurrentLanguages:
} return this.setCurrentLanguagesCallback!(c.value, s);
} catch (e) { default:
if (e instanceof ReferenceError)
console.warn(`Unimplemented command ${c}`); console.warn(`Unimplemented command ${c}`);
else throw e;
} }
} }
); );
@ -72,6 +77,12 @@ export class Communicator {
sendMessage({ sendMessage({
commandKind: commandKinds.getLanguages, commandKind: commandKinds.getLanguages,
}); });
static getCurrentLanguages = (): Promise<LanguagePair> =>
sendMessage({
commandKind: commandKinds.getCurrentLanguages,
});
static setCurrentLanguages = (v: Partial<LanguagePair>) =>
sendMessage({ commandKind: commandKinds.setCurrentLanguages, value: v });
} }
const sendMessage = (m: command) => browser.runtime.sendMessage(m); const sendMessage = (m: command) => browser.runtime.sendMessage(m);
@ -83,17 +94,22 @@ export enum commandKinds {
addFlashcard = "addFlashcard", addFlashcard = "addFlashcard",
removeFlashcard = "removeFlashcard", removeFlashcard = "removeFlashcard",
getLanguages = "getLanguages", getLanguages = "getLanguages",
getCurrentLanguages = "getCurrentLanguage",
setCurrentLanguages = "setCurrentLanguages",
} }
interface setEnabled { interface setEnabled {
commandKind: commandKinds.setEnabled; commandKind: commandKinds.setEnabled;
value: boolean; value: boolean;
} }
interface getEnabled { interface getCommand {
commandKind: commandKinds.getEnabled; commandKind:
| commandKinds.getEnabled
| commandKinds.getLanguages
| commandKinds.getCurrentLanguages;
} }
interface translate { interface translateCommand {
commandKind: commandKinds.translate; commandKind: commandKinds.translate;
toTranslate: string; toTranslate: string;
} }
@ -108,14 +124,15 @@ interface removeFlashcard {
card: Flashcard; card: Flashcard;
} }
interface getLanguages { interface setCurrentLanguages {
commandKind: commandKinds.getLanguages; commandKind: commandKinds.setCurrentLanguages;
value: Partial<LanguagePair>;
} }
export type command = export type command =
| setEnabled | setEnabled
| getEnabled | getCommand
| translate | translateCommand
| addFlashcard | addFlashcard
| removeFlashcard | removeFlashcard
| getLanguages; | setCurrentLanguages;

View file

@ -1,3 +1,5 @@
<div id="spinner" />
<style lang="scss"> <style lang="scss">
#spinner { #spinner {
border: 0.3em solid #f3f3f3; border: 0.3em solid #f3f3f3;
@ -18,5 +20,3 @@
} }
} }
</style> </style>
<div id="spinner" />

View file

@ -14,6 +14,25 @@
}; };
</script> </script>
<div class="translatedContainer">
<span>{trans.result}</span>
{#if !card}
<button on:click={addFlashcard}>
Add
<br />
card
</button>
{:else}
<button on:click={removeFlashcard} class="enabled">
Remove
<br />
card
</button>
{/if}
</div>
<style lang="scss"> <style lang="scss">
@use "../color_scheme"; @use "../color_scheme";
@ -47,22 +66,3 @@
} }
} }
</style> </style>
<div class="translatedContainer">
<span>{trans.result}</span>
{#if !card}
<button on:click={addFlashcard}>
Add
<br />
card
</button>
{:else}
<button on:click={removeFlashcard} class="enabled">
Remove
<br />
card
</button>
{/if}
</div>

View file

@ -8,6 +8,17 @@
label.toLowerCase().startsWith(filterText.toLowerCase()); label.toLowerCase().startsWith(filterText.toLowerCase());
</script> </script>
<div class="select-styling">
<Select
items={langs}
getOptionLabel={getLabel}
getSelectionLabel={getLabel}
optionIdentifier="code"
{itemFilter}
isClearable={false}
bind:selectedValue />
</div>
<style lang="scss"> <style lang="scss">
@use "../color_scheme.scss"; @use "../color_scheme.scss";
.select-styling { .select-styling {
@ -20,14 +31,3 @@
--border: none; --border: none;
} }
</style> </style>
<div class="select-styling">
<Select
items={langs}
getOptionLabel={getLabel}
getSelectionLabel={getLabel}
optionIdentifier="code"
{itemFilter}
isClearable={false}
bind:selectedValue />
</div>

View file

@ -4,18 +4,33 @@
Communicator.getEnabled().then((v) => (enabled = v)); Communicator.getEnabled().then((v) => (enabled = v));
const languages = Communicator.getLanguages();
let enabled: boolean; let enabled: boolean;
$: if (enabled !== undefined) Communicator.setEnabled(enabled); $: if (enabled !== undefined) Communicator.setEnabled(enabled);
const langPromise = Communicator.getLanguages(); let selected: LanguagePair;
let srcLang: Language; Communicator.getCurrentLanguages().then((l) => (selected = l));
let dstLang: Language; $: if (selected) Communicator.setCurrentLanguages(selected);
langPromise.then((langs) => {
srcLang = langs[0];
dstLang = langs[0];
});
</script> </script>
<div id="container">
<label class:enabled>
<input type="checkbox" bind:checked={enabled} />
{enabled ? 'Disable Extension' : 'Enable Extension'}
</label>
{#if selected}
{#await languages then langs}
Translate from
<LanguageSelection {langs} bind:selectedValue={selected.srcLang} />
To
<LanguageSelection {langs} bind:selectedValue={selected.dstLang} />
{/await}
{/if}
</div>
<style lang="scss"> <style lang="scss">
@use "../color_scheme.scss"; @use "../color_scheme.scss";
@ -59,18 +74,3 @@
} }
} }
</style> </style>
<div id="container">
<label class:enabled>
<input type="checkbox" bind:checked={enabled} />
{enabled ? 'ON' : 'OFF'}
</label>
{#await langPromise then langs}
Translate from
<LanguageSelection {langs} bind:selectedValue={srcLang} />
To
<LanguageSelection {langs} bind:selectedValue={dstLang} />
{/await}
</div>

View file

@ -1,2 +1,3 @@
import App from "./Popup.svelte"; import App from "./Popup.svelte";
new App({ target: document.body }); new App({ target: document.body });
document.addEventListener;

View file

@ -13,5 +13,11 @@
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
"background": { "scripts": ["background.bundle.js"] }, "background": { "scripts": ["background.bundle.js"] },
"permissions": ["<all_urls>", "sessions", "webRequest", "webRequestBlocking"] "permissions": [
"<all_urls>",
"sessions",
"webRequest",
"webRequestBlocking",
"storage"
]
} }

7
src/types.d.ts vendored
View file

@ -6,6 +6,7 @@ declare module "*?file" {
declare interface Translation { declare interface Translation {
src: string; src: string;
result: string; result: string;
languages: LanguagePair;
} }
declare interface Flashcard { declare interface Flashcard {
@ -14,6 +15,12 @@ declare interface Flashcard {
result: string; result: string;
dateAdded: Date; dateAdded: Date;
exported: boolean; exported: boolean;
languages: LanguagePair;
}
declare interface LanguagePair {
srcLang: Language;
dstLang: Language;
} }
declare interface Language { declare interface Language {

18
src/utils.ts Normal file
View file

@ -0,0 +1,18 @@
type resolveCallback<T> = (value: T) => void;
type rejectCallback = (reason: any) => void;
export const newPromise = <T>(): [
Promise<T>,
resolveCallback<T>,
rejectCallback
] => {
let resolve: resolveCallback<T>;
let reject: rejectCallback;
const promise = new Promise(
(res: resolveCallback<T>, rej: rejectCallback) => {
resolve = res;
reject = rej;
}
);
return [promise, resolve!, reject!];
};