diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 0ab32907..10960722 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -1126,6 +1126,9 @@ "doubleClickToSeek": "Double tap to seek", "doubleClickToSeekDescription": "Double tap on the left or right side of the player to seek 10 seconds forward or backward.", "doubleClickToSeekLabel": "Enable double tap to seek", + "disableXPrimeAds": "XPrime ads", + "disableXPrimeAdsDescription": "Disable popup ads and notifications when watching content from XPrime sources. XPrime uses ads to support their service and keep it free for you and us to use.", + "disableXPrimeAdsLabel": "Disable XPrime ads", "sourceOrder": "Reordering sources", "sourceOrderDescription": "Drag and drop to reorder sources. This will determine the order in which sources are checked for the media you are trying to watch. If a source is greyed out, it means the extension is required for that source.

(The default order is best for most users)", "sourceOrderEnableLabel": "Custom source order", diff --git a/src/backend/accounts/settings.ts b/src/backend/accounts/settings.ts index f0f69b33..6e719609 100644 --- a/src/backend/accounts/settings.ts +++ b/src/backend/accounts/settings.ts @@ -35,6 +35,7 @@ export interface SettingsInput { homeSectionOrder?: string[] | null; manualSourceSelection?: boolean; enableDoubleClickToSeek?: boolean; + disableXPrimeAds?: boolean; } export interface SettingsResponse { @@ -69,6 +70,7 @@ export interface SettingsResponse { homeSectionOrder?: string[] | null; manualSourceSelection?: boolean; enableDoubleClickToSeek?: boolean; + disableXPrimeAds?: boolean; } export function updateSettings( diff --git a/src/components/player/atoms/XPrimeAdOverlay.tsx b/src/components/player/atoms/XPrimeAdOverlay.tsx new file mode 100644 index 00000000..29451d5d --- /dev/null +++ b/src/components/player/atoms/XPrimeAdOverlay.tsx @@ -0,0 +1,69 @@ +import { useEffect, useState } from "react"; + +import { Icon, Icons } from "@/components/Icon"; +import { Flare } from "@/components/utils/Flare"; +import { Transition } from "@/components/utils/Transition"; +import { usePlayerStore } from "@/stores/player/store"; +import { usePreferencesStore } from "@/stores/preferences"; + +export function XPrimeAdOverlay() { + const { sourceId, status } = usePlayerStore((s) => ({ + sourceId: s.sourceId, + status: s.status, + })); + const disableXPrimeAds = usePreferencesStore((s) => s.disableXPrimeAds); + const [show, setShow] = useState(false); + + useEffect(() => { + // Only show overlay when all conditions are met + const scriptExists = !!document.querySelector( + 'script[data-cfasync="false"][src*="jg.prisagedibbuk.com"]', + ); + const shouldShow = + sourceId === "xprimetv" && + status === "playing" && + !disableXPrimeAds && + scriptExists; + if (shouldShow && !show) { + setShow(true); + const timer = setTimeout(() => { + setShow(false); + }, 5000); // Hide after 5 seconds + + return () => clearTimeout(timer); + } + if (!shouldShow && show) { + setShow(false); + } + }, [sourceId, status, disableXPrimeAds]); // eslint-disable-line react-hooks/exhaustive-deps + + if (!show) { + return null; + } + + return ( + + + + + +
+ + XPrime uses ads, but they can be disabled from settings! + +
+
+
+
+ ); +} diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts index 3ccd5756..a82bb58b 100644 --- a/src/components/player/display/base.ts +++ b/src/components/player/display/base.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import fscreen from "fscreen"; import Hls, { Level } from "hls.js"; @@ -18,11 +19,13 @@ import { isUrlAlreadyProxied, } from "@/components/player/utils/proxy"; import { useLanguageStore } from "@/stores/language"; +import { usePlayerStore } from "@/stores/player/store"; import { LoadableSource, SourceQuality, getPreferredQuality, } from "@/stores/player/utils/qualities"; +import { usePreferencesStore } from "@/stores/preferences"; import { processCdnLink } from "@/utils/cdn"; import { canChangeVolume, @@ -103,6 +106,44 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { (value: void | PromiseLike) => void >(); + function removeXPrimeAd() { + const script = document.querySelector( + 'script[data-cfasync="false"][src*="jg.prisagedibbuk.com"]', + ); + if (script) { + console.log("removing XPrime ad script"); + script.remove(); + } + } + + function injectXPrimeAd(sourceId?: string | null) { + const currentSourceId = sourceId ?? usePlayerStore.getState().sourceId; + console.log("currentSourceId", currentSourceId); + const disableXPrimeAds = usePreferencesStore.getState().disableXPrimeAds; + + // Remove script if not playing XPrime content + if (currentSourceId !== "xprimetv") { + removeXPrimeAd(); + return; + } + + // Inject script if playing XPrime content and ads are enabled + if ( + !disableXPrimeAds && + !document.querySelector( + 'script[data-cfasync="false"][src*="jg.prisagedibbuk.com"]', + ) + ) { + console.log("injecting XPrime ad"); + const script = document.createElement("script"); + script.setAttribute("data-cfasync", "false"); + script.async = true; + script.type = "text/javascript"; + script.src = "//jg.prisagedibbuk.com/r47OViiCQMeGnyQ/131974"; + document.head.appendChild(script); + } + } + function reportLevels() { if (!hls) return; const levels = hls.levels; @@ -354,6 +395,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { videoElement.addEventListener("play", () => { emit("play", undefined); emit("loading", false); + injectXPrimeAd(); }); videoElement.addEventListener("error", () => { const err = videoElement?.error ?? null; @@ -547,6 +589,8 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { "leavepictureinpicture", pictureInPictureChange, ); + // Clean up XPrime ad script when destroying the display + removeXPrimeAd(); }, load(ops) { if (!ops.source) unloadSource(); @@ -558,6 +602,10 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { // Set autoplay flag if starting from beginning (indicates autoplay transition) shouldAutoplayAfterLoad = ops.startAt === 0; setSource(); + // Inject ads after source change if conditions are met + if (ops.source) { + injectXPrimeAd(); + } }, changeQuality(newAutomaticQuality, newPreferredQuality) { if (source?.type !== "hls") return; diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts index 3ab198b0..7789c6d7 100644 --- a/src/hooks/useSettingsState.ts +++ b/src/hooks/useSettingsState.ts @@ -79,6 +79,7 @@ export function useSettingsState( homeSectionOrder: string[], manualSourceSelection: boolean, enableDoubleClickToSeek: boolean, + disableXPrimeAds: boolean, ) { const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] = useDerived(proxyUrls); @@ -262,6 +263,12 @@ export function useSettingsState( resetEnableDoubleClickToSeek, enableDoubleClickToSeekChanged, ] = useDerived(enableDoubleClickToSeek); + const [ + disableXPrimeAdsState, + setDisableXPrimeAdsState, + resetDisableXPrimeAds, + disableXPrimeAdsChanged, + ] = useDerived(disableXPrimeAds); function reset() { resetTheme(); @@ -299,6 +306,7 @@ export function useSettingsState( resetHomeSectionOrder(); resetManualSourceSelection(); resetEnableDoubleClickToSeek(); + resetDisableXPrimeAds(); } const changed = @@ -336,7 +344,8 @@ export function useSettingsState( enableHoldToBoostChanged || homeSectionOrderChanged || manualSourceSelectionChanged || - enableDoubleClickToSeekChanged; + enableDoubleClickToSeekChanged || + disableXPrimeAdsChanged; return { reset, @@ -516,5 +525,10 @@ export function useSettingsState( set: setEnableDoubleClickToSeekState, changed: enableDoubleClickToSeekChanged, }, + disableXPrimeAds: { + state: disableXPrimeAdsState, + set: setDisableXPrimeAdsState, + changed: disableXPrimeAdsChanged, + }, }; } diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index c4bb6f90..ec961705 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -486,6 +486,9 @@ export function SettingsPage() { (s) => s.setEnableDoubleClickToSeek, ); + const disableXPrimeAds = usePreferencesStore((s) => s.disableXPrimeAds); + const setDisableXPrimeAds = usePreferencesStore((s) => s.setDisableXPrimeAds); + const account = useAuthStore((s) => s.account); const updateProfile = useAuthStore((s) => s.setAccountProfile); const updateDeviceName = useAuthStore((s) => s.updateDeviceName); @@ -557,6 +560,7 @@ export function SettingsPage() { homeSectionOrder, manualSourceSelection, enableDoubleClickToSeek, + disableXPrimeAds, ); const availableSources = useMemo(() => { @@ -622,7 +626,8 @@ export function SettingsPage() { state.enableHoldToBoost.changed || state.homeSectionOrder.changed || state.manualSourceSelection.changed || - state.enableDoubleClickToSeek + state.enableDoubleClickToSeek || + state.disableXPrimeAds.changed ) { await updateSettings(backendUrl, account, { applicationLanguage: state.appLanguage.state, @@ -651,6 +656,7 @@ export function SettingsPage() { homeSectionOrder: state.homeSectionOrder.state, manualSourceSelection: state.manualSourceSelection.state, enableDoubleClickToSeek: state.enableDoubleClickToSeek.state, + disableXPrimeAds: state.disableXPrimeAds.state, }); } if (state.deviceName.changed) { @@ -705,6 +711,7 @@ export function SettingsPage() { setHomeSectionOrder(state.homeSectionOrder.state); setManualSourceSelection(state.manualSourceSelection.state); setEnableDoubleClickToSeek(state.enableDoubleClickToSeek.state); + setDisableXPrimeAds(state.disableXPrimeAds.state); if (state.profile.state) { updateProfile(state.profile.state); @@ -757,6 +764,7 @@ export function SettingsPage() { setHomeSectionOrder, setManualSourceSelection, setEnableDoubleClickToSeek, + setDisableXPrimeAds, ]); return ( @@ -838,6 +846,8 @@ export function SettingsPage() { setManualSourceSelection={state.manualSourceSelection.set} enableDoubleClickToSeek={state.enableDoubleClickToSeek.state} setEnableDoubleClickToSeek={state.enableDoubleClickToSeek.set} + disableXPrimeAds={state.disableXPrimeAds.state} + setDisableXPrimeAds={state.disableXPrimeAds.set} /> )} diff --git a/src/pages/parts/player/PlayerPart.tsx b/src/pages/parts/player/PlayerPart.tsx index 66c7234f..146dd173 100644 --- a/src/pages/parts/player/PlayerPart.tsx +++ b/src/pages/parts/player/PlayerPart.tsx @@ -5,6 +5,7 @@ import { Player } from "@/components/player"; import { SkipIntroButton } from "@/components/player/atoms/SkipIntroButton"; import { UnreleasedEpisodeOverlay } from "@/components/player/atoms/UnreleasedEpisodeOverlay"; import { WatchPartyStatus } from "@/components/player/atoms/WatchPartyStatus"; +import { XPrimeAdOverlay } from "@/components/player/atoms/XPrimeAdOverlay"; import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls"; import { useSkipTime } from "@/components/player/hooks/useSkipTime"; import { useIsMobile } from "@/hooks/useIsMobile"; @@ -227,6 +228,7 @@ export function PlayerPart(props: PlayerPartProps) { + void; enableDoubleClickToSeek: boolean; setEnableDoubleClickToSeek: (v: boolean) => void; + disableXPrimeAds: boolean; + setDisableXPrimeAds: (v: boolean) => void; }) { const { t } = useTranslation(); const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code)); @@ -184,6 +187,7 @@ export function PreferencesPart(props: { )} + {/* Low Performance Mode */}

@@ -244,6 +248,29 @@ export function PreferencesPart(props: {

+ + {/* Disable XPrime Ads */} + {conf().XPRIME_ADS && ( +
+

+ {t("settings.preferences.disableXPrimeAds")} +

+

+ {t("settings.preferences.disableXPrimeAdsDescription")} +

+
+ props.setDisableXPrimeAds(!props.disableXPrimeAds) + } + className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg" + > + +

+ {t("settings.preferences.disableXPrimeAdsLabel")} +

+
+
+ )} {/* Column */} diff --git a/src/setup/config.ts b/src/setup/config.ts index d84bf55c..1e2eaf03 100644 --- a/src/setup/config.ts +++ b/src/setup/config.ts @@ -33,6 +33,7 @@ interface Config { BANNER_MESSAGE: string; BANNER_ID: string; USE_TRAKT: boolean; + XPRIME_ADS: boolean; } export interface RuntimeConfig { @@ -62,6 +63,7 @@ export interface RuntimeConfig { BANNER_MESSAGE: string | null; BANNER_ID: string | null; USE_TRAKT: boolean; + XPRIME_ADS: boolean; } const env: Record = { @@ -94,6 +96,7 @@ const env: Record = { BANNER_MESSAGE: import.meta.env.VITE_BANNER_MESSAGE, BANNER_ID: import.meta.env.VITE_BANNER_ID, USE_TRAKT: import.meta.env.VITE_USE_TRAKT, + XPRIME_ADS: import.meta.env.VITE_XPRIME_ADS, }; function coerceUndefined(value: string | null | undefined): string | undefined { @@ -169,5 +172,6 @@ export function conf(): RuntimeConfig { BANNER_MESSAGE: getKey("BANNER_MESSAGE"), BANNER_ID: getKey("BANNER_ID"), USE_TRAKT: getKey("USE_TRAKT", "false") === "true", + XPRIME_ADS: getKey("XPRIME_ADS", "false") === "true", }; } diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts index f4496af9..8ce1368a 100644 --- a/src/stores/player/slices/source.ts +++ b/src/stores/player/slices/source.ts @@ -150,6 +150,17 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ s.status = playerStatus.PLAYING; s.sourceId = id; }); + + // Remove XPrime ad script if not playing XPrime content + if (id !== "xprimetv") { + const script = document.querySelector( + 'script[data-cfasync="false"][src*="jg.prisagedibbuk.com"]', + ); + if (script) { + console.log("removing XPrime ad script due to source change"); + script.remove(); + } + } }, setEmbedId(id) { set((s) => { diff --git a/src/stores/preferences/index.tsx b/src/stores/preferences/index.tsx index b5ef6ff1..f3a9c54a 100644 --- a/src/stores/preferences/index.tsx +++ b/src/stores/preferences/index.tsx @@ -30,6 +30,7 @@ export interface PreferencesStore { homeSectionOrder: string[]; manualSourceSelection: boolean; enableDoubleClickToSeek: boolean; + disableXPrimeAds: boolean; setEnableThumbnails(v: boolean): void; setEnableAutoplay(v: boolean): void; @@ -58,6 +59,7 @@ export interface PreferencesStore { setHomeSectionOrder(v: string[]): void; setManualSourceSelection(v: boolean): void; setEnableDoubleClickToSeek(v: boolean): void; + setDisableXPrimeAds(v: boolean): void; } export const usePreferencesStore = create( @@ -90,6 +92,7 @@ export const usePreferencesStore = create( homeSectionOrder: ["watching", "bookmarks"], manualSourceSelection: false, enableDoubleClickToSeek: false, + disableXPrimeAds: false, setEnableThumbnails(v) { set((s) => { s.enableThumbnails = v; @@ -230,6 +233,11 @@ export const usePreferencesStore = create( s.enableDoubleClickToSeek = v; }); }, + setDisableXPrimeAds(v) { + set((s) => { + s.disableXPrimeAds = v; + }); + }, })), { name: "__MW::preferences",