From c62a7c310ea94f6f00b51215ef6ce573f0672375 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 2 Dec 2025 17:56:14 -0800 Subject: [PATCH] Convert app state to zustand stores --- .../AddNewSeries/AddNewSeriesModalContent.tsx | 5 +- .../AddNewSeries/AddNewSeriesSearchResult.tsx | 4 +- .../ImportSeries/Import/ImportSeriesTable.tsx | 4 +- frontend/src/App/AppUpdatedModalContent.tsx | 5 +- frontend/src/App/State/AppState.ts | 20 -- frontend/src/App/State/MessagesAppState.ts | 15 -- frontend/src/App/appStore.ts | 202 +++++++++++++++++ frontend/src/App/messagesStore.ts | 56 +++++ frontend/src/Calendar/Day/CalendarDays.tsx | 49 ++--- .../src/Calendar/Header/CalendarHeader.tsx | 6 +- .../src/Components/Page/Header/PageHeader.tsx | 12 +- frontend/src/Components/Page/Page.tsx | 31 +-- .../Page/Sidebar/Messages/Message.tsx | 9 +- .../Page/Sidebar/Messages/Messages.tsx | 6 +- .../Components/Page/Sidebar/PageSidebar.tsx | 35 ++- frontend/src/Components/SignalRListener.tsx | 50 ++--- .../Series/Details/SeriesDetailsSeason.tsx | 4 +- frontend/src/Series/Index/SeriesIndex.tsx | 4 +- frontend/src/Store/Actions/appActions.js | 207 ------------------ frontend/src/Store/Actions/commandActions.js | 11 +- frontend/src/Store/Actions/index.js | 2 - .../Selectors/createDimensionsSelector.ts | 14 +- .../Backup/RestoreBackupModalContent.tsx | 6 +- .../src/System/Status/Health/HealthStatus.tsx | 8 +- frontend/src/System/Updates/Updates.tsx | 4 +- frontend/src/System/useSystem.ts | 54 ++--- 26 files changed, 389 insertions(+), 434 deletions(-) delete mode 100644 frontend/src/App/State/MessagesAppState.ts create mode 100644 frontend/src/App/appStore.ts create mode 100644 frontend/src/App/messagesStore.ts delete mode 100644 frontend/src/Store/Actions/appActions.js diff --git a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesModalContent.tsx b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesModalContent.tsx index 27fde88d8..5374a24ca 100644 --- a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesModalContent.tsx +++ b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesModalContent.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { useSelector } from 'react-redux'; import AddSeries from 'AddSeries/AddSeries'; import { AddSeriesOptions, @@ -8,6 +7,7 @@ import { } from 'AddSeries/addSeriesOptionsStore'; import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent'; import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent'; +import { useAppDimension } from 'App/appStore'; import CheckInput from 'Components/Form/CheckInput'; import Form from 'Components/Form/Form'; import FormGroup from 'Components/Form/FormGroup'; @@ -24,7 +24,6 @@ import { getValidationFailures } from 'Helpers/Hooks/useApiMutation'; import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; import { SeriesType } from 'Series/Series'; import SeriesPoster from 'Series/SeriesPoster'; -import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import selectSettings from 'Store/Selectors/selectSettings'; import { useIsWindows } from 'System/Status/useSystemStatus'; import { InputChanged } from 'typings/inputs'; @@ -45,7 +44,7 @@ function AddNewSeriesModalContent({ }: AddNewSeriesModalContentProps) { const { title, year, overview, images, folder } = series; const options = useAddSeriesOptions(); - const { isSmallScreen } = useSelector(createDimensionsSelector()); + const isSmallScreen = useAppDimension('isSmallScreen'); const isWindows = useIsWindows(); const { isAdding, addError, addSeries } = useAddSeries(); diff --git a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.tsx b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.tsx index fff4361bf..289cb4192 100644 --- a/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.tsx +++ b/frontend/src/AddSeries/AddNewSeries/AddNewSeriesSearchResult.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import AddSeries from 'AddSeries/AddSeries'; +import { useAppDimension } from 'App/appStore'; import HeartRating from 'Components/HeartRating'; import Icon from 'Components/Icon'; import Label from 'Components/Label'; @@ -10,7 +11,6 @@ import { icons, kinds, sizes } from 'Helpers/Props'; import { Statistics } from 'Series/Series'; import SeriesGenres from 'Series/SeriesGenres'; import SeriesPoster from 'Series/SeriesPoster'; -import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createExistingSeriesSelector from 'Store/Selectors/createExistingSeriesSelector'; import translate from 'Utilities/String/translate'; import AddNewSeriesModal from './AddNewSeriesModal'; @@ -38,7 +38,7 @@ function AddNewSeriesSearchResult({ series }: AddNewSeriesSearchResultProps) { } = series; const isExistingSeries = useSelector(createExistingSeriesSelector(tvdbId)); - const { isSmallScreen } = useSelector(createDimensionsSelector()); + const isSmallScreen = useAppDimension('isSmallScreen'); const [isNewAddSeriesModalOpen, setIsNewAddSeriesModalOpen] = useState(false); const seasonCount = statistics.seasonCount; diff --git a/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesTable.tsx b/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesTable.tsx index c6356cd3b..a1b276b69 100644 --- a/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesTable.tsx +++ b/frontend/src/AddSeries/ImportSeries/Import/ImportSeriesTable.tsx @@ -2,6 +2,7 @@ import React, { RefObject, useCallback, useEffect, useRef } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { FixedSizeList, ListChildComponentProps } from 'react-window'; import { useAddSeriesOptions } from 'AddSeries/addSeriesOptionsStore'; +import { useAppDimension } from 'App/appStore'; import { useSelect } from 'App/Select/SelectContext'; import AppState from 'App/State/AppState'; import { ImportSeries } from 'App/State/ImportSeriesAppState'; @@ -12,7 +13,6 @@ import { setImportSeriesValue, } from 'Store/Actions/importSeriesActions'; import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; -import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import { CheckInputChanged } from 'typings/inputs'; import { SelectStateInputProps } from 'typings/props'; import { UnmappedFolder } from 'typings/RootFolder'; @@ -64,7 +64,7 @@ function ImportSeriesTable({ useAddSeriesOptions(); const items = useSelector((state: AppState) => state.importSeries.items); - const { isSmallScreen } = useSelector(createDimensionsSelector()); + const isSmallScreen = useAppDimension('isSmallScreen'); const allSeries = useSelector(createAllSeriesSelector()); const { allSelected, diff --git a/frontend/src/App/AppUpdatedModalContent.tsx b/frontend/src/App/AppUpdatedModalContent.tsx index 7e5bd1e24..18aab1bd2 100644 --- a/frontend/src/App/AppUpdatedModalContent.tsx +++ b/frontend/src/App/AppUpdatedModalContent.tsx @@ -1,5 +1,4 @@ import React, { useCallback, useEffect } from 'react'; -import { useSelector } from 'react-redux'; import Button from 'Components/Link/Button'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import InlineMarkdown from 'Components/Markdown/InlineMarkdown'; @@ -13,7 +12,7 @@ import UpdateChanges from 'System/Updates/UpdateChanges'; import useUpdates from 'System/Updates/useUpdates'; import Update from 'typings/Update'; import translate from 'Utilities/String/translate'; -import AppState from './State/AppState'; +import { useAppValues } from './appStore'; import styles from './AppUpdatedModalContent.css'; function mergeUpdates(items: Update[], version: string, prevVersion?: string) { @@ -63,7 +62,7 @@ interface AppUpdatedModalContentProps { } function AppUpdatedModalContent(props: AppUpdatedModalContentProps) { - const { version, prevVersion } = useSelector((state: AppState) => state.app); + const { version, prevVersion } = useAppValues('version', 'prevVersion'); const { isFetched, error, data, refetch } = useUpdates(); const previousVersion = usePrevious(version); diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 82152934d..a09bd14a5 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -4,33 +4,13 @@ import CommandAppState from './CommandAppState'; import HistoryAppState, { SeriesHistoryAppState } from './HistoryAppState'; import ImportSeriesAppState from './ImportSeriesAppState'; import InteractiveImportAppState from './InteractiveImportAppState'; -import MessagesAppState from './MessagesAppState'; import OAuthAppState from './OAuthAppState'; import OrganizePreviewAppState from './OrganizePreviewAppState'; import ProviderOptionsAppState from './ProviderOptionsAppState'; import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState'; import SettingsAppState from './SettingsAppState'; -export interface AppSectionState { - isUpdated: boolean; - isConnected: boolean; - isDisconnected: boolean; - isReconnecting: boolean; - isRestarting: boolean; - isSidebarVisible: boolean; - version: string; - prevVersion?: string; - dimensions: { - isSmallScreen: boolean; - isLargeScreen: boolean; - width: number; - height: number; - }; - messages: MessagesAppState; -} - interface AppState { - app: AppSectionState; blocklist: BlocklistAppState; captcha: CaptchaAppState; commands: CommandAppState; diff --git a/frontend/src/App/State/MessagesAppState.ts b/frontend/src/App/State/MessagesAppState.ts deleted file mode 100644 index 9f258ba4b..000000000 --- a/frontend/src/App/State/MessagesAppState.ts +++ /dev/null @@ -1,15 +0,0 @@ -import ModelBase from 'App/ModelBase'; -import AppSectionState from 'App/State/AppSectionState'; - -export type MessageType = 'error' | 'info' | 'success' | 'warning'; - -export interface Message extends ModelBase { - hideAfter: number; - message: string; - name: string; - type: MessageType; -} - -type MessagesAppState = AppSectionState; - -export default MessagesAppState; diff --git a/frontend/src/App/appStore.ts b/frontend/src/App/appStore.ts new file mode 100644 index 000000000..a0758c501 --- /dev/null +++ b/frontend/src/App/appStore.ts @@ -0,0 +1,202 @@ +import { create } from 'zustand'; +import { useShallow } from 'zustand/react/shallow'; +import getQueryPath from 'Utilities/Fetch/getQueryPath'; +import fetchJson from 'Utilities/requestAction'; + +function getDimensions(width: number, height: number) { + const dimensions = { + width, + height, + isExtraSmallScreen: width <= 480, + isSmallScreen: width <= 768, + isMediumScreen: width <= 992, + isLargeScreen: width <= 1200, + }; + + return dimensions; +} + +interface Dimensions { + width: number; + height: number; + isExtraSmallScreen: boolean; + isSmallScreen: boolean; + isMediumScreen: boolean; + isLargeScreen: boolean; +} + +interface AppState { + dimensions: Dimensions; + version: string; + prevVersion?: string; + isUpdated: boolean; + isConnected: boolean; + isReconnecting: boolean; + isDisconnected: boolean; + isRestarting: boolean; + isSidebarVisible: boolean; +} + +// Variables for ping functionality +let abortPingServer: (() => void) | null = null; +let pingTimeout: ReturnType | null = null; + +const useAppStore = create()(() => { + const dimensions = getDimensions(window.innerWidth, window.innerHeight); + + return { + dimensions, + version: window.Sonarr.version, + isUpdated: false, + isConnected: true, + isReconnecting: false, + isDisconnected: false, + isRestarting: false, + isSidebarVisible: !dimensions.isSmallScreen, + }; +}); + +export const useAppValues = (...keys: K[]) => { + return useAppStore( + useShallow((state) => { + return keys.reduce((acc, key) => { + acc[key] = state[key]; + return acc; + }, {} as Pick); + }) + ); +}; + +export const useAppValue = (key: K) => { + return useAppStore(useShallow((state) => state[key])); +}; + +export const useAppDimensions = () => { + return useAppStore(useShallow((state) => state.dimensions)); +}; + +export const useAppDimension = (key: K) => { + return useAppStore(useShallow((state) => state.dimensions[key])); +}; + +export const getAppDimensions = () => { + return useAppStore.getState().dimensions; +}; + +export const getAppValues = (...keys: K[]) => { + const state = useAppStore.getState(); + return keys.reduce((acc, key) => { + acc[key] = state[key]; + return acc; + }, {} as Pick); +}; + +export const getAppValue = (key: K) => { + return useAppStore.getState()[key]; +}; + +function pingServerAfterTimeout() { + if (abortPingServer) { + abortPingServer(); + abortPingServer = null; + } + + if (pingTimeout) { + clearTimeout(pingTimeout); + pingTimeout = null; + } + + pingTimeout = setTimeout(async () => { + const { isRestarting, isConnected } = getAppValues( + 'isRestarting', + 'isConnected' + ); + + if (!isRestarting && isConnected) { + return; + } + + const abortController = new AbortController(); + abortPingServer = () => abortController.abort(); + + try { + await fetchJson({ + url: getQueryPath('/system/status'), + method: 'GET', + signal: abortController.signal, + headers: { + 'X-Api-Key': window.Sonarr.apiKey, + 'X-Sonarr-Client': 'Sonarr', + }, + }); + + abortPingServer = null; + pingTimeout = null; + + setAppValue({ + isRestarting: false, + }); + } catch (error: unknown) { + abortPingServer = null; + pingTimeout = null; + + if ((error as { status?: number }).status === 401) { + setAppValue({ + isRestarting: false, + }); + } else if (!abortController.signal.aborted) { + pingServerAfterTimeout(); + } + } + }, 5000); +} + +export const saveDimensions = ({ + width, + height, +}: { + width: number; + height: number; +}) => { + const dimensions = getDimensions(width, height); + useAppStore.setState({ dimensions }); +}; + +export const setVersion = ({ version }: { version: string }) => { + useAppStore.setState((state) => { + const newState: Partial = { + version, + }; + + if (state.version !== version) { + if (!state.prevVersion) { + newState.prevVersion = state.version; + } + newState.isUpdated = true; + } + + return newState; + }); +}; + +export const setIsSidebarVisible = ({ + isSidebarVisible, +}: { + isSidebarVisible: boolean; +}) => { + useAppStore.setState({ isSidebarVisible }); +}; + +export const toggleIsSidebarVisible = () => { + useAppStore.setState((state) => ({ + isSidebarVisible: !state.isSidebarVisible, + })); +}; + +export const setAppValue = (payload: Partial) => { + useAppStore.setState(payload); +}; + +export const pingServer = () => { + pingServerAfterTimeout(); +}; diff --git a/frontend/src/App/messagesStore.ts b/frontend/src/App/messagesStore.ts new file mode 100644 index 000000000..16e2ed2b4 --- /dev/null +++ b/frontend/src/App/messagesStore.ts @@ -0,0 +1,56 @@ +import { create } from 'zustand'; +import { useShallow } from 'zustand/react/shallow'; +import ModelBase from './ModelBase'; + +export type MessageType = 'error' | 'info' | 'success' | 'warning'; + +export interface Message extends ModelBase { + hideAfter: number; + message: string; + name: string; + type: MessageType; +} + +interface MessagesState { + messages: Message[]; +} + +const useMessagesStore = create()(() => ({ + messages: [], +})); + +export const useMessages = () => { + return useMessagesStore(useShallow((state) => state.messages)); +}; + +export const getMessages = () => { + return useMessagesStore.getState().messages; +}; + +export const showMessage = (payload: Message) => { + useMessagesStore.setState((state) => { + const messages = [...state.messages]; + const index = messages.findIndex((item) => item.id === payload.id); + + if (index >= 0) { + const item = messages[index]; + messages.splice(index, 1, { ...item, ...payload }); + } else { + messages.push({ ...payload }); + } + + return { + messages, + }; + }); +}; + +export const hideMessage = ({ id }: { id: string | number }) => { + useMessagesStore.setState((state) => { + const messages = state.messages.filter((item) => item.id !== id); + + return { + messages, + }; + }); +}; diff --git a/frontend/src/Calendar/Day/CalendarDays.tsx b/frontend/src/Calendar/Day/CalendarDays.tsx index 75efa315b..cc200e3f5 100644 --- a/frontend/src/Calendar/Day/CalendarDays.tsx +++ b/frontend/src/Calendar/Day/CalendarDays.tsx @@ -1,8 +1,7 @@ import classNames from 'classnames'; import moment from 'moment'; import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; +import { useAppValue } from 'App/appStore'; import { useCalendarOption } from 'Calendar/calendarOptionsStore'; import * as calendarViews from 'Calendar/calendarViews'; import { @@ -14,12 +13,9 @@ import CalendarDay from './CalendarDay'; import styles from './CalendarDays.css'; function CalendarDays() { - const dispatch = useDispatch(); const view = useCalendarOption('view'); const dates = useCalendarDates(); - const isSidebarVisible = useSelector( - (state: AppState) => state.app.isSidebarVisible - ); + const isSidebarVisible = useAppValue('isSidebarVisible'); const updateTimeout = useRef>(); const touchStart = useRef(null); @@ -61,31 +57,28 @@ function CalendarDays() { [isSidebarVisible] ); - const handleTouchEnd = useCallback( - (event: TouchEvent) => { - const touches = event.changedTouches; - const currentTouch = touches[0].pageX; + const handleTouchEnd = useCallback((event: TouchEvent) => { + const touches = event.changedTouches; + const currentTouch = touches[0].pageX; - if (!touchStart.current) { - return; - } + if (!touchStart.current) { + return; + } - if ( - currentTouch > touchStart.current && - currentTouch - touchStart.current > 100 - ) { - dispatch(goToPreviousRange()); - } else if ( - currentTouch < touchStart.current && - touchStart.current - currentTouch > 100 - ) { - dispatch(goToNextRange()); - } + if ( + currentTouch > touchStart.current && + currentTouch - touchStart.current > 100 + ) { + goToPreviousRange(); + } else if ( + currentTouch < touchStart.current && + touchStart.current - currentTouch > 100 + ) { + goToNextRange(); + } - touchStart.current = null; - }, - [dispatch] - ); + touchStart.current = null; + }, []); const handleTouchCancel = useCallback(() => { touchStart.current = null; diff --git a/frontend/src/Calendar/Header/CalendarHeader.tsx b/frontend/src/Calendar/Header/CalendarHeader.tsx index d2e86bccb..82ae83f36 100644 --- a/frontend/src/Calendar/Header/CalendarHeader.tsx +++ b/frontend/src/Calendar/Header/CalendarHeader.tsx @@ -1,6 +1,7 @@ import moment from 'moment'; import React, { useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; +import { useAppDimensions } from 'App/appStore'; import { setCalendarOption, useCalendarOption, @@ -21,7 +22,6 @@ import MenuButton from 'Components/Menu/MenuButton'; import MenuContent from 'Components/Menu/MenuContent'; import ViewMenuItem from 'Components/Menu/ViewMenuItem'; import { align, icons } from 'Helpers/Props'; -import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import translate from 'Utilities/String/translate'; import CalendarHeaderViewButton from './CalendarHeaderViewButton'; @@ -33,9 +33,7 @@ function CalendarHeader() { const time = useCalendarTime(); const { start, end } = useCalendarRange(); - const { isSmallScreen, isLargeScreen } = useSelector( - createDimensionsSelector() - ); + const { isSmallScreen, isLargeScreen } = useAppDimensions(); const { longDateFormat } = useSelector(createUISettingsSelector()); diff --git a/frontend/src/Components/Page/Header/PageHeader.tsx b/frontend/src/Components/Page/Header/PageHeader.tsx index 1f96d3b44..c63447bbf 100644 --- a/frontend/src/Components/Page/Header/PageHeader.tsx +++ b/frontend/src/Components/Page/Header/PageHeader.tsx @@ -1,11 +1,9 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; +import { toggleIsSidebarVisible } from 'App/appStore'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts'; import { icons } from 'Helpers/Props'; -import { setIsSidebarVisible } from 'Store/Actions/appActions'; import translate from 'Utilities/String/translate'; import KeyboardShortcutsModal from './KeyboardShortcutsModal'; import PageHeaderActionsMenu from './PageHeaderActionsMenu'; @@ -13,18 +11,14 @@ import SeriesSearchInput from './SeriesSearchInput'; import styles from './PageHeader.css'; function PageHeader() { - const dispatch = useDispatch(); - - const { isSidebarVisible } = useSelector((state: AppState) => state.app); - const [isKeyboardShortcutsModalOpen, setIsKeyboardShortcutsModalOpen] = useState(false); const { bindShortcut, unbindShortcut } = useKeyboardShortcuts(); const handleSidebarToggle = useCallback(() => { - dispatch(setIsSidebarVisible({ isSidebarVisible: !isSidebarVisible })); - }, [isSidebarVisible, dispatch]); + toggleIsSidebarVisible(); + }, []); const handleOpenKeyboardShortcutsModal = useCallback(() => { setIsKeyboardShortcutsModalOpen(true); diff --git a/frontend/src/Components/Page/Page.tsx b/frontend/src/Components/Page/Page.tsx index f806b7bd1..b82d0dc1b 100644 --- a/frontend/src/Components/Page/Page.tsx +++ b/frontend/src/Components/Page/Page.tsx @@ -1,14 +1,12 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; +import { saveDimensions, useAppValue } from 'App/appStore'; import AppUpdatedModal from 'App/AppUpdatedModal'; import ColorImpairedContext from 'App/ColorImpairedContext'; import ConnectionLostModal from 'App/ConnectionLostModal'; -import AppState from 'App/State/AppState'; import SignalRListener from 'Components/SignalRListener'; import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal'; import useAppPage from 'Helpers/Hooks/useAppPage'; -import { saveDimensions } from 'Store/Actions/appActions'; -import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import { useSystemStatusData } from 'System/Status/useSystemStatus'; import ErrorPage from './ErrorPage'; @@ -22,7 +20,9 @@ interface PageProps { } function Page({ children }: PageProps) { - const dispatch = useDispatch(); + const isUpdated = useAppValue('isUpdated'); + const isDisconnected = useAppValue('isDisconnected'); + const version = useAppValue('version'); const { hasError, errors, isPopulated, isLocalStorageSupported } = useAppPage(); const [isUpdatedModalOpen, setIsUpdatedModalOpen] = useState(false); @@ -30,26 +30,20 @@ function Page({ children }: PageProps) { useState(false); const { enableColorImpairedMode } = useSelector(createUISettingsSelector()); - const { isSmallScreen } = useSelector(createDimensionsSelector()); const { authentication } = useSystemStatusData(); const authenticationEnabled = authentication !== 'none'; - const { isSidebarVisible, isUpdated, isDisconnected, version } = useSelector( - (state: AppState) => state.app - ); const handleUpdatedModalClose = useCallback(() => { setIsUpdatedModalOpen(false); }, []); const handleResize = useCallback(() => { - dispatch( - saveDimensions({ - width: window.innerWidth, - height: window.innerHeight, - }) - ); - }, [dispatch]); + saveDimensions({ + width: window.innerWidth, + height: window.innerHeight, + }); + }, []); useEffect(() => { window.addEventListener('resize', handleResize); @@ -93,10 +87,7 @@ function Page({ children }: PageProps) {
- + {children}
diff --git a/frontend/src/Components/Page/Sidebar/Messages/Message.tsx b/frontend/src/Components/Page/Sidebar/Messages/Message.tsx index 2de1613dd..c0c526b79 100644 --- a/frontend/src/Components/Page/Sidebar/Messages/Message.tsx +++ b/frontend/src/Components/Page/Sidebar/Messages/Message.tsx @@ -1,10 +1,8 @@ import classNames from 'classnames'; import React, { useEffect, useMemo, useRef } from 'react'; -import { useDispatch } from 'react-redux'; -import { MessageType } from 'App/State/MessagesAppState'; +import { hideMessage, MessageType } from 'App/messagesStore'; import Icon, { IconName } from 'Components/Icon'; import { icons } from 'Helpers/Props'; -import { hideMessage } from 'Store/Actions/appActions'; import styles from './Message.css'; interface MessageProps { @@ -16,7 +14,6 @@ interface MessageProps { } function Message({ id, hideAfter, name, message, type }: MessageProps) { - const dispatch = useDispatch(); const dismissTimeout = useRef>(); const icon: IconName = useMemo(() => { @@ -49,7 +46,7 @@ function Message({ id, hideAfter, name, message, type }: MessageProps) { useEffect(() => { if (hideAfter) { dismissTimeout.current = setTimeout(() => { - dispatch(hideMessage({ id })); + hideMessage({ id }); dismissTimeout.current = undefined; }, hideAfter * 1000); @@ -60,7 +57,7 @@ function Message({ id, hideAfter, name, message, type }: MessageProps) { clearTimeout(dismissTimeout.current); } }; - }, [id, hideAfter, message, type, dispatch]); + }, [id, hideAfter, message, type]); return (
diff --git a/frontend/src/Components/Page/Sidebar/Messages/Messages.tsx b/frontend/src/Components/Page/Sidebar/Messages/Messages.tsx index d6ea1057a..9f04f9f04 100644 --- a/frontend/src/Components/Page/Sidebar/Messages/Messages.tsx +++ b/frontend/src/Components/Page/Sidebar/Messages/Messages.tsx @@ -1,12 +1,10 @@ import React, { useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; -import { Message as MessageModel } from 'App/State/MessagesAppState'; +import { Message as MessageModel, useMessages } from 'App/messagesStore'; import Message from './Message'; import styles from './Messages.css'; function Messages() { - const items = useSelector((state: AppState) => state.app.messages.items); + const items = useMessages(); const messages = useMemo(() => { return items.reduce((acc, item) => { diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.tsx b/frontend/src/Components/Page/Sidebar/PageSidebar.tsx index 6e86dbbbb..e2f5460f9 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.tsx +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.tsx @@ -6,9 +6,13 @@ import React, { 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 { + setIsSidebarVisible, + useAppDimension, + useAppValue, +} from 'App/appStore'; import { IconName } from 'Components/Icon'; import IconButton from 'Components/Link/IconButton'; import Link from 'Components/Link/Link'; @@ -16,7 +20,6 @@ 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'; @@ -211,13 +214,9 @@ function hasActiveChildLink(link: SidebarItem, pathname: string) { }); } -interface PageSidebarProps { - isSmallScreen: boolean; - isSidebarVisible: boolean; -} - -function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) { - const dispatch = useDispatch(); +function PageSidebar() { + const isSidebarVisible = useAppValue('isSidebarVisible'); + const isSmallScreen = useAppDimension('isSmallScreen'); const location = useLocation(); const sidebarRef = useRef(null); const touchStartX = useRef(null); @@ -286,15 +285,15 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) { ) { event.preventDefault(); event.stopPropagation(); - dispatch(setIsSidebarVisible({ isSidebarVisible: false })); + setIsSidebarVisible({ isSidebarVisible: false }); } }, - [isSidebarVisible, dispatch] + [isSidebarVisible] ); const handleItemPress = useCallback(() => { - dispatch(setIsSidebarVisible({ isSidebarVisible: false })); - }, [dispatch]); + setIsSidebarVisible({ isSidebarVisible: false }); + }, []); const handleTouchStart = useCallback( (event: TouchEvent) => { @@ -378,8 +377,8 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) { }, []); const handleSidebarClosePress = useCallback(() => { - dispatch(setIsSidebarVisible({ isSidebarVisible: false })); - }, [dispatch]); + setIsSidebarVisible({ isSidebarVisible: false }); + }, []); useEffect(() => { if (isSmallScreen) { @@ -413,14 +412,14 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) { transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1, }); } else if (sidebarTransform.transform === 0 && !isSidebarVisible) { - dispatch(setIsSidebarVisible({ isSidebarVisible: true })); + setIsSidebarVisible({ isSidebarVisible: true }); } else if ( sidebarTransform.transform === -SIDEBAR_WIDTH && isSidebarVisible ) { - dispatch(setIsSidebarVisible({ isSidebarVisible: false })); + setIsSidebarVisible({ isSidebarVisible: false }); } - }, [sidebarTransform, isSidebarVisible, wasSidebarVisible, dispatch]); + }, [sidebarTransform, isSidebarVisible, wasSidebarVisible]); const containerStyle = useMemo(() => { if (!isSmallScreen) { diff --git a/frontend/src/Components/SignalRListener.tsx b/frontend/src/Components/SignalRListener.tsx index 8a2984746..6d427f21f 100644 --- a/frontend/src/Components/SignalRListener.tsx +++ b/frontend/src/Components/SignalRListener.tsx @@ -6,12 +6,12 @@ import { import { QueryKey, useQueryClient } from '@tanstack/react-query'; import { useEffect, useRef } from 'react'; import { useDispatch } from 'react-redux'; +import { setAppValue, setVersion } from 'App/appStore'; import ModelBase from 'App/ModelBase'; import Command from 'Commands/Command'; import Episode from 'Episode/Episode'; import { EpisodeFile } from 'EpisodeFile/EpisodeFile'; import { PagedQueryResponse } from 'Helpers/Hooks/usePagedApiQuery'; -import { setAppValue, setVersion } from 'Store/Actions/appActions'; import { removeItem, updateItem } from 'Store/Actions/baseActions'; import { fetchCommands, @@ -45,42 +45,36 @@ function SignalRListener() { console.error('[signalR] failed to connect'); console.error(error); - dispatch( - setAppValue({ - isConnected: false, - isReconnecting: false, - isDisconnected: false, - isRestarting: false, - }) - ); + setAppValue({ + isConnected: false, + isReconnecting: false, + isDisconnected: false, + isRestarting: false, + }); }); const handleStart = useRef(() => { console.debug('[signalR] connected'); - dispatch( - setAppValue({ - isConnected: true, - isReconnecting: false, - isDisconnected: false, - isRestarting: false, - }) - ); + setAppValue({ + isConnected: true, + isReconnecting: false, + isDisconnected: false, + isRestarting: false, + }); }); const handleReconnecting = useRef(() => { - dispatch(setAppValue({ isReconnecting: true })); + setAppValue({ isReconnecting: true }); }); const handleReconnected = useRef(() => { - dispatch( - setAppValue({ - isConnected: true, - isReconnecting: false, - isDisconnected: false, - isRestarting: false, - }) - ); + setAppValue({ + isConnected: true, + isReconnecting: false, + isDisconnected: false, + isRestarting: false, + }); // Repopulate the page (if a repopulator is set) to ensure things // are in sync after reconnecting. @@ -387,7 +381,7 @@ function SignalRListener() { } if (name === 'version') { - dispatch(setVersion({ version: body.version })); + setVersion({ version: body.version }); return; } @@ -435,7 +429,7 @@ function SignalRListener() { .withAutomaticReconnect({ nextRetryDelayInMilliseconds: (retryContext) => { if (retryContext.elapsedMilliseconds > 180000) { - dispatch(setAppValue({ isDisconnected: true })); + setAppValue({ isDisconnected: true }); } return Math.min(retryContext.previousRetryCount, 10) * 1000; }, diff --git a/frontend/src/Series/Details/SeriesDetailsSeason.tsx b/frontend/src/Series/Details/SeriesDetailsSeason.tsx index a94d74536..acf35ebac 100644 --- a/frontend/src/Series/Details/SeriesDetailsSeason.tsx +++ b/frontend/src/Series/Details/SeriesDetailsSeason.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; +import { useAppDimension } from 'App/appStore'; import * as commandNames from 'Commands/commandNames'; import Icon from 'Components/Icon'; import Label from 'Components/Label'; @@ -35,7 +36,6 @@ import { Statistics } from 'Series/Series'; import useSeries from 'Series/useSeries'; import { toggleSeasonMonitored } from 'Store/Actions/seriesActions'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; -import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import { TableOptionsChangePayload } from 'typings/Table'; import { findCommand, isCommandExecuting } from 'Utilities/Command'; import isAfter from 'Utilities/Date/isAfter'; @@ -123,7 +123,7 @@ function SeriesDetailsSeason({ const { columns, sortKey, sortDirection } = useEpisodeOptions(); - const { isSmallScreen } = useSelector(createDimensionsSelector()); + const isSmallScreen = useAppDimension('isSmallScreen'); const isSearching = useSelector( createIsSearchingSelector(seriesId, seasonNumber) ); diff --git a/frontend/src/Series/Index/SeriesIndex.tsx b/frontend/src/Series/Index/SeriesIndex.tsx index 7a4d9d49f..6f87cb8b1 100644 --- a/frontend/src/Series/Index/SeriesIndex.tsx +++ b/frontend/src/Series/Index/SeriesIndex.tsx @@ -7,6 +7,7 @@ import React, { } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider'; +import { useAppDimension } from 'App/appStore'; import { SelectProvider } from 'App/Select/SelectContext'; import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState'; import SeriesAppState, { SeriesIndexAppState } from 'App/State/SeriesAppState'; @@ -37,7 +38,6 @@ import { } from 'Store/Actions/seriesIndexActions'; import scrollPositions from 'Store/scrollPositions'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; -import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createSeriesClientSideCollectionItemsSelector from 'Store/Selectors/createSeriesClientSideCollectionItemsSelector'; import translate from 'Utilities/String/translate'; import SeriesIndexFilterMenu from './Menus/SeriesIndexFilterMenu'; @@ -95,7 +95,7 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => { const isRssSyncExecuting = useSelector( createCommandExecutingSelector(RSS_SYNC) ); - const { isSmallScreen } = useSelector(createDimensionsSelector()); + const isSmallScreen = useAppDimension('isSmallScreen'); const dispatch = useDispatch(); const scrollerRef = useRef(null); const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false); diff --git a/frontend/src/Store/Actions/appActions.js b/frontend/src/Store/Actions/appActions.js deleted file mode 100644 index 464b7b1a0..000000000 --- a/frontend/src/Store/Actions/appActions.js +++ /dev/null @@ -1,207 +0,0 @@ -import _ from 'lodash'; -import { createAction } from 'redux-actions'; -import { createThunk, handleThunks } from 'Store/thunks'; -import createAjaxRequest from 'Utilities/createAjaxRequest'; -import getSectionState from 'Utilities/State/getSectionState'; -import updateSectionState from 'Utilities/State/updateSectionState'; -import createHandleActions from './Creators/createHandleActions'; - -function getDimensions(width, height) { - const dimensions = { - width, - height, - isExtraSmallScreen: width <= 480, - isSmallScreen: width <= 768, - isMediumScreen: width <= 992, - isLargeScreen: width <= 1200 - }; - - return dimensions; -} - -// -// Variables - -export const section = 'app'; -const messagesSection = 'app.messages'; -let abortPingServer = null; -let pingTimeout = null; - -// -// State - -export const defaultState = { - dimensions: getDimensions(window.innerWidth, window.innerHeight), - messages: { - items: [] - }, - version: window.Sonarr.version, - isUpdated: false, - isConnected: true, - isReconnecting: false, - isDisconnected: false, - isRestarting: false, - isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen -}; - -// -// Action Types - -export const SHOW_MESSAGE = 'app/showMessage'; -export const HIDE_MESSAGE = 'app/hideMessage'; -export const SAVE_DIMENSIONS = 'app/saveDimensions'; -export const SET_VERSION = 'app/setVersion'; -export const SET_APP_VALUE = 'app/setAppValue'; -export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible'; - -export const PING_SERVER = 'app/pingServer'; - -// -// Action Creators - -export const saveDimensions = createAction(SAVE_DIMENSIONS); -export const setVersion = createAction(SET_VERSION); -export const setIsSidebarVisible = createAction(SET_IS_SIDEBAR_VISIBLE); -export const setAppValue = createAction(SET_APP_VALUE); -export const showMessage = createAction(SHOW_MESSAGE); -export const hideMessage = createAction(HIDE_MESSAGE); -export const pingServer = createThunk(PING_SERVER); - -// -// Helpers - -function pingServerAfterTimeout(getState, dispatch) { - if (abortPingServer) { - abortPingServer(); - abortPingServer = null; - } - - if (pingTimeout) { - clearTimeout(pingTimeout); - pingTimeout = null; - } - - pingTimeout = setTimeout(() => { - if (!getState().isRestarting && getState().isConnected) { - return; - } - - const ajaxOptions = { - url: '/system/status', - method: 'GET', - contentType: 'application/json' - }; - - const { request, abortRequest } = createAjaxRequest(ajaxOptions); - - abortPingServer = abortRequest; - - request.done(() => { - abortPingServer = null; - pingTimeout = null; - - dispatch(setAppValue({ - isRestarting: false - })); - }); - - request.fail((xhr) => { - abortPingServer = null; - pingTimeout = null; - - // Unauthorized, but back online - if (xhr.status === 401) { - dispatch(setAppValue({ - isRestarting: false - })); - } else { - pingServerAfterTimeout(getState, dispatch); - } - }); - }, 5000); -} - -// -// Action Handlers - -export const actionHandlers = handleThunks({ - [PING_SERVER]: function(getState, payload, dispatch) { - pingServerAfterTimeout(getState, dispatch); - } -}); - -// -// Reducers - -export const reducers = createHandleActions({ - - [SAVE_DIMENSIONS]: function(state, { payload }) { - const { - width, - height - } = payload; - - const dimensions = getDimensions(width, height); - - return Object.assign({}, state, { dimensions }); - }, - - [SHOW_MESSAGE]: function(state, { payload }) { - const newState = getSectionState(state, messagesSection); - const items = newState.items; - const index = _.findIndex(items, { id: payload.id }); - - newState.items = [...items]; - - if (index >= 0) { - const item = items[index]; - - newState.items.splice(index, 1, { ...item, ...payload }); - } else { - newState.items.push({ ...payload }); - } - - return updateSectionState(state, messagesSection, newState); - }, - - [HIDE_MESSAGE]: function(state, { payload }) { - const newState = getSectionState(state, messagesSection); - - newState.items = [...newState.items]; - _.remove(newState.items, { id: payload.id }); - - return updateSectionState(state, messagesSection, newState); - }, - - [SET_APP_VALUE]: function(state, { payload }) { - const newState = Object.assign(getSectionState(state, section), payload); - - return updateSectionState(state, section, newState); - }, - - [SET_VERSION]: function(state, { payload }) { - const version = payload.version; - - const newState = { - version - }; - - if (state.version !== version) { - if (!state.prevVersion) { - newState.prevVersion = state.version; - } - newState.isUpdated = true; - } - - return Object.assign({}, state, newState); - }, - - [SET_IS_SIDEBAR_VISIBLE]: function(state, { payload }) { - const newState = { - isSidebarVisible: payload.isSidebarVisible - }; - - return Object.assign({}, state, newState); - } - -}, defaultState, section); diff --git a/frontend/src/Store/Actions/commandActions.js b/frontend/src/Store/Actions/commandActions.js index 083bdd61f..efdd99d93 100644 --- a/frontend/src/Store/Actions/commandActions.js +++ b/frontend/src/Store/Actions/commandActions.js @@ -1,9 +1,9 @@ import { batchActions } from 'redux-batched-actions'; +import { hideMessage, showMessage } from 'App/messagesStore'; import { messageTypes } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import { isSameCommand } from 'Utilities/Command'; import createAjaxRequest from 'Utilities/createAjaxRequest'; -import { hideMessage, showMessage } from './appActions'; import { removeItem, updateItem } from './baseActions'; import createFetchHandler from './Creators/createFetchHandler'; import createHandleActions from './Creators/createHandleActions'; @@ -85,13 +85,13 @@ function showCommandMessage(payload, dispatch) { hideAfter = trigger === 'manual' ? 10 : 4; } - dispatch(showMessage({ + showMessage({ id, name, message, type, hideAfter - })); + }); } function scheduleRemoveCommand(command, dispatch) { @@ -112,10 +112,11 @@ function scheduleRemoveCommand(command, dispatch) { removeCommandTimeoutIds[id] = setTimeout(() => { dispatch(batchActions([ - removeCommand({ section: 'commands', id }), - hideMessage({ id }) + removeCommand({ section: 'commands', id }) ])); + hideMessage({ id }); + delete removeCommandTimeoutIds[id]; }, 60000 * 5); } diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js index 7029c5db0..848be367a 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -1,4 +1,3 @@ -import * as app from './appActions'; import * as captcha from './captchaActions'; import * as commands from './commandActions'; import * as episodeHistory from './episodeHistoryActions'; @@ -13,7 +12,6 @@ import * as seriesIndex from './seriesIndexActions'; import * as settings from './settingsActions'; export default [ - app, captcha, commands, episodeHistory, diff --git a/frontend/src/Store/Selectors/createDimensionsSelector.ts b/frontend/src/Store/Selectors/createDimensionsSelector.ts index b9602cb02..21beb2ed5 100644 --- a/frontend/src/Store/Selectors/createDimensionsSelector.ts +++ b/frontend/src/Store/Selectors/createDimensionsSelector.ts @@ -1,13 +1 @@ -import { createSelector } from 'reselect'; -import AppState from 'App/State/AppState'; - -function createDimensionsSelector() { - return createSelector( - (state: AppState) => state.app.dimensions, - (dimensions) => { - return dimensions; - } - ); -} - -export default createDimensionsSelector; +// This file has been removed - use useAppDimensions or useAppDimension from App/appStore instead diff --git a/frontend/src/System/Backup/RestoreBackupModalContent.tsx b/frontend/src/System/Backup/RestoreBackupModalContent.tsx index 94abd3835..40f212692 100644 --- a/frontend/src/System/Backup/RestoreBackupModalContent.tsx +++ b/frontend/src/System/Backup/RestoreBackupModalContent.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; +import { useAppValue } from 'App/appStore'; import { Error } from 'App/State/AppSectionState'; -import AppState from 'App/State/AppState'; import TextInput from 'Components/Form/TextInput'; import Icon, { IconName, IconProps } from 'Components/Icon'; import Button from 'Components/Link/Button'; @@ -79,7 +79,7 @@ function RestoreBackupModalContent({ name, onModalClose, }: RestoreBackupModalContentProps) { - const { isRestarting } = useSelector((state: AppState) => state.app); + const isRestarting = useAppValue('isRestarting'); const dispatch = useDispatch(); const { restoreBackupById, isRestoringBackup, restoreBackupError } = diff --git a/frontend/src/System/Status/Health/HealthStatus.tsx b/frontend/src/System/Status/Health/HealthStatus.tsx index 34f2cc56e..326d9525b 100644 --- a/frontend/src/System/Status/Health/HealthStatus.tsx +++ b/frontend/src/System/Status/Health/HealthStatus.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; +import { useAppValues } from 'App/appStore'; import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; import usePrevious from 'Helpers/Hooks/usePrevious'; import useHealth from './useHealth'; function HealthStatus() { - const { isConnected, isReconnecting } = useSelector( - (state: AppState) => state.app + const { isConnected, isReconnecting } = useAppValues( + 'isConnected', + 'isReconnecting' ); const { data, refetch } = useHealth(); diff --git a/frontend/src/System/Updates/Updates.tsx b/frontend/src/System/Updates/Updates.tsx index 18a90cf87..f3975e6de 100644 --- a/frontend/src/System/Updates/Updates.tsx +++ b/frontend/src/System/Updates/Updates.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import AppState from 'App/State/AppState'; +import { useAppValue } from 'App/appStore'; import * as commandNames from 'Commands/commandNames'; import Alert from 'Components/Alert'; import Icon from 'Components/Icon'; @@ -29,7 +29,7 @@ import styles from './Updates.css'; const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i; function Updates() { - const currentVersion = useSelector((state: AppState) => state.app.version); + const currentVersion = useAppValue('version'); const { packageUpdateMechanismMessage } = useSystemStatusData(); const { shortDateFormat, longDateFormat, timeFormat } = useSelector( diff --git a/frontend/src/System/useSystem.ts b/frontend/src/System/useSystem.ts index 14c357bb5..3e3a3229d 100644 --- a/frontend/src/System/useSystem.ts +++ b/frontend/src/System/useSystem.ts @@ -1,40 +1,30 @@ -import { useMutation } from '@tanstack/react-query'; -import { useDispatch } from 'react-redux'; -import { pingServer, setAppValue } from 'Store/Actions/appActions'; +import { pingServer, setAppValue } from 'App/appStore'; +import useApiMutation from 'Helpers/Hooks/useApiMutation'; -const createSystemMutationFn = (endpoint: string) => { - return async () => { - const response = await fetch( - `${window.Sonarr.urlBase}/system/${endpoint}`, - { - method: 'POST', - headers: { - 'X-Api-Key': window.Sonarr.apiKey, - 'X-Sonarr-Client': 'Sonarr', - }, - } - ); +export const useRestart = () => { + const mutation = useApiMutation({ + method: 'POST', + path: '/system/restart', + }); - if (!response.ok) { - throw new Error(`Failed to ${endpoint}: ${response.statusText}`); - } + const restart = () => { + mutation.mutate(undefined, { + onSuccess: () => { + setAppValue({ isRestarting: true }); + pingServer(); + }, + }); + }; + + return { + ...mutation, + mutate: restart, }; }; -export const useRestart = () => { - const dispatch = useDispatch(); - - return useMutation({ - mutationFn: createSystemMutationFn('restart'), - onSuccess: () => { - dispatch(setAppValue({ isRestarting: true })); - dispatch(pingServer()); - }, - }); -}; - export const useShutdown = () => { - return useMutation({ - mutationFn: createSystemMutationFn('shutdown'), + return useApiMutation({ + method: 'POST', + path: '/system/shutdown', }); };