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 | ||
|
|
652d0588fc | ||
|
|
a2a7132d0b | ||
|
|
0696165932 | ||
|
|
8cb9a83b2c | ||
|
|
f200312ad5 | ||
|
|
1cf4940453 | ||
|
|
67dbb9c685 | ||
|
|
f9a0197f7f | ||
|
|
d8bb70890a | ||
|
|
45900b2e86 | ||
|
|
3058847f31 | ||
|
|
082f30722c |
+14
-10
@@ -10,31 +10,35 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
version: 11
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 26
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build Firefox (mv2)
|
||||
run: npm run build:firefox
|
||||
run: pnpm run build:firefox
|
||||
|
||||
- name: Build Chrome (mv3)
|
||||
run: npm run build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Upload Firefox (mv2)
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: stream-bypass-mv2
|
||||
path: .output/firefox-mv2
|
||||
|
||||
- name: Upload Chrome (mv3)
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: stream-bypass-mv3
|
||||
path: .output/chrome-mv3
|
||||
|
||||
@@ -2,6 +2,10 @@ name: lint
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags-ignore:
|
||||
- '*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
@@ -9,16 +13,20 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
version: 11
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 26
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
run: pnpm run lint
|
||||
|
||||
@@ -19,28 +19,56 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@008330803749db0355799c700092d9a85fd074e9 # v6.0.9
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
version: 11
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: 26
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Zip extensions
|
||||
run: |
|
||||
npm run zip:firefox
|
||||
npm run zip
|
||||
pnpm run zip:firefox
|
||||
pnpm run zip
|
||||
|
||||
- name: Resolve zip extension paths
|
||||
id: paths
|
||||
run: |
|
||||
MV2_PATH=$(find .output -type f -name "*-mv2.zip")
|
||||
MV2_NAME=$(basename $MV2_PATH)
|
||||
MV3_PATH=$(find .output -type f -name "*-mv3.zip")
|
||||
MV3_NAME=$(basename $MV3_PATH)
|
||||
echo "mv2_path=$MV2_PATH" >> "$GITHUB_OUTPUT"
|
||||
echo "mv2_name=$MV2_NAME" >> "$GITHUB_OUTPUT"
|
||||
echo "mv3_path=$MV3_PATH" >> "$GITHUB_OUTPUT"
|
||||
echo "mv3_name=$MV3_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload mv2 as artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ steps.paths.outputs.mv2_name }}
|
||||
path: ${{ steps.paths.outputs.mv2_path }}
|
||||
|
||||
- name: Upload mv3 as artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ steps.paths.outputs.mv3_name }}
|
||||
path: ${{ steps.paths.outputs.mv3_path }}
|
||||
|
||||
- name: Upload to latest release
|
||||
if: github.event.inputs.dry_run != 'true'
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
uses: svenstaro/upload-release-action@ac75e1407b33421f1368f08e0e6cc567cc4edd25 # 2.11.5
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: .output/*.zip
|
||||
file: .output/*mv*.zip
|
||||
tag: ${{ github.ref }}
|
||||
overwrite: true
|
||||
file_glob: true
|
||||
@@ -56,6 +84,6 @@ jobs:
|
||||
CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||
CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||
run: |
|
||||
npx wxt submit \
|
||||
pnpm exec wxt submit \
|
||||
--firefox-zip .output/*-mv2.zip --firefox-sources-zip .output/*-sources.zip \
|
||||
--chrome-zip .output/*-mv3.zip
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
.zed
|
||||
|
||||
node_modules
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
.output
|
||||
.wxt
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ A multi-browser addon / extension for multiple streaming providers which redirec
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/bytedream/stream-bypass/releases/latest">
|
||||
<img src="https://img.shields.io/github/v/release/ByteDream/stream-bypass?label=Version&style=flat-square" alt="Version">
|
||||
<img src="https://img.shields.io/github/v/release/bytedream/stream-bypass?label=Version&style=flat-square" alt="Version">
|
||||
</a>
|
||||
<a href="https://addons.mozilla.org/de/firefox/addon/stream-bypass/">
|
||||
<img src="https://img.shields.io/amo/users/stream-bypass?label=Firefox%20Users&style=flat-square" alt="Firefox Addon Store">
|
||||
@@ -13,7 +13,7 @@ A multi-browser addon / extension for multiple streaming providers which redirec
|
||||
<img src="https://img.shields.io/chrome-web-store/users/ddfpfjomnakfckhmilacnbokdaknamdb?style=flat-square&label=Chrome%20Users" alt="Chrome Store">
|
||||
</a>
|
||||
<a href="https://github.com/bytedream/stream-bypass/releases/latest">
|
||||
<img src="https://img.shields.io/github/downloads/ByteDream/stream-bypass/total?label=GitHub%20Downloads&style=flat-square" alt="GitHub Downloads">
|
||||
<img src="https://img.shields.io/github/downloads/bytedream/stream-bypass/total?label=GitHub%20Downloads&style=flat-square" alt="GitHub Downloads">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -54,13 +54,18 @@ The best way to install the extension are the official browser extension stores:
|
||||
- [Chrome Web Store](https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb)
|
||||
|
||||
<details>
|
||||
<summary><h3>Manual installation</h3></summary>
|
||||
<summary><h3 id="manual-installation1">Manual installation</h3></summary>
|
||||
|
||||
- Firefox (mv2)
|
||||
- Download `stream-bypass-<version>-mv2.zip` from the [latest release](https://github.com/ByteDream/stream-bypass/releases/latest) and unzip it (with [7zip](https://www.7-zip.org/) or something like that)
|
||||
- Download `stream-bypass-<version>-mv2.zip` from the [latest release](https://github.com/bytedream/stream-bypass/releases/latest) and unzip it (e.g. with [7zip](https://www.7-zip.org/))
|
||||
- Go into your browser and type `about:debugging#/runtime/this-firefox` in the address bar
|
||||
- Click the `Load Temporary Add-on...` button and choose the `manifest.json` file in the unzipped directory
|
||||
- Chromium / Google Chrome (mv3) > As nearly every browser other than Firefox is based on Chromium, this should be the same for most of them - Download `stream-bypass-<version>-mv3.zip` from the [latest release](https://github.com/ByteDream/stream-bypass/releases/latest) and unzip it (with [7zip](https://www.7-zip.org/) or something like that) - Go into your browser and type `chrome://extensions` in the address bar - Turn on the developer mode by checking the switch in the top right corner - Click `Load unpacked` and choose the unzipped directory
|
||||
- Chromium / Google Chrome (mv3)
|
||||
> As nearly every browser other than Firefox is based on Chromium, this should be the same for most of them
|
||||
- Download `stream-bypass-<version>-mv3.zip` from the [latest release](https://github.com/bytedream/stream-bypass/releases/latest) and unzip it (e.g. [7zip](https://www.7-zip.org/))
|
||||
- Go into your browser and type `chrome://extensions` in the address bar
|
||||
- Turn on the developer mode by checking the switch in the top right corner
|
||||
- Click `Load unpacked` and choose the unzipped directory
|
||||
|
||||
</details>
|
||||
|
||||
@@ -73,7 +78,7 @@ The best way to install the extension are the official browser extension stores:
|
||||
| --------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------ | ------------------------- |
|
||||
| Replace site-speicifc video player with browser native video player | ✔ | ✔ | ✔ |
|
||||
| Support websites that are accessed via a redirect | ✔ | ✖ | ✔ |
|
||||
| Open video in mpv (with [ff2mpv](https://github.com/ByteDream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams)) | ✔ | ✔ | ✖ |
|
||||
| Open video in mpv (with [ff2mpv](https://github.com/bytedream/stream-bypass/tree/master#ff2mpv-use-mpv-to-directly-play-streams)) | ✔ | ✔ | ✖ |
|
||||
|
||||
## 📜 Supported websites
|
||||
|
||||
@@ -81,8 +86,8 @@ The best way to install the extension are the official browser extension stores:
|
||||
- ⚠: Works with limitations.
|
||||
- ✖: 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) | ✔ | ✔ |
|
||||
| [doodstream.com](doodstream.com) / [dood.pm](https://dood.pm) | ✔ | ⚠ (redirect probably required) |
|
||||
| [filemoon.to](https://filemoon.to) | ✔ | ✔ |
|
||||
@@ -113,25 +118,27 @@ If you want to build the addon from source and not using the way described in [i
|
||||
|
||||
Requirements:
|
||||
|
||||
- `npm` installed.
|
||||
- `pnpm` installed.
|
||||
- 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:
|
||||
|
||||
```shell
|
||||
# install all dependencies
|
||||
$ npm install
|
||||
$ pnpm i
|
||||
|
||||
# build the extension source to the dist/ directory
|
||||
$ npm run build
|
||||
# build the extension and start it in a new firefox instance
|
||||
$ pnpm run dev:firefox
|
||||
|
||||
# same as build + more optimizations and browser specific settings at release/
|
||||
$ npm run release:firefox # or "release:chrome" to create a release for chromium based browsers
|
||||
# build the extension with optimizations to the .output/firefox-mv2 directory
|
||||
$ pnpm run build:firefox
|
||||
```
|
||||
|
||||
You can omit the `:firefox` suffix, then it's built for Chrome.
|
||||
|
||||
##### Install
|
||||
|
||||
If you want to use the addon in Chromium or any browser which is based on it, follow the steps in [installation](#-installation).
|
||||
If you want to use the addon in Chromium or any browser which is based on it, follow the steps in the [manual installation](#-installation).
|
||||
When using firefox, use the following:
|
||||
|
||||
1. Type `about:debugging` in the browser's address bar.
|
||||
@@ -141,11 +148,15 @@ When using firefox, use the following:
|
||||
|
||||
## ⚙️ 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ export default defineConfig(
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'no-alert': 'error',
|
||||
'no-console': 'error',
|
||||
'no-undef': 'off'
|
||||
}
|
||||
}
|
||||
|
||||
Generated
-8355
File diff suppressed because it is too large
Load Diff
+23
-23
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "stream-bypass",
|
||||
"version": "4.0.0",
|
||||
"version": "5.0.0",
|
||||
"displayName": "Stream Bypass",
|
||||
"author": "bytedream",
|
||||
"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",
|
||||
"zip": "wxt zip",
|
||||
"zip:firefox": "wxt zip -b firefox",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"postinstall": "wxt prepare",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint ."
|
||||
"lint": "prettier --check . && eslint . && svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.4.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
||||
"@eslint/compat": "^2.1.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
||||
"@steeze-ui/heroicons": "^2.4.2",
|
||||
"@steeze-ui/svelte-icon": "^1.6.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"@tsconfig/svelte": "^5.0.5",
|
||||
"@wxt-dev/module-svelte": "^2.0.4",
|
||||
"eslint": "^9.38.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.2.0",
|
||||
"@tailwindcss/vite": "^4.3.2",
|
||||
"@tsconfig/svelte": "^5.0.8",
|
||||
"@wxt-dev/module-svelte": "^2.0.5",
|
||||
"eslint": "^10.6.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.12.5",
|
||||
"hls.js": "^1.6.13",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"sass": "^1.93.2",
|
||||
"svelte": "^5.43.2",
|
||||
"svelte-check": "^4.3.3",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"eslint-plugin-svelte": "^3.20.0",
|
||||
"hls.js": "^1.6.16",
|
||||
"prettier": "^3.9.4",
|
||||
"prettier-plugin-svelte": "^4.1.1",
|
||||
"sass": "^1.101.0",
|
||||
"svelte": "^5.56.4",
|
||||
"svelte-check": "^4.7.2",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.2",
|
||||
"vite": "^7.1.12",
|
||||
"web-ext": "^9.1.0",
|
||||
"wxt": "^0.20.11"
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.63.0",
|
||||
"vite": "^8.1.4",
|
||||
"web-ext": "^10.4.0",
|
||||
"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,19 +1,96 @@
|
||||
import { UrlReferer } from '@/lib/settings';
|
||||
import { BackgroundMessageData, BackgroundMessageType } from '@/lib/communication';
|
||||
import { getHost, hosts } from '@/lib/host';
|
||||
import { HostSettings } from '@/lib/settings';
|
||||
|
||||
export default defineBackground(() => {
|
||||
browser.runtime.onMessage.addListener(async (message) => {
|
||||
if (message.action == 'ff2mpv') {
|
||||
await browser.runtime.sendNativeMessage('ff2mpv', { url: message.url });
|
||||
const temporaryReferers: Record<string, string> = {};
|
||||
const allHostDomains = hosts.flatMap((host) => host.domains);
|
||||
|
||||
function domainToMatch(domain: string) {
|
||||
return `*://${domain}/*`;
|
||||
}
|
||||
|
||||
function domainsToMatch(domains: string[]) {
|
||||
return domains.map(domainToMatch);
|
||||
}
|
||||
|
||||
async function reInjectContentScript(domain: string) {
|
||||
const contentScript: Browser.scripting.RegisteredContentScript = {
|
||||
id: 'temporary-hosts',
|
||||
js: ['content-scripts/content.js'],
|
||||
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;
|
||||
}
|
||||
|
||||
// the following listener is only available in mv2
|
||||
if (import.meta.env.MANIFEST_VERSION === 3) return;
|
||||
return response;
|
||||
}
|
||||
function registerBrowserRuntimeOnMessageListener() {
|
||||
browser.runtime.onMessage.addListener(browserRuntimeOnMessageListener);
|
||||
}
|
||||
|
||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
async (details) => {
|
||||
const referer = await UrlReferer.get(new URL(details.url).hostname);
|
||||
function browserWebRequestOnBeforeSendHeadersListener(
|
||||
details: Browser.webRequest.OnBeforeSendHeadersDetails
|
||||
): Browser.webRequest.BlockingResponse | undefined {
|
||||
const referer = temporaryReferers[new URL(details.url).host];
|
||||
if (!referer) return;
|
||||
|
||||
details.requestHeaders!.push({
|
||||
@@ -22,9 +99,53 @@ export default defineBackground(() => {
|
||||
});
|
||||
|
||||
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']
|
||||
);
|
||||
}
|
||||
|
||||
async function browserWebRequestOnBeforeRedirectListener(details: Browser.webRequest.OnBeforeRedirectDetails) {
|
||||
const domain = new URL(details.url).host;
|
||||
const host = await getHost(domain);
|
||||
if (!host) return;
|
||||
|
||||
const newDomain = new URL(details.redirectUrl).host;
|
||||
|
||||
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,21 +1,28 @@
|
||||
import { getHost, hosts, type Host, type HostMatch } from '@/lib/host';
|
||||
import { FF2MPVSettings } from '@/lib/settings';
|
||||
import { BackgroundMessageType, sendBackgroundMessage } from '@/lib/communication';
|
||||
import { getHost, HostMatchType, hosts, type HostMatch } from '@/lib/host';
|
||||
import { FF2MPVSettings, PerDomainSettings } from '@/lib/settings';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: [
|
||||
...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>'] : [])
|
||||
],
|
||||
matches: [...Object.values(hosts).flatMap((h) => h.domains.map((d) => `*://${d}/*`))],
|
||||
allFrames: true,
|
||||
runAt: 'document_end',
|
||||
main
|
||||
});
|
||||
|
||||
async function main() {
|
||||
let host: Host | null;
|
||||
if ((host = await getHost(window.location.host)) === null) {
|
||||
return;
|
||||
const host = await getHost(window.location.host);
|
||||
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;
|
||||
@@ -24,7 +31,7 @@ async function main() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (re === null) {
|
||||
if (!re) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,7 +49,7 @@ async function main() {
|
||||
await browser.runtime.sendMessage({ action: 'ff2mpv', url: hostMatch.url });
|
||||
}
|
||||
|
||||
if (host.replace && hostMatch.type != 'hls') {
|
||||
if (host.replace && hostMatch.type != HostMatchType.HLS) {
|
||||
// this destroys all intervals that may spawn popups or events
|
||||
let intervalId = window.setInterval(() => {}, 0);
|
||||
while (intervalId--) {
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
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 { UrlReferer } from '@/lib/settings';
|
||||
|
||||
async function playNative(url: string, domain: string, videoElem: HTMLVideoElement) {
|
||||
// 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;
|
||||
}
|
||||
@@ -18,7 +26,11 @@ async function playHls(url: string, domain: string, videoElem: HTMLVideoElement)
|
||||
enableWorker: false,
|
||||
xhrSetup: async (xhr: XMLHttpRequest, url: string) => {
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
@@ -56,7 +68,7 @@ export async function play(videoElem: HTMLVideoElement) {
|
||||
|
||||
function initCommunication(id: string, url: string, domain: string) {
|
||||
const notifyActiveMatch = () =>
|
||||
sendMessage(MessageType.NotifyActiveMatch, {
|
||||
sendTabBroadcast(TabMessageType.NotifyActiveMatch, {
|
||||
id: id,
|
||||
url: url,
|
||||
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
|
||||
// data
|
||||
const cancel = listenMessages((type) => {
|
||||
if (type !== MessageType.RequestActiveMatch) return;
|
||||
const cancel = listenTabMessages((type) => {
|
||||
if (type !== TabMessageType.RequestActiveMatch) return;
|
||||
notifyActiveMatch();
|
||||
});
|
||||
window.onbeforeunload = cancel;
|
||||
|
||||
@@ -2,32 +2,46 @@
|
||||
import '@/assets/base.css';
|
||||
|
||||
import { fly } from 'svelte/transition';
|
||||
import Toast from '@/entrypoints/popup/components/Toast.svelte';
|
||||
import Main from '@/entrypoints/popup/pages/main/Main.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';
|
||||
|
||||
/* state init */
|
||||
browser.runtime.getPlatformInfo().then((info) => ($isMobile = info.os === 'android'));
|
||||
|
||||
/* types */
|
||||
type Page = 'main' | 'settings';
|
||||
type Page = 'main' | 'settings' | 'site-config';
|
||||
|
||||
/* states */
|
||||
let activePage = $state<Page>('main');
|
||||
</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'}
|
||||
<div transition:fly={{ x: -300, duration: 150 }} class="min-w-full w-full h-[300px] flex-1 flex flex-col">
|
||||
<Main onSettingsOpenRequest={() => (activePage = 'settings')} />
|
||||
<div transition:fly={{ x: -300, duration: 150 }} class="min-w-full w-full h-80 flex-1 flex flex-col">
|
||||
<Main
|
||||
onSettingsOpenRequest={() => (activePage = 'settings')}
|
||||
onSiteConfigOpenRequest={() => (activePage = 'site-config')}
|
||||
/>
|
||||
</div>
|
||||
{:else if activePage === 'settings'}
|
||||
<div
|
||||
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}
|
||||
>
|
||||
<Settings onSettingsCloseRequest={() => (activePage = 'main')} />
|
||||
</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}
|
||||
</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;
|
||||
onChecked?: (checked: boolean) => void | boolean | Promise<void> | Promise<boolean>;
|
||||
size?: 'sm' | 'md';
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/* 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));
|
||||
|
||||
/* callbacks */
|
||||
async function onInputChange() {
|
||||
if (disabled) return;
|
||||
internalChecked = !internalChecked;
|
||||
let approved = false;
|
||||
|
||||
@@ -36,9 +39,21 @@
|
||||
}
|
||||
</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">
|
||||
<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
|
||||
class="block rounded-full box bg-red-700 peer-checked:bg-linux-mint-green"
|
||||
class:w-8={size === 'sm'}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<script lang="ts"></script>
|
||||
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<p class="text-[1.05rem]">Extension disabled</p>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Clipboard, InformationCircle } from '@steeze-ui/heroicons';
|
||||
import { Icon } from '@steeze-ui/svelte-icon';
|
||||
import { isMobile } from '@/entrypoints/popup/state';
|
||||
import { isMobile, showToast } from '@/entrypoints/popup/state';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
@@ -28,8 +28,13 @@
|
||||
}
|
||||
|
||||
/* callbacks */
|
||||
function copyUrl() {
|
||||
navigator.clipboard.writeText(urlOutput);
|
||||
async function copyUrl() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(urlOutput);
|
||||
showToast('URL copied');
|
||||
} catch {
|
||||
showToast('Failed to copy');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -47,7 +52,7 @@
|
||||
<div class="relative group h-4 flex justify-center items-center">
|
||||
<button class="text-sm peer"><Icon src={InformationCircle} size="1rem" /></button>
|
||||
<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
|
||||
the url</span
|
||||
>
|
||||
@@ -57,7 +62,7 @@
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<div class="relative group w-full" tabindex={$isMobile ? 0 : undefined}>
|
||||
<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
|
||||
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">
|
||||
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 { HostSettings } from '@/lib/settings';
|
||||
|
||||
@@ -32,15 +35,17 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex justify-between items-center p-2">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<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">
|
||||
<PageHeader title="stream-bypass" subtitle="v{import.meta.env.VERSION}">
|
||||
{#snippet actionsRight()}
|
||||
{#key allHostsDisabled}
|
||||
<Toggle bind:checked={() => !allHostsDisabled, (v) => (allHostsDisabled = !v)} />
|
||||
{/key}
|
||||
<button class="font-bold cursor-pointer" onclick={() => onSettingsClick()}>⋮</button>
|
||||
</div>
|
||||
</div>
|
||||
<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={() => onSettingsClick()}
|
||||
>
|
||||
<Icon src={Cog6Tooth} size="1.1rem" />
|
||||
</button>
|
||||
{/snippet}
|
||||
</PageHeader>
|
||||
|
||||
@@ -3,63 +3,92 @@
|
||||
|
||||
import { onDestroy } from '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 CurrentSite from '@/entrypoints/popup/pages/main/CurrentSite.svelte';
|
||||
import Header from '@/entrypoints/popup/pages/main/Header.svelte';
|
||||
import Match from '@/entrypoints/popup/pages/main/Match.svelte';
|
||||
import NoMatch from '@/entrypoints/popup/pages/main/NoMatch.svelte';
|
||||
import { listenMessages, MessageType, sendMessageToActiveTab } from '@/lib/communication';
|
||||
import { listenTabMessages, sendTabMessageToActiveTab, TabMessageType } from '@/lib/communication';
|
||||
import { hosts, type Host } from '@/lib/host';
|
||||
import { HostSettings } from '@/lib/settings';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
onSettingsOpenRequest: () => void;
|
||||
onSiteConfigOpenRequest: () => void;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { onSettingsOpenRequest }: Props = $props();
|
||||
let { onSettingsOpenRequest, onSiteConfigOpenRequest }: Props = $props();
|
||||
let currentMatch = $state<{ host: Host; url: string; domain: string } | null>(null);
|
||||
let currentDomain = $state<string | null>(null);
|
||||
|
||||
let allHostsDisabled = $state(false);
|
||||
HostSettings.getAllHostsDisabled().then((val) => (allHostsDisabled = val));
|
||||
|
||||
/* lifecycle */
|
||||
const cancel = listenMessages((type, data) => {
|
||||
if (type !== MessageType.NotifyActiveMatch) return;
|
||||
currentMatch = {
|
||||
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;
|
||||
});
|
||||
sendMessageToActiveTab(MessageType.RequestActiveMatch, undefined);
|
||||
|
||||
onDestroy(cancel);
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="w-full shrink-0">
|
||||
<Header bind:allHostsDisabled onSettingsClick={onSettingsOpenRequest} />
|
||||
<Divider />
|
||||
</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}
|
||||
<AllDisabled />
|
||||
{:else if !currentMatch}
|
||||
<NoMatch />
|
||||
{:else}
|
||||
<div class="flex flex-col justify-between h-full pb-2">
|
||||
<Match host={currentMatch.host} domain={currentMatch.domain} />
|
||||
<div class="divider border-dashed"></div>
|
||||
<div class="mt-2">
|
||||
<Notice
|
||||
level="warning"
|
||||
title="Extension disabled"
|
||||
description="Configurations are still saved and take effect the when extension is re-enabled."
|
||||
/>
|
||||
{:else if currentMatch}
|
||||
<div class="flex flex-col justify-between h-full px-2">
|
||||
<Match host={currentMatch.host} />
|
||||
<CopyMatch url={currentMatch.url} domain={currentMatch.domain} />
|
||||
</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>
|
||||
{/if}
|
||||
</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>
|
||||
* {
|
||||
@apply select-none;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,17 +4,13 @@
|
||||
/* types */
|
||||
interface Props {
|
||||
host: Host;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
/* states */
|
||||
const { host, domain }: Props = $props();
|
||||
const { host }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center justify-center w-full h-full">
|
||||
<p class="text-lg">Match found:</p>
|
||||
<div class="[&>*]:select-text">
|
||||
<span class="underline text-green-400 text-2xl font-bold">{host.name}</span>
|
||||
<span class="text-xs text-slate-300">({domain})</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<p class="text-md text-gray-400">Match found:</p>
|
||||
<span class="underline text-green-400 text-xl font-bold select-text">{host.name}</span>
|
||||
</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">
|
||||
import { showToast } from '../../state';
|
||||
import { InformationCircle } from '@steeze-ui/heroicons';
|
||||
import { Icon } from '@steeze-ui/svelte-icon';
|
||||
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||
@@ -9,21 +10,28 @@
|
||||
FF2MPVSettings.getEnabled().then((val) => (enabled = val));
|
||||
|
||||
/* callbacks */
|
||||
function onEnableChange(enabled: boolean) {
|
||||
async function onEnableChange(enabled: boolean) {
|
||||
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>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative mr-3">
|
||||
<span>Communication enabled</span>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<p class="text-sm font-semibold text-gray-100">Communication enabled</p>
|
||||
<a
|
||||
class="absolute -top-1 -right-4 text-sm"
|
||||
href="https://github.com/bytedream/stream-bypass/tree/main?tab=readme-ov-file#ff2mpv-use-mpv-to-directly-play-streams"
|
||||
target="_blank"><Icon src={InformationCircle} size="1rem" /></a
|
||||
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"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon src={InformationCircle} size="0.85rem" />
|
||||
<span>Learn more</span>
|
||||
</a>
|
||||
</div>
|
||||
{#key enabled}
|
||||
<Toggle
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
<script lang="ts">
|
||||
import BackButton from '@/entrypoints/popup/components/BackButton.svelte';
|
||||
import PageHeader from '@/entrypoints/popup/components/PageHeader.svelte';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
onBackClick: () => void;
|
||||
@@ -8,9 +11,8 @@
|
||||
let { onBackClick }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex justify-between items-center p-2">
|
||||
<div class="flex items-baseline gap-2">
|
||||
<button class="cursor-pointer" onclick={() => onBackClick()}>←</button>
|
||||
<h1>Settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
<PageHeader title="Settings" subtitle="Manage hosts and integrations">
|
||||
{#snippet actionsLeft()}
|
||||
<BackButton onclick={onBackClick} />
|
||||
{/snippet}
|
||||
</PageHeader>
|
||||
|
||||
@@ -1,41 +1,47 @@
|
||||
<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 { hosts } from '@/lib/host';
|
||||
import { filterHosts, hosts } from '@/lib/host';
|
||||
import { HostSettings } from '@/lib/settings';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
searchQuery: string;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { searchQuery = $bindable('') }: Props = $props();
|
||||
let disabledHostIds = $state<Array<string>>([]);
|
||||
HostSettings.getDisabledHosts().then((val) => (disabledHostIds = val));
|
||||
|
||||
let filtered = $derived(filterHosts(hosts, searchQuery));
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-[35%_43%_22%] gap-y-0.75">
|
||||
<p class="font-bold">Host</p>
|
||||
<p class="font-bold">Domains</p>
|
||||
<p class="font-bold">Enabled</p>
|
||||
{#each hosts as host (host.id)}
|
||||
{@const domainList = host.domains.join(', ')}
|
||||
<p>{host.name}</p>
|
||||
<div>
|
||||
<label for={host.id}>
|
||||
<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]">
|
||||
<div class="flex flex-col gap-2 max-h-full">
|
||||
<SearchInput bind:value={searchQuery} placeholder="Filter hosts…" />
|
||||
{#if filtered.length === 0}
|
||||
<p class="text-xs text-gray-400 text-center py-2">No hosts match "{searchQuery}"</p>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y divide-gray-700/40 -mx-1 max-h-full overflow-y-scroll">
|
||||
{#each filtered as host (host.id)}
|
||||
<HostRow {host} enabled={!disabledHostIds.includes(host.id)}>
|
||||
{#snippet actions()}
|
||||
{#key disabledHostIds}
|
||||
<Toggle
|
||||
bind:checked={
|
||||
() => !disabledHostIds.includes(host.id),
|
||||
(v) => (v ? HostSettings.removeDisabledHost(host) : HostSettings.addDisabledHost(host))
|
||||
(v) =>
|
||||
v
|
||||
? HostSettings.removeDisabledHost(host.id)
|
||||
: HostSettings.addDisabledHost(host.id)
|
||||
}
|
||||
size="sm"
|
||||
></Toggle>
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{/snippet}
|
||||
</HostRow>
|
||||
{/each}
|
||||
</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">
|
||||
import { CommandLine, MapPin, ServerStack } from '@steeze-ui/heroicons';
|
||||
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 Header from '@/entrypoints/popup/pages/settings/Header.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 { PerDomainSettings } from '@/lib/settings';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
@@ -12,49 +16,38 @@
|
||||
|
||||
/* states */
|
||||
let { onSettingsCloseRequest }: Props = $props();
|
||||
let searchQuery = $state('');
|
||||
|
||||
onDestroy(PerDomainSettings.cleanEmpty);
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<Header onBackClick={onSettingsCloseRequest} />
|
||||
<Divider />
|
||||
</div>
|
||||
<div class="flex flex-col gap-y-1 pt-1 h-full mx-2 my-1 overflow-y-scroll">
|
||||
<details class="details" open>
|
||||
<summary>Hosts</summary>
|
||||
<HostsTable />
|
||||
</details>
|
||||
<div class="flex flex-col gap-2 p-2 h-full overflow-y-scroll">
|
||||
<div class="max-h-full">
|
||||
<Section
|
||||
title="Hosts"
|
||||
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}
|
||||
<details class="details">
|
||||
<summary>ff2mpv</summary>
|
||||
<Section title="ff2mpv" description="Play streams directly in mpv via native messaging" icon={CommandLine}>
|
||||
<Ff2mpv />
|
||||
</details>
|
||||
</Section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
* {
|
||||
@apply 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;
|
||||
}
|
||||
user-select: none;
|
||||
}
|
||||
</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';
|
||||
|
||||
/* --- global init --- */
|
||||
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 {
|
||||
RequestActiveMatch,
|
||||
// --- tab communication --- //
|
||||
export enum TabMessageType {
|
||||
RequestActiveMatch = 10,
|
||||
NotifyActiveMatch
|
||||
}
|
||||
|
||||
export type MessageData<T extends MessageType> = {
|
||||
[MessageType.RequestActiveMatch]: undefined;
|
||||
[MessageType.NotifyActiveMatch]: {
|
||||
export type TabMessageData<T extends TabMessageType> = {
|
||||
[TabMessageType.RequestActiveMatch]: undefined;
|
||||
[TabMessageType.NotifyActiveMatch]: {
|
||||
id: string;
|
||||
url: string;
|
||||
domain: string;
|
||||
};
|
||||
}[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 });
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
await browser.tabs.sendMessage(tabs[0].id!, { type: message, data: data }).catch(() => {});
|
||||
}
|
||||
|
||||
export function listenMessages(listener: (type: MessageType, data: any) => void): () => void {
|
||||
const callback = (callbackData: { type: MessageType; data: any }) => {
|
||||
export function listenTabMessages(listener: (type: TabMessageType, data: any) => void): () => void {
|
||||
const callback = (callbackData: { type: TabMessageType; data: any }) => {
|
||||
const { type, data } = callbackData;
|
||||
listener(type, data);
|
||||
};
|
||||
|
||||
browser.runtime.onMessage.addListener(callback);
|
||||
return () => browser.runtime.onMessage.removeListener(callback);
|
||||
}
|
||||
|
||||
return () => {
|
||||
browser.runtime.onMessage.removeListener(callback);
|
||||
// --- background script communication --- //
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export default {
|
||||
name: 'Doodstream',
|
||||
id: 'doodstream',
|
||||
domains: [
|
||||
'do7go.com',
|
||||
'doodstream.com',
|
||||
'dood.pm',
|
||||
'dood.ws',
|
||||
@@ -34,7 +35,7 @@ export default {
|
||||
referrer: `https://${window.location.host}/e/${window.location.pathname.split('/').slice(-1)[0]}`
|
||||
});
|
||||
return {
|
||||
type: HostMatchType.HLS,
|
||||
type: HostMatchType.NATIVE,
|
||||
url: `${await response.text()}1234567890${match[2]}${Date.now()}`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
|
||||
match: async function (match: RegExpMatchArray) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+35
-8
@@ -17,7 +17,7 @@ import Vidmoly from './vidmoly';
|
||||
import Vidoza from './vidoza';
|
||||
import Voe from './voe';
|
||||
import Vupload from './vupload';
|
||||
import { HostSettings } from '@/lib/settings';
|
||||
import { HostSettings, PerDomainSettings } from '@/lib/settings';
|
||||
|
||||
export enum HostMatchType {
|
||||
NATIVE = 'native',
|
||||
@@ -30,9 +30,11 @@ export interface HostMatch {
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
export type HostId = string;
|
||||
|
||||
export interface Host {
|
||||
name: string;
|
||||
id: string;
|
||||
id: HostId;
|
||||
domains: string[];
|
||||
replace?: boolean;
|
||||
regex: RegExp[];
|
||||
@@ -63,16 +65,41 @@ export const hosts = [
|
||||
Vupload
|
||||
];
|
||||
|
||||
export async function getHost(domain: string) {
|
||||
if (await HostSettings.getAllHostsDisabled()) return null;
|
||||
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> {
|
||||
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) {
|
||||
if (host.domains.includes(domain)) {
|
||||
if (!disabledIds.includes(host.id)) return host;
|
||||
else return null;
|
||||
// host is generally disabled via popup
|
||||
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) {
|
||||
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 {
|
||||
|
||||
@@ -3,13 +3,17 @@ import { HostMatchType, type Host } from '@/lib/host';
|
||||
export default {
|
||||
name: 'Vidmoly',
|
||||
id: 'vidmoly',
|
||||
domains: ['vidmoly.me', 'vidmoly.net', 'vidmoly.to'],
|
||||
regex: [/(?<=file:").+\.m3u8.*(?=")/gm],
|
||||
// vidmoly has multiple domains, but regardless of the domain, the actual videos are loaded via an iframe which
|
||||
// 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,
|
||||
// but said link doesn't always point the same video as the iframe
|
||||
domains: ['vidmoly.net', 'vidmoly.biz'],
|
||||
regex: [/file:\s?'(http.*)'/],
|
||||
|
||||
match: async function (match: RegExpMatchArray) {
|
||||
return {
|
||||
type: HostMatchType.HLS,
|
||||
url: match[0]
|
||||
url: match[1]
|
||||
};
|
||||
}
|
||||
} satisfies Host;
|
||||
|
||||
@@ -9,7 +9,7 @@ export default {
|
||||
|
||||
match: async function (match: RegExpMatchArray) {
|
||||
return {
|
||||
type: HostMatchType.HLS,
|
||||
type: HostMatchType.NATIVE,
|
||||
url: match[0]
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ export default {
|
||||
|
||||
match: async function (match: RegExpMatchArray) {
|
||||
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;
|
||||
} else {
|
||||
let json = match[0];
|
||||
|
||||
+98
-65
@@ -1,88 +1,121 @@
|
||||
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 {
|
||||
/* disabled hosts */
|
||||
private static disabledHosts = storage.defineItem<string[]>('local:disabledHosts', { fallback: [] });
|
||||
private static allHostsDisabled = storage.defineItem<boolean>('local:allHostsDisabled', { fallback: false });
|
||||
private static disabledHosts = new Setting<HostId[]>('local:disabledHosts', []);
|
||||
private static allHostsDisabled = new Setting<boolean>('local:allHostsDisabled', false);
|
||||
|
||||
static async addDisabledHost(host: Host) {
|
||||
const ids = await this.disabledHosts.getValue();
|
||||
|
||||
const index = ids.indexOf(host.id);
|
||||
if (index === -1) {
|
||||
ids.push(host.id);
|
||||
await this.disabledHosts.setValue(ids);
|
||||
}
|
||||
}
|
||||
static async removeDisabledHost(host: Host) {
|
||||
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 addDisabledHost = (hostId: HostId) =>
|
||||
this.disabledHosts.update((val) => {
|
||||
if (!val.includes(hostId)) val.push(hostId);
|
||||
return val;
|
||||
});
|
||||
static removeDisabledHost = (hostId: HostId) =>
|
||||
this.disabledHosts.update((val) => {
|
||||
const index = val.indexOf(hostId);
|
||||
if (index !== -1) val.splice(index, 1);
|
||||
return val;
|
||||
});
|
||||
|
||||
static async addTemporaryHostDomain(host: Host, domain: string) {
|
||||
const temporaryHostDomains = await this.temporaryHostDomain.getValue();
|
||||
static getDisabledHosts = () => this.disabledHosts.get();
|
||||
static getAllHostsDisabled = () => this.allHostsDisabled.get();
|
||||
static setAllHostsDisabled = (disabled: boolean) => this.allHostsDisabled.set(disabled);
|
||||
|
||||
temporaryHostDomains[domain] = host.id;
|
||||
await this.temporaryHostDomain.setValue(temporaryHostDomains);
|
||||
console.log(await this.temporaryHostDomain.getValue());
|
||||
}
|
||||
static async checkTemporaryHostDomain(domain: string) {
|
||||
const temporaryHostDomains = await this.temporaryHostDomain.getValue();
|
||||
/* tmp */
|
||||
private static temporaryHostDomain = new Setting<Record<string, string>>('local:temporaryHostDomain', {});
|
||||
|
||||
const hostId = temporaryHostDomains[domain];
|
||||
return hostId ? (hosts.find((host) => host.id === hostId) ?? null) : null;
|
||||
static addTemporaryHostDomain = async (
|
||||
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 {
|
||||
private static ff2mpvEnabled = storage.defineItem<boolean>('local:ff2mpv', { fallback: false });
|
||||
private static ff2mpvEnabled = new Setting<boolean>('local:ff2mpv', false);
|
||||
|
||||
static async getEnabled() {
|
||||
return await this.ff2mpvEnabled.getValue();
|
||||
}
|
||||
static async setEnabled(enabled: boolean) {
|
||||
console.log('set', enabled);
|
||||
await this.ff2mpvEnabled.setValue(enabled);
|
||||
}
|
||||
static getEnabled = () => this.ff2mpvEnabled.get();
|
||||
static setEnabled = (enabled: boolean) => this.ff2mpvEnabled.set(enabled);
|
||||
}
|
||||
|
||||
export class UrlReferer {
|
||||
private static temporaryUrlReferer = storage.defineItem<Record<string, string>>('local:temporaryUrlReferer', {
|
||||
fallback: {}
|
||||
export interface PerDomainConfig {
|
||||
allDisabled: boolean;
|
||||
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) {
|
||||
const tmpUrlReferer = await this.temporaryUrlReferer.getValue();
|
||||
static setHostDisabled = (domain: string, hostId: HostId, disabled: boolean) =>
|
||||
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;
|
||||
await this.temporaryUrlReferer.setValue(tmpUrlReferer);
|
||||
}
|
||||
static add = (domain: string) =>
|
||||
this.storage.update((val) => {
|
||||
this.ensureDomain(val, domain);
|
||||
return val;
|
||||
});
|
||||
|
||||
static async get(hostname: string) {
|
||||
const tmpUrlReferer = await this.temporaryUrlReferer.getValue();
|
||||
static clear = (domain: string) =>
|
||||
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: {}
|
||||
}
|
||||
: undefined,
|
||||
permissions: ['storage', ...(manifestVersion === 2 ? ['webRequest', 'webRequestBlocking', '<all_urls>'] : [])],
|
||||
permissions: [
|
||||
'storage',
|
||||
...(manifestVersion === 2 ? ['scripting', 'webRequest', 'webRequestBlocking', '<all_urls>'] : [])
|
||||
],
|
||||
optional_permissions: ['nativeMessaging'],
|
||||
web_accessible_resources: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user