New communicator

This commit is contained in:
a 2020-08-16 21:31:49 +02:00
parent 74fe885fd0
commit 72568aa9b5
8 changed files with 181 additions and 179 deletions

View file

@ -1,20 +1,21 @@
import { browser, Runtime } from "webextension-polyfill-ts"; import { browser, Runtime } from "webextension-polyfill-ts";
import { GTranslateScraper } from "./gtranslate_scraper"; import { GTranslateScraper } from "./gtranslate_scraper";
import { Flashcards } from "./database"; import { Flashcards } from "./database";
import { Communicator, commandKinds } from "../communication"; import { BackgroundMessenger } from "../background_frontend_commands";
import content_script from "../frontend/content_script/content_script.ts?file"; import content_script from "../frontend/content_script/content_script.ts?file";
let com = new Communicator(); let con = new BackgroundMessenger();
const scraper = new GTranslateScraper(); const scraper = new GTranslateScraper();
com.translateCallback = async (toTranslate, sender) => con.addMessageListener("translate", async (toTranslate, sender) =>
scraper.translate(toTranslate, await getCurrentLanguages(), sender.tab?.id); scraper.translate(toTranslate, await getCurrentLanguages(), sender.tab?.id)
);
com.getLanguagesCallback = () => scraper.languages; con.addMessageListener("getLanguages", () => scraper.languages);
const db = new Flashcards(); const db = new Flashcards();
com.addFlashcardCallback = (c) => db.addFlashcard(c); con.addMessageListener("addFlashcard", (c) => db.addFlashcard(c));
com.removeFlashcardCallback = (c) => db.removeFlashcard(c); con.addMessageListener("removeFlashcard", (c) => db.removeFlashcard(c));
const getCurrentLanguages = async () => { const getCurrentLanguages = async () => {
const langs = await scraper.languages; const langs = await scraper.languages;
@ -33,8 +34,8 @@ const setCurrentLanguages = async (l: Partial<LanguagePair>) => {
if (l.dstLang) await browser.storage.local.set({ dstLang: l.dstLang.code }); if (l.dstLang) await browser.storage.local.set({ dstLang: l.dstLang.code });
}; };
com.getCurrentLanguagesCallback = getCurrentLanguages; con.addMessageListener("getCurrentLanguages", getCurrentLanguages);
com.setCurrentLanguagesCallback = setCurrentLanguages; con.addMessageListener("setCurrentLanguages", 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;
@ -56,24 +57,24 @@ const injectScript = async (tabID: number, enabled?: boolean) => {
file: content_script, file: content_script,
}); });
if (enabled === undefined) enabled = await isEnabledSession(tabID); if (enabled === undefined) enabled = await isEnabledSession(tabID);
browser.tabs.sendMessage(tabID, { con.runCommand("setEnabled", enabled, tabID);
commandKind: commandKinds.setEnabled,
value: enabled,
});
}; };
com.setEnabledCallback = async (v: boolean, s: Runtime.MessageSender) => { con.addMessageListener(
"setEnabled",
async (v: boolean, s: Runtime.MessageSender) => {
const tab = await getTabID(s); const tab = await getTabID(s);
if ((await isEnabledSession(tab)) == v) return; if ((await isEnabledSession(tab)) == v) return;
injectScript(tab); injectScript(tab);
await setEnabledSession(tab, v); await setEnabledSession(tab, v);
}; }
);
com.getEnabledCallback = async (s: Runtime.MessageSender): Promise<boolean> => { con.addMessageListener("getEnabled", async (s: Runtime.MessageSender) => {
const tab = await getTabID(s); const tab = await getTabID(s);
return isEnabledSession(tab); return isEnabledSession(tab);
}; });
browser.tabs.onUpdated.addListener(async (tabID, changeInfo) => { browser.tabs.onUpdated.addListener(async (tabID, changeInfo) => {
if (changeInfo.status == "complete") { if (changeInfo.status == "complete") {

View file

@ -0,0 +1,77 @@
import { Communicator, commandFunction, commandList } from "./communicator";
import { browser } from "webextension-polyfill-ts";
export interface commands extends commandList {
setEnabled: setEnabled;
getEnabled: getEnabled;
translate: translate;
addFlashcard: addFlashcard;
removeFlashcard: removeFlashcard;
getLanguages: getLanguages;
getCurrentLanguages: getCurrentLanguages;
setCurrentLanguages: setCurrentLanguages;
}
interface setEnabled extends commandFunction {
args: boolean;
}
interface getEnabled extends commandFunction {
return: boolean;
}
interface getLanguages extends commandFunction {
return: Array<Language>;
}
interface translate extends commandFunction {
args: string;
return: Translation;
}
interface getCurrentLanguages extends commandFunction {
return: LanguagePair;
}
interface addFlashcard extends commandFunction {
args: Translation | Flashcard;
return: Flashcard;
}
interface removeFlashcard extends commandFunction {
args: Flashcard;
}
interface setCurrentLanguages extends commandFunction {
args: Partial<LanguagePair>;
}
export class BackgroundMessenger extends Communicator<commands> {
constructor() {
super();
browser.runtime.onMessage.addListener((m, s) => this.onMessage(m, s));
}
runCommand<K extends keyof commands>(
command: K,
args: commands[K]["args"],
tabID: number
) {
const msg = this.getCommandMessage(command, args);
return browser.tabs.sendMessage(tabID, msg);
}
}
export class FrontendMessenger extends Communicator<commands> {
constructor() {
super();
browser.runtime.onMessage.addListener((m, s) => this.onMessage(m, s));
}
runCommand<K extends keyof commands>(
command: K,
args: commands[K]["args"]
): commands[K]["return"] {
const message = this.getCommandMessage(command, args);
return browser.runtime.sendMessage(message);
}
}

View file

@ -1,138 +0,0 @@
//There has to be a better way to do this while remaining type safe...
import { browser, Runtime } from "webextension-polyfill-ts";
export class Communicator {
setEnabledCallback?: (value: boolean, sender: Runtime.MessageSender) => void;
getEnabledCallback?: (sender: Runtime.MessageSender) => Promise<boolean>;
translateCallback?: (
toTranslate: string,
sender: Runtime.MessageSender
) => Promise<Translation>;
addFlashcardCallback?: (
value: Translation | Flashcard,
sender: Runtime.MessageSender
) => Promise<Flashcard>;
removeFlashcardCallback?: (
value: Flashcard,
sender: Runtime.MessageSender
) => Promise<void>;
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) => {
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}`);
}
}
);
}
static setEnabled = (v: boolean) =>
sendMessage({ commandKind: commandKinds.setEnabled, value: v });
static getEnabled = (): Promise<boolean> =>
sendMessage({ commandKind: commandKinds.getEnabled });
static translate = (toTranslate: string): Promise<Translation> =>
sendMessage({
commandKind: commandKinds.translate,
toTranslate: toTranslate,
});
static addFlashcard = (card: Flashcard | Translation): Promise<Flashcard> =>
sendMessage({
commandKind: commandKinds.addFlashcard,
card: card,
});
static removeFlashcard = (card: Flashcard) =>
sendMessage({
commandKind: commandKinds.removeFlashcard,
card: card,
});
static getLanguages = (): Promise<Array<Language>> =>
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);
export enum commandKinds {
setEnabled = "setEnabled",
getEnabled = "getEnabled",
translate = "translate",
addFlashcard = "addFlashcard",
removeFlashcard = "removeFlashcard",
getLanguages = "getLanguages",
getCurrentLanguages = "getCurrentLanguage",
setCurrentLanguages = "setCurrentLanguages",
}
interface setEnabled {
commandKind: commandKinds.setEnabled;
value: boolean;
}
interface getCommand {
commandKind:
| commandKinds.getEnabled
| commandKinds.getLanguages
| commandKinds.getCurrentLanguages;
}
interface translateCommand {
commandKind: commandKinds.translate;
toTranslate: string;
}
interface addFlashcard {
commandKind: commandKinds.addFlashcard;
card: Translation | Flashcard;
}
interface removeFlashcard {
commandKind: commandKinds.removeFlashcard;
card: Flashcard;
}
interface setCurrentLanguages {
commandKind: commandKinds.setCurrentLanguages;
value: Partial<LanguagePair>;
}
export type command =
| setEnabled
| getCommand
| translateCommand
| addFlashcard
| removeFlashcard
| setCurrentLanguages;

52
src/communicator.ts Normal file
View file

@ -0,0 +1,52 @@
import type { Runtime } from "webextension-polyfill-ts";
export interface commandFunction {
args: any;
return: any;
}
export interface commandList {
[functionName: string]: commandFunction;
}
interface commandMessage<T extends commandList, K extends keyof T> {
name: K;
args: T[K]["args"];
}
declare type listener<T extends commandList, K extends keyof T> = (
args: T[K]["args"],
s: Runtime.MessageSender
) => Promise<T[K]["return"]>;
export abstract class Communicator<T extends commandList> {
private listeners = new Map<keyof T, listener<T, keyof T>>();
abstract runCommand<K extends keyof T>(
command: K,
args: T[K]["args"],
...rest: any[]
): Promise<T[K]["return"]>;
addMessageListener<K extends keyof T>(command: K, callback: listener<T, K>) {
this.listeners.set(command, <any>callback);
}
getCommandMessage<K extends keyof T>(
command: K,
...args: T[K]["args"]
): commandMessage<T, K> {
return { name: command, args: args };
}
onMessage<K extends keyof T>(
m: commandMessage<T, K>,
s: Runtime.MessageSender
) {
let listener = this.listeners.get(m.name);
let args: [T[keyof T]["args"], Runtime.MessageSender] = m.args;
if (args[0] === undefined) args.pop();
args.push(s);
if (listener) return listener.apply(null, args);
}
}

View file

@ -1,16 +1,20 @@
<script lang="ts"> <script lang="ts">
import Spinner from "./Spinner.svelte"; import Spinner from "./Spinner.svelte";
import Translated from "./Translated.svelte"; import Translated from "./Translated.svelte";
import { Communicator } from "../../communication"; import { createEventDispatcher, tick, setContext } from "svelte";
import { createEventDispatcher, tick } from "svelte"; import type { FrontendMessenger } from "../../background_frontend_commands";
export let toTranslate: string; export let toTranslate: string;
export let connection: FrontendMessenger;
setContext("connection", connection);
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let trans_promise: Promise<Translation>; let trans_promise: Promise<Translation>;
$: { $: {
trans_promise = Communicator.translate(toTranslate).then((r) => { trans_promise = connection
.runCommand("translate", toTranslate)
.then((r) => {
tick().then(() => dispatch("translationFinished")); tick().then(() => dispatch("translationFinished"));
return r; return r;
}); });
@ -21,4 +25,6 @@
<Spinner /> <Spinner />
{:then trans} {:then trans}
<Translated {trans} /> <Translated {trans} />
{:catch}
Error
{/await} {/await}

View file

@ -1,15 +1,17 @@
<script lang="ts"> <script lang="ts">
import { Communicator } from "../../communication"; import { getContext } from "svelte";
import type { FrontendMessenger } from "../../background_frontend_commands";
let connection = getContext("connection") as FrontendMessenger;
export let trans: Translation; export let trans: Translation;
let card: Flashcard | undefined; let card: Flashcard | undefined;
const addFlashcard = async () => { const addFlashcard = async () => {
card = await Communicator.addFlashcard(trans); card = await connection.runCommand("addFlashcard", trans);
}; };
const removeFlashcard = async () => { const removeFlashcard = async () => {
if (card) await Communicator.removeFlashcard(card); if (card) await connection.runCommand("removeFlashcard", card);
card = undefined; card = undefined;
}; };
</script> </script>

View file

@ -1,13 +1,15 @@
import { Communicator } from "../../communication";
import tippy, { roundArrow } from "tippy.js"; import tippy, { roundArrow } from "tippy.js";
import App from "./Translate.svelte"; import App from "./Translate.svelte";
import "./tippy.scss"; import "./tippy.scss";
import { FrontendMessenger } from "../../background_frontend_commands";
(() => { (() => {
//Make sure our script only runs once //Make sure our script only runs once
if (window.hasRun) return; if (window.hasRun) return;
window.hasRun = true; window.hasRun = true;
let con = new FrontendMessenger();
const tippyInstance = setupTippy(); const tippyInstance = setupTippy();
let timeoutID: number; let timeoutID: number;
@ -18,20 +20,19 @@ import "./tippy.scss";
timeoutID = window.setTimeout(tippyInstance.show, 500); timeoutID = window.setTimeout(tippyInstance.show, 500);
}; };
let con = new Communicator(); con.addMessageListener("setEnabled", async (v: boolean) => {
con.setEnabledCallback = (v: boolean) => {
document.removeEventListener("selectionchange", onSelectionChange); //Always remove it avoid duplicate event listeners document.removeEventListener("selectionchange", onSelectionChange); //Always remove it avoid duplicate event listeners
if (v) { if (v) {
onSelectionChange(); // Call it since selection may have changed since it was enabled onSelectionChange(); // Call it since selection may have changed since it was enabled
document.addEventListener("selectionchange", onSelectionChange); document.addEventListener("selectionchange", onSelectionChange);
} else tippyInstance.hide(); } else tippyInstance.hide();
}; });
function setupTippy() { function setupTippy() {
const svelteElement = document.createElement("div"); const svelteElement = document.createElement("div");
const sveleteApp = new App({ const sveleteApp = new App({
target: svelteElement, target: svelteElement,
props: { toTranslate: "" }, props: { toTranslate: "", connection: con },
}); });
return tippy(document.body, { return tippy(document.body, {
content: svelteElement, content: svelteElement,

View file

@ -1,17 +1,18 @@
<script lang="ts"> <script lang="ts">
import { Communicator } from "../../communication";
import LanguageSelection from "./LanguageSelection.svelte"; import LanguageSelection from "./LanguageSelection.svelte";
import { FrontendMessenger } from "../../background_frontend_commands";
Communicator.getEnabled().then((v) => (enabled = v)); const conn = new FrontendMessenger();
conn.runCommand("getEnabled", undefined).then((v) => (enabled = v));
const languages = Communicator.getLanguages(); const languages = conn.runCommand("getLanguages", undefined);
let enabled: boolean; let enabled: boolean;
$: if (enabled !== undefined) Communicator.setEnabled(enabled); $: if (enabled !== undefined) conn.runCommand("setEnabled", enabled);
let selected: LanguagePair; let selected: LanguagePair;
Communicator.getCurrentLanguages().then((l) => (selected = l)); conn.runCommand("getCurrentLanguages", undefined).then((l) => (selected = l));
$: if (selected) Communicator.setCurrentLanguages(selected); $: if (selected) conn.runCommand("setCurrentLanguages", selected);
</script> </script>
<div id="container"> <div id="container">