mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +01:00
Merge pull request #1072 from qtchaos/feat/autoplay
feat: add autoplay preference
This commit is contained in:
commit
ff95d1f713
10 changed files with 1791 additions and 58 deletions
|
@ -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
|
||||||
|
|
1741
pnpm-lock.yaml
1741
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(",")
|
||||||
|
|
|
@ -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
11
src/utils/autoplay.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in a new issue