diff --git a/.prettierrc.json b/.prettierrc.json index 3d75029..37de190 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1 +1,5 @@ -{ "useTabs": true, "semi": true } +{ + "useTabs": true, + "semi": true, + "svelteSortOrder": "scripts-markup-styles" +} diff --git a/src/background/background.ts b/src/background/background.ts index c841e9e..02aa096 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -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) => { + 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 => { if (s.tab?.id) return s.tab.id; let tabs = await browser.tabs.query({ diff --git a/src/background/database.ts b/src/background/database.ts index 6cfb199..5fb47e3 100644 --- a/src/background/database.ts +++ b/src/background/database.ts @@ -1,29 +1,33 @@ +import { newPromise } from "../utils"; + export class Flashcards { - db!: IDBDatabase; + db: Promise; constructor() { + let [promise, resolve, reject] = newPromise(); + 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 { + async addFlashcard(t: Translation | Flashcard): Promise { 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(); + req.onsuccess = () => { + card.id = req.result; + resolve(card); + }; + req.onerror = () => reject(req.error); + return promise; } - removeFlashcard(card: Flashcard): Promise { - 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 { + 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(); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + return promise; } async getAllCards(): Promise> { - 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(); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + return promise; } } diff --git a/src/background/gtranslate_content_script.ts b/src/background/gtranslate_content_script.ts index 07d5683..5a64915 100644 --- a/src/background/gtranslate_content_script.ts +++ b/src/background/gtranslate_content_script.ts @@ -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; } @@ -25,11 +25,17 @@ export type Message = TranslationMessage | LanguageListMessage; const sendMessage = (m: Message) => conn.postMessage(m); const src = document.querySelector("textarea#source"); - const result = ( 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, + }, + }, }); }; diff --git a/src/background/gtranslate_scraper.ts b/src/background/gtranslate_scraper.ts index 0e48303..a710498 100644 --- a/src/background/gtranslate_scraper.ts +++ b/src/background/gtranslate_scraper.ts @@ -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 = []; + private iframe = document.createElement("iframe"); languages: Promise>; + lang: LanguagePair | undefined; constructor() { let languagesResolve: (value: Array) => void; - this.languages = new Promise((resolv) => (languagesResolve = resolv)); + [this.languages, languagesResolve] = newPromise>(); 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: [""], + 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 = 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: [""], - 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 { - if (to_translate == "") return Promise.resolve({ src: "", result: "" }); + translate( + toTranslate: string, + langs: LanguagePair, + tabID?: number + ): Promise { + 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(); + + 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; } } diff --git a/src/communication.ts b/src/communication.ts index 5e8dc21..6d5170d 100644 --- a/src/communication.ts +++ b/src/communication.ts @@ -19,31 +19,36 @@ export class Communicator { getLanguagesCallback?: ( sender: Runtime.MessageSender ) => Promise>; + getCurrentLanguagesCallback?: ( + sender: Runtime.MessageSender + ) => Promise; + setCurrentLanguagesCallback?: ( + value: Partial, + 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 => + sendMessage({ + commandKind: commandKinds.getCurrentLanguages, + }); + static setCurrentLanguages = (v: Partial) => + 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; } export type command = | setEnabled - | getEnabled - | translate + | getCommand + | translateCommand | addFlashcard | removeFlashcard - | getLanguages; + | setCurrentLanguages; diff --git a/src/frontend/content_script/Spinner.svelte b/src/frontend/content_script/Spinner.svelte index 41c5afe..b3a73a5 100644 --- a/src/frontend/content_script/Spinner.svelte +++ b/src/frontend/content_script/Spinner.svelte @@ -1,3 +1,5 @@ +
+ - -
diff --git a/src/frontend/content_script/Translated.svelte b/src/frontend/content_script/Translated.svelte index 73cc2e4..df8aed5 100644 --- a/src/frontend/content_script/Translated.svelte +++ b/src/frontend/content_script/Translated.svelte @@ -14,6 +14,25 @@ }; +
+ {trans.result} + + {#if !card} + + {:else} + + {/if} + +
+ - -
- {trans.result} - - {#if !card} - - {:else} - - {/if} - -
diff --git a/src/frontend/popup/LanguageSelection.svelte b/src/frontend/popup/LanguageSelection.svelte index a3207b5..c6f0896 100644 --- a/src/frontend/popup/LanguageSelection.svelte +++ b/src/frontend/popup/LanguageSelection.svelte @@ -8,6 +8,17 @@ label.toLowerCase().startsWith(filterText.toLowerCase()); +
+ -
diff --git a/src/frontend/popup/Popup.svelte b/src/frontend/popup/Popup.svelte index 5c3c94c..3b4361b 100644 --- a/src/frontend/popup/Popup.svelte +++ b/src/frontend/popup/Popup.svelte @@ -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); +
+ + + + {#if selected} + {#await languages then langs} + Translate from + + To + + {/await} + {/if} +
+ - -
- - - - {#await langPromise then langs} - Translate from - - To - - {/await} -
diff --git a/src/frontend/popup/popup.ts b/src/frontend/popup/popup.ts index 7355cf6..075468c 100644 --- a/src/frontend/popup/popup.ts +++ b/src/frontend/popup/popup.ts @@ -1,2 +1,3 @@ import App from "./Popup.svelte"; new App({ target: document.body }); +document.addEventListener; diff --git a/src/manifest.json b/src/manifest.json index 8b9204e..5b041b9 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -13,5 +13,11 @@ "default_popup": "popup.html" }, "background": { "scripts": ["background.bundle.js"] }, - "permissions": ["", "sessions", "webRequest", "webRequestBlocking"] + "permissions": [ + "", + "sessions", + "webRequest", + "webRequestBlocking", + "storage" + ] } diff --git a/src/types.d.ts b/src/types.d.ts index b084099..ea0cd0b 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -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 { diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..7e961fb --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,18 @@ +type resolveCallback = (value: T) => void; +type rejectCallback = (reason: any) => void; + +export const newPromise = (): [ + Promise, + resolveCallback, + rejectCallback +] => { + let resolve: resolveCallback; + let reject: rejectCallback; + const promise = new Promise( + (res: resolveCallback, rej: rejectCallback) => { + resolve = res; + reject = rej; + } + ); + return [promise, resolve!, reject!]; +};