From d9cc66444a328dd4308afc0518d5e8edd9c2fcf3 Mon Sep 17 00:00:00 2001 From: Wendelin Date: Tue, 16 Jan 2024 15:19:01 +0100 Subject: [PATCH 1/8] merge background script and trektor.js; migrate to manifest v3 --- background.js | 76 ------------------------------------------ content_script.js | 8 ++--- manifest.json | 18 ++++------ trektor.js | 84 ++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 92 insertions(+), 94 deletions(-) delete mode 100644 background.js diff --git a/background.js b/background.js deleted file mode 100644 index 2b0086b..0000000 --- a/background.js +++ /dev/null @@ -1,76 +0,0 @@ -browser.runtime.onMessage.addListener(async (msg) => { - switch (msg.action) { - case 'track': - await track(...msg.args); - return; - case 'addTask': - await addTask(...msg.args); - return; - default: - throw new Error(`unknown action: ${msg.action}`); - } -}); - -async function track(cardId) { - const task = await addTask(cardId); - const card = await trektor.trelloGateway.getCard(cardId); - const cardName = stripStoryPointsAndTaskToken(card.name); - const response = await trektor.togglGateway.startTimeEntry(task.id, cardName); - return response.data; -} - -async function addTask(cardId) { - const card = await trektor.trelloGateway.getCard(cardId); - - const taskPrefixes = card.labels - .map((label) => label.name.match(/(?<=#)[a-z0-9]+$/)?.[0]) - .filter((prefix) => prefix !== undefined); - - if (taskPrefixes.length === 0) { - throw new Error('Card has no valid project labels.'); - } - if (taskPrefixes.length > 1) { - throw new Error('Card has multiple project labels.'); - } - const taskPrefix = taskPrefixes[0]; - - let taskName = card.name.match(/(?<=#)[A-Za-z0-9_-]+/)?.[0]; - - if (taskName === undefined) { - taskName = `${taskPrefix}_${card.idShort}`; - - await trektor.trelloGateway.updateCard(card.id, { - name: `${card.name} #${taskName}`, - }); - } - - const workspaces = await trektor.togglGateway.getWorkspaces(); - - if (workspaces.length === 0) { - throw new Error('Could not find any toggl workspaces.'); - } - if (workspaces.length > 1) { - throw new Error('Found multiple toggl workspaces. Not sure how to deal with that...'); - } - const allProjects = await trektor.togglGateway.getProjects(workspaces[0].id); - const projects = allProjects.filter((project) => project.name.endsWith(`(${taskPrefix})`)); - - if (projects.length === 0) { - throw new Error('Could not find any matching toggl project.'); - } - if (projects.length > 1) { - throw new Error('Found multiple matching toggl projects. Not sure how to deal with that...'); - } - const tasks = await trektor.togglGateway.getTasks(projects[0].id); - const task = tasks.find((task) => task.name === taskName); - if (task !== undefined) return task; - - const response = await trektor.togglGateway.createTask(projects[0].id, taskName) - return response.data; -} - -function stripStoryPointsAndTaskToken(cardName) { - return cardName - .replace(/^(\s*\(\d+\))?\s*/, '') // story points, e.g. (3) - .replace(/\s*#[a-z0-9_]+\s*$/, ''); // task token, e.g. #orga_5417 -} diff --git a/content_script.js b/content_script.js index 1fcd717..612fd8e 100644 --- a/content_script.js +++ b/content_script.js @@ -35,7 +35,7 @@ async function addButton() { try { await browser.runtime.sendMessage({ - action: "track", + action: 'track', args: [window.location.pathname.split("/", 3)[2]], }); trackButtonIcon.classList.replace("icon-clock", "icon-check-circle"); @@ -50,7 +50,7 @@ async function addButton() { addButton.addEventListener("click", async () => { try { await browser.runtime.sendMessage({ - action: "addTask", + action: 'addTask', args: [window.location.pathname.split("/", 3)[2]], }); } catch (err) { @@ -82,7 +82,3 @@ function awaitSelector(selector, timeout) { window.addEventListener("pushstate", () => { if (window.location.pathname.startsWith("/c/")) addButton(); }); - -window.addEventListener('load', () => { - if (window.location.pathname.startsWith("/c/")) addButton(); -}); diff --git a/manifest.json b/manifest.json index a04da4a..63dd70e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,13 +1,8 @@ { - "manifest_version": 2, + "manifest_version": 3, "name": "Trektor", "description": "Browser-Extension zum automatischen Anlegen von Toggl tracking tasks", "version": "0.0.11", - "browser_specific_settings": { - "gecko": { - "id": "trektor@aboutsource.net" - } - }, "icons": { "64": "icons/64.png" }, @@ -26,17 +21,18 @@ } ], "background": { + "service_worker": "trektor.js", "scripts": [ - "vendor/browser-polyfill.js", - "trektor.js", - "background.js" + "trektor.js" ] }, "permissions": [ - "https://api.trello.com/*", - "https://api.track.toggl.com/*", "storage" ], + "host_permissions": [ + "https://api.trello.com/*", + "https://api.track.toggl.com/*" + ], "options_ui": { "page": "options/index.html" } diff --git a/trektor.js b/trektor.js index cc5d72c..3e20fa8 100644 --- a/trektor.js +++ b/trektor.js @@ -1,3 +1,7 @@ +if (typeof browser == "undefined") { + globalThis.browser = chrome +} + class TrelloGateway { static ENDPOINT = "https://api.trello.com/1"; static API_KEY = "2379d540412e417f6f0696c1397f38a6"; @@ -95,7 +99,85 @@ class TogglGateway { } } -window.trektor = { +var trektor = { trelloGateway: new TrelloGateway(browser.storage.local), togglGateway: new TogglGateway(browser.storage.local), }; + + +browser.runtime.onMessage.addListener(async (msg) => { + switch (msg.action) { + case 'track': + await track(...msg.args); + return; + case 'addTask': + await addTask(...msg.args); + return; + default: + throw new Error(`unknown action: ${msg.action}`); + } +}); + +async function track(cardId) { + const task = await addTask(cardId); + const card = await trektor.trelloGateway.getCard(cardId); + const cardName = stripStoryPointsAndTaskToken(card.name); + const response = await trektor.togglGateway.startTimeEntry(task.id, cardName); + return response.data; +} + +async function addTask(cardId) { + const card = await trektor.trelloGateway.getCard(cardId); + + const taskPrefixes = card.labels + .map((label) => label.name.match(/(?<=#)[a-z0-9]+$/)?.[0]) + .filter((prefix) => prefix !== undefined); + + if (taskPrefixes.length === 0) { + throw new Error('Card has no valid project labels.'); + } + if (taskPrefixes.length > 1) { + throw new Error('Card has multiple project labels.'); + } + const taskPrefix = taskPrefixes[0]; + + let taskName = card.name.match(/(?<=#)[A-Za-z0-9_-]+/)?.[0]; + + if (taskName === undefined) { + taskName = `${taskPrefix}_${card.idShort}`; + + await trektor.trelloGateway.updateCard(card.id, { + name: `${card.name} #${taskName}`, + }); + } + + const workspaces = await trektor.togglGateway.getWorkspaces(); + + if (workspaces.length === 0) { + throw new Error('Could not find any toggl workspaces.'); + } + if (workspaces.length > 1) { + throw new Error('Found multiple toggl workspaces. Not sure how to deal with that...'); + } + const allProjects = await trektor.togglGateway.getProjects(workspaces[0].id); + const projects = allProjects.filter((project) => project.name.endsWith(`(${taskPrefix})`)); + + if (projects.length === 0) { + throw new Error('Could not find any matching toggl project.'); + } + if (projects.length > 1) { + throw new Error('Found multiple matching toggl projects. Not sure how to deal with that...'); + } + const tasks = await trektor.togglGateway.getTasks(projects[0].id); + const task = tasks.find((task) => task.name === taskName); + if (task !== undefined) return task; + + const response = await trektor.togglGateway.createTask(projects[0].id, taskName) + return response.data; +} + +function stripStoryPointsAndTaskToken(cardName) { + return cardName + .replace(/^(\s*\(\d+\))?\s*/, '') // story points, e.g. (3) + .replace(/\s*#[a-z0-9_]+\s*$/, ''); // task token, e.g. #orga_5417 +} From 446a5f202578fc794e9b9df73901acb258d35069 Mon Sep 17 00:00:00 2001 From: Wendelin Date: Wed, 17 Jan 2024 12:38:02 +0100 Subject: [PATCH 2/8] keep background script and trektor.js seperate; make backgroundScript a class; move scripts to seperate folder --- manifest.json | 8 +- scripts/background.js | 89 +++++++++ scripts/chromium.js | 3 + .../content_script.js | 4 + scripts/firefox.js | 1 + scripts/trektor.js | 107 ++++++++++ trektor.js | 183 ------------------ 7 files changed, 209 insertions(+), 186 deletions(-) create mode 100644 scripts/background.js create mode 100644 scripts/chromium.js rename content_script.js => scripts/content_script.js (96%) create mode 100644 scripts/firefox.js create mode 100644 scripts/trektor.js delete mode 100644 trektor.js diff --git a/manifest.json b/manifest.json index 63dd70e..44291d2 100644 --- a/manifest.json +++ b/manifest.json @@ -13,7 +13,7 @@ ], "js": [ "vendor/browser-polyfill.js", - "content_script.js" + "scripts/content_script.js" ], "css": [ "content_style.css" @@ -21,9 +21,11 @@ } ], "background": { - "service_worker": "trektor.js", + "service_worker": "scripts/chromium.js", "scripts": [ - "trektor.js" + "scripts/trektor.js", + "scripts/background.js", + "scripts/firefox.js" ] }, "permissions": [ diff --git a/scripts/background.js b/scripts/background.js new file mode 100644 index 0000000..86c137d --- /dev/null +++ b/scripts/background.js @@ -0,0 +1,89 @@ +if (typeof browser == "undefined") { + globalThis.browser = chrome +} + +class BackgroundScript { + + static trektor; + + static init(trektor) { + BackgroundScript.trektor = trektor + + browser.runtime.onMessage.addListener(async (msg) => { + switch (msg.action) { + case 'track': + await BackgroundScript.track(...msg.args); + return; + case 'addTask': + await BackgroundScript.addTask(...msg.args); + return; + default: + throw new Error(`unknown action: ${msg.action}`); + } + }); + } + + static async track(cardId) { + const task = await BackgroundScript.addTask(cardId); + const card = await BackgroundScript.trektor.trelloGateway.getCard(cardId); + const cardName = BackgroundScript.stripStoryPointsAndTaskToken(card.name); + const response = await BackgroundScript.trektor.togglGateway.startTimeEntry(task.id, cardName); + return response.data; + } + + static async addTask(cardId) { + const card = await BackgroundScript.trektor.trelloGateway.getCard(cardId); + + const taskPrefixes = card.labels + .map((label) => label.name.match(/(?<=#)[a-z0-9]+$/)?.[0]) + .filter((prefix) => prefix !== undefined); + + if (taskPrefixes.length === 0) { + throw new Error('Card has no valid project labels.'); + } + if (taskPrefixes.length > 1) { + throw new Error('Card has multiple project labels.'); + } + const taskPrefix = taskPrefixes[0]; + + let taskName = card.name.match(/(?<=#)[A-Za-z0-9_-]+/)?.[0]; + + if (taskName === undefined) { + taskName = `${taskPrefix}_${card.idShort}`; + + await BackgroundScript.trektor.trelloGateway.updateCard(card.id, { + name: `${card.name} #${taskName}`, + }); + } + + const workspaces = await BackgroundScript.trektor.togglGateway.getWorkspaces(); + + if (workspaces.length === 0) { + throw new Error('Could not find any toggl workspaces.'); + } + if (workspaces.length > 1) { + throw new Error('Found multiple toggl workspaces. Not sure how to deal with that...'); + } + const allProjects = await BackgroundScript.trektor.togglGateway.getProjects(workspaces[0].id); + const projects = allProjects.filter((project) => project.name.endsWith(`(${taskPrefix})`)); + + if (projects.length === 0) { + throw new Error('Could not find any matching toggl project.'); + } + if (projects.length > 1) { + throw new Error('Found multiple matching toggl projects. Not sure how to deal with that...'); + } + const tasks = await BackgroundScript.trektor.togglGateway.getTasks(projects[0].id); + const task = tasks.find((task) => task.name === taskName); + if (task !== undefined) return task; + + const response = await BackgroundScript.trektor.togglGateway.createTask(projects[0].id, taskName) + return response.data; + } + + static stripStoryPointsAndTaskToken(cardName) { + return cardName + .replace(/^(\s*\(\d+\))?\s*/, '') // story points, e.g. (3) + .replace(/\s*#[a-z0-9_]+\s*$/, ''); // task token, e.g. #orga_5417 + } +} diff --git a/scripts/chromium.js b/scripts/chromium.js new file mode 100644 index 0000000..7f7e853 --- /dev/null +++ b/scripts/chromium.js @@ -0,0 +1,3 @@ +importScripts('background.js', 'trektor.js'); + +BackgroundScript.init(getTrektor()); diff --git a/content_script.js b/scripts/content_script.js similarity index 96% rename from content_script.js rename to scripts/content_script.js index 612fd8e..d1c2c64 100644 --- a/content_script.js +++ b/scripts/content_script.js @@ -34,6 +34,7 @@ async function addButton() { trackButtonIcon.classList.add("trektor-state-loading"); try { + console.log("start tracking card") await browser.runtime.sendMessage({ action: 'track', args: [window.location.pathname.split("/", 3)[2]], @@ -42,6 +43,7 @@ async function addButton() { window.setTimeout(() => trackButtonIcon.classList.replace("icon-check-circle", "icon-clock"), 2000); } catch (err) { window.alert(err); + throw err; } finally { trackButtonIcon.classList.remove("trektor-state-loading"); } @@ -49,12 +51,14 @@ async function addButton() { addButton.addEventListener("click", async () => { try { + console.log("adding task") await browser.runtime.sendMessage({ action: 'addTask', args: [window.location.pathname.split("/", 3)[2]], }); } catch (err) { window.alert(err); + throw err; } }); } diff --git a/scripts/firefox.js b/scripts/firefox.js new file mode 100644 index 0000000..171a688 --- /dev/null +++ b/scripts/firefox.js @@ -0,0 +1 @@ +BackgroundScript.init(getTrektor()) diff --git a/scripts/trektor.js b/scripts/trektor.js new file mode 100644 index 0000000..7b73943 --- /dev/null +++ b/scripts/trektor.js @@ -0,0 +1,107 @@ +if (typeof browser == "undefined") { + globalThis.browser = chrome +} + +class TrelloGateway { + static ENDPOINT = "https://api.trello.com/1"; + static API_KEY = "2379d540412e417f6f0696c1397f38a6"; + + #storage; + + constructor(storage) { + this.#storage = storage; + } + + getCard(id) { + return this.#request("get", `/cards/${id}`); + } + + updateCard(id, data) { + return this.#request("put", `/cards/${id}`, data); + } + + async #request(method, path, data = null) { + const url = this.constructor.ENDPOINT + path; + const { trello: token } = await this.#storage.get("trello"); + + const response = await fetch(url, { + method, + headers: { + "Authorization": `OAuth oauth_consumer_key="${this.constructor.API_KEY}", oauth_token="${token}"`, + "Content-Type": "application/json; charset=utf-8", + }, + body: (data === null) ? null : JSON.stringify(data), + }); + + if (response.status === 401) { + throw new Error('Invalid or expired trello token.'); + } else { + return response.json(); + } + } +} + +class TogglGateway { + static ENDPOINT = "https://api.track.toggl.com/api/v8"; + + #storage; + + constructor(storage) { + this.#storage = storage; + } + + getWorkspaces() { + return this.#request("get", "/workspaces"); + } + + getProjects(workspaceId) { + return this.#request("get", `/workspaces/${workspaceId}/projects`); + } + + getTasks(projectId) { + return this.#request("get", `/projects/${projectId}/tasks`); + } + + createTask(projectId, name) { + return this.#request("post", "/tasks", { + task: { name, pid: projectId }, + }); + } + + getCurrentTimeEntry() { + return this.#request("get", "/time_entries/current"); + } + + startTimeEntry(taskId, description) { + return this.#request("post", "/time_entries/start", { + time_entry: { description, tid: taskId, created_with: "trektor" }, + }); + } + + async #request(method, path, data = null) { + const url = this.constructor.ENDPOINT + path; + const { toggl: token } = await this.#storage.get("toggl"); + + const response = await fetch(url, { + method, + headers: { + "Authorization": `Basic ${btoa(`${token}:api_token`)}`, + "Content-Type": "application/json; charset=utf-8", + }, + body: (data === null) ? null : JSON.stringify(data), + }); + + if (response.status === 403) { + throw new Error('Invalid toggl token.'); + } else { + return response.json(); + } + } +} + +function getTrektor() { + return { + trelloGateway: new TrelloGateway(browser.storage.local), + togglGateway: new TogglGateway(browser.storage.local), + } +} \ No newline at end of file diff --git a/trektor.js b/trektor.js deleted file mode 100644 index 3e20fa8..0000000 --- a/trektor.js +++ /dev/null @@ -1,183 +0,0 @@ -if (typeof browser == "undefined") { - globalThis.browser = chrome -} - -class TrelloGateway { - static ENDPOINT = "https://api.trello.com/1"; - static API_KEY = "2379d540412e417f6f0696c1397f38a6"; - - #storage; - - constructor(storage) { - this.#storage = storage; - } - - getCard(id) { - return this.#request("get", `/cards/${id}`); - } - - updateCard(id, data) { - return this.#request("put", `/cards/${id}`, data); - } - - async #request(method, path, data = null) { - const url = this.constructor.ENDPOINT + path; - const { trello: token } = await this.#storage.get("trello"); - - const response = await fetch(url, { - method, - headers: { - "Authorization": `OAuth oauth_consumer_key="${this.constructor.API_KEY}", oauth_token="${token}"`, - "Content-Type": "application/json; charset=utf-8", - }, - body: (data === null) ? null : JSON.stringify(data), - }); - - if (response.status === 401) { - throw new Error('Invalid or expired trello token.'); - } else { - return response.json(); - } - } -} - -class TogglGateway { - static ENDPOINT = "https://api.track.toggl.com/api/v8"; - - #storage; - - constructor(storage) { - this.#storage = storage; - } - - getWorkspaces() { - return this.#request("get", "/workspaces"); - } - - getProjects(workspaceId) { - return this.#request("get", `/workspaces/${workspaceId}/projects`); - } - - getTasks(projectId) { - return this.#request("get", `/projects/${projectId}/tasks`); - } - - createTask(projectId, name) { - return this.#request("post", "/tasks", { - task: { name, pid: projectId }, - }); - } - - getCurrentTimeEntry() { - return this.#request("get", "/time_entries/current"); - } - - startTimeEntry(taskId, description) { - return this.#request("post", "/time_entries/start", { - time_entry: { description, tid: taskId, created_with: "trektor" }, - }); - } - - async #request(method, path, data = null) { - const url = this.constructor.ENDPOINT + path; - const { toggl: token } = await this.#storage.get("toggl"); - - const response = await fetch(url, { - method, - headers: { - "Authorization": `Basic ${btoa(`${token}:api_token`)}`, - "Content-Type": "application/json; charset=utf-8", - }, - body: (data === null) ? null : JSON.stringify(data), - }); - - if (response.status === 403) { - throw new Error('Invalid toggl token.'); - } else { - return response.json(); - } - } -} - -var trektor = { - trelloGateway: new TrelloGateway(browser.storage.local), - togglGateway: new TogglGateway(browser.storage.local), -}; - - -browser.runtime.onMessage.addListener(async (msg) => { - switch (msg.action) { - case 'track': - await track(...msg.args); - return; - case 'addTask': - await addTask(...msg.args); - return; - default: - throw new Error(`unknown action: ${msg.action}`); - } -}); - -async function track(cardId) { - const task = await addTask(cardId); - const card = await trektor.trelloGateway.getCard(cardId); - const cardName = stripStoryPointsAndTaskToken(card.name); - const response = await trektor.togglGateway.startTimeEntry(task.id, cardName); - return response.data; -} - -async function addTask(cardId) { - const card = await trektor.trelloGateway.getCard(cardId); - - const taskPrefixes = card.labels - .map((label) => label.name.match(/(?<=#)[a-z0-9]+$/)?.[0]) - .filter((prefix) => prefix !== undefined); - - if (taskPrefixes.length === 0) { - throw new Error('Card has no valid project labels.'); - } - if (taskPrefixes.length > 1) { - throw new Error('Card has multiple project labels.'); - } - const taskPrefix = taskPrefixes[0]; - - let taskName = card.name.match(/(?<=#)[A-Za-z0-9_-]+/)?.[0]; - - if (taskName === undefined) { - taskName = `${taskPrefix}_${card.idShort}`; - - await trektor.trelloGateway.updateCard(card.id, { - name: `${card.name} #${taskName}`, - }); - } - - const workspaces = await trektor.togglGateway.getWorkspaces(); - - if (workspaces.length === 0) { - throw new Error('Could not find any toggl workspaces.'); - } - if (workspaces.length > 1) { - throw new Error('Found multiple toggl workspaces. Not sure how to deal with that...'); - } - const allProjects = await trektor.togglGateway.getProjects(workspaces[0].id); - const projects = allProjects.filter((project) => project.name.endsWith(`(${taskPrefix})`)); - - if (projects.length === 0) { - throw new Error('Could not find any matching toggl project.'); - } - if (projects.length > 1) { - throw new Error('Found multiple matching toggl projects. Not sure how to deal with that...'); - } - const tasks = await trektor.togglGateway.getTasks(projects[0].id); - const task = tasks.find((task) => task.name === taskName); - if (task !== undefined) return task; - - const response = await trektor.togglGateway.createTask(projects[0].id, taskName) - return response.data; -} - -function stripStoryPointsAndTaskToken(cardName) { - return cardName - .replace(/^(\s*\(\d+\))?\s*/, '') // story points, e.g. (3) - .replace(/\s*#[a-z0-9_]+\s*$/, ''); // task token, e.g. #orga_5417 -} From 894fe7ba0223f858a8bb544a2e2dd3e68b1b57bc Mon Sep 17 00:00:00 2001 From: Wendelin Date: Mon, 22 Jan 2024 12:10:57 +0100 Subject: [PATCH 3/8] adress requested changes; background script non-static; etc. --- manifest.json | 1 - options/script.js | 2 +- scripts/background.js | 66 +- scripts/chromium.js | 2 +- scripts/content_script.js | 14 +- scripts/firefox.js | 2 +- scripts/trektor.js | 1 + vendor/browser-polyfill.js | 1277 ------------------------------------ 8 files changed, 42 insertions(+), 1323 deletions(-) delete mode 100644 vendor/browser-polyfill.js diff --git a/manifest.json b/manifest.json index 44291d2..fa96bb7 100644 --- a/manifest.json +++ b/manifest.json @@ -12,7 +12,6 @@ "https://trello.com/*" ], "js": [ - "vendor/browser-polyfill.js", "scripts/content_script.js" ], "css": [ diff --git a/options/script.js b/options/script.js index fd1c05c..39251d3 100644 --- a/options/script.js +++ b/options/script.js @@ -4,6 +4,6 @@ document.querySelectorAll("input").forEach((field) => { }); browser.storage.local.get(field.name).then(({ [field.name]: value }) => { - field.value = (value === undefined) ? '' : value; + field.value = (value === undefined) ? "" : value; }); }); diff --git a/scripts/background.js b/scripts/background.js index 86c137d..b03b197 100644 --- a/scripts/background.js +++ b/scripts/background.js @@ -1,21 +1,17 @@ -if (typeof browser == "undefined") { - globalThis.browser = chrome -} - -class BackgroundScript { +class BackgroundWorker { - static trektor; - - static init(trektor) { - BackgroundScript.trektor = trektor + constructor(trektor) { + this.trektor = trektor; + } - browser.runtime.onMessage.addListener(async (msg) => { + run() { + this.trektor.browser.runtime.onMessage.addListener(async (msg) => { switch (msg.action) { - case 'track': - await BackgroundScript.track(...msg.args); + case "track": + await this.track(...msg.args); return; - case 'addTask': - await BackgroundScript.addTask(...msg.args); + case "addTask": + await this.addTask(...msg.args); return; default: throw new Error(`unknown action: ${msg.action}`); @@ -23,26 +19,26 @@ class BackgroundScript { }); } - static async track(cardId) { - const task = await BackgroundScript.addTask(cardId); - const card = await BackgroundScript.trektor.trelloGateway.getCard(cardId); - const cardName = BackgroundScript.stripStoryPointsAndTaskToken(card.name); - const response = await BackgroundScript.trektor.togglGateway.startTimeEntry(task.id, cardName); + async track(cardId) { + const task = await this.addTask(cardId); + const card = await this.trektor.trelloGateway.getCard(cardId); + const cardName = this.stripStoryPointsAndTaskToken(card.name); + const response = await this.trektor.togglGateway.startTimeEntry(task.id, cardName); return response.data; } - static async addTask(cardId) { - const card = await BackgroundScript.trektor.trelloGateway.getCard(cardId); + async addTask(cardId) { + const card = await this.trektor.trelloGateway.getCard(cardId); const taskPrefixes = card.labels .map((label) => label.name.match(/(?<=#)[a-z0-9]+$/)?.[0]) .filter((prefix) => prefix !== undefined); if (taskPrefixes.length === 0) { - throw new Error('Card has no valid project labels.'); + throw new Error("Card has no valid project labels."); } if (taskPrefixes.length > 1) { - throw new Error('Card has multiple project labels.'); + throw new Error("Card has multiple project labels."); } const taskPrefix = taskPrefixes[0]; @@ -51,39 +47,39 @@ class BackgroundScript { if (taskName === undefined) { taskName = `${taskPrefix}_${card.idShort}`; - await BackgroundScript.trektor.trelloGateway.updateCard(card.id, { + await this.trektor.trelloGateway.updateCard(card.id, { name: `${card.name} #${taskName}`, }); } - const workspaces = await BackgroundScript.trektor.togglGateway.getWorkspaces(); + const workspaces = await this.trektor.togglGateway.getWorkspaces(); if (workspaces.length === 0) { - throw new Error('Could not find any toggl workspaces.'); + throw new Error("Could not find any toggl workspaces."); } if (workspaces.length > 1) { - throw new Error('Found multiple toggl workspaces. Not sure how to deal with that...'); + throw new Error("Found multiple toggl workspaces. Not sure how to deal with that..."); } - const allProjects = await BackgroundScript.trektor.togglGateway.getProjects(workspaces[0].id); + const allProjects = await this.trektor.togglGateway.getProjects(workspaces[0].id); const projects = allProjects.filter((project) => project.name.endsWith(`(${taskPrefix})`)); if (projects.length === 0) { - throw new Error('Could not find any matching toggl project.'); + throw new Error("Could not find any matching toggl project."); } if (projects.length > 1) { - throw new Error('Found multiple matching toggl projects. Not sure how to deal with that...'); + throw new Error("Found multiple matching toggl projects. Not sure how to deal with that..."); } - const tasks = await BackgroundScript.trektor.togglGateway.getTasks(projects[0].id); + const tasks = await this.trektor.togglGateway.getTasks(projects[0].id); const task = tasks.find((task) => task.name === taskName); if (task !== undefined) return task; - const response = await BackgroundScript.trektor.togglGateway.createTask(projects[0].id, taskName) + const response = await this.trektor.togglGateway.createTask(projects[0].id, taskName) return response.data; } - static stripStoryPointsAndTaskToken(cardName) { + stripStoryPointsAndTaskToken(cardName) { return cardName - .replace(/^(\s*\(\d+\))?\s*/, '') // story points, e.g. (3) - .replace(/\s*#[a-z0-9_]+\s*$/, ''); // task token, e.g. #orga_5417 + .replace(/^(\s*\(\d+\))?\s*/, "") // story points, e.g. (3) + .replace(/\s*#[a-z0-9_]+\s*$/, ""); // task token, e.g. #orga_5417 } } diff --git a/scripts/chromium.js b/scripts/chromium.js index 7f7e853..4a1648f 100644 --- a/scripts/chromium.js +++ b/scripts/chromium.js @@ -1,3 +1,3 @@ importScripts('background.js', 'trektor.js'); -BackgroundScript.init(getTrektor()); +new BackgroundWorker(getTrektor()).run() diff --git a/scripts/content_script.js b/scripts/content_script.js index d1c2c64..b043ae2 100644 --- a/scripts/content_script.js +++ b/scripts/content_script.js @@ -1,3 +1,7 @@ +if (typeof browser == "undefined") { + globalThis.browser = chrome +} + async function addButton() { const sidebar = await awaitSelector(".window-sidebar", 10000); @@ -34,16 +38,14 @@ async function addButton() { trackButtonIcon.classList.add("trektor-state-loading"); try { - console.log("start tracking card") await browser.runtime.sendMessage({ - action: 'track', + action: "track", args: [window.location.pathname.split("/", 3)[2]], }); trackButtonIcon.classList.replace("icon-clock", "icon-check-circle"); window.setTimeout(() => trackButtonIcon.classList.replace("icon-check-circle", "icon-clock"), 2000); } catch (err) { window.alert(err); - throw err; } finally { trackButtonIcon.classList.remove("trektor-state-loading"); } @@ -51,14 +53,12 @@ async function addButton() { addButton.addEventListener("click", async () => { try { - console.log("adding task") await browser.runtime.sendMessage({ - action: 'addTask', + action: "addTask", args: [window.location.pathname.split("/", 3)[2]], }); } catch (err) { window.alert(err); - throw err; } }); } @@ -75,7 +75,7 @@ function awaitSelector(selector, timeout) { resolve(element); window.clearInterval(interval); } else if (timeout < 0) { - reject(new Error('timeout')); + reject(new Error("timeout")); window.clearInterval(interval); } timeout -= 100; diff --git a/scripts/firefox.js b/scripts/firefox.js index 171a688..0e01727 100644 --- a/scripts/firefox.js +++ b/scripts/firefox.js @@ -1 +1 @@ -BackgroundScript.init(getTrektor()) +new BackgroundWorker(getTrektor()).run() diff --git a/scripts/trektor.js b/scripts/trektor.js index 7b73943..8df0748 100644 --- a/scripts/trektor.js +++ b/scripts/trektor.js @@ -103,5 +103,6 @@ function getTrektor() { return { trelloGateway: new TrelloGateway(browser.storage.local), togglGateway: new TogglGateway(browser.storage.local), + browser: browser, } } \ No newline at end of file diff --git a/vendor/browser-polyfill.js b/vendor/browser-polyfill.js deleted file mode 100644 index 230b763..0000000 --- a/vendor/browser-polyfill.js +++ /dev/null @@ -1,1277 +0,0 @@ -(function (global, factory) { - if (typeof define === "function" && define.amd) { - define("webextension-polyfill", ["module"], factory); - } else if (typeof exports !== "undefined") { - factory(module); - } else { - var mod = { - exports: {} - }; - factory(mod); - global.browser = mod.exports; - } -})(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (module) { - /* webextension-polyfill - v0.8.0 - Tue Apr 20 2021 11:27:38 */ - - /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ - - /* vim: set sts=2 sw=2 et tw=80: */ - - /* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - "use strict"; - - if (typeof browser === "undefined" || Object.getPrototypeOf(browser) !== Object.prototype) { - const CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE = "The message port closed before a response was received."; - const SEND_RESPONSE_DEPRECATION_WARNING = "Returning a Promise is the preferred way to send a reply from an onMessage/onMessageExternal listener, as the sendResponse will be removed from the specs (See https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage)"; // Wrapping the bulk of this polyfill in a one-time-use function is a minor - // optimization for Firefox. Since Spidermonkey does not fully parse the - // contents of a function until the first time it's called, and since it will - // never actually need to be called, this allows the polyfill to be included - // in Firefox nearly for free. - - const wrapAPIs = extensionAPIs => { - // NOTE: apiMetadata is associated to the content of the api-metadata.json file - // at build time by replacing the following "include" with the content of the - // JSON file. - const apiMetadata = { - "alarms": { - "clear": { - "minArgs": 0, - "maxArgs": 1 - }, - "clearAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "bookmarks": { - "create": { - "minArgs": 1, - "maxArgs": 1 - }, - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getChildren": { - "minArgs": 1, - "maxArgs": 1 - }, - "getRecent": { - "minArgs": 1, - "maxArgs": 1 - }, - "getSubTree": { - "minArgs": 1, - "maxArgs": 1 - }, - "getTree": { - "minArgs": 0, - "maxArgs": 0 - }, - "move": { - "minArgs": 2, - "maxArgs": 2 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeTree": { - "minArgs": 1, - "maxArgs": 1 - }, - "search": { - "minArgs": 1, - "maxArgs": 1 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - }, - "browserAction": { - "disable": { - "minArgs": 0, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "enable": { - "minArgs": 0, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "getBadgeBackgroundColor": { - "minArgs": 1, - "maxArgs": 1 - }, - "getBadgeText": { - "minArgs": 1, - "maxArgs": 1 - }, - "getPopup": { - "minArgs": 1, - "maxArgs": 1 - }, - "getTitle": { - "minArgs": 1, - "maxArgs": 1 - }, - "openPopup": { - "minArgs": 0, - "maxArgs": 0 - }, - "setBadgeBackgroundColor": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setBadgeText": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setIcon": { - "minArgs": 1, - "maxArgs": 1 - }, - "setPopup": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setTitle": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - } - }, - "browsingData": { - "remove": { - "minArgs": 2, - "maxArgs": 2 - }, - "removeCache": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeCookies": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeDownloads": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeFormData": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeHistory": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeLocalStorage": { - "minArgs": 1, - "maxArgs": 1 - }, - "removePasswords": { - "minArgs": 1, - "maxArgs": 1 - }, - "removePluginData": { - "minArgs": 1, - "maxArgs": 1 - }, - "settings": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "commands": { - "getAll": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "contextMenus": { - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - }, - "cookies": { - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAllCookieStores": { - "minArgs": 0, - "maxArgs": 0 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "set": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "devtools": { - "inspectedWindow": { - "eval": { - "minArgs": 1, - "maxArgs": 2, - "singleCallbackArg": false - } - }, - "panels": { - "create": { - "minArgs": 3, - "maxArgs": 3, - "singleCallbackArg": true - }, - "elements": { - "createSidebarPane": { - "minArgs": 1, - "maxArgs": 1 - } - } - } - }, - "downloads": { - "cancel": { - "minArgs": 1, - "maxArgs": 1 - }, - "download": { - "minArgs": 1, - "maxArgs": 1 - }, - "erase": { - "minArgs": 1, - "maxArgs": 1 - }, - "getFileIcon": { - "minArgs": 1, - "maxArgs": 2 - }, - "open": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "pause": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeFile": { - "minArgs": 1, - "maxArgs": 1 - }, - "resume": { - "minArgs": 1, - "maxArgs": 1 - }, - "search": { - "minArgs": 1, - "maxArgs": 1 - }, - "show": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - } - }, - "extension": { - "isAllowedFileSchemeAccess": { - "minArgs": 0, - "maxArgs": 0 - }, - "isAllowedIncognitoAccess": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "history": { - "addUrl": { - "minArgs": 1, - "maxArgs": 1 - }, - "deleteAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "deleteRange": { - "minArgs": 1, - "maxArgs": 1 - }, - "deleteUrl": { - "minArgs": 1, - "maxArgs": 1 - }, - "getVisits": { - "minArgs": 1, - "maxArgs": 1 - }, - "search": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "i18n": { - "detectLanguage": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAcceptLanguages": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "identity": { - "launchWebAuthFlow": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "idle": { - "queryState": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "management": { - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "getSelf": { - "minArgs": 0, - "maxArgs": 0 - }, - "setEnabled": { - "minArgs": 2, - "maxArgs": 2 - }, - "uninstallSelf": { - "minArgs": 0, - "maxArgs": 1 - } - }, - "notifications": { - "clear": { - "minArgs": 1, - "maxArgs": 1 - }, - "create": { - "minArgs": 1, - "maxArgs": 2 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "getPermissionLevel": { - "minArgs": 0, - "maxArgs": 0 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - }, - "pageAction": { - "getPopup": { - "minArgs": 1, - "maxArgs": 1 - }, - "getTitle": { - "minArgs": 1, - "maxArgs": 1 - }, - "hide": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setIcon": { - "minArgs": 1, - "maxArgs": 1 - }, - "setPopup": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "setTitle": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - }, - "show": { - "minArgs": 1, - "maxArgs": 1, - "fallbackToNoCallback": true - } - }, - "permissions": { - "contains": { - "minArgs": 1, - "maxArgs": 1 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 0 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "request": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "runtime": { - "getBackgroundPage": { - "minArgs": 0, - "maxArgs": 0 - }, - "getPlatformInfo": { - "minArgs": 0, - "maxArgs": 0 - }, - "openOptionsPage": { - "minArgs": 0, - "maxArgs": 0 - }, - "requestUpdateCheck": { - "minArgs": 0, - "maxArgs": 0 - }, - "sendMessage": { - "minArgs": 1, - "maxArgs": 3 - }, - "sendNativeMessage": { - "minArgs": 2, - "maxArgs": 2 - }, - "setUninstallURL": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "sessions": { - "getDevices": { - "minArgs": 0, - "maxArgs": 1 - }, - "getRecentlyClosed": { - "minArgs": 0, - "maxArgs": 1 - }, - "restore": { - "minArgs": 0, - "maxArgs": 1 - } - }, - "storage": { - "local": { - "clear": { - "minArgs": 0, - "maxArgs": 0 - }, - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getBytesInUse": { - "minArgs": 0, - "maxArgs": 1 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "set": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "managed": { - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getBytesInUse": { - "minArgs": 0, - "maxArgs": 1 - } - }, - "sync": { - "clear": { - "minArgs": 0, - "maxArgs": 0 - }, - "get": { - "minArgs": 0, - "maxArgs": 1 - }, - "getBytesInUse": { - "minArgs": 0, - "maxArgs": 1 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "set": { - "minArgs": 1, - "maxArgs": 1 - } - } - }, - "tabs": { - "captureVisibleTab": { - "minArgs": 0, - "maxArgs": 2 - }, - "create": { - "minArgs": 1, - "maxArgs": 1 - }, - "detectLanguage": { - "minArgs": 0, - "maxArgs": 1 - }, - "discard": { - "minArgs": 0, - "maxArgs": 1 - }, - "duplicate": { - "minArgs": 1, - "maxArgs": 1 - }, - "executeScript": { - "minArgs": 1, - "maxArgs": 2 - }, - "get": { - "minArgs": 1, - "maxArgs": 1 - }, - "getCurrent": { - "minArgs": 0, - "maxArgs": 0 - }, - "getZoom": { - "minArgs": 0, - "maxArgs": 1 - }, - "getZoomSettings": { - "minArgs": 0, - "maxArgs": 1 - }, - "goBack": { - "minArgs": 0, - "maxArgs": 1 - }, - "goForward": { - "minArgs": 0, - "maxArgs": 1 - }, - "highlight": { - "minArgs": 1, - "maxArgs": 1 - }, - "insertCSS": { - "minArgs": 1, - "maxArgs": 2 - }, - "move": { - "minArgs": 2, - "maxArgs": 2 - }, - "query": { - "minArgs": 1, - "maxArgs": 1 - }, - "reload": { - "minArgs": 0, - "maxArgs": 2 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "removeCSS": { - "minArgs": 1, - "maxArgs": 2 - }, - "sendMessage": { - "minArgs": 2, - "maxArgs": 3 - }, - "setZoom": { - "minArgs": 1, - "maxArgs": 2 - }, - "setZoomSettings": { - "minArgs": 1, - "maxArgs": 2 - }, - "update": { - "minArgs": 1, - "maxArgs": 2 - } - }, - "topSites": { - "get": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "webNavigation": { - "getAllFrames": { - "minArgs": 1, - "maxArgs": 1 - }, - "getFrame": { - "minArgs": 1, - "maxArgs": 1 - } - }, - "webRequest": { - "handlerBehaviorChanged": { - "minArgs": 0, - "maxArgs": 0 - } - }, - "windows": { - "create": { - "minArgs": 0, - "maxArgs": 1 - }, - "get": { - "minArgs": 1, - "maxArgs": 2 - }, - "getAll": { - "minArgs": 0, - "maxArgs": 1 - }, - "getCurrent": { - "minArgs": 0, - "maxArgs": 1 - }, - "getLastFocused": { - "minArgs": 0, - "maxArgs": 1 - }, - "remove": { - "minArgs": 1, - "maxArgs": 1 - }, - "update": { - "minArgs": 2, - "maxArgs": 2 - } - } - }; - - if (Object.keys(apiMetadata).length === 0) { - throw new Error("api-metadata.json has not been included in browser-polyfill"); - } - /** - * A WeakMap subclass which creates and stores a value for any key which does - * not exist when accessed, but behaves exactly as an ordinary WeakMap - * otherwise. - * - * @param {function} createItem - * A function which will be called in order to create the value for any - * key which does not exist, the first time it is accessed. The - * function receives, as its only argument, the key being created. - */ - - - class DefaultWeakMap extends WeakMap { - constructor(createItem, items = undefined) { - super(items); - this.createItem = createItem; - } - - get(key) { - if (!this.has(key)) { - this.set(key, this.createItem(key)); - } - - return super.get(key); - } - - } - /** - * Returns true if the given object is an object with a `then` method, and can - * therefore be assumed to behave as a Promise. - * - * @param {*} value The value to test. - * @returns {boolean} True if the value is thenable. - */ - - - const isThenable = value => { - return value && typeof value === "object" && typeof value.then === "function"; - }; - /** - * Creates and returns a function which, when called, will resolve or reject - * the given promise based on how it is called: - * - * - If, when called, `chrome.runtime.lastError` contains a non-null object, - * the promise is rejected with that value. - * - If the function is called with exactly one argument, the promise is - * resolved to that value. - * - Otherwise, the promise is resolved to an array containing all of the - * function's arguments. - * - * @param {object} promise - * An object containing the resolution and rejection functions of a - * promise. - * @param {function} promise.resolve - * The promise's resolution function. - * @param {function} promise.reject - * The promise's rejection function. - * @param {object} metadata - * Metadata about the wrapped method which has created the callback. - * @param {boolean} metadata.singleCallbackArg - * Whether or not the promise is resolved with only the first - * argument of the callback, alternatively an array of all the - * callback arguments is resolved. By default, if the callback - * function is invoked with only a single argument, that will be - * resolved to the promise, while all arguments will be resolved as - * an array if multiple are given. - * - * @returns {function} - * The generated callback function. - */ - - - const makeCallback = (promise, metadata) => { - return (...callbackArgs) => { - if (extensionAPIs.runtime.lastError) { - promise.reject(new Error(extensionAPIs.runtime.lastError.message)); - } else if (metadata.singleCallbackArg || callbackArgs.length <= 1 && metadata.singleCallbackArg !== false) { - promise.resolve(callbackArgs[0]); - } else { - promise.resolve(callbackArgs); - } - }; - }; - - const pluralizeArguments = numArgs => numArgs == 1 ? "argument" : "arguments"; - /** - * Creates a wrapper function for a method with the given name and metadata. - * - * @param {string} name - * The name of the method which is being wrapped. - * @param {object} metadata - * Metadata about the method being wrapped. - * @param {integer} metadata.minArgs - * The minimum number of arguments which must be passed to the - * function. If called with fewer than this number of arguments, the - * wrapper will raise an exception. - * @param {integer} metadata.maxArgs - * The maximum number of arguments which may be passed to the - * function. If called with more than this number of arguments, the - * wrapper will raise an exception. - * @param {boolean} metadata.singleCallbackArg - * Whether or not the promise is resolved with only the first - * argument of the callback, alternatively an array of all the - * callback arguments is resolved. By default, if the callback - * function is invoked with only a single argument, that will be - * resolved to the promise, while all arguments will be resolved as - * an array if multiple are given. - * - * @returns {function(object, ...*)} - * The generated wrapper function. - */ - - - const wrapAsyncFunction = (name, metadata) => { - return function asyncFunctionWrapper(target, ...args) { - if (args.length < metadata.minArgs) { - throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); - } - - if (args.length > metadata.maxArgs) { - throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); - } - - return new Promise((resolve, reject) => { - if (metadata.fallbackToNoCallback) { - // This API method has currently no callback on Chrome, but it return a promise on Firefox, - // and so the polyfill will try to call it with a callback first, and it will fallback - // to not passing the callback if the first call fails. - try { - target[name](...args, makeCallback({ - resolve, - reject - }, metadata)); - } catch (cbError) { - console.warn(`${name} API method doesn't seem to support the callback parameter, ` + "falling back to call it without a callback: ", cbError); - target[name](...args); // Update the API method metadata, so that the next API calls will not try to - // use the unsupported callback anymore. - - metadata.fallbackToNoCallback = false; - metadata.noCallback = true; - resolve(); - } - } else if (metadata.noCallback) { - target[name](...args); - resolve(); - } else { - target[name](...args, makeCallback({ - resolve, - reject - }, metadata)); - } - }); - }; - }; - /** - * Wraps an existing method of the target object, so that calls to it are - * intercepted by the given wrapper function. The wrapper function receives, - * as its first argument, the original `target` object, followed by each of - * the arguments passed to the original method. - * - * @param {object} target - * The original target object that the wrapped method belongs to. - * @param {function} method - * The method being wrapped. This is used as the target of the Proxy - * object which is created to wrap the method. - * @param {function} wrapper - * The wrapper function which is called in place of a direct invocation - * of the wrapped method. - * - * @returns {Proxy} - * A Proxy object for the given method, which invokes the given wrapper - * method in its place. - */ - - - const wrapMethod = (target, method, wrapper) => { - return new Proxy(method, { - apply(targetMethod, thisObj, args) { - return wrapper.call(thisObj, target, ...args); - } - - }); - }; - - let hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); - /** - * Wraps an object in a Proxy which intercepts and wraps certain methods - * based on the given `wrappers` and `metadata` objects. - * - * @param {object} target - * The target object to wrap. - * - * @param {object} [wrappers = {}] - * An object tree containing wrapper functions for special cases. Any - * function present in this object tree is called in place of the - * method in the same location in the `target` object tree. These - * wrapper methods are invoked as described in {@see wrapMethod}. - * - * @param {object} [metadata = {}] - * An object tree containing metadata used to automatically generate - * Promise-based wrapper functions for asynchronous. Any function in - * the `target` object tree which has a corresponding metadata object - * in the same location in the `metadata` tree is replaced with an - * automatically-generated wrapper function, as described in - * {@see wrapAsyncFunction} - * - * @returns {Proxy} - */ - - const wrapObject = (target, wrappers = {}, metadata = {}) => { - let cache = Object.create(null); - let handlers = { - has(proxyTarget, prop) { - return prop in target || prop in cache; - }, - - get(proxyTarget, prop, receiver) { - if (prop in cache) { - return cache[prop]; - } - - if (!(prop in target)) { - return undefined; - } - - let value = target[prop]; - - if (typeof value === "function") { - // This is a method on the underlying object. Check if we need to do - // any wrapping. - if (typeof wrappers[prop] === "function") { - // We have a special-case wrapper for this method. - value = wrapMethod(target, target[prop], wrappers[prop]); - } else if (hasOwnProperty(metadata, prop)) { - // This is an async method that we have metadata for. Create a - // Promise wrapper for it. - let wrapper = wrapAsyncFunction(prop, metadata[prop]); - value = wrapMethod(target, target[prop], wrapper); - } else { - // This is a method that we don't know or care about. Return the - // original method, bound to the underlying object. - value = value.bind(target); - } - } else if (typeof value === "object" && value !== null && (hasOwnProperty(wrappers, prop) || hasOwnProperty(metadata, prop))) { - // This is an object that we need to do some wrapping for the children - // of. Create a sub-object wrapper for it with the appropriate child - // metadata. - value = wrapObject(value, wrappers[prop], metadata[prop]); - } else if (hasOwnProperty(metadata, "*")) { - // Wrap all properties in * namespace. - value = wrapObject(value, wrappers[prop], metadata["*"]); - } else { - // We don't need to do any wrapping for this property, - // so just forward all access to the underlying object. - Object.defineProperty(cache, prop, { - configurable: true, - enumerable: true, - - get() { - return target[prop]; - }, - - set(value) { - target[prop] = value; - } - - }); - return value; - } - - cache[prop] = value; - return value; - }, - - set(proxyTarget, prop, value, receiver) { - if (prop in cache) { - cache[prop] = value; - } else { - target[prop] = value; - } - - return true; - }, - - defineProperty(proxyTarget, prop, desc) { - return Reflect.defineProperty(cache, prop, desc); - }, - - deleteProperty(proxyTarget, prop) { - return Reflect.deleteProperty(cache, prop); - } - - }; // Per contract of the Proxy API, the "get" proxy handler must return the - // original value of the target if that value is declared read-only and - // non-configurable. For this reason, we create an object with the - // prototype set to `target` instead of using `target` directly. - // Otherwise we cannot return a custom object for APIs that - // are declared read-only and non-configurable, such as `chrome.devtools`. - // - // The proxy handlers themselves will still use the original `target` - // instead of the `proxyTarget`, so that the methods and properties are - // dereferenced via the original targets. - - let proxyTarget = Object.create(target); - return new Proxy(proxyTarget, handlers); - }; - /** - * Creates a set of wrapper functions for an event object, which handles - * wrapping of listener functions that those messages are passed. - * - * A single wrapper is created for each listener function, and stored in a - * map. Subsequent calls to `addListener`, `hasListener`, or `removeListener` - * retrieve the original wrapper, so that attempts to remove a - * previously-added listener work as expected. - * - * @param {DefaultWeakMap} wrapperMap - * A DefaultWeakMap object which will create the appropriate wrapper - * for a given listener function when one does not exist, and retrieve - * an existing one when it does. - * - * @returns {object} - */ - - - const wrapEvent = wrapperMap => ({ - addListener(target, listener, ...args) { - target.addListener(wrapperMap.get(listener), ...args); - }, - - hasListener(target, listener) { - return target.hasListener(wrapperMap.get(listener)); - }, - - removeListener(target, listener) { - target.removeListener(wrapperMap.get(listener)); - } - - }); - - const onRequestFinishedWrappers = new DefaultWeakMap(listener => { - if (typeof listener !== "function") { - return listener; - } - /** - * Wraps an onRequestFinished listener function so that it will return a - * `getContent()` property which returns a `Promise` rather than using a - * callback API. - * - * @param {object} req - * The HAR entry object representing the network request. - */ - - - return function onRequestFinished(req) { - const wrappedReq = wrapObject(req, {} - /* wrappers */ - , { - getContent: { - minArgs: 0, - maxArgs: 0 - } - }); - listener(wrappedReq); - }; - }); // Keep track if the deprecation warning has been logged at least once. - - let loggedSendResponseDeprecationWarning = false; - const onMessageWrappers = new DefaultWeakMap(listener => { - if (typeof listener !== "function") { - return listener; - } - /** - * Wraps a message listener function so that it may send responses based on - * its return value, rather than by returning a sentinel value and calling a - * callback. If the listener function returns a Promise, the response is - * sent when the promise either resolves or rejects. - * - * @param {*} message - * The message sent by the other end of the channel. - * @param {object} sender - * Details about the sender of the message. - * @param {function(*)} sendResponse - * A callback which, when called with an arbitrary argument, sends - * that value as a response. - * @returns {boolean} - * True if the wrapped listener returned a Promise, which will later - * yield a response. False otherwise. - */ - - - return function onMessage(message, sender, sendResponse) { - let didCallSendResponse = false; - let wrappedSendResponse; - let sendResponsePromise = new Promise(resolve => { - wrappedSendResponse = function (response) { - if (!loggedSendResponseDeprecationWarning) { - console.warn(SEND_RESPONSE_DEPRECATION_WARNING, new Error().stack); - loggedSendResponseDeprecationWarning = true; - } - - didCallSendResponse = true; - resolve(response); - }; - }); - let result; - - try { - result = listener(message, sender, wrappedSendResponse); - } catch (err) { - result = Promise.reject(err); - } - - const isResultThenable = result !== true && isThenable(result); // If the listener didn't returned true or a Promise, or called - // wrappedSendResponse synchronously, we can exit earlier - // because there will be no response sent from this listener. - - if (result !== true && !isResultThenable && !didCallSendResponse) { - return false; - } // A small helper to send the message if the promise resolves - // and an error if the promise rejects (a wrapped sendMessage has - // to translate the message into a resolved promise or a rejected - // promise). - - - const sendPromisedResult = promise => { - promise.then(msg => { - // send the message value. - sendResponse(msg); - }, error => { - // Send a JSON representation of the error if the rejected value - // is an instance of error, or the object itself otherwise. - let message; - - if (error && (error instanceof Error || typeof error.message === "string")) { - message = error.message; - } else { - message = "An unexpected error occurred"; - } - - sendResponse({ - __mozWebExtensionPolyfillReject__: true, - message - }); - }).catch(err => { - // Print an error on the console if unable to send the response. - console.error("Failed to send onMessage rejected reply", err); - }); - }; // If the listener returned a Promise, send the resolved value as a - // result, otherwise wait the promise related to the wrappedSendResponse - // callback to resolve and send it as a response. - - - if (isResultThenable) { - sendPromisedResult(result); - } else { - sendPromisedResult(sendResponsePromise); - } // Let Chrome know that the listener is replying. - - - return true; - }; - }); - - const wrappedSendMessageCallback = ({ - reject, - resolve - }, reply) => { - if (extensionAPIs.runtime.lastError) { - // Detect when none of the listeners replied to the sendMessage call and resolve - // the promise to undefined as in Firefox. - // See https://github.com/mozilla/webextension-polyfill/issues/130 - if (extensionAPIs.runtime.lastError.message === CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE) { - resolve(); - } else { - reject(new Error(extensionAPIs.runtime.lastError.message)); - } - } else if (reply && reply.__mozWebExtensionPolyfillReject__) { - // Convert back the JSON representation of the error into - // an Error instance. - reject(new Error(reply.message)); - } else { - resolve(reply); - } - }; - - const wrappedSendMessage = (name, metadata, apiNamespaceObj, ...args) => { - if (args.length < metadata.minArgs) { - throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); - } - - if (args.length > metadata.maxArgs) { - throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); - } - - return new Promise((resolve, reject) => { - const wrappedCb = wrappedSendMessageCallback.bind(null, { - resolve, - reject - }); - args.push(wrappedCb); - apiNamespaceObj.sendMessage(...args); - }); - }; - - const staticWrappers = { - devtools: { - network: { - onRequestFinished: wrapEvent(onRequestFinishedWrappers) - } - }, - runtime: { - onMessage: wrapEvent(onMessageWrappers), - onMessageExternal: wrapEvent(onMessageWrappers), - sendMessage: wrappedSendMessage.bind(null, "sendMessage", { - minArgs: 1, - maxArgs: 3 - }) - }, - tabs: { - sendMessage: wrappedSendMessage.bind(null, "sendMessage", { - minArgs: 2, - maxArgs: 3 - }) - } - }; - const settingMetadata = { - clear: { - minArgs: 1, - maxArgs: 1 - }, - get: { - minArgs: 1, - maxArgs: 1 - }, - set: { - minArgs: 1, - maxArgs: 1 - } - }; - apiMetadata.privacy = { - network: { - "*": settingMetadata - }, - services: { - "*": settingMetadata - }, - websites: { - "*": settingMetadata - } - }; - return wrapObject(extensionAPIs, staticWrappers, apiMetadata); - }; - - if (typeof chrome != "object" || !chrome || !chrome.runtime || !chrome.runtime.id) { - throw new Error("This script should only be loaded in a browser extension."); - } // The build process adds a UMD wrapper around this file, which makes the - // `module` variable available. - - - module.exports = wrapAPIs(chrome); - } else { - module.exports = browser; - } -}); -//# sourceMappingURL=browser-polyfill.js.map From 8cc6a55b9dbb1033c55a5420bd6c5fb02d4fc8f1 Mon Sep 17 00:00:00 2001 From: Wendelin Date: Tue, 23 Jan 2024 10:56:28 +0100 Subject: [PATCH 4/8] trektor nicht als funktion; trektor im content script einbinden --- manifest.json | 3 ++- scripts/background.js | 21 ++++++++++----------- scripts/chromium.js | 2 +- scripts/content_script.js | 8 ++------ scripts/firefox.js | 2 +- scripts/trektor.js | 11 +++++------ 6 files changed, 21 insertions(+), 26 deletions(-) diff --git a/manifest.json b/manifest.json index fa96bb7..402a2d0 100644 --- a/manifest.json +++ b/manifest.json @@ -12,7 +12,8 @@ "https://trello.com/*" ], "js": [ - "scripts/content_script.js" + "scripts/content_script.js", + "scripts/trektor.js" ], "css": [ "content_style.css" diff --git a/scripts/background.js b/scripts/background.js index b03b197..0d26949 100644 --- a/scripts/background.js +++ b/scripts/background.js @@ -1,11 +1,10 @@ class BackgroundWorker { - constructor(trektor) { - this.trektor = trektor; + constructor() { } run() { - this.trektor.browser.runtime.onMessage.addListener(async (msg) => { + trektor.browser.runtime.onMessage.addListener(async (msg) => { switch (msg.action) { case "track": await this.track(...msg.args); @@ -21,14 +20,14 @@ class BackgroundWorker { async track(cardId) { const task = await this.addTask(cardId); - const card = await this.trektor.trelloGateway.getCard(cardId); + const card = await trektor.trelloGateway.getCard(cardId); const cardName = this.stripStoryPointsAndTaskToken(card.name); - const response = await this.trektor.togglGateway.startTimeEntry(task.id, cardName); + const response = await trektor.togglGateway.startTimeEntry(task.id, cardName); return response.data; } async addTask(cardId) { - const card = await this.trektor.trelloGateway.getCard(cardId); + const card = await trektor.trelloGateway.getCard(cardId); const taskPrefixes = card.labels .map((label) => label.name.match(/(?<=#)[a-z0-9]+$/)?.[0]) @@ -47,12 +46,12 @@ class BackgroundWorker { if (taskName === undefined) { taskName = `${taskPrefix}_${card.idShort}`; - await this.trektor.trelloGateway.updateCard(card.id, { + await trektor.trelloGateway.updateCard(card.id, { name: `${card.name} #${taskName}`, }); } - const workspaces = await this.trektor.togglGateway.getWorkspaces(); + const workspaces = await trektor.togglGateway.getWorkspaces(); if (workspaces.length === 0) { throw new Error("Could not find any toggl workspaces."); @@ -60,7 +59,7 @@ class BackgroundWorker { if (workspaces.length > 1) { throw new Error("Found multiple toggl workspaces. Not sure how to deal with that..."); } - const allProjects = await this.trektor.togglGateway.getProjects(workspaces[0].id); + const allProjects = await trektor.togglGateway.getProjects(workspaces[0].id); const projects = allProjects.filter((project) => project.name.endsWith(`(${taskPrefix})`)); if (projects.length === 0) { @@ -69,11 +68,11 @@ class BackgroundWorker { if (projects.length > 1) { throw new Error("Found multiple matching toggl projects. Not sure how to deal with that..."); } - const tasks = await this.trektor.togglGateway.getTasks(projects[0].id); + const tasks = await trektor.togglGateway.getTasks(projects[0].id); const task = tasks.find((task) => task.name === taskName); if (task !== undefined) return task; - const response = await this.trektor.togglGateway.createTask(projects[0].id, taskName) + const response = await trektor.togglGateway.createTask(projects[0].id, taskName) return response.data; } diff --git a/scripts/chromium.js b/scripts/chromium.js index 4a1648f..d8bb7a8 100644 --- a/scripts/chromium.js +++ b/scripts/chromium.js @@ -1,3 +1,3 @@ importScripts('background.js', 'trektor.js'); -new BackgroundWorker(getTrektor()).run() +new BackgroundWorker().run() diff --git a/scripts/content_script.js b/scripts/content_script.js index b043ae2..cdd789b 100644 --- a/scripts/content_script.js +++ b/scripts/content_script.js @@ -1,7 +1,3 @@ -if (typeof browser == "undefined") { - globalThis.browser = chrome -} - async function addButton() { const sidebar = await awaitSelector(".window-sidebar", 10000); @@ -38,7 +34,7 @@ async function addButton() { trackButtonIcon.classList.add("trektor-state-loading"); try { - await browser.runtime.sendMessage({ + await trektor.browser.runtime.sendMessage({ action: "track", args: [window.location.pathname.split("/", 3)[2]], }); @@ -53,7 +49,7 @@ async function addButton() { addButton.addEventListener("click", async () => { try { - await browser.runtime.sendMessage({ + await trektor.browser.runtime.sendMessage({ action: "addTask", args: [window.location.pathname.split("/", 3)[2]], }); diff --git a/scripts/firefox.js b/scripts/firefox.js index 0e01727..9660239 100644 --- a/scripts/firefox.js +++ b/scripts/firefox.js @@ -1 +1 @@ -new BackgroundWorker(getTrektor()).run() +new BackgroundWorker().run() diff --git a/scripts/trektor.js b/scripts/trektor.js index 8df0748..2ed56a6 100644 --- a/scripts/trektor.js +++ b/scripts/trektor.js @@ -99,10 +99,9 @@ class TogglGateway { } } -function getTrektor() { - return { - trelloGateway: new TrelloGateway(browser.storage.local), - togglGateway: new TogglGateway(browser.storage.local), - browser: browser, - } + +const trektor = { + trelloGateway: new TrelloGateway(browser.storage.local), + togglGateway: new TogglGateway(browser.storage.local), + browser: browser, } \ No newline at end of file From 96074d981480ebb0b42b7577b90e10310565caca Mon Sep 17 00:00:00 2001 From: Wendelin Date: Tue, 23 Jan 2024 11:00:03 +0100 Subject: [PATCH 5/8] add permission detection to options page --- options/index.html | 19 +++++-------------- options/script.js | 33 +++++++++++++++++++++++++++++++++ options/style.css | 30 ++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 options/style.css diff --git a/options/index.html b/options/index.html index 3a45c93..d35d24e 100644 --- a/options/index.html +++ b/options/index.html @@ -2,34 +2,25 @@ + Trektor settings - +

Willkommen bei Trektor!

+
✔ Alle Berechtigungen vorhanden
+
✘ Berechtigung fehlt!
+ Trello Token - Toggl Token - - diff --git a/options/script.js b/options/script.js index 39251d3..bb2a763 100644 --- a/options/script.js +++ b/options/script.js @@ -1,3 +1,15 @@ +if (typeof browser == "undefined") { + globalThis.browser = chrome +} + +const permissions = { + origins: [ + "https://api.trello.com/*", + "https://api.track.toggl.com/*", + "https://trello.com/*" + ] +} + document.querySelectorAll("input").forEach((field) => { field.addEventListener("input", (e) => { browser.storage.local.set({ [e.target.name]: e.target.value }); @@ -7,3 +19,24 @@ document.querySelectorAll("input").forEach((field) => { field.value = (value === undefined) ? "" : value; }); }); + +check_permissions() + +document.getElementById("request").addEventListener("click", () => { requestPermissions() }) + +function requestPermissions() { + console.log("hallo") + browser.permissions.request(permissions, () => { check_permissions() }) +} + +function check_permissions() { + browser.permissions.contains(permissions, (contains) => { + if (contains) { + document.getElementById("permissions-granted").style.display = "block"; + document.getElementById("permissions-not-granted").style.display = "none"; + } else { + document.getElementById("permissions-granted").style.display = "none"; + document.getElementById("permissions-not-granted").style.display = "block"; + } + }) +} diff --git a/options/style.css b/options/style.css new file mode 100644 index 0000000..cea7222 --- /dev/null +++ b/options/style.css @@ -0,0 +1,30 @@ +a { + display: block; + color: blue; + margin: 10px; +} +input { + width: 100%; + box-sizing: border-box; + margin-bottom: 10px; +} +body { + font-family: sans-serif; + width: 90%; + max-width: 500px; + margin: 1.5rem auto; +} + +#permissions-granted { + background-color: rgba(0, 255, 0, 0.5); + border: 2px green solid; +} +#permissions-not-granted { + background-color: rgba(255, 20, 0, 0.5); + border: 2px red solid; +} +.notification { + text-align: center; + padding: 10px; + border-radius: 10px; +} From 9ac111a2e6e869767e76b3527f77678613971675 Mon Sep 17 00:00:00 2001 From: Wendelin Date: Tue, 23 Jan 2024 13:04:18 +0100 Subject: [PATCH 6/8] auto open options page on install --- scripts/background.js | 4 ++++ scripts/trektor.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/background.js b/scripts/background.js index 0d26949..68bc8d1 100644 --- a/scripts/background.js +++ b/scripts/background.js @@ -16,6 +16,10 @@ class BackgroundWorker { throw new Error(`unknown action: ${msg.action}`); } }); + + trektor.browser.runtime.onInstalled.addListener((details) => { + if (details.reason == "install") trektor.browser.runtime.openOptionsPage() + }) } async track(cardId) { diff --git a/scripts/trektor.js b/scripts/trektor.js index 2ed56a6..2193f19 100644 --- a/scripts/trektor.js +++ b/scripts/trektor.js @@ -34,7 +34,7 @@ class TrelloGateway { }); if (response.status === 401) { - throw new Error('Invalid or expired trello token.'); + throw new Error('Invalid or expired trello token. \n Open Trektor settings to get a new one.'); } else { return response.json(); } @@ -92,7 +92,7 @@ class TogglGateway { }); if (response.status === 403) { - throw new Error('Invalid toggl token.'); + throw new Error('Invalid toggl token. \n Open Trektor settings page to get a valid one.'); } else { return response.json(); } From 8a05255ba494b6f7e2742e0da9dfca3ff3802311 Mon Sep 17 00:00:00 2001 From: Wendelin Date: Tue, 23 Jan 2024 15:17:58 +0100 Subject: [PATCH 7/8] improve UX --- options/index.html | 8 ++++---- scripts/background.js | 4 +++- scripts/content_script.js | 4 ++-- scripts/trektor.js | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/options/index.html b/options/index.html index d35d24e..a07ce6a 100644 --- a/options/index.html +++ b/options/index.html @@ -12,14 +12,14 @@

Willkommen bei Trektor!

✘ Berechtigung fehlt!
- Trello Token + Trello Token ↗ - + - Toggl Token + Toggl Token ↗ - + diff --git a/scripts/background.js b/scripts/background.js index 68bc8d1..4e27767 100644 --- a/scripts/background.js +++ b/scripts/background.js @@ -12,13 +12,15 @@ class BackgroundWorker { case "addTask": await this.addTask(...msg.args); return; + case "options": + trektor.browser.runtime.openOptionsPage(); default: throw new Error(`unknown action: ${msg.action}`); } }); trektor.browser.runtime.onInstalled.addListener((details) => { - if (details.reason == "install") trektor.browser.runtime.openOptionsPage() + if (details.reason == "install") trektor.browser.runtime.openOptionsPage(); }) } diff --git a/scripts/content_script.js b/scripts/content_script.js index cdd789b..346d19a 100644 --- a/scripts/content_script.js +++ b/scripts/content_script.js @@ -41,7 +41,7 @@ async function addButton() { trackButtonIcon.classList.replace("icon-clock", "icon-check-circle"); window.setTimeout(() => trackButtonIcon.classList.replace("icon-check-circle", "icon-clock"), 2000); } catch (err) { - window.alert(err); + if (window.confirm(err + "\n\n Open Trektor options page now?")) trektor.browser.runtime.sendMessage({ action: "options" }); } finally { trackButtonIcon.classList.remove("trektor-state-loading"); } @@ -54,7 +54,7 @@ async function addButton() { args: [window.location.pathname.split("/", 3)[2]], }); } catch (err) { - window.alert(err); + if (window.confirm(err + "\n\n Open Trektor options page now?")) trektor.browser.runtime.sendMessage({ action: "options" }); } }); } diff --git a/scripts/trektor.js b/scripts/trektor.js index 2193f19..b77fd79 100644 --- a/scripts/trektor.js +++ b/scripts/trektor.js @@ -34,7 +34,7 @@ class TrelloGateway { }); if (response.status === 401) { - throw new Error('Invalid or expired trello token. \n Open Trektor settings to get a new one.'); + throw new Error('Invalid or expired trello token. \n Get a new one in the settings.'); } else { return response.json(); } @@ -92,7 +92,7 @@ class TogglGateway { }); if (response.status === 403) { - throw new Error('Invalid toggl token. \n Open Trektor settings page to get a valid one.'); + throw new Error('Invalid toggl token. \n Get a valid one from the settings.'); } else { return response.json(); } From 1ea21093741d22d55d3b2953593f2269174da59e Mon Sep 17 00:00:00 2001 From: Wendelin Date: Tue, 23 Jan 2024 15:43:37 +0100 Subject: [PATCH 8/8] consistent naming --- options/script.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/options/script.js b/options/script.js index bb2a763..32ed71e 100644 --- a/options/script.js +++ b/options/script.js @@ -20,16 +20,16 @@ document.querySelectorAll("input").forEach((field) => { }); }); -check_permissions() +checkPermissions() document.getElementById("request").addEventListener("click", () => { requestPermissions() }) function requestPermissions() { console.log("hallo") - browser.permissions.request(permissions, () => { check_permissions() }) + browser.permissions.request(permissions, () => { checkPermissions() }) } -function check_permissions() { +function checkPermissions() { browser.permissions.contains(permissions, (contains) => { if (contains) { document.getElementById("permissions-granted").style.display = "block";