mirror of
https://github.com/p-stream/extension.git
synced 2026-07-27 00:42:09 +00:00
Merge remote-tracking branch 'public/main' into safari
This commit is contained in:
commit
62e205c5e4
14 changed files with 453 additions and 114 deletions
83
SAFARI.md
Normal file
83
SAFARI.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Safari Extension Development Guide
|
||||
|
||||
## Building for Safari
|
||||
|
||||
To build the extension specifically for Safari:
|
||||
|
||||
```bash
|
||||
pnpm build:safari
|
||||
```
|
||||
|
||||
## Development for Safari
|
||||
|
||||
For Safari development (without hot-reload to avoid WebSocket issues):
|
||||
|
||||
```bash
|
||||
pnpm dev:safari
|
||||
```
|
||||
|
||||
## Safari Installation & Testing
|
||||
|
||||
### 1. Load Extension in Safari
|
||||
|
||||
1. Build the extension: `pnpm build:safari`
|
||||
2. Open Safari and go to Safari → Preferences → Advanced
|
||||
3. Check "Show Develop menu in menu bar"
|
||||
4. Go to Develop → Allow Unsigned Extensions (for development)
|
||||
5. Go to Safari → Preferences → Extensions
|
||||
6. Click the "+" button and select the `build/safari-mv3-prod` folder
|
||||
|
||||
### 2. Grant Permissions
|
||||
|
||||
Safari requires manual permission setup:
|
||||
|
||||
1. **Enable Extension**: Safari → Preferences → Extensions → Enable "P-Stream extension"
|
||||
2. **Website Access**: Safari → Preferences → Websites → P-Stream extension → Set to "Allow on all websites"
|
||||
3. **Reload Pages**: Refresh any streaming website where you want to use the extension
|
||||
|
||||
### 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**:
|
||||
|
||||
- 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
|
||||
|
||||
### 4. Safari-Specific Behavior
|
||||
|
||||
- **No Runtime Permission Requests**: Safari doesn't support `chrome.permissions.request()` - users must enable manually
|
||||
- **Different Timing**: Safari background scripts load differently - we added delays and retry logic
|
||||
- **Security Policies**: Safari blocks insecure content more aggressively than Chrome
|
||||
- **Manifest Differences**: Safari requires `host_permissions` for proper website access
|
||||
|
||||
### 5. Testing Checklist
|
||||
|
||||
- [ ] Extension loads in Safari Extensions preferences
|
||||
- [ ] Website permissions are granted in Safari Preferences → Websites
|
||||
- [ ] No "Invalid call to runtime.connect()" errors
|
||||
- [ ] No WebSocket connection warnings
|
||||
- [ ] Permission guide shows correctly when Safari is detected
|
||||
- [ ] All message handlers work properly
|
||||
|
||||
### 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
|
||||
4. The built extension in `build/safari-mv3-prod` can be embedded in the macOS app
|
||||
12
manifest.safari.ts
Normal file
12
manifest.safari.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const manifest = {
|
||||
host_permissions: ['<all_urls>'],
|
||||
permissions: ['storage', 'declarativeNetRequest', 'activeTab', 'cookies'],
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ['assets/active.png', 'assets/inactive.png'],
|
||||
matches: ['<all_urls>']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default manifest
|
||||
11
package.json
11
package.json
|
|
@ -6,10 +6,13 @@
|
|||
"author": "P-Stream",
|
||||
"scripts": {
|
||||
"dev": "plasmo dev",
|
||||
"dev:safari": "plasmo dev --target=safari-mv3 --no-hmr",
|
||||
"build": "plasmo build",
|
||||
"build:firefox": "plasmo build --target=firefox-mv3",
|
||||
"build:safari": "plasmo build --target=safari-mv3",
|
||||
"package": "plasmo package",
|
||||
"package:firefox": "plasmo package --target=firefox-mv3",
|
||||
"package:safari": "plasmo package --target=safari-mv3",
|
||||
"lint": "eslint --ext .tsx,.ts src",
|
||||
"lint:fix": "eslint --fix --ext .tsx,.ts src",
|
||||
"lint:report": "eslint --ext .tsx,.ts --output-file eslint_report.json --format json src",
|
||||
|
|
@ -18,7 +21,7 @@
|
|||
"dependencies": {
|
||||
"@plasmohq/messaging": "^0.6.2",
|
||||
"@plasmohq/storage": "^1.11.0",
|
||||
"plasmo": "0.89.4",
|
||||
"plasmo": "0.90.5",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"sharp": "^0.34.3"
|
||||
|
|
@ -46,11 +49,15 @@
|
|||
"permissions": [
|
||||
"declarativeNetRequest",
|
||||
"activeTab",
|
||||
"cookies"
|
||||
"cookies",
|
||||
"storage"
|
||||
],
|
||||
"optional_host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "{de055456-589b-45fe-8342-c685a7ffb424}"
|
||||
|
|
|
|||
24
safari.config.json
Normal file
24
safari.config.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "P-Stream Safari Extension",
|
||||
"displayName": "P-Stream Safari Extension",
|
||||
"description": "Enhance your streaming experience with just one click - Safari version",
|
||||
"author": "P-Stream",
|
||||
"version": "1.3.2",
|
||||
"safari": {
|
||||
"bundleIdentifier": "com.pstream.safari.extension",
|
||||
"minimumVersion": "14.0",
|
||||
"appName": "P-Stream Extension",
|
||||
"developerIdentifier": "YOUR_TEAM_ID"
|
||||
},
|
||||
"permissions": {
|
||||
"storage": true,
|
||||
"declarativeNetRequest": true,
|
||||
"activeTab": true,
|
||||
"cookies": true,
|
||||
"hostPermissions": ["<all_urls>"]
|
||||
},
|
||||
"background": {
|
||||
"persistent": false,
|
||||
"type": "module"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,47 @@
|
|||
import { isChrome } from '~utils/extension';
|
||||
import '@plasmohq/messaging/background';
|
||||
import { getBrowserAPI, isChrome, isFirefox, isSafari } from '~utils/extension';
|
||||
|
||||
// Both brave and firefox for some reason need this extension reload,
|
||||
// If this isn't done, they will never load properly and will fail updateDynamicRules()
|
||||
if (isChrome()) {
|
||||
const browserAPI = getBrowserAPI();
|
||||
|
||||
if (isSafari()) {
|
||||
// Safari doesn't need the reload behavior that Chrome/Firefox require
|
||||
console.log('Running on Safari');
|
||||
|
||||
// Ensure messaging is ready for Safari
|
||||
browserAPI.runtime.onInstalled.addListener(() => {
|
||||
console.log('Safari extension installed/updated');
|
||||
// Give Safari time to initialize messaging
|
||||
setTimeout(() => {
|
||||
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);
|
||||
port.onDisconnect.addListener(() => {
|
||||
if (browserAPI.runtime.lastError) {
|
||||
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();
|
||||
});
|
||||
} else {
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
console.log('Chrome extension installed/updated');
|
||||
});
|
||||
} else if (isFirefox()) {
|
||||
// Firefox behavior
|
||||
browser.runtime.onStartup.addListener(() => {
|
||||
browser.runtime.reload();
|
||||
});
|
||||
|
||||
browser.runtime.onInstalled.addListener(() => {
|
||||
console.log('Firefox extension installed/updated');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { PlasmoMessaging } from '@plasmohq/messaging';
|
|||
|
||||
import type { BaseRequest } from '~types/request';
|
||||
import type { BaseResponse } from '~types/response';
|
||||
import { isChrome } from '~utils/extension';
|
||||
import { getBrowserAPI } from '~utils/extension';
|
||||
|
||||
type Request = BaseRequest & {
|
||||
page: string;
|
||||
|
|
@ -16,15 +16,12 @@ const handler: PlasmoMessaging.MessageHandler<Request, BaseResponse> = async (re
|
|||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('redirectUrl', req.body.redirectUrl);
|
||||
const url = (chrome || browser).runtime.getURL(`/tabs/${req.body.page}.html?${searchParams.toString()}`);
|
||||
|
||||
if (isChrome()) {
|
||||
await chrome.tabs.update(req.sender.tab.id, {
|
||||
url,
|
||||
});
|
||||
} else {
|
||||
await browser.tabs.update(req.sender.tab.id, { url });
|
||||
}
|
||||
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,
|
||||
});
|
||||
|
||||
res.send({
|
||||
success: true,
|
||||
|
|
|
|||
89
src/components/SafariPermissionGuide.tsx
Normal file
89
src/components/SafariPermissionGuide.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import { Button } from '~components/Button';
|
||||
import { Icon } from '~components/Icon';
|
||||
import { usePermission } from '~hooks/usePermission';
|
||||
import { isSafari } from '~utils/extension';
|
||||
|
||||
import '../tabs/PermissionRequest.css';
|
||||
|
||||
function Card(props: { purple?: boolean; children: React.ReactNode; icon?: React.ReactNode; right?: React.ReactNode }) {
|
||||
return (
|
||||
<div className={['card', props.purple ? 'purple' : ''].join(' ')}>
|
||||
<div>
|
||||
<div className="icon-circle">{props.icon}</div>
|
||||
</div>
|
||||
<div>{props.children}</div>
|
||||
{props.right ? <div className="center-y">{props.right}</div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SafariPermissionGuide() {
|
||||
const { grantPermission } = usePermission();
|
||||
|
||||
const handleSafariSetup = useCallback(() => {
|
||||
if (isSafari()) {
|
||||
grantPermission().then(() => {
|
||||
// Check if permissions were granted
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
}, [grantPermission]);
|
||||
|
||||
if (!isSafari()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container permission-request">
|
||||
<div className="inner-container">
|
||||
<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>
|
||||
<div>
|
||||
<h3 className="card-title">Step 1: Enable Extension</h3>
|
||||
<p className="card-description">
|
||||
Open Safari → Preferences → Extensions, then enable "P-Stream extension"
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="button-spacing">
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,18 +5,44 @@ export const config: PlasmoCSConfig = {
|
|||
matches: ['<all_urls>'],
|
||||
};
|
||||
|
||||
relayMessage({
|
||||
name: 'hello',
|
||||
});
|
||||
// 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://'))
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
relayMessage({
|
||||
name: 'makeRequest',
|
||||
});
|
||||
const setupMessaging = () => {
|
||||
try {
|
||||
relayMessage({
|
||||
name: 'hello',
|
||||
});
|
||||
|
||||
relayMessage({
|
||||
name: 'prepareStream',
|
||||
});
|
||||
relayMessage({
|
||||
name: 'makeRequest',
|
||||
});
|
||||
|
||||
relayMessage({
|
||||
name: 'openPage',
|
||||
});
|
||||
relayMessage({
|
||||
name: 'prepareStream',
|
||||
});
|
||||
|
||||
relayMessage({
|
||||
name: 'openPage',
|
||||
});
|
||||
} catch (error) {
|
||||
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);
|
||||
} else {
|
||||
setupMessaging();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,25 +1,76 @@
|
|||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { getBrowserAPI, isSafari } from '~utils/extension';
|
||||
|
||||
import { useDomainWhitelist } from './useDomainWhitelist';
|
||||
|
||||
export async function hasPermission() {
|
||||
return chrome.permissions.contains({
|
||||
origins: ['<all_urls>'],
|
||||
});
|
||||
const browserAPI = getBrowserAPI();
|
||||
|
||||
if (isSafari()) {
|
||||
try {
|
||||
const tabs = await (browserAPI.tabs as any).query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
return tabs && tabs.length > 0;
|
||||
} catch (error) {
|
||||
console.log('Safari permission check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await (browserAPI.permissions as any).contains({
|
||||
origins: ['<all_urls>'],
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Permission check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function usePermission() {
|
||||
const { addDomain } = useDomainWhitelist();
|
||||
const [permission, setPermission] = useState(false);
|
||||
|
||||
const grantPermission = useCallback(async (domain?: string) => {
|
||||
const granted = await chrome.permissions.request({
|
||||
origins: ['<all_urls>'],
|
||||
});
|
||||
setPermission(granted);
|
||||
if (granted && domain) addDomain(domain);
|
||||
return granted;
|
||||
}, []);
|
||||
const grantPermission = useCallback(
|
||||
async (domain?: string) => {
|
||||
const browserAPI = getBrowserAPI();
|
||||
|
||||
if (isSafari()) {
|
||||
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;
|
||||
}
|
||||
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;
|
||||
} catch (error) {
|
||||
console.log('Permission request failed:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[addDomain],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
hasPermission().then((has) => setPermission(has));
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ import { useCallback } from 'react';
|
|||
|
||||
import { Button } from '~components/Button';
|
||||
import { Icon } from '~components/Icon';
|
||||
import SafariPermissionGuide from '~components/SafariPermissionGuide';
|
||||
import { usePermission } from '~hooks/usePermission';
|
||||
import { isSafari } from '~utils/extension';
|
||||
|
||||
import './PermissionRequest.css';
|
||||
|
||||
|
|
@ -25,6 +27,10 @@ export default function PermissionRequest() {
|
|||
grantPermission().then(() => window.close());
|
||||
}, [grantPermission]);
|
||||
|
||||
if (isSafari()) {
|
||||
return <SafariPermissionGuide />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container permission-request">
|
||||
<div className="inner-container">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { isChrome } from './extension';
|
||||
import { modifiableResponseHeaders } from './storage';
|
||||
import { getBrowserAPI, isChrome, isSafari } from './extension';
|
||||
|
||||
interface DynamicRule {
|
||||
ruleId: number;
|
||||
|
|
@ -21,54 +20,10 @@ const mapHeadersToDeclarativeNetRequestHeaders = (
|
|||
};
|
||||
|
||||
export const setDynamicRules = async (body: DynamicRule) => {
|
||||
if (isChrome()) {
|
||||
await chrome.declarativeNetRequest.updateDynamicRules({
|
||||
removeRuleIds: [body.ruleId],
|
||||
addRules: [
|
||||
{
|
||||
id: body.ruleId,
|
||||
condition: {
|
||||
...(body.targetDomains && { requestDomains: body.targetDomains }),
|
||||
...(body.targetRegex && { regexFilter: body.targetRegex }),
|
||||
},
|
||||
action: {
|
||||
type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS,
|
||||
...(body.requestHeaders && Object.keys(body.requestHeaders).length > 0
|
||||
? {
|
||||
requestHeaders: mapHeadersToDeclarativeNetRequestHeaders(
|
||||
body.requestHeaders,
|
||||
chrome.declarativeNetRequest.HeaderOperation.SET,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
responseHeaders: [
|
||||
{
|
||||
header: 'Access-Control-Allow-Origin',
|
||||
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
|
||||
value: '*',
|
||||
},
|
||||
{
|
||||
header: 'Access-Control-Allow-Methods',
|
||||
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
|
||||
value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||
},
|
||||
{
|
||||
header: 'Access-Control-Allow-Headers',
|
||||
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
|
||||
value: '*',
|
||||
},
|
||||
...mapHeadersToDeclarativeNetRequestHeaders(
|
||||
body.responseHeaders ?? {},
|
||||
chrome.declarativeNetRequest.HeaderOperation.SET,
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
if (chrome.runtime.lastError?.message) throw new Error(chrome.runtime.lastError.message);
|
||||
} else {
|
||||
await browser.declarativeNetRequest.updateDynamicRules({
|
||||
const browserAPI = getBrowserAPI();
|
||||
|
||||
if (isChrome() || isSafari()) {
|
||||
await (browserAPI.declarativeNetRequest as any).updateDynamicRules({
|
||||
removeRuleIds: [body.ruleId],
|
||||
addRules: [
|
||||
{
|
||||
|
|
@ -106,14 +61,59 @@ export const setDynamicRules = async (body: DynamicRule) => {
|
|||
},
|
||||
],
|
||||
});
|
||||
if (browser.runtime.lastError?.message) throw new Error(browser.runtime.lastError.message);
|
||||
} else {
|
||||
// Firefox
|
||||
await (browserAPI.declarativeNetRequest as any).updateDynamicRules({
|
||||
removeRuleIds: [body.ruleId],
|
||||
addRules: [
|
||||
{
|
||||
id: body.ruleId,
|
||||
condition: {
|
||||
...(body.targetDomains && { requestDomains: body.targetDomains }),
|
||||
...(body.targetRegex && { regexFilter: body.targetRegex }),
|
||||
},
|
||||
action: {
|
||||
type: 'modifyHeaders',
|
||||
...(body.requestHeaders && Object.keys(body.requestHeaders).length > 0
|
||||
? {
|
||||
requestHeaders: mapHeadersToDeclarativeNetRequestHeaders(body.requestHeaders, 'set'),
|
||||
}
|
||||
: {}),
|
||||
responseHeaders: [
|
||||
{
|
||||
header: 'Access-Control-Allow-Origin',
|
||||
operation: 'set',
|
||||
value: '*',
|
||||
},
|
||||
{
|
||||
header: 'Access-Control-Allow-Methods',
|
||||
operation: 'set',
|
||||
value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
||||
},
|
||||
{
|
||||
header: 'Access-Control-Allow-Headers',
|
||||
operation: 'set',
|
||||
value: '*',
|
||||
},
|
||||
...mapHeadersToDeclarativeNetRequestHeaders(body.responseHeaders ?? {}, 'set'),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (browserAPI.runtime.lastError?.message) {
|
||||
throw new Error(browserAPI.runtime.lastError.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDynamicRules = async (ruleIds: number[]) => {
|
||||
await (chrome || browser).declarativeNetRequest.updateDynamicRules({
|
||||
const browserAPI = getBrowserAPI();
|
||||
await (browserAPI.declarativeNetRequest as any).updateDynamicRules({
|
||||
removeRuleIds: ruleIds,
|
||||
});
|
||||
if ((chrome || browser).runtime.lastError?.message)
|
||||
throw new Error((chrome || browser).runtime.lastError?.message ?? 'Unknown error');
|
||||
if (browserAPI.runtime.lastError?.message) {
|
||||
throw new Error(browserAPI.runtime.lastError?.message ?? 'Unknown error');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,3 +9,24 @@ export const isFirefox = () => {
|
|||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isSafari = () => {
|
||||
try {
|
||||
return (
|
||||
chrome.runtime.getURL('').startsWith('safari-web-extension://') ||
|
||||
browser.runtime.getURL('').startsWith('safari-web-extension://')
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getBrowserAPI = () => {
|
||||
if (isSafari()) {
|
||||
return typeof browser !== 'undefined' ? browser : chrome;
|
||||
}
|
||||
if (isFirefox()) {
|
||||
return browser;
|
||||
}
|
||||
return chrome;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { isChrome } from './extension';
|
||||
import { getBrowserAPI } from './extension';
|
||||
|
||||
export function queryCurrentDomain(cb: (domain: string | null) => void) {
|
||||
const handle = (tabUrl: string | undefined) => {
|
||||
|
|
@ -6,27 +6,18 @@ export function queryCurrentDomain(cb: (domain: string | null) => void) {
|
|||
else cb(tabUrl);
|
||||
};
|
||||
const ops = { active: true, currentWindow: true } as const;
|
||||
|
||||
if (isChrome()) chrome.tabs.query(ops).then((tabs) => handle(tabs[0]?.url));
|
||||
else browser.tabs.query(ops).then((tabs) => handle(tabs[0]?.url));
|
||||
const browserAPI = getBrowserAPI();
|
||||
(browserAPI.tabs as any).query(ops).then((tabs: any[]) => handle(tabs[0]?.url));
|
||||
}
|
||||
|
||||
export function listenToTabChanges(cb: () => void) {
|
||||
if (isChrome()) {
|
||||
chrome.tabs.onActivated.addListener(cb);
|
||||
chrome.tabs.onUpdated.addListener(cb);
|
||||
} else if (browser) {
|
||||
browser.tabs.onActivated.addListener(cb);
|
||||
browser.tabs.onUpdated.addListener(cb);
|
||||
}
|
||||
const browserAPI = getBrowserAPI();
|
||||
(browserAPI.tabs as any).onActivated.addListener(cb);
|
||||
(browserAPI.tabs as any).onUpdated.addListener(cb);
|
||||
}
|
||||
|
||||
export function stopListenToTabChanges(cb: () => void) {
|
||||
if (isChrome()) {
|
||||
chrome.tabs.onActivated.removeListener(cb);
|
||||
chrome.tabs.onUpdated.removeListener(cb);
|
||||
} else if (browser) {
|
||||
browser.tabs.onActivated.removeListener(cb);
|
||||
browser.tabs.onUpdated.removeListener(cb);
|
||||
}
|
||||
const browserAPI = getBrowserAPI();
|
||||
(browserAPI.tabs as any).onActivated.removeListener(cb);
|
||||
(browserAPI.tabs as any).onUpdated.removeListener(cb);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue