fix: lint check

This commit is contained in:
TrinityHades 2025-08-22 14:57:38 -07:00
parent b8a678d46c
commit 4772efdd83
11 changed files with 240 additions and 290 deletions

View file

@ -38,16 +38,21 @@ Safari requires manual permission setup:
### 3. Common Safari Issues & Fixes
#### Issue: "Invalid call to runtime.connect()"
**Fix**: This occurs when the background script isn't ready. The extension now includes:
- Delayed messaging setup for Safari
- Retry logic for failed connections
- Better error handling
#### Issue: WebSocket Connection Blocked
**Fix**: Use `pnpm dev:safari` which disables hot-reload WebSockets that Safari blocks.
#### Issue: Permissions Not Working
**Fix**:
**Fix**:
- Ensure both `host_permissions` and `optional_host_permissions` are in manifest
- Guide users through Safari's manual permission setup
- Check permissions through Safari Preferences, not runtime API
@ -71,6 +76,7 @@ Safari requires manual permission setup:
### 6. Production Deployment
For Safari App Store distribution:
1. You'll need to create a native macOS app wrapper
2. Use Xcode to create the Safari extension project
3. Follow Apple's Safari extension guidelines

View file

@ -1,56 +1,47 @@
import '@plasmohq/messaging/background'
import '@plasmohq/messaging/background';
import { getBrowserAPI, isChrome, isFirefox, isSafari } from '~utils/extension';
import { getBrowserAPI, isChrome, isFirefox, isSafari } from '~utils/extension'
const browserAPI = getBrowserAPI();
// Initialize Plasmo messaging system
// This ensures that the messaging handlers are properly set up
console.log('Background script loaded')
const browserAPI = getBrowserAPI()
// Safari has different startup behavior, so we need to handle it differently
if (isSafari()) {
// Safari doesn't need the reload behavior that Chrome/Firefox require
console.log('Running on Safari')
console.log('Running on Safari');
// Ensure messaging is ready for Safari
browserAPI.runtime.onInstalled.addListener(() => {
console.log('Safari extension installed/updated')
console.log('Safari extension installed/updated');
// Give Safari time to initialize messaging
setTimeout(() => {
console.log('Safari messaging should be ready')
}, 1000)
})
console.log('Safari messaging should be ready');
}, 1000);
});
// Handle Safari runtime errors more gracefully
browserAPI.runtime.onConnect.addListener(port => {
console.log('Safari port connected:', port.name)
browserAPI.runtime.onConnect.addListener((port) => {
console.log('Safari port connected:', port.name);
port.onDisconnect.addListener(() => {
if (browserAPI.runtime.lastError) {
console.log(
'Safari port disconnected with error:',
browserAPI.runtime.lastError.message
)
console.log('Safari port disconnected with error:', browserAPI.runtime.lastError.message);
}
})
})
});
});
} else if (isChrome()) {
// Both brave and chrome for some reason need this extension reload,
// If this isn't done, they will never load properly and will fail updateDynamicRules()
chrome.runtime.onStartup.addListener(() => {
chrome.runtime.reload()
})
chrome.runtime.reload();
});
chrome.runtime.onInstalled.addListener(() => {
console.log('Chrome extension installed/updated')
})
console.log('Chrome extension installed/updated');
});
} else if (isFirefox()) {
// Firefox behavior
browser.runtime.onStartup.addListener(() => {
browser.runtime.reload()
})
browser.runtime.reload();
});
browser.runtime.onInstalled.addListener(() => {
console.log('Firefox extension installed/updated')
})
console.log('Firefox extension installed/updated');
});
}

View file

@ -1,70 +1,60 @@
import type { PlasmoMessaging } from '@plasmohq/messaging'
import type { PlasmoMessaging } from '@plasmohq/messaging';
import type { BaseRequest } from '~types/request'
import type { BaseResponse } from '~types/response'
import {
removeDynamicRules,
setDynamicRules
} from '~utils/declarativeNetRequest'
import { getBrowserAPI, isFirefox, isSafari } from '~utils/extension'
import { makeFullUrl } from '~utils/fetcher'
import { assertDomainWhitelist, canAccessCookies } from '~utils/storage'
import type { BaseRequest } from '~types/request';
import type { BaseResponse } from '~types/response';
import { removeDynamicRules, setDynamicRules } from '~utils/declarativeNetRequest';
import { isFirefox } from '~utils/extension';
import { makeFullUrl } from '~utils/fetcher';
import { assertDomainWhitelist, canAccessCookies } from '~utils/storage';
const MAKE_REQUEST_DYNAMIC_RULE = 23498
const MAKE_REQUEST_DYNAMIC_RULE = 23498;
export interface Request extends BaseRequest {
baseUrl?: string
headers?: Record<string, string>
method?: string
query?: Record<string, string>
readHeaders?: Record<string, string>
url: string
body?: any
bodyType?: 'string' | 'FormData' | 'URLSearchParams' | 'object'
baseUrl?: string;
headers?: Record<string, string>;
method?: string;
query?: Record<string, string>;
readHeaders?: Record<string, string>;
url: string;
body?: any;
bodyType?: 'string' | 'FormData' | 'URLSearchParams' | 'object';
}
type Response<T> = BaseResponse<{
response: {
statusCode: number
headers: Record<string, string>
finalUrl: string
body: T
}
}>
statusCode: number;
headers: Record<string, string>;
finalUrl: string;
body: T;
};
}>;
const mapBodyToFetchBody = (
body: Request['body'],
bodyType: Request['bodyType']
): BodyInit => {
const mapBodyToFetchBody = (body: Request['body'], bodyType: Request['bodyType']): BodyInit => {
if (bodyType === 'FormData') {
const formData = new FormData()
const formData = new FormData();
body.forEach(([key, value]: [any, any]) => {
formData.append(key, value.toString())
})
formData.append(key, value.toString());
});
}
if (bodyType === 'URLSearchParams') {
return new URLSearchParams(body)
return new URLSearchParams(body);
}
if (bodyType === 'object') {
return JSON.stringify(body)
return JSON.stringify(body);
}
if (bodyType === 'string') {
return body
return body;
}
return body
}
return body;
};
const handler: PlasmoMessaging.MessageHandler<Request, Response<any>> = async (
req,
res
) => {
const handler: PlasmoMessaging.MessageHandler<Request, Response<any>> = async (req, res) => {
try {
if (!req.sender?.tab?.url)
throw new Error('No tab URL found in the request.')
if (!req.body) throw new Error('No request body found in the request.')
if (!req.sender?.tab?.url) throw new Error('No tab URL found in the request.');
if (!req.body) throw new Error('No request body found in the request.');
const url = makeFullUrl(req.body.url, req.body)
await assertDomainWhitelist(req.sender.tab.url)
const url = makeFullUrl(req.body.url, req.body);
await assertDomainWhitelist(req.sender.tab.url);
await setDynamicRules({
ruleId: MAKE_REQUEST_DYNAMIC_RULE,
@ -73,28 +63,26 @@ const handler: PlasmoMessaging.MessageHandler<Request, Response<any>> = async (
// set Access-Control-Allow-Credentials if the reqested host has access to cookies
responseHeaders: {
...(canAccessCookies(new URL(url).hostname) && {
'Access-Control-Allow-Credentials': 'true'
})
}
})
'Access-Control-Allow-Credentials': 'true',
}),
},
});
const response = await fetch(url, {
method: req.body.method,
headers: req.body.headers,
body: mapBodyToFetchBody(req.body.body, req.body.bodyType)
})
await removeDynamicRules([MAKE_REQUEST_DYNAMIC_RULE])
const contentType = response.headers.get('content-type')
const body = contentType?.includes('application/json')
? await response.json()
: await response.text()
body: mapBodyToFetchBody(req.body.body, req.body.bodyType),
});
await removeDynamicRules([MAKE_REQUEST_DYNAMIC_RULE]);
const contentType = response.headers.get('content-type');
const body = contentType?.includes('application/json') ? await response.json() : await response.text();
const cookies = await (chrome || browser).cookies.getAll({
url: response.url,
...(isFirefox() && {
firstPartyDomain: new URL(response.url).hostname
})
})
firstPartyDomain: new URL(response.url).hostname,
}),
});
res.send({
success: true,
@ -104,22 +92,20 @@ const handler: PlasmoMessaging.MessageHandler<Request, Response<any>> = async (
...Object.fromEntries(response.headers.entries()),
// include cookies if allowed for the reqested host
...(canAccessCookies(new URL(url).hostname) && {
'Set-Cookie': cookies
.map(cookie => `${cookie.name}=${cookie.value}`)
.join(', ')
})
'Set-Cookie': cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join(', '),
}),
},
body,
finalUrl: response.url
}
})
finalUrl: response.url,
},
});
} catch (err) {
console.error('failed request', err)
console.error('failed request', err);
res.send({
success: false,
error: err instanceof Error ? err.message : String(err)
})
error: err instanceof Error ? err.message : String(err),
});
}
}
};
export default handler
export default handler;

View file

@ -1,8 +1,8 @@
import type { PlasmoMessaging } from '@plasmohq/messaging'
import type { PlasmoMessaging } from '@plasmohq/messaging';
import type { BaseRequest } from '~types/request'
import type { BaseResponse } from '~types/response'
import { getBrowserAPI } from '~utils/extension'
import type { BaseRequest } from '~types/request';
import type { BaseResponse } from '~types/response';
import { getBrowserAPI } from '~utils/extension';
type Request = BaseRequest & {
page: string;
@ -14,15 +14,13 @@ const handler: PlasmoMessaging.MessageHandler<Request, BaseResponse> = async (re
if (!req.sender?.tab?.id) throw new Error('No tab ID found in the request.');
if (!req.body) throw new Error('No body found in the request.');
const searchParams = new URLSearchParams()
searchParams.set('redirectUrl', req.body.redirectUrl)
const searchParams = new URLSearchParams();
searchParams.set('redirectUrl', req.body.redirectUrl);
const browserAPI = getBrowserAPI()
const url = browserAPI.runtime.getURL(
`/tabs/${req.body.page}.html?${searchParams.toString()}`
)
const browserAPI = getBrowserAPI();
const url = browserAPI.runtime.getURL(`/tabs/${req.body.page}.html?${searchParams.toString()}`);
await (browserAPI.tabs as any).update(req.sender.tab.id, {
url
url,
});
res.send({
@ -34,6 +32,6 @@ const handler: PlasmoMessaging.MessageHandler<Request, BaseResponse> = async (re
error: err instanceof Error ? err.message : String(err),
});
}
}
};
export default handler;

View file

@ -2,8 +2,8 @@ import { useCallback } from 'react';
import { Button } from '~components/Button';
import { Icon } from '~components/Icon';
import { isSafari } from '~utils/extension';
import { usePermission } from '~hooks/usePermission';
import { isSafari } from '~utils/extension';
import '../tabs/PermissionRequest.css';
@ -40,59 +40,47 @@ export default function SafariPermissionGuide() {
return (
<div className="container permission-request">
<div className="inner-container">
<h1 className="color-white">
Safari Setup Required
</h1>
<h1 className="color-white">Safari Setup Required</h1>
<p className="text-color paragraph">
Safari extensions require manual setup. Please follow these steps to enable the P-Stream extension:
</p>
<div className="card-list" style={{ marginTop: '2.5rem' }}>
<Card
icon={<Icon name="shield" />}
purple
>
<Card icon={<Icon name="shield" />} purple>
<div>
<h3 className="card-title">Step 1: Enable Extension</h3>
<p className="card-description">
Open Safari Preferences Extensions, then enable "P-Stream extension"
Open Safari Preferences Extensions, then enable &quot;P-Stream extension&quot;
</p>
</div>
</Card>
<Card
icon={<Icon name="network" />}
>
<Card icon={<Icon name="network" />}>
<div>
<h3 className="card-title">Step 2: Grant Website Access</h3>
<p className="card-description">
Go to Safari Preferences Websites P-Stream extension Set to "Allow on all websites" or add specific sites
Go to Safari Preferences Websites P-Stream extension Set to &quot;Allow on all websites&quot; or
add specific sites
</p>
</div>
</Card>
<Card
icon={<Icon name="power" />}
>
<Card icon={<Icon name="power" />}>
<div>
<h3 className="card-title">Step 3: Reload Pages</h3>
<p className="card-description">
Reload any streaming website pages where you want to use the extension
</p>
<p className="card-description">Reload any streaming website pages where you want to use the extension</p>
</div>
</Card>
</div>
<div className="button-spacing">
<Button onClick={handleSafariSetup}>
Test Extension Setup
</Button>
<Button onClick={handleSafariSetup}>Test Extension Setup</Button>
</div>
<div style={{ marginTop: '1.5rem' }}>
<p className="text-color paragraph" style={{ fontSize: '0.9rem' }}>
<strong>Note:</strong> Safari handles extension permissions differently than Chrome.
These steps are required for the extension to work properly with streaming websites.
<strong>Note:</strong> Safari handles extension permissions differently than Chrome. These steps are
required for the extension to work properly with streaming websites.
</p>
</div>
</div>

View file

@ -1,49 +1,48 @@
import { relayMessage } from '@plasmohq/messaging'
import type { PlasmoCSConfig } from 'plasmo'
import { relayMessage } from '@plasmohq/messaging';
import type { PlasmoCSConfig } from 'plasmo';
export const config: PlasmoCSConfig = {
matches: ['<all_urls>']
}
matches: ['<all_urls>'],
};
// Safari requires a delay before setting up messaging
const isSafari = () => {
try {
return (
chrome.runtime.getURL('').startsWith('safari-web-extension://') ||
(typeof browser !== 'undefined' &&
browser.runtime.getURL('').startsWith('safari-web-extension://'))
)
(typeof browser !== 'undefined' && browser.runtime.getURL('').startsWith('safari-web-extension://'))
);
} catch {
return false
return false;
}
}
};
const setupMessaging = () => {
try {
relayMessage({
name: 'hello'
})
name: 'hello',
});
relayMessage({
name: 'makeRequest'
})
name: 'makeRequest',
});
relayMessage({
name: 'prepareStream'
})
name: 'prepareStream',
});
relayMessage({
name: 'openPage'
})
name: 'openPage',
});
} catch (error) {
console.log('Failed to setup messaging, retrying...', error)
setTimeout(setupMessaging, 1000)
console.log('Failed to setup messaging, retrying...', error);
setTimeout(setupMessaging, 1000);
}
}
};
// Safari needs a delay to ensure background script is ready
if (isSafari()) {
setTimeout(setupMessaging, 500)
setTimeout(setupMessaging, 500);
} else {
setupMessaging()
setupMessaging();
}

View file

@ -27,9 +27,7 @@ export function useToggleWhitelistDomain(domain: string | null) {
const { domainWhitelist, addDomain, removeDomain } = useDomainWhitelist();
const isWhitelisted = domainWhitelist.includes(domain ?? '');
const { grantPermission } = usePermission();
const iconPath = (chrome || browser).runtime.getURL(
isWhitelisted ? 'assets/active.png' : 'assets/inactive.png',
);
const iconPath = (chrome || browser).runtime.getURL(isWhitelisted ? 'assets/active.png' : 'assets/inactive.png');
(chrome || browser).action.setIcon({
path: iconPath,

View file

@ -1,81 +1,83 @@
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react';
import { getBrowserAPI, isSafari } from '~utils/extension'
import { useDomainWhitelist } from './useDomainWhitelist'
import { getBrowserAPI, isSafari } from '~utils/extension';
export async function hasPermission () {
const browserAPI = getBrowserAPI()
import { useDomainWhitelist } from './useDomainWhitelist';
export async function hasPermission() {
const browserAPI = getBrowserAPI();
if (isSafari()) {
try {
const tabs = await (browserAPI.tabs as any).query({
active: true,
currentWindow: true
})
return tabs && tabs.length > 0
currentWindow: true,
});
return tabs && tabs.length > 0;
} catch (error) {
console.log('Safari permission check failed:', error)
return false
console.log('Safari permission check failed:', error);
return false;
}
}
try {
return await (browserAPI.permissions as any).contains({
origins: ['<all_urls>']
})
origins: ['<all_urls>'],
});
} catch (error) {
console.log('Permission check failed:', error)
return false
console.log('Permission check failed:', error);
return false;
}
}
export function usePermission () {
const { addDomain } = useDomainWhitelist()
const [permission, setPermission] = useState(false)
export function usePermission() {
const { addDomain } = useDomainWhitelist();
const [permission, setPermission] = useState(false);
const grantPermission = useCallback(
async (domain?: string) => {
const browserAPI = getBrowserAPI()
const browserAPI = getBrowserAPI();
if (isSafari()) {
const hasPerms = await hasPermission()
const hasPerms = await hasPermission();
if (!hasPerms) {
// eslint-disable-next-line no-alert
alert(
'To use this extension, please:\n\n' +
'1. Open Safari Preferences\n' +
'2. Go to the Websites tab\n' +
'3. Find "P-Stream extension" in the left sidebar\n' +
'4. Set it to "Allow" for the websites you want to use\n\n' +
'You may also need to enable the extension in Safari > Preferences > Extensions'
)
return false
'You may also need to enable the extension in Safari > Preferences > Extensions',
);
return false;
}
setPermission(true)
if (domain) addDomain(domain)
return true
setPermission(true);
if (domain) addDomain(domain);
return true;
}
try {
const granted = await (browserAPI.permissions as any).request({
origins: ['<all_urls>']
})
setPermission(granted)
if (granted && domain) addDomain(domain)
return granted
origins: ['<all_urls>'],
});
setPermission(granted);
if (granted && domain) addDomain(domain);
return granted;
} catch (error) {
console.log('Permission request failed:', error)
return false
console.log('Permission request failed:', error);
return false;
}
},
[addDomain]
)
[addDomain],
);
useEffect(() => {
hasPermission().then(has => setPermission(has))
}, [])
hasPermission().then((has) => setPermission(has));
}, []);
return {
hasPermission: permission,
grantPermission
}
grantPermission,
};
}

View file

@ -1,27 +1,26 @@
import { getBrowserAPI, isChrome, isSafari } from './extension'
import { modifiableResponseHeaders } from './storage'
import { getBrowserAPI, isChrome, isSafari } from './extension';
interface DynamicRule {
ruleId: number
targetDomains?: [string, ...string[]]
targetRegex?: string
requestHeaders?: Record<string, string>
responseHeaders?: Record<string, string>
ruleId: number;
targetDomains?: [string, ...string[]];
targetRegex?: string;
requestHeaders?: Record<string, string>;
responseHeaders?: Record<string, string>;
}
const mapHeadersToDeclarativeNetRequestHeaders = (
headers: Record<string, string>,
op: string
op: string,
): { header: string; operation: any; value: string }[] => {
return Object.entries(headers).map(([name, value]) => ({
header: name,
operation: op,
value
}))
}
value,
}));
};
export const setDynamicRules = async (body: DynamicRule) => {
const browserAPI = getBrowserAPI()
const browserAPI = getBrowserAPI();
if (isChrome() || isSafari()) {
await (browserAPI.declarativeNetRequest as any).updateDynamicRules({
@ -31,44 +30,37 @@ export const setDynamicRules = async (body: DynamicRule) => {
id: body.ruleId,
condition: {
...(body.targetDomains && { requestDomains: body.targetDomains }),
...(body.targetRegex && { regexFilter: body.targetRegex })
...(body.targetRegex && { regexFilter: body.targetRegex }),
},
action: {
type: 'modifyHeaders',
...(body.requestHeaders &&
Object.keys(body.requestHeaders).length > 0
...(body.requestHeaders && Object.keys(body.requestHeaders).length > 0
? {
requestHeaders: mapHeadersToDeclarativeNetRequestHeaders(
body.requestHeaders,
'set'
)
requestHeaders: mapHeadersToDeclarativeNetRequestHeaders(body.requestHeaders, 'set'),
}
: {}),
responseHeaders: [
{
header: 'Access-Control-Allow-Origin',
operation: 'set',
value: '*'
value: '*',
},
{
header: 'Access-Control-Allow-Methods',
operation: 'set',
value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS'
value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
},
{
header: 'Access-Control-Allow-Headers',
operation: 'set',
value: '*'
value: '*',
},
...mapHeadersToDeclarativeNetRequestHeaders(
body.responseHeaders ?? {},
'set'
)
]
}
}
]
})
...mapHeadersToDeclarativeNetRequestHeaders(body.responseHeaders ?? {}, 'set'),
],
},
},
],
});
} else {
// Firefox
await (browserAPI.declarativeNetRequest as any).updateDynamicRules({
@ -78,57 +70,50 @@ export const setDynamicRules = async (body: DynamicRule) => {
id: body.ruleId,
condition: {
...(body.targetDomains && { requestDomains: body.targetDomains }),
...(body.targetRegex && { regexFilter: body.targetRegex })
...(body.targetRegex && { regexFilter: body.targetRegex }),
},
action: {
type: 'modifyHeaders',
...(body.requestHeaders &&
Object.keys(body.requestHeaders).length > 0
...(body.requestHeaders && Object.keys(body.requestHeaders).length > 0
? {
requestHeaders: mapHeadersToDeclarativeNetRequestHeaders(
body.requestHeaders,
'set'
)
requestHeaders: mapHeadersToDeclarativeNetRequestHeaders(body.requestHeaders, 'set'),
}
: {}),
responseHeaders: [
{
header: 'Access-Control-Allow-Origin',
operation: 'set',
value: '*'
value: '*',
},
{
header: 'Access-Control-Allow-Methods',
operation: 'set',
value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS'
value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
},
{
header: 'Access-Control-Allow-Headers',
operation: 'set',
value: '*'
value: '*',
},
...mapHeadersToDeclarativeNetRequestHeaders(
body.responseHeaders ?? {},
'set'
)
]
}
}
]
})
...mapHeadersToDeclarativeNetRequestHeaders(body.responseHeaders ?? {}, 'set'),
],
},
},
],
});
}
if (browserAPI.runtime.lastError?.message) {
throw new Error(browserAPI.runtime.lastError.message)
throw new Error(browserAPI.runtime.lastError.message);
}
}
};
export const removeDynamicRules = async (ruleIds: number[]) => {
const browserAPI = getBrowserAPI()
const browserAPI = getBrowserAPI();
await (browserAPI.declarativeNetRequest as any).updateDynamicRules({
removeRuleIds: ruleIds
})
removeRuleIds: ruleIds,
});
if (browserAPI.runtime.lastError?.message) {
throw new Error(browserAPI.runtime.lastError?.message ?? 'Unknown error')
throw new Error(browserAPI.runtime.lastError?.message ?? 'Unknown error');
}
}
};

View file

@ -1,32 +1,32 @@
export const isChrome = () => {
return chrome.runtime.getURL('').startsWith('chrome-extension://')
}
return chrome.runtime.getURL('').startsWith('chrome-extension://');
};
export const isFirefox = () => {
try {
return browser.runtime.getURL('').startsWith('moz-extension://')
return browser.runtime.getURL('').startsWith('moz-extension://');
} catch {
return false
return false;
}
}
};
export const isSafari = () => {
try {
return (
chrome.runtime.getURL('').startsWith('safari-web-extension://') ||
browser.runtime.getURL('').startsWith('safari-web-extension://')
)
);
} catch {
return false
return false;
}
}
};
export const getBrowserAPI = () => {
if (isSafari()) {
return typeof browser !== 'undefined' ? browser : chrome
return typeof browser !== 'undefined' ? browser : chrome;
}
if (isFirefox()) {
return browser
return browser;
}
return chrome
}
return chrome;
};

View file

@ -1,26 +1,23 @@
import { getBrowserAPI, isChrome, isSafari } from './extension'
import { getBrowserAPI } from './extension';
export function queryCurrentDomain (cb: (domain: string | null) => void) {
export function queryCurrentDomain(cb: (domain: string | null) => void) {
const handle = (tabUrl: string | undefined) => {
if (!tabUrl) cb(null)
else cb(tabUrl)
}
const ops = { active: true, currentWindow: true } as const
const browserAPI = getBrowserAPI()
;(browserAPI.tabs as any)
.query(ops)
.then((tabs: any[]) => handle(tabs[0]?.url))
if (!tabUrl) cb(null);
else cb(tabUrl);
};
const ops = { active: true, currentWindow: true } as const;
const browserAPI = getBrowserAPI();
(browserAPI.tabs as any).query(ops).then((tabs: any[]) => handle(tabs[0]?.url));
}
export function listenToTabChanges (cb: () => void) {
const browserAPI = getBrowserAPI()
;(browserAPI.tabs as any).onActivated.addListener(cb)
;(browserAPI.tabs as any).onUpdated.addListener(cb)
export function listenToTabChanges(cb: () => void) {
const browserAPI = getBrowserAPI();
(browserAPI.tabs as any).onActivated.addListener(cb);
(browserAPI.tabs as any).onUpdated.addListener(cb);
}
export function stopListenToTabChanges (cb: () => void) {
const browserAPI = getBrowserAPI()
;(browserAPI.tabs as any).onActivated.removeListener(cb)
;(browserAPI.tabs as any).onUpdated.removeListener(cb)
export function stopListenToTabChanges(cb: () => void) {
const browserAPI = getBrowserAPI();
(browserAPI.tabs as any).onActivated.removeListener(cb);
(browserAPI.tabs as any).onUpdated.removeListener(cb);
}