From 62a9034f5b75dd6c5b2dc44e8a08a9e183435d68 Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Wed, 16 Nov 2022 21:46:30 +0300 Subject: [PATCH 1/2] Convert emby-button and emby-scroller to react --- src/elements/emby-button/Button.tsx | 42 +++ src/elements/emby-button/IconButton.tsx | 41 +++ src/elements/emby-button/LinkButton.tsx | 65 +++++ .../emby-scrollbuttons/ScrollButtons.tsx | 83 ++++++ src/elements/emby-scroller/Scroller.tsx | 253 ++++++++++++++++++ 5 files changed, 484 insertions(+) create mode 100644 src/elements/emby-button/Button.tsx create mode 100644 src/elements/emby-button/IconButton.tsx create mode 100644 src/elements/emby-button/LinkButton.tsx create mode 100644 src/elements/emby-scrollbuttons/ScrollButtons.tsx create mode 100644 src/elements/emby-scroller/Scroller.tsx diff --git a/src/elements/emby-button/Button.tsx b/src/elements/emby-button/Button.tsx new file mode 100644 index 0000000000..ba3e7b9d11 --- /dev/null +++ b/src/elements/emby-button/Button.tsx @@ -0,0 +1,42 @@ +import React, { ButtonHTMLAttributes, DetailedHTMLProps } from 'react'; +import classNames from 'classnames'; +import layoutManager from '../../components/layoutManager'; +import './emby-button.scss'; + +interface ButtonProps extends DetailedHTMLProps, + HTMLButtonElement + > { + icon?: string; + iconClassName?: string; + iconPos?: string; +} + +const Button: React.FC = ({ + className, + title, + icon, + iconClassName, + iconPos, + onClick, + ...rest +}) => { + let cssClass = classNames('emby-button', className); + + if (layoutManager.tv) { + cssClass += ' show-focus'; + } + + return ( + + ); +}; + +export default Button; diff --git a/src/elements/emby-button/IconButton.tsx b/src/elements/emby-button/IconButton.tsx new file mode 100644 index 0000000000..e908a81920 --- /dev/null +++ b/src/elements/emby-button/IconButton.tsx @@ -0,0 +1,41 @@ +import React, { ButtonHTMLAttributes, DetailedHTMLProps } from 'react'; +import classNames from 'classnames'; +import layoutManager from '../../components/layoutManager'; +import './emby-button.scss'; + +interface IconButtonProps extends DetailedHTMLProps, + HTMLButtonElement + > { + icon?: string; + iconClassName?: string; +} + +const IconButton: React.FC = ({ + className, + title, + icon, + iconClassName, + disabled = false, + onClick, + ...rest +}) => { + let cssClass = classNames('paper-icon-button-light', className); + + if (layoutManager.tv) { + cssClass += ' show-focus'; + } + + return ( + + ); +}; + +export default IconButton; diff --git a/src/elements/emby-button/LinkButton.tsx b/src/elements/emby-button/LinkButton.tsx new file mode 100644 index 0000000000..fdf88cca29 --- /dev/null +++ b/src/elements/emby-button/LinkButton.tsx @@ -0,0 +1,65 @@ +import React, { AnchorHTMLAttributes, DetailedHTMLProps, MouseEvent } from 'react'; +import classNames from 'classnames'; +import layoutManager from '../../components/layoutManager'; +import shell from '../../scripts/shell'; +import { appRouter } from '../../components/appRouter'; +import { appHost } from '../../components/apphost'; +import './emby-button.scss'; + +interface LinkButtonProps extends DetailedHTMLProps, + HTMLAnchorElement + > { + className?: string; + isAutoHideEnabled?: boolean; + href?: string; + target?: string; +} + +const LinkButton: React.FC = ({ + className, + isAutoHideEnabled, + href, + target, + children, + ...rest +}) => { + const onAnchorClick = (e: MouseEvent) => { + const url = href || ''; + if (url !== '#') { + if (target) { + if (!appHost.supports('targetblank')) { + e.preventDefault(); + shell.openUrl(url); + } + } else { + e.preventDefault(); + appRouter.show(url); + } + } else { + e.preventDefault(); + } + }; + + if (isAutoHideEnabled === true && !appHost.supports('externallinks')) { + return null; + } + let cssClass = classNames('emby-button', className); + + if (layoutManager.tv) { + cssClass += ' show-focus'; + } + + return ( + + {children} + + ); +}; + +export default LinkButton; diff --git a/src/elements/emby-scrollbuttons/ScrollButtons.tsx b/src/elements/emby-scrollbuttons/ScrollButtons.tsx new file mode 100644 index 0000000000..e19f145dbb --- /dev/null +++ b/src/elements/emby-scrollbuttons/ScrollButtons.tsx @@ -0,0 +1,83 @@ +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import scrollerFactory from '../../libraries/scroller'; +import globalize from '../../scripts/globalize'; +import IconButton from '../emby-button/IconButton'; +import './emby-scrollbuttons.scss'; + +enum Direction { + RIGHT, + LEFT, + } + +interface ScrollButtonsProps { + scrollRef?: React.MutableRefObject; + scrollerFactoryRef: React.MutableRefObject; + scrollState: { + scrollSize: number; + scrollPos: number; + scrollWidth: number; + } +} + +const ScrollButtons: FC = ({ scrollerFactoryRef, scrollState }) => { + const [localeScrollPos, SetLocaleScrollPos] = useState(0); + const scrollButtonsRef = useRef(null); + + const scrollToPosition = useCallback((pos: number, immediate: boolean) => { + if (scrollerFactoryRef.current) { + scrollerFactoryRef.current.slideTo(pos, immediate, undefined ); + } + }, [scrollerFactoryRef]); + + const onScrollButtonClick = (direction: Direction) => { + let newPos; + if (direction === Direction.LEFT) { + newPos = Math.max(0, scrollState.scrollPos - scrollState.scrollSize); + } else { + newPos = scrollState.scrollPos + scrollState.scrollSize; + } + + if (globalize.getIsRTL() && direction === Direction.LEFT) { + newPos = scrollState.scrollPos + scrollState.scrollSize; + } else if (globalize.getIsRTL()) { + newPos = Math.min(0, scrollState.scrollPos - scrollState.scrollSize); + } + + scrollToPosition(newPos, false); + }; + + useEffect(() => { + const parent = scrollButtonsRef.current?.parentNode as HTMLDivElement; + parent.classList.add('emby-scroller-container'); + + let localeAwarePos = scrollState.scrollPos; + if (globalize.getIsElementRTL(scrollButtonsRef.current)) { + localeAwarePos *= -1; + } + SetLocaleScrollPos(localeAwarePos); + }, [scrollState.scrollPos]); + + return ( +
+ + onScrollButtonClick(Direction.LEFT)} + icon='chevron_left' + disabled={localeScrollPos > 0 ? false : true} + /> + + onScrollButtonClick(Direction.RIGHT)} + icon='chevron_right' + disabled={scrollState.scrollWidth > 0 && localeScrollPos + scrollState.scrollSize >= scrollState.scrollWidth ? true : false} + /> +
+ ); +}; + +export default ScrollButtons; + diff --git a/src/elements/emby-scroller/Scroller.tsx b/src/elements/emby-scroller/Scroller.tsx new file mode 100644 index 0000000000..1956b1d664 --- /dev/null +++ b/src/elements/emby-scroller/Scroller.tsx @@ -0,0 +1,253 @@ +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import classNames from 'classnames'; +import layoutManager from '../../components/layoutManager'; +import dom from '../../scripts/dom'; +import browser from '../../scripts/browser'; +import focusManager from '../../components/focusManager'; +import scrollerFactory from '../../libraries/scroller'; +import ScrollButtons from '../emby-scrollbuttons/ScrollButtons'; +import './emby-scroller.scss'; + +interface ScrollerProps { + className?: string; + isHorizontalEnabled?: boolean; + isMouseWheelEnabled?: boolean; + isCenterFocusEnabled?: boolean; + isScrollButtonsEnabled?: boolean; + isSkipFocusWhenVisibleEnabled?: boolean; + isScrollEventEnabled?: boolean; + isHideScrollbarEnabled?: boolean; + isAllowNativeSmoothScrollEnabled?: boolean; +} + +const Scroller: FC = ({ + className, + isHorizontalEnabled, + isMouseWheelEnabled, + isCenterFocusEnabled, + isScrollButtonsEnabled, + isSkipFocusWhenVisibleEnabled, + isScrollEventEnabled, + isHideScrollbarEnabled, + isAllowNativeSmoothScrollEnabled, + children +}) => { + const [showControls, SetShowControls] = useState(false); + const [scrollState, setScrollState] = useState({ + scrollSize: 0, + scrollPos: 0, + scrollWidth: 0 + }); + + const scrollRef = useRef(null); + const scrollerFactoryRef = useRef(null); + + const getScrollSlider = useCallback(() => { + if (scrollerFactoryRef.current) { + return scrollerFactoryRef.current.getScrollSlider(); + } + }, [scrollerFactoryRef]); + + const getScrollPosition = useCallback(() => { + if (scrollerFactoryRef.current) { + return scrollerFactoryRef.current.getScrollPosition(); + } + + return 0; + }, [scrollerFactoryRef]); + + const getScrollWidth = useCallback(() => { + if (scrollerFactoryRef.current) { + return scrollerFactoryRef.current.getScrollSize(); + } + + return 0; + }, [scrollerFactoryRef]); + + const getStyleValue = useCallback((style: CSSStyleDeclaration, name: string) => { + let value = style.getPropertyValue(name); + if (!value) { + return 0; + } + + value = value.replace('px', ''); + if (!value) { + return 0; + } + + if (isNaN(parseInt(value))) { + return 0; + } + + return Number(value); + }, []); + + const getScrollSize = useCallback(() => { + const scroller = scrollRef?.current as HTMLDivElement; + let scrollSize = scroller.offsetWidth; + let style = window.getComputedStyle(scroller, null); + + let paddingLeft = getStyleValue(style, 'padding-left'); + if (paddingLeft) { + scrollSize -= paddingLeft; + } + + let paddingRight = getStyleValue(style, 'padding-right'); + if (paddingRight) { + scrollSize -= paddingRight; + } + + const slider = getScrollSlider(); + style = window.getComputedStyle(slider, null); + + paddingLeft = getStyleValue(style, 'padding-left'); + if (paddingLeft) { + scrollSize -= paddingLeft; + } + + paddingRight = getStyleValue(style, 'padding-right'); + if (paddingRight) { + scrollSize -= paddingRight; + } + + return scrollSize; + }, [getScrollSlider, getStyleValue, scrollRef]); + + const onScroll = useCallback(() => { + const scrollSize = getScrollSize(); + const scrollPos = getScrollPosition(); + const scrollWidth = getScrollWidth(); + + setScrollState({ + scrollSize: scrollSize, + scrollPos: scrollPos, + scrollWidth: scrollWidth + }); + }, [getScrollPosition, getScrollSize, getScrollWidth]); + + const initCenterFocus = useCallback((elem: EventTarget, scrollerInstance: scrollerFactory) => { + dom.addEventListener(elem, 'focus', function (e: { target: any; }) { + const focused = focusManager.focusableParent(e.target); + if (focused) { + scrollerInstance.toCenter(focused, false); + } + }, { + capture: true, + passive: true + }); + }, []); + + const addScrollEventListener = useCallback((fn, options) => { + if (scrollerFactoryRef.current) { + dom.addEventListener(scrollerFactoryRef.current.getScrollFrame(), scrollerFactoryRef.current.getScrollEventName(), fn, options); + } + }, [scrollerFactoryRef]); + + const removeScrollEventListener = useCallback((fn, options) => { + if (scrollerFactoryRef.current) { + dom.removeEventListener(scrollerFactoryRef.current.getScrollFrame(), scrollerFactoryRef.current.getScrollEventName(), fn, options); + } + }, [scrollerFactoryRef]); + + useEffect(() => { + const scrollerElement = scrollRef.current as HTMLDivElement; + + const horizontal = isHorizontalEnabled !== false; + const scrollbuttons = isScrollButtonsEnabled !== false; + const mousewheel = isMouseWheelEnabled !== false; + + const slider = scrollerElement.querySelector('.scrollSlider'); + + const scrollFrame = scrollerElement; + const enableScrollButtons = layoutManager.desktop && horizontal && scrollbuttons; + + const options = { + horizontal: horizontal, + mouseDragging: 1, + mouseWheel: mousewheel, + touchDragging: 1, + slidee: slider, + scrollBy: 200, + speed: horizontal ? 270 : 240, + elasticBounds: 1, + dragHandle: 1, + autoImmediate: true, + skipSlideToWhenVisible: isSkipFocusWhenVisibleEnabled === true, + dispatchScrollEvent: enableScrollButtons || isScrollEventEnabled === true, + hideScrollbar: enableScrollButtons || isHideScrollbarEnabled === true, + allowNativeSmoothScroll: isAllowNativeSmoothScrollEnabled === true && !enableScrollButtons, + allowNativeScroll: !enableScrollButtons, + forceHideScrollbars: enableScrollButtons, + // In edge, with the native scroll, the content jumps around when hovering over the buttons + requireAnimation: enableScrollButtons && browser.edge + }; + + // If just inserted it might not have any height yet - yes this is a hack + scrollerFactoryRef.current = new scrollerFactory(scrollFrame, options); + scrollerFactoryRef.current.init(); + scrollerFactoryRef.current.reload(); + + if (layoutManager.tv && isCenterFocusEnabled) { + initCenterFocus(scrollerElement, scrollerFactoryRef.current); + } + + if (enableScrollButtons) { + addScrollEventListener(onScroll, { + capture: false, + passive: true + }); + SetShowControls(true); + } + + return () => { + const scrollerInstance = scrollerFactoryRef.current; + if (scrollerInstance) { + scrollerInstance.destroy(); + scrollerFactoryRef.current = null; + } + + removeScrollEventListener(onScroll, { + capture: false, + passive: true + }); + }; + }, [ + addScrollEventListener, + initCenterFocus, + isAllowNativeSmoothScrollEnabled, + isCenterFocusEnabled, + isHideScrollbarEnabled, + isHorizontalEnabled, + isMouseWheelEnabled, + isScrollButtonsEnabled, + isScrollEventEnabled, + isSkipFocusWhenVisibleEnabled, + onScroll, + removeScrollEventListener + ]); + + return ( + <> + { + showControls && scrollState.scrollWidth > scrollState.scrollSize + 20 ? + : null + } + +
+ {children} + +
+ + + ); +}; + +export default Scroller; + From 72cbd3718206ceb15c0c2b68e53039ff2fc466db Mon Sep 17 00:00:00 2001 From: grafixeyehero Date: Thu, 15 Dec 2022 22:54:58 +0300 Subject: [PATCH 2/2] apply suggestion --- src/elements/emby-button/Button.tsx | 25 +++++++++++++------ src/elements/emby-button/IconButton.tsx | 18 ++++++++----- src/elements/emby-button/LinkButton.tsx | 9 ++++--- .../emby-scrollbuttons/ScrollButtons.tsx | 4 +-- src/elements/emby-scroller/Scroller.tsx | 8 +++--- 5 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/elements/emby-button/Button.tsx b/src/elements/emby-button/Button.tsx index ba3e7b9d11..8f742c41bb 100644 --- a/src/elements/emby-button/Button.tsx +++ b/src/elements/emby-button/Button.tsx @@ -3,6 +3,11 @@ import classNames from 'classnames'; import layoutManager from '../../components/layoutManager'; import './emby-button.scss'; +enum IconPosition { + RIGHT = 'RIGHT', + LEFT = 'LEFT', + } + interface ButtonProps extends DetailedHTMLProps, HTMLButtonElement > { @@ -20,21 +25,27 @@ const Button: React.FC = ({ onClick, ...rest }) => { - let cssClass = classNames('emby-button', className); + const btnClass = classNames( + 'emby-button', + className, + { 'show-focus': layoutManager.tv } + ); - if (layoutManager.tv) { - cssClass += ' show-focus'; - } + const iconClass = classNames( + 'material-icons', + iconClassName, + icon + ); return ( ); }; diff --git a/src/elements/emby-button/IconButton.tsx b/src/elements/emby-button/IconButton.tsx index e908a81920..3f4f66e198 100644 --- a/src/elements/emby-button/IconButton.tsx +++ b/src/elements/emby-button/IconButton.tsx @@ -19,21 +19,27 @@ const IconButton: React.FC = ({ onClick, ...rest }) => { - let cssClass = classNames('paper-icon-button-light', className); + const btnClass = classNames( + 'paper-icon-button-light', + className, + { 'show-focus': layoutManager.tv } + ); - if (layoutManager.tv) { - cssClass += ' show-focus'; - } + const iconClass = classNames( + 'material-icons', + iconClassName, + icon + ); return ( ); }; diff --git a/src/elements/emby-button/LinkButton.tsx b/src/elements/emby-button/LinkButton.tsx index fdf88cca29..2a175bd177 100644 --- a/src/elements/emby-button/LinkButton.tsx +++ b/src/elements/emby-button/LinkButton.tsx @@ -43,11 +43,12 @@ const LinkButton: React.FC = ({ if (isAutoHideEnabled === true && !appHost.supports('externallinks')) { return null; } - let cssClass = classNames('emby-button', className); - if (layoutManager.tv) { - cssClass += ' show-focus'; - } + const cssClass = classNames( + 'emby-button', + className, + { 'show-focus': layoutManager.tv } + ); return ( = ({ scrollerFactoryRef, scrollState }) => { - const [localeScrollPos, SetLocaleScrollPos] = useState(0); + const [localeScrollPos, setLocaleScrollPos] = useState(0); const scrollButtonsRef = useRef(null); const scrollToPosition = useCallback((pos: number, immediate: boolean) => { @@ -54,7 +54,7 @@ const ScrollButtons: FC = ({ scrollerFactoryRef, scrollState if (globalize.getIsElementRTL(scrollButtonsRef.current)) { localeAwarePos *= -1; } - SetLocaleScrollPos(localeAwarePos); + setLocaleScrollPos(localeAwarePos); }, [scrollState.scrollPos]); return ( diff --git a/src/elements/emby-scroller/Scroller.tsx b/src/elements/emby-scroller/Scroller.tsx index 1956b1d664..6cd83fc784 100644 --- a/src/elements/emby-scroller/Scroller.tsx +++ b/src/elements/emby-scroller/Scroller.tsx @@ -32,7 +32,7 @@ const Scroller: FC = ({ isAllowNativeSmoothScrollEnabled, children }) => { - const [showControls, SetShowControls] = useState(false); + const [showControls, setShowControls] = useState(false); const [scrollState, setScrollState] = useState({ scrollSize: 0, scrollPos: 0, @@ -196,7 +196,7 @@ const Scroller: FC = ({ capture: false, passive: true }); - SetShowControls(true); + setShowControls(true); } return () => { @@ -229,12 +229,12 @@ const Scroller: FC = ({ return ( <> { - showControls && scrollState.scrollWidth > scrollState.scrollSize + 20 ? + showControls && scrollState.scrollWidth > scrollState.scrollSize + 20 && : null + /> }