mirror of
https://github.com/bytedream/stream-bypass.git
synced 2025-12-17 09:10:45 +01:00
update
This commit is contained in:
30
src/entrypoints/background/index.ts
Normal file
30
src/entrypoints/background/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { UrlReferer } from '@/lib/settings';
|
||||
|
||||
export default defineBackground(() => {
|
||||
browser.runtime.onMessage.addListener(async (message) => {
|
||||
if (message.action == 'ff2mpv') {
|
||||
await browser.runtime.sendNativeMessage('ff2mpv', { url: message.url });
|
||||
}
|
||||
|
||||
// the following listener is only available in mv2
|
||||
if (import.meta.env.MANIFEST_VERSION === 3) return;
|
||||
|
||||
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);
|
||||
if (!referer) return;
|
||||
|
||||
details.requestHeaders!.push({
|
||||
name: 'Referer',
|
||||
value: `https://${referer}/`
|
||||
});
|
||||
|
||||
return { requestHeaders: details.requestHeaders };
|
||||
},
|
||||
{ urls: ['<all_urls>'], types: ['xmlhttprequest'] },
|
||||
['blocking', 'requestHeaders']
|
||||
);
|
||||
});
|
||||
});
|
||||
78
src/entrypoints/content/index.ts
Normal file
78
src/entrypoints/content/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { getHost, hosts, type Host, type HostMatch } from '@/lib/host';
|
||||
import { FF2MPVSettings } 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>'] : [])
|
||||
],
|
||||
allFrames: true,
|
||||
runAt: 'document_end',
|
||||
main
|
||||
});
|
||||
|
||||
async function main() {
|
||||
let host: Host | null;
|
||||
if ((host = await getHost(window.location.host)) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let re = null;
|
||||
for (const regex of host.regex) {
|
||||
if ((re = document.body.innerHTML.match(regex)) !== null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (re === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hostMatch: HostMatch | null;
|
||||
try {
|
||||
hostMatch = await host.match(re);
|
||||
} catch {
|
||||
hostMatch = null;
|
||||
}
|
||||
|
||||
if (!hostMatch || !hostMatch.url) return;
|
||||
|
||||
// send the url to the ff2mpv (https://github.com/woodruffw/ff2mpv) application
|
||||
if (await FF2MPVSettings.getEnabled()) {
|
||||
await browser.runtime.sendMessage({ action: 'ff2mpv', url: hostMatch.url });
|
||||
}
|
||||
|
||||
if (host.replace && hostMatch.type != 'hls') {
|
||||
// this destroys all intervals that may spawn popups or events
|
||||
let intervalId = window.setInterval(() => {}, 0);
|
||||
while (intervalId--) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
let timeoutId = window.setTimeout(() => {}, 0);
|
||||
while (timeoutId--) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
// clear completed document
|
||||
document.documentElement.innerHTML = '';
|
||||
|
||||
document.body.style.backgroundColor = '#131313';
|
||||
|
||||
// video player
|
||||
const player = document.createElement('video');
|
||||
player.style.width = '100%';
|
||||
player.style.height = '100%';
|
||||
player.controls = true;
|
||||
player.src = hostMatch.url;
|
||||
|
||||
// add video player to document body
|
||||
document.body.style.margin = '0';
|
||||
document.body.append(player);
|
||||
} else {
|
||||
window.location.assign(
|
||||
browser.runtime.getURL(
|
||||
`/player.html?id=${host.id}&url=${encodeURIComponent(hostMatch.url)}&domain=${window.location.hostname}&type=${hostMatch.type}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
27
src/entrypoints/player/Player.svelte
Normal file
27
src/entrypoints/player/Player.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { play } from './player';
|
||||
|
||||
let errorMessage: string | null = $state(null);
|
||||
|
||||
let videoElem: HTMLVideoElement;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await play(videoElem);
|
||||
videoElem.controls = true;
|
||||
} catch (e) {
|
||||
errorMessage = e as string;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_media_has_caption -->
|
||||
<video class="absolute top-0 left-0 w-full h-full m-0" bind:this={videoElem}></video>
|
||||
{#if errorMessage}
|
||||
<div class="h-full flex items-center justify-center text-center">
|
||||
<p>
|
||||
{errorMessage} <a class="underline" href="https://github.com/bytedream/stream-bypass/issues">here</a>.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
26
src/entrypoints/player/index.html
Normal file
26
src/entrypoints/player/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Stream Bypass</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#131313]">
|
||||
<script type="module">
|
||||
import { mount } from 'svelte';
|
||||
import Player from './Player.svelte';
|
||||
|
||||
mount(Player, {
|
||||
target: document.body
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
75
src/entrypoints/player/player.ts
Normal file
75
src/entrypoints/player/player.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import Hls from 'hls.js';
|
||||
import { listenMessages, MessageType, sendMessage } 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);
|
||||
|
||||
videoElem.src = url;
|
||||
}
|
||||
|
||||
async function playHls(url: string, domain: string, videoElem: HTMLVideoElement) {
|
||||
if (videoElem.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
videoElem.src = url;
|
||||
} else if (Hls.isSupported()) {
|
||||
const hls = new Hls({
|
||||
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);
|
||||
xhr.open('GET', url);
|
||||
}
|
||||
});
|
||||
hls.loadSource(url);
|
||||
hls.attachMedia(videoElem);
|
||||
} else {
|
||||
throw 'Failed to play m3u8 video (hls is not supported). Try again or create a new issue';
|
||||
}
|
||||
}
|
||||
|
||||
export async function play(videoElem: HTMLVideoElement) {
|
||||
const urlQuery = new URLSearchParams(window.location.search);
|
||||
const id = urlQuery.get('id') as string;
|
||||
const url = decodeURIComponent(urlQuery.get('url') as string);
|
||||
const domain = urlQuery.get('domain') as string;
|
||||
const type = urlQuery.get('type') as HostMatchType;
|
||||
|
||||
const host = hosts.find((host) => host.id === id);
|
||||
if (!host) {
|
||||
throw `Invalid id: ${id}. Please report this`;
|
||||
}
|
||||
document.title = `Stream Bypass (${domain})`;
|
||||
|
||||
initCommunication(id, url, domain);
|
||||
|
||||
switch (type) {
|
||||
case HostMatchType.NATIVE:
|
||||
await playNative(url, domain, videoElem);
|
||||
break;
|
||||
case HostMatchType.HLS:
|
||||
await playHls(url, domain, videoElem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function initCommunication(id: string, url: string, domain: string) {
|
||||
const notifyActiveMatch = () =>
|
||||
sendMessage(MessageType.NotifyActiveMatch, {
|
||||
id: id,
|
||||
url: url,
|
||||
domain: domain
|
||||
});
|
||||
|
||||
// if an extension popup is open, it will be notified that a match/player is now active
|
||||
notifyActiveMatch();
|
||||
|
||||
// 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;
|
||||
notifyActiveMatch();
|
||||
});
|
||||
window.onbeforeunload = cancel;
|
||||
}
|
||||
33
src/entrypoints/popup/App.svelte
Normal file
33
src/entrypoints/popup/App.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import '@/assets/base.css';
|
||||
|
||||
import { fly } from 'svelte/transition';
|
||||
import Main from '@/entrypoints/popup/pages/main/Main.svelte';
|
||||
import Settings from '@/entrypoints/popup/pages/settings/Settings.svelte';
|
||||
import { isMobile } from '@/entrypoints/popup/state.js';
|
||||
|
||||
/* state init */
|
||||
browser.runtime.getPlatformInfo().then((info) => ($isMobile = info.os === 'android'));
|
||||
|
||||
/* types */
|
||||
type Page = 'main' | 'settings';
|
||||
|
||||
/* states */
|
||||
let activePage = $state<Page>('main');
|
||||
</script>
|
||||
|
||||
<div class="flex w-[350px] overflow-hidden" class:w-screen={$isMobile}>
|
||||
{#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>
|
||||
{: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:h-screen={$isMobile}
|
||||
>
|
||||
<Settings onSettingsCloseRequest={() => (activePage = 'main')} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
1
src/entrypoints/popup/components/Divider.svelte
Normal file
1
src/entrypoints/popup/components/Divider.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<div class="w-full border-b-[1px] border-gray-400"></div>
|
||||
57
src/entrypoints/popup/components/Toggle.svelte
Normal file
57
src/entrypoints/popup/components/Toggle.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
/* types */
|
||||
interface Props {
|
||||
checked: boolean;
|
||||
onChecked?: (checked: boolean) => void | boolean | Promise<void> | Promise<boolean>;
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { checked = $bindable(), onChecked, size = 'md' }: Props = $props();
|
||||
let internalChecked = $state($state.snapshot(checked));
|
||||
|
||||
/* callbacks */
|
||||
async function onInputChange() {
|
||||
internalChecked = !internalChecked;
|
||||
let approved = false;
|
||||
|
||||
if (!onChecked) {
|
||||
approved = true;
|
||||
} else {
|
||||
const ret = onChecked(!internalChecked);
|
||||
|
||||
if (typeof ret === 'boolean') {
|
||||
approved = ret;
|
||||
} else if (typeof ret === 'object' && 'then' in ret && typeof ret.then === 'function') {
|
||||
const promiseRet = await ret;
|
||||
if (typeof promiseRet === 'undefined') approved = true;
|
||||
else approved = promiseRet;
|
||||
}
|
||||
}
|
||||
|
||||
if (approved) {
|
||||
internalChecked = !internalChecked;
|
||||
checked = internalChecked;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<div class="relative">
|
||||
<input type="checkbox" class="peer sr-only" bind:checked={internalChecked} onchange={onInputChange} />
|
||||
<div
|
||||
class="block rounded-full box bg-red-700 peer-checked:bg-linux-mint-green"
|
||||
class:w-8={size === 'sm'}
|
||||
class:h-4={size === 'sm'}
|
||||
class:w-10={size === 'md'}
|
||||
class:h-5={size === 'md'}
|
||||
></div>
|
||||
<div
|
||||
class="absolute flex items-center justify-center transition bg-white rounded-full dot left-0 top-0 peer-checked:translate-x-full"
|
||||
class:w-4={size === 'sm'}
|
||||
class:h-4={size === 'sm'}
|
||||
class:w-5={size === 'md'}
|
||||
class:h-5={size === 'md'}
|
||||
></div>
|
||||
</div>
|
||||
</label>
|
||||
13
src/entrypoints/popup/index.html
Normal file
13
src/entrypoints/popup/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Stream Bypass</title>
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="contents h-full"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
src/entrypoints/popup/main.ts
Normal file
8
src/entrypoints/popup/main.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { mount } from 'svelte';
|
||||
import App from './App.svelte';
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('app')!
|
||||
});
|
||||
|
||||
export default app;
|
||||
3
src/entrypoints/popup/pages/main/AllDisabled.svelte
Normal file
3
src/entrypoints/popup/pages/main/AllDisabled.svelte
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<p class="text-[1.05rem]">Extension disabled</p>
|
||||
</div>
|
||||
69
src/entrypoints/popup/pages/main/CopyMatch.svelte
Normal file
69
src/entrypoints/popup/pages/main/CopyMatch.svelte
Normal file
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import { Clipboard, InformationCircle } from '@steeze-ui/heroicons';
|
||||
import { Icon } from '@steeze-ui/svelte-icon';
|
||||
import { isMobile } from '@/entrypoints/popup/state';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
url: string;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
type UrlType = 'url' | 'curl';
|
||||
|
||||
/* states */
|
||||
let { url, domain }: Props = $props();
|
||||
|
||||
let urlOutputType: UrlType = $state('url');
|
||||
let urlOutput = $derived(getUrl(urlOutputType));
|
||||
|
||||
/* functions */
|
||||
function getUrl(type: UrlType) {
|
||||
switch (type) {
|
||||
case 'url':
|
||||
return url;
|
||||
case 'curl':
|
||||
return `curl -H "Referer: https://${domain}/" "${encodeURI(url)}"`;
|
||||
}
|
||||
}
|
||||
|
||||
/* callbacks */
|
||||
function copyUrl() {
|
||||
navigator.clipboard.writeText(urlOutput);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex gap-2 items-center pb-1">
|
||||
<p class="mt-0.5 text-sm">Show video as</p>
|
||||
<select
|
||||
class="w-fit text-xs border text-slate-200 border-gray-500 rounded cursor-pointer pt-1 pb-0.5 pl-1"
|
||||
bind:value={urlOutputType}
|
||||
>
|
||||
<option value="url">URL</option>
|
||||
<option value="curl">cURL</option>
|
||||
</select>
|
||||
{#if urlOutputType === 'url'}
|
||||
<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]"
|
||||
>You may have to send the referer header <code class="select-text">Referer: {domain}</code> when accessing
|
||||
the url</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- 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>
|
||||
<div
|
||||
class="absolute top-2 right-2 transition-opacity duration-100 opacity-0 group-hover:opacity-100 group-focus:opacity-100 h-full"
|
||||
>
|
||||
<button class="cursor-pointer" title="Copy to clipboard" onclick={copyUrl}>
|
||||
<Icon src={Clipboard} size="1rem" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
46
src/entrypoints/popup/pages/main/Header.svelte
Normal file
46
src/entrypoints/popup/pages/main/Header.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||
import { HostSettings } from '@/lib/settings';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
allHostsDisabled: boolean;
|
||||
onSettingsClick: () => void;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { allHostsDisabled = $bindable(), onSettingsClick }: Props = $props();
|
||||
|
||||
/* effects */
|
||||
$effect(() => {
|
||||
HostSettings.setAllHostsDisabled(allHostsDisabled);
|
||||
browser.browserAction.setIcon({
|
||||
path: allHostsDisabled
|
||||
? {
|
||||
16: 'icon/stream-bypass_disabled@16px.png',
|
||||
32: 'icon/stream-bypass_disabled@32px.png',
|
||||
48: 'icon/stream-bypass_disabled@48px.png',
|
||||
128: 'icon/stream-bypass_disabled@128px.png'
|
||||
}
|
||||
: {
|
||||
16: 'icon/stream-bypass@16px.png',
|
||||
32: 'icon/stream-bypass@32px.png',
|
||||
48: 'icon/stream-bypass@48px.png',
|
||||
128: 'icon/stream-bypass@128px.png'
|
||||
}
|
||||
});
|
||||
});
|
||||
</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">
|
||||
{#key allHostsDisabled}
|
||||
<Toggle bind:checked={() => !allHostsDisabled, (v) => (allHostsDisabled = !v)} />
|
||||
{/key}
|
||||
<button class="font-bold cursor-pointer" onclick={() => onSettingsClick()}>⋮</button>
|
||||
</div>
|
||||
</div>
|
||||
65
src/entrypoints/popup/pages/main/Main.svelte
Normal file
65
src/entrypoints/popup/pages/main/Main.svelte
Normal file
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import '@/assets/base.css';
|
||||
|
||||
import { onDestroy } from 'svelte';
|
||||
import Divider from '@/entrypoints/popup/components/Divider.svelte';
|
||||
import AllDisabled from '@/entrypoints/popup/pages/main/AllDisabled.svelte';
|
||||
import CopyMatch from '@/entrypoints/popup/pages/main/CopyMatch.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 { hosts, type Host } from '@/lib/host';
|
||||
import { HostSettings } from '@/lib/settings';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
onSettingsOpenRequest: () => void;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { onSettingsOpenRequest }: Props = $props();
|
||||
let currentMatch = $state<{ host: Host; url: string; domain: 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 = {
|
||||
host: hosts.find((host) => host.id === data.id)!,
|
||||
url: data.url,
|
||||
domain: data.domain
|
||||
};
|
||||
});
|
||||
sendMessageToActiveTab(MessageType.RequestActiveMatch, undefined);
|
||||
|
||||
onDestroy(cancel);
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<Header bind:allHostsDisabled onSettingsClick={onSettingsOpenRequest} />
|
||||
<Divider />
|
||||
</div>
|
||||
<div class="px-2 h-full">
|
||||
{#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">
|
||||
<CopyMatch url={currentMatch.url} domain={currentMatch.domain} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
* {
|
||||
@apply select-none;
|
||||
}
|
||||
</style>
|
||||
20
src/entrypoints/popup/pages/main/Match.svelte
Normal file
20
src/entrypoints/popup/pages/main/Match.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { type Host } from '@/lib/host';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
host: Host;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
/* states */
|
||||
const { host, domain }: 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>
|
||||
13
src/entrypoints/popup/pages/main/NoMatch.svelte
Normal file
13
src/entrypoints/popup/pages/main/NoMatch.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<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>
|
||||
35
src/entrypoints/popup/pages/settings/Ff2mpv.svelte
Normal file
35
src/entrypoints/popup/pages/settings/Ff2mpv.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { InformationCircle } from '@steeze-ui/heroicons';
|
||||
import { Icon } from '@steeze-ui/svelte-icon';
|
||||
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||
import { FF2MPVSettings } from '@/lib/settings';
|
||||
|
||||
/* states */
|
||||
let enabled = $state(false);
|
||||
FF2MPVSettings.getEnabled().then((val) => (enabled = val));
|
||||
|
||||
/* callbacks */
|
||||
function onEnableChange(enabled: boolean) {
|
||||
if (!enabled) return true;
|
||||
|
||||
return browser.permissions.request({ permissions: ['nativeMessaging'] });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative mr-3">
|
||||
<span>Communication enabled</span>
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
{#key enabled}
|
||||
<Toggle
|
||||
bind:checked={() => enabled, (v) => FF2MPVSettings.setEnabled(v)}
|
||||
onChecked={onEnableChange}
|
||||
size="sm"
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
16
src/entrypoints/popup/pages/settings/Header.svelte
Normal file
16
src/entrypoints/popup/pages/settings/Header.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
/* types */
|
||||
interface Props {
|
||||
onBackClick: () => void;
|
||||
}
|
||||
|
||||
/* states */
|
||||
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>
|
||||
41
src/entrypoints/popup/pages/settings/HostsTable.svelte
Normal file
41
src/entrypoints/popup/pages/settings/HostsTable.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import Toggle from '@/entrypoints/popup/components/Toggle.svelte';
|
||||
import { hosts } from '@/lib/host';
|
||||
import { HostSettings } from '@/lib/settings';
|
||||
|
||||
/* states */
|
||||
let disabledHostIds = $state<Array<string>>([]);
|
||||
HostSettings.getDisabledHosts().then((val) => (disabledHostIds = val));
|
||||
</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]">
|
||||
{#key disabledHostIds}
|
||||
<Toggle
|
||||
bind:checked={
|
||||
() => !disabledHostIds.includes(host.id),
|
||||
(v) => (v ? HostSettings.removeDisabledHost(host) : HostSettings.addDisabledHost(host))
|
||||
}
|
||||
size="sm"
|
||||
></Toggle>
|
||||
{/key}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
60
src/entrypoints/popup/pages/settings/Settings.svelte
Normal file
60
src/entrypoints/popup/pages/settings/Settings.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import Divider from '@/entrypoints/popup/components/Divider.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 { isMobile } from '@/entrypoints/popup/state';
|
||||
|
||||
/* types */
|
||||
interface Props {
|
||||
onSettingsCloseRequest: () => void;
|
||||
}
|
||||
|
||||
/* states */
|
||||
let { onSettingsCloseRequest }: Props = $props();
|
||||
</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>
|
||||
{#if !$isMobile}
|
||||
<details class="details">
|
||||
<summary>ff2mpv</summary>
|
||||
<Ff2mpv />
|
||||
</details>
|
||||
{/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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
3
src/entrypoints/popup/state.ts
Normal file
3
src/entrypoints/popup/state.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const isMobile = writable(false);
|
||||
Reference in New Issue
Block a user