mirror of
https://github.com/p-stream/extension.git
synced 2026-04-21 04:42:25 +00:00
feat: Add Safari support and improve messaging system
This commit is contained in:
parent
1c1cc9cfdf
commit
b8a678d46c
16 changed files with 1659 additions and 931 deletions
77
SAFARI.md
Normal file
77
SAFARI.md
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
# 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",
|
"author": "P-Stream",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "plasmo dev",
|
"dev": "plasmo dev",
|
||||||
|
"dev:safari": "plasmo dev --target=safari-mv3 --no-hmr",
|
||||||
"build": "plasmo build",
|
"build": "plasmo build",
|
||||||
"build:firefox": "plasmo build --target=firefox-mv3",
|
"build:firefox": "plasmo build --target=firefox-mv3",
|
||||||
|
"build:safari": "plasmo build --target=safari-mv3",
|
||||||
"package": "plasmo package",
|
"package": "plasmo package",
|
||||||
"package:firefox": "plasmo package --target=firefox-mv3",
|
"package:firefox": "plasmo package --target=firefox-mv3",
|
||||||
|
"package:safari": "plasmo package --target=safari-mv3",
|
||||||
"lint": "eslint --ext .tsx,.ts src",
|
"lint": "eslint --ext .tsx,.ts src",
|
||||||
"lint:fix": "eslint --fix --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",
|
"lint:report": "eslint --ext .tsx,.ts --output-file eslint_report.json --format json src",
|
||||||
|
|
@ -18,7 +21,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@plasmohq/messaging": "^0.6.2",
|
"@plasmohq/messaging": "^0.6.2",
|
||||||
"@plasmohq/storage": "^1.11.0",
|
"@plasmohq/storage": "^1.11.0",
|
||||||
"plasmo": "0.89.4",
|
"plasmo": "0.90.5",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0"
|
"react-dom": "18.2.0"
|
||||||
},
|
},
|
||||||
|
|
@ -45,11 +48,15 @@
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"declarativeNetRequest",
|
"declarativeNetRequest",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"cookies"
|
"cookies",
|
||||||
|
"storage"
|
||||||
],
|
],
|
||||||
"optional_host_permissions": [
|
"optional_host_permissions": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
],
|
],
|
||||||
|
"host_permissions": [
|
||||||
|
"<all_urls>"
|
||||||
|
],
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"id": "{0c3fcdbd-5e0f-40d5-8f6c-d5eef8ff2b7c}"
|
"id": "{0c3fcdbd-5e0f-40d5-8f6c-d5eef8ff2b7c}"
|
||||||
|
|
|
||||||
1735
pnpm-lock.yaml
1735
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
6
pnpm-workspace.yaml
Normal file
6
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- '@swc/core'
|
||||||
|
- esbuild
|
||||||
|
- lmdb
|
||||||
|
- msgpackr-extract
|
||||||
|
- sharp
|
||||||
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,56 @@
|
||||||
import { isChrome } from '~utils/extension';
|
import '@plasmohq/messaging/background'
|
||||||
|
|
||||||
// Both brave and firefox for some reason need this extension reload,
|
import { getBrowserAPI, isChrome, isFirefox, isSafari } from '~utils/extension'
|
||||||
// If this isn't done, they will never load properly and will fail updateDynamicRules()
|
|
||||||
if (isChrome()) {
|
// 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')
|
||||||
|
|
||||||
|
// 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.onStartup.addListener(() => {
|
||||||
chrome.runtime.reload();
|
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.onStartup.addListener(() => {
|
||||||
browser.runtime.reload();
|
browser.runtime.reload()
|
||||||
});
|
})
|
||||||
|
|
||||||
|
browser.runtime.onInstalled.addListener(() => {
|
||||||
|
console.log('Firefox extension installed/updated')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,70 @@
|
||||||
import type { PlasmoMessaging } from '@plasmohq/messaging';
|
import type { PlasmoMessaging } from '@plasmohq/messaging'
|
||||||
|
|
||||||
import type { BaseRequest } from '~types/request';
|
import type { BaseRequest } from '~types/request'
|
||||||
import type { BaseResponse } from '~types/response';
|
import type { BaseResponse } from '~types/response'
|
||||||
import { removeDynamicRules, setDynamicRules } from '~utils/declarativeNetRequest';
|
import {
|
||||||
import { isFirefox } from '~utils/extension';
|
removeDynamicRules,
|
||||||
import { makeFullUrl } from '~utils/fetcher';
|
setDynamicRules
|
||||||
import { assertDomainWhitelist, canAccessCookies } from '~utils/storage';
|
} from '~utils/declarativeNetRequest'
|
||||||
|
import { getBrowserAPI, isFirefox, isSafari } 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 {
|
export interface Request extends BaseRequest {
|
||||||
baseUrl?: string;
|
baseUrl?: string
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>
|
||||||
method?: string;
|
method?: string
|
||||||
query?: Record<string, string>;
|
query?: Record<string, string>
|
||||||
readHeaders?: Record<string, string>;
|
readHeaders?: Record<string, string>
|
||||||
url: string;
|
url: string
|
||||||
body?: any;
|
body?: any
|
||||||
bodyType?: 'string' | 'FormData' | 'URLSearchParams' | 'object';
|
bodyType?: 'string' | 'FormData' | 'URLSearchParams' | 'object'
|
||||||
}
|
}
|
||||||
|
|
||||||
type Response<T> = BaseResponse<{
|
type Response<T> = BaseResponse<{
|
||||||
response: {
|
response: {
|
||||||
statusCode: number;
|
statusCode: number
|
||||||
headers: Record<string, string>;
|
headers: Record<string, string>
|
||||||
finalUrl: string;
|
finalUrl: string
|
||||||
body: T;
|
body: T
|
||||||
};
|
}
|
||||||
}>;
|
}>
|
||||||
|
|
||||||
const mapBodyToFetchBody = (body: Request['body'], bodyType: Request['bodyType']): BodyInit => {
|
const mapBodyToFetchBody = (
|
||||||
|
body: Request['body'],
|
||||||
|
bodyType: Request['bodyType']
|
||||||
|
): BodyInit => {
|
||||||
if (bodyType === 'FormData') {
|
if (bodyType === 'FormData') {
|
||||||
const formData = new FormData();
|
const formData = new FormData()
|
||||||
body.forEach(([key, value]: [any, any]) => {
|
body.forEach(([key, value]: [any, any]) => {
|
||||||
formData.append(key, value.toString());
|
formData.append(key, value.toString())
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
if (bodyType === 'URLSearchParams') {
|
if (bodyType === 'URLSearchParams') {
|
||||||
return new URLSearchParams(body);
|
return new URLSearchParams(body)
|
||||||
}
|
}
|
||||||
if (bodyType === 'object') {
|
if (bodyType === 'object') {
|
||||||
return JSON.stringify(body);
|
return JSON.stringify(body)
|
||||||
}
|
}
|
||||||
if (bodyType === 'string') {
|
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 {
|
try {
|
||||||
if (!req.sender?.tab?.url) throw new Error('No tab URL found in the request.');
|
if (!req.sender?.tab?.url)
|
||||||
if (!req.body) throw new Error('No request body found in the request.');
|
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);
|
const url = makeFullUrl(req.body.url, req.body)
|
||||||
await assertDomainWhitelist(req.sender.tab.url);
|
await assertDomainWhitelist(req.sender.tab.url)
|
||||||
|
|
||||||
await setDynamicRules({
|
await setDynamicRules({
|
||||||
ruleId: MAKE_REQUEST_DYNAMIC_RULE,
|
ruleId: MAKE_REQUEST_DYNAMIC_RULE,
|
||||||
|
|
@ -63,26 +73,28 @@ const handler: PlasmoMessaging.MessageHandler<Request, Response<any>> = async (r
|
||||||
// set Access-Control-Allow-Credentials if the reqested host has access to cookies
|
// set Access-Control-Allow-Credentials if the reqested host has access to cookies
|
||||||
responseHeaders: {
|
responseHeaders: {
|
||||||
...(canAccessCookies(new URL(url).hostname) && {
|
...(canAccessCookies(new URL(url).hostname) && {
|
||||||
'Access-Control-Allow-Credentials': 'true',
|
'Access-Control-Allow-Credentials': 'true'
|
||||||
}),
|
})
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: req.body.method,
|
method: req.body.method,
|
||||||
headers: req.body.headers,
|
headers: req.body.headers,
|
||||||
body: mapBodyToFetchBody(req.body.body, req.body.bodyType),
|
body: mapBodyToFetchBody(req.body.body, req.body.bodyType)
|
||||||
});
|
})
|
||||||
await removeDynamicRules([MAKE_REQUEST_DYNAMIC_RULE]);
|
await removeDynamicRules([MAKE_REQUEST_DYNAMIC_RULE])
|
||||||
const contentType = response.headers.get('content-type');
|
const contentType = response.headers.get('content-type')
|
||||||
const body = contentType?.includes('application/json') ? await response.json() : await response.text();
|
const body = contentType?.includes('application/json')
|
||||||
|
? await response.json()
|
||||||
|
: await response.text()
|
||||||
|
|
||||||
const cookies = await (chrome || browser).cookies.getAll({
|
const cookies = await (chrome || browser).cookies.getAll({
|
||||||
url: response.url,
|
url: response.url,
|
||||||
...(isFirefox() && {
|
...(isFirefox() && {
|
||||||
firstPartyDomain: new URL(response.url).hostname,
|
firstPartyDomain: new URL(response.url).hostname
|
||||||
}),
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -92,20 +104,22 @@ const handler: PlasmoMessaging.MessageHandler<Request, Response<any>> = async (r
|
||||||
...Object.fromEntries(response.headers.entries()),
|
...Object.fromEntries(response.headers.entries()),
|
||||||
// include cookies if allowed for the reqested host
|
// include cookies if allowed for the reqested host
|
||||||
...(canAccessCookies(new URL(url).hostname) && {
|
...(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,
|
body,
|
||||||
finalUrl: response.url,
|
finalUrl: response.url
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('failed request', err);
|
console.error('failed request', err)
|
||||||
res.send({
|
res.send({
|
||||||
success: false,
|
success: false,
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: err instanceof Error ? err.message : String(err)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default handler;
|
export default handler
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type { PlasmoMessaging } from '@plasmohq/messaging';
|
import type { PlasmoMessaging } from '@plasmohq/messaging'
|
||||||
|
|
||||||
import type { BaseRequest } from '~types/request';
|
import type { BaseRequest } from '~types/request'
|
||||||
import type { BaseResponse } from '~types/response';
|
import type { BaseResponse } from '~types/response'
|
||||||
import { isChrome } from '~utils/extension';
|
import { getBrowserAPI } from '~utils/extension'
|
||||||
|
|
||||||
type Request = BaseRequest & {
|
type Request = BaseRequest & {
|
||||||
page: string;
|
page: string;
|
||||||
|
|
@ -14,17 +14,16 @@ 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.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.');
|
if (!req.body) throw new Error('No body found in the request.');
|
||||||
|
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams()
|
||||||
searchParams.set('redirectUrl', req.body.redirectUrl);
|
searchParams.set('redirectUrl', req.body.redirectUrl)
|
||||||
const url = (chrome || browser).runtime.getURL(`/tabs/${req.body.page}.html?${searchParams.toString()}`);
|
|
||||||
|
|
||||||
if (isChrome()) {
|
const browserAPI = getBrowserAPI()
|
||||||
await chrome.tabs.update(req.sender.tab.id, {
|
const url = browserAPI.runtime.getURL(
|
||||||
url,
|
`/tabs/${req.body.page}.html?${searchParams.toString()}`
|
||||||
});
|
)
|
||||||
} else {
|
await (browserAPI.tabs as any).update(req.sender.tab.id, {
|
||||||
await browser.tabs.update(req.sender.tab.id, { url });
|
url
|
||||||
}
|
});
|
||||||
|
|
||||||
res.send({
|
res.send({
|
||||||
success: true,
|
success: true,
|
||||||
|
|
@ -35,6 +34,6 @@ const handler: PlasmoMessaging.MessageHandler<Request, BaseResponse> = async (re
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: err instanceof Error ? err.message : String(err),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default handler;
|
export default handler;
|
||||||
|
|
|
||||||
101
src/components/SafariPermissionGuide.tsx
Normal file
101
src/components/SafariPermissionGuide.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
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 '../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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,49 @@
|
||||||
import { relayMessage } from '@plasmohq/messaging';
|
import { relayMessage } from '@plasmohq/messaging'
|
||||||
import type { PlasmoCSConfig } from 'plasmo';
|
import type { PlasmoCSConfig } from 'plasmo'
|
||||||
|
|
||||||
export const config: PlasmoCSConfig = {
|
export const config: PlasmoCSConfig = {
|
||||||
matches: ['<all_urls>'],
|
matches: ['<all_urls>']
|
||||||
};
|
}
|
||||||
|
|
||||||
relayMessage({
|
// Safari requires a delay before setting up messaging
|
||||||
name: 'hello',
|
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({
|
const setupMessaging = () => {
|
||||||
name: 'makeRequest',
|
try {
|
||||||
});
|
relayMessage({
|
||||||
|
name: 'hello'
|
||||||
|
})
|
||||||
|
|
||||||
relayMessage({
|
relayMessage({
|
||||||
name: 'prepareStream',
|
name: 'makeRequest'
|
||||||
});
|
})
|
||||||
|
|
||||||
relayMessage({
|
relayMessage({
|
||||||
name: 'openPage',
|
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()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,81 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { useDomainWhitelist } from './useDomainWhitelist';
|
import { getBrowserAPI, isSafari } from '~utils/extension'
|
||||||
|
import { useDomainWhitelist } from './useDomainWhitelist'
|
||||||
|
|
||||||
export async function hasPermission() {
|
export async function hasPermission () {
|
||||||
return chrome.permissions.contains({
|
const browserAPI = getBrowserAPI()
|
||||||
origins: ['<all_urls>'],
|
|
||||||
});
|
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() {
|
export function usePermission () {
|
||||||
const { addDomain } = useDomainWhitelist();
|
const { addDomain } = useDomainWhitelist()
|
||||||
const [permission, setPermission] = useState(false);
|
const [permission, setPermission] = useState(false)
|
||||||
|
|
||||||
const grantPermission = useCallback(async (domain?: string) => {
|
const grantPermission = useCallback(
|
||||||
const granted = await chrome.permissions.request({
|
async (domain?: string) => {
|
||||||
origins: ['<all_urls>'],
|
const browserAPI = getBrowserAPI()
|
||||||
});
|
|
||||||
setPermission(granted);
|
if (isSafari()) {
|
||||||
if (granted && domain) addDomain(domain);
|
const hasPerms = await hasPermission()
|
||||||
return granted;
|
if (!hasPerms) {
|
||||||
}, []);
|
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(() => {
|
useEffect(() => {
|
||||||
hasPermission().then((has) => setPermission(has));
|
hasPermission().then(has => setPermission(has))
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasPermission: permission,
|
hasPermission: permission,
|
||||||
grantPermission,
|
grantPermission
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ import { useCallback } from 'react';
|
||||||
|
|
||||||
import { Button } from '~components/Button';
|
import { Button } from '~components/Button';
|
||||||
import { Icon } from '~components/Icon';
|
import { Icon } from '~components/Icon';
|
||||||
|
import SafariPermissionGuide from '~components/SafariPermissionGuide';
|
||||||
import { usePermission } from '~hooks/usePermission';
|
import { usePermission } from '~hooks/usePermission';
|
||||||
|
import { isSafari } from '~utils/extension';
|
||||||
|
|
||||||
import './PermissionRequest.css';
|
import './PermissionRequest.css';
|
||||||
|
|
||||||
|
|
@ -25,6 +27,10 @@ export default function PermissionRequest() {
|
||||||
grantPermission().then(() => window.close());
|
grantPermission().then(() => window.close());
|
||||||
}, [grantPermission]);
|
}, [grantPermission]);
|
||||||
|
|
||||||
|
if (isSafari()) {
|
||||||
|
return <SafariPermissionGuide />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container permission-request">
|
<div className="container permission-request">
|
||||||
<div className="inner-container">
|
<div className="inner-container">
|
||||||
|
|
|
||||||
|
|
@ -1,119 +1,134 @@
|
||||||
import { isChrome } from './extension';
|
import { getBrowserAPI, isChrome, isSafari } from './extension'
|
||||||
import { modifiableResponseHeaders } from './storage';
|
import { modifiableResponseHeaders } from './storage'
|
||||||
|
|
||||||
interface DynamicRule {
|
interface DynamicRule {
|
||||||
ruleId: number;
|
ruleId: number
|
||||||
targetDomains?: [string, ...string[]];
|
targetDomains?: [string, ...string[]]
|
||||||
targetRegex?: string;
|
targetRegex?: string
|
||||||
requestHeaders?: Record<string, string>;
|
requestHeaders?: Record<string, string>
|
||||||
responseHeaders?: Record<string, string>;
|
responseHeaders?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapHeadersToDeclarativeNetRequestHeaders = (
|
const mapHeadersToDeclarativeNetRequestHeaders = (
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
op: string,
|
op: string
|
||||||
): { header: string; operation: any; value: string }[] => {
|
): { header: string; operation: any; value: string }[] => {
|
||||||
return Object.entries(headers).map(([name, value]) => ({
|
return Object.entries(headers).map(([name, value]) => ({
|
||||||
header: name,
|
header: name,
|
||||||
operation: op,
|
operation: op,
|
||||||
value,
|
value
|
||||||
}));
|
}))
|
||||||
};
|
}
|
||||||
|
|
||||||
export const setDynamicRules = async (body: DynamicRule) => {
|
export const setDynamicRules = async (body: DynamicRule) => {
|
||||||
if (isChrome()) {
|
const browserAPI = getBrowserAPI()
|
||||||
await chrome.declarativeNetRequest.updateDynamicRules({
|
|
||||||
|
if (isChrome() || isSafari()) {
|
||||||
|
await (browserAPI.declarativeNetRequest as any).updateDynamicRules({
|
||||||
removeRuleIds: [body.ruleId],
|
removeRuleIds: [body.ruleId],
|
||||||
addRules: [
|
addRules: [
|
||||||
{
|
{
|
||||||
id: body.ruleId,
|
id: body.ruleId,
|
||||||
condition: {
|
condition: {
|
||||||
...(body.targetDomains && { requestDomains: body.targetDomains }),
|
...(body.targetDomains && { requestDomains: body.targetDomains }),
|
||||||
...(body.targetRegex && { regexFilter: body.targetRegex }),
|
...(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({
|
|
||||||
removeRuleIds: [body.ruleId],
|
|
||||||
addRules: [
|
|
||||||
{
|
|
||||||
id: body.ruleId,
|
|
||||||
condition: {
|
|
||||||
...(body.targetDomains && { requestDomains: body.targetDomains }),
|
|
||||||
...(body.targetRegex && { regexFilter: body.targetRegex }),
|
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
type: 'modifyHeaders',
|
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: [
|
responseHeaders: [
|
||||||
{
|
{
|
||||||
header: 'Access-Control-Allow-Origin',
|
header: 'Access-Control-Allow-Origin',
|
||||||
operation: 'set',
|
operation: 'set',
|
||||||
value: '*',
|
value: '*'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Access-Control-Allow-Methods',
|
header: 'Access-Control-Allow-Methods',
|
||||||
operation: 'set',
|
operation: 'set',
|
||||||
value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
value: 'GET, POST, PUT, DELETE, PATCH, OPTIONS'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Access-Control-Allow-Headers',
|
header: 'Access-Control-Allow-Headers',
|
||||||
operation: 'set',
|
operation: 'set',
|
||||||
value: '*',
|
value: '*'
|
||||||
},
|
},
|
||||||
...mapHeadersToDeclarativeNetRequestHeaders(body.responseHeaders ?? {}, 'set'),
|
...mapHeadersToDeclarativeNetRequestHeaders(
|
||||||
],
|
body.responseHeaders ?? {},
|
||||||
|
'set'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
} 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 &&
|
||||||
if (browser.runtime.lastError?.message) throw new Error(browser.runtime.lastError.message);
|
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[]) => {
|
export const removeDynamicRules = async (ruleIds: number[]) => {
|
||||||
await (chrome || browser).declarativeNetRequest.updateDynamicRules({
|
const browserAPI = getBrowserAPI()
|
||||||
removeRuleIds: ruleIds,
|
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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,32 @@
|
||||||
export const isChrome = () => {
|
export const isChrome = () => {
|
||||||
return chrome.runtime.getURL('').startsWith('chrome-extension://');
|
return chrome.runtime.getURL('').startsWith('chrome-extension://')
|
||||||
};
|
}
|
||||||
|
|
||||||
export const isFirefox = () => {
|
export const isFirefox = () => {
|
||||||
try {
|
try {
|
||||||
return browser.runtime.getURL('').startsWith('moz-extension://');
|
return browser.runtime.getURL('').startsWith('moz-extension://')
|
||||||
} catch {
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBrowserAPI = () => {
|
||||||
|
if (isSafari()) {
|
||||||
|
return typeof browser !== 'undefined' ? browser : chrome
|
||||||
|
}
|
||||||
|
if (isFirefox()) {
|
||||||
|
return browser
|
||||||
|
}
|
||||||
|
return chrome
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,26 @@
|
||||||
import { isChrome } from './extension';
|
import { getBrowserAPI, isChrome, isSafari } from './extension'
|
||||||
|
|
||||||
export function queryCurrentDomain(cb: (domain: string | null) => void) {
|
export function queryCurrentDomain (cb: (domain: string | null) => void) {
|
||||||
const handle = (tabUrl: string | undefined) => {
|
const handle = (tabUrl: string | undefined) => {
|
||||||
if (!tabUrl) cb(null);
|
if (!tabUrl) cb(null)
|
||||||
else cb(tabUrl);
|
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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 stopListenToTabChanges(cb: () => void) {
|
export function listenToTabChanges (cb: () => void) {
|
||||||
if (isChrome()) {
|
const browserAPI = getBrowserAPI()
|
||||||
chrome.tabs.onActivated.removeListener(cb);
|
;(browserAPI.tabs as any).onActivated.addListener(cb)
|
||||||
chrome.tabs.onUpdated.removeListener(cb);
|
;(browserAPI.tabs as any).onUpdated.addListener(cb)
|
||||||
} else if (browser) {
|
}
|
||||||
browser.tabs.onActivated.removeListener(cb);
|
|
||||||
browser.tabs.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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue