Merge pull request #1072 from qtchaos/feat/autoplay

feat: add autoplay preference
This commit is contained in:
William Oldham 2024-04-14 21:44:11 +01:00 committed by GitHub
commit ff95d1f713
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1791 additions and 58 deletions

View file

@ -23,6 +23,7 @@ ARG ONBOARDING_PROXY_INSTALL_LINK
ARG DISALLOWED_IDS ARG DISALLOWED_IDS
ARG CDN_REPLACEMENTS ARG CDN_REPLACEMENTS
ARG TURNSTILE_KEY ARG TURNSTILE_KEY
ARG ALLOW_AUTOPLAY="false"
ENV VITE_PWA_ENABLED=${PWA_ENABLED} ENV VITE_PWA_ENABLED=${PWA_ENABLED}
ENV VITE_GA_ID=${GA_ID} ENV VITE_GA_ID=${GA_ID}
@ -39,6 +40,7 @@ ENV VITE_ONBOARDING_PROXY_INSTALL_LINK=${ONBOARDING_PROXY_INSTALL_LINK}
ENV VITE_DISALLOWED_IDS=${DISALLOWED_IDS} ENV VITE_DISALLOWED_IDS=${DISALLOWED_IDS}
ENV VITE_CDN_REPLACEMENTS=${CDN_REPLACEMENTS} ENV VITE_CDN_REPLACEMENTS=${CDN_REPLACEMENTS}
ENV VITE_TURNSTILE_KEY=${TURNSTILE_KEY} ENV VITE_TURNSTILE_KEY=${TURNSTILE_KEY}
ENV VITE_ALLOW_AUTOPLAY=${ALLOW_AUTOPLAY}
COPY . ./ COPY . ./
RUN pnpm run build RUN pnpm run build

File diff suppressed because it is too large Load diff

View file

@ -524,6 +524,9 @@
"thumbnail": "Generate thumbnails", "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.", "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", "thumbnailLabel": "Generate thumbnails",
"autoplay": "Autoplay",
"autoplayDescription": "Automatically play the next episode in a series after reaching the end. Can be enabled by users with the browser extension, a custom proxy, or with the default setup if allowed by the host.",
"autoplayLabel": "Autoplay",
"title": "Preferences" "title": "Preferences"
}, },
"reset": "Reset", "reset": "Reset",

View file

@ -1,5 +1,5 @@
import classNames from "classnames"; import classNames from "classnames";
import { useCallback } from "react"; import { useCallback, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAsync } from "react-use"; import { useAsync } from "react-use";
@ -10,7 +10,9 @@ import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta";
import { Transition } from "@/components/utils/Transition"; import { Transition } from "@/components/utils/Transition";
import { PlayerMeta } from "@/stores/player/slices/source"; import { PlayerMeta } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences";
import { useProgressStore } from "@/stores/progress"; import { useProgressStore } from "@/stores/progress";
import { isAutoplayAllowed } from "@/utils/autoplay";
import { hasAired } from "../utils/aired"; import { hasAired } from "../utils/aired";
@ -101,6 +103,7 @@ export function NextEpisodeButton(props: {
(s) => s.setShouldStartFromBeginning, (s) => s.setShouldStartFromBeginning,
); );
const updateItem = useProgressStore((s) => s.updateItem); const updateItem = useProgressStore((s) => s.updateItem);
const enableAutoplay = usePreferencesStore((s) => s.enableAutoplay);
const isLastEpisode = const isLastEpisode =
meta?.episode?.number === meta?.episodes?.at(-1)?.number; meta?.episode?.number === meta?.episodes?.at(-1)?.number;
@ -117,6 +120,7 @@ export function NextEpisodeButton(props: {
); );
let show = false; let show = false;
const hasAutoplayed = useRef(false);
if (showingState === "always") show = true; if (showingState === "always") show = true;
else if (showingState === "hover" && props.controlsShowing) show = true; else if (showingState === "hover" && props.controlsShowing) show = true;
if (isHidden || status !== "playing" || duration === 0) show = false; if (isHidden || status !== "playing" || duration === 0) show = false;
@ -164,6 +168,18 @@ export function NextEpisodeButton(props: {
nextSeason, nextSeason,
]); ]);
useEffect(() => {
if (!enableAutoplay || metaType !== "show") return;
const onePercent = duration / 100;
const isEnding = time >= duration - onePercent && duration !== 0;
if (duration === 0) hasAutoplayed.current = false;
if (isEnding && isAutoplayAllowed() && !hasAutoplayed.current) {
hasAutoplayed.current = true;
loadNextEpisode();
}
}, [duration, enableAutoplay, loadNextEpisode, metaType, time]);
if (!meta?.episode || !nextEp) return null; if (!meta?.episode || !nextEp) return null;
if (metaType !== "show") return null; if (metaType !== "show") return null;

View file

@ -51,6 +51,7 @@ export function useSettingsState(
} }
| undefined, | undefined,
enableThumbnails: boolean, enableThumbnails: boolean,
enableAutoplay: boolean,
) { ) {
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] = const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
useDerived(proxyUrls); useDerived(proxyUrls);
@ -84,6 +85,12 @@ export function useSettingsState(
resetEnableThumbnails, resetEnableThumbnails,
enableThumbnailsChanged, enableThumbnailsChanged,
] = useDerived(enableThumbnails); ] = useDerived(enableThumbnails);
const [
enableAutoplayState,
setEnableAutoplayState,
resetEnableAutoplay,
enableAutoplayChanged,
] = useDerived(enableAutoplay);
function reset() { function reset() {
resetTheme(); resetTheme();
@ -95,6 +102,7 @@ export function useSettingsState(
resetDeviceName(); resetDeviceName();
resetProfile(); resetProfile();
resetEnableThumbnails(); resetEnableThumbnails();
resetEnableAutoplay();
} }
const changed = const changed =
@ -105,7 +113,8 @@ export function useSettingsState(
backendUrlChanged || backendUrlChanged ||
proxyUrlsChanged || proxyUrlsChanged ||
profileChanged || profileChanged ||
enableThumbnailsChanged; enableThumbnailsChanged ||
enableAutoplayChanged;
return { return {
reset, reset,
@ -150,5 +159,10 @@ export function useSettingsState(
set: setEnableThumbnailsState, set: setEnableThumbnailsState,
changed: enableThumbnailsChanged, changed: enableThumbnailsChanged,
}, },
enableAutoplay: {
state: enableAutoplayState,
set: setEnableAutoplayState,
changed: enableAutoplayChanged,
},
}; };
} }

View file

@ -122,6 +122,9 @@ export function SettingsPage() {
const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails); const enableThumbnails = usePreferencesStore((s) => s.enableThumbnails);
const setEnableThumbnails = usePreferencesStore((s) => s.setEnableThumbnails); const setEnableThumbnails = usePreferencesStore((s) => s.setEnableThumbnails);
const enableAutoplay = usePreferencesStore((s) => s.enableAutoplay);
const setEnableAutoplay = usePreferencesStore((s) => s.setEnableAutoplay);
const account = useAuthStore((s) => s.account); const account = useAuthStore((s) => s.account);
const updateProfile = useAuthStore((s) => s.setAccountProfile); const updateProfile = useAuthStore((s) => s.setAccountProfile);
const updateDeviceName = useAuthStore((s) => s.updateDeviceName); const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
@ -144,6 +147,7 @@ export function SettingsPage() {
backendUrlSetting, backendUrlSetting,
account?.profile, account?.profile,
enableThumbnails, enableThumbnails,
enableAutoplay,
); );
useEffect(() => { useEffect(() => {
@ -196,6 +200,7 @@ export function SettingsPage() {
} }
setEnableThumbnails(state.enableThumbnails.state); setEnableThumbnails(state.enableThumbnails.state);
setEnableAutoplay(state.enableAutoplay.state);
setAppLanguage(state.appLanguage.state); setAppLanguage(state.appLanguage.state);
setTheme(state.theme.state); setTheme(state.theme.state);
setSubStyling(state.subtitleStyling.state); setSubStyling(state.subtitleStyling.state);
@ -217,18 +222,19 @@ export function SettingsPage() {
setBackendUrl(url); setBackendUrl(url);
} }
}, [ }, [
state,
account, account,
backendUrl, backendUrl,
setEnableThumbnails, setEnableThumbnails,
state,
setEnableAutoplay,
setAppLanguage, setAppLanguage,
setTheme, setTheme,
setSubStyling, setSubStyling,
setProxySet,
updateDeviceName, updateDeviceName,
updateProfile, updateProfile,
setProxySet,
setBackendUrl,
logout, logout,
setBackendUrl,
]); ]);
return ( return (
<SubPageLayout> <SubPageLayout>
@ -266,6 +272,8 @@ export function SettingsPage() {
setLanguage={state.appLanguage.set} setLanguage={state.appLanguage.set}
enableThumbnails={state.enableThumbnails.state} enableThumbnails={state.enableThumbnails.state}
setEnableThumbnails={state.enableThumbnails.set} setEnableThumbnails={state.enableThumbnails.set}
enableAutoplay={state.enableAutoplay.state}
setEnableAutoplay={state.enableAutoplay.set}
/> />
</div> </div>
<div id="settings-appearance" className="mt-48"> <div id="settings-appearance" className="mt-48">

View file

@ -1,3 +1,4 @@
import classNames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Toggle } from "@/components/buttons/Toggle"; import { Toggle } from "@/components/buttons/Toggle";
@ -5,6 +6,7 @@ import { FlagIcon } from "@/components/FlagIcon";
import { Dropdown } from "@/components/form/Dropdown"; import { Dropdown } from "@/components/form/Dropdown";
import { Heading1 } from "@/components/utils/Text"; import { Heading1 } from "@/components/utils/Text";
import { appLanguageOptions } from "@/setup/i18n"; import { appLanguageOptions } from "@/setup/i18n";
import { isAutoplayAllowed } from "@/utils/autoplay";
import { getLocaleInfo, sortLangCodes } from "@/utils/language"; import { getLocaleInfo, sortLangCodes } from "@/utils/language";
export function PreferencesPart(props: { export function PreferencesPart(props: {
@ -12,10 +14,14 @@ export function PreferencesPart(props: {
setLanguage: (l: string) => void; setLanguage: (l: string) => void;
enableThumbnails: boolean; enableThumbnails: boolean;
setEnableThumbnails: (v: boolean) => void; setEnableThumbnails: (v: boolean) => void;
enableAutoplay: boolean;
setEnableAutoplay: (v: boolean) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code)); const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code));
const allowAutoplay = isAutoplayAllowed();
const options = appLanguageOptions const options = appLanguageOptions
.sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code)) .sort((a, b) => sorted.indexOf(a.code) - sorted.indexOf(b.code))
.map((opt) => ({ .map((opt) => ({
@ -62,6 +68,32 @@ export function PreferencesPart(props: {
</p> </p>
</div> </div>
</div> </div>
<div>
<p className="text-white font-bold mb-3">
{t("settings.preferences.autoplay")}
</p>
<p className="max-w-[25rem] font-medium">
{t("settings.preferences.autoplayDescription")}
</p>
<div
onClick={() =>
allowAutoplay
? props.setEnableAutoplay(!props.enableAutoplay)
: null
}
className={classNames(
"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",
allowAutoplay
? "cursor-pointer opacity-100 pointer-events-auto"
: "cursor-not-allowed opacity-50 pointer-events-none",
)}
>
<Toggle enabled={props.enableAutoplay && allowAutoplay} />
<p className="flex-1 text-white font-bold">
{t("settings.preferences.autoplayLabel")}
</p>
</div>
</div>
</div> </div>
); );
} }

View file

@ -23,6 +23,7 @@ interface Config {
ONBOARDING_CHROME_EXTENSION_INSTALL_LINK: string; ONBOARDING_CHROME_EXTENSION_INSTALL_LINK: string;
ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK: string; ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK: string;
ONBOARDING_PROXY_INSTALL_LINK: string; ONBOARDING_PROXY_INSTALL_LINK: string;
ALLOW_AUTOPLAY: boolean;
} }
export interface RuntimeConfig { export interface RuntimeConfig {
@ -39,6 +40,7 @@ export interface RuntimeConfig {
TURNSTILE_KEY: string | null; TURNSTILE_KEY: string | null;
CDN_REPLACEMENTS: Array<string[]>; CDN_REPLACEMENTS: Array<string[]>;
HAS_ONBOARDING: boolean; HAS_ONBOARDING: boolean;
ALLOW_AUTOPLAY: boolean;
ONBOARDING_CHROME_EXTENSION_INSTALL_LINK: string | null; ONBOARDING_CHROME_EXTENSION_INSTALL_LINK: string | null;
ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK: string | null; ONBOARDING_FIREFOX_EXTENSION_INSTALL_LINK: string | null;
ONBOARDING_PROXY_INSTALL_LINK: string | null; ONBOARDING_PROXY_INSTALL_LINK: string | null;
@ -64,6 +66,7 @@ const env: Record<keyof Config, undefined | string> = {
TURNSTILE_KEY: import.meta.env.VITE_TURNSTILE_KEY, TURNSTILE_KEY: import.meta.env.VITE_TURNSTILE_KEY,
CDN_REPLACEMENTS: import.meta.env.VITE_CDN_REPLACEMENTS, CDN_REPLACEMENTS: import.meta.env.VITE_CDN_REPLACEMENTS,
HAS_ONBOARDING: import.meta.env.VITE_HAS_ONBOARDING, HAS_ONBOARDING: import.meta.env.VITE_HAS_ONBOARDING,
ALLOW_AUTOPLAY: import.meta.env.VITE_ALLOW_AUTOPLAY,
}; };
function coerceUndefined(value: string | null | undefined): string | undefined { function coerceUndefined(value: string | null | undefined): string | undefined {
@ -109,6 +112,7 @@ export function conf(): RuntimeConfig {
.filter((v) => v.length > 0), .filter((v) => v.length > 0),
NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true", NORMAL_ROUTER: getKey("NORMAL_ROUTER", "false") === "true",
HAS_ONBOARDING: getKey("HAS_ONBOARDING", "true") === "true", HAS_ONBOARDING: getKey("HAS_ONBOARDING", "true") === "true",
ALLOW_AUTOPLAY: getKey("ALLOW_AUTOPLAY", "false") === "true",
TURNSTILE_KEY: getKey("TURNSTILE_KEY"), TURNSTILE_KEY: getKey("TURNSTILE_KEY"),
DISALLOWED_IDS: getKey("DISALLOWED_IDS", "") DISALLOWED_IDS: getKey("DISALLOWED_IDS", "")
.split(",") .split(",")

View file

@ -5,6 +5,8 @@ import { immer } from "zustand/middleware/immer";
export interface PreferencesStore { export interface PreferencesStore {
enableThumbnails: boolean; enableThumbnails: boolean;
setEnableThumbnails(v: boolean): void; setEnableThumbnails(v: boolean): void;
enableAutoplay: boolean;
setEnableAutoplay(v: boolean): void;
} }
export const usePreferencesStore = create( export const usePreferencesStore = create(
@ -16,6 +18,12 @@ export const usePreferencesStore = create(
s.enableThumbnails = v; s.enableThumbnails = v;
}); });
}, },
enableAutoplay: false,
setEnableAutoplay(v) {
set((s) => {
s.enableAutoplay = v;
});
},
})), })),
{ {
name: "__MW::preferences", name: "__MW::preferences",

11
src/utils/autoplay.ts Normal file
View file

@ -0,0 +1,11 @@
import { isExtensionActiveCached } from "@/backend/extension/messaging";
import { conf } from "@/setup/config";
import { useAuthStore } from "@/stores/auth";
export function isAutoplayAllowed() {
return Boolean(
conf().ALLOW_AUTOPLAY ||
isExtensionActiveCached() ||
useAuthStore.getState().proxySet,
);
}