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();
const scraper = new GTranslateScraper();
com.translateCallback = (toTranslate, sender) =>
scraper.translate(toTranslate, sender.tab?.id);
com.translateCallback = async (toTranslate, sender) =>
scraper.translate(toTranslate, await getCurrentLanguages(), sender.tab?.id);
com.getLanguagesCallback = () => scraper.languages;
const db = new Flashcards();
com.addFlashcardCallback = (c) => db.addFlashcard(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> => {
if (s.tab?.id) return s.tab.id;
let tabs = await browser.tabs.query({

View file

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

View file

@ -4,11 +4,11 @@ export enum MessageKinds {
translationFinished = "translation",
languageList = "languages",
}
interface TranslationMessage {
export interface TranslationMessage {
messageKind: MessageKinds.translationFinished;
translation: Translation;
}
interface LanguageListMessage {
export interface LanguageListMessage {
messageKind: MessageKinds.languageList;
languages: Array<Language>;
}
@ -25,11 +25,17 @@ export type Message = TranslationMessage | LanguageListMessage;
const sendMessage = (m: Message) => conn.postMessage(m);
const src = <HTMLTextAreaElement>document.querySelector("textarea#source");
const result = <HTMLDivElement>(
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 observer = new MutationObserver((mutations) => {
@ -50,7 +56,14 @@ export type Message = TranslationMessage | LanguageListMessage;
const sendTranslation = () => {
sendMessage({
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 content_script from "./gtranslate_content_script.ts?file";
import { Message, MessageKinds } from "./gtranslate_content_script";
import { newPromise } from "../utils";
interface TranslationRequest {
tabID?: number;
resolveFn: (value: Translation) => void;
rejectFn: (reason: String) => void;
toTranslate: string;
lang: LanguagePair;
}
export class GTranslateScraper {
private conn?: Runtime.Port;
private current?: TranslationRequest;
private queue: Array<TranslationRequest> = [];
private iframe = <HTMLIFrameElement>document.createElement("iframe");
languages: Promise<Array<Language>>;
lang: LanguagePair | undefined;
constructor() {
let languagesResolve: (value: Array<Language>) => void;
this.languages = new Promise((resolv) => (languagesResolve = resolv));
[this.languages, languagesResolve] = newPromise<Array<Language>>();
browser.runtime.onConnect.addListener((p) => {
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
browser.webRequest.onHeadersReceived.addListener(
@ -43,71 +70,59 @@ export class GTranslateScraper {
},
["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);
this.current = undefined;
private onTranslationRecieved(trans: Translation) {
const current = this.queue.pop();
if (current) current.resolveFn(trans);
this.processFromQueue(this.conn!);
}
private processTranslationRequest(
request: TranslationRequest,
conn: Runtime.Port
) {
this.current = request;
conn.postMessage(request.toTranslate);
}
private processFromQueue(conn: Runtime.Port) {
const next = this.queue.pop();
if (next) this.processTranslationRequest(next, conn);
const request = this.queue.slice(-1).pop();
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> {
if (to_translate == "") return Promise.resolve({ src: "", result: "" });
translate(
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) => {
const req = {
toTranslate: to_translate,
resolveFn: resolve,
rejectFn: reject,
tabID: tabID,
};
if (this.current || !this.conn) {
//Remove the requests from the same tab
if (tabID)
this.queue = this.queue.filter((r) => {
if (r.tabID !== tabID) {
r.rejectFn("Got another request from the same tab");
return true;
}
});
this.queue.push(req);
} else {
this.processTranslationRequest(req, this.conn);
}
});
return p;
let [promise, resolve, reject] = newPromise<Translation>();
const req = {
toTranslate: toTranslate,
resolveFn: resolve,
rejectFn: reject,
tabID: tabID,
lang: langs,
};
//Remove the requests from the same tab
if (tabID)
this.queue = this.queue.filter((r) => {
if (r.tabID !== tabID) {
r.rejectFn("Got another request from the same tab");
return true;
}
});
this.queue.push(req);
if (this.queue.length == 1 && this.conn) this.processFromQueue(this.conn);
return promise;
}
}

View file

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

View file

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

View file

@ -14,6 +14,25 @@
};
</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">
@use "../color_scheme";
@ -47,22 +66,3 @@
}
}
</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());
</script>
<div class="select-styling">
<Select
items={langs}
getOptionLabel={getLabel}
getSelectionLabel={getLabel}
optionIdentifier="code"
{itemFilter}
isClearable={false}
bind:selectedValue />
</div>
<style lang="scss">
@use "../color_scheme.scss";
.select-styling {
@ -20,14 +31,3 @@
--border: none;
}
</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));
const languages = Communicator.getLanguages();
let enabled: boolean;
$: if (enabled !== undefined) Communicator.setEnabled(enabled);
const langPromise = Communicator.getLanguages();
let srcLang: Language;
let dstLang: Language;
langPromise.then((langs) => {
srcLang = langs[0];
dstLang = langs[0];
});
let selected: LanguagePair;
Communicator.getCurrentLanguages().then((l) => (selected = l));
$: if (selected) Communicator.setCurrentLanguages(selected);
</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">
@use "../color_scheme.scss";
@ -59,18 +74,3 @@
}
}
</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";
new App({ target: document.body });
document.addEventListener;

View file

@ -13,5 +13,11 @@
"default_popup": "popup.html"
},
"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 {
src: string;
result: string;
languages: LanguagePair;
}
declare interface Flashcard {
@ -14,6 +15,12 @@ declare interface Flashcard {
result: string;
dateAdded: Date;
exported: boolean;
languages: LanguagePair;
}
declare interface LanguagePair {
srcLang: Language;
dstLang: 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!];
};