mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-21 22:05:43 -04:00
Convert Page components to TypeScript
(cherry picked from commit f35a27449d253260ba9c9fae28909cec8a87b4fe)
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
import classNames from 'classnames';
|
||||
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 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 MessagesConnector from './Messages/MessagesConnector';
|
||||
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.MOVIE_CONTINUING,
|
||||
title: () => translate('Movies'),
|
||||
to: '/',
|
||||
alias: '/movies',
|
||||
children: [
|
||||
{
|
||||
title: () => translate('AddNew'),
|
||||
to: '/add/new',
|
||||
},
|
||||
{
|
||||
title: () => translate('ImportLibrary'),
|
||||
to: '/add/import',
|
||||
},
|
||||
{
|
||||
title: () => translate('Collections'),
|
||||
to: '/collections',
|
||||
},
|
||||
{
|
||||
title: () => translate('Discover'),
|
||||
to: '/add/discover',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
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('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<number | null>(null);
|
||||
const touchStartY = useRef<number | null>();
|
||||
const wasSidebarVisible = usePrevious(isSidebarVisible);
|
||||
|
||||
const [sidebarTransform, setSidebarTransform] = useState<{
|
||||
transition: string;
|
||||
transform: number;
|
||||
}>({
|
||||
transition: 'none',
|
||||
transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1,
|
||||
});
|
||||
const [sidebarStyle, setSidebarStyle] = useState({
|
||||
top: dimensions.headerHeight,
|
||||
height: `${window.innerHeight - HEADER_HEIGHT}px`,
|
||||
});
|
||||
|
||||
const urlBase = window.Radarr.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 handleWindowScroll = useCallback(() => {
|
||||
const windowScroll =
|
||||
window.scrollY == null
|
||||
? document.documentElement.scrollTop
|
||||
: window.scrollY;
|
||||
const sidebarTop = Math.max(HEADER_HEIGHT - windowScroll, 0);
|
||||
const sidebarHeight = window.innerHeight - sidebarTop;
|
||||
|
||||
if (isSmallScreen) {
|
||||
setSidebarStyle({
|
||||
top: `${sidebarTop}px`,
|
||||
height: `${sidebarHeight}px`,
|
||||
});
|
||||
}
|
||||
}, [isSmallScreen]);
|
||||
|
||||
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: 0,
|
||||
});
|
||||
}
|
||||
|
||||
touchStartX.current = null;
|
||||
touchStartY.current = null;
|
||||
}, []);
|
||||
|
||||
const handleTouchCancel = useCallback(() => {
|
||||
touchStartX.current = null;
|
||||
touchStartY.current = null;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSmallScreen) {
|
||||
window.addEventListener('click', handleWindowClick, { capture: true });
|
||||
window.addEventListener('scroll', handleWindowScroll);
|
||||
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('scroll', handleWindowScroll);
|
||||
window.removeEventListener('touchstart', handleTouchStart);
|
||||
window.removeEventListener('touchmove', handleTouchMove);
|
||||
window.removeEventListener('touchend', handleTouchEnd);
|
||||
window.removeEventListener('touchcancel', handleTouchCancel);
|
||||
};
|
||||
}, [
|
||||
isSmallScreen,
|
||||
handleWindowClick,
|
||||
handleWindowScroll,
|
||||
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 (
|
||||
<div
|
||||
ref={sidebarRef}
|
||||
className={classNames(styles.sidebarContainer)}
|
||||
style={containerStyle}
|
||||
>
|
||||
<ScrollerComponent
|
||||
className={styles.sidebar}
|
||||
scrollDirection="vertical"
|
||||
style={sidebarStyle}
|
||||
>
|
||||
<div>
|
||||
{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 (
|
||||
<PageSidebarItem
|
||||
key={link.to}
|
||||
iconName={link.iconName}
|
||||
title={link.title}
|
||||
to={link.to}
|
||||
statusComponent={
|
||||
isActiveParent || !childStatusComponent
|
||||
? link.statusComponent
|
||||
: childStatusComponent
|
||||
}
|
||||
isActive={pathname === link.to && !hasActiveChild}
|
||||
isActiveParent={isActiveParent}
|
||||
isParentItem={!!link.children}
|
||||
onPress={handleItemPress}
|
||||
>
|
||||
{link.children &&
|
||||
link.to === activeParent &&
|
||||
link.children.map((child) => {
|
||||
return (
|
||||
<PageSidebarItem
|
||||
key={child.to}
|
||||
title={child.title}
|
||||
to={child.to}
|
||||
isActive={pathname === child.to}
|
||||
isParentItem={false}
|
||||
isChildItem={true}
|
||||
statusComponent={child.statusComponent}
|
||||
onPress={handleItemPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</PageSidebarItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<MessagesConnector />
|
||||
</ScrollerComponent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageSidebar;
|
||||
Reference in New Issue
Block a user