diff --git a/src/background/background.ts b/src/background/background.ts index 27b4c8f..4537b8e 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -8,7 +8,7 @@ let con = new BackgroundMessenger(); const scraper = new GTranslateScraper(); con.addMessageListener("translate", async (toTranslate, sender) => - scraper.translate(toTranslate, await getCurrentLanguages(), sender.tab?.id) + scraper.translate(toTranslate, await getCurrentLanguages(), sender!.tab?.id) ); con.addMessageListener("getLanguages", () => scraper.languages); @@ -62,8 +62,8 @@ const injectScript = async (tabID: number, enabled?: boolean) => { con.addMessageListener( "setEnabled", - async (v: boolean, s: Runtime.MessageSender) => { - const tab = await getTabID(s); + async (v: boolean, s?: Runtime.MessageSender) => { + const tab = await getTabID(s!); if ((await isEnabledSession(tab)) == v) return; injectScript(tab); diff --git a/src/background/gtranslate_content_script.ts b/src/background/gtranslate_content_script.ts index 5a64915..1a2b8d8 100644 --- a/src/background/gtranslate_content_script.ts +++ b/src/background/gtranslate_content_script.ts @@ -1,4 +1,4 @@ -import { browser } from "webextension-polyfill-ts"; +import { ContentScriptMessenger } from "./gtranslate_commands"; export enum MessageKinds { translationFinished = "translation", @@ -18,11 +18,7 @@ export type Message = TranslationMessage | LanguageListMessage; if (window.location.host != "translate.google.com" || window.hasRun) return; window.hasRun = true; - const conn = browser.runtime.connect({ - name: "gtranslate_scraper_conn", - }); - - const sendMessage = (m: Message) => conn.postMessage(m); + const conn = new ContentScriptMessenger(); const src = document.querySelector("textarea#source"); const result = ( @@ -39,7 +35,6 @@ export type Message = TranslationMessage | LanguageListMessage; const isTranslating = () => result.classList.contains("translating"); const observer = new MutationObserver((mutations) => { - console.log(mutations); const wasTranslating = mutations.some((m) => m.oldValue!.split(" ").some((c) => c == "translating") ); @@ -54,22 +49,19 @@ export type Message = TranslationMessage | LanguageListMessage; }); const sendTranslation = () => { - sendMessage({ - messageKind: MessageKinds.translationFinished, - translation: { - src: src.value, - result: result.innerText.trim(), - languages: { - srcLang: srcLang, - dstLang: dstLang, - }, + conn.runCommand("translationFinished", { + src: src.value, + result: result.innerText.trim(), + languages: { + srcLang: srcLang, + dstLang: dstLang, }, }); }; - conn.onMessage.addListener((to_translate: string) => { - if (src.value == to_translate && !isTranslating()) sendTranslation(); - else src.value = to_translate; + conn.addMessageListener("translate", (toTranslate) => { + if (src.value == toTranslate && !isTranslating()) sendTranslation(); + else src.value = toTranslate; }); const getLanguages = () => { @@ -83,8 +75,5 @@ export type Message = TranslationMessage | LanguageListMessage; return { code: code!, name: name! }; }); }; - sendMessage({ - messageKind: MessageKinds.languageList, - languages: getLanguages(), - }); + conn.runCommand("languageList", getLanguages()); })(); diff --git a/src/background/gtranslate_scraper.ts b/src/background/gtranslate_scraper.ts index a710498..a469c45 100644 --- a/src/background/gtranslate_scraper.ts +++ b/src/background/gtranslate_scraper.ts @@ -1,6 +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"; +import { BackgroundMessenger } from "./gtranslate_commands"; import { newPromise } from "../utils"; interface TranslationRequest { @@ -12,42 +12,50 @@ interface TranslationRequest { } export class GTranslateScraper { - private conn?: Runtime.Port; private queue: Array = []; + private current?: TranslationRequest; private iframe = document.createElement("iframe"); + private current_lang: LanguagePair | undefined; + languages: Promise>; - lang: LanguagePair | undefined; + msg: BackgroundMessenger; constructor() { let languagesResolve: (value: Array) => void; [this.languages, languagesResolve] = newPromise>(); - browser.runtime.onConnect.addListener((p) => { - if (p.name != "gtranslate_scraper_conn") return; - this.conn = p; - this.processFromQueue(p); - p.onMessage.addListener((m: Message) => { - switch (m.messageKind) { - case MessageKinds.languageList: - languagesResolve(m.languages); - return; - case MessageKinds.translationFinished: - this.onTranslationRecieved(m.translation); - } - }); - }); - this.loadIframe("de", "en"); //Sample languages so we can get the language list + this.msg = new BackgroundMessenger(); + this.msg.addMessageListener("languageList", languagesResolve); + this.msg.addMessageListener("translationFinished", (trans) => + this.onTranslationRecieved(trans) + ); + + //This allows us to create the google translate iframe by removing the x-frame-options header + browser.webRequest.onHeadersReceived.addListener( + allowIFrameAccess, + { + urls: ["https://translate.google.com/?*"], + types: ["sub_frame"], + }, + ["blocking", "responseHeaders"] + ); + + this.ChangeLanguage("de", "en"); //Sample languages so we can get the language list } - private loadIframe(srcLangCode: string, dstLangCode: string) { + private async ChangeLanguage( + srcLangCode: string, + dstLangCode: string + ): Promise { if (!document.body.contains(this.iframe)) document.body.appendChild(this.iframe); - this.conn = undefined; + + const [connectedPromise, onConnected] = newPromise(); //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: [""], + matches: ["https://translate.google.com/?*"], allFrames: true, js: [ { @@ -57,39 +65,60 @@ export class GTranslateScraper { runAt: "document_end", }) .then((r) => { - this.iframe.addEventListener("load", r.unregister, { once: true }); + this.iframe.addEventListener( + "load", + () => { + r.unregister; + this.msg.conn.then(() => onConnected()); + }, + { 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( - allowIFrameAccess, - { - urls: [""], - types: ["sub_frame"], - }, - ["blocking", "responseHeaders"] - ); + return connectedPromise; } private onTranslationRecieved(trans: Translation) { - const current = this.queue.pop(); - if (current) current.resolveFn(trans); - this.processFromQueue(this.conn!); + if (this.current?.toTranslate !== trans.src) + //I don't know how to code well so I gotta put in sanity checks like this to make debugging easier + console.error( + "Current request changed before OnTranslationRecieved was called. This should never happen" + ); + if (this.current) this.current.resolveFn(trans); + this.current = undefined; + this.processFromQueue(); } - private processFromQueue(conn: Runtime.Port) { - const request = this.queue.slice(-1).pop(); - if (!request) return; + private async processFromQueue() { + if (this.current) return; + this.current = this.queue.pop(); + if (!this.current) 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); + this.processCurrent(); } - translate( + private async processCurrent() { + if (!this.current) + throw "ProcessCurrent called while current translation request is undefined"; + const cur = this.current; + + let promise = Promise.resolve(); + if (this.current_lang != this.current.lang) { + this.current_lang = cur.lang; + + promise = this.ChangeLanguage( + this.current_lang.srcLang.code, + this.current_lang.dstLang.code + ); + } + await promise; + if (cur == this.current) this.msg.runCommand("translate", cur.toTranslate); + else + throw "The current request changed while processCurrent was running. This should never happen"; + } + + async translate( toTranslate: string, langs: LanguagePair, tabID?: number @@ -119,9 +148,14 @@ export class GTranslateScraper { return true; } }); - this.queue.push(req); - if (this.queue.length == 1 && this.conn) this.processFromQueue(this.conn); + this.queue.push(req); + + if (this.queue.length == 1 && !this.current) { + this.current = this.queue.pop(); + await this.msg.conn; + this.processCurrent(); + } return promise; } } diff --git a/src/background_frontend_commands.ts b/src/background_frontend_commands.ts index 750e233..7bb70f9 100644 --- a/src/background_frontend_commands.ts +++ b/src/background_frontend_commands.ts @@ -1,7 +1,7 @@ import { Communicator, commandFunction, commandList } from "./communicator"; import { browser } from "webextension-polyfill-ts"; -export interface commands extends commandList { +interface BackgroundCommands extends commandList { setEnabled: setEnabled; getEnabled: getEnabled; translate: translate; @@ -12,6 +12,10 @@ export interface commands extends commandList { setCurrentLanguages: setCurrentLanguages; } +interface FrontendCommands extends commandList { + setEnabled: setEnabled; +} + interface setEnabled extends commandFunction { args: boolean; } @@ -46,14 +50,17 @@ interface setCurrentLanguages extends commandFunction { args: Partial; } -export class BackgroundMessenger extends Communicator { +export class BackgroundMessenger extends Communicator< + BackgroundCommands, + FrontendCommands +> { constructor() { super(); browser.runtime.onMessage.addListener((m, s) => this.onMessage(m, s)); } - runCommand( + runCommand( command: K, - args: commands[K]["args"], + args: BackgroundCommands[K]["args"], tabID: number ) { const msg = this.getCommandMessage(command, args); @@ -61,16 +68,19 @@ export class BackgroundMessenger extends Communicator { } } -export class FrontendMessenger extends Communicator { +export class FrontendMessenger extends Communicator< + FrontendCommands, + BackgroundCommands +> { constructor() { super(); browser.runtime.onMessage.addListener((m, s) => this.onMessage(m, s)); } - runCommand( + runCommand( command: K, - args: commands[K]["args"] - ): commands[K]["return"] { + args: BackgroundCommands[K]["args"] + ): BackgroundCommands[K]["return"] { const message = this.getCommandMessage(command, args); return browser.runtime.sendMessage(message); } diff --git a/src/communicator.ts b/src/communicator.ts index 9ad68ab..1abcff4 100644 --- a/src/communicator.ts +++ b/src/communicator.ts @@ -16,35 +16,47 @@ interface commandMessage { declare type listener = ( args: T[K]["args"], - s: Runtime.MessageSender -) => Promise; + s?: Runtime.MessageSender +) => Promise | T[K]["return"]; -export abstract class Communicator { - private listeners = new Map>(); +export abstract class Communicator< + RecvCommands extends commandList, + SendCommands extends commandList +> { + private listeners = new Map< + keyof RecvCommands, + listener + >(); - abstract runCommand( + abstract async runCommand( command: K, - args: T[K]["args"], + args: SendCommands[K]["args"], ...rest: any[] - ): Promise; + ): Promise; - addMessageListener(command: K, callback: listener) { + addMessageListener( + command: K, + callback: listener + ) { this.listeners.set(command, callback); } - getCommandMessage( + protected getCommandMessage( command: K, - ...args: T[K]["args"] - ): commandMessage { + ...args: RecvCommands[K]["args"] + ): commandMessage { return { name: command, args: args }; } - onMessage( - m: commandMessage, - s: Runtime.MessageSender + protected onMessage( + m: commandMessage, + s?: Runtime.MessageSender ) { let listener = this.listeners.get(m.name); - let args: [T[keyof T]["args"], Runtime.MessageSender] = m.args; + let args: [ + RecvCommands[keyof RecvCommands]["args"], + Runtime.MessageSender + ] = m.args; if (args[0] === undefined) args.pop(); args.push(s); if (listener) return listener.apply(null, args);