mirror of
https://github.com/bytedream/stream-bypass.git
synced 2026-08-05 05:20:41 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e22e92f89 | ||
|
|
f7cfe41211 | ||
|
|
d7e1cbfb48 | ||
|
|
3541c7d2f0 | ||
|
|
df3c66a24c | ||
|
|
b929f0956d | ||
|
|
4c9825ebd5 | ||
|
|
71593f6762 | ||
|
|
ddcb13013a | ||
|
|
8cf14fb17a | ||
|
|
3e3e2f5e75 | ||
|
|
c5deca1b2c | ||
|
|
c4fea538f0 | ||
|
|
0552df008d | ||
|
|
e437d5436a | ||
|
|
c09cf1188d | ||
|
|
eef560f224 | ||
|
|
9857ba6416 | ||
|
|
c08e071c88 | ||
|
|
dbec92375c | ||
|
|
a1328e8908 | ||
|
|
0455b53992 | ||
|
|
c9f5b594ff | ||
|
|
9639f148f9 | ||
|
|
85f4a3b776 | ||
|
|
30e079d665 | ||
|
|
e499adb4ac | ||
|
|
aa6827b059 | ||
|
|
5a86625f3f | ||
|
|
a44f71f056 |
+14
-10
@@ -10,31 +10,35 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Setup node
|
- name: Setup pnpm
|
||||||
uses: actions/setup-node@v6
|
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
version: 11
|
||||||
cache: 'npm'
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
|
with:
|
||||||
|
node-version: 26
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build Firefox (mv2)
|
- name: Build Firefox (mv2)
|
||||||
run: npm run build:firefox
|
run: pnpm run build:firefox
|
||||||
|
|
||||||
- name: Build Chrome (mv3)
|
- name: Build Chrome (mv3)
|
||||||
run: npm run build
|
run: pnpm run build
|
||||||
|
|
||||||
- name: Upload Firefox (mv2)
|
- name: Upload Firefox (mv2)
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: stream-bypass-mv2
|
name: stream-bypass-mv2
|
||||||
path: .output/firefox-mv2
|
path: .output/firefox-mv2
|
||||||
|
|
||||||
- name: Upload Chrome (mv3)
|
- name: Upload Chrome (mv3)
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: stream-bypass-mv3
|
name: stream-bypass-mv3
|
||||||
path: .output/chrome-mv3
|
path: .output/chrome-mv3
|
||||||
|
|||||||
@@ -13,16 +13,20 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Setup node
|
- name: Setup pnpm
|
||||||
uses: actions/setup-node@v6
|
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
version: 11
|
||||||
cache: 'npm'
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
|
with:
|
||||||
|
node-version: 26
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: npm run lint
|
run: pnpm run lint
|
||||||
|
|||||||
@@ -19,21 +19,25 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Setup node
|
- name: Setup pnpm
|
||||||
uses: actions/setup-node@v6
|
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
version: 11
|
||||||
cache: 'npm'
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
|
with:
|
||||||
|
node-version: 26
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Zip extensions
|
- name: Zip extensions
|
||||||
run: |
|
run: |
|
||||||
npm run zip:firefox
|
pnpm run zip:firefox
|
||||||
npm run zip
|
pnpm run zip
|
||||||
|
|
||||||
- name: Resolve zip extension paths
|
- name: Resolve zip extension paths
|
||||||
id: paths
|
id: paths
|
||||||
@@ -48,20 +52,20 @@ jobs:
|
|||||||
echo "mv3_name=$MV3_NAME" >> "$GITHUB_OUTPUT"
|
echo "mv3_name=$MV3_NAME" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Upload mv2 as artifact
|
- name: Upload mv2 as artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.paths.outputs.mv2_name }}
|
name: ${{ steps.paths.outputs.mv2_name }}
|
||||||
path: ${{ steps.paths.outputs.mv2_path }}
|
path: ${{ steps.paths.outputs.mv2_path }}
|
||||||
|
|
||||||
- name: Upload mv3 as artifact
|
- name: Upload mv3 as artifact
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.paths.outputs.mv3_name }}
|
name: ${{ steps.paths.outputs.mv3_name }}
|
||||||
path: ${{ steps.paths.outputs.mv3_path }}
|
path: ${{ steps.paths.outputs.mv3_path }}
|
||||||
|
|
||||||
- name: Upload to latest release
|
- name: Upload to latest release
|
||||||
if: github.event.inputs.dry_run != 'true'
|
if: github.event.inputs.dry_run != 'true'
|
||||||
uses: svenstaro/upload-release-action@v2
|
uses: svenstaro/upload-release-action@ac75e1407b33421f1368f08e0e6cc567cc4edd25 # 2.11.5
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
file: .output/*mv*.zip
|
file: .output/*mv*.zip
|
||||||
@@ -80,6 +84,6 @@ jobs:
|
|||||||
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
|
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||||
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
npx wxt submit \
|
pnpm exec wxt submit \
|
||||||
--firefox-zip .output/*-mv2.zip --firefox-sources-zip .output/*-sources.zip \
|
--firefox-zip .output/*-mv2.zip --firefox-sources-zip .output/*-sources.zip \
|
||||||
--chrome-zip .output/*-mv3.zip
|
--chrome-zip .output/*-mv3.zip
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
.output
|
.output
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
.zed
|
.zed
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
pnpm-lock.yaml
|
||||||
.output
|
.output
|
||||||
.wxt
|
.wxt
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ The best way to install the extension are the official browser extension stores:
|
|||||||
- ⚠: Works with limitations.
|
- ⚠: Works with limitations.
|
||||||
- ✖: Not supported.
|
- ✖: Not supported.
|
||||||
|
|
||||||
| Site | Firefox & Firefox for Android (mv2) | Chrome & Chromium based (mv2) |
|
| Site | Firefox & Firefox for Android (mv2) | Chrome & Chromium based (mv3) |
|
||||||
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
| --------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||||
| [dropload.io](https://dropload.io) | ✔ | ✔ |
|
| [dropload.io](https://dropload.io) | ✔ | ✔ |
|
||||||
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔ | ⚠ (redirect probably required) |
|
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔ | ⚠ (redirect probably required) |
|
||||||
@@ -118,20 +118,20 @@ If you want to build the addon from source and not using the way described in [i
|
|||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
- `npm` installed.
|
- `pnpm` installed.
|
||||||
- A copy of this repository and a shell / console open in the copied directory.
|
- A copy of this repository and a shell / console open in the copied directory.
|
||||||
|
|
||||||
If the requirements are satisfied, you can continue with the following commands:
|
If the requirements are satisfied, you can continue with the following commands:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# install all dependencies
|
# install all dependencies
|
||||||
$ npm i
|
$ pnpm i
|
||||||
|
|
||||||
# build the extension and start it in a new firefox instance
|
# build the extension and start it in a new firefox instance
|
||||||
$ npm run dev:firefox
|
$ pnpm run dev:firefox
|
||||||
|
|
||||||
# build the extension with optimizations to the .output/firefox-mv2 directory
|
# build the extension with optimizations to the .output/firefox-mv2 directory
|
||||||
$ npm run build:firefox
|
$ pnpm run build:firefox
|
||||||
```
|
```
|
||||||
|
|
||||||
You can omit the `:firefox` suffix, then it's built for Chrome.
|
You can omit the `:firefox` suffix, then it's built for Chrome.
|
||||||
@@ -148,11 +148,15 @@ When using firefox, use the following:
|
|||||||
|
|
||||||
## ⚙️ Settings
|
## ⚙️ Settings
|
||||||
|
|
||||||
> You reach the settings by pressing the tree dots (⋮) in the top right corner of the extension popup.
|
> You reach the settings by pressing the settings icon in the top right corner of the extension popup.
|
||||||
|
|
||||||
### Hosts
|
### Hosts
|
||||||
|
|
||||||
You can enable or disabled for which hosts the extension should redirect.
|
You can enable or disable for which hosts the extension should work.
|
||||||
|
|
||||||
|
### Per-site disables
|
||||||
|
|
||||||
|
Herer you can disable specific hosts (or all of them) for the current site only - useful when a site uses multiple hosts and you only want to bypass a few, or when a host misbehaves on a particular site.
|
||||||
|
|
||||||
### ff2mpv
|
### ff2mpv
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export default defineConfig(
|
|||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'no-alert': 'error',
|
||||||
|
'no-console': 'error',
|
||||||
'no-undef': 'off'
|
'no-undef': 'off'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
-8452
File diff suppressed because it is too large
Load Diff
+23
-23
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stream-bypass",
|
"name": "stream-bypass",
|
||||||
"version": "4.0.1",
|
"version": "5.0.0",
|
||||||
"displayName": "Stream Bypass",
|
"displayName": "Stream Bypass",
|
||||||
"author": "bytedream",
|
"author": "bytedream",
|
||||||
"description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video",
|
"description": "Multi-browser addon for multiple streaming providers which redirects directly to the source video",
|
||||||
@@ -20,35 +20,35 @@
|
|||||||
"build:firefox": "wxt build -b firefox",
|
"build:firefox": "wxt build -b firefox",
|
||||||
"zip": "wxt zip",
|
"zip": "wxt zip",
|
||||||
"zip:firefox": "wxt zip -b firefox",
|
"zip:firefox": "wxt zip -b firefox",
|
||||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
|
||||||
"postinstall": "wxt prepare",
|
"postinstall": "wxt prepare",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"lint": "prettier --check . && eslint ."
|
"lint": "prettier --check . && eslint . && svelte-check --tsconfig ./tsconfig.json"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^2.0.1",
|
"@eslint/compat": "^2.1.0",
|
||||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
"@eslint/js": "^10.0.1",
|
||||||
|
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
||||||
"@steeze-ui/heroicons": "^2.4.2",
|
"@steeze-ui/heroicons": "^2.4.2",
|
||||||
"@steeze-ui/svelte-icon": "^1.6.2",
|
"@steeze-ui/svelte-icon": "^1.6.2",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"@tsconfig/svelte": "^5.0.6",
|
"@tsconfig/svelte": "^5.0.8",
|
||||||
"@wxt-dev/module-svelte": "^2.0.4",
|
"@wxt-dev/module-svelte": "^2.0.5",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^10.6.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-svelte": "^3.14.0",
|
"eslint-plugin-svelte": "^3.20.0",
|
||||||
"hls.js": "^1.6.15",
|
"hls.js": "^1.6.16",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.9.4",
|
||||||
"prettier-plugin-svelte": "^3.4.1",
|
"prettier-plugin-svelte": "^4.1.1",
|
||||||
"sass": "^1.97.2",
|
"sass": "^1.101.0",
|
||||||
"svelte": "^5.46.1",
|
"svelte": "^5.56.4",
|
||||||
"svelte-check": "^4.3.5",
|
"svelte-check": "^4.7.2",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.3.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.52.0",
|
"typescript-eslint": "^8.63.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^8.1.4",
|
||||||
"web-ext": "^9.2.0",
|
"web-ext": "^10.4.0",
|
||||||
"wxt": "^0.20.13"
|
"wxt": "^0.20.27"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+5724
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,4 @@
|
|||||||
|
allowBuilds:
|
||||||
|
'@parcel/watcher': true
|
||||||
|
esbuild: true
|
||||||
|
spawn-sync: false
|
||||||
@@ -1,21 +1,96 @@
|
|||||||
import { getHost } from '@/lib/host';
|
import { BackgroundMessageData, BackgroundMessageType } from '@/lib/communication';
|
||||||
import { HostSettings, UrlReferer } from '@/lib/settings';
|
import { getHost, hosts } from '@/lib/host';
|
||||||
|
import { HostSettings } from '@/lib/settings';
|
||||||
|
|
||||||
export default defineBackground(() => {
|
const temporaryReferers: Record<string, string> = {};
|
||||||
browser.runtime.onMessage.addListener(async (message) => {
|
const allHostDomains = hosts.flatMap((host) => host.domains);
|
||||||
if (message.action == 'ff2mpv') {
|
|
||||||
await browser.runtime.sendNativeMessage('ff2mpv', { url: message.url });
|
function domainToMatch(domain: string) {
|
||||||
|
return `*://${domain}/*`;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// the following listener is only available in mv2
|
function domainsToMatch(domains: string[]) {
|
||||||
if (import.meta.env.MANIFEST_VERSION === 3) return;
|
return domains.map(domainToMatch);
|
||||||
|
}
|
||||||
|
|
||||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
async function reInjectContentScript(domain: string) {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
const contentScript: Browser.scripting.RegisteredContentScript = {
|
||||||
// @ts-ignore
|
id: 'temporary-hosts',
|
||||||
async (details) => {
|
js: ['content-scripts/content.js'],
|
||||||
const referer = await UrlReferer.get(new URL(details.url).hostname);
|
matches: [domainToMatch(domain)],
|
||||||
|
persistAcrossSessions: false,
|
||||||
|
allFrames: true,
|
||||||
|
runAt: 'document_end'
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentScripts = await browser.scripting.getRegisteredContentScripts({ ids: [contentScript.id] });
|
||||||
|
if (contentScripts.length !== 0) {
|
||||||
|
const registeredContentScript = contentScripts[0];
|
||||||
|
|
||||||
|
// do not double register for same domain
|
||||||
|
if (registeredContentScript.matches!.indexOf(contentScript.matches![0]) !== -1) return;
|
||||||
|
|
||||||
|
contentScript.matches!.push(...contentScripts[0].matches!);
|
||||||
|
await browser.scripting.updateContentScripts([contentScript]);
|
||||||
|
} else {
|
||||||
|
await browser.scripting.registerContentScripts([contentScript]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- listeners --- */
|
||||||
|
async function browserRuntimeOnMessageListener(message: any, sender: Browser.runtime.MessageSender) {
|
||||||
|
const type = message.type as BackgroundMessageType;
|
||||||
|
let data = message.data;
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case BackgroundMessageType.Ff2mpv: {
|
||||||
|
data = data as BackgroundMessageData<typeof type>;
|
||||||
|
|
||||||
|
await browser.runtime.sendNativeMessage('ff2mpv', { url: data.url });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BackgroundMessageType.RequestTabUrl: {
|
||||||
|
response = sender.tab?.url ?? null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BackgroundMessageType.RegisterContentScript: {
|
||||||
|
// requires "dynamic" host permissions, which is only available in mv2
|
||||||
|
if (import.meta.env.MANIFEST_VERSION === 3) break;
|
||||||
|
|
||||||
|
data = data as BackgroundMessageData<typeof type>;
|
||||||
|
await reInjectContentScript(data.domain);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BackgroundMessageType.RegisterTemporaryReferer: {
|
||||||
|
// requires "dynamic" host permissions, which is only available in mv2
|
||||||
|
if (import.meta.env.MANIFEST_VERSION === 3) break;
|
||||||
|
|
||||||
|
data = data as BackgroundMessageData<typeof type>;
|
||||||
|
if (data.domain in temporaryReferers) break;
|
||||||
|
temporaryReferers[data.domain] = data.referer;
|
||||||
|
|
||||||
|
// remount before send headers listener which to also listen on the new temporary referer domain
|
||||||
|
registerBrowserWebRequestOnBeforeSendHeadersListener();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
function registerBrowserRuntimeOnMessageListener() {
|
||||||
|
browser.runtime.onMessage.addListener(browserRuntimeOnMessageListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
function browserWebRequestOnBeforeSendHeadersListener(
|
||||||
|
details: Browser.webRequest.OnBeforeSendHeadersDetails
|
||||||
|
): Browser.webRequest.BlockingResponse | undefined {
|
||||||
|
const referer = temporaryReferers[new URL(details.url).host];
|
||||||
if (!referer) return;
|
if (!referer) return;
|
||||||
|
|
||||||
details.requestHeaders!.push({
|
details.requestHeaders!.push({
|
||||||
@@ -24,18 +99,53 @@ export default defineBackground(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return { requestHeaders: details.requestHeaders };
|
return { requestHeaders: details.requestHeaders };
|
||||||
|
}
|
||||||
|
function registerBrowserWebRequestOnBeforeSendHeadersListener() {
|
||||||
|
// only available in mv2
|
||||||
|
if (import.meta.env.MANIFEST_VERSION === 3) return;
|
||||||
|
// background crashes when a listener with empty url is registered
|
||||||
|
else if (Object.keys(temporaryReferers).length === 0) return;
|
||||||
|
|
||||||
|
browser.webRequest.onBeforeSendHeaders.removeListener(browserWebRequestOnBeforeSendHeadersListener);
|
||||||
|
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||||
|
browserWebRequestOnBeforeSendHeadersListener,
|
||||||
|
{
|
||||||
|
urls: domainsToMatch(Object.keys(temporaryReferers)),
|
||||||
|
types: ['xmlhttprequest']
|
||||||
},
|
},
|
||||||
{ urls: ['<all_urls>'], types: ['xmlhttprequest'] },
|
|
||||||
['blocking', 'requestHeaders']
|
['blocking', 'requestHeaders']
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
browser.webRequest.onBeforeRedirect.addListener(
|
async function browserWebRequestOnBeforeRedirectListener(details: Browser.webRequest.OnBeforeRedirectDetails) {
|
||||||
async (details) => {
|
const domain = new URL(details.url).host;
|
||||||
const host = await getHost(new URL(details.url).hostname);
|
const host = await getHost(domain);
|
||||||
if (!host) return;
|
if (!host) return;
|
||||||
|
|
||||||
await HostSettings.addTemporaryHostDomain(host, new URL(details.redirectUrl).hostname);
|
const newDomain = new URL(details.redirectUrl).host;
|
||||||
},
|
|
||||||
{ urls: ['<all_urls>'], types: ['main_frame', 'sub_frame'] }
|
await Promise.all([
|
||||||
);
|
HostSettings.addTemporaryHostDomain(host.id, newDomain, { registerContentScript: false }),
|
||||||
|
// must be called manually, a message sent from a background script is only received on non-background pages
|
||||||
|
reInjectContentScript(newDomain)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// add new domain to internal list of supported hosts and remount listener to also listen on the new domain
|
||||||
|
if (allHostDomains.indexOf(newDomain) === -1) allHostDomains.push(newDomain);
|
||||||
|
queueMicrotask(registerBrowserWebRequestOnBeforeRedirectListener);
|
||||||
|
}
|
||||||
|
function registerBrowserWebRequestOnBeforeRedirectListener() {
|
||||||
|
// only available in mv2
|
||||||
|
if (import.meta.env.MANIFEST_VERSION === 3) return;
|
||||||
|
|
||||||
|
browser.webRequest.onBeforeRedirect.removeListener(browserWebRequestOnBeforeRedirectListener);
|
||||||
|
browser.webRequest.onBeforeRedirect.addListener(browserWebRequestOnBeforeRedirectListener, {
|
||||||
|
urls: domainsToMatch(allHostDomains),
|
||||||
|
types: ['main_frame', 'sub_frame']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineBackground(() => {
|
||||||
|
registerBrowserRuntimeOnMessageListener();
|
||||||
|
registerBrowserWebRequestOnBeforeRedirectListener();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
|
import { BackgroundMessageType, sendBackgroundMessage } from '@/lib/communication';
|
||||||
import { getHost, HostMatchType, hosts, type HostMatch } from '@/lib/host';
|
import { getHost, HostMatchType, hosts, type HostMatch } from '@/lib/host';
|
||||||
import { FF2MPVSettings } from '@/lib/settings';
|
import { FF2MPVSettings, PerDomainSettings } from '@/lib/settings';
|
||||||
|
|
||||||
export default defineContentScript({
|
export default defineContentScript({
|
||||||
matches: [
|
matches: [...Object.values(hosts).flatMap((h) => h.domains.map((d) => `*://${d}/*`))],
|
||||||
...Object.values(hosts).flatMap((h) => h.domains.map((d) => `*://${d}/*`)),
|
|
||||||
// only mv2 allows to match all urls
|
|
||||||
...(import.meta.env.MANIFEST_VERSION === 2 ? ['<all_urls>'] : [])
|
|
||||||
],
|
|
||||||
allFrames: true,
|
allFrames: true,
|
||||||
runAt: 'document_end',
|
runAt: 'document_end',
|
||||||
main
|
main
|
||||||
@@ -16,6 +13,18 @@ async function main() {
|
|||||||
const host = await getHost(window.location.host);
|
const host = await getHost(window.location.host);
|
||||||
if (!host) return;
|
if (!host) return;
|
||||||
|
|
||||||
|
// if current windows is an iframe, check if the parent has any disabled hosts
|
||||||
|
if (window !== window.top) {
|
||||||
|
const tabUrl = await sendBackgroundMessage(BackgroundMessageType.RequestTabUrl, undefined);
|
||||||
|
if (tabUrl) {
|
||||||
|
const domain = new URL(tabUrl).host;
|
||||||
|
|
||||||
|
const perDomainSetting = await PerDomainSettings.get(domain);
|
||||||
|
if (perDomainSetting.allDisabled) return;
|
||||||
|
else if (perDomainSetting.disabledHostIds.indexOf(host.id) !== -1) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let re = null;
|
let re = null;
|
||||||
for (const regex of host.regex) {
|
for (const regex of host.regex) {
|
||||||
if ((re = document.body.innerHTML.match(regex)) !== null) {
|
if ((re = document.body.innerHTML.match(regex)) !== null) {
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import Hls from 'hls.js';
|
import Hls from 'hls.js';
|
||||||
import { listenMessages, MessageType, sendMessage } from '@/lib/communication';
|
import {
|
||||||
|
BackgroundMessageType,
|
||||||
|
listenTabMessages,
|
||||||
|
sendBackgroundMessage,
|
||||||
|
sendTabBroadcast,
|
||||||
|
TabMessageType
|
||||||
|
} from '@/lib/communication';
|
||||||
import { HostMatchType, hosts } from '@/lib/host';
|
import { HostMatchType, hosts } from '@/lib/host';
|
||||||
import { UrlReferer } from '@/lib/settings';
|
|
||||||
|
|
||||||
async function playNative(url: string, domain: string, videoElem: HTMLVideoElement) {
|
async function playNative(url: string, domain: string, videoElem: HTMLVideoElement) {
|
||||||
// multiple hosts need to have a correct referer set
|
// multiple hosts need to have a correct referer set
|
||||||
await UrlReferer.addTemporary(new URL(url).hostname, domain);
|
await sendBackgroundMessage(BackgroundMessageType.RegisterTemporaryReferer, {
|
||||||
|
domain: new URL(url).host,
|
||||||
|
referer: domain
|
||||||
|
});
|
||||||
|
|
||||||
videoElem.src = url;
|
videoElem.src = url;
|
||||||
}
|
}
|
||||||
@@ -18,7 +26,11 @@ async function playHls(url: string, domain: string, videoElem: HTMLVideoElement)
|
|||||||
enableWorker: false,
|
enableWorker: false,
|
||||||
xhrSetup: async (xhr: XMLHttpRequest, url: string) => {
|
xhrSetup: async (xhr: XMLHttpRequest, url: string) => {
|
||||||
// multiple hosts need to have a correct referer set
|
// multiple hosts need to have a correct referer set
|
||||||
await UrlReferer.addTemporary(new URL(url).hostname, domain);
|
await sendBackgroundMessage(BackgroundMessageType.RegisterTemporaryReferer, {
|
||||||
|
domain: new URL(url).host,
|
||||||
|
referer: domain
|
||||||
|
});
|
||||||
|
|
||||||
xhr.open('GET', url);
|
xhr.open('GET', url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -56,7 +68,7 @@ export async function play(videoElem: HTMLVideoElement) {
|
|||||||
|
|
||||||
function initCommunication(id: string, url: string, domain: string) {
|
function initCommunication(id: string, url: string, domain: string) {
|
||||||
const notifyActiveMatch = () =>
|
const notifyActiveMatch = () =>
|
||||||
sendMessage(MessageType.NotifyActiveMatch, {
|
sendTabBroadcast(TabMessageType.NotifyActiveMatch, {
|
||||||
id: id,
|
id: id,
|
||||||
url: url,
|
url: url,
|
||||||
domain: domain
|
domain: domain
|
||||||
@@ -67,8 +79,8 @@ function initCommunication(id: string, url: string, domain: string) {
|
|||||||
|
|
||||||
// if an extension popup is opened, the listener will recognize it's active match request and send the match/player
|
// if an extension popup is opened, the listener will recognize it's active match request and send the match/player
|
||||||
// data
|
// data
|
||||||
const cancel = listenMessages((type) => {
|
const cancel = listenTabMessages((type) => {
|
||||||
if (type !== MessageType.RequestActiveMatch) return;
|
if (type !== TabMessageType.RequestActiveMatch) return;
|
||||||
notifyActiveMatch();
|
notifyActiveMatch();
|
||||||
});
|
});
|
||||||
window.onbeforeunload = cancel;
|
window.onbeforeunload = cancel;
|
||||||
|
|||||||
@@ -2,32 +2,46 @@
|
|||||||
import '@/assets/base.css';
|
import '@/assets/base.css';
|
||||||
|
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
|
import Toast from '@/entrypoints/popup/components/Toast.svelte';
|
||||||
import Main from '@/entrypoints/popup/pages/main/Main.svelte';
|
import Main from '@/entrypoints/popup/pages/main/Main.svelte';
|
||||||
import Settings from '@/entrypoints/popup/pages/settings/Settings.svelte';
|
import Settings from '@/entrypoints/popup/pages/settings/Settings.svelte';
|
||||||
|
import SiteConfig from '@/entrypoints/popup/pages/site-config/SiteConfig.svelte';
|
||||||
import { isMobile } from '@/entrypoints/popup/state.js';
|
import { isMobile } from '@/entrypoints/popup/state.js';
|
||||||
|
|
||||||
/* state init */
|
/* state init */
|
||||||
browser.runtime.getPlatformInfo().then((info) => ($isMobile = info.os === 'android'));
|
browser.runtime.getPlatformInfo().then((info) => ($isMobile = info.os === 'android'));
|
||||||
|
|
||||||
/* types */
|
/* types */
|
||||||
type Page = 'main' | 'settings';
|
type Page = 'main' | 'settings' | 'site-config';
|
||||||
|
|
||||||
/* states */
|
/* states */
|
||||||
let activePage = $state<Page>('main');
|
let activePage = $state<Page>('main');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex w-[350px] overflow-hidden" class:w-screen={$isMobile}>
|
<div class="flex w-78 overflow-hidden" class:w-screen={$isMobile}>
|
||||||
|
<Toast />
|
||||||
{#if activePage === 'main'}
|
{#if activePage === 'main'}
|
||||||
<div transition:fly={{ x: -300, duration: 150 }} class="min-w-full w-full h-[300px] flex-1 flex flex-col">
|
<div transition:fly={{ x: -300, duration: 150 }} class="min-w-full w-full h-80 flex-1 flex flex-col">
|
||||||
<Main onSettingsOpenRequest={() => (activePage = 'settings')} />
|
<Main
|
||||||
|
onSettingsOpenRequest={() => (activePage = 'settings')}
|
||||||
|
onSiteConfigOpenRequest={() => (activePage = 'site-config')}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if activePage === 'settings'}
|
{:else if activePage === 'settings'}
|
||||||
<div
|
<div
|
||||||
transition:fly={{ x: 300, duration: 150 }}
|
transition:fly={{ x: 300, duration: 150 }}
|
||||||
class="min-w-full w-full h-[300px] flex-1 flex flex-col"
|
class="min-w-full w-full h-120 flex-1 flex flex-col"
|
||||||
class:h-screen={$isMobile}
|
class:h-screen={$isMobile}
|
||||||
>
|
>
|
||||||
<Settings onSettingsCloseRequest={() => (activePage = 'main')} />
|
<Settings onSettingsCloseRequest={() => (activePage = 'main')} />
|
||||||
</div>
|
</div>
|
||||||
|
{:else if activePage === 'site-config'}
|
||||||
|
<div
|
||||||
|
transition:fly={{ x: 300, duration: 150 }}
|
||||||
|
class="min-w-full w-full h-120 flex-1 flex flex-col"
|
||||||
|
class:h-screen={$isMobile}
|
||||||
|
>
|
||||||
|
<SiteConfig onBackClick={() => (activePage = 'main')} />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ArrowLeft } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
onclick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { onclick }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 flex items-center justify-center w-7 h-7 rounded-md text-gray-300 hover:text-gray-100 hover:bg-gray-800/60 cursor-pointer transition-colors"
|
||||||
|
{onclick}
|
||||||
|
>
|
||||||
|
<Icon src={ArrowLeft} size="1.1rem" />
|
||||||
|
</button>
|
||||||
@@ -1 +1,3 @@
|
|||||||
<div class="w-full border-b-[1px] border-gray-400"></div>
|
<script lang="ts"></script>
|
||||||
|
|
||||||
|
<div class="w-full border-b border-gray-400"></div>
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
import type { Host } from '@/lib/host';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
host: Host;
|
||||||
|
enabled?: boolean;
|
||||||
|
badge?: string;
|
||||||
|
onclick?: () => void;
|
||||||
|
actions: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { host, enabled, badge, onclick, actions }: Props = $props();
|
||||||
|
|
||||||
|
let rowClass = $derived(
|
||||||
|
`flex items-center gap-3 px-3 py-2 rounded-md transition-colors ${onclick ? 'hover:bg-gray-800/40 cursor-pointer' : ''}`
|
||||||
|
);
|
||||||
|
|
||||||
|
/* functions */
|
||||||
|
function handleKeydown(event: KeyboardEvent) {
|
||||||
|
if (!onclick) return;
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
onclick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<div
|
||||||
|
class={rowClass}
|
||||||
|
role={onclick ? 'button' : undefined}
|
||||||
|
tabindex={onclick ? 0 : undefined}
|
||||||
|
{onclick}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="w-2 h-2 rounded-full shrink-0"
|
||||||
|
class:bg-linux-mint-green={enabled === true}
|
||||||
|
class:bg-gray-500={enabled === false}
|
||||||
|
class:bg-transparent={enabled === undefined}
|
||||||
|
></span>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
|
<p class="text-sm font-semibold text-gray-100 truncate">{host.name}</p>
|
||||||
|
{#if badge}
|
||||||
|
<span
|
||||||
|
class="shrink-0 text-[0.65rem] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded bg-linux-mint-green/20 text-linux-mint-green"
|
||||||
|
>
|
||||||
|
{badge}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-400 truncate" title={host.domains.join(', ')}>
|
||||||
|
{host.domains.join(', ')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="shrink-0">
|
||||||
|
{@render actions()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ExclamationTriangle, InformationCircle } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
level: 'warning' | 'info';
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { level, title, description }: Props = $props();
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
warning: ExclamationTriangle,
|
||||||
|
info: InformationCircle
|
||||||
|
};
|
||||||
|
const containerStyles = {
|
||||||
|
warning: 'bg-yellow-900/30 border-yellow-700/50 text-yellow-200',
|
||||||
|
info: 'bg-blue-900/30 border-blue-700/50 text-blue-200'
|
||||||
|
};
|
||||||
|
const iconStyles = {
|
||||||
|
warning: 'text-yellow-400',
|
||||||
|
info: 'text-blue-400'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-2 mt-1 flex items-start gap-2 px-2.5 py-2 border rounded-md text-xs {containerStyles[level]}">
|
||||||
|
<Icon src={icons[level]} size="0.95rem" class={iconStyles[level]} />
|
||||||
|
<div class="leading-snug">
|
||||||
|
{#if title}
|
||||||
|
<p class="font-semibold">{title}</p>
|
||||||
|
{/if}
|
||||||
|
{#if description}
|
||||||
|
<p class="opacity-80">{description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
actionsLeft?: Snippet;
|
||||||
|
actionsRight?: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { title, subtitle, actionsLeft, actionsRight }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 px-3 py-2.5">
|
||||||
|
{#if actionsLeft}
|
||||||
|
<div class="shrink-0 flex items-center gap-2">
|
||||||
|
{@render actionsLeft()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<h1 class="text-lg font-semibold text-gray-100 leading-tight truncate">{title}</h1>
|
||||||
|
{#if subtitle}
|
||||||
|
<p class="text-xs text-gray-400 leading-tight truncate" title={subtitle}>{subtitle}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if actionsRight}
|
||||||
|
<div class="shrink-0 flex items-center gap-2">
|
||||||
|
{@render actionsRight()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { MagnifyingGlass, XMark } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
value: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { value = $bindable(''), placeholder = 'Search…' }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 px-2.5 py-1.5 bg-gray-900/60 border border-gray-700 rounded-md focus-within:border-linux-mint-green transition-colors"
|
||||||
|
>
|
||||||
|
<Icon src={MagnifyingGlass} size="0.95rem" class="text-gray-400 shrink-0" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value
|
||||||
|
{placeholder}
|
||||||
|
class="flex-1 bg-transparent text-sm text-gray-100 placeholder:text-gray-500 outline-none min-w-0"
|
||||||
|
/>
|
||||||
|
{#if value}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-gray-400 hover:text-gray-100 cursor-pointer shrink-0"
|
||||||
|
onclick={() => (value = '')}
|
||||||
|
>
|
||||||
|
<Icon src={XMark} size="0.95rem" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ChevronDown } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon, type IconSource } from '@steeze-ui/svelte-icon';
|
||||||
|
import { untrack, type Snippet } from 'svelte';
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: IconSource;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
children: Snippet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { title, description, icon, defaultOpen = false, children }: Props = $props();
|
||||||
|
let open = $state(untrack(() => defaultOpen));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="bg-gray-800/40 border border-gray-700/60 rounded-lg max-h-full flex flex-col">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="w-full flex items-center justify-between gap-2 px-3 py-2.5 cursor-pointer hover:bg-gray-800/60 transition-colors"
|
||||||
|
onclick={() => (open = !open)}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
|
{#if icon}
|
||||||
|
<Icon src={icon} size="1.1rem" class="text-gray-400 shrink-0" />
|
||||||
|
{/if}
|
||||||
|
<div class="min-w-0 text-left">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-100 leading-tight">{title}</h3>
|
||||||
|
{#if description}
|
||||||
|
<p class="text-xs text-gray-400 mt-0.5 leading-tight">{description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-gray-400 shrink-0 inline-flex chevron" class:rotated={open}>
|
||||||
|
<Icon src={ChevronDown} size="1rem" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{#if open}
|
||||||
|
<div class="px-3 py-3 flex overflow-hidden" transition:slide={{ duration: 150 }}>
|
||||||
|
<div class="w-full">
|
||||||
|
{@render children()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chevron {
|
||||||
|
transition: transform 150ms ease;
|
||||||
|
}
|
||||||
|
.rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fly } from 'svelte/transition';
|
||||||
|
import { toast } from '@/entrypoints/popup/state';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $toast}
|
||||||
|
<div
|
||||||
|
class="absolute top-2 left-1/2 -translate-x-1/2 z-20 w-max max-w-11/12 px-3 py-1.5 bg-gray-700 text-gray-100 text-sm rounded shadow-lg pointer-events-none"
|
||||||
|
transition:fly={{ y: -20, duration: 200 }}
|
||||||
|
>
|
||||||
|
{$toast.message}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -4,14 +4,17 @@
|
|||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChecked?: (checked: boolean) => void | boolean | Promise<void> | Promise<boolean>;
|
onChecked?: (checked: boolean) => void | boolean | Promise<void> | Promise<boolean>;
|
||||||
size?: 'sm' | 'md';
|
size?: 'sm' | 'md';
|
||||||
|
disabled?: boolean;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* states */
|
/* states */
|
||||||
let { checked = $bindable(), onChecked, size = 'md' }: Props = $props();
|
let { checked = $bindable(), onChecked, size = 'md', disabled = false, title }: Props = $props();
|
||||||
let internalChecked = $state($state.snapshot(checked));
|
let internalChecked = $state($state.snapshot(checked));
|
||||||
|
|
||||||
/* callbacks */
|
/* callbacks */
|
||||||
async function onInputChange() {
|
async function onInputChange() {
|
||||||
|
if (disabled) return;
|
||||||
internalChecked = !internalChecked;
|
internalChecked = !internalChecked;
|
||||||
let approved = false;
|
let approved = false;
|
||||||
|
|
||||||
@@ -36,9 +39,21 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<label class="flex items-center cursor-pointer">
|
<label
|
||||||
|
class="flex items-center"
|
||||||
|
class:cursor-pointer={!disabled}
|
||||||
|
class:cursor-not-allowed={disabled}
|
||||||
|
class:opacity-50={disabled}
|
||||||
|
{title}
|
||||||
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input type="checkbox" class="peer sr-only" bind:checked={internalChecked} onchange={onInputChange} />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="peer sr-only"
|
||||||
|
bind:checked={internalChecked}
|
||||||
|
onchange={onInputChange}
|
||||||
|
{disabled}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
class="block rounded-full box bg-red-700 peer-checked:bg-linux-mint-green"
|
class="block rounded-full box bg-red-700 peer-checked:bg-linux-mint-green"
|
||||||
class:w-8={size === 'sm'}
|
class:w-8={size === 'sm'}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<script lang="ts"></script>
|
||||||
|
|
||||||
<div class="h-full flex items-center justify-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<p class="text-[1.05rem]">Extension disabled</p>
|
<p class="text-[1.05rem]">Extension disabled</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Clipboard, InformationCircle } from '@steeze-ui/heroicons';
|
import { Clipboard, InformationCircle } from '@steeze-ui/heroicons';
|
||||||
import { Icon } from '@steeze-ui/svelte-icon';
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
import { isMobile } from '@/entrypoints/popup/state';
|
import { isMobile, showToast } from '@/entrypoints/popup/state';
|
||||||
|
|
||||||
/* types */
|
/* types */
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -28,8 +28,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* callbacks */
|
/* callbacks */
|
||||||
function copyUrl() {
|
async function copyUrl() {
|
||||||
navigator.clipboard.writeText(urlOutput);
|
try {
|
||||||
|
await navigator.clipboard.writeText(urlOutput);
|
||||||
|
showToast('URL copied');
|
||||||
|
} catch {
|
||||||
|
showToast('Failed to copy');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -47,7 +52,7 @@
|
|||||||
<div class="relative group h-4 flex justify-center items-center">
|
<div class="relative group h-4 flex justify-center items-center">
|
||||||
<button class="text-sm peer"><Icon src={InformationCircle} size="1rem" /></button>
|
<button class="text-sm peer"><Icon src={InformationCircle} size="1rem" /></button>
|
||||||
<span
|
<span
|
||||||
class="z-10 absolute w-58 bottom-5/4 p-1 bg-gray-800 text-xs invisible opacity-0 group-hover:visible group-hover:opacity-100 peer-focus:visible peer-focus:opacity-100 transition-[opacity]"
|
class="z-10 absolute w-58 bottom-5/4 p-1 bg-gray-800 text-xs invisible opacity-0 group-hover:visible group-hover:opacity-100 peer-focus:visible peer-focus:opacity-100 transition-opacity"
|
||||||
>You may have to send the referer header <code class="select-text">Referer: {domain}</code> when accessing
|
>You may have to send the referer header <code class="select-text">Referer: {domain}</code> when accessing
|
||||||
the url</span
|
the url</span
|
||||||
>
|
>
|
||||||
@@ -57,7 +62,7 @@
|
|||||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
<div class="relative group w-full" tabindex={$isMobile ? 0 : undefined}>
|
<div class="relative group w-full" tabindex={$isMobile ? 0 : undefined}>
|
||||||
<pre
|
<pre
|
||||||
class="w-full h-20 overflow-y-scroll text-[0.8rem] wrap-anywhere text-wrap select-text rounded bg-gray-900 py-[0.25rem] px-1.5">{urlOutput}</pre>
|
class="w-full h-20 overflow-y-scroll text-[0.8rem] wrap-anywhere text-wrap select-text rounded bg-gray-900 py-1 px-1.5">{urlOutput}</pre>
|
||||||
<div
|
<div
|
||||||
class="absolute top-2 right-2 transition-opacity duration-100 opacity-0 group-hover:opacity-100 group-focus:opacity-100 h-full"
|
class="absolute top-2 right-2 transition-opacity duration-100 opacity-0 group-hover:opacity-100 group-focus:opacity-100 h-full"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AdjustmentsHorizontal, ChevronRight, GlobeAlt } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
domain: string;
|
||||||
|
onConfigureClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { domain, onConfigureClick }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between gap-2 px-3 py-1.5 text-sm text-gray-300">
|
||||||
|
<span class="flex items-center gap-2 min-w-0">
|
||||||
|
<Icon src={GlobeAlt} size="0.95rem" class="text-gray-400 shrink-0" />
|
||||||
|
<span class="truncate" title={domain}>{domain}</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center gap-1 shrink-0 text-xs text-gray-400 hover:text-gray-200 px-1.5 py-0.5 rounded hover:bg-gray-800/60 cursor-pointer transition-colors"
|
||||||
|
onclick={onConfigureClick}
|
||||||
|
>
|
||||||
|
<Icon src={AdjustmentsHorizontal} size="0.85rem" />
|
||||||
|
<span>Configure</span>
|
||||||
|
<Icon src={ChevronRight} size="0.75rem" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Cog6Tooth } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
|
import PageHeader from '@/entrypoints/popup/components/PageHeader.svelte';
|
||||||
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||||
import { HostSettings } from '@/lib/settings';
|
import { HostSettings } from '@/lib/settings';
|
||||||
|
|
||||||
@@ -32,15 +35,17 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-between items-center p-2">
|
<PageHeader title="stream-bypass" subtitle="v{import.meta.env.VERSION}">
|
||||||
<div class="flex items-baseline gap-2">
|
{#snippet actionsRight()}
|
||||||
<h1>stream-bypass</h1>
|
|
||||||
<span class="text-xs text-gray-400">v{import.meta.env.VERSION}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
{#key allHostsDisabled}
|
{#key allHostsDisabled}
|
||||||
<Toggle bind:checked={() => !allHostsDisabled, (v) => (allHostsDisabled = !v)} />
|
<Toggle bind:checked={() => !allHostsDisabled, (v) => (allHostsDisabled = !v)} />
|
||||||
{/key}
|
{/key}
|
||||||
<button class="font-bold cursor-pointer" onclick={() => onSettingsClick()}>⋮</button>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
</div>
|
class="shrink-0 flex items-center justify-center w-7 h-7 rounded-md text-gray-300 hover:text-gray-100 hover:bg-gray-800/60 cursor-pointer transition-colors"
|
||||||
|
onclick={() => onSettingsClick()}
|
||||||
|
>
|
||||||
|
<Icon src={Cog6Tooth} size="1.1rem" />
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</PageHeader>
|
||||||
|
|||||||
@@ -3,63 +3,92 @@
|
|||||||
|
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
||||||
import AllDisabled from '@/entrypoints/popup/pages/main/AllDisabled.svelte';
|
import Notice from '@/entrypoints/popup/components/Notice.svelte';
|
||||||
import CopyMatch from '@/entrypoints/popup/pages/main/CopyMatch.svelte';
|
import CopyMatch from '@/entrypoints/popup/pages/main/CopyMatch.svelte';
|
||||||
|
import CurrentSite from '@/entrypoints/popup/pages/main/CurrentSite.svelte';
|
||||||
import Header from '@/entrypoints/popup/pages/main/Header.svelte';
|
import Header from '@/entrypoints/popup/pages/main/Header.svelte';
|
||||||
import Match from '@/entrypoints/popup/pages/main/Match.svelte';
|
import Match from '@/entrypoints/popup/pages/main/Match.svelte';
|
||||||
import NoMatch from '@/entrypoints/popup/pages/main/NoMatch.svelte';
|
import { listenTabMessages, sendTabMessageToActiveTab, TabMessageType } from '@/lib/communication';
|
||||||
import { listenMessages, MessageType, sendMessageToActiveTab } from '@/lib/communication';
|
|
||||||
import { hosts, type Host } from '@/lib/host';
|
import { hosts, type Host } from '@/lib/host';
|
||||||
import { HostSettings } from '@/lib/settings';
|
import { HostSettings } from '@/lib/settings';
|
||||||
|
|
||||||
/* types */
|
/* types */
|
||||||
interface Props {
|
interface Props {
|
||||||
onSettingsOpenRequest: () => void;
|
onSettingsOpenRequest: () => void;
|
||||||
|
onSiteConfigOpenRequest: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* states */
|
/* states */
|
||||||
let { onSettingsOpenRequest }: Props = $props();
|
let { onSettingsOpenRequest, onSiteConfigOpenRequest }: Props = $props();
|
||||||
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
|
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
|
||||||
|
let currentDomain = $state<string | null>(null);
|
||||||
|
|
||||||
let allHostsDisabled = $state(false);
|
let allHostsDisabled = $state(false);
|
||||||
HostSettings.getAllHostsDisabled().then((val) => (allHostsDisabled = val));
|
HostSettings.getAllHostsDisabled().then((val) => (allHostsDisabled = val));
|
||||||
|
|
||||||
/* lifecycle */
|
/* lifecycle */
|
||||||
const cancel = listenMessages((type, data) => {
|
const cancel = listenTabMessages((type, data) => {
|
||||||
if (type !== MessageType.NotifyActiveMatch) return;
|
if (type !== TabMessageType.NotifyActiveMatch) return;
|
||||||
currentMatch = {
|
const match = {
|
||||||
host: hosts.find((host) => host.id === data.id)!,
|
host: hosts.find((host) => host.id === data.id)!,
|
||||||
url: data.url,
|
url: data.url,
|
||||||
domain: data.domain
|
domain: data.domain
|
||||||
};
|
};
|
||||||
|
currentMatch = match;
|
||||||
|
});
|
||||||
|
sendTabMessageToActiveTab(TabMessageType.RequestActiveMatch, undefined);
|
||||||
|
|
||||||
|
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
|
||||||
|
const url = tabs[0]?.url;
|
||||||
|
if (!url) return;
|
||||||
|
currentDomain = new URL(url).hostname;
|
||||||
});
|
});
|
||||||
sendMessageToActiveTab(MessageType.RequestActiveMatch, undefined);
|
|
||||||
|
|
||||||
onDestroy(cancel);
|
onDestroy(cancel);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full shrink-0">
|
||||||
<Header bind:allHostsDisabled onSettingsClick={onSettingsOpenRequest} />
|
<Header bind:allHostsDisabled onSettingsClick={onSettingsOpenRequest} />
|
||||||
<Divider />
|
<Divider />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-2 h-full">
|
<div class="grid grid-cols-1 grid-rows-[min-content_auto_min-content] gap-2 h-full">
|
||||||
|
<div class="px-2 pt-1.5 shrink-0">
|
||||||
|
{#if currentDomain}
|
||||||
|
<CurrentSite domain={currentDomain} onConfigureClick={onSiteConfigOpenRequest} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
{#if allHostsDisabled}
|
{#if allHostsDisabled}
|
||||||
<AllDisabled />
|
<Notice
|
||||||
{:else if !currentMatch}
|
level="warning"
|
||||||
<NoMatch />
|
title="Extension disabled"
|
||||||
{:else}
|
description="Configurations are still saved and take effect the when extension is re-enabled."
|
||||||
<div class="flex flex-col justify-between h-full pb-2">
|
/>
|
||||||
<Match host={currentMatch.host} domain={currentMatch.domain} />
|
{:else if currentMatch}
|
||||||
<div class="divider border-dashed"></div>
|
<div class="flex flex-col justify-between h-full px-2">
|
||||||
<div class="mt-2">
|
<Match host={currentMatch.host} />
|
||||||
<CopyMatch url={currentMatch.url} domain={currentMatch.domain} />
|
<CopyMatch url={currentMatch.url} domain={currentMatch.domain} />
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex-1 min-h-0 flex flex-col">
|
||||||
|
<Notice level="info" title="No match found" description="No redirectable host found on this site" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="px-2 pb-0.5">
|
||||||
|
<p class="text-[.65rem] text-gray-400 text-center">
|
||||||
|
Suggestions or bugs can be submitted <a
|
||||||
|
class="underline"
|
||||||
|
href="https://github.com/bytedream/stream-bypass/issues">here</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
@apply select-none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,17 +4,13 @@
|
|||||||
/* types */
|
/* types */
|
||||||
interface Props {
|
interface Props {
|
||||||
host: Host;
|
host: Host;
|
||||||
domain: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* states */
|
/* states */
|
||||||
const { host, domain }: Props = $props();
|
const { host }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col items-center justify-center w-full h-full">
|
<div class="flex flex-col items-center justify-center">
|
||||||
<p class="text-lg">Match found:</p>
|
<p class="text-md text-gray-400">Match found:</p>
|
||||||
<div class="[&>*]:select-text">
|
<span class="underline text-green-400 text-xl font-bold select-text">{host.name}</span>
|
||||||
<span class="underline text-green-400 text-2xl font-bold">{host.name}</span>
|
|
||||||
<span class="text-xs text-slate-300">({domain})</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<div class="relative h-full">
|
|
||||||
<div class="h-full flex items-center justify-center">
|
|
||||||
<p class="text-[1.05rem]">No supported video found on this site</p>
|
|
||||||
</div>
|
|
||||||
<div class="absolute bottom-0.5">
|
|
||||||
<p class="text-xs text-gray-400">
|
|
||||||
Suggestions or bugs can be submitted <a
|
|
||||||
class="underline"
|
|
||||||
href="https://github.com/bytedream/stream-bypass/issues">here</a
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { showToast } from '../../state';
|
||||||
import { InformationCircle } from '@steeze-ui/heroicons';
|
import { InformationCircle } from '@steeze-ui/heroicons';
|
||||||
import { Icon } from '@steeze-ui/svelte-icon';
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||||
@@ -9,21 +10,28 @@
|
|||||||
FF2MPVSettings.getEnabled().then((val) => (enabled = val));
|
FF2MPVSettings.getEnabled().then((val) => (enabled = val));
|
||||||
|
|
||||||
/* callbacks */
|
/* callbacks */
|
||||||
function onEnableChange(enabled: boolean) {
|
async function onEnableChange(enabled: boolean) {
|
||||||
if (!enabled) return true;
|
if (!enabled) return true;
|
||||||
|
|
||||||
return browser.permissions.request({ permissions: ['nativeMessaging'] });
|
const id = setTimeout(() => showToast('Accept the permission request to allow communication with mpv'), 100);
|
||||||
|
const confirmed = await browser.permissions.request({ permissions: ['nativeMessaging'] });
|
||||||
|
clearTimeout(id);
|
||||||
|
|
||||||
|
return confirmed;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center justify-between">
|
||||||
<div class="relative mr-3">
|
<div class="flex flex-col gap-0.5">
|
||||||
<span>Communication enabled</span>
|
<p class="text-sm font-semibold text-gray-100">Communication enabled</p>
|
||||||
<a
|
<a
|
||||||
class="absolute -top-1 -right-4 text-sm"
|
class="inline-flex items-center gap-1 text-xs text-gray-400 hover:text-gray-200 transition-colors"
|
||||||
href="https://github.com/bytedream/stream-bypass/tree/main?tab=readme-ov-file#ff2mpv-use-mpv-to-directly-play-streams"
|
href="https://github.com/bytedream/stream-bypass/tree/main?tab=readme-ov-file#ff2mpv"
|
||||||
target="_blank"><Icon src={InformationCircle} size="1rem" /></a
|
target="_blank"
|
||||||
>
|
>
|
||||||
|
<Icon src={InformationCircle} size="0.85rem" />
|
||||||
|
<span>Learn more</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{#key enabled}
|
{#key enabled}
|
||||||
<Toggle
|
<Toggle
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import BackButton from '@/entrypoints/popup/components/BackButton.svelte';
|
||||||
|
import PageHeader from '@/entrypoints/popup/components/PageHeader.svelte';
|
||||||
|
|
||||||
/* types */
|
/* types */
|
||||||
interface Props {
|
interface Props {
|
||||||
onBackClick: () => void;
|
onBackClick: () => void;
|
||||||
@@ -8,9 +11,8 @@
|
|||||||
let { onBackClick }: Props = $props();
|
let { onBackClick }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-between items-center p-2">
|
<PageHeader title="Settings" subtitle="Manage hosts and integrations">
|
||||||
<div class="flex items-baseline gap-2">
|
{#snippet actionsLeft()}
|
||||||
<button class="cursor-pointer" onclick={() => onBackClick()}>←</button>
|
<BackButton onclick={onBackClick} />
|
||||||
<h1>Settings</h1>
|
{/snippet}
|
||||||
</div>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,41 +1,47 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import HostRow from '@/entrypoints/popup/components/HostRow.svelte';
|
||||||
|
import SearchInput from '@/entrypoints/popup/components/SearchInput.svelte';
|
||||||
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||||
import { hosts } from '@/lib/host';
|
import { filterHosts, hosts } from '@/lib/host';
|
||||||
import { HostSettings } from '@/lib/settings';
|
import { HostSettings } from '@/lib/settings';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
searchQuery: string;
|
||||||
|
}
|
||||||
|
|
||||||
/* states */
|
/* states */
|
||||||
|
let { searchQuery = $bindable('') }: Props = $props();
|
||||||
let disabledHostIds = $state<Array<string>>([]);
|
let disabledHostIds = $state<Array<string>>([]);
|
||||||
HostSettings.getDisabledHosts().then((val) => (disabledHostIds = val));
|
HostSettings.getDisabledHosts().then((val) => (disabledHostIds = val));
|
||||||
|
|
||||||
|
let filtered = $derived(filterHosts(hosts, searchQuery));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid grid-cols-[35%_43%_22%] gap-y-0.75">
|
<div class="flex flex-col gap-2 max-h-full">
|
||||||
<p class="font-bold">Host</p>
|
<SearchInput bind:value={searchQuery} placeholder="Filter hosts…" />
|
||||||
<p class="font-bold">Domains</p>
|
{#if filtered.length === 0}
|
||||||
<p class="font-bold">Enabled</p>
|
<p class="text-xs text-gray-400 text-center py-2">No hosts match "{searchQuery}"</p>
|
||||||
{#each hosts as host (host.id)}
|
{:else}
|
||||||
{@const domainList = host.domains.join(', ')}
|
<div class="flex flex-col divide-y divide-gray-700/40 -mx-1 max-h-full overflow-y-scroll">
|
||||||
<p>{host.name}</p>
|
{#each filtered as host (host.id)}
|
||||||
<div>
|
<HostRow {host} enabled={!disabledHostIds.includes(host.id)}>
|
||||||
<label for={host.id}>
|
{#snippet actions()}
|
||||||
<input id={host.id} type="checkbox" class="peer hidden" checked />
|
|
||||||
<p
|
|
||||||
title={domainList}
|
|
||||||
class="cursor-pointer overflow-hidden peer-checked:text-ellipsis peer-checked:text-nowrap"
|
|
||||||
>
|
|
||||||
{domainList}
|
|
||||||
</p>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="mt-[.2rem]">
|
|
||||||
{#key disabledHostIds}
|
{#key disabledHostIds}
|
||||||
<Toggle
|
<Toggle
|
||||||
bind:checked={
|
bind:checked={
|
||||||
() => !disabledHostIds.includes(host.id),
|
() => !disabledHostIds.includes(host.id),
|
||||||
(v) => (v ? HostSettings.removeDisabledHost(host) : HostSettings.addDisabledHost(host))
|
(v) =>
|
||||||
|
v
|
||||||
|
? HostSettings.removeDisabledHost(host.id)
|
||||||
|
: HostSettings.addDisabledHost(host.id)
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
></Toggle>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
{/snippet}
|
||||||
|
</HostRow>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { showToast } from '../../state';
|
||||||
|
import { Trash } from '@steeze-ui/heroicons';
|
||||||
|
import { Icon } from '@steeze-ui/svelte-icon';
|
||||||
|
import HostRow from '@/entrypoints/popup/components/HostRow.svelte';
|
||||||
|
import { getHostFromId, hosts } from '@/lib/host';
|
||||||
|
import { PerDomainSettings, type PerDomainConfig } from '@/lib/settings';
|
||||||
|
|
||||||
|
/* constants */
|
||||||
|
const ALL_HOSTS = '__all__';
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let perDomainMap = $state<Record<string, PerDomainConfig>>({});
|
||||||
|
let expanded = $state<string | null>(null);
|
||||||
|
|
||||||
|
let newDomainInput = $state<string>('');
|
||||||
|
let selectedHostId = $state<string>('');
|
||||||
|
|
||||||
|
loadMap();
|
||||||
|
|
||||||
|
/* functions */
|
||||||
|
async function loadMap() {
|
||||||
|
perDomainMap = await PerDomainSettings.getMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDomain(input: string): string | null {
|
||||||
|
let d = input.trim().toLowerCase();
|
||||||
|
if (!d) return null;
|
||||||
|
d = d.replace(/^https?:\/\//, '');
|
||||||
|
d = d.split('/')[0];
|
||||||
|
d = d.split(':')[0];
|
||||||
|
if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/.test(d)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* callbacks */
|
||||||
|
async function onClear(domain: string) {
|
||||||
|
await PerDomainSettings.clear(domain);
|
||||||
|
await loadMap();
|
||||||
|
if (expanded === domain) expanded = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAllRemove(domain: string) {
|
||||||
|
await PerDomainSettings.setAllDisabled(domain, false);
|
||||||
|
await loadMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onHostRemove(domain: string, hostId: string) {
|
||||||
|
await PerDomainSettings.setHostDisabled(domain, hostId, false);
|
||||||
|
await loadMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onHostAdd(domain: string, hostId: string) {
|
||||||
|
if (!hostId) return;
|
||||||
|
if (hostId === ALL_HOSTS) {
|
||||||
|
await PerDomainSettings.setAllDisabled(domain, true);
|
||||||
|
} else {
|
||||||
|
await PerDomainSettings.setHostDisabled(domain, hostId, true);
|
||||||
|
}
|
||||||
|
await loadMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDomainAdd(input: string) {
|
||||||
|
const domain = parseDomain(input);
|
||||||
|
if (!domain) {
|
||||||
|
showToast('Invalid domain');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await PerDomainSettings.add(domain);
|
||||||
|
await loadMap();
|
||||||
|
expanded = domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleExpand(domain: string) {
|
||||||
|
expanded = expanded === domain ? null : domain;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1 h-full overflow-y-scroll">
|
||||||
|
<div class="flex items-center gap-2 pb-1.5">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={newDomainInput}
|
||||||
|
placeholder="Add domain…"
|
||||||
|
class="flex-1 min-w-0 text-xs bg-gray-900/60 border border-gray-700 rounded px-2 py-1 text-gray-100 placeholder:text-gray-500 focus:border-linux-mint-green focus:outline-none transition-colors"
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === 'Enter') onDomainAdd(newDomainInput).then(() => (newDomainInput = ''));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 px-2 py-1 text-xs rounded bg-gray-700/60 hover:bg-linux-mint-green hover:text-white text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors"
|
||||||
|
disabled={!newDomainInput.trim()}
|
||||||
|
onclick={() => onDomainAdd(newDomainInput).then(() => (newDomainInput = ''))}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if Object.keys(perDomainMap).length === 0}
|
||||||
|
<p class="text-xs text-gray-400 text-center py-2">
|
||||||
|
No per-site disables configured. Use the popup on a streaming site to configure it.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
{#each Object.entries(perDomainMap) as [domain, settings] (domain)}
|
||||||
|
{@const isOpen = expanded === domain}
|
||||||
|
<div class="border border-gray-700/60 rounded-md overflow-hidden">
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<div
|
||||||
|
class="w-full flex items-center justify-between gap-2 px-2.5 py-1.5 bg-gray-900/40 hover:bg-gray-800/60 cursor-pointer transition-colors"
|
||||||
|
role="button"
|
||||||
|
tabindex={0}
|
||||||
|
onclick={() => toggleExpand(domain)}
|
||||||
|
>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-sm font-semibold text-gray-100 truncate">{domain}</p>
|
||||||
|
<p class="text-[0.7rem] text-gray-400">
|
||||||
|
{#if settings.allDisabled}
|
||||||
|
All hosts disabled
|
||||||
|
{:else}
|
||||||
|
{settings.disabledHostIds.length}
|
||||||
|
{settings.disabledHostIds.length === 1 ? 'host' : 'hosts'} disabled
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-700/40 cursor-pointer transition-colors"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClear(domain);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon src={Trash} size="0.9rem" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if isOpen}
|
||||||
|
{@const availableHosts = hosts.filter((h) => !settings.disabledHostIds.includes(h.id))}
|
||||||
|
<div class="px-2 py-2 border-t border-gray-700/60 flex flex-col gap-2">
|
||||||
|
{#if settings.allDisabled}
|
||||||
|
<div class="flex items-center justify-between gap-2 px-2 py-1.5 bg-gray-900/40 rounded">
|
||||||
|
<p class="text-xs text-gray-300">All hosts disabled</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-700/40 cursor-pointer transition-colors"
|
||||||
|
onclick={() => onAllRemove(domain)}
|
||||||
|
>
|
||||||
|
<Icon src={Trash} size="0.9rem" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if settings.disabledHostIds.length > 0}
|
||||||
|
<div class="flex flex-col -mx-1 max-h-60 overflow-y-auto">
|
||||||
|
{#each settings.disabledHostIds as hostId (hostId)}
|
||||||
|
{@const host = getHostFromId(hostId)}
|
||||||
|
{#if host}
|
||||||
|
<HostRow {host} enabled={false}>
|
||||||
|
{#snippet actions()}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 p-1 rounded text-gray-400 hover:text-red-400 hover:bg-gray-700/40 cursor-pointer transition-colors"
|
||||||
|
onclick={() => onHostRemove(domain, hostId)}
|
||||||
|
>
|
||||||
|
<Icon src={Trash} size="0.9rem" />
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
</HostRow>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !settings.allDisabled && settings.disabledHostIds.length === 0}
|
||||||
|
<p class="text-xs text-gray-400 text-center py-1">No overrides set</p>
|
||||||
|
{/if}
|
||||||
|
<div class="flex items-center gap-2 pt-1">
|
||||||
|
<select
|
||||||
|
bind:value={selectedHostId}
|
||||||
|
class="flex-1 min-w-0 text-xs bg-gray-900/60 border border-gray-700 rounded px-1.5 py-1 text-gray-100 cursor-pointer focus:border-linux-mint-green focus:outline-none transition-colors"
|
||||||
|
>
|
||||||
|
<option value="" disabled>Add host…</option>
|
||||||
|
{#if !settings.allDisabled}
|
||||||
|
<option value={ALL_HOSTS}>All hosts</option>
|
||||||
|
<option disabled>─────────</option>
|
||||||
|
{/if}
|
||||||
|
{#each availableHosts as host (host.id)}
|
||||||
|
<option value={host.id}>{host.name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 px-2 py-1 text-xs rounded bg-gray-700/60 hover:bg-linux-mint-green hover:text-white text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer transition-colors"
|
||||||
|
disabled={!selectedHostId}
|
||||||
|
onclick={() => onHostAdd(domain, selectedHostId).then(() => (selectedHostId = ''))}
|
||||||
|
>
|
||||||
|
{selectedHostId === ALL_HOSTS ? 'Disable all' : 'Add'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { CommandLine, MapPin, ServerStack } from '@steeze-ui/heroicons';
|
||||||
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
||||||
|
import Section from '@/entrypoints/popup/components/Section.svelte';
|
||||||
import Ff2mpv from '@/entrypoints/popup/pages/settings/Ff2mpv.svelte';
|
import Ff2mpv from '@/entrypoints/popup/pages/settings/Ff2mpv.svelte';
|
||||||
import Header from '@/entrypoints/popup/pages/settings/Header.svelte';
|
import Header from '@/entrypoints/popup/pages/settings/Header.svelte';
|
||||||
import HostsTable from '@/entrypoints/popup/pages/settings/HostsTable.svelte';
|
import HostsTable from '@/entrypoints/popup/pages/settings/HostsTable.svelte';
|
||||||
|
import PerSiteDisables from '@/entrypoints/popup/pages/settings/PerSiteDisables.svelte';
|
||||||
import { isMobile } from '@/entrypoints/popup/state';
|
import { isMobile } from '@/entrypoints/popup/state';
|
||||||
|
import { PerDomainSettings } from '@/lib/settings';
|
||||||
|
|
||||||
/* types */
|
/* types */
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -12,49 +16,38 @@
|
|||||||
|
|
||||||
/* states */
|
/* states */
|
||||||
let { onSettingsCloseRequest }: Props = $props();
|
let { onSettingsCloseRequest }: Props = $props();
|
||||||
|
let searchQuery = $state('');
|
||||||
|
|
||||||
|
onDestroy(PerDomainSettings.cleanEmpty);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<Header onBackClick={onSettingsCloseRequest} />
|
<Header onBackClick={onSettingsCloseRequest} />
|
||||||
<Divider />
|
<Divider />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-y-1 pt-1 h-full mx-2 my-1 overflow-y-scroll">
|
<div class="flex flex-col gap-2 p-2 h-full overflow-y-scroll">
|
||||||
<details class="details" open>
|
<div class="max-h-full">
|
||||||
<summary>Hosts</summary>
|
<Section
|
||||||
<HostsTable />
|
title="Hosts"
|
||||||
</details>
|
description="Enable or disable support for specific streaming providers"
|
||||||
|
icon={ServerStack}
|
||||||
|
defaultOpen
|
||||||
|
>
|
||||||
|
<HostsTable bind:searchQuery />
|
||||||
|
</Section>
|
||||||
|
</div>
|
||||||
|
<Section title="Per-site disables" description="Disable hosts for specific websites" icon={MapPin}>
|
||||||
|
<PerSiteDisables />
|
||||||
|
</Section>
|
||||||
{#if !$isMobile}
|
{#if !$isMobile}
|
||||||
<details class="details">
|
<Section title="ff2mpv" description="Play streams directly in mpv via native messaging" icon={CommandLine}>
|
||||||
<summary>ff2mpv</summary>
|
|
||||||
<Ff2mpv />
|
<Ff2mpv />
|
||||||
</details>
|
</Section>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
@apply select-none;
|
user-select: none;
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
/* using normal css instead of tailwind in the following blocks.
|
|
||||||
for some reason tailwind fails to resolve many references */
|
|
||||||
|
|
||||||
&::before,
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
border-top: 1px solid var(--color-gray-600);
|
|
||||||
margin: 0.25rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > summary {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[open] > summary {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import BackButton from '@/entrypoints/popup/components/BackButton.svelte';
|
||||||
|
import PageHeader from '@/entrypoints/popup/components/PageHeader.svelte';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
domain: string;
|
||||||
|
onBackClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { domain, onBackClick }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<PageHeader title="Site settings" subtitle={domain}>
|
||||||
|
{#snippet actionsLeft()}
|
||||||
|
<BackButton onclick={onBackClick} />
|
||||||
|
{/snippet}
|
||||||
|
</PageHeader>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import HostRow from '@/entrypoints/popup/components/HostRow.svelte';
|
||||||
|
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||||
|
import { hosts, type Host } from '@/lib/host';
|
||||||
|
import { PerDomainSettings } from '@/lib/settings';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
domain: string;
|
||||||
|
matchedHostId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { domain, matchedHostId }: Props = $props();
|
||||||
|
|
||||||
|
let perDomain = $state<{ allDisabled: boolean; disabledHostIds: string[] }>({
|
||||||
|
allDisabled: false,
|
||||||
|
disabledHostIds: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// `domain` is updated after this component is active, and thus we must react to this update
|
||||||
|
$effect(() => {
|
||||||
|
PerDomainSettings.get(domain).then((val) => (perDomain = val));
|
||||||
|
});
|
||||||
|
|
||||||
|
let orderedHosts = $derived(getOrderedHosts(matchedHostId));
|
||||||
|
|
||||||
|
/* functions */
|
||||||
|
function getOrderedHosts(matchedId: string | null): Host[] {
|
||||||
|
const matched = matchedId ? hosts.find((h) => h.id === matchedId) : null;
|
||||||
|
const rest = hosts
|
||||||
|
.filter((h) => !matched || h.id !== matched.id)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
return matched ? [matched, ...rest] : rest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* callbacks */
|
||||||
|
async function onHostToggle(hostId: string, enabled: boolean) {
|
||||||
|
await PerDomainSettings.setHostDisabled(domain, hostId, !enabled);
|
||||||
|
perDomain = await PerDomainSettings.get(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAllToggle(enabled: boolean) {
|
||||||
|
await PerDomainSettings.setAllDisabled(domain, !enabled);
|
||||||
|
perDomain = await PerDomainSettings.get(domain);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 px-2 pb-2 h-full overflow-hidden">
|
||||||
|
<div class="flex-1 min-h-0 overflow-y-auto -mx-1 divide-y divide-gray-700/40">
|
||||||
|
{#each orderedHosts as host (host.id)}
|
||||||
|
{@const isInDisabledList = perDomain.disabledHostIds.includes(host.id)}
|
||||||
|
<HostRow
|
||||||
|
{host}
|
||||||
|
enabled={!isInDisabledList && !perDomain.allDisabled}
|
||||||
|
badge={host.id === matchedHostId ? 'Match' : undefined}
|
||||||
|
>
|
||||||
|
{#snippet actions()}
|
||||||
|
{#key perDomain.allDisabled + perDomain.disabledHostIds.join(',')}
|
||||||
|
<Toggle
|
||||||
|
disabled={perDomain.allDisabled}
|
||||||
|
title={perDomain.allDisabled ? 'All hosts are disabled for this site' : undefined}
|
||||||
|
bind:checked={
|
||||||
|
() => !isInDisabledList,
|
||||||
|
(v) => {
|
||||||
|
onHostToggle(host.id, v);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
{/snippet}
|
||||||
|
</HostRow>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between gap-2 px-3 py-2 mt-1 bg-gray-800/40 border border-gray-700/60 rounded-md shrink-0"
|
||||||
|
>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="text-sm font-semibold text-gray-100">Disable all on this site</p>
|
||||||
|
<p class="text-xs text-gray-400 truncate">Turn off all hosts for {domain}</p>
|
||||||
|
</div>
|
||||||
|
{#key perDomain.allDisabled}
|
||||||
|
<Toggle
|
||||||
|
bind:checked={
|
||||||
|
() => !perDomain.allDisabled,
|
||||||
|
(v) => {
|
||||||
|
onAllToggle(v);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
||||||
|
import Header from '@/entrypoints/popup/pages/site-config/Header.svelte';
|
||||||
|
import PerSiteHostsList from '@/entrypoints/popup/pages/site-config/PerSiteHostsList.svelte';
|
||||||
|
import { listenTabMessages, sendTabMessageToActiveTab, TabMessageType } from '@/lib/communication';
|
||||||
|
import { hosts, type Host } from '@/lib/host';
|
||||||
|
import { PerDomainSettings } from '@/lib/settings';
|
||||||
|
|
||||||
|
/* types */
|
||||||
|
interface Props {
|
||||||
|
onBackClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* states */
|
||||||
|
let { onBackClick }: Props = $props();
|
||||||
|
|
||||||
|
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
|
||||||
|
let currentDomain = $state<string | null>(null);
|
||||||
|
|
||||||
|
/* lifecycle */
|
||||||
|
const cancel = listenTabMessages((type, data) => {
|
||||||
|
if (type !== TabMessageType.NotifyActiveMatch) return;
|
||||||
|
const match = {
|
||||||
|
host: hosts.find((host) => host.id === data.id)!,
|
||||||
|
url: data.url,
|
||||||
|
domain: data.domain
|
||||||
|
};
|
||||||
|
currentMatch = match;
|
||||||
|
});
|
||||||
|
sendTabMessageToActiveTab(TabMessageType.RequestActiveMatch, undefined);
|
||||||
|
|
||||||
|
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
|
||||||
|
const url = tabs[0]?.url;
|
||||||
|
if (!url) return;
|
||||||
|
currentDomain = new URL(url).hostname;
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
cancel();
|
||||||
|
PerDomainSettings.cleanEmpty();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<Header domain={currentDomain ?? ''} {onBackClick} />
|
||||||
|
<Divider />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col h-full overflow-hidden">
|
||||||
|
{#if currentDomain}
|
||||||
|
<PerSiteHostsList domain={currentDomain} matchedHostId={currentMatch?.host.id ?? null} />
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-center justify-center h-full text-sm text-gray-400 p-4 text-center">
|
||||||
|
No active website detected.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
/* --- global init --- */
|
||||||
export const isMobile = writable(false);
|
export const isMobile = writable(false);
|
||||||
|
|
||||||
|
/* --- toast --- */
|
||||||
|
interface Toast {
|
||||||
|
id: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toast = writable<Toast | null>(null);
|
||||||
|
|
||||||
|
let toastId = 0;
|
||||||
|
|
||||||
|
export function showToast(message: string, duration = 2000) {
|
||||||
|
const id = ++toastId;
|
||||||
|
toast.set({ id, message });
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.update((current) => (current?.id === id ? null : current));
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
|||||||
+46
-11
@@ -1,36 +1,71 @@
|
|||||||
export enum MessageType {
|
// --- tab communication --- //
|
||||||
RequestActiveMatch,
|
export enum TabMessageType {
|
||||||
|
RequestActiveMatch = 10,
|
||||||
NotifyActiveMatch
|
NotifyActiveMatch
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageData<T extends MessageType> = {
|
export type TabMessageData<T extends TabMessageType> = {
|
||||||
[MessageType.RequestActiveMatch]: undefined;
|
[TabMessageType.RequestActiveMatch]: undefined;
|
||||||
[MessageType.NotifyActiveMatch]: {
|
[TabMessageType.NotifyActiveMatch]: {
|
||||||
id: string;
|
id: string;
|
||||||
url: string;
|
url: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
};
|
};
|
||||||
}[T];
|
}[T];
|
||||||
|
|
||||||
export async function sendMessage<T extends MessageType>(message: T, data: MessageData<T>) {
|
export async function sendTabBroadcast<T extends TabMessageType>(message: T, data: TabMessageData<T>) {
|
||||||
await browser.runtime.sendMessage({ type: message, data: data });
|
await browser.runtime.sendMessage({ type: message, data: data });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendMessageToActiveTab<T extends MessageType>(message: T, data: MessageData<T>) {
|
export async function sendTabMessageToActiveTab<T extends TabMessageType>(message: T, data: TabMessageData<T>) {
|
||||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
|
|
||||||
await browser.tabs.sendMessage(tabs[0].id!, { type: message, data: data }).catch(() => {});
|
await browser.tabs.sendMessage(tabs[0].id!, { type: message, data: data }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listenMessages(listener: (type: MessageType, data: any) => void): () => void {
|
export function listenTabMessages(listener: (type: TabMessageType, data: any) => void): () => void {
|
||||||
const callback = (callbackData: { type: MessageType; data: any }) => {
|
const callback = (callbackData: { type: TabMessageType; data: any }) => {
|
||||||
const { type, data } = callbackData;
|
const { type, data } = callbackData;
|
||||||
listener(type, data);
|
listener(type, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener(callback);
|
browser.runtime.onMessage.addListener(callback);
|
||||||
|
return () => browser.runtime.onMessage.removeListener(callback);
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
// --- background script communication --- //
|
||||||
browser.runtime.onMessage.removeListener(callback);
|
export enum BackgroundMessageType {
|
||||||
|
Ff2mpv = 20,
|
||||||
|
RequestTabUrl,
|
||||||
|
RegisterContentScript,
|
||||||
|
RegisterTemporaryReferer
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BackgroundMessageData<T extends BackgroundMessageType> = {
|
||||||
|
[BackgroundMessageType.Ff2mpv]: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
[BackgroundMessageType.RequestTabUrl]: undefined;
|
||||||
|
[BackgroundMessageType.RegisterContentScript]: {
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
[BackgroundMessageType.RegisterTemporaryReferer]: {
|
||||||
|
domain: string;
|
||||||
|
referer: string;
|
||||||
};
|
};
|
||||||
|
}[T];
|
||||||
|
|
||||||
|
export type BackgroundMessageReply<T extends BackgroundMessageType> = {
|
||||||
|
[BackgroundMessageType.Ff2mpv]: undefined;
|
||||||
|
[BackgroundMessageType.RequestTabUrl]: string | null;
|
||||||
|
[BackgroundMessageType.RegisterContentScript]: undefined;
|
||||||
|
[BackgroundMessageType.RegisterTemporaryReferer]: undefined;
|
||||||
|
}[T];
|
||||||
|
|
||||||
|
export async function sendBackgroundMessage<T extends BackgroundMessageType>(
|
||||||
|
message: T,
|
||||||
|
data: BackgroundMessageData<T>
|
||||||
|
): Promise<BackgroundMessageReply<T>> {
|
||||||
|
const response = await browser.runtime.sendMessage({ type: message, data: data });
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default {
|
|||||||
|
|
||||||
match: async function (match: RegExpMatchArray) {
|
match: async function (match: RegExpMatchArray) {
|
||||||
if (window.location.host.startsWith('filemoon')) {
|
if (window.location.host.startsWith('filemoon')) {
|
||||||
await HostSettings.addTemporaryHostDomain(this, new URL(match[0]).host);
|
await HostSettings.addTemporaryHostDomain(this.id, new URL(match[0]).host);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+34
-7
@@ -17,7 +17,7 @@ import Vidmoly from './vidmoly';
|
|||||||
import Vidoza from './vidoza';
|
import Vidoza from './vidoza';
|
||||||
import Voe from './voe';
|
import Voe from './voe';
|
||||||
import Vupload from './vupload';
|
import Vupload from './vupload';
|
||||||
import { HostSettings } from '@/lib/settings';
|
import { HostSettings, PerDomainSettings } from '@/lib/settings';
|
||||||
|
|
||||||
export enum HostMatchType {
|
export enum HostMatchType {
|
||||||
NATIVE = 'native',
|
NATIVE = 'native',
|
||||||
@@ -30,9 +30,11 @@ export interface HostMatch {
|
|||||||
url: string | null;
|
url: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HostId = string;
|
||||||
|
|
||||||
export interface Host {
|
export interface Host {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: HostId;
|
||||||
domains: string[];
|
domains: string[];
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
regex: RegExp[];
|
regex: RegExp[];
|
||||||
@@ -63,16 +65,41 @@ export const hosts = [
|
|||||||
Vupload
|
Vupload
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function getHostFromId(id: HostId): Host | null {
|
||||||
|
return hosts.find((host) => host.id === id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterHosts(list: Host[], query: string): Host[] {
|
||||||
|
const q = query.trim().toLowerCase();
|
||||||
|
if (!q) return list;
|
||||||
|
return list.filter((host) => {
|
||||||
|
if (host.name.toLowerCase().includes(q)) return true;
|
||||||
|
return host.domains.some((domain) => domain.toLowerCase().includes(q));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function getHost(domain: string): Promise<Host | null> {
|
export async function getHost(domain: string): Promise<Host | null> {
|
||||||
if (await HostSettings.getAllHostsDisabled()) return null;
|
const [allHostsDisabled, disabledHosts, perDomainSetting] = await Promise.all([
|
||||||
|
HostSettings.getAllHostsDisabled(),
|
||||||
|
HostSettings.getDisabledHosts(),
|
||||||
|
PerDomainSettings.get(domain)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// extension is disabled via popup
|
||||||
|
if (allHostsDisabled) return null;
|
||||||
|
// domain is disabled via popup
|
||||||
|
else if (perDomainSetting.allDisabled) return null;
|
||||||
|
|
||||||
const disabledIds = await HostSettings.getDisabledHosts();
|
|
||||||
for (const host of hosts) {
|
for (const host of hosts) {
|
||||||
if (host.domains.includes(domain)) {
|
if (host.domains.includes(domain)) {
|
||||||
if (!disabledIds.includes(host.id)) return host;
|
// host is generally disabled via popup
|
||||||
else return null;
|
if (disabledHosts.includes(host.id)) return null;
|
||||||
|
// host is disabled for this domain via popup
|
||||||
|
else if (perDomainSetting.disabledHostIds.includes(host.id)) return null;
|
||||||
|
|
||||||
|
return host;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return HostSettings.checkTemporaryHostDomain(domain);
|
return HostSettings.checkTemporaryHostDomain(domain).then(getHostFromId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default {
|
|||||||
|
|
||||||
match: async function (match: RegExpMatchArray) {
|
match: async function (match: RegExpMatchArray) {
|
||||||
if (this.domains.indexOf(window.location.hostname) !== -1) {
|
if (this.domains.indexOf(window.location.hostname) !== -1) {
|
||||||
await HostSettings.addTemporaryHostDomain(this, window.location.hostname);
|
await HostSettings.addTemporaryHostDomain(this.id, window.location.hostname);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ export default {
|
|||||||
// always points to vidmoly.net. the "outer" vidmoly site also has a link to some video, which, would
|
// always points to vidmoly.net. the "outer" vidmoly site also has a link to some video, which, would
|
||||||
// be preferred to use, as the whole site could be replaced by the native video player instead of just the iframe,
|
// be preferred to use, as the whole site could be replaced by the native video player instead of just the iframe,
|
||||||
// but said link doesn't always point the same video as the iframe
|
// but said link doesn't always point the same video as the iframe
|
||||||
domains: ['vidmoly.net'],
|
domains: ['vidmoly.net', 'vidmoly.biz'],
|
||||||
regex: [/(?<=file:").+\.m3u8.*(?=")/gm],
|
regex: [/file:\s?'(http.*)'/],
|
||||||
|
|
||||||
match: async function (match: RegExpMatchArray) {
|
match: async function (match: RegExpMatchArray) {
|
||||||
return {
|
return {
|
||||||
type: HostMatchType.HLS,
|
type: HostMatchType.HLS,
|
||||||
url: match[0]
|
url: match[1]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} satisfies Host;
|
} satisfies Host;
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ export default {
|
|||||||
|
|
||||||
match: async function (match: RegExpMatchArray) {
|
match: async function (match: RegExpMatchArray) {
|
||||||
if (window.location.host === 'voe.sx') {
|
if (window.location.host === 'voe.sx') {
|
||||||
await HostSettings.addTemporaryHostDomain(this, new URL(match[0]).host);
|
await HostSettings.addTemporaryHostDomain(this.id, new URL(match[0]).host);
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
let json = match[0];
|
let json = match[0];
|
||||||
|
|||||||
+98
-65
@@ -1,88 +1,121 @@
|
|||||||
import { storage } from '#imports';
|
import { storage } from '#imports';
|
||||||
import { hosts, type Host } from '@/lib/host';
|
import { BackgroundMessageType, sendBackgroundMessage } from './communication';
|
||||||
|
import type { HostId } from '@/lib/host';
|
||||||
|
|
||||||
|
class Setting<T> {
|
||||||
|
private item;
|
||||||
|
|
||||||
|
constructor(key: Parameters<typeof storage.defineItem>[0], fallback: T) {
|
||||||
|
this.item = storage.defineItem(key, { fallback });
|
||||||
|
}
|
||||||
|
|
||||||
|
get = () => this.item.getValue();
|
||||||
|
set = (value: T) => this.item.setValue(value);
|
||||||
|
|
||||||
|
update = (fn: (val: T) => T) => this.get().then(fn).then(this.set);
|
||||||
|
}
|
||||||
|
|
||||||
export class HostSettings {
|
export class HostSettings {
|
||||||
/* disabled hosts */
|
/* disabled hosts */
|
||||||
private static disabledHosts = storage.defineItem<string[]>('local:disabledHosts', { fallback: [] });
|
private static disabledHosts = new Setting<HostId[]>('local:disabledHosts', []);
|
||||||
private static allHostsDisabled = storage.defineItem<boolean>('local:allHostsDisabled', { fallback: false });
|
private static allHostsDisabled = new Setting<boolean>('local:allHostsDisabled', false);
|
||||||
|
|
||||||
static async addDisabledHost(host: Host) {
|
static addDisabledHost = (hostId: HostId) =>
|
||||||
const ids = await this.disabledHosts.getValue();
|
this.disabledHosts.update((val) => {
|
||||||
|
if (!val.includes(hostId)) val.push(hostId);
|
||||||
const index = ids.indexOf(host.id);
|
return val;
|
||||||
if (index === -1) {
|
});
|
||||||
ids.push(host.id);
|
static removeDisabledHost = (hostId: HostId) =>
|
||||||
await this.disabledHosts.setValue(ids);
|
this.disabledHosts.update((val) => {
|
||||||
}
|
const index = val.indexOf(hostId);
|
||||||
}
|
if (index !== -1) val.splice(index, 1);
|
||||||
static async removeDisabledHost(host: Host) {
|
return val;
|
||||||
const ids = await this.disabledHosts.getValue();
|
|
||||||
|
|
||||||
const index = ids.indexOf(host.id);
|
|
||||||
if (index !== -1) {
|
|
||||||
ids.splice(index, 1);
|
|
||||||
await this.disabledHosts.setValue(ids);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
static async getDisabledHosts() {
|
|
||||||
return await this.disabledHosts.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getAllHostsDisabled() {
|
|
||||||
return await this.allHostsDisabled.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
static async setAllHostsDisabled(disabled: boolean) {
|
|
||||||
await this.allHostsDisabled.setValue(disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tmp */
|
|
||||||
private static temporaryHostDomain = storage.defineItem<Record<string, string>>('local:temporaryHostDomain', {
|
|
||||||
fallback: {}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
static async addTemporaryHostDomain(host: Host, domain: string) {
|
static getDisabledHosts = () => this.disabledHosts.get();
|
||||||
const temporaryHostDomains = await this.temporaryHostDomain.getValue();
|
static getAllHostsDisabled = () => this.allHostsDisabled.get();
|
||||||
|
static setAllHostsDisabled = (disabled: boolean) => this.allHostsDisabled.set(disabled);
|
||||||
|
|
||||||
temporaryHostDomains[domain] = host.id;
|
/* tmp */
|
||||||
await this.temporaryHostDomain.setValue(temporaryHostDomains);
|
private static temporaryHostDomain = new Setting<Record<string, string>>('local:temporaryHostDomain', {});
|
||||||
console.log(await this.temporaryHostDomain.getValue());
|
|
||||||
}
|
|
||||||
static async checkTemporaryHostDomain(domain: string) {
|
|
||||||
const temporaryHostDomains = await this.temporaryHostDomain.getValue();
|
|
||||||
|
|
||||||
const hostId = temporaryHostDomains[domain];
|
static addTemporaryHostDomain = async (
|
||||||
return hostId ? (hosts.find((host) => host.id === hostId) ?? null) : null;
|
hostId: HostId,
|
||||||
|
domain: string,
|
||||||
|
options = {
|
||||||
|
// only has an effect with mv2
|
||||||
|
registerContentScript: import.meta.env.MANIFEST_VERSION === 2
|
||||||
}
|
}
|
||||||
|
) => {
|
||||||
|
const backgroundScriptInject = options.registerContentScript
|
||||||
|
? sendBackgroundMessage(BackgroundMessageType.RegisterContentScript, { domain: domain })
|
||||||
|
: Promise.resolve();
|
||||||
|
const temporaryHostDomainUpdate = this.temporaryHostDomain.update((val) => {
|
||||||
|
val[domain] = hostId;
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all([backgroundScriptInject, temporaryHostDomainUpdate]);
|
||||||
|
};
|
||||||
|
static checkTemporaryHostDomain = (domain: string) => this.temporaryHostDomain.get().then((val) => val[domain]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FF2MPVSettings {
|
export class FF2MPVSettings {
|
||||||
private static ff2mpvEnabled = storage.defineItem<boolean>('local:ff2mpv', { fallback: false });
|
private static ff2mpvEnabled = new Setting<boolean>('local:ff2mpv', false);
|
||||||
|
|
||||||
static async getEnabled() {
|
static getEnabled = () => this.ff2mpvEnabled.get();
|
||||||
return await this.ff2mpvEnabled.getValue();
|
static setEnabled = (enabled: boolean) => this.ff2mpvEnabled.set(enabled);
|
||||||
}
|
|
||||||
static async setEnabled(enabled: boolean) {
|
|
||||||
console.log('set', enabled);
|
|
||||||
await this.ff2mpvEnabled.setValue(enabled);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UrlReferer {
|
export interface PerDomainConfig {
|
||||||
private static temporaryUrlReferer = storage.defineItem<Record<string, string>>('local:temporaryUrlReferer', {
|
allDisabled: boolean;
|
||||||
fallback: {}
|
disabledHostIds: HostId[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerDomainSettings {
|
||||||
|
private static storage = new Setting<Record<string, PerDomainConfig>>('local:perDomainSettings', {});
|
||||||
|
|
||||||
|
private static ensureDomain = (val: Record<string, PerDomainConfig>, domain: string) => {
|
||||||
|
if (!val[domain]) val[domain] = { allDisabled: false, disabledHostIds: [] };
|
||||||
|
return val[domain];
|
||||||
|
};
|
||||||
|
|
||||||
|
static get = (domain: string): Promise<PerDomainConfig> =>
|
||||||
|
this.storage.get().then((val) => val[domain] ?? { allDisabled: false, disabledHostIds: [] });
|
||||||
|
|
||||||
|
static getMap = () => this.storage.get();
|
||||||
|
|
||||||
|
static setAllDisabled = (domain: string, disabled: boolean) =>
|
||||||
|
this.storage.update((val) => {
|
||||||
|
this.ensureDomain(val, domain).allDisabled = disabled;
|
||||||
|
return val;
|
||||||
});
|
});
|
||||||
|
|
||||||
static async addTemporary(hostname: string, referer: string) {
|
static setHostDisabled = (domain: string, hostId: HostId, disabled: boolean) =>
|
||||||
const tmpUrlReferer = await this.temporaryUrlReferer.getValue();
|
this.storage.update((val) => {
|
||||||
|
const entry = this.ensureDomain(val, domain);
|
||||||
|
if (!disabled) entry.disabledHostIds = entry.disabledHostIds.filter((id) => id !== hostId);
|
||||||
|
else if (!entry.disabledHostIds.includes(hostId)) entry.disabledHostIds.push(hostId);
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
tmpUrlReferer[hostname] = referer;
|
static add = (domain: string) =>
|
||||||
await this.temporaryUrlReferer.setValue(tmpUrlReferer);
|
this.storage.update((val) => {
|
||||||
}
|
this.ensureDomain(val, domain);
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
static async get(hostname: string) {
|
static clear = (domain: string) =>
|
||||||
const tmpUrlReferer = await this.temporaryUrlReferer.getValue();
|
this.storage.update((val) => {
|
||||||
|
delete val[domain];
|
||||||
|
return val;
|
||||||
|
});
|
||||||
|
|
||||||
return tmpUrlReferer[hostname] ?? null;
|
static cleanEmpty = () =>
|
||||||
|
this.storage.update((val) => {
|
||||||
|
for (const [domain, settings] of Object.entries(val)) {
|
||||||
|
if (!settings.allDisabled && settings.disabledHostIds.length === 0) delete val[domain];
|
||||||
}
|
}
|
||||||
|
return val;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -26,7 +26,10 @@ export default defineConfig({
|
|||||||
gecko_android: {}
|
gecko_android: {}
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
permissions: ['storage', ...(manifestVersion === 2 ? ['webRequest', 'webRequestBlocking', '<all_urls>'] : [])],
|
permissions: [
|
||||||
|
'storage',
|
||||||
|
...(manifestVersion === 2 ? ['scripting', 'webRequest', 'webRequestBlocking', '<all_urls>'] : [])
|
||||||
|
],
|
||||||
optional_permissions: ['nativeMessaging'],
|
optional_permissions: ['nativeMessaging'],
|
||||||
web_accessible_resources: [
|
web_accessible_resources: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user