From f443223d16629db83d4af31b8f76f68fb9ef497b Mon Sep 17 00:00:00 2001 From: Elad Lachmi Date: Tue, 13 Apr 2021 07:59:37 +0300 Subject: [PATCH] [Feature] Added slideshow to gallery in wall display mode (#1224) --- graphql/documents/data/config.graphql | 3 +- graphql/schema/types/config.graphql | 4 + pkg/api/resolver_mutation_configure.go | 4 + pkg/api/resolver_query_configuration.go | 2 + pkg/manager/config/config.go | 6 + .../src/components/Changelog/versions/v070.md | 1 + ui/v2.5/src/components/Images/ImageList.tsx | 32 ++- .../SettingsInterfacePanel.tsx | 22 ++ ui/v2.5/src/hooks/Interval.ts | 65 +++++ ui/v2.5/src/hooks/Lightbox/Lightbox.tsx | 266 +++++++++++++++--- ui/v2.5/src/hooks/Lightbox/context.tsx | 15 +- ui/v2.5/src/hooks/Lightbox/hooks.ts | 7 +- ui/v2.5/src/hooks/Lightbox/lightbox.scss | 39 ++- ui/v2.5/src/hooks/PageVisibility.ts | 50 ++++ ui/v2.5/src/hooks/index.ts | 2 + 15 files changed, 463 insertions(+), 55 deletions(-) create mode 100644 ui/v2.5/src/hooks/Interval.ts create mode 100644 ui/v2.5/src/hooks/PageVisibility.ts diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index 250c937b4..e1597c0ca 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -2,7 +2,7 @@ fragment ConfigGeneralData on ConfigGeneralResult { stashes { path excludeVideo - excludeImage + excludeImage } databasePath generatedPath @@ -52,6 +52,7 @@ fragment ConfigInterfaceData on ConfigInterfaceResult { css cssEnabled language + slideshowDelay } fragment ConfigData on ConfigResult { diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 9cf463125..fd13b7419 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -188,6 +188,8 @@ input ConfigInterfaceInput { cssEnabled: Boolean """Interface language""" language: String + """Slideshow Delay""" + slideshowDelay: Int } type ConfigInterfaceResult { @@ -210,6 +212,8 @@ type ConfigInterfaceResult { cssEnabled: Boolean """Interface language""" language: String + """Slideshow Delay""" + slideshowDelay: Int } """All configuration settings""" diff --git a/pkg/api/resolver_mutation_configure.go b/pkg/api/resolver_mutation_configure.go index e5cd71c9e..b3734dfc7 100644 --- a/pkg/api/resolver_mutation_configure.go +++ b/pkg/api/resolver_mutation_configure.go @@ -219,6 +219,10 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models. c.Set(config.Language, *input.Language) } + if input.SlideshowDelay != nil { + c.Set(config.SlideshowDelay, *input.SlideshowDelay) + } + css := "" if input.CSS != nil { diff --git a/pkg/api/resolver_query_configuration.go b/pkg/api/resolver_query_configuration.go index c11d8dc0c..1d47acb38 100644 --- a/pkg/api/resolver_query_configuration.go +++ b/pkg/api/resolver_query_configuration.go @@ -93,6 +93,7 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult { css := config.GetCSS() cssEnabled := config.GetCSSEnabled() language := config.GetLanguage() + slideshowDelay := config.GetSlideshowDelay() return &models.ConfigInterfaceResult{ MenuItems: menuItems, @@ -105,5 +106,6 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult { CSS: &css, CSSEnabled: &cssEnabled, Language: &language, + SlideshowDelay: &slideshowDelay, } } diff --git a/pkg/manager/config/config.go b/pkg/manager/config/config.go index 1ce880b1a..b852ca835 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -118,6 +118,7 @@ const AutostartVideo = "autostart_video" const ShowStudioAsText = "show_studio_as_text" const CSSEnabled = "cssEnabled" const WallPlayback = "wall_playback" +const SlideshowDelay = "slideshow_delay" // Logging options const LogFile = "logFile" @@ -560,6 +561,11 @@ func (i *Instance) GetShowStudioAsText() bool { return viper.GetBool(ShowStudioAsText) } +func (i *Instance) GetSlideshowDelay() int { + viper.SetDefault(SlideshowDelay, 5000) + return viper.GetInt(SlideshowDelay) +} + func (i *Instance) GetCSSPath() string { // use custom.css in the same directory as the config file configFileUsed := viper.ConfigFileUsed() diff --git a/ui/v2.5/src/components/Changelog/versions/v070.md b/ui/v2.5/src/components/Changelog/versions/v070.md index c3bfdd6e4..acce2e74c 100644 --- a/ui/v2.5/src/components/Changelog/versions/v070.md +++ b/ui/v2.5/src/components/Changelog/versions/v070.md @@ -4,6 +4,7 @@ * Added scene queue. ### 🎨 Improvements +* Add slideshow to image wall view. * Support API key via URL query parameter, and added API key to stream link in Scene File Info. * Revamped setup wizard and migration UI. * Add various `count` filter criteria and sort options. diff --git a/ui/v2.5/src/components/Images/ImageList.tsx b/ui/v2.5/src/components/Images/ImageList.tsx index 103e1799f..b304e12a7 100644 --- a/ui/v2.5/src/components/Images/ImageList.tsx +++ b/ui/v2.5/src/components/Images/ImageList.tsx @@ -17,6 +17,7 @@ import { showWhenSelected, PersistanceLevel, } from "src/hooks/ListHook"; + import { ImageCard } from "./ImageCard"; import { EditImagesDialog } from "./EditImagesDialog"; import { DeleteImagesDialog } from "./DeleteImagesDialog"; @@ -36,34 +37,57 @@ const ImageWall: React.FC = ({ currentPage, pageCount, }) => { + const [slideshowRunning, setSlideshowRunning] = useState(false); const handleLightBoxPage = useCallback( (direction: number) => { if (direction === -1) { if (currentPage === 1) return false; onChangePage(currentPage - 1); } else { - if (currentPage === pageCount) return false; + if (currentPage === pageCount) { + // if the slideshow is running + // return to the first page + if (slideshowRunning) { + onChangePage(0); + return true; + } + return false; + } onChangePage(currentPage + 1); } return direction === -1 || direction === 1; }, - [onChangePage, currentPage, pageCount] + [onChangePage, currentPage, pageCount, slideshowRunning] ); + const handleClose = useCallback(() => { + setSlideshowRunning(false); + }, [setSlideshowRunning]); + const showLightbox = useLightbox({ images, showNavigation: false, pageCallback: handleLightBoxPage, pageHeader: `Page ${currentPage} / ${pageCount}`, + slideshowEnabled: slideshowRunning, + onClose: handleClose, }); + const handleImageOpen = useCallback( + (index) => { + setSlideshowRunning(true); + showLightbox(index, true); + }, + [showLightbox] + ); + const thumbs = images.map((image, index) => (
showLightbox(index)} - onKeyPress={() => showLightbox(index)} + onClick={() => handleImageOpen(index)} + onKeyPress={() => handleImageOpen(index)} > { const Toast = useToast(); const { data: config, error, loading } = useConfiguration(); @@ -27,6 +29,7 @@ export const SettingsInterfacePanel: React.FC = () => { const [wallPlayback, setWallPlayback] = useState("video"); const [maximumLoopDuration, setMaximumLoopDuration] = useState(0); const [autostartVideo, setAutostartVideo] = useState(false); + const [slideshowDelay, setSlideshowDelay] = useState(0); const [showStudioAsText, setShowStudioAsText] = useState(false); const [css, setCSS] = useState(); const [cssEnabled, setCSSEnabled] = useState(false); @@ -43,6 +46,7 @@ export const SettingsInterfacePanel: React.FC = () => { css, cssEnabled, language, + slideshowDelay, }); useEffect(() => { @@ -57,6 +61,7 @@ export const SettingsInterfacePanel: React.FC = () => { setCSS(iCfg?.css ?? ""); setCSSEnabled(iCfg?.cssEnabled ?? false); setLanguage(iCfg?.language ?? "en-US"); + setSlideshowDelay(iCfg?.slideshowDelay ?? 5000); }, [config]); async function onSave() { @@ -187,6 +192,23 @@ export const SettingsInterfacePanel: React.FC = () => { + +
Slideshow Delay
+ ) => { + setSlideshowDelay( + Number.parseInt(e.currentTarget.value, 10) * SECONDS_TO_MS + ); + }} + /> + + Slideshow is available in galleries when in wall view mode + +
+
Custom CSS
void, + delay: number | null = 5000 +): (() => void)[] => { + const savedCallback = useRef<() => void>(); + const savedIntervalId = useRef(); + const [savedDelay, setSavedDelay] = useState(delay); + + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + useEffect(() => { + let validDelay; + if (delay !== null) { + validDelay = delay >= MIN_VALID_INTERVAL ? delay : MIN_VALID_INTERVAL; + } else { + validDelay = delay; + } + + setSavedDelay(validDelay); + }, [delay]); + + const cancel = () => { + const intervalId = savedIntervalId.current; + if (intervalId) { + savedIntervalId.current = undefined; + clearInterval(intervalId); + } + }; + + const reset = () => { + cancel(); + + const tick = () => { + if (savedCallback.current) savedCallback.current(); + }; + + if (savedDelay !== null) { + savedIntervalId.current = setInterval(tick, savedDelay); + } + }; + + useEffect(() => { + cancel(); + + const tick = () => { + if (savedCallback.current) savedCallback.current(); + }; + + if (savedDelay !== null) { + savedIntervalId.current = setInterval(tick, savedDelay); + return cancel; + } + }, [callback, savedDelay]); + + return delay ? [cancel, reset] : [noop, noop]; +}; + +export default useInterval; diff --git a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx index 43e429935..0ee1a342a 100644 --- a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx +++ b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx @@ -1,15 +1,30 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import * as GQL from "src/core/generated-graphql"; -import { Button } from "react-bootstrap"; +import { + Button, + Col, + FormControl, + InputGroup, + FormLabel, + OverlayTrigger, + Popover, +} from "react-bootstrap"; import cx from "classnames"; import Mousetrap from "mousetrap"; -import { debounce } from "lodash"; +import debounce from "lodash/debounce"; import { Icon, LoadingIndicator } from "src/components/Shared"; +import { useInterval, usePageVisibility } from "src/hooks"; +import { useConfiguration } from "src/core/StashService"; const CLASSNAME = "Lightbox"; const CLASSNAME_HEADER = `${CLASSNAME}-header`; +const CLASSNAME_LEFT_SPACER = `${CLASSNAME_HEADER}-left-spacer`; const CLASSNAME_INDICATOR = `${CLASSNAME_HEADER}-indicator`; +const CLASSNAME_DELAY = `${CLASSNAME_HEADER}-delay`; +const CLASSNAME_DELAY_ICON = `${CLASSNAME_DELAY}-icon`; +const CLASSNAME_DELAY_INLINE = `${CLASSNAME_DELAY}-inline`; +const CLASSNAME_RIGHT = `${CLASSNAME_HEADER}-right`; const CLASSNAME_DISPLAY = `${CLASSNAME}-display`; const CLASSNAME_CAROUSEL = `${CLASSNAME}-carousel`; const CLASSNAME_INSTANT = `${CLASSNAME_CAROUSEL}-instant`; @@ -19,6 +34,10 @@ const CLASSNAME_NAV = `${CLASSNAME}-nav`; const CLASSNAME_NAVIMAGE = `${CLASSNAME_NAV}-image`; const CLASSNAME_NAVSELECTED = `${CLASSNAME_NAV}-selected`; +const DEFAULT_SLIDESHOW_DELAY = 5000; +const SECONDS_TO_MS = 1000; +const MIN_VALID_INTERVAL_SECONDS = 1; + type Image = Pick; interface IProps { images: Image[]; @@ -26,6 +45,7 @@ interface IProps { isLoading: boolean; initialIndex?: number; showNavigation: boolean; + slideshowEnabled?: boolean; pageHeader?: string; pageCallback?: (direction: number) => boolean; hide: () => void; @@ -37,6 +57,7 @@ export const LightboxComponent: React.FC = ({ isLoading, initialIndex = 0, showNavigation, + slideshowEnabled = false, pageHeader, pageCallback, hide, @@ -49,6 +70,27 @@ export const LightboxComponent: React.FC = ({ const carouselRef = useRef(null); const indicatorRef = useRef(null); const navRef = useRef(null); + const clearIntervalCallback = useRef<() => void>(); + const resetIntervalCallback = useRef<() => void>(); + const config = useConfiguration(); + + const userSelectedSlideshowDelayOrDefault = + config?.data?.configuration.interface.slideshowDelay ?? + DEFAULT_SLIDESHOW_DELAY; + + // slideshowInterval is used for controlling the logic + // displaySlideshowInterval is for display purposes only + // keeping them separate and independant allows us to handle the logic however we want + // while still displaying something that makes sense to the user + const [slideshowInterval, setSlideshowInterval] = useState( + null + ); + const [ + displayedSlideshowInterval, + setDisplayedSlideshowInterval, + ] = useState( + (userSelectedSlideshowDelayOrDefault / SECONDS_TO_MS).toString() + ); useEffect(() => { setIsSwitchingPage(false); @@ -59,6 +101,7 @@ export const LightboxComponent: React.FC = ({ () => setInstantTransition(false), 400 ); + const setInstant = useCallback(() => { setInstantTransition(true); disableInstantTransition(); @@ -108,6 +151,28 @@ export const LightboxComponent: React.FC = ({ } }, [initialIndex, isVisible, setIndex]); + const toggleSlideshow = useCallback(() => { + if (slideshowInterval) { + setSlideshowInterval(null); + } else if ( + displayedSlideshowInterval !== null && + typeof displayedSlideshowInterval !== "undefined" + ) { + const intervalNumber = Number.parseInt(displayedSlideshowInterval, 10); + setSlideshowInterval(intervalNumber * SECONDS_TO_MS); + } else { + setSlideshowInterval(userSelectedSlideshowDelayOrDefault); + } + }, [ + slideshowInterval, + userSelectedSlideshowDelayOrDefault, + displayedSlideshowInterval, + ]); + + usePageVisibility(() => { + toggleSlideshow(); + }); + const close = useCallback(() => { if (!isFullscreen) { hide(); @@ -122,37 +187,52 @@ export const LightboxComponent: React.FC = ({ if (nodeName === "DIV" || nodeName === "PICTURE") close(); }; - const handleLeft = useCallback(() => { - if (isSwitchingPage || index.current === -1) return; + const handleLeft = useCallback( + (isUserAction = true) => { + if (isSwitchingPage || index.current === -1) return; - if (index.current === 0) { - if (pageCallback) { - setIsSwitchingPage(true); - setIndex(-1); - // Check if calling page wants to swap page - const repage = pageCallback(-1); - if (!repage) { - setIsSwitchingPage(false); + if (index.current === 0) { + if (pageCallback) { + setIsSwitchingPage(true); + setIndex(-1); + // Check if calling page wants to swap page + const repage = pageCallback(-1); + if (!repage) { + setIsSwitchingPage(false); + setIndex(0); + } + } else setIndex(images.length - 1); + } else setIndex((index.current ?? 0) - 1); + + if (isUserAction && resetIntervalCallback.current) { + resetIntervalCallback.current(); + } + }, + [images, setIndex, pageCallback, isSwitchingPage, resetIntervalCallback] + ); + + const handleRight = useCallback( + (isUserAction = true) => { + if (isSwitchingPage) return; + + if (index.current === images.length - 1) { + if (pageCallback) { + setIsSwitchingPage(true); setIndex(0); - } - } else setIndex(images.length - 1); - } else setIndex((index.current ?? 0) - 1); - }, [images, setIndex, pageCallback, isSwitchingPage]); - const handleRight = useCallback(() => { - if (isSwitchingPage) return; + const repage = pageCallback?.(1); + if (!repage) { + setIsSwitchingPage(false); + setIndex(images.length - 1); + } + } else setIndex(0); + } else setIndex((index.current ?? 0) + 1); - if (index.current === images.length - 1) { - if (pageCallback) { - setIsSwitchingPage(true); - setIndex(0); - const repage = pageCallback?.(1); - if (!repage) { - setIsSwitchingPage(false); - setIndex(images.length - 1); - } - } else setIndex(0); - } else setIndex((index.current ?? 0) + 1); - }, [images, setIndex, pageCallback, isSwitchingPage]); + if (isUserAction && resetIntervalCallback.current) { + resetIntervalCallback.current(); + } + }, + [images, setIndex, pageCallback, isSwitchingPage, resetIntervalCallback] + ); const handleKey = useCallback( (e: KeyboardEvent) => { @@ -164,8 +244,12 @@ export const LightboxComponent: React.FC = ({ }, [setInstant, handleLeft, handleRight, close] ); - const handleFullScreenChange = () => + const handleFullScreenChange = () => { + if (clearIntervalCallback.current) { + clearIntervalCallback.current(); + } setFullscreen(document.fullscreenElement !== null); + }; const handleTouchStart = (ev: React.TouchEvent) => { setInstantTransition(true); @@ -212,6 +296,16 @@ export const LightboxComponent: React.FC = ({ el.addEventListener("touchcancel", handleCancel); }; + const [clearCallback, resetCallback] = useInterval( + () => { + handleRight(false); + }, + slideshowEnabled ? slideshowInterval : null + ); + + resetIntervalCallback.current = resetCallback; + clearIntervalCallback.current = clearCallback; + useEffect(() => { if (isVisible) { document.addEventListener("keydown", handleKey); @@ -228,6 +322,10 @@ export const LightboxComponent: React.FC = ({ else document.exitFullscreen(); }, [isFullscreen]); + const handleSlideshowIntervalChange = (newSlideshowInterval: number) => { + setSlideshowInterval(newSlideshowInterval); + }; + const navItems = images.map((image, i) => ( = ({ /> )); + const onDelayChange = (e: React.ChangeEvent) => { + let numberValue = Number.parseInt(e.currentTarget.value, 10); + // Without this exception, the blocking of updates for invalid values is even weirder + if (e.currentTarget.value === "-" || e.currentTarget.value === "") { + setDisplayedSlideshowInterval(e.currentTarget.value); + return; + } + + setDisplayedSlideshowInterval(e.currentTarget.value); + if (slideshowInterval !== null) { + numberValue = + numberValue >= MIN_VALID_INTERVAL_SECONDS + ? numberValue + : MIN_VALID_INTERVAL_SECONDS; + handleSlideshowIntervalChange(numberValue * SECONDS_TO_MS); + } + }; + const currentIndex = index.current === null ? initialIndex : index.current; + const DelayForm: React.FC<{}> = () => ( + <> + + Delay (Sec) + + + + + + ); + + const delayPopover = ( + + Set slideshow delay + + + + + + + ); + const element = isVisible ? (
{images.length > 0 && !isLoading && !isSwitchingPage ? ( <>
+
{pageHeader} {`${currentIndex + 1} / ${images.length}`}
- {document.fullscreenEnabled && ( +
+ {slideshowEnabled && ( + <> +
+
+ + + +
+ + + +
+ + + )} + {document.fullscreenEnabled && ( + + )} - )} - +
{images.length > 1 && ( diff --git a/ui/v2.5/src/hooks/Lightbox/context.tsx b/ui/v2.5/src/hooks/Lightbox/context.tsx index cff2e2e28..8a54ccf93 100644 --- a/ui/v2.5/src/hooks/Lightbox/context.tsx +++ b/ui/v2.5/src/hooks/Lightbox/context.tsx @@ -12,6 +12,8 @@ export interface IState { initialIndex?: number; pageCallback?: (direction: number) => boolean; pageHeader?: string; + slideshowEnabled: boolean; + onClose?: () => void; } interface IContext { setLightboxState: (state: Partial) => void; @@ -26,6 +28,7 @@ const Lightbox: React.FC = ({ children }) => { isVisible: false, isLoading: false, showNavigation: true, + slideshowEnabled: false, }); const setPartialState = useCallback( @@ -38,14 +41,18 @@ const Lightbox: React.FC = ({ children }) => { [setLightboxState] ); + const onHide = () => { + setLightboxState({ ...lightboxState, isVisible: false }); + if (lightboxState.onClose) { + lightboxState.onClose(); + } + }; + return ( {children} {lightboxState.isVisible && ( - setLightboxState({ ...lightboxState, isVisible: false })} - /> + )} ); diff --git a/ui/v2.5/src/hooks/Lightbox/hooks.ts b/ui/v2.5/src/hooks/Lightbox/hooks.ts index fa5685e17..f2863b55b 100644 --- a/ui/v2.5/src/hooks/Lightbox/hooks.ts +++ b/ui/v2.5/src/hooks/Lightbox/hooks.ts @@ -12,6 +12,8 @@ export const useLightbox = (state: Partial>) => { pageCallback: state.pageCallback, initialIndex: state.initialIndex, pageHeader: state.pageHeader, + slideshowEnabled: state.slideshowEnabled, + onClose: state.onClose, }); }, [ setLightboxState, @@ -20,13 +22,16 @@ export const useLightbox = (state: Partial>) => { state.pageCallback, state.initialIndex, state.pageHeader, + state.slideshowEnabled, + state.onClose, ]); const show = useCallback( - (index?: number) => { + (index?: number, slideshowEnabled = false) => { setLightboxState({ initialIndex: index, isVisible: true, + slideshowEnabled, }); }, [setLightboxState] diff --git a/ui/v2.5/src/hooks/Lightbox/lightbox.scss b/ui/v2.5/src/hooks/Lightbox/lightbox.scss index 895c7bd80..fa83980d0 100644 --- a/ui/v2.5/src/hooks/Lightbox/lightbox.scss +++ b/ui/v2.5/src/hooks/Lightbox/lightbox.scss @@ -26,14 +26,51 @@ flex-shrink: 0; height: 4rem; + &-left-spacer { + display: flex; + flex: 1; + justify-content: center; + } + &-indicator { display: flex; + flex: 1; flex-direction: column; - margin-left: 49%; margin-right: auto; text-align: center; } + &-delay { + display: flex; + flex-direction: column; + margin-left: 100px; + text-align: left; + + &-icon { + display: inline-block; + } + + &-inline { + display: none; + } + + @media screen and (min-width: 1300px) { + &-icon { + display: none; + } + + &-inline { + display: flex; + } + } + } + + &-right { + display: flex; + flex: 1; + justify-content: flex-end; + } + .fa-icon { height: 1.5rem; opacity: 1; diff --git a/ui/v2.5/src/hooks/PageVisibility.ts b/ui/v2.5/src/hooks/PageVisibility.ts new file mode 100644 index 000000000..afc7c6af1 --- /dev/null +++ b/ui/v2.5/src/hooks/PageVisibility.ts @@ -0,0 +1,50 @@ +import { useEffect, useRef } from "react"; + +const usePageVisibility = (visibilityChangeCallback: () => void): void => { + const savedVisibilityChangedCallback = useRef<() => void>(); + + useEffect(() => { + // resolve event names for different browsers + let hidden = ""; + let visibilityChange = ""; + + if (typeof document.hidden !== "undefined") { + hidden = "hidden"; + visibilityChange = "visibilitychange"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } else if (typeof (document as any).msHidden !== "undefined") { + hidden = "msHidden"; + visibilityChange = "msvisibilitychange"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } else if (typeof (document as any).webkitHidden !== "undefined") { + hidden = "webkitHidden"; + visibilityChange = "webkitvisibilitychange"; + } + + if ( + typeof document.addEventListener === "undefined" || + hidden === undefined + ) { + // this browser doesn't have support for modern event listeners or the Page Visibility API + return; + } + + savedVisibilityChangedCallback.current = visibilityChangeCallback; + + document.addEventListener( + visibilityChange, + savedVisibilityChangedCallback.current + ); + + return () => { + if (savedVisibilityChangedCallback.current) { + document.removeEventListener( + visibilityChange, + savedVisibilityChangedCallback.current + ); + } + }; + }, [visibilityChangeCallback]); +}; + +export default usePageVisibility; diff --git a/ui/v2.5/src/hooks/index.ts b/ui/v2.5/src/hooks/index.ts index 55f5ddeba..457ce6cd0 100644 --- a/ui/v2.5/src/hooks/index.ts +++ b/ui/v2.5/src/hooks/index.ts @@ -1,4 +1,6 @@ export { default as useToast } from "./Toast"; +export { default as useInterval } from "./Interval"; +export { default as usePageVisibility } from "./PageVisibility"; export { useInterfaceLocalForage, useChangelogStorage,