Rewrite the background scripts.

This commit is contained in:
Bauke 2023-04-27 12:47:19 +02:00
parent 974d8f22fd
commit 20f399bda8
Signed by: Bauke
GPG Key ID: C1C0F29952BCF558
6 changed files with 212 additions and 196 deletions

View File

@ -1,43 +0,0 @@
import browser from 'webextension-polyfill';
import {Settings} from '../settings/settings.js';
import {updateBadge} from '../utilities/badge.js';
// Chromium action handler in service worker.
export async function actionClicked(): Promise<void> {
await nextItem();
}
let timeoutId: number | undefined;
// Firefox browser action handler in background script.
export async function browserActionClicked(): Promise<void> {
// When the extension icon is initially clicked, create a timeout for 500ms
// that will open the next queue item when it expires.
if (timeoutId === undefined) {
timeoutId = window.setTimeout(async () => {
timeoutId = undefined;
await nextItem();
}, 500);
return;
}
// If the icon is clicked again in those 500ms, open the options page instead.
window.clearTimeout(timeoutId);
timeoutId = undefined;
await browser.runtime.openOptionsPage();
}
async function nextItem(): Promise<void> {
const settings = await Settings.fromSyncStorage();
const nextItem = settings.nextQueueItem();
if (nextItem === undefined) {
await browser.runtime.openOptionsPage();
return;
}
await browser.tabs.update({url: nextItem.url});
await settings.removeQueueItem(nextItem.id);
await updateBadge(settings);
}

View File

@ -1,111 +0,0 @@
import browser from 'webextension-polyfill';
import {Settings} from '../settings/settings.js';
import {updateBadge} from '../utilities/badge.js';
export function getContextMenus(): browser.Menus.CreateCreatePropertiesType[] {
const actionContext =
import.meta.env.VITE_BROWSER === 'chromium' ? 'action' : 'browser_action';
const contextMenus: browser.Menus.CreateCreatePropertiesType[] = [
{
id: 'queue-add-new-link',
title: 'Add to Queue',
contexts: ['link'],
},
{
id: 'queue-open-next-link-in-new-tab',
title: 'Open next link in new tab',
contexts: [actionContext],
},
{
id: 'queue-open-options-page',
title: 'Open the extension page',
contexts: [actionContext],
},
];
if (import.meta.env.VITE_BROWSER === 'firefox') {
contextMenus.push({
id: 'queue-add-new-link-tab',
title: 'Add to Queue',
contexts: ['tab'],
});
}
return contextMenus;
}
export async function initializeContextMenus(): Promise<void> {
const contextMenus = getContextMenus();
await browser.contextMenus.removeAll();
for (const contextMenu of contextMenus) {
browser.contextMenus.create(contextMenu, contextCreated);
}
}
function contextCreated(): void {
const error = browser.runtime.lastError;
if (error !== null && error !== undefined) {
console.error('Queue', error.message);
}
}
export async function contextClicked(
contextMenuIds: Set<string>,
info: browser.Menus.OnClickData,
tab?: browser.Tabs.Tab,
): Promise<void> {
const id = info.menuItemId.toString();
if (!contextMenuIds.has(id)) {
return;
}
const settings = await Settings.fromSyncStorage();
if (id.startsWith('queue-add-new-link')) {
let text: string | undefined;
let url: string | undefined;
switch (id) {
case 'queue-add-new-link': {
text = info.linkText;
url = info.linkUrl;
break;
}
case 'queue-add-new-link-tab': {
text = tab?.title;
url = info.pageUrl;
break;
}
default: {
console.warn(`Encountered unknown context menu ID: ${id}`);
return;
}
}
if (url === undefined) {
console.warn('Cannot add a new item without a URL.');
return;
}
await settings.insertQueueItem(text ?? url, url);
await updateBadge(settings);
} else if (id === 'queue-open-next-link-in-new-tab') {
const nextItem = settings.nextQueueItem();
if (nextItem === undefined) {
await browser.runtime.openOptionsPage();
} else {
await browser.tabs.create({active: true, url: nextItem.url});
await settings.removeQueueItem(nextItem.id);
await updateBadge(settings);
}
} else if (id === 'queue-open-options-page') {
await browser.runtime.openOptionsPage();
}
}

View File

@ -1,42 +0,0 @@
import browser from 'webextension-polyfill';
import {Settings} from '../settings/settings.js';
import {updateBadge} from '../utilities/badge.js';
import {History} from '../utilities/history.js';
import {actionClicked, browserActionClicked} from './browser-action.js';
import {
contextClicked,
getContextMenus,
initializeContextMenus,
} from './context-menus.js';
browser.runtime.onStartup.addListener(async () => {
console.debug('Clearing history.');
await History.clear();
await updateBadge(await Settings.fromSyncStorage());
});
browser.runtime.onInstalled.addListener(async () => {
await initializeContextMenus();
await updateBadge(await Settings.fromSyncStorage());
});
browser.contextMenus.onClicked.addListener(async (info, tab) => {
const contextMenus = getContextMenus();
const contextMenuIds = new Set<string>(
contextMenus.map(({id}) => id ?? 'queue-unknown'),
);
await contextClicked(contextMenuIds, info, tab);
});
if (import.meta.env.DEV) {
void browser.runtime.openOptionsPage();
}
if (import.meta.env.VITE_BROWSER === 'chromium') {
browser.action.onClicked.addListener(actionClicked);
} else {
browser.browserAction.onClicked.addListener(browserActionClicked);
void initializeContextMenus();
}

View File

@ -0,0 +1,50 @@
// Code for the WebExtension icon (AKA the "browser action").
import browser from "webextension-polyfill";
import {createValue} from "@holllo/webextension-storage";
import {
nextItem,
setBadgeText,
openNextItemOrOptionsPage,
} from "../item/item.js";
/**
* Handle single and double clicks for Firefox.
* - For single click: open the next queued item or the options page if none are
* in the queue.
* - For double click: open the options page.
*
* The reason this can't be done in Chromium is due to Manifest V3 running
* background scripts in service workers where `setTimeout` doesn't work
* reliably. The solution is to use `browser.alarms` instead, however, alarms
* also don't work reliably for this use case because they can only run every
* minute and we need milliseconds for this. And so, Chromium doesn't get double
* click functionality.
*/
export async function firefoxActionClick(): Promise<void> {
const timeoutId = await createValue<number | undefined>({
deserialize: Number,
key: "actionClickTimeoutId",
value: undefined,
});
// If no ID is in storage, this is the first click so start a timeout and
// save its ID.
if (timeoutId.value === undefined) {
timeoutId.value = window.setTimeout(async () => {
// When no second click happens, open the next item or the options page.
await openNextItemOrOptionsPage();
await timeoutId.remove();
}, 500);
await timeoutId.save();
return;
}
// If an ID is present in storage, this is the second click and we want to
// open the options page instead.
window.clearTimeout(timeoutId.value);
await browser.runtime.openOptionsPage();
await timeoutId.remove();
}

View File

@ -0,0 +1,117 @@
import browser from "webextension-polyfill";
import {
createItem,
setBadgeText,
openNextItemOrOptionsPage,
} from "../item/item.js";
/**
* Get properties for all the context menu entries.
*
* @returns The context menu entries.
*/
export function getContextMenus(): browser.Menus.CreateCreatePropertiesType[] {
// In Manifest V2 the WebExtension icon is referred to as the
// "browser action", in MV3 it's just "action".
const actionContext: browser.Menus.ContextType =
$browser === "firefox" ? "browser_action" : "action";
const contextMenus: ReturnType<typeof getContextMenus> = [
{
id: "queue-add-new-link",
title: "Add to Queue",
contexts: ["link"],
},
{
id: "queue-open-next-link-in-new-tab",
title: "Open next link in new tab",
contexts: [actionContext],
},
{
id: "queue-open-options-page",
title: "Open the extension page",
contexts: [actionContext],
},
];
// Only Firefox supports context menu entries for tabs.
if ($browser === "firefox") {
contextMenus.push({
id: "queue-add-new-link-tab",
title: "Add to Queue",
contexts: ["tab"],
});
}
return contextMenus;
}
/**
* Initialize all the context menu entries.
*/
export async function initializeContextMenus(): Promise<void> {
const contextMenus = getContextMenus();
await browser.contextMenus.removeAll();
for (const contextMenu of contextMenus) {
browser.contextMenus.create(contextMenu, contextCreatedHandler);
}
}
/**
* Event handler for context menu creation.
*/
function contextCreatedHandler(): void {
const error = browser.runtime.lastError;
if (error !== null && error !== undefined) {
console.error("Queue", error.message);
}
}
/**
* Event handler for context menu clicks.
*
* @param contextMenuIds A set of all our context menu IDs.
* @param info The context menu click data.
* @param tab The browser tab, if available.
*/
export async function contextClicked(
contextMenuIds: Set<string>,
info: browser.Menus.OnClickData,
tab?: browser.Tabs.Tab,
): Promise<void> {
// Only handle context menus that we know the ID of.
const id = info.menuItemId.toString();
if (!contextMenuIds.has(id)) {
return;
}
if (id.startsWith("queue-add-new-link")) {
let text: string | undefined;
let url: string | undefined;
if (id === "queue-add-new-link") {
text = info.linkText;
url = info.linkUrl;
} else if (id === "queue-add-new-link-tab") {
text = tab?.title;
url = info.pageUrl;
} else {
console.warn(`Encountered unknown context menu ID: ${id}`);
return;
}
if (url === undefined) {
console.warn("Cannot add a new item without a URL.");
return;
}
const item = await createItem(text, url);
await item.save();
await setBadgeText();
} else if (id === "queue-open-next-link-in-new-tab") {
await openNextItemOrOptionsPage(true);
} else if (id === "queue-open-options-page") {
await browser.runtime.openOptionsPage();
}
}

View File

@ -0,0 +1,45 @@
// The main entry point for the background script. Note that in Manifest V3 this
// is run in a service worker.
// https://developer.chrome.com/docs/extensions/migrating/to-service-workers/
import browser from "webextension-polyfill";
import {
clearHistory,
openNextItemOrOptionsPage,
setBadgeText,
} from "../item/item.js";
import {firefoxActionClick} from "./action.js";
import {
contextClicked,
getContextMenus,
initializeContextMenus,
} from "./context-menu.js";
if ($browser === "firefox") {
browser.browserAction.onClicked.addListener(firefoxActionClick);
} else {
browser.action.onClicked.addListener(async () => {
await openNextItemOrOptionsPage();
});
}
browser.runtime.onStartup.addListener(async () => {
await clearHistory();
await setBadgeText();
});
browser.runtime.onInstalled.addListener(async () => {
await initializeContextMenus();
await setBadgeText();
if ($dev) {
await browser.runtime.openOptionsPage();
}
});
browser.contextMenus.onClicked.addListener(async (info, tab) => {
const contextMenus = getContextMenus();
const contextMenuIds = new Set<string>(contextMenus.map(({id}) => id!));
await contextClicked(contextMenuIds, info, tab);
});