From ef85c217f7a6288321eb8c785c8bb33f23b582df Mon Sep 17 00:00:00 2001 From: Jorrin Date: Mon, 8 Jan 2024 17:06:27 +0100 Subject: [PATCH 01/40] first mvp extension --- package.json | 1 + pnpm-lock.yaml | 21 +++ src/@types/plasmo.d.ts | 37 ++++++ src/components/player/display/base.ts | 120 ++++++++++-------- .../player/utils/convertRunoutputToSource.ts | 2 + src/stores/player/utils/qualities.ts | 5 +- src/utils/providers.ts | 4 +- 7 files changed, 134 insertions(+), 56 deletions(-) create mode 100644 src/@types/plasmo.d.ts diff --git a/package.json b/package.json index 388054df..3148f860 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@ladjs/country-language": "^1.0.3", "@movie-web/providers": "^2.0.5", "@noble/hashes": "^1.3.3", + "@plasmohq/messaging": "^0.6.1", "@react-spring/web": "^9.7.3", "@scure/bip39": "^1.2.2", "@sozialhelden/ietf-language-tags": "^5.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d6bb9bd..5c1d1c9e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,6 +27,9 @@ dependencies: '@noble/hashes': specifier: ^1.3.3 version: 1.3.3 + '@plasmohq/messaging': + specifier: ^0.6.1 + version: 0.6.1(react@18.2.0) '@react-spring/web': specifier: ^9.7.3 version: 9.7.3(react-dom@18.2.0)(react@18.2.0) @@ -1980,6 +1983,18 @@ packages: tslib: 2.6.2 dev: true + /@plasmohq/messaging@0.6.1(react@18.2.0): + resolution: {integrity: sha512-/nn1k8SG5z++o/NnZu+byHWcC9MhPLxfmvj+AP3buqMn7uwfYDcYWURLuMW2Knw08HBg+wku2v1Ltt4evN0nzA==} + peerDependencies: + react: ^16.8.6 || ^17 || ^18 + peerDependenciesMeta: + react: + optional: true + dependencies: + nanoid: 5.0.3 + react: 18.2.0 + dev: false + /@react-spring/animated@9.7.3(react@18.2.0): resolution: {integrity: sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==} peerDependencies: @@ -5156,6 +5171,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanoid@5.0.3: + resolution: {integrity: sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==} + engines: {node: ^18 || >=20} + hasBin: true + dev: false + /nanoid@5.0.4: resolution: {integrity: sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==} engines: {node: ^18 || >=20} diff --git a/src/@types/plasmo.d.ts b/src/@types/plasmo.d.ts new file mode 100644 index 00000000..4570cae4 --- /dev/null +++ b/src/@types/plasmo.d.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import "@plasmohq/messaging"; + +export interface PlasmoRequestBody { + ruleId: number; + domain: string; + requestHeaders?: Record; + responseHeaders?: Record; +} + +export type PlasmoResponseBody = + | { + success: true; + ruleId: number; + } + | { + success: false; + error: string; + }; + +interface MmMetadata { + "declarative-net-request": { + req: PlasmoRequestBody; + res: PlasmoResponseBody; + }; + "proxy-request": { + req: PlasmoRequestBody; + res: PlasmoResponseBody; + }; +} + +interface MpMetadata {} + +declare module "@plasmohq/messaging" { + interface MessagesMetadata extends MmMetadata {} + interface PortsMetadata extends MpMetadata {} +} diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts index 7a9ce041..11a0799c 100644 --- a/src/components/player/display/base.ts +++ b/src/components/player/display/base.ts @@ -1,6 +1,8 @@ +import { sendToBackgroundViaRelay } from "@plasmohq/messaging"; import fscreen from "fscreen"; import Hls, { Level } from "hls.js"; +import { PlasmoRequestBody, PlasmoResponseBody } from "@/@types/plasmo"; import { DisplayInterface, DisplayInterfaceEvents, @@ -100,65 +102,75 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { } function setupSource(vid: HTMLVideoElement, src: LoadableSource) { - if (src.type === "hls") { - if (canPlayHlsNatively(vid)) { - vid.src = processCdnLink(src.url); + // TODO: Add check whether the extension is installed + sendToBackgroundViaRelay({ + name: "declarative-net-request", + body: { + ruleId: 1, + domain: src.type === "hls" ? new URL(src.url).hostname : src.url, + requestHeaders: src.preferredHeaders, + }, + }).then(() => { + if (src.type === "hls") { + if (canPlayHlsNatively(vid)) { + vid.src = processCdnLink(src.url); + vid.currentTime = startAt; + return; + } + + if (!Hls.isSupported()) throw new Error("HLS not supported"); + if (!hls) { + hls = new Hls({ + maxBufferSize: 500 * 1000 * 1000, // 500 mb of buffering, should load more fragments at once + fragLoadPolicy: { + default: { + maxLoadTimeMs: 30 * 1000, // allow it load extra long, fragments are slow if requested for the first time on an origin + maxTimeToFirstByteMs: 30 * 1000, + errorRetry: { + maxNumRetry: 2, + retryDelayMs: 1000, + maxRetryDelayMs: 8000, + }, + timeoutRetry: { + maxNumRetry: 3, + maxRetryDelayMs: 0, + retryDelayMs: 0, + }, + }, + }, + }); + hls.on(Hls.Events.ERROR, (event, data) => { + console.error("HLS error", data); + if (data.fatal) { + emit("error", { + message: data.error.message, + stackTrace: data.error.stack, + errorName: data.error.name, + type: "hls", + }); + } + }); + hls.on(Hls.Events.MANIFEST_LOADED, () => { + if (!hls) return; + reportLevels(); + setupQualityForHls(); + }); + hls.on(Hls.Events.LEVEL_SWITCHED, () => { + if (!hls) return; + const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]); + emit("changedquality", quality); + }); + } + + hls.attachMedia(vid); + hls.loadSource(processCdnLink(src.url)); vid.currentTime = startAt; return; } - if (!Hls.isSupported()) throw new Error("HLS not supported"); - if (!hls) { - hls = new Hls({ - maxBufferSize: 500 * 1000 * 1000, // 500 mb of buffering, should load more fragments at once - fragLoadPolicy: { - default: { - maxLoadTimeMs: 30 * 1000, // allow it load extra long, fragments are slow if requested for the first time on an origin - maxTimeToFirstByteMs: 30 * 1000, - errorRetry: { - maxNumRetry: 2, - retryDelayMs: 1000, - maxRetryDelayMs: 8000, - }, - timeoutRetry: { - maxNumRetry: 3, - maxRetryDelayMs: 0, - retryDelayMs: 0, - }, - }, - }, - }); - hls.on(Hls.Events.ERROR, (event, data) => { - console.error("HLS error", data); - if (data.fatal) { - emit("error", { - message: data.error.message, - stackTrace: data.error.stack, - errorName: data.error.name, - type: "hls", - }); - } - }); - hls.on(Hls.Events.MANIFEST_LOADED, () => { - if (!hls) return; - reportLevels(); - setupQualityForHls(); - }); - hls.on(Hls.Events.LEVEL_SWITCHED, () => { - if (!hls) return; - const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]); - emit("changedquality", quality); - }); - } - - hls.attachMedia(vid); - hls.loadSource(processCdnLink(src.url)); + vid.src = processCdnLink(src.url); vid.currentTime = startAt; - return; - } - - vid.src = processCdnLink(src.url); - vid.currentTime = startAt; + }); } function setSource() { diff --git a/src/components/player/utils/convertRunoutputToSource.ts b/src/components/player/utils/convertRunoutputToSource.ts index fba59e63..f54c5396 100644 --- a/src/components/player/utils/convertRunoutputToSource.ts +++ b/src/components/player/utils/convertRunoutputToSource.ts @@ -28,6 +28,7 @@ export function convertRunoutputToSource(out: { return { type: "hls", url: out.stream.playlist, + preferredHeaders: out.stream.preferredHeaders, }; } if (out.stream.type === "file") { @@ -49,6 +50,7 @@ export function convertRunoutputToSource(out: { return { type: "file", qualities, + preferredHeaders: out.stream.preferredHeaders, }; } throw new Error("unrecognized type"); diff --git a/src/stores/player/utils/qualities.ts b/src/stores/player/utils/qualities.ts index dbd84b5c..e5140d53 100644 --- a/src/stores/player/utils/qualities.ts +++ b/src/stores/player/utils/qualities.ts @@ -1,4 +1,4 @@ -import { Qualities } from "@movie-web/providers"; +import { Qualities, Stream } from "@movie-web/providers"; import { QualityStore } from "@/stores/quality"; @@ -14,16 +14,19 @@ export type SourceFileStream = { export type LoadableSource = { type: StreamType; url: string; + preferredHeaders?: Stream["preferredHeaders"]; }; export type SourceSliceSource = | { type: "file"; qualities: Partial>; + preferredHeaders?: Stream["preferredHeaders"]; } | { type: "hls"; url: string; + preferredHeaders?: Stream["preferredHeaders"]; }; const qualitySorting: Record = { diff --git a/src/utils/providers.ts b/src/utils/providers.ts index e5c8503c..73c95662 100644 --- a/src/utils/providers.ts +++ b/src/utils/providers.ts @@ -62,5 +62,7 @@ function makeLoadBalancedSimpleProxyFetcher() { export const providers = makeProviders({ fetcher: makeStandardFetcher(fetch), proxiedFetcher: makeLoadBalancedSimpleProxyFetcher(), - target: targets.BROWSER, + // TODO: Add check whether the extension is installed + // target: targets.BROWSER, + target: targets.BROWSER_EXTENSION, }) as any as ProviderControls; From f70d13f2c9e3b91b5b3caccbdf29e9ae77827944 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 9 Jan 2024 20:07:22 +0100 Subject: [PATCH 02/40] Clean up extension code --- src/backend/extension/messaging.ts | 51 ++++++++ .../extension/plasmo.ts} | 17 ++- src/backend/helpers/fetch.ts | 2 +- .../providers/fetchers.ts} | 25 ++-- src/backend/providers/providers.ts | 26 ++++ src/components/player/display/base.ts | 120 ++++++++---------- .../player/hooks/useSourceSelection.ts | 9 +- src/hooks/useProviderScrape.tsx | 5 +- src/pages/parts/player/MetaPart.tsx | 15 ++- 9 files changed, 172 insertions(+), 98 deletions(-) create mode 100644 src/backend/extension/messaging.ts rename src/{@types/plasmo.d.ts => backend/extension/plasmo.ts} (72%) rename src/{utils/providers.ts => backend/providers/fetchers.ts} (74%) create mode 100644 src/backend/providers/providers.ts diff --git a/src/backend/extension/messaging.ts b/src/backend/extension/messaging.ts new file mode 100644 index 00000000..ec0b4bf6 --- /dev/null +++ b/src/backend/extension/messaging.ts @@ -0,0 +1,51 @@ +import { + MessagesMetadata, + sendToBackgroundViaRelay, +} from "@plasmohq/messaging"; + +let activeExtension = false; + +export interface ExtensionHello { + version: string; +} + +function sendMessage( + message: keyof MessagesMetadata, + payload: any, + timeout: number = -1, +) { + return new Promise((resolve) => { + if (timeout >= 0) setTimeout(() => resolve(null), timeout); + sendToBackgroundViaRelay({ + name: message, + body: payload, + }) + .then((res) => { + activeExtension = true; + resolve(res); + }) + .catch(() => { + activeExtension = false; + resolve(null); + }); + }); +} + +export async function sendExtensionRequest( + url: string, + ops: any, +): Promise { + return sendMessage("proxy-request", { url, ...ops }); +} + +export async function extensionInfo(): Promise { + return sendMessage("hello", null, 300); +} + +export function isExtensionActiveCached(): boolean { + return activeExtension; +} + +export async function isExtensionActive(): Promise { + return !!(await extensionInfo()); +} diff --git a/src/@types/plasmo.d.ts b/src/backend/extension/plasmo.ts similarity index 72% rename from src/@types/plasmo.d.ts rename to src/backend/extension/plasmo.ts index 4570cae4..2e9cacbf 100644 --- a/src/@types/plasmo.d.ts +++ b/src/backend/extension/plasmo.ts @@ -1,6 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-types */ -import "@plasmohq/messaging"; - export interface PlasmoRequestBody { ruleId: number; domain: string; @@ -8,7 +5,11 @@ export interface PlasmoRequestBody { responseHeaders?: Record; } -export type PlasmoResponseBody = +export interface ExtensionHelloReply { + version: string; +} + +export type ExtensionRequestReply = | { success: true; ruleId: number; @@ -21,11 +22,15 @@ export type PlasmoResponseBody = interface MmMetadata { "declarative-net-request": { req: PlasmoRequestBody; - res: PlasmoResponseBody; + res: ExtensionRequestReply; }; "proxy-request": { req: PlasmoRequestBody; - res: PlasmoResponseBody; + res: ExtensionRequestReply; + }; + hello: { + req: null; + res: ExtensionHelloReply; }; } diff --git a/src/backend/helpers/fetch.ts b/src/backend/helpers/fetch.ts index cc3e735e..f9aa145a 100644 --- a/src/backend/helpers/fetch.ts +++ b/src/backend/helpers/fetch.ts @@ -1,7 +1,7 @@ import { ofetch } from "ofetch"; import { getApiToken, setApiToken } from "@/backend/helpers/providerApi"; -import { getLoadbalancedProxyUrl } from "@/utils/providers"; +import { getLoadbalancedProxyUrl } from "@/backend/providers/fetchers"; type P = Parameters>; type R = ReturnType>; diff --git a/src/utils/providers.ts b/src/backend/providers/fetchers.ts similarity index 74% rename from src/utils/providers.ts rename to src/backend/providers/fetchers.ts index 73c95662..596e8376 100644 --- a/src/utils/providers.ts +++ b/src/backend/providers/fetchers.ts @@ -1,12 +1,6 @@ -import { - Fetcher, - ProviderControls, - makeProviders, - makeSimpleProxyFetcher, - makeStandardFetcher, - targets, -} from "@movie-web/providers"; +import { Fetcher, makeSimpleProxyFetcher } from "@movie-web/providers"; +import { sendExtensionRequest } from "@/backend/extension/messaging"; import { getApiToken, setApiToken } from "@/backend/helpers/providerApi"; import { getProviderApiUrls, getProxyUrls } from "@/utils/proxyUrls"; @@ -48,7 +42,7 @@ async function fetchButWithApiTokens( return response; } -function makeLoadBalancedSimpleProxyFetcher() { +export function makeLoadBalancedSimpleProxyFetcher() { const fetcher: Fetcher = async (a, b) => { const currentFetcher = makeSimpleProxyFetcher( getLoadbalancedProxyUrl(), @@ -59,10 +53,9 @@ function makeLoadBalancedSimpleProxyFetcher() { return fetcher; } -export const providers = makeProviders({ - fetcher: makeStandardFetcher(fetch), - proxiedFetcher: makeLoadBalancedSimpleProxyFetcher(), - // TODO: Add check whether the extension is installed - // target: targets.BROWSER, - target: targets.BROWSER_EXTENSION, -}) as any as ProviderControls; +export function makeExtensionFetcher() { + const fetcher: Fetcher = async (a, b) => { + return sendExtensionRequest(a, b) as any; + }; + return fetcher; +} diff --git a/src/backend/providers/providers.ts b/src/backend/providers/providers.ts new file mode 100644 index 00000000..1a7b484a --- /dev/null +++ b/src/backend/providers/providers.ts @@ -0,0 +1,26 @@ +import { + makeProviders, + makeStandardFetcher, + targets, +} from "@movie-web/providers"; + +import { isExtensionActiveCached } from "@/backend/extension/messaging"; +import { + makeExtensionFetcher, + makeLoadBalancedSimpleProxyFetcher, +} from "@/backend/providers/fetchers"; + +export function getProviders() { + if (isExtensionActiveCached()) { + return makeProviders({ + fetcher: makeExtensionFetcher(), + target: targets.BROWSER_EXTENSION, + }); + } + + return makeProviders({ + fetcher: makeStandardFetcher(fetch), + proxiedFetcher: makeLoadBalancedSimpleProxyFetcher(), + target: targets.BROWSER, + }); +} diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts index 11a0799c..91f36722 100644 --- a/src/components/player/display/base.ts +++ b/src/components/player/display/base.ts @@ -1,8 +1,6 @@ -import { sendToBackgroundViaRelay } from "@plasmohq/messaging"; import fscreen from "fscreen"; import Hls, { Level } from "hls.js"; -import { PlasmoRequestBody, PlasmoResponseBody } from "@/@types/plasmo"; import { DisplayInterface, DisplayInterfaceEvents, @@ -43,6 +41,7 @@ function qualityToHlsLevel(quality: SourceQuality): number | null { ); return found ? +found[0] : null; } + function hlsLevelsToQualities(levels: Level[]): SourceQuality[] { return levels .map((v) => hlsLevelToQuality(v)) @@ -103,74 +102,65 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { function setupSource(vid: HTMLVideoElement, src: LoadableSource) { // TODO: Add check whether the extension is installed - sendToBackgroundViaRelay({ - name: "declarative-net-request", - body: { - ruleId: 1, - domain: src.type === "hls" ? new URL(src.url).hostname : src.url, - requestHeaders: src.preferredHeaders, - }, - }).then(() => { - if (src.type === "hls") { - if (canPlayHlsNatively(vid)) { - vid.src = processCdnLink(src.url); - vid.currentTime = startAt; - return; - } - - if (!Hls.isSupported()) throw new Error("HLS not supported"); - if (!hls) { - hls = new Hls({ - maxBufferSize: 500 * 1000 * 1000, // 500 mb of buffering, should load more fragments at once - fragLoadPolicy: { - default: { - maxLoadTimeMs: 30 * 1000, // allow it load extra long, fragments are slow if requested for the first time on an origin - maxTimeToFirstByteMs: 30 * 1000, - errorRetry: { - maxNumRetry: 2, - retryDelayMs: 1000, - maxRetryDelayMs: 8000, - }, - timeoutRetry: { - maxNumRetry: 3, - maxRetryDelayMs: 0, - retryDelayMs: 0, - }, - }, - }, - }); - hls.on(Hls.Events.ERROR, (event, data) => { - console.error("HLS error", data); - if (data.fatal) { - emit("error", { - message: data.error.message, - stackTrace: data.error.stack, - errorName: data.error.name, - type: "hls", - }); - } - }); - hls.on(Hls.Events.MANIFEST_LOADED, () => { - if (!hls) return; - reportLevels(); - setupQualityForHls(); - }); - hls.on(Hls.Events.LEVEL_SWITCHED, () => { - if (!hls) return; - const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]); - emit("changedquality", quality); - }); - } - - hls.attachMedia(vid); - hls.loadSource(processCdnLink(src.url)); + if (src.type === "hls") { + if (canPlayHlsNatively(vid)) { + vid.src = processCdnLink(src.url); vid.currentTime = startAt; return; } - vid.src = processCdnLink(src.url); + if (!Hls.isSupported()) throw new Error("HLS not supported"); + if (!hls) { + hls = new Hls({ + maxBufferSize: 500 * 1000 * 1000, // 500 mb of buffering, should load more fragments at once + fragLoadPolicy: { + default: { + maxLoadTimeMs: 30 * 1000, // allow it load extra long, fragments are slow if requested for the first time on an origin + maxTimeToFirstByteMs: 30 * 1000, + errorRetry: { + maxNumRetry: 2, + retryDelayMs: 1000, + maxRetryDelayMs: 8000, + }, + timeoutRetry: { + maxNumRetry: 3, + maxRetryDelayMs: 0, + retryDelayMs: 0, + }, + }, + }, + }); + hls.on(Hls.Events.ERROR, (event, data) => { + console.error("HLS error", data); + if (data.fatal) { + emit("error", { + message: data.error.message, + stackTrace: data.error.stack, + errorName: data.error.name, + type: "hls", + }); + } + }); + hls.on(Hls.Events.MANIFEST_LOADED, () => { + if (!hls) return; + reportLevels(); + setupQualityForHls(); + }); + hls.on(Hls.Events.LEVEL_SWITCHED, () => { + if (!hls) return; + const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]); + emit("changedquality", quality); + }); + } + + hls.attachMedia(vid); + hls.loadSource(processCdnLink(src.url)); vid.currentTime = startAt; - }); + return; + } + + vid.src = processCdnLink(src.url); + vid.currentTime = startAt; } function setSource() { diff --git a/src/components/player/hooks/useSourceSelection.ts b/src/components/player/hooks/useSourceSelection.ts index e28507cf..bca884f7 100644 --- a/src/components/player/hooks/useSourceSelection.ts +++ b/src/components/player/hooks/useSourceSelection.ts @@ -13,12 +13,13 @@ import { scrapeSourceOutputToProviderMetric, useReportProviders, } from "@/backend/helpers/report"; +import { getLoadbalancedProviderApiUrl } from "@/backend/providers/fetchers"; +import { getProviders } from "@/backend/providers/providers"; import { convertProviderCaption } from "@/components/player/utils/captions"; import { convertRunoutputToSource } from "@/components/player/utils/convertRunoutputToSource"; import { useOverlayRouter } from "@/hooks/useOverlayRouter"; import { metaToScrapeMedia } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; -import { getLoadbalancedProviderApiUrl, providers } from "@/utils/providers"; export function useEmbedScraping( routerId: string, @@ -47,7 +48,7 @@ export function useEmbedScraping( ); result = await conn.promise(); } else { - result = await providers.runEmbedScraper({ + result = await getProviders().runEmbedScraper({ id: embedId, url, }); @@ -111,7 +112,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) { ); result = await conn.promise(); } else { - result = await providers.runSourceScraper({ + result = await getProviders().runSourceScraper({ id: sourceId, media: scrapeMedia, }); @@ -155,7 +156,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) { ); embedResult = await conn.promise(); } else { - embedResult = await providers.runEmbedScraper({ + embedResult = await getProviders().runEmbedScraper({ id: result.embeds[0].embedId, url: result.embeds[0].url, }); diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx index 43328184..21cb985e 100644 --- a/src/hooks/useProviderScrape.tsx +++ b/src/hooks/useProviderScrape.tsx @@ -10,7 +10,8 @@ import { getCachedMetadata, makeProviderUrl, } from "@/backend/helpers/providerApi"; -import { getLoadbalancedProviderApiUrl, providers } from "@/utils/providers"; +import { getLoadbalancedProviderApiUrl } from "@/backend/providers/fetchers"; +import { getProviders } from "@/backend/providers/providers"; export interface ScrapingItems { id: string; @@ -172,8 +173,8 @@ export function useScrape() { return getResult(sseOutput === "" ? null : sseOutput); } - if (!providers) return null; startScrape(); + const providers = getProviders(); const output = await providers.runAll({ media, events: { diff --git a/src/pages/parts/player/MetaPart.tsx b/src/pages/parts/player/MetaPart.tsx index 4930fffb..6d5b64ef 100644 --- a/src/pages/parts/player/MetaPart.tsx +++ b/src/pages/parts/player/MetaPart.tsx @@ -3,6 +3,7 @@ import { useNavigate, useParams } from "react-router-dom"; import { useAsync } from "react-use"; import type { AsyncReturnType } from "type-fest"; +import { isExtensionActive } from "@/backend/extension/messaging"; import { fetchMetadata, setCachedMetadata, @@ -10,6 +11,8 @@ import { import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; import { decodeTMDBId } from "@/backend/metadata/tmdb"; import { MWMediaType } from "@/backend/metadata/types/mw"; +import { getLoadbalancedProviderApiUrl } from "@/backend/providers/fetchers"; +import { getProviders } from "@/backend/providers/providers"; import { Button } from "@/components/buttons/Button"; import { Icons } from "@/components/Icon"; import { IconPill } from "@/components/layout/IconPill"; @@ -18,7 +21,6 @@ import { Paragraph } from "@/components/text/Paragraph"; import { Title } from "@/components/text/Title"; import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout"; import { conf } from "@/setup/config"; -import { getLoadbalancedProviderApiUrl, providers } from "@/utils/providers"; export interface MetaPartProps { onGetMeta?: (meta: DetailedMeta, episodeId?: string) => void; @@ -41,8 +43,12 @@ export function MetaPart(props: MetaPartProps) { const navigate = useNavigate(); const { error, value, loading } = useAsync(async () => { + // check extension + const isActive = await isExtensionActive(); + + // use api metadata or providers metadata const providerApiUrl = getLoadbalancedProviderApiUrl(); - if (providerApiUrl) { + if (providerApiUrl && !isActive) { try { await fetchMetadata(providerApiUrl); } catch (err) { @@ -50,11 +56,12 @@ export function MetaPart(props: MetaPartProps) { } } else { setCachedMetadata([ - ...providers.listSources(), - ...providers.listEmbeds(), + ...getProviders().listSources(), + ...getProviders().listEmbeds(), ]); } + // get media meta data let data: ReturnType = null; try { if (!params.media) throw new Error("no media params"); From 421186cb54737655446c3c87469697b4f82d180c Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 9 Jan 2024 21:56:39 +0100 Subject: [PATCH 03/40] Version checking + preparing streams --- src/backend/extension/compatibility.ts | 5 +++ src/backend/extension/messaging.ts | 17 +++++++- src/backend/extension/plasmo.ts | 4 +- src/backend/extension/streams.ts | 40 +++++++++++++++++++ .../player/hooks/useSourceSelection.ts | 3 ++ src/hooks/useProviderScrape.tsx | 3 ++ 6 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 src/backend/extension/compatibility.ts create mode 100644 src/backend/extension/streams.ts diff --git a/src/backend/extension/compatibility.ts b/src/backend/extension/compatibility.ts new file mode 100644 index 00000000..31471d26 --- /dev/null +++ b/src/backend/extension/compatibility.ts @@ -0,0 +1,5 @@ +const allowedExtensionVersion = ["0.0.1"]; + +export function isAllowedExtensionVersion(version: string): boolean { + return allowedExtensionVersion.includes(version); +} diff --git a/src/backend/extension/messaging.ts b/src/backend/extension/messaging.ts index ec0b4bf6..3c5480c1 100644 --- a/src/backend/extension/messaging.ts +++ b/src/backend/extension/messaging.ts @@ -3,6 +3,8 @@ import { sendToBackgroundViaRelay, } from "@plasmohq/messaging"; +import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; + let activeExtension = false; export interface ExtensionHello { @@ -35,7 +37,14 @@ export async function sendExtensionRequest( url: string, ops: any, ): Promise { - return sendMessage("proxy-request", { url, ...ops }); + return sendMessage("make-request", { url, ...ops }); +} + +export async function setDomainRule( + domains: string[], + headers: Record, +): Promise { + return sendMessage("prepare-stream", { domains, headers }); } export async function extensionInfo(): Promise { @@ -47,5 +56,9 @@ export function isExtensionActiveCached(): boolean { } export async function isExtensionActive(): Promise { - return !!(await extensionInfo()); + const info = await extensionInfo(); + if (!info) return false; + const allowedVersion = isAllowedExtensionVersion(info.version); + if (!allowedVersion) return false; + return true; } diff --git a/src/backend/extension/plasmo.ts b/src/backend/extension/plasmo.ts index 2e9cacbf..8c0093f0 100644 --- a/src/backend/extension/plasmo.ts +++ b/src/backend/extension/plasmo.ts @@ -20,11 +20,11 @@ export type ExtensionRequestReply = }; interface MmMetadata { - "declarative-net-request": { + "prepare-stream": { req: PlasmoRequestBody; res: ExtensionRequestReply; }; - "proxy-request": { + "make-request": { req: PlasmoRequestBody; res: ExtensionRequestReply; }; diff --git a/src/backend/extension/streams.ts b/src/backend/extension/streams.ts new file mode 100644 index 00000000..8d202992 --- /dev/null +++ b/src/backend/extension/streams.ts @@ -0,0 +1,40 @@ +import { Stream } from "@movie-web/providers"; + +import { setDomainRule } from "@/backend/extension/messaging"; + +function extractDomain(url: string): string { + try { + const u = new URL(url); + return u.hostname; + } catch { + return url; + } +} + +function extractDomainsFromStream(stream: Stream): string[] { + if (stream.type === "hls") { + return [extractDomain(stream.playlist)]; + } + if (stream.type === "file") { + return Object.values(stream.qualities).map((v) => extractDomain(v.url)); + } + return []; +} + +function buildHeadersFromStream(stream: Stream): Record { + const headers: Record = {}; + Object.entries(stream.headers ?? {}).forEach((entry) => { + headers[entry[0]] = entry[1]; + }); + Object.entries(stream.preferredHeaders ?? {}).forEach((entry) => { + headers[entry[0]] = entry[1]; + }); + return headers; +} + +export async function prepareStream(stream: Stream) { + await setDomainRule( + extractDomainsFromStream(stream), + buildHeadersFromStream(stream), + ); +} diff --git a/src/components/player/hooks/useSourceSelection.ts b/src/components/player/hooks/useSourceSelection.ts index bca884f7..b9e167d6 100644 --- a/src/components/player/hooks/useSourceSelection.ts +++ b/src/components/player/hooks/useSourceSelection.ts @@ -5,6 +5,7 @@ import { } from "@movie-web/providers"; import { useAsyncFn } from "react-use"; +import { prepareStream } from "@/backend/extension/streams"; import { connectServerSideEvents, makeProviderUrl, @@ -131,6 +132,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) { ]); if (result.stream) { + await prepareStream(result.stream[0]); setCaption(null); setSource( convertRunoutputToSource({ stream: result.stream[0] }), @@ -187,6 +189,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) { ]); setSourceId(sourceId); setCaption(null); + await prepareStream(embedResult.stream[0]); setSource( convertRunoutputToSource({ stream: embedResult.stream[0] }), convertProviderCaption(embedResult.stream[0].captions), diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx index 21cb985e..d6b48063 100644 --- a/src/hooks/useProviderScrape.tsx +++ b/src/hooks/useProviderScrape.tsx @@ -5,6 +5,7 @@ import { } from "@movie-web/providers"; import { RefObject, useCallback, useEffect, useRef, useState } from "react"; +import { prepareStream } from "@/backend/extension/streams"; import { connectServerSideEvents, getCachedMetadata, @@ -169,6 +170,7 @@ export function useScrape() { conn.on("update", updateEvent); conn.on("discoverEmbeds", discoverEmbedsEvent); const sseOutput = await conn.promise(); + if (sseOutput) await prepareStream(sseOutput.stream); return getResult(sseOutput === "" ? null : sseOutput); } @@ -184,6 +186,7 @@ export function useScrape() { discoverEmbeds: discoverEmbedsEvent, }, }); + if (output) await prepareStream(output.stream); return getResult(output); }, [ From 52bc66e7dd934c22913a562757048ec48bde0e58 Mon Sep 17 00:00:00 2001 From: Jorrin Date: Tue, 9 Jan 2024 23:00:54 +0100 Subject: [PATCH 04/40] improve typings --- src/backend/extension/messaging.ts | 51 ++++++++++++++--------- src/backend/extension/plasmo.ts | 67 ++++++++++++++++++++---------- src/backend/extension/streams.ts | 9 ++-- src/backend/providers/fetchers.ts | 9 +++- 4 files changed, 87 insertions(+), 49 deletions(-) diff --git a/src/backend/extension/messaging.ts b/src/backend/extension/messaging.ts index 3c5480c1..28f80ad2 100644 --- a/src/backend/extension/messaging.ts +++ b/src/backend/extension/messaging.ts @@ -7,18 +7,17 @@ import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; let activeExtension = false; -export interface ExtensionHello { - version: string; -} - -function sendMessage( - message: keyof MessagesMetadata, - payload: any, +function sendMessage( + message: MessageKey, + payload: MessagesMetadata[MessageKey]["req"], timeout: number = -1, ) { - return new Promise((resolve) => { + return new Promise((resolve) => { if (timeout >= 0) setTimeout(() => resolve(null), timeout); - sendToBackgroundViaRelay({ + sendToBackgroundViaRelay< + MessagesMetadata[MessageKey]["req"], + MessagesMetadata[MessageKey]["res"] + >({ name: message, body: payload, }) @@ -34,21 +33,33 @@ function sendMessage( } export async function sendExtensionRequest( - url: string, - ops: any, -): Promise { - return sendMessage("make-request", { url, ...ops }); + ops: Omit, +): Promise { + return sendMessage("makeRequest", { + requestDomain: window.location.origin, + ...ops, + }); } export async function setDomainRule( - domains: string[], - headers: Record, -): Promise { - return sendMessage("prepare-stream", { domains, headers }); + ops: Omit, +): Promise { + return sendMessage("prepareStream", { + requestDomain: window.location.origin, + ...ops, + }); } -export async function extensionInfo(): Promise { - return sendMessage("hello", null, 300); +export async function extensionInfo(): Promise< + MessagesMetadata["hello"]["res"] | null +> { + return sendMessage( + "hello", + { + requestDomain: window.location.origin, + }, + 300, + ); } export function isExtensionActiveCached(): boolean { @@ -57,7 +68,7 @@ export function isExtensionActiveCached(): boolean { export async function isExtensionActive(): Promise { const info = await extensionInfo(); - if (!info) return false; + if (!info?.success) return false; const allowedVersion = isAllowedExtensionVersion(info.version); if (!allowedVersion) return false; return true; diff --git a/src/backend/extension/plasmo.ts b/src/backend/extension/plasmo.ts index 8c0093f0..6c37ad14 100644 --- a/src/backend/extension/plasmo.ts +++ b/src/backend/extension/plasmo.ts @@ -1,6 +1,37 @@ -export interface PlasmoRequestBody { +export interface ExtensionBaseRequest { + requestDomain: string; +} + +export type ExtensionBaseResponse = + | ({ + success: true; + } & T) + | { + success: false; + error: string; + }; + +export type ExtensionHelloResponse = ExtensionBaseResponse<{ + version: string; +}>; + +export interface ExtensionMakeRequest extends ExtensionBaseRequest { + url: string; + method: string; + headers?: Record; + body?: string | FormData | URLSearchParams | Record; +} + +export type ExtensionMakeRequestResponse = ExtensionBaseResponse<{ + status: number; + requestHeaders: Record; + responseHeaders: Record; + data: string | Record; +}>; + +export interface ExtensionPrepareStreamRequest extends ExtensionBaseRequest { ruleId: number; - domain: string; + targetDomains: string[]; requestHeaders?: Record; responseHeaders?: Record; } @@ -9,28 +40,18 @@ export interface ExtensionHelloReply { version: string; } -export type ExtensionRequestReply = - | { - success: true; - ruleId: number; - } - | { - success: false; - error: string; - }; - -interface MmMetadata { - "prepare-stream": { - req: PlasmoRequestBody; - res: ExtensionRequestReply; - }; - "make-request": { - req: PlasmoRequestBody; - res: ExtensionRequestReply; - }; +export interface MmMetadata { hello: { - req: null; - res: ExtensionHelloReply; + req: ExtensionBaseRequest; + res: ExtensionHelloResponse; + }; + makeRequest: { + req: ExtensionMakeRequest; + res: ExtensionMakeRequestResponse; + }; + prepareStream: { + req: ExtensionPrepareStreamRequest; + res: ExtensionBaseResponse; }; } diff --git a/src/backend/extension/streams.ts b/src/backend/extension/streams.ts index 8d202992..2afb900d 100644 --- a/src/backend/extension/streams.ts +++ b/src/backend/extension/streams.ts @@ -33,8 +33,9 @@ function buildHeadersFromStream(stream: Stream): Record { } export async function prepareStream(stream: Stream) { - await setDomainRule( - extractDomainsFromStream(stream), - buildHeadersFromStream(stream), - ); + await setDomainRule({ + ruleId: 1, + targetDomains: extractDomainsFromStream(stream), + requestHeaders: buildHeadersFromStream(stream), + }); } diff --git a/src/backend/providers/fetchers.ts b/src/backend/providers/fetchers.ts index 596e8376..e133199c 100644 --- a/src/backend/providers/fetchers.ts +++ b/src/backend/providers/fetchers.ts @@ -54,8 +54,13 @@ export function makeLoadBalancedSimpleProxyFetcher() { } export function makeExtensionFetcher() { - const fetcher: Fetcher = async (a, b) => { - return sendExtensionRequest(a, b) as any; + const fetcher: Fetcher = async (url, ops) => { + return sendExtensionRequest({ + url, + method: ops?.method ?? "GET", + headers: ops?.headers, + body: ops?.body, + }) as any; }; return fetcher; } From 4bdb95ed0fd88fb456f03691c5a689c17b381a28 Mon Sep 17 00:00:00 2001 From: Jorrin Date: Tue, 9 Jan 2024 23:35:23 +0100 Subject: [PATCH 05/40] fix --- src/backend/providers/fetchers.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/backend/providers/fetchers.ts b/src/backend/providers/fetchers.ts index e133199c..f1dbbfb8 100644 --- a/src/backend/providers/fetchers.ts +++ b/src/backend/providers/fetchers.ts @@ -57,9 +57,7 @@ export function makeExtensionFetcher() { const fetcher: Fetcher = async (url, ops) => { return sendExtensionRequest({ url, - method: ops?.method ?? "GET", - headers: ops?.headers, - body: ops?.body, + ...ops, }) as any; }; return fetcher; From e1be30dde9ad62760b44300b945556791cad5e13 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Wed, 10 Jan 2024 18:20:51 +0100 Subject: [PATCH 06/40] Request messaging --- src/backend/extension/messaging.ts | 5 +++-- src/backend/extension/plasmo.ts | 14 ++++++++------ src/backend/providers/fetchers.ts | 24 ++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/backend/extension/messaging.ts b/src/backend/extension/messaging.ts index 28f80ad2..3d835a0e 100644 --- a/src/backend/extension/messaging.ts +++ b/src/backend/extension/messaging.ts @@ -4,6 +4,7 @@ import { } from "@plasmohq/messaging"; import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; +import { ExtensionMakeRequestResponse } from "@/backend/extension/plasmo"; let activeExtension = false; @@ -32,9 +33,9 @@ function sendMessage( }); } -export async function sendExtensionRequest( +export async function sendExtensionRequest( ops: Omit, -): Promise { +): Promise | null> { return sendMessage("makeRequest", { requestDomain: window.location.origin, ...ops, diff --git a/src/backend/extension/plasmo.ts b/src/backend/extension/plasmo.ts index 6c37ad14..0a2b3dc5 100644 --- a/src/backend/extension/plasmo.ts +++ b/src/backend/extension/plasmo.ts @@ -22,11 +22,13 @@ export interface ExtensionMakeRequest extends ExtensionBaseRequest { body?: string | FormData | URLSearchParams | Record; } -export type ExtensionMakeRequestResponse = ExtensionBaseResponse<{ - status: number; - requestHeaders: Record; - responseHeaders: Record; - data: string | Record; +export type ExtensionMakeRequestResponse = ExtensionBaseResponse<{ + response: { + statusCode: number; + headers: Record; + finalUrl: string; + body: T; + }; }>; export interface ExtensionPrepareStreamRequest extends ExtensionBaseRequest { @@ -47,7 +49,7 @@ export interface MmMetadata { }; makeRequest: { req: ExtensionMakeRequest; - res: ExtensionMakeRequestResponse; + res: ExtensionMakeRequestResponse; }; prepareStream: { req: ExtensionPrepareStreamRequest; diff --git a/src/backend/providers/fetchers.ts b/src/backend/providers/fetchers.ts index f1dbbfb8..9db649f5 100644 --- a/src/backend/providers/fetchers.ts +++ b/src/backend/providers/fetchers.ts @@ -53,12 +53,32 @@ export function makeLoadBalancedSimpleProxyFetcher() { return fetcher; } +function makeFinalHeaders( + readHeaders: string[], + headers: Record, +): Headers { + const lowercasedHeaders = readHeaders.map((v) => v.toLowerCase()); + return new Headers( + Object.entries(headers).filter((entry) => + lowercasedHeaders.includes(entry[0].toLowerCase()), + ), + ); +} + export function makeExtensionFetcher() { const fetcher: Fetcher = async (url, ops) => { - return sendExtensionRequest({ + const result = await sendExtensionRequest({ url, ...ops, - }) as any; + }); + if (!result?.success) throw new Error(`extension error: ${result?.error}`); + const res = result.response; + return { + body: res.body, + finalUrl: res.finalUrl, + statusCode: res.statusCode, + headers: makeFinalHeaders(ops.readHeaders, res.headers), + }; }; return fetcher; } From d32ef6ed9ae88db294a8c232c65018a65f1b4abc Mon Sep 17 00:00:00 2001 From: Jorrin Date: Wed, 10 Jan 2024 19:43:54 +0100 Subject: [PATCH 07/40] fix extension not preparing stream on embed selection --- src/components/player/hooks/useSourceSelection.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/player/hooks/useSourceSelection.ts b/src/components/player/hooks/useSourceSelection.ts index b9e167d6..3016e332 100644 --- a/src/components/player/hooks/useSourceSelection.ts +++ b/src/components/player/hooks/useSourceSelection.ts @@ -72,6 +72,7 @@ export function useEmbedScraping( report([ scrapeSourceOutputToProviderMetric(meta, sourceId, null, "success", null), ]); + await prepareStream(result.stream[0]); setSourceId(sourceId); setCaption(null); setSource( From 3704dfba10ae8ada3573c084848c7e0ebf8fb5c4 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Wed, 10 Jan 2024 22:04:21 +0100 Subject: [PATCH 08/40] Fix styling bugs, fix player not switching source after error, fix allowed state in extension, add ip locked sourced for extension Co-authored-by: Jip Frijlink --- src/backend/extension/messaging.ts | 7 +++++-- src/backend/extension/plasmo.ts | 5 +---- src/backend/providers/providers.ts | 1 + .../player/atoms/settings/SourceSelectingView.tsx | 2 +- src/stores/player/slices/source.ts | 10 +++++++++- src/utils/language.ts | 2 +- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/backend/extension/messaging.ts b/src/backend/extension/messaging.ts index 3d835a0e..4594fb23 100644 --- a/src/backend/extension/messaging.ts +++ b/src/backend/extension/messaging.ts @@ -37,7 +37,7 @@ export async function sendExtensionRequest( ops: Omit, ): Promise | null> { return sendMessage("makeRequest", { - requestDomain: window.location.origin, + requestDomain: window.location.origin, // TODO unsafe ...ops, }); } @@ -54,13 +54,16 @@ export async function setDomainRule( export async function extensionInfo(): Promise< MessagesMetadata["hello"]["res"] | null > { - return sendMessage( + const message = await sendMessage( "hello", { requestDomain: window.location.origin, }, 300, ); + if (!message?.success) return null; + if (!message.allowed) return null; + return message; } export function isExtensionActiveCached(): boolean { diff --git a/src/backend/extension/plasmo.ts b/src/backend/extension/plasmo.ts index 0a2b3dc5..e142ef6a 100644 --- a/src/backend/extension/plasmo.ts +++ b/src/backend/extension/plasmo.ts @@ -13,6 +13,7 @@ export type ExtensionBaseResponse = export type ExtensionHelloResponse = ExtensionBaseResponse<{ version: string; + allowed: boolean; }>; export interface ExtensionMakeRequest extends ExtensionBaseRequest { @@ -38,10 +39,6 @@ export interface ExtensionPrepareStreamRequest extends ExtensionBaseRequest { responseHeaders?: Record; } -export interface ExtensionHelloReply { - version: string; -} - export interface MmMetadata { hello: { req: ExtensionBaseRequest; diff --git a/src/backend/providers/providers.ts b/src/backend/providers/providers.ts index 1a7b484a..ac4a7dfa 100644 --- a/src/backend/providers/providers.ts +++ b/src/backend/providers/providers.ts @@ -15,6 +15,7 @@ export function getProviders() { return makeProviders({ fetcher: makeExtensionFetcher(), target: targets.BROWSER_EXTENSION, + consistentIpForRequests: true, }); } diff --git a/src/components/player/atoms/settings/SourceSelectingView.tsx b/src/components/player/atoms/settings/SourceSelectingView.tsx index 03d0875d..f995308e 100644 --- a/src/components/player/atoms/settings/SourceSelectingView.tsx +++ b/src/components/player/atoms/settings/SourceSelectingView.tsx @@ -147,7 +147,7 @@ export function SourceSelectionView({ router.navigate("/")}> {t("player.menus.sources.title")} - + {sources.map((v) => ( = (set, get) => ({ }, setSourceId(id) { set((s) => { + s.status = playerStatus.PLAYING; s.sourceId = id; }); }, @@ -153,6 +154,8 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ s.qualities = qualities as SourceQuality[]; s.currentQuality = loadableStream.quality; s.captionList = captions; + s.interface.error = undefined; + s.status = playerStatus.PLAYING; }); const store = get(); store.redisplaySource(startAt); @@ -166,7 +169,10 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ automaticQuality: qualityPreferences.quality.automaticQuality, lastChosenQuality: quality, }); - + set((s) => { + s.interface.error = undefined; + s.status = playerStatus.PLAYING; + }); store.display?.load({ source: loadableStream.stream, startAt, @@ -182,6 +188,8 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ if (!selectedQuality) return; set((s) => { s.currentQuality = quality; + s.status = playerStatus.PLAYING; + s.interface.error = undefined; }); store.display?.load({ source: selectedQuality, diff --git a/src/utils/language.ts b/src/utils/language.ts index 6fda7df8..41b8168b 100644 --- a/src/utils/language.ts +++ b/src/utils/language.ts @@ -86,7 +86,7 @@ function populateLanguageCode(language: string): string { * @returns pretty format for language, null if it no info can be found for language */ export function getPrettyLanguageNameFromLocale(locale: string): string | null { - const tag = getTag(populateLanguageCode(locale), true); + const tag = getTag(locale, true); const lang = tag?.language?.Description?.[0] ?? null; if (!lang) return null; From 6c7f1aceceab21bd5db888d2e1c2604084f0c0ae Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 11 Jan 2024 19:16:22 +0100 Subject: [PATCH 09/40] Remove requestDomain --- src/backend/extension/messaging.ts | 18 +++--------------- src/backend/extension/plasmo.ts | 4 +--- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/backend/extension/messaging.ts b/src/backend/extension/messaging.ts index 4594fb23..6d1f32fd 100644 --- a/src/backend/extension/messaging.ts +++ b/src/backend/extension/messaging.ts @@ -36,31 +36,19 @@ function sendMessage( export async function sendExtensionRequest( ops: Omit, ): Promise | null> { - return sendMessage("makeRequest", { - requestDomain: window.location.origin, // TODO unsafe - ...ops, - }); + return sendMessage("makeRequest", ops); } export async function setDomainRule( ops: Omit, ): Promise { - return sendMessage("prepareStream", { - requestDomain: window.location.origin, - ...ops, - }); + return sendMessage("prepareStream", ops); } export async function extensionInfo(): Promise< MessagesMetadata["hello"]["res"] | null > { - const message = await sendMessage( - "hello", - { - requestDomain: window.location.origin, - }, - 300, - ); + const message = await sendMessage("hello", {}, 300); if (!message?.success) return null; if (!message.allowed) return null; return message; diff --git a/src/backend/extension/plasmo.ts b/src/backend/extension/plasmo.ts index e142ef6a..12222b22 100644 --- a/src/backend/extension/plasmo.ts +++ b/src/backend/extension/plasmo.ts @@ -1,6 +1,4 @@ -export interface ExtensionBaseRequest { - requestDomain: string; -} +export interface ExtensionBaseRequest {} export type ExtensionBaseResponse = | ({ From ccbf888946082d339234f06317517a09aa65fa69 Mon Sep 17 00:00:00 2001 From: Jorrin Date: Thu, 11 Jan 2024 23:45:33 +0100 Subject: [PATCH 10/40] firefox support (kinda with manual permission set) --- src/backend/extension/streams.ts | 2 +- src/backend/providers/fetchers.ts | 36 ++++++++++++++++++++++--------- src/backend/providers/utils.ts | 29 +++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 src/backend/providers/utils.ts diff --git a/src/backend/extension/streams.ts b/src/backend/extension/streams.ts index 2afb900d..daa7a54c 100644 --- a/src/backend/extension/streams.ts +++ b/src/backend/extension/streams.ts @@ -34,7 +34,7 @@ function buildHeadersFromStream(stream: Stream): Record { export async function prepareStream(stream: Stream) { await setDomainRule({ - ruleId: 1, + ruleId: 2, targetDomains: extractDomainsFromStream(stream), requestHeaders: buildHeadersFromStream(stream), }); diff --git a/src/backend/providers/fetchers.ts b/src/backend/providers/fetchers.ts index 9db649f5..83e42835 100644 --- a/src/backend/providers/fetchers.ts +++ b/src/backend/providers/fetchers.ts @@ -1,9 +1,11 @@ import { Fetcher, makeSimpleProxyFetcher } from "@movie-web/providers"; -import { sendExtensionRequest } from "@/backend/extension/messaging"; +import { setDomainRule } from "@/backend/extension/messaging"; import { getApiToken, setApiToken } from "@/backend/helpers/providerApi"; import { getProviderApiUrls, getProxyUrls } from "@/utils/proxyUrls"; +import { makeFullUrl } from "./utils"; + function makeLoadbalancedList(getter: () => string[]) { let listIndex = -1; return () => { @@ -67,17 +69,31 @@ function makeFinalHeaders( export function makeExtensionFetcher() { const fetcher: Fetcher = async (url, ops) => { - const result = await sendExtensionRequest({ - url, - ...ops, + const fullUrl = makeFullUrl(url, ops); + const res = await setDomainRule({ + ruleId: 1, + targetDomains: [fullUrl], + requestHeaders: ops.headers, }); - if (!result?.success) throw new Error(`extension error: ${result?.error}`); - const res = result.response; + console.log(res, fullUrl); + const response = await fetch(fullUrl, { + method: ops.method, + headers: ops.headers, + body: ops.body as any, + }); + const contentType = response.headers.get("content-type"); + const body = contentType?.includes("application/json") + ? await response.json() + : await response.text(); + return { - body: res.body, - finalUrl: res.finalUrl, - statusCode: res.statusCode, - headers: makeFinalHeaders(ops.readHeaders, res.headers), + body, + finalUrl: response.url, + statusCode: response.status, + headers: makeFinalHeaders( + ops.readHeaders, + Object.fromEntries(response.headers.entries()), + ), }; }; return fetcher; diff --git a/src/backend/providers/utils.ts b/src/backend/providers/utils.ts new file mode 100644 index 00000000..7149046c --- /dev/null +++ b/src/backend/providers/utils.ts @@ -0,0 +1,29 @@ +import { DefaultedFetcherOptions } from "@movie-web/providers"; + +export function makeFullUrl( + url: string, + ops?: DefaultedFetcherOptions, +): string { + // glue baseUrl and rest of url together + let leftSide = ops?.baseUrl ?? ""; + let rightSide = url; + + // left side should always end with slash, if its set + if (leftSide.length > 0 && !leftSide.endsWith("/")) leftSide += "/"; + + // right side should never start with slash + if (rightSide.startsWith("/")) rightSide = rightSide.slice(1); + + const fullUrl = leftSide + rightSide; + if (!fullUrl.startsWith("http://") && !fullUrl.startsWith("https://")) + throw new Error( + `Invald URL -- URL doesn't start with a http scheme: '${fullUrl}'`, + ); + + const parsedUrl = new URL(fullUrl); + Object.entries(ops?.query ?? {}).forEach(([k, v]) => { + parsedUrl.searchParams.set(k, v as string); + }); + + return parsedUrl.toString(); +} From a4b925dc1b7749aa4caed3b4b4c174d38f018795 Mon Sep 17 00:00:00 2001 From: Jorrin Date: Fri, 12 Jan 2024 18:46:51 +0100 Subject: [PATCH 11/40] revert back to makeRequest --- src/backend/extension/streams.ts | 2 +- src/backend/providers/fetchers.ts | 38 +++++++++---------------------- src/backend/providers/utils.ts | 29 ----------------------- 3 files changed, 12 insertions(+), 57 deletions(-) delete mode 100644 src/backend/providers/utils.ts diff --git a/src/backend/extension/streams.ts b/src/backend/extension/streams.ts index daa7a54c..2afb900d 100644 --- a/src/backend/extension/streams.ts +++ b/src/backend/extension/streams.ts @@ -34,7 +34,7 @@ function buildHeadersFromStream(stream: Stream): Record { export async function prepareStream(stream: Stream) { await setDomainRule({ - ruleId: 2, + ruleId: 1, targetDomains: extractDomainsFromStream(stream), requestHeaders: buildHeadersFromStream(stream), }); diff --git a/src/backend/providers/fetchers.ts b/src/backend/providers/fetchers.ts index 83e42835..95267595 100644 --- a/src/backend/providers/fetchers.ts +++ b/src/backend/providers/fetchers.ts @@ -1,11 +1,9 @@ import { Fetcher, makeSimpleProxyFetcher } from "@movie-web/providers"; -import { setDomainRule } from "@/backend/extension/messaging"; +import { sendExtensionRequest } from "@/backend/extension/messaging"; import { getApiToken, setApiToken } from "@/backend/helpers/providerApi"; import { getProviderApiUrls, getProxyUrls } from "@/utils/proxyUrls"; -import { makeFullUrl } from "./utils"; - function makeLoadbalancedList(getter: () => string[]) { let listIndex = -1; return () => { @@ -69,31 +67,17 @@ function makeFinalHeaders( export function makeExtensionFetcher() { const fetcher: Fetcher = async (url, ops) => { - const fullUrl = makeFullUrl(url, ops); - const res = await setDomainRule({ - ruleId: 1, - targetDomains: [fullUrl], - requestHeaders: ops.headers, - }); - console.log(res, fullUrl); - const response = await fetch(fullUrl, { - method: ops.method, - headers: ops.headers, - body: ops.body as any, - }); - const contentType = response.headers.get("content-type"); - const body = contentType?.includes("application/json") - ? await response.json() - : await response.text(); - + const result = (await sendExtensionRequest({ + url, + ...ops, + })) as any; + if (!result?.success) throw new Error(`extension error: ${result?.error}`); + const res = result.response; return { - body, - finalUrl: response.url, - statusCode: response.status, - headers: makeFinalHeaders( - ops.readHeaders, - Object.fromEntries(response.headers.entries()), - ), + body: res.body, + finalUrl: res.finalUrl, + statusCode: res.statusCode, + headers: makeFinalHeaders(ops.readHeaders, res.headers), }; }; return fetcher; diff --git a/src/backend/providers/utils.ts b/src/backend/providers/utils.ts deleted file mode 100644 index 7149046c..00000000 --- a/src/backend/providers/utils.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { DefaultedFetcherOptions } from "@movie-web/providers"; - -export function makeFullUrl( - url: string, - ops?: DefaultedFetcherOptions, -): string { - // glue baseUrl and rest of url together - let leftSide = ops?.baseUrl ?? ""; - let rightSide = url; - - // left side should always end with slash, if its set - if (leftSide.length > 0 && !leftSide.endsWith("/")) leftSide += "/"; - - // right side should never start with slash - if (rightSide.startsWith("/")) rightSide = rightSide.slice(1); - - const fullUrl = leftSide + rightSide; - if (!fullUrl.startsWith("http://") && !fullUrl.startsWith("https://")) - throw new Error( - `Invald URL -- URL doesn't start with a http scheme: '${fullUrl}'`, - ); - - const parsedUrl = new URL(fullUrl); - Object.entries(ops?.query ?? {}).forEach(([k, v]) => { - parsedUrl.searchParams.set(k, v as string); - }); - - return parsedUrl.toString(); -} From 948843769820630fdc49e323bd64a788d968f7e6 Mon Sep 17 00:00:00 2001 From: Jorrin Date: Sun, 14 Jan 2024 18:19:38 +0100 Subject: [PATCH 12/40] check if extension has permissions --- src/backend/extension/messaging.ts | 10 +++++-- src/backend/extension/plasmo.ts | 8 ++++++ src/pages/parts/player/MetaPart.tsx | 43 ++++++++++++++++++++++++++--- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/backend/extension/messaging.ts b/src/backend/extension/messaging.ts index 6d1f32fd..2e2747c1 100644 --- a/src/backend/extension/messaging.ts +++ b/src/backend/extension/messaging.ts @@ -34,17 +34,23 @@ function sendMessage( } export async function sendExtensionRequest( - ops: Omit, + ops: MessagesMetadata["makeRequest"]["req"], ): Promise | null> { return sendMessage("makeRequest", ops); } export async function setDomainRule( - ops: Omit, + ops: MessagesMetadata["prepareStream"]["req"], ): Promise { return sendMessage("prepareStream", ops); } +export async function sendPage( + ops: MessagesMetadata["openPage"]["req"], +): Promise { + return sendMessage("openPage", ops); +} + export async function extensionInfo(): Promise< MessagesMetadata["hello"]["res"] | null > { diff --git a/src/backend/extension/plasmo.ts b/src/backend/extension/plasmo.ts index 12222b22..c13898be 100644 --- a/src/backend/extension/plasmo.ts +++ b/src/backend/extension/plasmo.ts @@ -12,6 +12,7 @@ export type ExtensionBaseResponse = export type ExtensionHelloResponse = ExtensionBaseResponse<{ version: string; allowed: boolean; + hasPermission: boolean; }>; export interface ExtensionMakeRequest extends ExtensionBaseRequest { @@ -50,6 +51,13 @@ export interface MmMetadata { req: ExtensionPrepareStreamRequest; res: ExtensionBaseResponse; }; + openPage: { + req: ExtensionBaseRequest & { + page: string; + redirectUrl: string; + }; + res: ExtensionBaseResponse; + }; } interface MpMetadata {} diff --git a/src/pages/parts/player/MetaPart.tsx b/src/pages/parts/player/MetaPart.tsx index 6d5b64ef..fb40379a 100644 --- a/src/pages/parts/player/MetaPart.tsx +++ b/src/pages/parts/player/MetaPart.tsx @@ -3,7 +3,8 @@ import { useNavigate, useParams } from "react-router-dom"; import { useAsync } from "react-use"; import type { AsyncReturnType } from "type-fest"; -import { isExtensionActive } from "@/backend/extension/messaging"; +import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; +import { extensionInfo, sendPage } from "@/backend/extension/messaging"; import { fetchMetadata, setCachedMetadata, @@ -43,12 +44,16 @@ export function MetaPart(props: MetaPartProps) { const navigate = useNavigate(); const { error, value, loading } = useAsync(async () => { - // check extension - const isActive = await isExtensionActive(); + const info = await extensionInfo(); + const isAllowed = info?.success && isAllowedExtensionVersion(info.version); + + if (isAllowed) { + if (!info.hasPermission) throw new Error("extension-no-permission"); + } // use api metadata or providers metadata const providerApiUrl = getLoadbalancedProviderApiUrl(); - if (providerApiUrl && !isActive) { + if (providerApiUrl && !isAllowed) { try { await fetchMetadata(providerApiUrl); } catch (err) { @@ -105,6 +110,36 @@ export function MetaPart(props: MetaPartProps) { props.onGetMeta?.(meta, epId); }, []); + if (error && error.message === "extension-no-permission") { + return ( + + + + {t("player.metadata.failed.badge")} + + Configure the extension + + You have the browser extension, but we need your permission to get + started using the extension. + + + + + ); + } + if (error && error.message === "dmca") { return ( From fa2b610ea6f3e4fc3d6481ed5f5c2090478adbd4 Mon Sep 17 00:00:00 2001 From: Jorrin Date: Sun, 14 Jan 2024 22:33:46 +0100 Subject: [PATCH 13/40] fix permission check on domain level --- src/backend/extension/messaging.ts | 6 ++---- src/pages/parts/player/MetaPart.tsx | 10 ++++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/backend/extension/messaging.ts b/src/backend/extension/messaging.ts index 2e2747c1..b738184a 100644 --- a/src/backend/extension/messaging.ts +++ b/src/backend/extension/messaging.ts @@ -10,7 +10,7 @@ let activeExtension = false; function sendMessage( message: MessageKey, - payload: MessagesMetadata[MessageKey]["req"], + payload: MessagesMetadata[MessageKey]["req"] | undefined = undefined, timeout: number = -1, ) { return new Promise((resolve) => { @@ -54,9 +54,7 @@ export async function sendPage( export async function extensionInfo(): Promise< MessagesMetadata["hello"]["res"] | null > { - const message = await sendMessage("hello", {}, 300); - if (!message?.success) return null; - if (!message.allowed) return null; + const message = await sendMessage("hello", undefined, 300); return message; } diff --git a/src/pages/parts/player/MetaPart.tsx b/src/pages/parts/player/MetaPart.tsx index fb40379a..1e18b6d1 100644 --- a/src/pages/parts/player/MetaPart.tsx +++ b/src/pages/parts/player/MetaPart.tsx @@ -45,15 +45,17 @@ export function MetaPart(props: MetaPartProps) { const { error, value, loading } = useAsync(async () => { const info = await extensionInfo(); - const isAllowed = info?.success && isAllowedExtensionVersion(info.version); + const isValidExtension = + info?.success && isAllowedExtensionVersion(info.version); - if (isAllowed) { - if (!info.hasPermission) throw new Error("extension-no-permission"); + if (isValidExtension) { + if (!info.allowed || !info.hasPermission) + throw new Error("extension-no-permission"); } // use api metadata or providers metadata const providerApiUrl = getLoadbalancedProviderApiUrl(); - if (providerApiUrl && !isAllowed) { + if (providerApiUrl && !isValidExtension) { try { await fetchMetadata(providerApiUrl); } catch (err) { From 925f3dff199fc6457eb9f97209addf780700e565 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 16 Jan 2024 20:28:33 +0100 Subject: [PATCH 14/40] Basic onboarding structure --- src/components/layout/Stepper.tsx | 25 ++++++++++ src/components/layout/ThinContainer.tsx | 14 ++++++ src/pages/PlayerView.tsx | 32 ++++++++++++- src/pages/layouts/MinimalPageLayout.tsx | 28 ++++++++++++ src/pages/onboarding/Onboarding.tsx | 48 ++++++++++++++++++++ src/pages/onboarding/OnboardingExtension.tsx | 27 +++++++++++ src/pages/onboarding/OnboardingProxy.tsx | 27 +++++++++++ src/pages/onboarding/onboardingHooks.ts | 22 +++++++++ src/setup/App.tsx | 9 ++++ src/setup/config.ts | 4 ++ src/stores/history/index.ts | 3 +- src/stores/onboarding/index.tsx | 22 +++++++++ src/utils/onboarding.ts | 23 ++++++++++ 13 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 src/components/layout/Stepper.tsx create mode 100644 src/pages/layouts/MinimalPageLayout.tsx create mode 100644 src/pages/onboarding/Onboarding.tsx create mode 100644 src/pages/onboarding/OnboardingExtension.tsx create mode 100644 src/pages/onboarding/OnboardingProxy.tsx create mode 100644 src/pages/onboarding/onboardingHooks.ts create mode 100644 src/stores/onboarding/index.tsx create mode 100644 src/utils/onboarding.ts diff --git a/src/components/layout/Stepper.tsx b/src/components/layout/Stepper.tsx new file mode 100644 index 00000000..d0c9c499 --- /dev/null +++ b/src/components/layout/Stepper.tsx @@ -0,0 +1,25 @@ +export interface StepperProps { + current: number; + steps: number; + className?: string; +} + +export function Stepper(props: StepperProps) { + const percentage = (props.current / (props.steps + 1)) * 100; + + return ( +
+

+ {props.current}/{props.steps} +

+
+
+
+
+ ); +} diff --git a/src/components/layout/ThinContainer.tsx b/src/components/layout/ThinContainer.tsx index f7f90acb..345afa5c 100644 --- a/src/components/layout/ThinContainer.tsx +++ b/src/components/layout/ThinContainer.tsx @@ -1,3 +1,4 @@ +import classNames from "classnames"; import { ReactNode } from "react"; interface ThinContainerProps { @@ -16,3 +17,16 @@ export function ThinContainer(props: ThinContainerProps) {
); } + +export function CenterContainer(props: ThinContainerProps) { + return ( +
+
{props.children}
+
+ ); +} diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx index aa73bd83..5282d9b3 100644 --- a/src/pages/PlayerView.tsx +++ b/src/pages/PlayerView.tsx @@ -1,6 +1,12 @@ import { RunOutput } from "@movie-web/providers"; import { useCallback, useEffect, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; +import { + Navigate, + useLocation, + useNavigate, + useParams, +} from "react-router-dom"; +import { useAsync } from "react-use"; import { usePlayer } from "@/components/player/hooks/usePlayer"; import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta"; @@ -15,9 +21,10 @@ import { ScrapeErrorPart } from "@/pages/parts/player/ScrapeErrorPart"; import { ScrapingPart } from "@/pages/parts/player/ScrapingPart"; import { useLastNonPlayerLink } from "@/stores/history"; import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; +import { needsOnboarding } from "@/utils/onboarding"; import { parseTimestamp } from "@/utils/timestamp"; -export function PlayerView() { +export function RealPlayerView() { const navigate = useNavigate(); const params = useParams<{ media: string; @@ -109,4 +116,25 @@ export function PlayerView() { ); } +export function PlayerView() { + const loc = useLocation(); + const { loading, error, value } = useAsync(() => { + return needsOnboarding(); + }); + + if (error) throw new Error("Failed to detect onboarding"); + if (loading) return null; + if (value) + return ( + + ); + return ; +} + export default PlayerView; diff --git a/src/pages/layouts/MinimalPageLayout.tsx b/src/pages/layouts/MinimalPageLayout.tsx new file mode 100644 index 00000000..6d2cf34d --- /dev/null +++ b/src/pages/layouts/MinimalPageLayout.tsx @@ -0,0 +1,28 @@ +import { Link } from "react-router-dom"; + +import { BrandPill } from "@/components/layout/BrandPill"; +import { BlurEllipsis } from "@/pages/layouts/SubPageLayout"; + +export function MinimalPageLayout(props: { children: React.ReactNode }) { + return ( +
+ + {/* Main page */} +
+ + + +
+
{props.children}
+
+ ); +} diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx new file mode 100644 index 00000000..439cc87d --- /dev/null +++ b/src/pages/onboarding/Onboarding.tsx @@ -0,0 +1,48 @@ +import { useNavigate } from "react-router-dom"; + +import { Button } from "@/components/buttons/Button"; +import { Stepper } from "@/components/layout/Stepper"; +import { CenterContainer } from "@/components/layout/ThinContainer"; +import { Modal, ModalCard, useModal } from "@/components/overlays/Modal"; +import { Heading2, Paragraph } from "@/components/utils/Text"; +import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; +import { useRedirectBack } from "@/pages/onboarding/onboardingHooks"; +import { PageTitle } from "@/pages/parts/util/PageTitle"; + +export function OnboardingPage() { + const navigate = useNavigate(); + const skipModal = useModal("skip"); + const { skipAndRedirect } = useRedirectBack(); + + return ( + + + + + + Lorem ipsum + Lorem ipsum Lorem ipsum Lorem ipsum + + + + + + + + Lorem ipsum + Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum + + + + + + ); +} diff --git a/src/pages/onboarding/OnboardingExtension.tsx b/src/pages/onboarding/OnboardingExtension.tsx new file mode 100644 index 00000000..8ee85be7 --- /dev/null +++ b/src/pages/onboarding/OnboardingExtension.tsx @@ -0,0 +1,27 @@ +import { useNavigate } from "react-router-dom"; + +import { Button } from "@/components/buttons/Button"; +import { Stepper } from "@/components/layout/Stepper"; +import { CenterContainer } from "@/components/layout/ThinContainer"; +import { Heading2, Paragraph } from "@/components/utils/Text"; +import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; +import { PageTitle } from "@/pages/parts/util/PageTitle"; + +export function OnboardingExtensionPage() { + const navigate = useNavigate(); + + return ( + + + + + Lorem ipsum + Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum + + + + + ); +} diff --git a/src/pages/onboarding/OnboardingProxy.tsx b/src/pages/onboarding/OnboardingProxy.tsx new file mode 100644 index 00000000..45d22aea --- /dev/null +++ b/src/pages/onboarding/OnboardingProxy.tsx @@ -0,0 +1,27 @@ +import { useNavigate } from "react-router-dom"; + +import { Button } from "@/components/buttons/Button"; +import { Stepper } from "@/components/layout/Stepper"; +import { CenterContainer } from "@/components/layout/ThinContainer"; +import { Heading2, Paragraph } from "@/components/utils/Text"; +import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; +import { PageTitle } from "@/pages/parts/util/PageTitle"; + +export function OnboardingProxyPage() { + const navigate = useNavigate(); + + return ( + + + + + Lorem ipsum + Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum + + + + + ); +} diff --git a/src/pages/onboarding/onboardingHooks.ts b/src/pages/onboarding/onboardingHooks.ts new file mode 100644 index 00000000..e80234e7 --- /dev/null +++ b/src/pages/onboarding/onboardingHooks.ts @@ -0,0 +1,22 @@ +import { useCallback } from "react"; +import { useNavigate } from "react-router-dom"; + +import { useQueryParam } from "@/hooks/useQueryParams"; +import { useOnboardingStore } from "@/stores/onboarding"; + +export function useRedirectBack() { + const [url] = useQueryParam("redirect"); + const navigate = useNavigate(); + const setSkipped = useOnboardingStore((s) => s.setSkipped); + + const redirectBack = useCallback(() => { + navigate(url ?? "/"); + }, [navigate, url]); + + const skipAndRedirect = useCallback(() => { + setSkipped(true); + redirectBack(); + }, [redirectBack, setSkipped]); + + return { redirectBack, skipAndRedirect }; +} diff --git a/src/setup/App.tsx b/src/setup/App.tsx index afa5f8ba..37c55e0f 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -19,6 +19,9 @@ import { DmcaPage, shouldHaveDmcaPage } from "@/pages/Dmca"; import { NotFoundPage } from "@/pages/errors/NotFoundPage"; import { HomePage } from "@/pages/HomePage"; import { LoginPage } from "@/pages/Login"; +import { OnboardingPage } from "@/pages/onboarding/Onboarding"; +import { OnboardingExtensionPage } from "@/pages/onboarding/OnboardingExtension"; +import { OnboardingProxyPage } from "@/pages/onboarding/OnboardingProxy"; import { RegisterPage } from "@/pages/Register"; import { Layout } from "@/setup/Layout"; import { useHistoryListener } from "@/stores/history"; @@ -119,6 +122,12 @@ function App() { } /> } /> } /> + } /> + } + /> + } /> {shouldHaveDmcaPage() ? ( } /> diff --git a/src/setup/config.ts b/src/setup/config.ts index 2e1634a4..d6aca1cb 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -19,6 +19,7 @@ interface Config { DISALLOWED_IDS: string; TURNSTILE_KEY: string; CDN_REPLACEMENTS: string; + HAS_ONBOARDING: string; } export interface RuntimeConfig { @@ -34,6 +35,7 @@ export interface RuntimeConfig { DISALLOWED_IDS: string[]; TURNSTILE_KEY: string | null; CDN_REPLACEMENTS: Array; + HAS_ONBOARDING: boolean; } const env: Record = { @@ -49,6 +51,7 @@ const env: Record = { DISALLOWED_IDS: import.meta.env.VITE_DISALLOWED_IDS, TURNSTILE_KEY: import.meta.env.VITE_TURNSTILE_KEY, CDN_REPLACEMENTS: import.meta.env.VITE_CDN_REPLACEMENTS, + HAS_ONBOARDING: import.meta.env.VITE_HAS_ONBOARDING, }; // loads from different locations, in order: environment (VITE_{KEY}), window (public/config.js) @@ -82,6 +85,7 @@ export function conf(): RuntimeConfig { .split(",") .map((v) => v.trim()), NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", + HAS_ONBOARDING: getKey("HAS_ONBOARDING", "false") === "true", TURNSTILE_KEY: turnstileKey.length > 0 ? turnstileKey : null, DISALLOWED_IDS: getKey("DISALLOWED_IDS", "") .split(",") diff --git a/src/stores/history/index.ts b/src/stores/history/index.ts index c90a7309..c1f507ee 100644 --- a/src/stores/history/index.ts +++ b/src/stores/history/index.ts @@ -46,7 +46,8 @@ export function useLastNonPlayerLink() { (v) => !v.path.startsWith("/media") && // cannot be a player link location.pathname !== v.path && // cannot be current link - !v.path.startsWith("/s/"), // cannot be a quick search link + !v.path.startsWith("/s/") && // cannot be a quick search link + !v.path.startsWith("/onboarding"), // cannot be an onboarding link ); return route?.path ?? "/"; }, [routes, location]); diff --git a/src/stores/onboarding/index.tsx b/src/stores/onboarding/index.tsx new file mode 100644 index 00000000..17d1965d --- /dev/null +++ b/src/stores/onboarding/index.tsx @@ -0,0 +1,22 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { immer } from "zustand/middleware/immer"; + +export interface OnboardingStore { + skipped: boolean; + setSkipped(v: boolean): void; +} + +export const useOnboardingStore = create( + persist( + immer((set) => ({ + skipped: false, + setSkipped(v) { + set((s) => { + s.skipped = v; + }); + }, + })), + { name: "__MW::onboarding" }, + ), +); diff --git a/src/utils/onboarding.ts b/src/utils/onboarding.ts new file mode 100644 index 00000000..7289fd1c --- /dev/null +++ b/src/utils/onboarding.ts @@ -0,0 +1,23 @@ +import { isExtensionActive } from "@/backend/extension/messaging"; +import { conf } from "@/setup/config"; +import { useAuthStore } from "@/stores/auth"; +import { useOnboardingStore } from "@/stores/onboarding"; + +export async function needsOnboarding(): Promise { + // if onboarding is dislabed, no onboarding needed + if (!conf().HAS_ONBOARDING) return false; + + // if extension is active and working, no onboarding needed + const extensionActive = await isExtensionActive(); + if (extensionActive) return false; + + // if there is any custom proxy urls, no onboarding needed + const proxyUrls = useAuthStore.getState().proxySet; + if (proxyUrls) return false; + + // if onboarding has been skipped, no onboarding needed + const skipped = useOnboardingStore.getState().skipped; + if (skipped) return false; + + return true; +} From a226f3347cb77affffca21dd00fb512f7bc0388b Mon Sep 17 00:00:00 2001 From: mrjvs Date: Tue, 16 Jan 2024 22:07:21 +0100 Subject: [PATCH 15/40] Add onboarding functionality --- src/pages/onboarding/Onboarding.tsx | 4 +- src/pages/onboarding/OnboardingExtension.tsx | 55 +++++++++++++++++++- src/pages/onboarding/OnboardingProxy.tsx | 27 ++++++++-- src/pages/onboarding/onboardingHooks.ts | 10 ++-- src/stores/onboarding/index.tsx | 10 ++-- src/utils/onboarding.ts | 6 +-- 6 files changed, 92 insertions(+), 20 deletions(-) diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx index 439cc87d..3247a7ee 100644 --- a/src/pages/onboarding/Onboarding.tsx +++ b/src/pages/onboarding/Onboarding.tsx @@ -12,7 +12,7 @@ import { PageTitle } from "@/pages/parts/util/PageTitle"; export function OnboardingPage() { const navigate = useNavigate(); const skipModal = useModal("skip"); - const { skipAndRedirect } = useRedirectBack(); + const { completeAndRedirect } = useRedirectBack(); return ( @@ -25,7 +25,7 @@ export function OnboardingPage() { - diff --git a/src/pages/onboarding/OnboardingExtension.tsx b/src/pages/onboarding/OnboardingExtension.tsx index 8ee85be7..c3fa5380 100644 --- a/src/pages/onboarding/OnboardingExtension.tsx +++ b/src/pages/onboarding/OnboardingExtension.tsx @@ -1,14 +1,64 @@ +import { ReactNode } from "react"; import { useNavigate } from "react-router-dom"; +import { useAsyncFn, useInterval } from "react-use"; +import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; +import { extensionInfo } from "@/backend/extension/messaging"; import { Button } from "@/components/buttons/Button"; import { Stepper } from "@/components/layout/Stepper"; import { CenterContainer } from "@/components/layout/ThinContainer"; import { Heading2, Paragraph } from "@/components/utils/Text"; import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; +import { useRedirectBack } from "@/pages/onboarding/onboardingHooks"; import { PageTitle } from "@/pages/parts/util/PageTitle"; +type ExtensionStatus = + | "unknown" + | "failed" + | "disallowed" + | "noperms" + | "outdated" + | "success"; + +async function getExtensionState(): Promise { + const info = await extensionInfo(); + if (!info) return "unknown"; // cant talk to extension + if (!info.success) return "failed"; // extension failed to respond + if (!info.allowed) return "disallowed"; // extension is not enabled on this page + if (!info.hasPermission) return "noperms"; // extension has no perms to do it's tasks + if (!isAllowedExtensionVersion(info.version)) return "outdated"; // extension is too old + return "success"; // no problems +} + +export function ExtensionStatus(props: { + status: ExtensionStatus; + loading: boolean; +}) { + let content: ReactNode = null; + if (props.loading || props.status === "unknown") + content =

waiting on extension...

; + if (props.status === "disallowed") + content =

Extension disabled for this page

; + else if (props.status === "failed") content =

Failed to request status

; + else if (props.status === "outdated") content =

Extension too old

; + else if (props.status === "noperms") content =

No permissions to act

; + else if (props.status === "success") content =

Extension is working!

; + return
{content}
; +} + export function OnboardingExtensionPage() { const navigate = useNavigate(); + const { completeAndRedirect } = useRedirectBack(); + + const [{ loading, value }, exec] = useAsyncFn( + async (triggeredManually: boolean = false) => { + const status = await getExtensionState(); + if (status === "success" && triggeredManually) completeAndRedirect(); + return status; + }, + [completeAndRedirect], + ); + useInterval(exec, 1000); return ( @@ -17,9 +67,10 @@ export function OnboardingExtensionPage() { Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum + - diff --git a/src/pages/onboarding/OnboardingProxy.tsx b/src/pages/onboarding/OnboardingProxy.tsx index 45d22aea..0cb2b38b 100644 --- a/src/pages/onboarding/OnboardingProxy.tsx +++ b/src/pages/onboarding/OnboardingProxy.tsx @@ -1,14 +1,33 @@ +import { useState } from "react"; import { useNavigate } from "react-router-dom"; +import { useAsyncFn } from "react-use"; +import { singularProxiedFetch } from "@/backend/helpers/fetch"; import { Button } from "@/components/buttons/Button"; import { Stepper } from "@/components/layout/Stepper"; import { CenterContainer } from "@/components/layout/ThinContainer"; +import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; import { Heading2, Paragraph } from "@/components/utils/Text"; import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; +import { useRedirectBack } from "@/pages/onboarding/onboardingHooks"; import { PageTitle } from "@/pages/parts/util/PageTitle"; +import { useAuthStore } from "@/stores/auth"; + +const testUrl = "https://postman-echo.com/get"; export function OnboardingProxyPage() { const navigate = useNavigate(); + const { completeAndRedirect } = useRedirectBack(); + const [url, setUrl] = useState(""); + const setProxySet = useAuthStore((s) => s.setProxySet); + + const [{ loading, error }, test] = useAsyncFn(async () => { + if (!url.startsWith("http")) throw new Error("Not a valid URL"); + const res = await singularProxiedFetch(url, testUrl, {}); + if (res.url !== testUrl) throw new Error("Not a proxy"); + setProxySet([url]); + completeAndRedirect(); + }, [url, completeAndRedirect, setProxySet]); return ( @@ -17,10 +36,12 @@ export function OnboardingProxyPage() { Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum - - + ); diff --git a/src/pages/onboarding/onboardingHooks.ts b/src/pages/onboarding/onboardingHooks.ts index e80234e7..bcbbea48 100644 --- a/src/pages/onboarding/onboardingHooks.ts +++ b/src/pages/onboarding/onboardingHooks.ts @@ -7,16 +7,16 @@ import { useOnboardingStore } from "@/stores/onboarding"; export function useRedirectBack() { const [url] = useQueryParam("redirect"); const navigate = useNavigate(); - const setSkipped = useOnboardingStore((s) => s.setSkipped); + const setCompleted = useOnboardingStore((s) => s.setCompleted); const redirectBack = useCallback(() => { navigate(url ?? "/"); }, [navigate, url]); - const skipAndRedirect = useCallback(() => { - setSkipped(true); + const completeAndRedirect = useCallback(() => { + setCompleted(true); redirectBack(); - }, [redirectBack, setSkipped]); + }, [redirectBack, setCompleted]); - return { redirectBack, skipAndRedirect }; + return { completeAndRedirect }; } diff --git a/src/stores/onboarding/index.tsx b/src/stores/onboarding/index.tsx index 17d1965d..be5af563 100644 --- a/src/stores/onboarding/index.tsx +++ b/src/stores/onboarding/index.tsx @@ -3,17 +3,17 @@ import { persist } from "zustand/middleware"; import { immer } from "zustand/middleware/immer"; export interface OnboardingStore { - skipped: boolean; - setSkipped(v: boolean): void; + completed: boolean; + setCompleted(v: boolean): void; } export const useOnboardingStore = create( persist( immer((set) => ({ - skipped: false, - setSkipped(v) { + completed: false, + setCompleted(v) { set((s) => { - s.skipped = v; + s.completed = v; }); }, })), diff --git a/src/utils/onboarding.ts b/src/utils/onboarding.ts index 7289fd1c..c2678b1c 100644 --- a/src/utils/onboarding.ts +++ b/src/utils/onboarding.ts @@ -15,9 +15,9 @@ export async function needsOnboarding(): Promise { const proxyUrls = useAuthStore.getState().proxySet; if (proxyUrls) return false; - // if onboarding has been skipped, no onboarding needed - const skipped = useOnboardingStore.getState().skipped; - if (skipped) return false; + // if onboarding has been completed, no onboarding needed + const completed = useOnboardingStore.getState().completed; + if (completed) return false; return true; } From 965cc56570424e4929ff0754b5d9ff731ceebc84 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 18 Jan 2024 19:21:43 +0100 Subject: [PATCH 16/40] Fix bug where stream freezes without extension Co-authored-by: Jip Frijlink --- src/hooks/useProviderScrape.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx index d6b48063..fd27c06d 100644 --- a/src/hooks/useProviderScrape.tsx +++ b/src/hooks/useProviderScrape.tsx @@ -5,6 +5,7 @@ import { } from "@movie-web/providers"; import { RefObject, useCallback, useEffect, useRef, useState } from "react"; +import { isExtensionActiveCached } from "@/backend/extension/messaging"; import { prepareStream } from "@/backend/extension/streams"; import { connectServerSideEvents, @@ -186,7 +187,8 @@ export function useScrape() { discoverEmbeds: discoverEmbedsEvent, }, }); - if (output) await prepareStream(output.stream); + if (output && isExtensionActiveCached()) + await prepareStream(output.stream); return getResult(output); }, [ From 3b2be31691edd47f7cf9d07e5d79b9845733ce43 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 18 Jan 2024 19:22:39 +0100 Subject: [PATCH 17/40] Add thumbnail generation as a setting Co-authored-by: Jip Frijlink --- src/components/player/base/Container.tsx | 4 +++- src/hooks/useSettingsState.ts | 16 ++++++++++++- src/pages/Settings.tsx | 15 +++++++++--- .../{LocalePart.tsx => PreferencesPart.tsx} | 7 +++++- src/stores/preferences/index.tsx | 24 +++++++++++++++++++ 5 files changed, 60 insertions(+), 6 deletions(-) rename src/pages/parts/settings/{LocalePart.tsx => PreferencesPart.tsx} (83%) create mode 100644 src/stores/preferences/index.tsx diff --git a/src/components/player/base/Container.tsx b/src/components/player/base/Container.tsx index 9fc10bff..a4310ad8 100644 --- a/src/components/player/base/Container.tsx +++ b/src/components/player/base/Container.tsx @@ -11,6 +11,7 @@ import { VideoClickTarget } from "@/components/player/internals/VideoClickTarget import { VideoContainer } from "@/components/player/internals/VideoContainer"; import { PlayerHoverState } from "@/stores/player/slices/interface"; import { usePlayerStore } from "@/stores/player/store"; +import { usePreferencesStore } from "@/stores/preferences"; export interface PlayerProps { children?: ReactNode; @@ -78,6 +79,7 @@ function BaseContainer(props: { children?: ReactNode }) { export function Container(props: PlayerProps) { const propRef = useRef(props.onLoad); + const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails); useEffect(() => { propRef.current?.(); }, []); @@ -86,7 +88,7 @@ export function Container(props: PlayerProps) {
- + {enableThumbnails ? : null} diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts index 01d76610..d0946deb 100644 --- a/src/hooks/useSettingsState.ts +++ b/src/hooks/useSettingsState.ts @@ -49,6 +49,7 @@ export function useSettingsState( icon: string; } | undefined, + enableThumbnails: boolean, ) { const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] = useDerived(proxyUrls); @@ -71,6 +72,12 @@ export function useSettingsState( ] = useDerived(deviceName); const [profileState, setProfileState, resetProfile, profileChanged] = useDerived(profile); + const [ + enableThumbnailsState, + setEnableThumbnailsState, + resetEnableThumbnails, + enableThumbnailsChanged, + ] = useDerived(enableThumbnails); function reset() { resetTheme(); @@ -80,6 +87,7 @@ export function useSettingsState( resetBackendUrl(); resetDeviceName(); resetProfile(); + resetEnableThumbnails(); } const changed = @@ -89,7 +97,8 @@ export function useSettingsState( deviceNameChanged || backendUrlChanged || proxyUrlsChanged || - profileChanged; + profileChanged || + enableThumbnailsChanged; return { reset, @@ -129,5 +138,10 @@ export function useSettingsState( set: setProfileState, changed: profileChanged, }, + enableThumbnails: { + state: enableThumbnailsState, + set: setEnableThumbnailsState, + changed: enableThumbnailsChanged, + }, }; } diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 0fb27d85..18b755ca 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -31,11 +31,12 @@ import { ThemePart } from "@/pages/parts/settings/ThemePart"; import { PageTitle } from "@/pages/parts/util/PageTitle"; import { AccountWithToken, useAuthStore } from "@/stores/auth"; import { useLanguageStore } from "@/stores/language"; +import { usePreferencesStore } from "@/stores/preferences"; import { useSubtitleStore } from "@/stores/subtitles"; import { useThemeStore } from "@/stores/theme"; import { SubPageLayout } from "./layouts/SubPageLayout"; -import { LocalePart } from "./parts/settings/LocalePart"; +import { PreferencesPart } from "./parts/settings/PreferencesPart"; function SettingsLayout(props: { children: React.ReactNode }) { const { isMobile } = useIsMobile(); @@ -115,6 +116,9 @@ export function SettingsPage() { const backendUrlSetting = useAuthStore((s) => s.backendUrl); const setBackendUrl = useAuthStore((s) => s.setBackendUrl); + const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails); + const setEnableThumbnails = usePreferencesStore((s) => s.setEnableThumbnails); + const account = useAuthStore((s) => s.account); const updateProfile = useAuthStore((s) => s.setAccountProfile); const updateDeviceName = useAuthStore((s) => s.updateDeviceName); @@ -136,6 +140,7 @@ export function SettingsPage() { proxySet, backendUrlSetting, account?.profile, + enableThumbnails, ); const saveChanges = useCallback(async () => { @@ -168,6 +173,7 @@ export function SettingsPage() { } } + setEnableThumbnails(state.enableThumbnails.state); setAppLanguage(state.appLanguage.state); setTheme(state.theme.state); setSubStyling(state.subtitleStyling.state); @@ -186,6 +192,7 @@ export function SettingsPage() { state, account, backendUrl, + setEnableThumbnails, setAppLanguage, setTheme, setSubStyling, @@ -225,10 +232,12 @@ export function SettingsPage() { )}
-
- +
diff --git a/src/pages/parts/settings/LocalePart.tsx b/src/pages/parts/settings/PreferencesPart.tsx similarity index 83% rename from src/pages/parts/settings/LocalePart.tsx rename to src/pages/parts/settings/PreferencesPart.tsx index 87819451..ea06db6f 100644 --- a/src/pages/parts/settings/LocalePart.tsx +++ b/src/pages/parts/settings/PreferencesPart.tsx @@ -6,9 +6,11 @@ import { Heading1 } from "@/components/utils/Text"; import { appLanguageOptions } from "@/setup/i18n"; import { getLocaleInfo, sortLangCodes } from "@/utils/language"; -export function LocalePart(props: { +export function PreferencesPart(props: { language: string; setLanguage: (l: string) => void; + enableThumbnails: boolean; + setEnableThumbnails: (v: boolean) => void; }) { const { t } = useTranslation(); const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code)); @@ -39,6 +41,9 @@ export function LocalePart(props: { selectedItem={selected || options[0]} setSelectedItem={(opt) => props.setLanguage(opt.id)} /> +

props.setEnableThumbnails(!props.enableThumbnails)}> + thumbnails: {props.enableThumbnails.toString()} +

); } diff --git a/src/stores/preferences/index.tsx b/src/stores/preferences/index.tsx new file mode 100644 index 00000000..65fae22d --- /dev/null +++ b/src/stores/preferences/index.tsx @@ -0,0 +1,24 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { immer } from "zustand/middleware/immer"; + +export interface PreferencesStore { + enableThumbnails: boolean; + setEnableThumbnails(v: boolean): void; +} + +export const usePreferencesStore = create( + persist( + immer((set) => ({ + enableThumbnails: false, + setEnableThumbnails(v) { + set((s) => { + s.enableThumbnails = v; + }); + }, + })), + { + name: "__MW::preferences", + }, + ), +); From dc95bd7455adc85a3ef9fe7ee986c18b9486150f Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 18 Jan 2024 19:22:55 +0100 Subject: [PATCH 18/40] Add full layout and styling for the first step in onboarding Co-authored-by: Jip Frijlink --- src/components/layout/Stepper.tsx | 4 +- src/components/layout/ThinContainer.tsx | 2 +- src/pages/onboarding/Onboarding.tsx | 67 ++++++++++++++++++++++--- src/pages/onboarding/utils.tsx | 59 ++++++++++++++++++++++ themes/default.ts | 14 ++++++ 5 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 src/pages/onboarding/utils.tsx diff --git a/src/components/layout/Stepper.tsx b/src/components/layout/Stepper.tsx index d0c9c499..867a438c 100644 --- a/src/components/layout/Stepper.tsx +++ b/src/components/layout/Stepper.tsx @@ -12,9 +12,9 @@ export function Stepper(props: StepperProps) {

{props.current}/{props.steps}

-
+
-
{props.children}
+
{props.children}
); } diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx index 3247a7ee..05879507 100644 --- a/src/pages/onboarding/Onboarding.tsx +++ b/src/pages/onboarding/Onboarding.tsx @@ -1,14 +1,24 @@ +import classNames from "classnames"; import { useNavigate } from "react-router-dom"; import { Button } from "@/components/buttons/Button"; import { Stepper } from "@/components/layout/Stepper"; import { CenterContainer } from "@/components/layout/ThinContainer"; import { Modal, ModalCard, useModal } from "@/components/overlays/Modal"; -import { Heading2, Paragraph } from "@/components/utils/Text"; +import { Heading1, Heading2, Paragraph } from "@/components/utils/Text"; import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; import { useRedirectBack } from "@/pages/onboarding/onboardingHooks"; +import { Card, CardContent, Link } from "@/pages/onboarding/utils"; import { PageTitle } from "@/pages/parts/util/PageTitle"; +function VerticalLine(props: { className?: string }) { + return ( +
+
+
+ ); +} + export function OnboardingPage() { const navigate = useNavigate(); const skipModal = useModal("skip"); @@ -20,7 +30,7 @@ export function OnboardingPage() { - Lorem ipsum + Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum - + */} ); diff --git a/src/pages/onboarding/utils.tsx b/src/pages/onboarding/utils.tsx new file mode 100644 index 00000000..eaf722b2 --- /dev/null +++ b/src/pages/onboarding/utils.tsx @@ -0,0 +1,59 @@ +import classNames from "classnames"; + +import { Icon, Icons } from "@/components/Icon"; +import { Heading2, Heading3, Paragraph } from "@/components/utils/Text"; + +export function Card(props: { + children: React.ReactNode; + onClick: () => void; +}) { + return ( +
+ {props.children} +
+ ); +} + +export function CardContent(props: { + title: string; + description: string; + subtitle: string; + colorClass: string; + children?: React.ReactNode; +}) { + return ( +
+
+ + + {props.subtitle} + + {props.title} + + {props.description} + +
+
{props.children}
+
+ ); +} + +export function Link(props: { children: React.ReactNode }) { + return ( + + {props.children} + + + ); +} diff --git a/themes/default.ts b/themes/default.ts index 7a010338..bf2510ff 100644 --- a/themes/default.ts +++ b/themes/default.ts @@ -228,10 +228,24 @@ export const defaultTheme = { } }, + // Utilities utils: { divider: tokens.ash.c300, }, + // Onboarding + onboarding: { + bar: tokens.shade.c400, + barFilled: tokens.purple.c300, + divider: tokens.shade.c200, + card: tokens.shade.c800, + cardHover: tokens.shade.c700, + border: tokens.shade.c600, + good: tokens.purple.c100, + best: tokens.semantic.yellow.c100, + link: tokens.purple.c100, + }, + // Error page errors: { card: tokens.shade.c800, From b3222b130fdc32bddae05a8b92db6cf1fb26d90a Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 18 Jan 2024 19:29:58 +0100 Subject: [PATCH 19/40] Fix some thumbnail scraping setting logic Co-authored-by: Jip Frijlink --- src/components/player/base/Container.tsx | 4 +--- src/components/player/hooks/useSourceSelection.ts | 7 ++++--- src/components/player/internals/ThumbnailScraper.tsx | 10 ++++++---- src/hooks/useProviderScrape.tsx | 3 ++- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/player/base/Container.tsx b/src/components/player/base/Container.tsx index a4310ad8..9fc10bff 100644 --- a/src/components/player/base/Container.tsx +++ b/src/components/player/base/Container.tsx @@ -11,7 +11,6 @@ import { VideoClickTarget } from "@/components/player/internals/VideoClickTarget import { VideoContainer } from "@/components/player/internals/VideoContainer"; import { PlayerHoverState } from "@/stores/player/slices/interface"; import { usePlayerStore } from "@/stores/player/store"; -import { usePreferencesStore } from "@/stores/preferences"; export interface PlayerProps { children?: ReactNode; @@ -79,7 +78,6 @@ function BaseContainer(props: { children?: ReactNode }) { export function Container(props: PlayerProps) { const propRef = useRef(props.onLoad); - const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails); useEffect(() => { propRef.current?.(); }, []); @@ -88,7 +86,7 @@ export function Container(props: PlayerProps) {
- {enableThumbnails ? : null} + diff --git a/src/components/player/hooks/useSourceSelection.ts b/src/components/player/hooks/useSourceSelection.ts index 3016e332..3813fa9f 100644 --- a/src/components/player/hooks/useSourceSelection.ts +++ b/src/components/player/hooks/useSourceSelection.ts @@ -5,6 +5,7 @@ import { } from "@movie-web/providers"; import { useAsyncFn } from "react-use"; +import { isExtensionActiveCached } from "@/backend/extension/messaging"; import { prepareStream } from "@/backend/extension/streams"; import { connectServerSideEvents, @@ -72,7 +73,7 @@ export function useEmbedScraping( report([ scrapeSourceOutputToProviderMetric(meta, sourceId, null, "success", null), ]); - await prepareStream(result.stream[0]); + if (isExtensionActiveCached()) await prepareStream(result.stream[0]); setSourceId(sourceId); setCaption(null); setSource( @@ -133,7 +134,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) { ]); if (result.stream) { - await prepareStream(result.stream[0]); + if (isExtensionActiveCached()) await prepareStream(result.stream[0]); setCaption(null); setSource( convertRunoutputToSource({ stream: result.stream[0] }), @@ -190,7 +191,7 @@ export function useSourceScraping(sourceId: string | null, routerId: string) { ]); setSourceId(sourceId); setCaption(null); - await prepareStream(embedResult.stream[0]); + if (isExtensionActiveCached()) await prepareStream(embedResult.stream[0]); setSource( convertRunoutputToSource({ stream: embedResult.stream[0] }), convertProviderCaption(embedResult.stream[0].captions), diff --git a/src/components/player/internals/ThumbnailScraper.tsx b/src/components/player/internals/ThumbnailScraper.tsx index b0fc8474..cede84c8 100644 --- a/src/components/player/internals/ThumbnailScraper.tsx +++ b/src/components/player/internals/ThumbnailScraper.tsx @@ -5,6 +5,7 @@ import { playerStatus } from "@/stores/player/slices/source"; import { ThumbnailImage } from "@/stores/player/slices/thumbnails"; import { usePlayerStore } from "@/stores/player/store"; import { LoadableSource, selectQuality } from "@/stores/player/utils/qualities"; +import { usePreferencesStore } from "@/stores/preferences"; import { processCdnLink } from "@/utils/cdn"; import { isSafari } from "@/utils/detectFeatures"; @@ -128,6 +129,7 @@ export function ThumbnailScraper() { const resetImages = usePlayerStore((s) => s.thumbnails.resetImages); const meta = usePlayerStore((s) => s.meta); const source = usePlayerStore((s) => s.source); + const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails); const workerRef = useRef(null); // object references dont always trigger changes, so we serialize it to detect *any* change @@ -159,8 +161,8 @@ export function ThumbnailScraper() { // start worker with the stream useEffect(() => { - startRef.current(); - }, [sourceSeralized]); + if (enableThumbnails) startRef.current(); + }, [sourceSeralized, enableThumbnails]); // destroy worker on unmount useEffect(() => { @@ -183,8 +185,8 @@ export function ThumbnailScraper() { workerRef.current.destroy(); workerRef.current = null; } - startRef.current(); - }, [serializedMeta, sourceSeralized, status]); + if (enableThumbnails) startRef.current(); + }, [serializedMeta, sourceSeralized, status, enableThumbnails]); return null; } diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx index fd27c06d..84aeec4e 100644 --- a/src/hooks/useProviderScrape.tsx +++ b/src/hooks/useProviderScrape.tsx @@ -171,7 +171,8 @@ export function useScrape() { conn.on("update", updateEvent); conn.on("discoverEmbeds", discoverEmbedsEvent); const sseOutput = await conn.promise(); - if (sseOutput) await prepareStream(sseOutput.stream); + if (sseOutput && isExtensionActiveCached()) + await prepareStream(sseOutput.stream); return getResult(sseOutput === "" ? null : sseOutput); } From 47d680da8a18662d3ec9fb2366284378a5fecf69 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 18 Jan 2024 20:37:27 +0100 Subject: [PATCH 20/40] Add some hover effects --- src/pages/onboarding/Onboarding.tsx | 4 ++-- src/pages/onboarding/utils.tsx | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx index 05879507..3387f044 100644 --- a/src/pages/onboarding/Onboarding.tsx +++ b/src/pages/onboarding/Onboarding.tsx @@ -70,7 +70,7 @@ export function OnboardingPage() { navigate("/onboarding/extension")}> @@ -84,7 +84,7 @@ export function OnboardingPage() { use the default setup diff --git a/src/pages/onboarding/utils.tsx b/src/pages/onboarding/utils.tsx index eaf722b2..410875b7 100644 --- a/src/pages/onboarding/utils.tsx +++ b/src/pages/onboarding/utils.tsx @@ -1,4 +1,5 @@ import classNames from "classnames"; +import { ReactNode } from "react"; import { Icon, Icons } from "@/components/Icon"; import { Heading2, Heading3, Paragraph } from "@/components/utils/Text"; @@ -18,9 +19,9 @@ export function Card(props: { } export function CardContent(props: { - title: string; - description: string; - subtitle: string; + title: ReactNode; + description: ReactNode; + subtitle: ReactNode; colorClass: string; children?: React.ReactNode; }) { @@ -51,9 +52,12 @@ export function CardContent(props: { export function Link(props: { children: React.ReactNode }) { return ( - + {props.children} - + ); } From a362559d9c482e9fba843900adf11cc87b1e0180 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Fri, 19 Jan 2024 19:30:25 +0100 Subject: [PATCH 21/40] Add onboarding state to settings --- src/pages/parts/settings/ConnectionsPart.tsx | 2 + src/pages/parts/settings/SetupPart.tsx | 79 ++++++++++++++++++++ src/pages/parts/settings/SidebarPart.tsx | 4 +- 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 src/pages/parts/settings/SetupPart.tsx diff --git a/src/pages/parts/settings/ConnectionsPart.tsx b/src/pages/parts/settings/ConnectionsPart.tsx index 007a6220..8a01f2b3 100644 --- a/src/pages/parts/settings/ConnectionsPart.tsx +++ b/src/pages/parts/settings/ConnectionsPart.tsx @@ -8,6 +8,7 @@ import { SettingsCard } from "@/components/layout/SettingsCard"; import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; import { Divider } from "@/components/utils/Divider"; import { Heading1 } from "@/components/utils/Text"; +import { SetupPart } from "@/pages/parts/settings/SetupPart"; interface ProxyEditProps { proxyUrls: string[] | null; @@ -147,6 +148,7 @@ export function ConnectionsPart(props: BackendEditProps & ProxyEditProps) {
{t("settings.connections.title")}
+ ((resolve, reject) => { + setTimeout(() => reject(new Error("Timed out!")), 1000); + singularProxiedFetch(url, testUrl, {}) + .then((res) => { + if (res.url !== testUrl) return reject(new Error("Not a proxy")); + resolve(); + }) + .catch(reject); + }); +} + +function useIsSetup() { + const proxyUrls = useAuthStore((s) => s.proxySet); + const { loading, value } = useAsync(async (): Promise => { + const extensionStatus: Status = (await isExtensionActive()) + ? "success" + : "unset"; + let proxyStatus: Status = "unset"; + if (proxyUrls && proxyUrls.length > 0) { + try { + await testProxy(proxyUrls[0]); + proxyStatus = "success"; + } catch { + proxyStatus = "error"; + } + } + return { + extension: extensionStatus, + proxy: proxyStatus, + defaultProxy: "success", + }; + }, [proxyUrls]); + + let globalState: Status = "unset"; + if (value?.extension === "success" || value?.proxy === "success") + globalState = "success"; + if (value?.proxy === "error" || value?.extension === "error") + globalState = "error"; + + return { + setupStates: value, + globalState, + loading, + }; +} + +export function SetupPart() { + const navigate = useNavigate(); + const { loading, setupStates, globalState } = useIsSetup(); + if (loading || !setupStates) return

Loading states...

; + return ( +
+

state: {globalState}

+

extension: {setupStates.extension}

+

proxy: {setupStates.proxy}

+

defaults: {setupStates.defaultProxy}

+ +
+ ); +} diff --git a/src/pages/parts/settings/SidebarPart.tsx b/src/pages/parts/settings/SidebarPart.tsx index 2b6e5c3f..560d9e5a 100644 --- a/src/pages/parts/settings/SidebarPart.tsx +++ b/src/pages/parts/settings/SidebarPart.tsx @@ -45,8 +45,8 @@ export function SidebarPart() { }, { textKey: "settings.locale.title", - id: "settings-locale", - icon: Icons.BOOKMARK, + id: "settings-preferences", + icon: Icons.SETTINGS, }, { textKey: "settings.appearance.title", From bb147a1367a43203e35058c4d25cd61592009b1f Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Jan 2024 00:24:06 +0100 Subject: [PATCH 22/40] Fixed flares --- src/components/utils/Flare.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/utils/Flare.tsx b/src/components/utils/Flare.tsx index 1fc20825..9c19e027 100644 --- a/src/components/utils/Flare.tsx +++ b/src/components/utils/Flare.tsx @@ -69,7 +69,7 @@ function Light(props: FlareProps) { }, )} style={{ - backgroundImage: `radial-gradient(circle at center, rgba(var(${cssVar}), 1), rgba(var(${cssVar}), 0) 70%)`, + backgroundImage: `radial-gradient(circle at center, rgba(var(${cssVar}) / 1), rgba(var(${cssVar}) / 0) 70%)`, backgroundPosition: `var(--bg-x) var(--bg-y)`, backgroundRepeat: "no-repeat", backgroundSize: `${size.toFixed(0)}px ${size.toFixed(0)}px`, @@ -85,7 +85,7 @@ function Light(props: FlareProps) {
Date: Sat, 20 Jan 2024 11:18:16 +0100 Subject: [PATCH 23/40] Style onboarding pages Co-authored-by: William Oldham --- src/components/layout/Stepper.tsx | 2 +- src/components/text-inputs/AuthInputBox.tsx | 5 +- src/components/utils/ErrorLine.tsx | 18 +++++++ src/pages/onboarding/Onboarding.tsx | 8 ---- src/pages/onboarding/OnboardingExtension.tsx | 42 +++++++++++++---- src/pages/onboarding/OnboardingProxy.tsx | 49 +++++++++++++++----- src/pages/onboarding/utils.tsx | 34 ++++++++++++-- 7 files changed, 123 insertions(+), 35 deletions(-) create mode 100644 src/components/utils/ErrorLine.tsx diff --git a/src/components/layout/Stepper.tsx b/src/components/layout/Stepper.tsx index 867a438c..75dc150b 100644 --- a/src/components/layout/Stepper.tsx +++ b/src/components/layout/Stepper.tsx @@ -5,7 +5,7 @@ export interface StepperProps { } export function Stepper(props: StepperProps) { - const percentage = (props.current / (props.steps + 1)) * 100; + const percentage = (props.current / props.steps) * 100; return (
diff --git a/src/components/text-inputs/AuthInputBox.tsx b/src/components/text-inputs/AuthInputBox.tsx index c79c079d..2e34594a 100644 --- a/src/components/text-inputs/AuthInputBox.tsx +++ b/src/components/text-inputs/AuthInputBox.tsx @@ -1,3 +1,5 @@ +import classNames from "classnames"; + import { TextInputControl } from "./TextInputControl"; export function AuthInputBox(props: { @@ -8,9 +10,10 @@ export function AuthInputBox(props: { placeholder?: string; onChange?: (data: string) => void; passwordToggleable?: boolean; + className?: string; }) { return ( -
+
{props.label ? (

{props.label}

) : null} diff --git a/src/components/utils/ErrorLine.tsx b/src/components/utils/ErrorLine.tsx new file mode 100644 index 00000000..b0761fee --- /dev/null +++ b/src/components/utils/ErrorLine.tsx @@ -0,0 +1,18 @@ +import classNames from "classnames"; +import { ReactNode } from "react"; + +import { Icon, Icons } from "@/components/Icon"; + +export function ErrorLine(props: { children?: ReactNode; className?: string }) { + return ( +

+ + {props.children} +

+ ); +} diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx index 3387f044..01e4631b 100644 --- a/src/pages/onboarding/Onboarding.tsx +++ b/src/pages/onboarding/Onboarding.tsx @@ -89,14 +89,6 @@ export function OnboardingPage() { use the default setup

- - {/* - - */} ); diff --git a/src/pages/onboarding/OnboardingExtension.tsx b/src/pages/onboarding/OnboardingExtension.tsx index c3fa5380..b2e98c83 100644 --- a/src/pages/onboarding/OnboardingExtension.tsx +++ b/src/pages/onboarding/OnboardingExtension.tsx @@ -5,11 +5,13 @@ import { useAsyncFn, useInterval } from "react-use"; import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; import { extensionInfo } from "@/backend/extension/messaging"; import { Button } from "@/components/buttons/Button"; +import { Loading } from "@/components/layout/Loading"; import { Stepper } from "@/components/layout/Stepper"; import { CenterContainer } from "@/components/layout/ThinContainer"; import { Heading2, Paragraph } from "@/components/utils/Text"; import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; import { useRedirectBack } from "@/pages/onboarding/onboardingHooks"; +import { Card, Link } from "@/pages/onboarding/utils"; import { PageTitle } from "@/pages/parts/util/PageTitle"; type ExtensionStatus = @@ -36,14 +38,25 @@ export function ExtensionStatus(props: { }) { let content: ReactNode = null; if (props.loading || props.status === "unknown") - content =

waiting on extension...

; + content = ( + <> + +

waiting on extension

+ + ); if (props.status === "disallowed") content =

Extension disabled for this page

; else if (props.status === "failed") content =

Failed to request status

; else if (props.status === "outdated") content =

Extension too old

; else if (props.status === "noperms") content =

No permissions to act

; else if (props.status === "success") content =

Extension is working!

; - return
{content}
; + return ( + +
+ {content} +
+
+ ); } export function OnboardingExtensionPage() { @@ -65,13 +78,26 @@ export function OnboardingExtensionPage() { - Lorem ipsum - Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum + + Let's start with an extension + + + Using the browser extension, you can get the best streams we have to + offer. With just a simple install. + + + Install extension + + - - +
+ + +
); diff --git a/src/pages/onboarding/OnboardingProxy.tsx b/src/pages/onboarding/OnboardingProxy.tsx index 0cb2b38b..be001de6 100644 --- a/src/pages/onboarding/OnboardingProxy.tsx +++ b/src/pages/onboarding/OnboardingProxy.tsx @@ -7,9 +7,12 @@ import { Button } from "@/components/buttons/Button"; import { Stepper } from "@/components/layout/Stepper"; import { CenterContainer } from "@/components/layout/ThinContainer"; import { AuthInputBox } from "@/components/text-inputs/AuthInputBox"; +import { Divider } from "@/components/utils/Divider"; +import { ErrorLine } from "@/components/utils/ErrorLine"; import { Heading2, Paragraph } from "@/components/utils/Text"; import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; import { useRedirectBack } from "@/pages/onboarding/onboardingHooks"; +import { Link } from "@/pages/onboarding/utils"; import { PageTitle } from "@/pages/parts/util/PageTitle"; import { useAuthStore } from "@/stores/auth"; @@ -23,10 +26,14 @@ export function OnboardingProxyPage() { const [{ loading, error }, test] = useAsyncFn(async () => { if (!url.startsWith("http")) throw new Error("Not a valid URL"); - const res = await singularProxiedFetch(url, testUrl, {}); - if (res.url !== testUrl) throw new Error("Not a proxy"); - setProxySet([url]); - completeAndRedirect(); + try { + const res = await singularProxiedFetch(url, testUrl, {}); + if (res.url !== testUrl) throw new Error("Not a proxy"); + setProxySet([url]); + completeAndRedirect(); + } catch (e) { + throw new Error("Could not connect to proxy"); + } }, [url, completeAndRedirect, setProxySet]); return ( @@ -34,14 +41,32 @@ export function OnboardingProxyPage() { - Lorem ipsum - Lorem ipsum Lorem ipsum Lorem ipsum Lorem ipsum - - {error ?

url invalid

: null} - - + + Let's setup a custom proxy + + + Using a custom proxy, you can get great quality streams! + + Learn how to make a custom proxy +
+ + {error ? {error.message} : null} +
+ +
+ + +
); diff --git a/src/pages/onboarding/utils.tsx b/src/pages/onboarding/utils.tsx index 410875b7..e8129230 100644 --- a/src/pages/onboarding/utils.tsx +++ b/src/pages/onboarding/utils.tsx @@ -1,16 +1,22 @@ import classNames from "classnames"; import { ReactNode } from "react"; +import { useNavigate } from "react-router-dom"; import { Icon, Icons } from "@/components/Icon"; import { Heading2, Heading3, Paragraph } from "@/components/utils/Text"; export function Card(props: { - children: React.ReactNode; - onClick: () => void; + children?: React.ReactNode; + onClick?: () => void; }) { return (
{props.children} @@ -50,9 +56,27 @@ export function CardContent(props: { ); } -export function Link(props: { children: React.ReactNode }) { +export function Link(props: { + children?: React.ReactNode; + to?: string; + href?: string; + className?: string; + target?: "_blank"; +}) { + const navigate = useNavigate(); return ( - + { + if (props.to) navigate(props.to); + }} + href={props.href} + target={props.target} + className={classNames( + "text-onboarding-link cursor-pointer inline-flex gap-2 items-center group hover:opacity-75 transition-opacity", + props.className, + )} + rel="noreferrer" + > {props.children} Date: Sat, 20 Jan 2024 11:53:24 +0100 Subject: [PATCH 24/40] Localize all onboarding screens --- src/assets/locales/en.json | 62 +++++++++++++++++++- src/pages/onboarding/Onboarding.tsx | 51 ++++++++-------- src/pages/onboarding/OnboardingExtension.tsx | 37 ++++++++---- src/pages/onboarding/OnboardingProxy.tsx | 28 +++++---- 4 files changed, 128 insertions(+), 50 deletions(-) diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 9329ec0e..77ac5aab 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -97,7 +97,8 @@ "login": "Login", "pagetitle": "{{title}} - movie-web", "register": "Register", - "settings": "Settings" + "settings": "Settings", + "onboarding": "Setup" } }, "home": { @@ -429,5 +430,64 @@ } }, "unsaved": "You have unsaved changes" + }, + "onboarding": { + "start": { + "title": "Let's get you setup with movie-web", + "explainer": "To get the best streams possible. You will need to choose which streaming method you want to use.", + "options": { + "proxy": { + "quality": "Good quality", + "title": "Custom proxy", + "description": "Setup a proxy in just 5 minutes and gain access to great sources.", + "action": "Setup proxy" + }, + "extension": { + "quality": "Best quality", + "title": "Browser extension", + "description": "Install browser extension and gain access to the best sources.", + "action": "Install extension" + }, + "default": { + "text": "I don't want good quality streams,<0 /> <1>use the default setup" + } + } + }, + "proxy": { + "title": "Let's make a new proxy", + "explainer": "With the proxy method, you can get great quality streams by making a self-service proxy.", + "link": "Learn how to make a proxy", + "input": { + "label": "Proxy URL", + "placeholder": "https://", + "errorInvalidUrl": "Not a valid URL", + "errorConnection": "Could not connect to proxy", + "errorNotProxy": "Expected a proxy but got a website" + }, + "back": "Go back", + "submit": "Submit proxy" + }, + "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.", + "link": "Install extension", + "back": "Go back", + "status": { + "loading": "Waiting on extension", + "disallowed": "Extension disabled for this page", + "failed": "Failed to request status", + "outdated": "Extension version too old", + "noperms": "Extension does not have sufficient permissions", + "success": "Extension is working as expected!" + }, + "submitCheck": "Check for extension", + "submitFinal": "Continue" + }, + "defaultConfirm": { + "title": "Are you sure?", + "description": "The default setup does not have the best streams and can be unbearably slow.", + "cancel": "Cancel", + "confirm": "Use default setup" + } } } diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx index 01e4631b..0d1e5a06 100644 --- a/src/pages/onboarding/Onboarding.tsx +++ b/src/pages/onboarding/Onboarding.tsx @@ -1,4 +1,5 @@ import classNames from "classnames"; +import { Trans, useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { Button } from "@/components/buttons/Button"; @@ -23,20 +24,23 @@ export function OnboardingPage() { const navigate = useNavigate(); const skipModal = useModal("skip"); const { completeAndRedirect } = useRedirectBack(); + const { t } = useTranslation(); return ( - + - Lorem ipsum - Lorem ipsum Lorem ipsum Lorem ipsum + + {t("onboarding.defaultConfirm.title")} + + {t("onboarding.defaultConfirm.description")} @@ -44,22 +48,21 @@ export function OnboardingPage() { - Let's get you set up with movie-web + {t("onboarding.start.title")} - To get the best streams possible, you will need to choose which - streaming method you want to use. + {t("onboarding.start.explainer")}
navigate("/onboarding/proxy")}> - Set up proxy + {t("onboarding.start.options.proxy.action")}
@@ -70,24 +73,24 @@ export function OnboardingPage() { navigate("/onboarding/extension")}> - Install extension + {t("onboarding.start.options.extension.action")}

- I don't want good quality,
-
- use the default setup - + +
+ +

diff --git a/src/pages/onboarding/OnboardingExtension.tsx b/src/pages/onboarding/OnboardingExtension.tsx index b2e98c83..ce699884 100644 --- a/src/pages/onboarding/OnboardingExtension.tsx +++ b/src/pages/onboarding/OnboardingExtension.tsx @@ -1,4 +1,5 @@ import { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useAsyncFn, useInterval } from "react-use"; @@ -36,20 +37,26 @@ export function ExtensionStatus(props: { status: ExtensionStatus; loading: boolean; }) { + const { t } = useTranslation(); + let content: ReactNode = null; if (props.loading || props.status === "unknown") content = ( <> -

waiting on extension

+

{t("onboarding.extension.status.loading")}

); if (props.status === "disallowed") - content =

Extension disabled for this page

; - else if (props.status === "failed") content =

Failed to request status

; - else if (props.status === "outdated") content =

Extension too old

; - else if (props.status === "noperms") content =

No permissions to act

; - else if (props.status === "success") content =

Extension is working!

; + content =

{t("onboarding.extension.status.disallowed")}

; + else if (props.status === "failed") + content =

{t("onboarding.extension.status.failed")}

; + else if (props.status === "outdated") + content =

{t("onboarding.extension.status.outdated")}

; + else if (props.status === "noperms") + content =

{t("onboarding.extension.status.noperms")}

; + else if (props.status === "success") + content =

{t("onboarding.extension.status.success")}

; return (
@@ -60,6 +67,7 @@ export function ExtensionStatus(props: { } export function OnboardingExtensionPage() { + const { t } = useTranslation(); const navigate = useNavigate(); const { completeAndRedirect } = useRedirectBack(); @@ -75,27 +83,30 @@ export function OnboardingExtensionPage() { return ( - + - Let's start with an extension + {t("onboarding.extension.title")} - Using the browser extension, you can get the best streams we have to - offer. With just a simple install. + {t("onboarding.extension.explainer")} - Install extension + {t("onboarding.extension.link")}
diff --git a/src/pages/onboarding/OnboardingProxy.tsx b/src/pages/onboarding/OnboardingProxy.tsx index be001de6..24595568 100644 --- a/src/pages/onboarding/OnboardingProxy.tsx +++ b/src/pages/onboarding/OnboardingProxy.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { useAsyncFn } from "react-use"; @@ -19,52 +20,55 @@ import { useAuthStore } from "@/stores/auth"; const testUrl = "https://postman-echo.com/get"; export function OnboardingProxyPage() { + const { t } = useTranslation(); const navigate = useNavigate(); const { completeAndRedirect } = useRedirectBack(); const [url, setUrl] = useState(""); const setProxySet = useAuthStore((s) => s.setProxySet); const [{ loading, error }, test] = useAsyncFn(async () => { - if (!url.startsWith("http")) throw new Error("Not a valid URL"); + if (!url.startsWith("http")) + throw new Error("onboarding.proxy.input.errorInvalidUrl"); try { const res = await singularProxiedFetch(url, testUrl, {}); - if (res.url !== testUrl) throw new Error("Not a proxy"); + if (res.url !== testUrl) + throw new Error("onboarding.proxy.input.errorNotProxy"); setProxySet([url]); completeAndRedirect(); } catch (e) { - throw new Error("Could not connect to proxy"); + throw new Error("onboarding.proxy.input.errorConnection"); } }, [url, completeAndRedirect, setProxySet]); return ( - + - Let's setup a custom proxy + {t("onboarding.proxy.title")} - Using a custom proxy, you can get great quality streams! + {t("onboarding.proxy.explainer")} - Learn how to make a custom proxy + {t("onboarding.proxy.link")}
- {error ? {error.message} : null} + {error ? {t(error.message)} : null}
From de5d47a7302e41c782263e7b40a7f487a1631986 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Jan 2024 12:24:02 +0100 Subject: [PATCH 25/40] Theme skip setup card --- src/components/overlays/Modal.tsx | 2 +- src/pages/onboarding/Onboarding.tsx | 16 +++++++++------- themes/default.ts | 5 +++++ themes/list/blue.ts | 4 ++++ themes/list/gray.ts | 4 ++++ themes/list/red.ts | 4 ++++ themes/list/teal.ts | 4 ++++ 7 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/components/overlays/Modal.tsx b/src/components/overlays/Modal.tsx index fc2c97f4..08abdbba 100644 --- a/src/components/overlays/Modal.tsx +++ b/src/components/overlays/Modal.tsx @@ -19,7 +19,7 @@ export function useModal(id: string) { export function ModalCard(props: { children?: ReactNode }) { return (
-
+
{props.children}
diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx index 0d1e5a06..f5bf1758 100644 --- a/src/pages/onboarding/Onboarding.tsx +++ b/src/pages/onboarding/Onboarding.tsx @@ -31,18 +31,20 @@ export function OnboardingPage() { - - - {t("onboarding.defaultConfirm.title")} - - {t("onboarding.defaultConfirm.description")} + + {t("onboarding.defaultConfirm.title")} + + + {t("onboarding.defaultConfirm.description")} + +
- - +
diff --git a/themes/default.ts b/themes/default.ts index bf2510ff..dfda2d86 100644 --- a/themes/default.ts +++ b/themes/default.ts @@ -137,6 +137,11 @@ export const defaultTheme = { accentA: tokens.purple.c500, accentB: tokens.blue.c500, }, + + // Modals + modal: { + background: tokens.shade.c800, + }, // typography type: { diff --git a/themes/list/blue.ts b/themes/list/blue.ts index e5b73409..e10592dc 100644 --- a/themes/list/blue.ts +++ b/themes/list/blue.ts @@ -95,6 +95,10 @@ export default createTheme({ accentB: tokens.blue.c500 }, + modal: { + background: tokens.shade.c800, + }, + type: { logo: tokens.purple.c100, text: tokens.shade.c50, diff --git a/themes/list/gray.ts b/themes/list/gray.ts index a0b9b742..c0c434e8 100644 --- a/themes/list/gray.ts +++ b/themes/list/gray.ts @@ -95,6 +95,10 @@ export default createTheme({ accentB: tokens.blue.c500 }, + modal: { + background: tokens.shade.c800, + }, + type: { logo: tokens.purple.c100, text: tokens.shade.c50, diff --git a/themes/list/red.ts b/themes/list/red.ts index 89614632..b42b935f 100644 --- a/themes/list/red.ts +++ b/themes/list/red.ts @@ -95,6 +95,10 @@ export default createTheme({ accentB: tokens.blue.c500 }, + modal: { + background: tokens.shade.c800, + }, + type: { logo: tokens.purple.c100, text: tokens.shade.c50, diff --git a/themes/list/teal.ts b/themes/list/teal.ts index cbae4748..742f4a32 100644 --- a/themes/list/teal.ts +++ b/themes/list/teal.ts @@ -95,6 +95,10 @@ export default createTheme({ accentB: tokens.blue.c500 }, + modal: { + background: tokens.shade.c800, + }, + type: { logo: tokens.purple.c100, text: tokens.shade.c50, From f96a0de373c81539f0db8ecf9a3fcb2598a5c6e3 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Jan 2024 13:09:42 +0100 Subject: [PATCH 26/40] Style the settings onboarding card --- .../player/internals/ScrapeCard.tsx | 20 +-- .../player/internals/StatusCircle.tsx | 40 +++--- src/pages/parts/settings/SetupPart.tsx | 117 ++++++++++++++++-- themes/default.ts | 1 + 4 files changed, 145 insertions(+), 33 deletions(-) diff --git a/src/components/player/internals/ScrapeCard.tsx b/src/components/player/internals/ScrapeCard.tsx index 479bf2b1..dc057901 100644 --- a/src/components/player/internals/ScrapeCard.tsx +++ b/src/components/player/internals/ScrapeCard.tsx @@ -2,7 +2,10 @@ import classNames from "classnames"; import { ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { StatusCircle } from "@/components/player/internals/StatusCircle"; +import { + StatusCircle, + StatusCircleProps, +} from "@/components/player/internals/StatusCircle"; import { Transition } from "@/components/utils/Transition"; export interface ScrapeItemProps { @@ -23,13 +26,14 @@ const statusTextMap: Partial> = { pending: "player.scraping.items.pending", }; -const statusMap: Record = { - failure: "error", - notfound: "noresult", - pending: "loading", - success: "success", - waiting: "waiting", -}; +const statusMap: Record = + { + failure: "error", + notfound: "noresult", + pending: "loading", + success: "success", + waiting: "waiting", + }; export function ScrapeItem(props: ScrapeItemProps) { const { t } = useTranslation(); diff --git a/src/components/player/internals/StatusCircle.tsx b/src/components/player/internals/StatusCircle.tsx index 32855321..ede7392e 100644 --- a/src/components/player/internals/StatusCircle.tsx +++ b/src/components/player/internals/StatusCircle.tsx @@ -4,23 +4,24 @@ import classNames from "classnames"; import { Icon, Icons } from "@/components/Icon"; import { Transition } from "@/components/utils/Transition"; -export interface StatusCircle { +export interface StatusCircleProps { type: "loading" | "success" | "error" | "noresult" | "waiting"; percentage?: number; + className?: string; } -export interface StatusCircleLoading extends StatusCircle { +export interface StatusCircleLoading extends StatusCircleProps { type: "loading"; percentage: number; } function statusIsLoading( - props: StatusCircle | StatusCircleLoading, + props: StatusCircleProps | StatusCircleLoading, ): props is StatusCircleLoading { return props.type === "loading"; } -export function StatusCircle(props: StatusCircle | StatusCircleLoading) { +export function StatusCircle(props: StatusCircleProps | StatusCircleLoading) { const [spring] = useSpring( () => ({ percentage: statusIsLoading(props) ? props.percentage : 0, @@ -30,18 +31,21 @@ export function StatusCircle(props: StatusCircle | StatusCircleLoading) { return (
diff --git a/src/pages/parts/settings/SetupPart.tsx b/src/pages/parts/settings/SetupPart.tsx index f639b43b..478722c6 100644 --- a/src/pages/parts/settings/SetupPart.tsx +++ b/src/pages/parts/settings/SetupPart.tsx @@ -1,9 +1,19 @@ +import classNames from "classnames"; +import { t } from "i18next"; +import { ReactNode } from "react"; import { useNavigate } from "react-router-dom"; import { useAsync } from "react-use"; import { isExtensionActive } from "@/backend/extension/messaging"; import { singularProxiedFetch } from "@/backend/helpers/fetch"; import { Button } from "@/components/buttons/Button"; +import { Icon, Icons } from "@/components/Icon"; +import { SettingsCard } from "@/components/layout/SettingsCard"; +import { + StatusCircle, + StatusCircleProps, +} from "@/components/player/internals/StatusCircle"; +import { Heading3 } from "@/components/utils/Text"; import { useAuthStore } from "@/stores/auth"; const testUrl = "https://postman-echo.com/get"; @@ -63,17 +73,110 @@ function useIsSetup() { }; } +function SetupCheckList(props: { + status: Status; + grey?: boolean; + children?: ReactNode; +}) { + const statusMap: Record = { + error: "error", + success: "success", + unset: "noresult", + }; + + return ( +
+ +
+

+ {props.children} +

+ {props.status === "error" ? ( +

+ There is something wrong with this setting. Go through setup again + to fix it. +

+ ) : null} +
+
+ ); +} + export function SetupPart() { const navigate = useNavigate(); const { loading, setupStates, globalState } = useIsSetup(); if (loading || !setupStates) return

Loading states...

; + + const textLookupMap: Record = { + error: { + title: "err1", + desc: "err2", + }, + success: { + title: "success1", + desc: "success2", + }, + unset: { + title: "unset1", + desc: "unset2", + }, + }; + return ( -
-

state: {globalState}

-

extension: {setupStates.extension}

-

proxy: {setupStates.proxy}

-

defaults: {setupStates.defaultProxy}

- -
+ +
+
+
+ +
+
+
+ + {t(textLookupMap[globalState].title)} + +

+ {t(textLookupMap[globalState].desc)} +

+ + Extension + + + Custom proxy + + + Default setup + +
+
+ +
+
+
); } diff --git a/themes/default.ts b/themes/default.ts index dfda2d86..bd31b3ff 100644 --- a/themes/default.ts +++ b/themes/default.ts @@ -152,6 +152,7 @@ export const defaultTheme = { divider: tokens.ash.c500, secondary: tokens.ash.c100, danger: tokens.semantic.red.c100, + success: tokens.semantic.green.c100, link: tokens.purple.c100, linkHover: tokens.purple.c50, }, From 741a35dee8b9468a01251addd08275f41cd8af6f Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Jan 2024 13:20:06 +0100 Subject: [PATCH 27/40] Style and localize the preference section of settings --- src/assets/locales/en.json | 7 ++- src/components/buttons/Toggle.tsx | 2 +- src/pages/parts/settings/PreferencesPart.tsx | 50 +++++++++++++------- src/pages/parts/settings/SidebarPart.tsx | 2 +- 4 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 77ac5aab..845d2756 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -408,10 +408,13 @@ "urlPlaceholder": "https://" } }, - "locale": { + "preferences": { "language": "Application language", "languageDescription": "Language applied to the entire application.", - "title": "Locale" + "title": "Preferences", + "thumbnail": "Generate thumbnails", + "thumbnailDescription": "Most of the time, videos don't have thumbnails. You can enable this setting to generate them on the fly but they can make your video slower.", + "thumbnailLabel": "Generate thumbnails" }, "reset": "Reset", "save": "Save", diff --git a/src/components/buttons/Toggle.tsx b/src/components/buttons/Toggle.tsx index 3fcb0071..8cedc245 100644 --- a/src/components/buttons/Toggle.tsx +++ b/src/components/buttons/Toggle.tsx @@ -1,6 +1,6 @@ import classNames from "classnames"; -export function Toggle(props: { onClick: () => void; enabled?: boolean }) { +export function Toggle(props: { onClick?: () => void; enabled?: boolean }) { return (
From e4ad8789b9309509104e3fd0b381d05b2a50ceb9 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Jan 2024 13:54:32 +0100 Subject: [PATCH 30/40] Add todos and fix some highlights --- src/pages/onboarding/OnboardingExtension.tsx | 2 ++ src/pages/onboarding/OnboardingProxy.tsx | 1 + src/pages/parts/settings/SetupPart.tsx | 14 ++++++++++---- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/pages/onboarding/OnboardingExtension.tsx b/src/pages/onboarding/OnboardingExtension.tsx index ce699884..ffa4bc84 100644 --- a/src/pages/onboarding/OnboardingExtension.tsx +++ b/src/pages/onboarding/OnboardingExtension.tsx @@ -47,6 +47,7 @@ export function ExtensionStatus(props: {

{t("onboarding.extension.status.loading")}

); + // TODO make proper actions for all of these states below if (props.status === "disallowed") content =

{t("onboarding.extension.status.disallowed")}

; else if (props.status === "failed") @@ -81,6 +82,7 @@ export function OnboardingExtensionPage() { ); useInterval(exec, 1000); + // TODO proper link to install extension return ( diff --git a/src/pages/onboarding/OnboardingProxy.tsx b/src/pages/onboarding/OnboardingProxy.tsx index 24595568..1d3b6b7c 100644 --- a/src/pages/onboarding/OnboardingProxy.tsx +++ b/src/pages/onboarding/OnboardingProxy.tsx @@ -40,6 +40,7 @@ export function OnboardingProxyPage() { } }, [url, completeAndRedirect, setProxySet]); + // TODO proper link to proxy deployment docs return ( diff --git a/src/pages/parts/settings/SetupPart.tsx b/src/pages/parts/settings/SetupPart.tsx index e9508d48..074f920d 100644 --- a/src/pages/parts/settings/SetupPart.tsx +++ b/src/pages/parts/settings/SetupPart.tsx @@ -76,6 +76,7 @@ function useIsSetup() { function SetupCheckList(props: { status: Status; grey?: boolean; + highlight?: boolean; children?: ReactNode; }) { const { t } = useTranslation(); @@ -98,9 +99,10 @@ function SetupCheckList(props: {

{props.children} @@ -119,7 +121,7 @@ export function SetupPart() { const { t } = useTranslation(); const navigate = useNavigate(); const { loading, setupStates, globalState } = useIsSetup(); - if (loading || !setupStates) return

Loading states...

; + if (loading || !setupStates) return

Loading states...

; // TODO proper loading screen const textLookupMap: Record< Status, @@ -174,7 +176,11 @@ export function SetupPart() { {t("settings.connections.setup.items.proxy")} - + {t("settings.connections.setup.items.default")}
From ad69c9381cd934fad459b07a923ae47015473966 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Jan 2024 14:00:15 +0100 Subject: [PATCH 31/40] remove resolved TODO --- src/components/player/display/base.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts index 91f36722..481c1982 100644 --- a/src/components/player/display/base.ts +++ b/src/components/player/display/base.ts @@ -101,7 +101,6 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { } function setupSource(vid: HTMLVideoElement, src: LoadableSource) { - // TODO: Add check whether the extension is installed if (src.type === "hls") { if (canPlayHlsNatively(vid)) { vid.src = processCdnLink(src.url); From ff045c95c77dfc57aa736928dee63dd3b75c15f1 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sat, 20 Jan 2024 18:28:49 +0100 Subject: [PATCH 32/40] update provider package --- package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d4e8db77..c5dae16f 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@formkit/auto-animate": "^0.8.1", "@headlessui/react": "^1.7.17", "@ladjs/country-language": "^1.0.3", - "@movie-web/providers": "^2.0.5", + "@movie-web/providers": "^2.1.0", "@noble/hashes": "^1.3.3", "@plasmohq/messaging": "^0.6.1", "@react-spring/web": "^9.7.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34d4eea7..2fda1a32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,8 +22,8 @@ dependencies: specifier: ^1.0.3 version: 1.0.3 '@movie-web/providers': - specifier: ^2.0.5 - version: 2.0.5 + specifier: ^2.1.0 + version: 2.1.0 '@noble/hashes': specifier: ^1.3.3 version: 1.3.3 @@ -1924,8 +1924,8 @@ packages: engines: {node: '>= 14'} dev: false - /@movie-web/providers@2.0.5: - resolution: {integrity: sha512-cefPTFXE7ctYeiibjk4HcNL3anRZ3lgYDAaJdzFzUrvkcSdxonP8GgGfDfPwmWWKip9dbP8Xv5aeauV/wrfaag==} + /@movie-web/providers@2.1.0: + resolution: {integrity: sha512-L7Nn5n1+0HNXha0A6bymJSGVLhyC4qd5S2r5Xk5FeqxMlqKBqOlMpUmfHiZOssog70sxTAvRfFqmKkM4UXV8kg==} dependencies: cheerio: 1.0.0-rc.12 crypto-js: 4.2.0 From 1918aa6d9b66e274a76bbee4613340c54102d2a1 Mon Sep 17 00:00:00 2001 From: William Oldham Date: Sat, 20 Jan 2024 17:59:18 +0000 Subject: [PATCH 33/40] Extension should only be classed as active/valid when it's allowed on the domain --- src/backend/extension/messaging.ts | 2 +- src/pages/parts/player/MetaPart.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/backend/extension/messaging.ts b/src/backend/extension/messaging.ts index b738184a..1f2a2b00 100644 --- a/src/backend/extension/messaging.ts +++ b/src/backend/extension/messaging.ts @@ -67,5 +67,5 @@ export async function isExtensionActive(): Promise { if (!info?.success) return false; const allowedVersion = isAllowedExtensionVersion(info.version); if (!allowedVersion) return false; - return true; + return info.allowed && info.hasPermission; } diff --git a/src/pages/parts/player/MetaPart.tsx b/src/pages/parts/player/MetaPart.tsx index 1e18b6d1..17803219 100644 --- a/src/pages/parts/player/MetaPart.tsx +++ b/src/pages/parts/player/MetaPart.tsx @@ -46,11 +46,10 @@ export function MetaPart(props: MetaPartProps) { const { error, value, loading } = useAsync(async () => { const info = await extensionInfo(); const isValidExtension = - info?.success && isAllowedExtensionVersion(info.version); + info?.success && isAllowedExtensionVersion(info.version) && info.allowed; if (isValidExtension) { - if (!info.allowed || !info.hasPermission) - throw new Error("extension-no-permission"); + if (!info.hasPermission) throw new Error("extension-no-permission"); } // use api metadata or providers metadata From 9157220317ffe1baed51f4573274eaaad83fd579 Mon Sep 17 00:00:00 2001 From: William Oldham Date: Sat, 20 Jan 2024 18:13:40 +0000 Subject: [PATCH 34/40] Add i81n strings for MetaPart --- src/assets/locales/en.json | 11 +++++++++++ src/pages/parts/player/MetaPart.tsx | 22 +++++++++------------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 4db7eeaa..20ece967 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -277,6 +277,17 @@ "homeButton": "Back to home", "text": "We couldn't find the media you requested. Either it's been removed or you tampered with the URL.", "title": "Couldn't find that media." + }, + "extensionPermission": { + "badge": "Permission Missing", + "title": "Configure the extension", + "text": "You have the browser extension, but we need your permission to get started using the extension.", + "button": "Use extension" + }, + "dmca": { + "badge": "Removed", + "title": "Media has been removed", + "text": "This media is no longer available due to a takedown notice or copyright claim." } }, "nextEpisode": { diff --git a/src/pages/parts/player/MetaPart.tsx b/src/pages/parts/player/MetaPart.tsx index 17803219..1b84b7bf 100644 --- a/src/pages/parts/player/MetaPart.tsx +++ b/src/pages/parts/player/MetaPart.tsx @@ -116,13 +116,10 @@ export function MetaPart(props: MetaPartProps) { - {t("player.metadata.failed.badge")} + {t("player.metadata.extensionPermission.badge")} - Configure the extension - - You have the browser extension, but we need your permission to get - started using the extension. - + {t("player.metadata.extensionPermission.title")} + {t("player.metadata.extensionPermission.text")} @@ -145,12 +142,11 @@ export function MetaPart(props: MetaPartProps) { return ( - Removed - Media has been removed - - This media is no longer available due to a takedown notice or - copyright claim. - + + {t("player.metadata.dmca.badge")} + + {t("player.metadata.dmca.title")} + {t("player.metadata.dmca.text")} + + ); else if (props.status === "failed") content =

{t("onboarding.extension.status.failed")}

; else if (props.status === "outdated") content =

{t("onboarding.extension.status.outdated")}

; - else if (props.status === "noperms") - content =

{t("onboarding.extension.status.noperms")}

; else if (props.status === "success") - content =

{t("onboarding.extension.status.success")}

; + content = ( +

+ + {t("onboarding.extension.status.success")} +

+ ); return ( - -
- {content} -
-
+ <> + +
+ {content} +
+
+ +
+ +

+ , + }} + /> +

+
+
+ ); } @@ -103,13 +138,11 @@ export function OnboardingExtensionPage() { - + {value === "success" ? ( + + ) : null}
diff --git a/src/pages/onboarding/utils.tsx b/src/pages/onboarding/utils.tsx index e8129230..3a57952b 100644 --- a/src/pages/onboarding/utils.tsx +++ b/src/pages/onboarding/utils.tsx @@ -7,16 +7,20 @@ import { Heading2, Heading3, Paragraph } from "@/components/utils/Text"; export function Card(props: { children?: React.ReactNode; + className?: string; onClick?: () => void; }) { return (
{props.children} From 7a5a9fe6fb4193cd6d896ed4c12ba613d0ba509f Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 21 Jan 2024 15:33:01 +0100 Subject: [PATCH 37/40] Fix typo --- src/assets/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 29bf3647..15038d6f 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -512,7 +512,7 @@ "status": { "loading": "Waiting for you to install the extension", "disallowed": "Extension is not enabled for this page", - "disallowedAction": "Enabled extension", + "disallowedAction": "Enable extension", "failed": "Failed to request status", "outdated": "Extension version too old", "success": "Extension is working as expected!" From 2064e42bcb387f0e4e1b21fb8a65426e5b3924e2 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 21 Jan 2024 20:31:34 +0100 Subject: [PATCH 38/40] handle errors for domain extraction --- src/backend/extension/streams.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/backend/extension/streams.ts b/src/backend/extension/streams.ts index 2afb900d..588718af 100644 --- a/src/backend/extension/streams.ts +++ b/src/backend/extension/streams.ts @@ -2,21 +2,23 @@ import { Stream } from "@movie-web/providers"; import { setDomainRule } from "@/backend/extension/messaging"; -function extractDomain(url: string): string { +function extractDomain(url: string): string | null { try { const u = new URL(url); return u.hostname; } catch { - return url; + return null; } } function extractDomainsFromStream(stream: Stream): string[] { if (stream.type === "hls") { - return [extractDomain(stream.playlist)]; + return [extractDomain(stream.playlist)].filter((v): v is string => !!v); } if (stream.type === "file") { - return Object.values(stream.qualities).map((v) => extractDomain(v.url)); + return Object.values(stream.qualities) + .map((v) => extractDomain(v.url)) + .filter((v): v is string => !!v); } return []; } From 20e35d05499936646d4055d91351af5502a39cc9 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Sun, 21 Jan 2024 20:33:49 +0100 Subject: [PATCH 39/40] fix linting problem --- src/components/player/atoms/settings/CaptionsView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/player/atoms/settings/CaptionsView.tsx b/src/components/player/atoms/settings/CaptionsView.tsx index 09ab5685..92104f1b 100644 --- a/src/components/player/atoms/settings/CaptionsView.tsx +++ b/src/components/player/atoms/settings/CaptionsView.tsx @@ -134,7 +134,7 @@ export function CaptionsView({ id }: { id: string }) { [selectCaptionById, setCurrentlyDownloading], ); - const content = subtitleList.map((v, i) => { + const content = subtitleList.map((v) => { return ( Date: Sun, 21 Jan 2024 20:41:09 +0100 Subject: [PATCH 40/40] onboarding preserve params --- src/pages/onboarding/Onboarding.tsx | 8 +++++--- src/pages/onboarding/OnboardingExtension.tsx | 8 +++++--- src/pages/onboarding/OnboardingProxy.tsx | 8 +++++--- src/pages/onboarding/onboardingHooks.ts | 17 ++++++++++++++++- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/pages/onboarding/Onboarding.tsx b/src/pages/onboarding/Onboarding.tsx index f5bf1758..525ed7df 100644 --- a/src/pages/onboarding/Onboarding.tsx +++ b/src/pages/onboarding/Onboarding.tsx @@ -1,6 +1,5 @@ import classNames from "classnames"; import { Trans, useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; import { Button } from "@/components/buttons/Button"; import { Stepper } from "@/components/layout/Stepper"; @@ -8,7 +7,10 @@ import { CenterContainer } from "@/components/layout/ThinContainer"; import { Modal, ModalCard, useModal } from "@/components/overlays/Modal"; import { Heading1, Heading2, Paragraph } from "@/components/utils/Text"; import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; -import { useRedirectBack } from "@/pages/onboarding/onboardingHooks"; +import { + useNavigateOnboarding, + useRedirectBack, +} from "@/pages/onboarding/onboardingHooks"; import { Card, CardContent, Link } from "@/pages/onboarding/utils"; import { PageTitle } from "@/pages/parts/util/PageTitle"; @@ -21,7 +23,7 @@ function VerticalLine(props: { className?: string }) { } export function OnboardingPage() { - const navigate = useNavigate(); + const navigate = useNavigateOnboarding(); const skipModal = useModal("skip"); const { completeAndRedirect } = useRedirectBack(); const { t } = useTranslation(); diff --git a/src/pages/onboarding/OnboardingExtension.tsx b/src/pages/onboarding/OnboardingExtension.tsx index 75408f28..70472a1a 100644 --- a/src/pages/onboarding/OnboardingExtension.tsx +++ b/src/pages/onboarding/OnboardingExtension.tsx @@ -1,6 +1,5 @@ import { ReactNode } from "react"; import { Trans, useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; import { useAsyncFn, useInterval } from "react-use"; import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; @@ -12,7 +11,10 @@ import { Stepper } from "@/components/layout/Stepper"; import { CenterContainer } from "@/components/layout/ThinContainer"; import { Heading2, Paragraph } from "@/components/utils/Text"; import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; -import { useRedirectBack } from "@/pages/onboarding/onboardingHooks"; +import { + useNavigateOnboarding, + useRedirectBack, +} from "@/pages/onboarding/onboardingHooks"; import { Card, Link } from "@/pages/onboarding/utils"; import { PageTitle } from "@/pages/parts/util/PageTitle"; @@ -104,7 +106,7 @@ export function ExtensionStatus(props: { export function OnboardingExtensionPage() { const { t } = useTranslation(); - const navigate = useNavigate(); + const navigate = useNavigateOnboarding(); const { completeAndRedirect } = useRedirectBack(); const [{ loading, value }, exec] = useAsyncFn( diff --git a/src/pages/onboarding/OnboardingProxy.tsx b/src/pages/onboarding/OnboardingProxy.tsx index 1d3b6b7c..e576779c 100644 --- a/src/pages/onboarding/OnboardingProxy.tsx +++ b/src/pages/onboarding/OnboardingProxy.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; import { useAsyncFn } from "react-use"; import { singularProxiedFetch } from "@/backend/helpers/fetch"; @@ -12,7 +11,10 @@ import { Divider } from "@/components/utils/Divider"; import { ErrorLine } from "@/components/utils/ErrorLine"; import { Heading2, Paragraph } from "@/components/utils/Text"; import { MinimalPageLayout } from "@/pages/layouts/MinimalPageLayout"; -import { useRedirectBack } from "@/pages/onboarding/onboardingHooks"; +import { + useNavigateOnboarding, + useRedirectBack, +} from "@/pages/onboarding/onboardingHooks"; import { Link } from "@/pages/onboarding/utils"; import { PageTitle } from "@/pages/parts/util/PageTitle"; import { useAuthStore } from "@/stores/auth"; @@ -21,7 +23,7 @@ const testUrl = "https://postman-echo.com/get"; export function OnboardingProxyPage() { const { t } = useTranslation(); - const navigate = useNavigate(); + const navigate = useNavigateOnboarding(); const { completeAndRedirect } = useRedirectBack(); const [url, setUrl] = useState(""); const setProxySet = useAuthStore((s) => s.setProxySet); diff --git a/src/pages/onboarding/onboardingHooks.ts b/src/pages/onboarding/onboardingHooks.ts index bcbbea48..cccf8825 100644 --- a/src/pages/onboarding/onboardingHooks.ts +++ b/src/pages/onboarding/onboardingHooks.ts @@ -1,5 +1,5 @@ import { useCallback } from "react"; -import { useNavigate } from "react-router-dom"; +import { useLocation, useNavigate } from "react-router-dom"; import { useQueryParam } from "@/hooks/useQueryParams"; import { useOnboardingStore } from "@/stores/onboarding"; @@ -20,3 +20,18 @@ export function useRedirectBack() { return { completeAndRedirect }; } + +export function useNavigateOnboarding() { + const navigate = useNavigate(); + const loc = useLocation(); + const nav = useCallback( + (path: string) => { + navigate({ + pathname: path, + search: loc.search, + }); + }, + [navigate, loc], + ); + return nav; +}