import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import ReactDOM from 'react-dom'; import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router'; import QueueStatus from 'Activity/Queue/Status/QueueStatus'; import { IconName } from 'Components/Icon'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import OverlayScroller from 'Components/Scroller/OverlayScroller'; import Scroller from 'Components/Scroller/Scroller'; import usePrevious from 'Helpers/Hooks/usePrevious'; import { icons } from 'Helpers/Props'; import { setIsSidebarVisible } from 'Store/Actions/appActions'; import dimensions from 'Styles/Variables/dimensions'; import HealthStatus from 'System/Status/Health/HealthStatus'; import translate from 'Utilities/String/translate'; import Messages from './Messages/Messages'; import PageSidebarItem from './PageSidebarItem'; import styles from './PageSidebar.css'; const HEADER_HEIGHT = parseInt(dimensions.headerHeight); const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth); interface SidebarItem { iconName?: IconName; title: string | (() => string); to: string; alias?: string; isActive?: boolean; isActiveParent?: boolean; isParentItem?: boolean; isChildItem?: boolean; statusComponent?: React.ElementType; children?: { title: string | (() => string); to: string; statusComponent?: React.ElementType; }[]; } const LINKS: SidebarItem[] = [ { iconName: icons.SERIES_CONTINUING, title: () => translate('Series'), to: '/', alias: '/series', children: [ { title: () => translate('AddNew'), to: '/add/new', }, { title: () => translate('LibraryImport'), to: '/add/import', }, ], }, { iconName: icons.CALENDAR, title: () => translate('Calendar'), to: '/calendar', }, { iconName: icons.ACTIVITY, title: () => translate('Activity'), to: '/activity/queue', children: [ { title: () => translate('Queue'), to: '/activity/queue', statusComponent: QueueStatus, }, { title: () => translate('History'), to: '/activity/history', }, { title: () => translate('Blocklist'), to: '/activity/blocklist', }, ], }, { iconName: icons.WARNING, title: () => translate('Wanted'), to: '/wanted/missing', children: [ { title: () => translate('Missing'), to: '/wanted/missing', }, { title: () => translate('CutoffUnmet'), to: '/wanted/cutoffunmet', }, ], }, { iconName: icons.SETTINGS, title: () => translate('Settings'), to: '/settings', children: [ { title: () => translate('MediaManagement'), to: '/settings/mediamanagement', }, { title: () => translate('Profiles'), to: '/settings/profiles', }, { title: () => translate('Quality'), to: '/settings/quality', }, { title: () => translate('CustomFormats'), to: '/settings/customformats', }, { title: () => translate('Indexers'), to: '/settings/indexers', }, { title: () => translate('DownloadClients'), to: '/settings/downloadclients', }, { title: () => translate('ImportLists'), to: '/settings/importlists', }, { title: () => translate('Connect'), to: '/settings/connect', }, { title: () => translate('Metadata'), to: '/settings/metadata', }, { title: () => translate('MetadataSource'), to: '/settings/metadatasource', }, { title: () => translate('Tags'), to: '/settings/tags', }, { title: () => translate('General'), to: '/settings/general', }, { title: () => translate('Ui'), to: '/settings/ui', }, ], }, { iconName: icons.SYSTEM, title: () => translate('System'), to: '/system/status', children: [ { title: () => translate('Status'), to: '/system/status', statusComponent: HealthStatus, }, { title: () => translate('Tasks'), to: '/system/tasks', }, { title: () => translate('Backup'), to: '/system/backup', }, { title: () => translate('Updates'), to: '/system/updates', }, { title: () => translate('Events'), to: '/system/events', }, { title: () => translate('LogFiles'), to: '/system/logs/files', }, ], }, ]; function hasActiveChildLink(link: SidebarItem, pathname: string) { const children = link.children; if (!children || !children.length) { return false; } return children.some((child) => { return child.to === pathname; }); } interface PageSidebarProps { isSmallScreen: boolean; isSidebarVisible: boolean; } function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) { const dispatch = useDispatch(); const location = useLocation(); const sidebarRef = useRef(null); const touchStartX = useRef(null); const touchStartY = useRef(); const wasSidebarVisible = usePrevious(isSidebarVisible); const [sidebarTransform, setSidebarTransform] = useState<{ transition: string; transform: number; }>({ transition: 'none', transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1, }); const urlBase = window.Sonarr.urlBase; const pathname = urlBase ? location.pathname.substr(urlBase.length) || '/' : location.pathname; const activeParent = useMemo(() => { return ( LINKS.find((link) => { if (link.to && link.to === pathname) { return true; } const children = link.children; if (children) { const matchingChild = children.find((childLink) => { return pathname.startsWith(childLink.to); }); if (matchingChild) { return matchingChild; } } if ( (link.to !== '/' && pathname.startsWith(link.to)) || (link.alias && pathname.startsWith(link.alias)) ) { return true; } return false; })?.to ?? LINKS[0].to ); }, [pathname]); const handleWindowClick = useCallback( (event: MouseEvent) => { const sidebar = ReactDOM.findDOMNode(sidebarRef.current); const toggleButton = document.getElementById('sidebar-toggle-button'); const target = event.target; if (!sidebar) { return; } if ( target instanceof Node && !sidebar.contains(target) && !toggleButton?.contains(target) && isSidebarVisible ) { event.preventDefault(); event.stopPropagation(); dispatch(setIsSidebarVisible({ isSidebarVisible: false })); } }, [isSidebarVisible, dispatch] ); const handleItemPress = useCallback(() => { dispatch(setIsSidebarVisible({ isSidebarVisible: false })); }, [dispatch]); const handleTouchStart = useCallback( (event: TouchEvent) => { const touches = event.touches; const x = touches[0].pageX; const y = touches[0].pageY; if (touches.length !== 1) { return; } if (isSidebarVisible && (x > 210 || x < 180)) { return; } else if (!isSidebarVisible && x > 40) { return; } touchStartX.current = x; touchStartY.current = y; }, [isSidebarVisible] ); const handleTouchMove = useCallback((event: TouchEvent) => { const touches = event.touches; const currentTouchX = touches[0].pageX; // const currentTouchY = touches[0].pageY; // const isSidebarVisible = this.props.isSidebarVisible; if (!touchStartX.current) { return; } if (Math.abs(touchStartX.current - currentTouchX) < 40) { return; } const transform = Math.min(currentTouchX - SIDEBAR_WIDTH, 0); setSidebarTransform({ transition: 'none', transform, }); }, []); const handleTouchEnd = useCallback( (event: TouchEvent) => { const touches = event.changedTouches; const currentTouch = touches[0].pageX; if (!touchStartX.current) { return; } if (currentTouch > touchStartX.current && currentTouch > 50) { setSidebarTransform({ transition: 'none', transform: 0, }); } else if (currentTouch < touchStartX.current && currentTouch < 80) { setSidebarTransform({ transition: 'transform 50ms ease-in-out', transform: SIDEBAR_WIDTH * -1, }); } else { setSidebarTransform({ transition: 'none', transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1, }); } touchStartX.current = null; touchStartY.current = null; }, [isSidebarVisible] ); const handleTouchCancel = useCallback(() => { touchStartX.current = null; touchStartY.current = null; }, []); const handleSidebarClosePress = useCallback(() => { dispatch(setIsSidebarVisible({ isSidebarVisible: false })); }, [dispatch]); useEffect(() => { if (isSmallScreen) { window.addEventListener('click', handleWindowClick, { capture: true }); window.addEventListener('touchstart', handleTouchStart); window.addEventListener('touchmove', handleTouchMove); window.addEventListener('touchend', handleTouchEnd); window.addEventListener('touchcancel', handleTouchCancel); } return () => { window.removeEventListener('click', handleWindowClick, { capture: true }); window.removeEventListener('touchstart', handleTouchStart); window.removeEventListener('touchmove', handleTouchMove); window.removeEventListener('touchend', handleTouchEnd); window.removeEventListener('touchcancel', handleTouchCancel); }; }, [ isSmallScreen, handleWindowClick, handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel, ]); useEffect(() => { if (wasSidebarVisible !== isSidebarVisible) { setSidebarTransform({ transition: 'none', transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1, }); } else if (sidebarTransform.transform === 0 && !isSidebarVisible) { dispatch(setIsSidebarVisible({ isSidebarVisible: true })); } else if ( sidebarTransform.transform === -SIDEBAR_WIDTH && isSidebarVisible ) { dispatch(setIsSidebarVisible({ isSidebarVisible: false })); } }, [sidebarTransform, isSidebarVisible, wasSidebarVisible, dispatch]); const containerStyle = useMemo(() => { if (!isSmallScreen) { return undefined; } return { transition: sidebarTransform.transition ?? 'none', transform: `translateX(${sidebarTransform.transform}px)`, }; }, [isSmallScreen, sidebarTransform]); const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller; return (
{isSmallScreen ? (
Sonarr Logo
) : null}
{LINKS.map((link) => { const childWithStatusComponent = link.children?.find((child) => { return !!child.statusComponent; }); const childStatusComponent = childWithStatusComponent ? childWithStatusComponent.statusComponent : null; const isActiveParent = activeParent === link.to; const hasActiveChild = hasActiveChildLink(link, pathname); return ( {link.children && link.to === activeParent && link.children.map((child) => { return ( ); })} ); })}
); } export default PageSidebar;