Compare commits

...
42 Commits
Author SHA1 Message Date
bytedream 8e22e92f89 update dependencies 2026-07-10 16:23:16 +02:00
bytedream f7cfe41211 only listen to send headers and redirect request where it's necessary 2026-07-10 16:10:24 +02:00
bytedream d7e1cbfb48 update text 2026-07-10 14:38:08 +02:00
bytedream 3541c7d2f0 re-inject content script on 3XX redirects 2026-07-07 18:25:48 +02:00
bytedream df3c66a24c fix vidmoly 2026-07-07 17:36:04 +02:00
bytedream b929f0956d disallow alert 2026-07-07 17:36:04 +02:00
bytedream 4c9825ebd5 redirect dynamically via injected content script instead of checking
each request
2026-07-07 17:36:01 +02:00
bytedream 71593f6762 clean empty per site settings 2026-07-07 14:16:13 +02:00
bytedream ddcb13013a add option to disable all hosts via settings page 2026-07-07 14:09:41 +02:00
bytedream 8cf14fb17a update dependencies and version 2026-07-07 13:53:20 +02:00
bytedream 3e3e2f5e75 replace slop dash with normal minus 2026-07-07 13:51:51 +02:00
bytedream c5deca1b2c unify headers 2026-07-07 13:50:05 +02:00
bytedream c4fea538f0 fix per site settings being set for wrong domain 2026-07-07 13:34:49 +02:00
bytedream 0552df008d update popup main page layout 2026-07-07 13:32:51 +02:00
bytedream e437d5436a fix per domain disable not working if host is in iframe 2026-07-07 13:26:33 +02:00
bytedream c09cf1188d show popup when ff2mpv settings are requested 2026-07-07 12:31:53 +02:00
bytedream eef560f224 add per domain settings (#30) 2026-07-07 12:26:54 +02:00
bytedream 9857ba6416 update settings layout 2026-07-02 11:11:56 +02:00
bytedream c08e071c88 fix tailwind classes 2026-06-28 20:46:27 +02:00
bytedream dbec92375c add toast when copying url to clipboard 2026-06-28 20:22:35 +02:00
bytedream a1328e8908 use host id instead of host in settings 2026-06-28 20:20:37 +02:00
bytedream 0455b53992 add svelte check to lint script 2026-06-28 16:42:56 +02:00
bytedream c9f5b594ff cleanup settings 2026-06-28 16:37:52 +02:00
bytedreamandMistral AI 9639f148f9 add no-console rule and remove console.log calls
Co-authored-by: Mistral AI <mistral@mistral.ai>
2026-06-25 18:21:24 +02:00
bytedream 85f4a3b776 update pnpm lockfile and workspace settings to v11 2026-06-22 23:12:49 +02:00
bytedreamandMistral Vibe 30e079d665 add pnpm/action-setup to workflows
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-22 23:02:20 +02:00
bytedreamandMistral Vibe e499adb4ac update action node version
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-22 22:46:06 +02:00
bytedreamandMistral Vibe aa6827b059 update github actions to latest versions
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-22 15:46:33 +02:00
bytedreamandMistral Vibe 5a86625f3f switch to pnpm
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-22 15:24:00 +02:00
bytedream a44f71f056 fix incorrect mv version in readme 2026-06-22 14:49:03 +02:00
bytedream 652d0588fc update dependencies and version 2026-01-11 21:26:52 +01:00
bytedream a2a7132d0b format 2026-01-11 21:25:53 +01:00
bytedream 0696165932 fix vidoza 2026-01-11 21:20:18 +01:00
bytedream 8cb9a83b2c fix doodstream 2026-01-11 21:17:42 +01:00
bytedream f200312ad5 fix background listeners not working 2026-01-11 21:17:30 +01:00
bytedream 1cf4940453 minor refactor 2026-01-11 21:17:10 +01:00
bytedream 67dbb9c685 fix vidmoly 2026-01-08 00:50:00 +01:00
bytedream f9a0197f7f update readme 2025-11-08 23:24:25 +01:00
bytedream d8bb70890a update build instructions 2025-11-08 22:26:18 +01:00
bytedream 45900b2e86 do not upload sources to releases 2025-11-08 22:21:10 +01:00
bytedream 3058847f31 upload artifacts when publishing 2025-11-08 22:20:35 +01:00
bytedream 082f30722c do not lint on tag push 2025-11-08 21:52:46 +01:00
50 changed files with 7136 additions and 8715 deletions
+14 -10
View File
@@ -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
+15 -7
View File
@@ -2,6 +2,10 @@ name: lint
on: on:
push: push:
branches:
- '**'
tags-ignore:
- '*'
pull_request: pull_request:
jobs: jobs:
@@ -9,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
+39 -11
View File
@@ -19,28 +19,56 @@ 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
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 - 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/*.zip file: .output/*mv*.zip
tag: ${{ github.ref }} tag: ${{ github.ref }}
overwrite: true overwrite: true
file_glob: true file_glob: true
@@ -56,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
-4
View File
@@ -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
View File
@@ -4,7 +4,7 @@
.zed .zed
node_modules node_modules
package-lock.json pnpm-lock.yaml
.output .output
.wxt .wxt
+28 -17
View File
@@ -4,7 +4,7 @@ A multi-browser addon / extension for multiple streaming providers which redirec
<p align="center"> <p align="center">
<a href="https://github.com/bytedream/stream-bypass/releases/latest"> <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>
<a href="https://addons.mozilla.org/de/firefox/addon/stream-bypass/"> <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"> <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"> <img src="https://img.shields.io/chrome-web-store/users/ddfpfjomnakfckhmilacnbokdaknamdb?style=flat-square&label=Chrome%20Users" alt="Chrome Store">
</a> </a>
<a href="https://github.com/bytedream/stream-bypass/releases/latest"> <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> </a>
</p> </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) - [Chrome Web Store](https://chromewebstore.google.com/detail/ddfpfjomnakfckhmilacnbokdaknamdb)
<details> <details>
<summary><h3>Manual installation</h3></summary> <summary><h3 id="manual-installation1">Manual installation</h3></summary>
- Firefox (mv2) - 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 - 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 - 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> </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 | ✔ | ✔ | ✔ | | Replace site-speicifc video player with browser native video player | ✔ | ✔ | ✔ |
| Support websites that are accessed via a redirect | ✔ | ✖ | ✔ | | 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 ## 📜 Supported websites
@@ -81,8 +86,8 @@ 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) |
| [filemoon.to](https://filemoon.to) | ✔ | ✔ | | [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: 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 install $ pnpm i
# build the extension source to the dist/ directory # build the extension and start it in a new firefox instance
$ npm run build $ pnpm run dev:firefox
# same as build + more optimizations and browser specific settings at release/ # build the extension with optimizations to the .output/firefox-mv2 directory
$ npm run release:firefox # or "release:chrome" to create a release for chromium based browsers $ pnpm run build:firefox
``` ```
You can omit the `:firefox` suffix, then it's built for Chrome.
##### Install ##### 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: When using firefox, use the following:
1. Type `about:debugging` in the browser's address bar. 1. Type `about:debugging` in the browser's address bar.
@@ -141,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
+2
View File
@@ -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'
} }
} }
-8355
View File
File diff suppressed because it is too large Load Diff
+23 -23
View File
@@ -1,6 +1,6 @@
{ {
"name": "stream-bypass", "name": "stream-bypass",
"version": "4.0.0", "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": "^1.4.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.1", "@sveltejs/vite-plugin-svelte": "^7.2.0",
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.3.2",
"@tsconfig/svelte": "^5.0.5", "@tsconfig/svelte": "^5.0.8",
"@wxt-dev/module-svelte": "^2.0.4", "@wxt-dev/module-svelte": "^2.0.5",
"eslint": "^9.38.0", "eslint": "^10.6.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.5", "eslint-plugin-svelte": "^3.20.0",
"hls.js": "^1.6.13", "hls.js": "^1.6.16",
"prettier": "^3.6.2", "prettier": "^3.9.4",
"prettier-plugin-svelte": "^3.4.0", "prettier-plugin-svelte": "^4.1.1",
"sass": "^1.93.2", "sass": "^1.101.0",
"svelte": "^5.43.2", "svelte": "^5.56.4",
"svelte-check": "^4.3.3", "svelte-check": "^4.7.2",
"tailwindcss": "^4.1.16", "tailwindcss": "^4.3.2",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.9.3", "typescript": "^6.0.3",
"typescript-eslint": "^8.46.2", "typescript-eslint": "^8.63.0",
"vite": "^7.1.12", "vite": "^8.1.4",
"web-ext": "^9.1.0", "web-ext": "^10.4.0",
"wxt": "^0.20.11" "wxt": "^0.20.27"
} }
} }
+5724
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
allowBuilds:
'@parcel/watcher': true
esbuild: true
spawn-sync: false
+134 -13
View File
@@ -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(() => { 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({
@@ -22,9 +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']
); );
}
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();
}); });
+19 -12
View File
@@ -1,21 +1,28 @@
import { getHost, hosts, type Host, type HostMatch } from '@/lib/host'; import { BackgroundMessageType, sendBackgroundMessage } from '@/lib/communication';
import { FF2MPVSettings } from '@/lib/settings'; import { getHost, HostMatchType, hosts, type HostMatch } from '@/lib/host';
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
}); });
async function main() { async function main() {
let host: Host | null; const host = await getHost(window.location.host);
if ((host = await getHost(window.location.host)) === null) { if (!host) return;
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;
@@ -24,7 +31,7 @@ async function main() {
break; break;
} }
} }
if (re === null) { if (!re) {
return; return;
} }
@@ -42,7 +49,7 @@ async function main() {
await browser.runtime.sendMessage({ action: 'ff2mpv', url: hostMatch.url }); 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 // this destroys all intervals that may spawn popups or events
let intervalId = window.setInterval(() => {}, 0); let intervalId = window.setInterval(() => {}, 0);
while (intervalId--) { while (intervalId--) {
+19 -7
View File
@@ -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;
+19 -5
View File
@@ -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}
+18 -3
View File
@@ -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>
+14 -9
View File
@@ -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>
+48 -19
View File
@@ -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>
+19
View File
@@ -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
View File
@@ -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;
} }
+2 -1
View File
@@ -4,6 +4,7 @@ export default {
name: 'Doodstream', name: 'Doodstream',
id: 'doodstream', id: 'doodstream',
domains: [ domains: [
'do7go.com',
'doodstream.com', 'doodstream.com',
'dood.pm', 'dood.pm',
'dood.ws', 'dood.ws',
@@ -34,7 +35,7 @@ export default {
referrer: `https://${window.location.host}/e/${window.location.pathname.split('/').slice(-1)[0]}` referrer: `https://${window.location.host}/e/${window.location.pathname.split('/').slice(-1)[0]}`
}); });
return { return {
type: HostMatchType.HLS, type: HostMatchType.NATIVE,
url: `${await response.text()}1234567890${match[2]}${Date.now()}` url: `${await response.text()}1234567890${match[2]}${Date.now()}`
}; };
} }
+1 -1
View File
@@ -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;
} }
+35 -8
View File
@@ -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 async function getHost(domain: string) { export function getHostFromId(id: HostId): Host | null {
if (await HostSettings.getAllHostsDisabled()) return 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) { 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);
} }
+1 -1
View File
@@ -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 -3
View File
@@ -3,13 +3,17 @@ import { HostMatchType, type Host } from '@/lib/host';
export default { export default {
name: 'Vidmoly', name: 'Vidmoly',
id: 'vidmoly', id: 'vidmoly',
domains: ['vidmoly.me', 'vidmoly.net', 'vidmoly.to'], // vidmoly has multiple domains, but regardless of the domain, the actual videos are loaded via an iframe which
regex: [/(?<=file:").+\.m3u8.*(?=")/gm], // 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) { 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
View File
@@ -9,7 +9,7 @@ export default {
match: async function (match: RegExpMatchArray) { match: async function (match: RegExpMatchArray) {
return { return {
type: HostMatchType.HLS, type: HostMatchType.NATIVE,
url: match[0] url: match[0]
}; };
} }
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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: [
{ {