commit 3454be6cf7ab6cc37270c354e2217ce83e14d713 Author: Marcel Kapfer Date: Sun Jul 28 23:22:45 2024 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15af1aa --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.xpi diff --git a/icon.afdesign b/icon.afdesign new file mode 100644 index 0000000..7f9fdf8 Binary files /dev/null and b/icon.afdesign differ diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..9a4c633 --- /dev/null +++ b/icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..dcf33b1 --- /dev/null +++ b/manifest.json @@ -0,0 +1,30 @@ +{ + "background": { + "scripts": ["scripts/background.js"], + "type": "module" + }, + "browser_specific_settings": { + "gecko": { + "id": "tb.spaceapper@mmk2410.org", + "strict_min_version": "115.0" + } + }, + "description": "Add webpages to the spaces toolbar.", + "developer": { + "name": "Marcel Kapfer", + "url": "https://mmk2410.org" + }, + "host_permissions": ["*://*/*"], + "icons": { + "48": "icon.svg", + "96": "icon.svg" + }, + "manifest_version": 3, + "name": "SpaceApper", + "options_ui": { + "page": "options/options.html", + "browser_style": false + }, + "permissions": ["storage", "webRequest", "webRequestBlocking"], + "version": "0.1.0" +} diff --git a/options/options.css b/options/options.css new file mode 100644 index 0000000..a9280ca --- /dev/null +++ b/options/options.css @@ -0,0 +1,64 @@ +html { + font-family: sans-serif; +} + +button { + background-color: #e7e7e7; + border-radius: 5px; + border: none; + padding: 5px 10px; + box-shadow: none; +} + +.btn-primary { + color: #fff; + background-color: #7a01d0; +} + +#spaces { + width: 100%; + border-collapse: collapse; +} + +#spaces td { + border-bottom: 1px solid #999; + border-top: 1px solid #999; + border-left: 1px solid #ddd; + border-right: 1px solid #ddd; +} + +#spaces td:first-of-type { + border-left: none; +} + +#spaces td:last-of-type { + border-right: none; +} + +#spaces thead td { + font-weight: bold; + padding: 10px; +} + +#spaces tbody input { + border: none; + height: 100%; + width: 100%; + font-size: inherit; + padding: 10px; + box-sizing: border-box; +} + +#spaces tbody button { + margin: 10px; +} + +#actions { + width: 100%; + margin: 10px 0; + text-align: right; +} + +#actions button:first-of-type { + margin-right: 5px; +} diff --git a/options/options.html b/options/options.html new file mode 100644 index 0000000..4f2c506 --- /dev/null +++ b/options/options.html @@ -0,0 +1,31 @@ + + + + + + + + + +
+ + + + + + + + + + +
NameURLIcon URL
+ +
+ + +
+
+ + + + diff --git a/options/options.js b/options/options.js new file mode 100644 index 0000000..94e2ecc --- /dev/null +++ b/options/options.js @@ -0,0 +1,81 @@ +import { createSpaces, removeSpaces, hashSpaceName } from "../scripts/spaces.mjs"; +import { getApps, storeApps } from "../scripts/storage.mjs"; + +function buildInputRow(name, value) { + const td = document.createElement("td"); + + const input = document.createElement("input"); + input.setAttribute("type", "text"); + input.setAttribute("name", name); + input.setAttribute("value", value); + + td.appendChild(input); + return td; +} + +function buildSpaceElement(app) { + const tr = document.createElement("tr"); + tr.className = "space"; + + tr.appendChild(buildInputRow("title", app.title)); + tr.appendChild(buildInputRow("url", app.url)); + tr.appendChild(buildInputRow("icon", app.icon)); + + const removeTd = document.createElement("td"); + const removeButton = document.createElement("button"); + removeButton.setAttribute("type", "button"); + removeButton.addEventListener("click", removeSpace); + removeButton.textContent = "Remove"; + removeTd.appendChild(removeButton); + + tr.appendChild(removeTd); + return tr; +} + +async function setupPage() { + const apps = await getApps(); + const spaces = document.querySelector("#spaces > tbody"); + + apps.forEach((app) => { + spaces.appendChild(buildSpaceElement(app)); + }); +} + +function addSpace() { + const spaces = document.querySelector("#spaces > tbody"); + const newApp = { + title: "", + url: "", + icon: "", + }; + spaces.appendChild(buildSpaceElement(newApp)); +} + +function removeSpace(e) { + e.target.parentElement.parentElement.remove(); +} + +async function saveChanges(e) { + await e.preventDefault(); + + const apps = []; + + const spaces = Array.from(document.getElementsByClassName("space")); + spaces.forEach((space) => { + const app = { + name: hashSpaceName(space.querySelector("input[name='url']").value), + title: space.querySelector("input[name='title']").value, + url: space.querySelector("input[name='url']").value, + icon: space.querySelector("input[name='icon']").value, + }; + apps.push(app); + }); + + await removeSpaces(); + await storeApps(apps); + await createSpaces(); +} + +document.addEventListener("DOMContentLoaded", setupPage); +document.getElementById("add").addEventListener("click", addSpace); +document.querySelector("form").addEventListener("submit", saveChanges); diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..cf2a885 --- /dev/null +++ b/readme.md @@ -0,0 +1,7 @@ +# SpaceApper + +**A Thunderbird add-on** + +Add webpages to the Thunderbird spaces toolbar. + +*Attention: The author has no experience in creating a add-on for Thunderbird. Therefore, the add-on should not be considered stable.* diff --git a/scripts/background.js b/scripts/background.js new file mode 100644 index 0000000..6749023 --- /dev/null +++ b/scripts/background.js @@ -0,0 +1,3 @@ +import { createSpaces } from "./spaces.mjs"; + +createSpaces(); diff --git a/scripts/listeners.mjs b/scripts/listeners.mjs new file mode 100644 index 0000000..22062e1 --- /dev/null +++ b/scripts/listeners.mjs @@ -0,0 +1,11 @@ +export { changeUserAgent }; + +function changeUserAgent(details) { + for (const header of details.requestHeaders) { + if (header.name.toLowerCase() === "user-agent") { + header.value = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0"; + break; + } + } + return { requestHeaders: details.requestHeaders }; +} diff --git a/scripts/spaces.mjs b/scripts/spaces.mjs new file mode 100644 index 0000000..d278cc3 --- /dev/null +++ b/scripts/spaces.mjs @@ -0,0 +1,49 @@ +import { changeUserAgent } from "./listeners.mjs"; +import { getApps, storeApps } from "./storage.mjs"; + +export { createSpaces, removeSpaces, hashSpaceName }; + +function hashSpaceName(url) { + return btoa(url).replaceAll("=", ""); +} + +async function createSpaces() { + const apps = await getApps(); + + if (apps.length === 0) { + return; + } + + const urls = []; + + apps.forEach(async (app) => { + urls.push(app.url); + await browser.spaces.create(app.name, app.url, { + title: app.title, + defaultIcons: app.icon, + }); + }); + + const matchUrls = urls.map((url) => { + return url.replace(/^(https?:\/\/).*((?<=[\/\.])[^\/\.?]*\.[^\/\.?]*)[\/?]?.*$/g, "$1*.$2/*"); + }); + + if (browser.webRequest.onBeforeSendHeaders.hasListener(changeUserAgent)) { + browser.webRequest.onBeforeSendHeaders.removeListener(changeUserAgent); + } + + browser.webRequest.onBeforeSendHeaders.addListener(changeUserAgent, { urls: matchUrls }, [ + "requestHeaders", + "blocking", + ]); +} + +async function removeSpaces() { + const spaces = await browser.spaces.query({ isSelfOwned: true }); + + spaces.forEach(async (space) => { + await browser.spaces.remove(space.id); + }); + + await storeApps([]); +} diff --git a/scripts/storage.mjs b/scripts/storage.mjs new file mode 100644 index 0000000..4cda683 --- /dev/null +++ b/scripts/storage.mjs @@ -0,0 +1,17 @@ +export { storeApps, getApps }; + +async function storeApps(apps) { + await browser.storage.sync.set({ + apps: apps, + }); +} + +async function getApps() { + const res = await browser.storage.sync.get("apps"); + + if (res.apps === undefined) { + return []; + } + + return res.apps; +}