Update dependencies

This commit is contained in:
bad 2022-04-21 20:01:59 +02:00
parent c0bad165a2
commit 872ca6d576
19 changed files with 2356 additions and 5020 deletions

View File

@ -6,7 +6,6 @@ module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
],
rules: {
"@typescript-eslint/no-non-null-assertion": "off",

View File

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

View File

@ -4,42 +4,44 @@
"description": "Lingo is a small browser extension to help you learn languages",
"main": "webpack.config.js",
"dependencies": {
"@asmagin/google-translate-api": "^8.0.2",
"@sveltejs/svelte-virtual-list": "^3.0.1",
"svelte-feather-icons": "^3.2.2",
"svelte-select": "^3.11.0",
"tippy.js": "^6.2.5"
"svelte-feather-icons": "^4.0.0",
"svelte-select": "^4.4.7",
"tippy.js": "^6.2.5",
"webextension-polyfill": "^0.9.0"
},
"devDependencies": {
"@tsconfig/svelte": "^1.0.8",
"@typescript-eslint/eslint-plugin": "^3.9.1",
"@typescript-eslint/parser": "^3.9.1",
"copy-webpack-plugin": "^6.0.3",
"css-loader": "^4.0.0",
"eslint": "^7.7.0",
"eslint-config-prettier": "^6.11.0",
"@tsconfig/svelte": "^3.0.0",
"@types/webextension-polyfill": "^0.8.3",
"@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.20.0",
"copy-webpack-plugin": "^10.2.4",
"css-loader": "^6.7.1",
"eslint": "^8.13.0",
"eslint-config-prettier": "^8.5.0",
"file-loader": "^6.0.0",
"html-loader": "^1.1.0",
"html-webpack-plugin": "4.3.0",
"html-loader": "^3.1.0",
"html-webpack-plugin": "^5.5.0",
"husky": ">=4",
"lint-staged": ">=10",
"prettier": "^2.0.5",
"prettier-plugin-svelte": "^1.1.0",
"prettier-plugin-svelte": "^2.7.0",
"raw-loader": "^4.0.1",
"sass": "^1.26.10",
"sass-loader": "^9.0.2",
"style-loader": "^1.2.1",
"sass-loader": "^12.6.0",
"style-loader": "^3.3.1",
"svelte": "^3.24.1",
"svelte-check": "^1.0.11",
"svelte-loader": "^2.13.6",
"svelte-check": "^2.7.0",
"svelte-loader": "^3.1.2",
"svelte-preprocess": "^4.0.10",
"ts-loader": "^8.0.1",
"typescript": "^3.9.7",
"web-ext": "^4.3.0",
"webextension-polyfill-ts": "^0.19.0",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12",
"worklet-loader": "^1.0.0",
"zip-webpack-plugin": "^3.0.0"
"ts-loader": "^9.2.8",
"typescript": "^4.6.3",
"web-ext": "^6.8.0",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2",
"worklet-loader": "^2.0.0",
"zip-webpack-plugin": "^4.0.1"
},
"scripts": {
"build": "webpack",
@ -48,11 +50,6 @@
"keywords": [],
"author": "",
"license": "ISC",
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*/**": [
"eslint --fix ",

View File

@ -1,21 +1,17 @@
import { browser, Runtime } from "webextension-polyfill-ts";
import { GTranslateScraper } from "./gtranslate_scraper";
import browser, { type Runtime } from "webextension-polyfill";
import { GTranslate } from "./translate";
import { Flashcards } from "./database";
import { BackgroundMessenger } from "../background_frontend_commands";
import content_script from "../frontend/content_script/content_script.ts?file";
const con = new BackgroundMessenger();
const scraper = new GTranslateScraper();
const scraper = new GTranslate();
con.addMessageListener("translate", async (toTranslate, sender) => {
return scraper.translate(
toTranslate,
await getCurrentLanguages(),
sender!.tab?.id
);
return scraper.translate(toTranslate, await getCurrentLanguages());
});
con.addMessageListener("getLanguages", () => scraper.languages);
con.addMessageListener("getLanguages", () => scraper.get_languages());
const db = new Flashcards();
window.flashcardDB = db;
@ -23,7 +19,7 @@ con.addMessageListener("addFlashcard", (c) => db.addFlashcard(c));
con.addMessageListener("removeFlashcard", (c) => db.removeFlashcard(c));
const getCurrentLanguages = async () => {
const langs = await scraper.languages;
const langs = await scraper.get_languages();
const srcLangCode =
(await browser.storage.local.get("srcLang"))["srcLang"] ?? "de";
const dstLangCode =

View File

@ -1,76 +0,0 @@
import { browser, Runtime } from "webextension-polyfill-ts";
import { Communicator, commandList, commandFunction } from "../communicator";
import { newPromise } from "../utils";
interface BackgroundCommands extends commandList {
translationFinished: TranslationMessage;
languageList: LanguageListMessage;
}
interface ContentScriptCommands extends commandList {
translationRequest: TranslationRequest;
}
interface TranslationMessage extends commandFunction {
args: Translation;
}
interface LanguageListMessage extends commandFunction {
args: Array<Language>;
}
interface TranslationRequest extends commandFunction {
args: string;
}
export class BackgroundMessenger extends Communicator<
BackgroundCommands,
ContentScriptCommands
> {
conn: Promise<Runtime.Port>;
private resolvConn: (p: Runtime.Port) => void;
constructor() {
super();
[this.conn, this.resolvConn] = newPromise<Runtime.Port>();
browser.runtime.onConnect.addListener((p) => {
if (p.name != "gtranslate_scraper_conn") return;
this.resolvConn(p);
p.onDisconnect.addListener(
() => ([this.conn, this.resolvConn] = newPromise<Runtime.Port>())
);
p.onMessage.addListener((m) => this.onMessage(m, p.sender!));
});
}
async runCommand<K extends keyof ContentScriptCommands>(
command: K,
args: ContentScriptCommands[K]["args"]
) {
const msg = this.getCommandMessage(command, args);
(await this.conn).postMessage(msg);
}
}
export class ContentScriptMessenger extends Communicator<
ContentScriptCommands,
BackgroundCommands
> {
conn: Runtime.Port;
constructor() {
super();
this.conn = browser.runtime.connect({
name: "gtranslate_scraper_conn",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any); //The declaration file is wrong
this.conn.onMessage.addListener((m) => this.onMessage(m));
}
async runCommand<K extends keyof BackgroundCommands>(
command: K,
args: BackgroundCommands[K]["args"]
) {
const msg = this.getCommandMessage(command, args);
return this.conn.postMessage(msg);
}
}

View File

@ -1,65 +0,0 @@
import { ContentScriptMessenger } from "./gtranslate_commands";
(async () => {
if (window.location.host != "translate.google.com" || window.hasRun) return;
window.hasRun = true;
const conn = new ContentScriptMessenger();
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) => {
const wasTranslating = mutations.some((m) =>
m.oldValue!.split(" ").some((c) => c == "translating")
);
if (wasTranslating && !isTranslating()) {
sendTranslation();
}
});
observer.observe(result, {
attributes: true,
attributeOldValue: true,
attributeFilter: ["class"],
});
const sendTranslation = () => {
conn.runCommand("translationFinished", {
src: src.value,
result: result.innerText.trim(),
languages: {
srcLang: srcLang,
dstLang: dstLang,
},
});
};
conn.addMessageListener("translate", (toTranslate) => {
if (src.value == toTranslate && !isTranslating()) sendTranslation();
else src.value = toTranslate;
});
const getLanguages = () => {
const langs = document.querySelectorAll(
"div.language_list_section:nth-child(3) > .language_list_item_wrapper"
);
return Array.from(langs).map((l) => {
const code = l.classList[1].split("-").pop();
const name = l.querySelector(".language_list_item_language_name")!
.textContent;
return { code: code!, name: name! };
});
};
conn.runCommand("languageList", getLanguages());
})();

View File

@ -1,177 +0,0 @@
import { browser, WebRequest } from "webextension-polyfill-ts";
import content_script from "./gtranslate_content_script.ts?file";
import { BackgroundMessenger } from "./gtranslate_commands";
import { newPromise } from "../utils";
interface TranslationRequest {
tabID?: number;
resolveFn: (value: Translation) => void;
rejectFn: (reason: string) => void;
toTranslate: string;
lang: LanguagePair;
}
export class GTranslateScraper {
private queue: Array<TranslationRequest> = [];
private current?: TranslationRequest;
private iframe = <HTMLIFrameElement>document.createElement("iframe");
private current_lang: LanguagePair | undefined;
languages: Promise<Array<Language>>;
msg: BackgroundMessenger;
constructor() {
let languagesResolve: (value: Array<Language>) => void;
[this.languages, languagesResolve] = newPromise<Array<Language>>();
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 async ChangeLanguage(
srcLangCode: string,
dstLangCode: string
): Promise<void> {
if (!document.body.contains(this.iframe))
document.body.appendChild(this.iframe);
const [connectedPromise, onConnected] = newPromise<void>();
//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: ["https://translate.google.com/?*"],
allFrames: true,
js: [
{
file: content_script,
},
],
runAt: "document_end",
})
.then((r) => {
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}`;
});
return connectedPromise;
}
private onTranslationRecieved(trans: Translation) {
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 async processFromQueue() {
if (this.current) return;
this.current = this.queue.pop();
if (!this.current) return;
this.processCurrent();
}
private async processCurrent() {
if (!this.current)
throw new Error(
"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 new Error(
"The current request changed while processCurrent was running. This should never happen"
);
}
async translate(
toTranslate: string,
langs: LanguagePair,
tabID?: number
): Promise<Translation> {
toTranslate = toTranslate.trim();
if (toTranslate == "")
return Promise.resolve({
src: "",
result: "",
languages: langs,
});
const [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.current) {
this.current = this.queue.pop();
await this.msg.conn;
this.processCurrent();
}
return promise;
}
}
const allowIFrameAccess = (info: WebRequest.OnHeadersReceivedDetailsType) => {
//Make sure that only the background page can access this iframe
if (info.documentUrl != document.URL) return;
let headers = info.responseHeaders;
headers = headers?.filter((header) => {
const h = header.name.toString().toLowerCase();
h != "x-frame-options" && h != "frame-options";
});
return { responseHeaders: headers };
};

View File

@ -0,0 +1,42 @@
import { newPromise } from "../utils";
import googleTranslateApi from "@asmagin/google-translate-api";
import translate from "@asmagin/google-translate-api";
const languages = googleTranslateApi.languages;
export abstract class Translator {
abstract translate(
toTranslate: string,
langs: LanguagePair
): Promise<Translation>;
abstract get_languages(): Promise<Language[]>;
}
export class GTranslate extends Translator {
async translate(
toTranslate: string,
langs: LanguagePair
): Promise<Translation> {
const resp = await translate(toTranslate, {
from: langs.srcLang.code,
to: langs.dstLang.code,
});
return {
src: resp.from.text.value,
languages: langs,
result: resp.text,
};
}
async get_languages() {
const res: Language[] = Object.keys(languages).map((k) => {
const v = (languages as any)[k as any] as string;
return {
code: k,
name: v,
};
});
return Promise.resolve(res);
}
}

View File

@ -1,5 +1,9 @@
import { Communicator, commandFunction, commandList } from "./communicator";
import { browser } from "webextension-polyfill-ts";
import {
Communicator,
type commandFunction,
type commandList,
} from "./communicator";
import browser from "webextension-polyfill";
interface BackgroundCommands extends commandList {
setEnabled: setEnabled;

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Runtime } from "webextension-polyfill-ts";
import type { Runtime } from "webextension-polyfill";
export interface commandFunction {
args: any;
@ -29,7 +29,7 @@ export abstract class Communicator<
listener<RecvCommands, keyof RecvCommands>
>();
abstract async runCommand<K extends keyof SendCommands>(
abstract runCommand<K extends keyof SendCommands>(
command: K,
args: SendCommands[K]["args"],
...rest: unknown[]

View File

@ -33,7 +33,6 @@
card
</button>
{/if}
</div>
<style lang="scss">

View File

@ -4,8 +4,8 @@
import type { Flashcards } from "../../background/database";
export let cardID: IDBValidKey;
const flashcardDB = getContext<Flashcards>("flashcardDB");
$: cardPromise = flashcardDB.getCard(cardID);
const flashcardDB = getContext<Promise<Flashcards>>("flashcardDB");
$: cardPromise = flashcardDB.then((db) => db.getCard(cardID));
</script>
<div class="container">
@ -36,7 +36,7 @@
padding: 1em;
margin: 0.1em;
height: 100%;
width: calc(100%-1vw);
width: calc(100% - 1vw);
background-color: var(--background-color-3);
font-family: sans-serif;
}

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { browser } from "webextension-polyfill-ts";
import browser from "webextension-polyfill";
import { setContext } from "svelte";
import VirtualList from "@sveltejs/svelte-virtual-list";
import VirtualList from "@sveltejs/svelte-virtual-list/VirtualList.svelte";
import Flashcard from "./Flashcard.svelte";
/*
const onCardChange = (card: Flashcard, change: "added" | "removed") => {
@ -19,14 +19,15 @@
*/
let cards: IDBValidKey[] = [];
const flashcardDBPromise = browser.runtime
.getBackgroundPage()
.then((bp) => bp.flashcardDB);
setContext("flashcardDB", flashcardDBPromise);
(async () => {
const backgroundPage = await browser.runtime.getBackgroundPage();
const flashcardDB = backgroundPage.flashcardDB;
const flashcardDB = await flashcardDBPromise;
cards = await flashcardDB.getAllKeys();
//flashcardDB.addChangeCallback(onCardChange);
//window.addEventListener("beforeunload", () => flashcardDB.removeChangeCallback(onCardChange));
setContext("flashcardDB", flashcardDB);
})();
</script>

View File

@ -1,11 +1,10 @@
<script lang="ts">
import Select from "svelte-select";
import Select from "svelte-select/src/Select.svelte";
export let langs: Array<Language>;
export let selectedValue: Language;
$: console.log(selectedValue);
const getLabel = (o: Language) => o.name;
const itemFilter = (label: string, filterText: string) =>
label.toLowerCase().startsWith(filterText.toLowerCase());
</script>
<div class="select-styling">
@ -14,9 +13,9 @@
getOptionLabel={getLabel}
getSelectionLabel={getLabel}
optionIdentifier="code"
{itemFilter}
isClearable={false}
bind:selectedValue />
bind:value={selectedValue}
/>
</div>
<style lang="scss">

View File

@ -16,10 +16,9 @@
</script>
<div id="container">
<label class:enabled>
<input type="checkbox" bind:checked={enabled} />
{enabled ? 'Disable Extension' : 'Enable Extension'}
{enabled ? "Disable Extension" : "Enable Extension"}
</label>
{#if selected}

1
src/global.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="svelte" />

View File

@ -6,6 +6,5 @@
"target": "ES6",
"strict": true
},
"include": ["src/**/*", "src/node_modules"],
"exclude": ["node_modules/*"]
"include": ["/**/*"]
}

View File

@ -61,6 +61,13 @@ let options = {
},
},
},
{
// required to prevent errors from Svelte on Webpack 5+, omit on Webpack 4
test: /node_modules\/svelte\/.*\.mjs$/,
resolve: {
fullySpecified: false,
},
},
],
},
plugins: [

6884
yarn.lock

File diff suppressed because it is too large Load Diff