feat(extension): add Firefox WebExtension support
Testing / remote-protocol-compat (0.9.5) (push) Successful in 48s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 47s
Build & Publish Package / publish (push) Successful in 46s
Package Extension / package-extension (push) Successful in 59s
Testing / test (push) Failing after 50s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 48s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 47s
Build & Publish Package / publish (push) Successful in 46s
Package Extension / package-extension (push) Successful in 59s
Testing / test (push) Failing after 50s
- Add a neutral WebExtension API adapter that uses Firefox browser.* or Chromium chrome.* without mutating globals. - Switch extension runtime code to the adapter and add Firefox-specific typings for tabs, windows, tab groups, storage, scripting, and native messaging ports. - Fix Firefox temporary add-on instructions to load the packaged manifest with background.scripts instead of the Chromium service worker manifest. - Detect Firefox in clients.list via runtime.getBrowserInfo and keep Chromium user-agent fallback support. - Make navigate.open wait briefly for Firefox to replace initial about:blank with the requested URL. - Add JS coverage for API selection, clients.list browser detection, and Firefox navigate.open URL polling. - Bump package and extension version to 0.15.2.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { TabUpdateInfo } from '../types';
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
import { getSessions, runLargeOperation, tabGroupsOnUpdated } from '../core';
|
||||
import { captureCurrentSession } from './session-snapshot';
|
||||
|
||||
@@ -16,31 +18,31 @@ export class AutoSaveManager {
|
||||
readonly autoSaveHandler = async (): Promise<void> => {
|
||||
await this.scheduleAutoSave();
|
||||
};
|
||||
readonly autoSaveUpdatedHandler = async (_tabId: number, changeInfo: chrome.tabs.OnUpdatedInfo = {}): Promise<void> => {
|
||||
readonly autoSaveUpdatedHandler = async (_tabId: number, changeInfo: TabUpdateInfo = {}): Promise<void> => {
|
||||
// Ignore noisy media/title/favicon/loading updates. Sessions only store URL and group/window structure.
|
||||
if (!("url" in changeInfo)) return;
|
||||
await this.scheduleAutoSave();
|
||||
};
|
||||
|
||||
async setEnabled(enabled: boolean) {
|
||||
await chrome.storage.local.set({ autoSave: enabled });
|
||||
chrome.tabs.onCreated.removeListener(this.autoSaveHandler);
|
||||
chrome.tabs.onRemoved.removeListener(this.autoSaveHandler);
|
||||
chrome.tabs.onMoved.removeListener(this.autoSaveHandler);
|
||||
chrome.tabs.onAttached.removeListener(this.autoSaveHandler);
|
||||
chrome.tabs.onDetached.removeListener(this.autoSaveHandler);
|
||||
chrome.tabs.onUpdated.removeListener(this.autoSaveUpdatedHandler);
|
||||
await api.storage.local.set({ autoSave: enabled });
|
||||
api.tabs.onCreated.removeListener(this.autoSaveHandler);
|
||||
api.tabs.onRemoved.removeListener(this.autoSaveHandler);
|
||||
api.tabs.onMoved.removeListener(this.autoSaveHandler);
|
||||
api.tabs.onAttached.removeListener(this.autoSaveHandler);
|
||||
api.tabs.onDetached.removeListener(this.autoSaveHandler);
|
||||
api.tabs.onUpdated.removeListener(this.autoSaveUpdatedHandler);
|
||||
tabGroupsOnUpdated()?.removeListener(this.autoSaveHandler);
|
||||
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer);
|
||||
this.autoSaveTimer = null;
|
||||
this.autoSavePending = false;
|
||||
if (enabled) {
|
||||
chrome.tabs.onCreated.addListener(this.autoSaveHandler);
|
||||
chrome.tabs.onRemoved.addListener(this.autoSaveHandler);
|
||||
chrome.tabs.onMoved.addListener(this.autoSaveHandler);
|
||||
chrome.tabs.onAttached.addListener(this.autoSaveHandler);
|
||||
chrome.tabs.onDetached.addListener(this.autoSaveHandler);
|
||||
chrome.tabs.onUpdated.addListener(this.autoSaveUpdatedHandler);
|
||||
api.tabs.onCreated.addListener(this.autoSaveHandler);
|
||||
api.tabs.onRemoved.addListener(this.autoSaveHandler);
|
||||
api.tabs.onMoved.addListener(this.autoSaveHandler);
|
||||
api.tabs.onAttached.addListener(this.autoSaveHandler);
|
||||
api.tabs.onDetached.addListener(this.autoSaveHandler);
|
||||
api.tabs.onUpdated.addListener(this.autoSaveUpdatedHandler);
|
||||
tabGroupsOnUpdated()?.addListener(this.autoSaveHandler);
|
||||
}
|
||||
return { enabled };
|
||||
@@ -48,12 +50,12 @@ export class AutoSaveManager {
|
||||
|
||||
private async saveAutoSessionIfChanged() {
|
||||
const { session, signature, tabCount } = await captureCurrentSession();
|
||||
const { autoSaveSignature } = await chrome.storage.local.get("autoSaveSignature");
|
||||
const { autoSaveSignature } = await api.storage.local.get("autoSaveSignature");
|
||||
if (autoSaveSignature === signature) return { skipped: true, tabs: tabCount };
|
||||
|
||||
const sessions = await getSessions();
|
||||
sessions.__auto__ = session;
|
||||
await chrome.storage.local.set({ sessions, autoSaveSignature: signature });
|
||||
await api.storage.local.set({ sessions, autoSaveSignature: signature });
|
||||
return { skipped: false, tabs: tabCount };
|
||||
}
|
||||
|
||||
@@ -64,7 +66,7 @@ export class AutoSaveManager {
|
||||
}
|
||||
this.autoSaveInFlight = true;
|
||||
try {
|
||||
const { autoSave } = await chrome.storage.local.get("autoSave");
|
||||
const { autoSave } = await api.storage.local.get("autoSave");
|
||||
if (autoSave) await runLargeOperation("session.auto_save", () => this.saveAutoSessionIfChanged());
|
||||
} finally {
|
||||
this.autoSaveInFlight = false;
|
||||
@@ -76,7 +78,7 @@ export class AutoSaveManager {
|
||||
}
|
||||
|
||||
private async scheduleAutoSave(delayMs = AUTOSAVE_DEBOUNCE_MS) {
|
||||
const { autoSave } = await chrome.storage.local.get("autoSave");
|
||||
const { autoSave } = await api.storage.local.get("autoSave");
|
||||
if (!autoSave) return;
|
||||
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer);
|
||||
this.autoSaveTimer = setTimeout(() => this.runAutoSave(), delayMs);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
import type { Tab } from '../types';
|
||||
import { assertScriptableUrl, executeScript, fetchTabHtml, isBrowserErrorUrl, isErrorPageScriptError, resolveTabUrl } from '../core';
|
||||
import { CommandGroup } from '../classes/CommandGroup';
|
||||
import type { CommandEntry } from '../classes/CommandGroup';
|
||||
import type { DomArgs, DomEvalArgs, DomWaitForArgs, DomPollArgs, Serializable } from '../types';
|
||||
|
||||
function fallbackForErrorPageDomOp(funcName: string, tab: chrome.tabs.Tab): Serializable {
|
||||
function fallbackForErrorPageDomOp(funcName: string, tab: Tab): Serializable {
|
||||
switch (funcName) {
|
||||
case "domExists":
|
||||
return false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
import { CommandGroup } from '../classes/CommandGroup';
|
||||
import type { CommandEntry } from '../classes/CommandGroup';
|
||||
|
||||
@@ -5,7 +6,7 @@ export class ExtensionCommands extends CommandGroup {
|
||||
readonly namespace = "extension";
|
||||
readonly commands: Record<string, CommandEntry> = {
|
||||
"extension.reload": () => {
|
||||
setTimeout(() => chrome.runtime.reload(), 200);
|
||||
setTimeout(() => api.runtime.reload(), 200);
|
||||
return { reloading: true };
|
||||
},
|
||||
"extension.info": () => this.extensionInfo(),
|
||||
@@ -29,9 +30,9 @@ export class ExtensionCommands extends CommandGroup {
|
||||
}
|
||||
|
||||
private extensionInfo() {
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
const manifest = api.runtime.getManifest();
|
||||
return {
|
||||
id: chrome.runtime.id,
|
||||
id: api.runtime.id,
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
manifestVersion: manifest.manifest_version,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
import { asTabIds, buildTabBlocks, getLargeOperationThrottle, getTabGroup, groupTabs, moveTabGroup, processInBatches, queryTabGroups, resolveGroupId, runLargeOperation, tabInfo, ungroupTabs, updateTabGroup } from '../core';
|
||||
import { CommandGroup } from '../classes/CommandGroup';
|
||||
import type { CommandEntry } from '../classes/CommandGroup';
|
||||
@@ -18,7 +19,7 @@ export class GroupsCommands extends CommandGroup {
|
||||
|
||||
private async groupList() {
|
||||
const groups = await queryTabGroups({});
|
||||
const all = await chrome.tabs.query({});
|
||||
const all = await api.tabs.query({});
|
||||
return groups.map(g => ({
|
||||
id: g.id,
|
||||
title: g.title,
|
||||
@@ -30,7 +31,7 @@ export class GroupsCommands extends CommandGroup {
|
||||
}
|
||||
|
||||
private async groupTabs({ groupId }: GroupTabsArgs) {
|
||||
const all = await chrome.tabs.query({});
|
||||
const all = await api.tabs.query({});
|
||||
return all.filter(t => t.groupId === groupId).map(tabInfo);
|
||||
}
|
||||
|
||||
@@ -47,7 +48,7 @@ export class GroupsCommands extends CommandGroup {
|
||||
|
||||
private async groupClose({ groupId, gentleMode, __job }: GroupCloseArgs = {}) {
|
||||
return runLargeOperation("group.close", async () => {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
const tabs = await api.tabs.query({});
|
||||
const groupTabs = tabs.filter(t => t.groupId === groupId);
|
||||
const tabIds = groupTabs.map(t => t.id);
|
||||
const throttle = await getLargeOperationThrottle(tabIds.length, gentleMode);
|
||||
@@ -57,7 +58,7 @@ export class GroupsCommands extends CommandGroup {
|
||||
}
|
||||
|
||||
private async groupOpen({ name }: GroupOpenArgs) {
|
||||
const tab = await chrome.tabs.create({ active: true });
|
||||
const tab = await api.tabs.create({ active: true });
|
||||
const groupId = await groupTabs({ tabIds: asTabIds([tab.id]) });
|
||||
await updateTabGroup(groupId, { title: name });
|
||||
return { id: groupId, name };
|
||||
@@ -65,15 +66,15 @@ export class GroupsCommands extends CommandGroup {
|
||||
|
||||
private async groupAddTab({ group, url }: GroupAddTabArgs) {
|
||||
const groupId = await resolveGroupId(group);
|
||||
const existingTabs = await chrome.tabs.query({ groupId });
|
||||
const tab = await chrome.tabs.create({ url: url || "chrome://newtab/", active: true });
|
||||
const existingTabs = await api.tabs.query({ groupId });
|
||||
const tab = await api.tabs.create({ url: url || "chrome://newtab/", active: true });
|
||||
await groupTabs({ tabIds: asTabIds([tab.id]), groupId });
|
||||
// If a URL was provided, close any blank placeholder tabs left from group creation
|
||||
if (url) {
|
||||
const placeholders = existingTabs.filter(t =>
|
||||
t.url === "chrome://newtab/" || t.url === "about:blank" || t.pendingUrl === "chrome://newtab/"
|
||||
);
|
||||
if (placeholders.length) await chrome.tabs.remove(placeholders.map(t => t.id));
|
||||
if (placeholders.length) await api.tabs.remove(placeholders.map(t => t.id));
|
||||
}
|
||||
return { tabId: tab.id, groupId };
|
||||
}
|
||||
@@ -81,7 +82,7 @@ export class GroupsCommands extends CommandGroup {
|
||||
private async groupMove({ group, forward, backward }: GroupMoveArgs) {
|
||||
const groupId = await resolveGroupId(group);
|
||||
const groupInfo = await getTabGroup(groupId);
|
||||
const allTabs = await chrome.tabs.query({ windowId: groupInfo.windowId });
|
||||
const allTabs = await api.tabs.query({ windowId: groupInfo.windowId });
|
||||
allTabs.sort((a, b) => a.index - b.index);
|
||||
|
||||
const blocks = buildTabBlocks(allTabs);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
import type { Tab } from '../types';
|
||||
import { getActiveTab, getAliases, groupTabs as groupTabIds, isBrowserErrorUrl, resolveGroupId, tabInfo, updateTabGroup } from '../core';
|
||||
import { CommandGroup } from '../classes/CommandGroup';
|
||||
import type { CommandEntry } from '../classes/CommandGroup';
|
||||
@@ -26,19 +28,19 @@ export class NavigationCommands extends CommandGroup {
|
||||
const entry = Object.entries(aliases).find(([, v]) => v === windowName);
|
||||
if (entry) windowId = parseInt(entry[0]);
|
||||
}
|
||||
const tab = await chrome.tabs.create({ url, active: Boolean(focus) && !background, windowId });
|
||||
const tab = await api.tabs.create({ url, active: Boolean(focus) && !background, windowId });
|
||||
if (groupNameOrId != null) {
|
||||
let groupId;
|
||||
try {
|
||||
groupId = await resolveGroupId(groupNameOrId);
|
||||
// Close any blank placeholder tabs that were created when the group was made
|
||||
const groupTabs = await chrome.tabs.query({ groupId });
|
||||
const groupTabs = await api.tabs.query({ groupId });
|
||||
const placeholders = groupTabs.filter(t =>
|
||||
t.id !== tab.id &&
|
||||
(t.url === "chrome://newtab/" || t.url === "about:blank" || t.pendingUrl === "chrome://newtab/")
|
||||
);
|
||||
await groupTabIds({ tabIds: [tab.id], groupId });
|
||||
if (placeholders.length) await chrome.tabs.remove(placeholders.map(t => t.id));
|
||||
if (placeholders.length) await api.tabs.remove(placeholders.map(t => t.id));
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error) || !e.message.startsWith("No tab group found")) throw e;
|
||||
// Group doesn't exist — create it with the tab already in it
|
||||
@@ -46,14 +48,37 @@ export class NavigationCommands extends CommandGroup {
|
||||
await updateTabGroup(groupId, { title: String(groupNameOrId) });
|
||||
}
|
||||
}
|
||||
return { id: tab.id, url: tab.url };
|
||||
const loadedTab = await this.waitForOpenedTabUrl(tab.id, url, tab);
|
||||
return { id: loadedTab.id, url: loadedTab.url || loadedTab.pendingUrl || url };
|
||||
}
|
||||
|
||||
private async waitForOpenedTabUrl(tabId: number, targetUrl: string, initialTab: Tab): Promise<Tab> {
|
||||
const initialUrl = initialTab.url || initialTab.pendingUrl || "";
|
||||
if (this.isOpenedTabUrlReady(initialUrl, targetUrl)) return initialTab;
|
||||
|
||||
const deadline = Date.now() + 2000;
|
||||
while (Date.now() < deadline) {
|
||||
const current = await api.tabs.get(tabId);
|
||||
const currentUrl = current.url || current.pendingUrl || "";
|
||||
if (this.isOpenedTabUrlReady(currentUrl, targetUrl)) return current;
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
}
|
||||
|
||||
return api.tabs.get(tabId);
|
||||
}
|
||||
|
||||
private isOpenedTabUrlReady(currentUrl: string, targetUrl: string): boolean {
|
||||
if (!currentUrl) return false;
|
||||
if (currentUrl === targetUrl || currentUrl.startsWith(targetUrl)) return true;
|
||||
if (targetUrl === "about:blank" || targetUrl === "chrome://newtab/") return currentUrl === targetUrl;
|
||||
return currentUrl !== "about:blank" && currentUrl !== "chrome://newtab/";
|
||||
}
|
||||
|
||||
private async navTo({ tabId, url }: NavToArgs) {
|
||||
const tab = await chrome.tabs.update(tabId, { url });
|
||||
const tab = await api.tabs.update(tabId, { url });
|
||||
const deadline = Date.now() + 1000;
|
||||
while (tabId && Date.now() < deadline) {
|
||||
const current = await chrome.tabs.get(tabId);
|
||||
const current = await api.tabs.get(tabId);
|
||||
const currentUrl = current.url || current.pendingUrl || "";
|
||||
if (currentUrl === url || currentUrl.startsWith(url)) {
|
||||
return { id: current.id, url: currentUrl };
|
||||
@@ -65,35 +90,35 @@ export class NavigationCommands extends CommandGroup {
|
||||
|
||||
private async navReload({ tabId }: NavTabArgs, bypassCache: boolean) {
|
||||
const tab = tabId ? { id: tabId } : await getActiveTab();
|
||||
await chrome.tabs.reload(tab.id, { bypassCache });
|
||||
await api.tabs.reload(tab.id, { bypassCache });
|
||||
return { tabId: tab.id };
|
||||
}
|
||||
|
||||
private async navBack({ tabId }: NavTabArgs) {
|
||||
const tab = tabId ? { id: tabId } : await getActiveTab();
|
||||
await chrome.tabs.goBack(tab.id);
|
||||
await api.tabs.goBack(tab.id);
|
||||
return { tabId: tab.id };
|
||||
}
|
||||
|
||||
private async navForward({ tabId }: NavTabArgs) {
|
||||
const tab = tabId ? { id: tabId } : await getActiveTab();
|
||||
await chrome.tabs.goForward(tab.id);
|
||||
await api.tabs.goForward(tab.id);
|
||||
return { tabId: tab.id };
|
||||
}
|
||||
|
||||
private async navFocus({ pattern }: NavFocusArgs) {
|
||||
// If pattern is a plain integer, treat it as a tab ID
|
||||
const asInt = parseInt(pattern);
|
||||
let match: chrome.tabs.Tab | undefined;
|
||||
let match: Tab | undefined;
|
||||
if (!isNaN(asInt) && String(asInt) === String(pattern)) {
|
||||
match = await chrome.tabs.get(asInt);
|
||||
match = await api.tabs.get(asInt);
|
||||
} else {
|
||||
const all = await chrome.tabs.query({});
|
||||
const all = await api.tabs.query({});
|
||||
match = all.find(t => (t.url && t.url.includes(pattern)) || (t.pendingUrl && t.pendingUrl.includes(pattern)));
|
||||
}
|
||||
if (!match) return null;
|
||||
await chrome.windows.update(match.windowId, { focused: true });
|
||||
await chrome.tabs.update(match.id, { active: true });
|
||||
await api.windows.update(match.windowId, { focused: true });
|
||||
await api.tabs.update(match.id, { active: true });
|
||||
return { id: match.id, url: match.url || match.pendingUrl, title: match.title };
|
||||
}
|
||||
|
||||
@@ -102,7 +127,7 @@ export class NavigationCommands extends CommandGroup {
|
||||
const deadline = Date.now() + timeout;
|
||||
const interval = 200;
|
||||
while (Date.now() < deadline) {
|
||||
const t = await chrome.tabs.get(tab.id);
|
||||
const t = await api.tabs.get(tab.id);
|
||||
const currentUrl = t.url || t.pendingUrl || "";
|
||||
if (isBrowserErrorUrl(currentUrl)) {
|
||||
throw new Error(`Tab ${tab.id} is showing an error page while waiting for load (${currentUrl})`);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
import type { Tab, TabGroup } from '../types';
|
||||
import { normalizeGroupColor, queryTabGroups } from '../core';
|
||||
import type { SessionTab, StoredSession } from '../types';
|
||||
|
||||
export function buildSessionSnapshot(tabs: chrome.tabs.Tab[], groups: chrome.tabGroups.TabGroup[]): SessionTab[] {
|
||||
export function buildSessionSnapshot(tabs: Tab[], groups: TabGroup[]): SessionTab[] {
|
||||
const groupById = new Map(groups.map(group => [group.id, group]));
|
||||
return tabs
|
||||
.filter(tab => Boolean(tab.url || tab.pendingUrl))
|
||||
@@ -27,7 +29,7 @@ export function buildSessionSnapshot(tabs: chrome.tabs.Tab[], groups: chrome.tab
|
||||
* its change-detection signature. Shared by session.save and the autosave path.
|
||||
*/
|
||||
export async function captureCurrentSession(): Promise<{ session: StoredSession; signature: string; tabCount: number }> {
|
||||
const tabs = await chrome.tabs.query({});
|
||||
const tabs = await api.tabs.query({});
|
||||
const groups = await queryTabGroups({});
|
||||
const sessionTabs = buildSessionSnapshot(tabs, groups);
|
||||
const signature = sessionSignature(sessionTabs);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
import { getLargeOperationThrottle, getProfileAlias, getSessionTabs, getSessions, groupTabs, normalizeGroupColor, runLargeOperation, throwIfJobCancelled, updateJobProgress, updateTabGroup, yieldForLargeOperation } from '../core';
|
||||
import { CommandGroup } from '../classes/CommandGroup';
|
||||
import { AutoSaveManager } from './autosave';
|
||||
@@ -32,18 +33,18 @@ export class SessionCommands extends CommandGroup {
|
||||
const { session, tabCount } = await captureCurrentSession();
|
||||
const sessions = await getSessions();
|
||||
sessions[name] = session;
|
||||
await chrome.storage.local.set({ sessions });
|
||||
await api.storage.local.set({ sessions });
|
||||
return { name, tabs: tabCount };
|
||||
}
|
||||
|
||||
// Public: invoked from index.ts on chrome.tabs.onActivated.
|
||||
// Public: invoked from index.ts on api.tabs.onActivated.
|
||||
async activateLazyTab(tabId: number | string) {
|
||||
const { lazySessionTabs } = await chrome.storage.local.get<{ lazySessionTabs?: LazySessionMap }>("lazySessionTabs");
|
||||
const { lazySessionTabs } = await api.storage.local.get<{ lazySessionTabs?: LazySessionMap }>("lazySessionTabs");
|
||||
const entry = lazySessionTabs?.[tabId];
|
||||
if (!entry?.url) return false;
|
||||
delete lazySessionTabs[tabId];
|
||||
await chrome.storage.local.set({ lazySessionTabs });
|
||||
await chrome.tabs.update(Number(tabId), { url: entry.url });
|
||||
await api.storage.local.set({ lazySessionTabs });
|
||||
await api.tabs.update(Number(tabId), { url: entry.url });
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -58,24 +59,24 @@ export class SessionCommands extends CommandGroup {
|
||||
const throttle = await getLargeOperationThrottle(sessionTabs.length, gentleMode);
|
||||
const createBatchSize = Math.max(1, Math.min(10, throttle.batchSize));
|
||||
const eagerLimit = lazy ? Math.max(0, Number(eagerTabs) || 0) : sessionTabs.length;
|
||||
const { lazySessionTabs } = await chrome.storage.local.get<{ lazySessionTabs?: LazySessionMap }>("lazySessionTabs");
|
||||
const { lazySessionTabs } = await api.storage.local.get<{ lazySessionTabs?: LazySessionMap }>("lazySessionTabs");
|
||||
const lazyMap: LazySessionMap = lazySessionTabs || {};
|
||||
updateJobProgress(__job, { phase: "opening", current: 0, total: sessionTabs.length });
|
||||
|
||||
for (const [idx, entry] of sessionTabs.entries()) {
|
||||
throwIfJobCancelled(__job);
|
||||
const shouldLazy = lazy && idx >= eagerLimit;
|
||||
const tab = await chrome.tabs.create({ url: shouldLazy ? lazyPlaceholderUrl(entry.url) : entry.url, active: false, pinned: Boolean(entry.pinned) });
|
||||
const tab = await api.tabs.create({ url: shouldLazy ? lazyPlaceholderUrl(entry.url) : entry.url, active: false, pinned: Boolean(entry.pinned) });
|
||||
createdTabs.push({ tabId: tab.id, entry });
|
||||
if (shouldLazy) {
|
||||
lazyMap[String(tab.id)] = { url: entry.url, createdAt: Date.now() };
|
||||
} else if (discardBackgroundTabs && !entry.pinned && chrome.tabs.discard) {
|
||||
try { await chrome.tabs.discard(tab.id); } catch (_) {}
|
||||
} else if (discardBackgroundTabs && !entry.pinned && api.tabs.discard) {
|
||||
try { await api.tabs.discard(tab.id); } catch (_) {}
|
||||
}
|
||||
updateJobProgress(__job, { phase: shouldLazy ? "creating lazy placeholders" : "opening", current: createdTabs.length, total: sessionTabs.length });
|
||||
await yieldForLargeOperation(createdTabs.length, createBatchSize, Math.max(50, throttle.pauseMs));
|
||||
}
|
||||
if (lazy) await chrome.storage.local.set({ lazySessionTabs: lazyMap });
|
||||
if (lazy) await api.storage.local.set({ lazySessionTabs: lazyMap });
|
||||
|
||||
const groups = new Map();
|
||||
for (const { tabId, entry } of createdTabs) {
|
||||
@@ -119,7 +120,7 @@ export class SessionCommands extends CommandGroup {
|
||||
const sessions = await getSessions();
|
||||
if (!(name in sessions)) throw new Error(`Session '${name}' not found`);
|
||||
delete sessions[name];
|
||||
await chrome.storage.local.set({ sessions });
|
||||
await api.storage.local.set({ sessions });
|
||||
return { name };
|
||||
}
|
||||
|
||||
@@ -154,16 +155,18 @@ export class SessionCommands extends CommandGroup {
|
||||
if (!overwrite && sessions[name]) throw new Error(`Session '${name}' already exists`);
|
||||
const stored = session as object as StoredSession;
|
||||
sessions[name] = { ...stored, savedAt: Number(stored.savedAt) || Date.now() };
|
||||
await chrome.storage.local.set({ sessions });
|
||||
await api.storage.local.set({ sessions });
|
||||
return { name, tabs: getSessionTabs(sessions[name]).length };
|
||||
}
|
||||
|
||||
private async clientsList() {
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
const manifest = api.runtime.getManifest();
|
||||
const alias = await getProfileAlias();
|
||||
const browserInfo = api.runtime.getBrowserInfo ? await api.runtime.getBrowserInfo() : null;
|
||||
const userAgent = navigator.userAgent;
|
||||
return [{
|
||||
name: "Chrome",
|
||||
version: navigator.userAgent.match(/Chrome\/([\d.]+)/)?.[1] || "unknown",
|
||||
name: browserInfo?.name || (userAgent.includes("Firefox/") ? "Firefox" : "Chrome"),
|
||||
version: browserInfo?.version || userAgent.match(/(?:Chrome|Firefox)\/([\d.]+)/)?.[1] || "unknown",
|
||||
platform: navigator.platform,
|
||||
extensionVersion: manifest.version,
|
||||
profile: alias,
|
||||
@@ -171,7 +174,7 @@ export class SessionCommands extends CommandGroup {
|
||||
}
|
||||
|
||||
private async clientsRenameProfile({ alias }: ClientsRenameProfileArgs) {
|
||||
await chrome.storage.local.set({ profileAlias: alias });
|
||||
await api.storage.local.set({ profileAlias: alias });
|
||||
return { alias };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
import { fetchTabHtml, getActiveTab, getAliases, isBrowserErrorUrl, tabInfo } from '../core';
|
||||
import { CommandGroup } from '../classes/CommandGroup';
|
||||
import type { CommandEntry } from '../classes/CommandGroup';
|
||||
@@ -17,7 +18,7 @@ export class TabsQueryCommands extends CommandGroup {
|
||||
};
|
||||
|
||||
private async tabsList() {
|
||||
const windows = await chrome.windows.getAll({ populate: true });
|
||||
const windows = await api.windows.getAll({ populate: true });
|
||||
const aliases = await getAliases();
|
||||
const tabs = [];
|
||||
for (const w of windows) {
|
||||
@@ -34,7 +35,7 @@ export class TabsQueryCommands extends CommandGroup {
|
||||
}
|
||||
|
||||
private async tabsActiveInWindow({ windowId }: TabsActiveInWindowArgs) {
|
||||
const activeTabs = await chrome.tabs.query({ windowId, active: true });
|
||||
const activeTabs = await api.tabs.query({ windowId, active: true });
|
||||
const tab = activeTabs[0];
|
||||
if (!tab) {
|
||||
throw new Error(`No active tab found for window ${windowId}`);
|
||||
@@ -43,24 +44,24 @@ export class TabsQueryCommands extends CommandGroup {
|
||||
}
|
||||
|
||||
private async tabsStatus({ tabId }: TabIdArgs) {
|
||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
||||
const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab();
|
||||
return tabInfo(tab);
|
||||
}
|
||||
|
||||
private async tabsFilter({ pattern }: TabsPatternArgs) {
|
||||
const all = await chrome.tabs.query({});
|
||||
const all = await api.tabs.query({});
|
||||
return all.filter(t => t.url && t.url.includes(pattern)).map(tabInfo);
|
||||
}
|
||||
|
||||
private async tabsCount({ pattern }: TabsPatternArgs) {
|
||||
const all = await chrome.tabs.query({});
|
||||
const all = await api.tabs.query({});
|
||||
if (pattern) return all.filter(t => t.url && t.url.includes(pattern)).length;
|
||||
return all.length;
|
||||
}
|
||||
|
||||
private async tabsQuery({ search }: TabsQueryArgs) {
|
||||
const q = search.toLowerCase();
|
||||
const all = await chrome.tabs.query({});
|
||||
const all = await api.tabs.query({});
|
||||
return all.filter(t =>
|
||||
(t.url && t.url.toLowerCase().includes(q)) ||
|
||||
(t.title && t.title.toLowerCase().includes(q))
|
||||
@@ -68,7 +69,7 @@ export class TabsQueryCommands extends CommandGroup {
|
||||
}
|
||||
|
||||
private async tabsWatchUrl({ pattern, timeout = 30000, tabId }: TabsWatchUrlArgs = {}) {
|
||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
||||
const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab();
|
||||
const deadline = Date.now() + timeout;
|
||||
const regex = new RegExp(pattern);
|
||||
let lastUrl = tab.url || tab.pendingUrl || "";
|
||||
@@ -81,7 +82,7 @@ export class TabsQueryCommands extends CommandGroup {
|
||||
if (matches(lastUrl)) return tabInfo(tab);
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const t = await chrome.tabs.get(tab.id);
|
||||
const t = await api.tabs.get(tab.id);
|
||||
lastUrl = t.url || t.pendingUrl || "";
|
||||
lastStatus = t.status || "unknown";
|
||||
if (matches(t.pendingUrl || "") || matches(t.url || "")) return tabInfo(t);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
import type { TabMoveProperties, BrowserWindow } from '../types';
|
||||
import { asTabIds, getActiveTab, getLargeOperationThrottle, groupTabs, processInBatches, resolveTabForDirectAction, runLargeOperation, throwIfJobCancelled, updateJobProgress, yieldForLargeOperation } from '../core';
|
||||
import { CommandGroup } from '../classes/CommandGroup';
|
||||
import type { CommandEntry } from '../classes/CommandGroup';
|
||||
@@ -23,7 +25,7 @@ export class TabsMutationCommands extends CommandGroup {
|
||||
return runLargeOperation("tabs.close", async () => {
|
||||
let toClose: number[] = [];
|
||||
if (duplicates) {
|
||||
const windows = await chrome.windows.getAll({ populate: true });
|
||||
const windows = await api.windows.getAll({ populate: true });
|
||||
const seen = new Set<string>();
|
||||
for (const w of windows) {
|
||||
for (const t of w.tabs || []) {
|
||||
@@ -34,7 +36,7 @@ export class TabsMutationCommands extends CommandGroup {
|
||||
}
|
||||
}
|
||||
} else if (inactive) {
|
||||
const all = await chrome.tabs.query({});
|
||||
const all = await api.tabs.query({});
|
||||
toClose = all.filter(t => !t.active).map(t => t.id);
|
||||
} else if (tabIds?.length) {
|
||||
toClose = tabIds.filter(id => id != null);
|
||||
@@ -42,17 +44,17 @@ export class TabsMutationCommands extends CommandGroup {
|
||||
toClose = [tabId];
|
||||
}
|
||||
const throttle = await getLargeOperationThrottle(toClose.length, gentleMode);
|
||||
await processInBatches(toClose, throttle, batch => chrome.tabs.remove(batch), { job: __job, phase: "closing tabs" });
|
||||
await processInBatches(toClose, throttle, batch => api.tabs.remove(batch), { job: __job, phase: "closing tabs" });
|
||||
return { closed: toClose.length, gentle: throttle.gentle, audible: throttle.audible };
|
||||
});
|
||||
}
|
||||
|
||||
private async tabsMove({ tabId, groupId, windowId, index, forward, backward }: TabsMoveArgs) {
|
||||
const moveProps: Partial<chrome.tabs.MoveProperties> = {};
|
||||
const moveProps: Partial<TabMoveProperties> = {};
|
||||
if (windowId != null) moveProps.windowId = windowId;
|
||||
|
||||
if (forward || backward) {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
const tab = await api.tabs.get(tabId);
|
||||
if (forward) moveProps.index = tab.index + 2; // +2 because Chrome shifts after removal
|
||||
else moveProps.index = Math.max(0, tab.index - 1);
|
||||
} else if (index != null) {
|
||||
@@ -62,7 +64,7 @@ export class TabsMutationCommands extends CommandGroup {
|
||||
}
|
||||
|
||||
// `index` is always assigned by one of the branches above before this call.
|
||||
await chrome.tabs.move(tabId, moveProps as chrome.tabs.MoveProperties);
|
||||
await api.tabs.move(tabId, moveProps as TabMoveProperties);
|
||||
if (groupId != null) {
|
||||
await groupTabs({ tabIds: asTabIds([tabId]), groupId });
|
||||
}
|
||||
@@ -70,7 +72,7 @@ export class TabsMutationCommands extends CommandGroup {
|
||||
}
|
||||
|
||||
private async tabsActive({ tabId }: TabIdArgs) {
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
await api.tabs.update(tabId, { active: true });
|
||||
return { tabId };
|
||||
}
|
||||
|
||||
@@ -80,7 +82,7 @@ export class TabsMutationCommands extends CommandGroup {
|
||||
|
||||
private async tabsSort({ by, gentleMode, __job }: TabsSortArgs = {}) {
|
||||
return runLargeOperation("tabs.sort", async () => {
|
||||
const windows = await chrome.windows.getAll({ populate: true });
|
||||
const windows = await api.windows.getAll({ populate: true });
|
||||
let moved = 0;
|
||||
const totalTabs = windows.reduce((sum, w) => sum + (w.tabs?.length || 0), 0);
|
||||
updateJobProgress(__job, { phase: "sorting tabs", current: 0, total: totalTabs });
|
||||
@@ -98,7 +100,7 @@ export class TabsMutationCommands extends CommandGroup {
|
||||
const moveBatchSize = Math.max(1, Math.min(10, throttle.batchSize));
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
throwIfJobCancelled(__job);
|
||||
await chrome.tabs.move(sorted[i].id, { index: i });
|
||||
await api.tabs.move(sorted[i].id, { index: i });
|
||||
moved++;
|
||||
updateJobProgress(__job, { phase: "sorting tabs", current: moved, total: totalTabs });
|
||||
await yieldForLargeOperation(moved, moveBatchSize, throttle.pauseMs);
|
||||
@@ -108,13 +110,13 @@ export class TabsMutationCommands extends CommandGroup {
|
||||
});
|
||||
}
|
||||
|
||||
private windowHasAudibleTabs(window: chrome.windows.Window): boolean {
|
||||
private windowHasAudibleTabs(window: BrowserWindow): boolean {
|
||||
return Boolean(window.tabs?.some(tab => tab.audible && !tab.mutedInfo?.muted));
|
||||
}
|
||||
|
||||
private async tabsMergeWindows({ gentleMode, __job }: TabsMergeWindowsArgs = {}) {
|
||||
return runLargeOperation("tabs.merge_windows", async () => {
|
||||
const all = await chrome.windows.getAll({ populate: true });
|
||||
const all = await api.windows.getAll({ populate: true });
|
||||
const movableWindows = all.filter(w => !this.windowHasAudibleTabs(w));
|
||||
const target = movableWindows.find(w => w.focused) || movableWindows[0];
|
||||
if (!target) return { moved: 0, skippedAudibleWindows: all.length };
|
||||
@@ -127,7 +129,7 @@ export class TabsMutationCommands extends CommandGroup {
|
||||
const ids = w.tabs.map(t => t.id);
|
||||
const throttle = await getLargeOperationThrottle(ids.length, gentleMode);
|
||||
moved = await processInBatches(ids, throttle,
|
||||
batch => chrome.tabs.move(batch, { windowId: target.id, index: -1 }),
|
||||
batch => api.tabs.move(batch, { windowId: target.id, index: -1 }),
|
||||
{ job: __job, phase: "merging windows", total: totalTabs, baseCurrent: moved });
|
||||
}
|
||||
return { moved, skippedAudibleWindows: all.length - movableWindows.length };
|
||||
@@ -135,42 +137,42 @@ export class TabsMutationCommands extends CommandGroup {
|
||||
}
|
||||
|
||||
private async tabsPin({ tabId }: TabIdArgs) {
|
||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
||||
await chrome.tabs.update(tab.id, { pinned: true });
|
||||
const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab();
|
||||
await api.tabs.update(tab.id, { pinned: true });
|
||||
return { tabId: tab.id, pinned: true };
|
||||
}
|
||||
|
||||
private async tabsUnpin({ tabId }: TabIdArgs) {
|
||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
||||
await chrome.tabs.update(tab.id, { pinned: false });
|
||||
const tab = tabId ? await api.tabs.get(tabId) : await getActiveTab();
|
||||
await api.tabs.update(tab.id, { pinned: false });
|
||||
return { tabId: tab.id, pinned: false };
|
||||
}
|
||||
|
||||
private async tabsScreenshot({ tabId, format = "png", quality }: TabsScreenshotArgs = {}) {
|
||||
let windowId: number | undefined;
|
||||
if (tabId) {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
await chrome.tabs.update(tabId, { active: true });
|
||||
const tab = await api.tabs.get(tabId);
|
||||
await api.tabs.update(tabId, { active: true });
|
||||
windowId = tab.windowId;
|
||||
} else {
|
||||
const tab = await getActiveTab();
|
||||
windowId = tab.windowId;
|
||||
}
|
||||
const opts: chrome.extensionTypes.ImageDetails = { format: format as chrome.extensionTypes.ImageFormat };
|
||||
const opts: browser.extensionTypes.ImageDetails = { format: format as browser.extensionTypes.ImageFormat };
|
||||
if (format === "jpeg" && quality != null) opts.quality = quality;
|
||||
const dataUrl = await chrome.tabs.captureVisibleTab(windowId, opts);
|
||||
const dataUrl = await api.tabs.captureVisibleTab(windowId, opts);
|
||||
return { dataUrl, format };
|
||||
}
|
||||
|
||||
private async tabsMute({ tabId }: TabIdArgs) {
|
||||
const tab = await resolveTabForDirectAction(tabId, "mute");
|
||||
await chrome.tabs.update(tab.id, { muted: true });
|
||||
await api.tabs.update(tab.id, { muted: true });
|
||||
return { tabId: tab.id, muted: true };
|
||||
}
|
||||
|
||||
private async tabsUnmute({ tabId }: TabIdArgs) {
|
||||
const tab = await resolveTabForDirectAction(tabId, "unmute");
|
||||
await chrome.tabs.update(tab.id, { muted: false });
|
||||
await api.tabs.update(tab.id, { muted: false });
|
||||
return { tabId: tab.id, muted: false };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { webExtApi as api } from '../browser-api';
|
||||
import type { WindowCreateData } from '../types';
|
||||
import { getAliases } from '../core';
|
||||
import { CommandGroup } from '../classes/CommandGroup';
|
||||
import type { CommandEntry } from '../classes/CommandGroup';
|
||||
@@ -13,7 +15,7 @@ export class WindowsCommands extends CommandGroup {
|
||||
};
|
||||
|
||||
private async windowsList() {
|
||||
const windows = await chrome.windows.getAll({ populate: true });
|
||||
const windows = await api.windows.getAll({ populate: true });
|
||||
const aliases = await getAliases();
|
||||
return windows.map(w => ({
|
||||
id: w.id,
|
||||
@@ -27,19 +29,19 @@ export class WindowsCommands extends CommandGroup {
|
||||
private async windowsRename({ windowId, name }: WindowsRenameArgs) {
|
||||
const aliases = await getAliases();
|
||||
aliases[windowId] = name;
|
||||
await chrome.storage.local.set({ windowAliases: aliases });
|
||||
await api.storage.local.set({ windowAliases: aliases });
|
||||
return { windowId, name };
|
||||
}
|
||||
|
||||
private async windowsClose({ windowId }: WindowsCloseArgs) {
|
||||
await chrome.windows.remove(windowId);
|
||||
await api.windows.remove(windowId);
|
||||
return { windowId };
|
||||
}
|
||||
|
||||
private async windowsOpen({ url }: WindowsOpenArgs) {
|
||||
const createData: chrome.windows.CreateData = { focused: true };
|
||||
const createData: WindowCreateData = { focused: true };
|
||||
if (url) createData.url = url;
|
||||
const w = await chrome.windows.create(createData);
|
||||
const w = await api.windows.create(createData);
|
||||
return { id: w.id };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user