mirror of
https://github.com/Sonarr/Sonarr
synced 2025-12-06 08:28:37 +01:00
Convert app state to zustand stores
This commit is contained in:
parent
9b0dc0dd4a
commit
c62a7c310e
26 changed files with 389 additions and 434 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Message>;
|
||||
|
||||
export default MessagesAppState;
|
||||
202
frontend/src/App/appStore.ts
Normal file
202
frontend/src/App/appStore.ts
Normal file
|
|
@ -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<typeof setTimeout> | null = null;
|
||||
|
||||
const useAppStore = create<AppState>()(() => {
|
||||
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 = <K extends keyof AppState>(...keys: K[]) => {
|
||||
return useAppStore(
|
||||
useShallow((state) => {
|
||||
return keys.reduce((acc, key) => {
|
||||
acc[key] = state[key];
|
||||
return acc;
|
||||
}, {} as Pick<AppState, K>);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const useAppValue = <K extends keyof AppState>(key: K) => {
|
||||
return useAppStore(useShallow((state) => state[key]));
|
||||
};
|
||||
|
||||
export const useAppDimensions = () => {
|
||||
return useAppStore(useShallow((state) => state.dimensions));
|
||||
};
|
||||
|
||||
export const useAppDimension = <K extends keyof Dimensions>(key: K) => {
|
||||
return useAppStore(useShallow((state) => state.dimensions[key]));
|
||||
};
|
||||
|
||||
export const getAppDimensions = () => {
|
||||
return useAppStore.getState().dimensions;
|
||||
};
|
||||
|
||||
export const getAppValues = <K extends keyof AppState>(...keys: K[]) => {
|
||||
const state = useAppStore.getState();
|
||||
return keys.reduce((acc, key) => {
|
||||
acc[key] = state[key];
|
||||
return acc;
|
||||
}, {} as Pick<AppState, K>);
|
||||
};
|
||||
|
||||
export const getAppValue = <K extends keyof AppState>(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<AppState> = {
|
||||
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<AppState>) => {
|
||||
useAppStore.setState(payload);
|
||||
};
|
||||
|
||||
export const pingServer = () => {
|
||||
pingServerAfterTimeout();
|
||||
};
|
||||
56
frontend/src/App/messagesStore.ts
Normal file
56
frontend/src/App/messagesStore.ts
Normal file
|
|
@ -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<MessagesState>()(() => ({
|
||||
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,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
@ -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<ReturnType<typeof setTimeout>>();
|
||||
const touchStart = useRef<number | null>(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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<PageHeader />
|
||||
|
||||
<div className={styles.main}>
|
||||
<PageSidebar
|
||||
isSmallScreen={isSmallScreen}
|
||||
isSidebarVisible={isSidebarVisible}
|
||||
/>
|
||||
<PageSidebar />
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<ReturnType<typeof setTimeout>>();
|
||||
|
||||
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 (
|
||||
<div className={classNames(styles.message, styles[type])}>
|
||||
|
|
|
|||
|
|
@ -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<MessageModel[]>((acc, item) => {
|
||||
|
|
|
|||
|
|
@ -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<number | null>(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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(null);
|
||||
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 } =
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<void, void>({
|
||||
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<void, Error, void>({
|
||||
mutationFn: createSystemMutationFn('restart'),
|
||||
onSuccess: () => {
|
||||
dispatch(setAppValue({ isRestarting: true }));
|
||||
dispatch(pingServer());
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useShutdown = () => {
|
||||
return useMutation<void, Error, void>({
|
||||
mutationFn: createSystemMutationFn('shutdown'),
|
||||
return useApiMutation<void, void>({
|
||||
method: 'POST',
|
||||
path: '/system/shutdown',
|
||||
});
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue