Merge pull request #819 from movie-web/extension-fixes

Small extension fixes
This commit is contained in:
mrjvs 2024-01-24 17:44:10 +01:00 committed by GitHub
commit 9dd59479f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 250 additions and 58 deletions

View File

@ -71,5 +71,4 @@ jobs:
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
push: false push: false
platforms: linux/amd64,linux/arm64
context: . context: .

View File

@ -38,6 +38,7 @@
"@types/node-forge": "^1.3.10", "@types/node-forge": "^1.3.10",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"core-js": "^3.34.0", "core-js": "^3.34.0",
"detect-browser": "^5.3.0",
"dompurify": "^3.0.6", "dompurify": "^3.0.6",
"flag-icons": "^7.1.0", "flag-icons": "^7.1.0",
"focus-trap-react": "^10.2.3", "focus-trap-react": "^10.2.3",

View File

@ -48,6 +48,9 @@ dependencies:
core-js: core-js:
specifier: ^3.34.0 specifier: ^3.34.0
version: 3.34.0 version: 3.34.0
detect-browser:
specifier: ^5.3.0
version: 5.3.0
dompurify: dompurify:
specifier: ^3.0.6 specifier: ^3.0.6
version: 3.0.6 version: 3.0.6
@ -3338,6 +3341,10 @@ packages:
resolution: {integrity: sha512-M1Ob1zPSIvlARiJUkKqvAZ3VAqQY6Jcuth/pBKQ2b1dX/Qx0OnJ8Vux6J2H5PTMQeRzWrrbTu70VxBfv/OPDJA==} resolution: {integrity: sha512-M1Ob1zPSIvlARiJUkKqvAZ3VAqQY6Jcuth/pBKQ2b1dX/Qx0OnJ8Vux6J2H5PTMQeRzWrrbTu70VxBfv/OPDJA==}
dev: false dev: false
/detect-browser@5.3.0:
resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==}
dev: false
/didyoumean@1.2.2: /didyoumean@1.2.2:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dev: true dev: true

View File

@ -232,7 +232,7 @@
"downloadSubtitle": "Download current subtitle", "downloadSubtitle": "Download current subtitle",
"downloadPlaylist": "Download playlist", "downloadPlaylist": "Download playlist",
"downloadVideo": "Download video", "downloadVideo": "Download video",
"hlsDisclaimer": "Downloads are taken directly from the provider. movie-web does not have control over how the downloads are provided.<br /><br />Please note that you are downloading an HLS playlist, it is <bold>not recommended to download if you are not familiar with advanced streaming formats</bold>. Try different sources for different formats.", "hlsDisclaimer": "Downloads are taken directly from the provider. movie-web does not have control over how the downloads are provided.<br /><br />Please note that you are downloading an HLS playlist, <bold>it is not recommended to download if you are not familiar with advanced streaming formats</bold>. Try different sources for different formats.",
"onAndroid": { "onAndroid": {
"1": "To download on Android, click the download button then, on the new page, <bold>tap and hold</bold> on the video, then select <bold>save</bold>.", "1": "To download on Android, click the download button then, on the new page, <bold>tap and hold</bold> on the video, then select <bold>save</bold>.",
"shortTitle": "Download / Android", "shortTitle": "Download / Android",
@ -506,8 +506,12 @@
"extension": { "extension": {
"title": "Let's start with an extension", "title": "Let's start with an extension",
"explainer": "Using the browser extension, you can get the best streams we have to offer. With just a simple install.", "explainer": "Using the browser extension, you can get the best streams we have to offer. With just a simple install.",
"explainerIos": "Unfortunately, the browser extension is not supported on IOS, Press <bold>Go back</bold> to choose another option.",
"extensionHelp": "If you've installed the extension but it's not detected. <bold>Open the extension through your browsers extension menu</bold> and follow the steps on screen.", "extensionHelp": "If you've installed the extension but it's not detected. <bold>Open the extension through your browsers extension menu</bold> and follow the steps on screen.",
"link": "Install extension", "notDetecting": "Installed on chrome but not showing up? Try reloading the page!",
"notDetectingAction": "Reload page",
"linkChrome": "Install Chrome extension",
"linkFirefox": "Install Firefox extension",
"back": "Go back", "back": "Go back",
"status": { "status": {
"loading": "Waiting for you to install the extension", "loading": "Waiting for you to install the extension",

View File

@ -6,13 +6,22 @@ import {
import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; import { isAllowedExtensionVersion } from "@/backend/extension/compatibility";
import { ExtensionMakeRequestResponse } from "@/backend/extension/plasmo"; import { ExtensionMakeRequestResponse } from "@/backend/extension/plasmo";
// for some reason, about 500 ms is needed after
// page load before the extension starts responding properly
const isExtensionReady = new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 500);
});
let activeExtension = false; let activeExtension = false;
function sendMessage<MessageKey extends keyof MessagesMetadata>( async function sendMessage<MessageKey extends keyof MessagesMetadata>(
message: MessageKey, message: MessageKey,
payload: MessagesMetadata[MessageKey]["req"] | undefined = undefined, payload: MessagesMetadata[MessageKey]["req"] | undefined = undefined,
timeout: number = -1, timeout: number = -1,
) { ) {
await isExtensionReady;
return new Promise<MessagesMetadata[MessageKey]["res"] | null>((resolve) => { return new Promise<MessagesMetadata[MessageKey]["res"] | null>((resolve) => {
if (timeout >= 0) setTimeout(() => resolve(null), timeout); if (timeout >= 0) setTimeout(() => resolve(null), timeout);
sendToBackgroundViaRelay< sendToBackgroundViaRelay<
@ -54,7 +63,7 @@ export async function sendPage(
export async function extensionInfo(): Promise< export async function extensionInfo(): Promise<
MessagesMetadata["hello"]["res"] | null MessagesMetadata["hello"]["res"] | null
> { > {
const message = await sendMessage("hello", undefined, 300); const message = await sendMessage("hello", undefined, 500);
return message; return message;
} }

View File

@ -1,4 +1,4 @@
import { useMemo } from "react"; import { useCallback, useMemo } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { Button } from "@/components/buttons/Button"; import { Button } from "@/components/buttons/Button";
@ -46,13 +46,13 @@ export function DownloadView({ id }: { id: string }) {
const sourceType = usePlayerStore((s) => s.source?.type); const sourceType = usePlayerStore((s) => s.source?.type);
const selectedCaption = usePlayerStore((s) => s.caption?.selected); const selectedCaption = usePlayerStore((s) => s.caption?.selected);
const subtitleUrl = useMemo( const openSubtitleDownload = useCallback(() => {
() => const dataUrl = selectedCaption
selectedCaption ? convertSubtitlesToSrtDataurl(selectedCaption?.srtData)
? convertSubtitlesToSrtDataurl(selectedCaption?.srtData) : null;
: null, if (!dataUrl) return;
[selectedCaption], window.open(dataUrl);
); }, [selectedCaption]);
if (!downloadUrl) return null; if (!downloadUrl) return null;
@ -74,10 +74,9 @@ export function DownloadView({ id }: { id: string }) {
</Button> </Button>
<Button <Button
className="w-full mt-2" className="w-full mt-2"
href={subtitleUrl ?? undefined} onClick={openSubtitleDownload}
disabled={!subtitleUrl} disabled={!selectedCaption}
theme="secondary" theme="secondary"
download="subtitles.srt"
> >
{t("player.menus.downloads.downloadSubtitle")} {t("player.menus.downloads.downloadSubtitle")}
</Button> </Button>
@ -109,8 +108,8 @@ export function DownloadView({ id }: { id: string }) {
</Button> </Button>
<Button <Button
className="w-full mt-2" className="w-full mt-2"
href={subtitleUrl ?? undefined} onClick={openSubtitleDownload}
disabled={!subtitleUrl} disabled={!selectedCaption}
theme="secondary" theme="secondary"
download="subtitles.srt" download="subtitles.srt"
> >

View File

@ -39,7 +39,7 @@ export function OnboardingPage() {
<Paragraph className="!mt-1 !mb-12"> <Paragraph className="!mt-1 !mb-12">
{t("onboarding.defaultConfirm.description")} {t("onboarding.defaultConfirm.description")}
</Paragraph> </Paragraph>
<div className="flex items-end justify-between"> <div className="flex flex-col-reverse gap-3 md:flex-row md:justify-between">
<Button theme="secondary" onClick={skipModal.hide}> <Button theme="secondary" onClick={skipModal.hide}>
{t("onboarding.defaultConfirm.cancel")} {t("onboarding.defaultConfirm.cancel")}
</Button> </Button>
@ -58,7 +58,7 @@ export function OnboardingPage() {
{t("onboarding.start.explainer")} {t("onboarding.start.explainer")}
</Paragraph> </Paragraph>
<div className="w-full grid grid-cols-[1fr,auto,1fr] gap-3"> <div className="w-full flex flex-col-reverse md:flex-row gap-3">
<Card onClick={() => navigate("/onboarding/proxy")}> <Card onClick={() => navigate("/onboarding/proxy")}>
<CardContent <CardContent
colorClass="!text-onboarding-good" colorClass="!text-onboarding-good"
@ -69,7 +69,7 @@ export function OnboardingPage() {
<Link>{t("onboarding.start.options.proxy.action")}</Link> <Link>{t("onboarding.start.options.proxy.action")}</Link>
</CardContent> </CardContent>
</Card> </Card>
<div className="grid grid-rows-[1fr,auto,1fr] justify-center gap-4"> <div className="hidden md:grid grid-rows-[1fr,auto,1fr] justify-center gap-4">
<VerticalLine className="items-end" /> <VerticalLine className="items-end" />
<span className="text-xs uppercase font-bold">or</span> <span className="text-xs uppercase font-bold">or</span>
<VerticalLine /> <VerticalLine />
@ -86,7 +86,7 @@ export function OnboardingPage() {
</Card> </Card>
</div> </div>
<p className="text-center mt-12"> <p className="text-center hidden md:block mt-12">
<Trans i18nKey="onboarding.start.options.default.text"> <Trans i18nKey="onboarding.start.options.default.text">
<br /> <br />
<a <a
@ -96,6 +96,21 @@ export function OnboardingPage() {
/> />
</Trans> </Trans>
</p> </p>
<div className=" max-w-[300px] mx-auto md:hidden mt-12 ">
<Button
className="!text-type-text !bg-opacity-50"
theme="secondary"
onClick={skipModal.show}
>
<span>
<Trans i18nKey="onboarding.start.options.default.text">
<span />
<span />
</Trans>
</span>
</Button>
</div>
</CenterContainer> </CenterContainer>
</MinimalPageLayout> </MinimalPageLayout>
); );

View File

@ -1,4 +1,4 @@
import { ReactNode } from "react"; import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { useAsyncFn, useInterval } from "react-use"; import { useAsyncFn, useInterval } from "react-use";
@ -18,6 +18,10 @@ import {
import { Card, Link } from "@/pages/onboarding/utils"; import { Card, Link } from "@/pages/onboarding/utils";
import { PageTitle } from "@/pages/parts/util/PageTitle"; import { PageTitle } from "@/pages/parts/util/PageTitle";
import { conf } from "@/setup/config"; import { conf } from "@/setup/config";
import {
ExtensionDetectionResult,
detectExtensionInstall,
} from "@/utils/detectFeatures";
type ExtensionStatus = type ExtensionStatus =
| "unknown" | "unknown"
@ -37,11 +41,33 @@ async function getExtensionState(): Promise<ExtensionStatus> {
return "success"; // no problems return "success"; // no problems
} }
function RefreshBar() {
const { t } = useTranslation();
const reload = useCallback(() => {
window.location.reload();
}, []);
return (
<Card className="mt-4">
<div className="flex items-center space-x-7">
<p className="flex-1">{t("onboarding.extension.notDetecting")}</p>
<Button theme="secondary" onClick={reload}>
{t("onboarding.extension.notDetectingAction")}
</Button>
</div>
</Card>
);
}
export function ExtensionStatus(props: { export function ExtensionStatus(props: {
status: ExtensionStatus; status: ExtensionStatus;
loading: boolean; loading: boolean;
showHelp?: boolean;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [lastKnownStatus, setLastKnownStatus] = useState(props.status);
useEffect(() => {
if (!props.loading) setLastKnownStatus(props.status);
}, [props.status, props.loading]);
let content: ReactNode = null; let content: ReactNode = null;
if (props.loading || props.status === "unknown") if (props.loading || props.status === "unknown")
@ -88,19 +114,120 @@ export function ExtensionStatus(props: {
{content} {content}
</div> </div>
</Card> </Card>
<Card className="mt-4"> {lastKnownStatus === "unknown" ? <RefreshBar /> : null}
<div className="flex items-center space-x-7"> {props.showHelp ? (
<Icon icon={Icons.WARNING} className="text-type-danger text-2xl" /> <Card className="mt-4">
<p className="flex-1"> <div className="flex items-center space-x-7">
<Trans <Icon icon={Icons.WARNING} className="text-type-danger text-2xl" />
i18nKey="onboarding.extension.extensionHelp" <p className="flex-1">
components={{ <Trans
bold: <span className="text-white" />, i18nKey="onboarding.extension.extensionHelp"
}} components={{
/> bold: <span className="text-white" />,
</p> }}
</div> />
</Card> </p>
</div>
</Card>
) : null}
</>
);
}
interface ExtensionPageProps {
status: ExtensionStatus;
loading: boolean;
}
function ChromeExtensionPage(props: ExtensionPageProps) {
const { t } = useTranslation();
const installLink = conf().ONBOARDING_CHROME_EXTENSION_INSTALL_LINK;
return (
<>
<Heading2 className="!mt-0 !text-3xl max-w-[435px]">
{t("onboarding.extension.title")}
</Heading2>
<Paragraph className="max-w-[320px] mb-4">
{t("onboarding.extension.explainer")}
</Paragraph>
{installLink ? (
<Link href={installLink} target="_blank" className="mb-12">
{t("onboarding.extension.linkChrome")}
</Link>
) : null}
<ExtensionStatus status={props.status} loading={props.loading} />
</>
);
}
function FirefoxExtensionPage(props: ExtensionPageProps) {
const { t } = useTranslation();
const installLink = conf().ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK;
return (
<>
<Heading2 className="!mt-0 !text-3xl max-w-[435px]">
{t("onboarding.extension.title")}
</Heading2>
<Paragraph className="max-w-[320px] mb-4">
{t("onboarding.extension.explainer")}
</Paragraph>
{installLink ? (
<Link href={installLink} target="_blank" className="mb-12">
{t("onboarding.extension.linkFirefox")}
</Link>
) : null}
<ExtensionStatus status={props.status} loading={props.loading} showHelp />
</>
);
}
function IosExtensionPage(_props: ExtensionPageProps) {
const { t } = useTranslation();
return (
<>
<Heading2 className="!mt-0 !text-3xl max-w-[435px]">
{t("onboarding.extension.title")}
</Heading2>
<Paragraph className="max-w-[320px] mb-4">
<Trans
i18nKey="onboarding.extension.explainerIos"
components={{ bold: <span className="text-white font-bold" /> }}
/>
</Paragraph>
</>
);
}
function UnknownExtensionPage(props: ExtensionPageProps) {
const { t } = useTranslation();
const installChromeLink = conf().ONBOARDING_CHROME_EXTENSION_INSTALL_LINK;
const installFirefoxLink = conf().ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK;
return (
<>
<Heading2 className="!mt-0 !text-3xl max-w-[435px]">
{t("onboarding.extension.title")}
</Heading2>
<Paragraph className="max-w-[320px] mb-4">
{t("onboarding.extension.explainer")}
</Paragraph>
<div className="mb-4">
{installChromeLink ? (
<Link href={installChromeLink} target="_blank">
{t("onboarding.extension.linkChrome")}
</Link>
) : null}
</div>
<div className="mb-12">
{installFirefoxLink ? (
<Link href={installFirefoxLink} target="_blank">
{t("onboarding.extension.linkFirefox")}
</Link>
) : null}
</div>
<ExtensionStatus status={props.status} loading={props.loading} showHelp />
</> </>
); );
} }
@ -109,7 +236,7 @@ export function OnboardingExtensionPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigateOnboarding(); const navigate = useNavigateOnboarding();
const { completeAndRedirect } = useRedirectBack(); const { completeAndRedirect } = useRedirectBack();
const installLink = conf().ONBOARDING_EXTENSION_INSTALL_LINK; const extensionSupport = useMemo(() => detectExtensionInstall(), []);
const [{ loading, value }, exec] = useAsyncFn( const [{ loading, value }, exec] = useAsyncFn(
async (triggeredManually: boolean = false) => { async (triggeredManually: boolean = false) => {
@ -121,24 +248,23 @@ export function OnboardingExtensionPage() {
); );
useInterval(exec, 1000); useInterval(exec, 1000);
const componentMap: Record<
ExtensionDetectionResult,
typeof UnknownExtensionPage
> = {
chrome: ChromeExtensionPage,
firefox: FirefoxExtensionPage,
ios: IosExtensionPage,
unknown: UnknownExtensionPage,
};
const PageContent = componentMap[extensionSupport];
return ( return (
<MinimalPageLayout> <MinimalPageLayout>
<PageTitle subpage k="global.pages.onboarding" /> <PageTitle subpage k="global.pages.onboarding" />
<CenterContainer> <CenterContainer>
<Stepper steps={2} current={2} className="mb-12" /> <Stepper steps={2} current={2} className="mb-12" />
<Heading2 className="!mt-0 !text-3xl max-w-[435px]"> <PageContent loading={loading} status={value ?? "unknown"} />
{t("onboarding.extension.title")}
</Heading2>
<Paragraph className="max-w-[320px] mb-4">
{t("onboarding.extension.explainer")}
</Paragraph>
{installLink ? (
<Link href={installLink} target="_blank" className="mb-12">
{t("onboarding.extension.link")}
</Link>
) : null}
<ExtensionStatus status={value ?? "unknown"} loading={loading} />
<div className="flex justify-between items-center mt-8"> <div className="flex justify-between items-center mt-8">
<Button onClick={() => navigate("/onboarding")} theme="secondary"> <Button onClick={() => navigate("/onboarding")} theme="secondary">
{t("onboarding.extension.back")} {t("onboarding.extension.back")}

View File

@ -29,7 +29,7 @@ type SetupData = {
function testProxy(url: string) { function testProxy(url: string) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
setTimeout(() => reject(new Error("Timed out!")), 1000); setTimeout(() => reject(new Error("Timed out!")), 3000);
singularProxiedFetch(url, testUrl, {}) singularProxiedFetch(url, testUrl, {})
.then((res) => { .then((res) => {
if (res.url !== testUrl) return reject(new Error("Not a proxy")); if (res.url !== testUrl) return reject(new Error("Not a proxy"));

View File

@ -20,7 +20,8 @@ interface Config {
TURNSTILE_KEY: string; TURNSTILE_KEY: string;
CDN_REPLACEMENTS: string; CDN_REPLACEMENTS: string;
HAS_ONBOARDING: string; HAS_ONBOARDING: string;
ONBOARDING_EXTENSION_INSTALL_LINK: string; ONBOARDING_CHROME_EXTENSION_INSTALL_LINK: string;
ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK: string;
ONBOARDING_PROXY_INSTALL_LINK: string; ONBOARDING_PROXY_INSTALL_LINK: string;
} }
@ -38,7 +39,8 @@ export interface RuntimeConfig {
TURNSTILE_KEY: string | null; TURNSTILE_KEY: string | null;
CDN_REPLACEMENTS: Array<string[]>; CDN_REPLACEMENTS: Array<string[]>;
HAS_ONBOARDING: boolean; HAS_ONBOARDING: boolean;
ONBOARDING_EXTENSION_INSTALL_LINK: string | null; ONBOARDING_CHROME_EXTENSION_INSTALL_LINK: string | null;
ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK: string | null;
ONBOARDING_PROXY_INSTALL_LINK: string | null; ONBOARDING_PROXY_INSTALL_LINK: string | null;
} }
@ -48,8 +50,10 @@ const env: Record<keyof Config, undefined | string> = {
GITHUB_LINK: undefined, GITHUB_LINK: undefined,
DONATION_LINK: undefined, DONATION_LINK: undefined,
DISCORD_LINK: undefined, DISCORD_LINK: undefined,
ONBOARDING_EXTENSION_INSTALL_LINK: import.meta.env ONBOARDING_CHROME_EXTENSION_INSTALL_LINK: import.meta.env
.VITE_ONBOARDING_EXTENSION_INSTALL_LINK, .VITE_ONBOARDING_CHROME_EXTENSION_INSTALL_LINK,
ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK: import.meta.env
.VITE_ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK,
ONBOARDING_PROXY_INSTALL_LINK: import.meta.env ONBOARDING_PROXY_INSTALL_LINK: import.meta.env
.VITE_ONBOARDING_PROXY_INSTALL_LINK, .VITE_ONBOARDING_PROXY_INSTALL_LINK,
DMCA_EMAIL: import.meta.env.VITE_DMCA_EMAIL, DMCA_EMAIL: import.meta.env.VITE_DMCA_EMAIL,
@ -80,7 +84,8 @@ function getKey(key: keyof Config, defaultString?: string): string {
export function conf(): RuntimeConfig { export function conf(): RuntimeConfig {
const dmcaEmail = getKey("DMCA_EMAIL"); const dmcaEmail = getKey("DMCA_EMAIL");
const extensionLink = getKey("ONBOARDING_EXTENSION_INSTALL_LINK"); const chromeExtension = getKey("ONBOARDING_CHROME_EXTENSION_INSTALL_LINK");
const firefoxExtension = getKey("ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK");
const proxyInstallLink = getKey("ONBOARDING_PROXY_INSTALL_LINK"); const proxyInstallLink = getKey("ONBOARDING_PROXY_INSTALL_LINK");
const turnstileKey = getKey("TURNSTILE_KEY"); const turnstileKey = getKey("TURNSTILE_KEY");
return { return {
@ -89,8 +94,10 @@ export function conf(): RuntimeConfig {
DONATION_LINK, DONATION_LINK,
DISCORD_LINK, DISCORD_LINK,
DMCA_EMAIL: dmcaEmail.length > 0 ? dmcaEmail : null, DMCA_EMAIL: dmcaEmail.length > 0 ? dmcaEmail : null,
ONBOARDING_EXTENSION_INSTALL_LINK: ONBOARDING_CHROME_EXTENSION_INSTALL_LINK:
extensionLink.length > 0 ? extensionLink : null, chromeExtension.length > 0 ? chromeExtension : null,
ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK:
firefoxExtension.length > 0 ? firefoxExtension : null,
ONBOARDING_PROXY_INSTALL_LINK: ONBOARDING_PROXY_INSTALL_LINK:
proxyInstallLink.length > 0 ? proxyInstallLink : null, proxyInstallLink.length > 0 ? proxyInstallLink : null,
BACKEND_URL: getKey("BACKEND_URL", BACKEND_URL), BACKEND_URL: getKey("BACKEND_URL", BACKEND_URL),

View File

@ -1,3 +1,4 @@
import { detect } from "detect-browser";
import fscreen from "fscreen"; import fscreen from "fscreen";
import Hls from "hls.js"; import Hls from "hls.js";
@ -52,3 +53,27 @@ export function canPlayHlsNatively(video: HTMLVideoElement): boolean {
if (Hls.isSupported()) return false; // no need to play natively if (Hls.isSupported()) return false; // no need to play natively
return !!video.canPlayType("application/vnd.apple.mpegurl"); return !!video.canPlayType("application/vnd.apple.mpegurl");
} }
export type ExtensionDetectionResult =
| "unknown" // unknown detection or weird browser
| "firefox" // firefox extensions
| "chrome" // chrome extension (could be chromium, but still works with chrome extensions)
| "ios"; // ios, no extensions
export function detectExtensionInstall(): ExtensionDetectionResult {
const res = detect();
// not a browser or failed to detect
if (res?.type !== "browser") return "unknown";
if (res.name === "ios" || res.name === "ios-webview") return "ios";
if (
res.name === "chrome" ||
res.name === "chromium-webview" ||
res.name === "edge-chromium" ||
res.name === "opera"
)
return "chrome";
if (res.name === "firefox") return "firefox";
return "unknown";
}