mirror of
https://github.com/Sonarr/Sonarr
synced 2025-12-09 18:02:39 +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 React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import AddSeries from 'AddSeries/AddSeries';
|
import AddSeries from 'AddSeries/AddSeries';
|
||||||
import {
|
import {
|
||||||
AddSeriesOptions,
|
AddSeriesOptions,
|
||||||
|
|
@ -8,6 +7,7 @@ import {
|
||||||
} from 'AddSeries/addSeriesOptionsStore';
|
} from 'AddSeries/addSeriesOptionsStore';
|
||||||
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
||||||
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
||||||
|
import { useAppDimension } from 'App/appStore';
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
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 { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import { SeriesType } from 'Series/Series';
|
import { SeriesType } from 'Series/Series';
|
||||||
import SeriesPoster from 'Series/SeriesPoster';
|
import SeriesPoster from 'Series/SeriesPoster';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
import { useIsWindows } from 'System/Status/useSystemStatus';
|
import { useIsWindows } from 'System/Status/useSystemStatus';
|
||||||
import { InputChanged } from 'typings/inputs';
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
|
@ -45,7 +44,7 @@ function AddNewSeriesModalContent({
|
||||||
}: AddNewSeriesModalContentProps) {
|
}: AddNewSeriesModalContentProps) {
|
||||||
const { title, year, overview, images, folder } = series;
|
const { title, year, overview, images, folder } = series;
|
||||||
const options = useAddSeriesOptions();
|
const options = useAddSeriesOptions();
|
||||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
const isSmallScreen = useAppDimension('isSmallScreen');
|
||||||
const isWindows = useIsWindows();
|
const isWindows = useIsWindows();
|
||||||
|
|
||||||
const { isAdding, addError, addSeries } = useAddSeries();
|
const { isAdding, addError, addSeries } = useAddSeries();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import AddSeries from 'AddSeries/AddSeries';
|
import AddSeries from 'AddSeries/AddSeries';
|
||||||
|
import { useAppDimension } from 'App/appStore';
|
||||||
import HeartRating from 'Components/HeartRating';
|
import HeartRating from 'Components/HeartRating';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
|
|
@ -10,7 +11,6 @@ import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
import { Statistics } from 'Series/Series';
|
import { Statistics } from 'Series/Series';
|
||||||
import SeriesGenres from 'Series/SeriesGenres';
|
import SeriesGenres from 'Series/SeriesGenres';
|
||||||
import SeriesPoster from 'Series/SeriesPoster';
|
import SeriesPoster from 'Series/SeriesPoster';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import createExistingSeriesSelector from 'Store/Selectors/createExistingSeriesSelector';
|
import createExistingSeriesSelector from 'Store/Selectors/createExistingSeriesSelector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AddNewSeriesModal from './AddNewSeriesModal';
|
import AddNewSeriesModal from './AddNewSeriesModal';
|
||||||
|
|
@ -38,7 +38,7 @@ function AddNewSeriesSearchResult({ series }: AddNewSeriesSearchResultProps) {
|
||||||
} = series;
|
} = series;
|
||||||
|
|
||||||
const isExistingSeries = useSelector(createExistingSeriesSelector(tvdbId));
|
const isExistingSeries = useSelector(createExistingSeriesSelector(tvdbId));
|
||||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
const isSmallScreen = useAppDimension('isSmallScreen');
|
||||||
const [isNewAddSeriesModalOpen, setIsNewAddSeriesModalOpen] = useState(false);
|
const [isNewAddSeriesModalOpen, setIsNewAddSeriesModalOpen] = useState(false);
|
||||||
|
|
||||||
const seasonCount = statistics.seasonCount;
|
const seasonCount = statistics.seasonCount;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React, { RefObject, useCallback, useEffect, useRef } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { FixedSizeList, ListChildComponentProps } from 'react-window';
|
import { FixedSizeList, ListChildComponentProps } from 'react-window';
|
||||||
import { useAddSeriesOptions } from 'AddSeries/addSeriesOptionsStore';
|
import { useAddSeriesOptions } from 'AddSeries/addSeriesOptionsStore';
|
||||||
|
import { useAppDimension } from 'App/appStore';
|
||||||
import { useSelect } from 'App/Select/SelectContext';
|
import { useSelect } from 'App/Select/SelectContext';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import { ImportSeries } from 'App/State/ImportSeriesAppState';
|
import { ImportSeries } from 'App/State/ImportSeriesAppState';
|
||||||
|
|
@ -12,7 +13,6 @@ import {
|
||||||
setImportSeriesValue,
|
setImportSeriesValue,
|
||||||
} from 'Store/Actions/importSeriesActions';
|
} from 'Store/Actions/importSeriesActions';
|
||||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
import { UnmappedFolder } from 'typings/RootFolder';
|
import { UnmappedFolder } from 'typings/RootFolder';
|
||||||
|
|
@ -64,7 +64,7 @@ function ImportSeriesTable({
|
||||||
useAddSeriesOptions();
|
useAddSeriesOptions();
|
||||||
|
|
||||||
const items = useSelector((state: AppState) => state.importSeries.items);
|
const items = useSelector((state: AppState) => state.importSeries.items);
|
||||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
const isSmallScreen = useAppDimension('isSmallScreen');
|
||||||
const allSeries = useSelector(createAllSeriesSelector());
|
const allSeries = useSelector(createAllSeriesSelector());
|
||||||
const {
|
const {
|
||||||
allSelected,
|
allSelected,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
|
|
@ -13,7 +12,7 @@ import UpdateChanges from 'System/Updates/UpdateChanges';
|
||||||
import useUpdates from 'System/Updates/useUpdates';
|
import useUpdates from 'System/Updates/useUpdates';
|
||||||
import Update from 'typings/Update';
|
import Update from 'typings/Update';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import AppState from './State/AppState';
|
import { useAppValues } from './appStore';
|
||||||
import styles from './AppUpdatedModalContent.css';
|
import styles from './AppUpdatedModalContent.css';
|
||||||
|
|
||||||
function mergeUpdates(items: Update[], version: string, prevVersion?: string) {
|
function mergeUpdates(items: Update[], version: string, prevVersion?: string) {
|
||||||
|
|
@ -63,7 +62,7 @@ interface AppUpdatedModalContentProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppUpdatedModalContent(props: 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 { isFetched, error, data, refetch } = useUpdates();
|
||||||
const previousVersion = usePrevious(version);
|
const previousVersion = usePrevious(version);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,33 +4,13 @@ import CommandAppState from './CommandAppState';
|
||||||
import HistoryAppState, { SeriesHistoryAppState } from './HistoryAppState';
|
import HistoryAppState, { SeriesHistoryAppState } from './HistoryAppState';
|
||||||
import ImportSeriesAppState from './ImportSeriesAppState';
|
import ImportSeriesAppState from './ImportSeriesAppState';
|
||||||
import InteractiveImportAppState from './InteractiveImportAppState';
|
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||||
import MessagesAppState from './MessagesAppState';
|
|
||||||
import OAuthAppState from './OAuthAppState';
|
import OAuthAppState from './OAuthAppState';
|
||||||
import OrganizePreviewAppState from './OrganizePreviewAppState';
|
import OrganizePreviewAppState from './OrganizePreviewAppState';
|
||||||
import ProviderOptionsAppState from './ProviderOptionsAppState';
|
import ProviderOptionsAppState from './ProviderOptionsAppState';
|
||||||
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
|
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
|
||||||
import SettingsAppState from './SettingsAppState';
|
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 {
|
interface AppState {
|
||||||
app: AppSectionState;
|
|
||||||
blocklist: BlocklistAppState;
|
blocklist: BlocklistAppState;
|
||||||
captcha: CaptchaAppState;
|
captcha: CaptchaAppState;
|
||||||
commands: CommandAppState;
|
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 classNames from 'classnames';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useAppValue } from 'App/appStore';
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import { useCalendarOption } from 'Calendar/calendarOptionsStore';
|
import { useCalendarOption } from 'Calendar/calendarOptionsStore';
|
||||||
import * as calendarViews from 'Calendar/calendarViews';
|
import * as calendarViews from 'Calendar/calendarViews';
|
||||||
import {
|
import {
|
||||||
|
|
@ -14,12 +13,9 @@ import CalendarDay from './CalendarDay';
|
||||||
import styles from './CalendarDays.css';
|
import styles from './CalendarDays.css';
|
||||||
|
|
||||||
function CalendarDays() {
|
function CalendarDays() {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const view = useCalendarOption('view');
|
const view = useCalendarOption('view');
|
||||||
const dates = useCalendarDates();
|
const dates = useCalendarDates();
|
||||||
const isSidebarVisible = useSelector(
|
const isSidebarVisible = useAppValue('isSidebarVisible');
|
||||||
(state: AppState) => state.app.isSidebarVisible
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateTimeout = useRef<ReturnType<typeof setTimeout>>();
|
const updateTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
const touchStart = useRef<number | null>(null);
|
const touchStart = useRef<number | null>(null);
|
||||||
|
|
@ -61,8 +57,7 @@ function CalendarDays() {
|
||||||
[isSidebarVisible]
|
[isSidebarVisible]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTouchEnd = useCallback(
|
const handleTouchEnd = useCallback((event: TouchEvent) => {
|
||||||
(event: TouchEvent) => {
|
|
||||||
const touches = event.changedTouches;
|
const touches = event.changedTouches;
|
||||||
const currentTouch = touches[0].pageX;
|
const currentTouch = touches[0].pageX;
|
||||||
|
|
||||||
|
|
@ -74,18 +69,16 @@ function CalendarDays() {
|
||||||
currentTouch > touchStart.current &&
|
currentTouch > touchStart.current &&
|
||||||
currentTouch - touchStart.current > 100
|
currentTouch - touchStart.current > 100
|
||||||
) {
|
) {
|
||||||
dispatch(goToPreviousRange());
|
goToPreviousRange();
|
||||||
} else if (
|
} else if (
|
||||||
currentTouch < touchStart.current &&
|
currentTouch < touchStart.current &&
|
||||||
touchStart.current - currentTouch > 100
|
touchStart.current - currentTouch > 100
|
||||||
) {
|
) {
|
||||||
dispatch(goToNextRange());
|
goToNextRange();
|
||||||
}
|
}
|
||||||
|
|
||||||
touchStart.current = null;
|
touchStart.current = null;
|
||||||
},
|
}, []);
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTouchCancel = useCallback(() => {
|
const handleTouchCancel = useCallback(() => {
|
||||||
touchStart.current = null;
|
touchStart.current = null;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import { useAppDimensions } from 'App/appStore';
|
||||||
import {
|
import {
|
||||||
setCalendarOption,
|
setCalendarOption,
|
||||||
useCalendarOption,
|
useCalendarOption,
|
||||||
|
|
@ -21,7 +22,6 @@ import MenuButton from 'Components/Menu/MenuButton';
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
|
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
|
||||||
import { align, icons } from 'Helpers/Props';
|
import { align, icons } from 'Helpers/Props';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import CalendarHeaderViewButton from './CalendarHeaderViewButton';
|
import CalendarHeaderViewButton from './CalendarHeaderViewButton';
|
||||||
|
|
@ -33,9 +33,7 @@ function CalendarHeader() {
|
||||||
const time = useCalendarTime();
|
const time = useCalendarTime();
|
||||||
const { start, end } = useCalendarRange();
|
const { start, end } = useCalendarRange();
|
||||||
|
|
||||||
const { isSmallScreen, isLargeScreen } = useSelector(
|
const { isSmallScreen, isLargeScreen } = useAppDimensions();
|
||||||
createDimensionsSelector()
|
|
||||||
);
|
|
||||||
|
|
||||||
const { longDateFormat } = useSelector(createUISettingsSelector());
|
const { longDateFormat } = useSelector(createUISettingsSelector());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { toggleIsSidebarVisible } from 'App/appStore';
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
|
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import { setIsSidebarVisible } from 'Store/Actions/appActions';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||||
|
|
@ -13,18 +11,14 @@ import SeriesSearchInput from './SeriesSearchInput';
|
||||||
import styles from './PageHeader.css';
|
import styles from './PageHeader.css';
|
||||||
|
|
||||||
function PageHeader() {
|
function PageHeader() {
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const { isSidebarVisible } = useSelector((state: AppState) => state.app);
|
|
||||||
|
|
||||||
const [isKeyboardShortcutsModalOpen, setIsKeyboardShortcutsModalOpen] =
|
const [isKeyboardShortcutsModalOpen, setIsKeyboardShortcutsModalOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
const { bindShortcut, unbindShortcut } = useKeyboardShortcuts();
|
const { bindShortcut, unbindShortcut } = useKeyboardShortcuts();
|
||||||
|
|
||||||
const handleSidebarToggle = useCallback(() => {
|
const handleSidebarToggle = useCallback(() => {
|
||||||
dispatch(setIsSidebarVisible({ isSidebarVisible: !isSidebarVisible }));
|
toggleIsSidebarVisible();
|
||||||
}, [isSidebarVisible, dispatch]);
|
}, []);
|
||||||
|
|
||||||
const handleOpenKeyboardShortcutsModal = useCallback(() => {
|
const handleOpenKeyboardShortcutsModal = useCallback(() => {
|
||||||
setIsKeyboardShortcutsModalOpen(true);
|
setIsKeyboardShortcutsModalOpen(true);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
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 AppUpdatedModal from 'App/AppUpdatedModal';
|
||||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||||
import ConnectionLostModal from 'App/ConnectionLostModal';
|
import ConnectionLostModal from 'App/ConnectionLostModal';
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import SignalRListener from 'Components/SignalRListener';
|
import SignalRListener from 'Components/SignalRListener';
|
||||||
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||||
import useAppPage from 'Helpers/Hooks/useAppPage';
|
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 createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import { useSystemStatusData } from 'System/Status/useSystemStatus';
|
import { useSystemStatusData } from 'System/Status/useSystemStatus';
|
||||||
import ErrorPage from './ErrorPage';
|
import ErrorPage from './ErrorPage';
|
||||||
|
|
@ -22,7 +20,9 @@ interface PageProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Page({ children }: 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 } =
|
const { hasError, errors, isPopulated, isLocalStorageSupported } =
|
||||||
useAppPage();
|
useAppPage();
|
||||||
const [isUpdatedModalOpen, setIsUpdatedModalOpen] = useState(false);
|
const [isUpdatedModalOpen, setIsUpdatedModalOpen] = useState(false);
|
||||||
|
|
@ -30,26 +30,20 @@ function Page({ children }: PageProps) {
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
const { enableColorImpairedMode } = useSelector(createUISettingsSelector());
|
const { enableColorImpairedMode } = useSelector(createUISettingsSelector());
|
||||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
|
||||||
const { authentication } = useSystemStatusData();
|
const { authentication } = useSystemStatusData();
|
||||||
|
|
||||||
const authenticationEnabled = authentication !== 'none';
|
const authenticationEnabled = authentication !== 'none';
|
||||||
const { isSidebarVisible, isUpdated, isDisconnected, version } = useSelector(
|
|
||||||
(state: AppState) => state.app
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpdatedModalClose = useCallback(() => {
|
const handleUpdatedModalClose = useCallback(() => {
|
||||||
setIsUpdatedModalOpen(false);
|
setIsUpdatedModalOpen(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleResize = useCallback(() => {
|
const handleResize = useCallback(() => {
|
||||||
dispatch(
|
|
||||||
saveDimensions({
|
saveDimensions({
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
})
|
});
|
||||||
);
|
}, []);
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
@ -93,10 +87,7 @@ function Page({ children }: PageProps) {
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
|
|
||||||
<div className={styles.main}>
|
<div className={styles.main}>
|
||||||
<PageSidebar
|
<PageSidebar />
|
||||||
isSmallScreen={isSmallScreen}
|
|
||||||
isSidebarVisible={isSidebarVisible}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useEffect, useMemo, useRef } from 'react';
|
import React, { useEffect, useMemo, useRef } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { hideMessage, MessageType } from 'App/messagesStore';
|
||||||
import { MessageType } from 'App/State/MessagesAppState';
|
|
||||||
import Icon, { IconName } from 'Components/Icon';
|
import Icon, { IconName } from 'Components/Icon';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import { hideMessage } from 'Store/Actions/appActions';
|
|
||||||
import styles from './Message.css';
|
import styles from './Message.css';
|
||||||
|
|
||||||
interface MessageProps {
|
interface MessageProps {
|
||||||
|
|
@ -16,7 +14,6 @@ interface MessageProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Message({ id, hideAfter, name, message, type }: MessageProps) {
|
function Message({ id, hideAfter, name, message, type }: MessageProps) {
|
||||||
const dispatch = useDispatch();
|
|
||||||
const dismissTimeout = useRef<ReturnType<typeof setTimeout>>();
|
const dismissTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
const icon: IconName = useMemo(() => {
|
const icon: IconName = useMemo(() => {
|
||||||
|
|
@ -49,7 +46,7 @@ function Message({ id, hideAfter, name, message, type }: MessageProps) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hideAfter) {
|
if (hideAfter) {
|
||||||
dismissTimeout.current = setTimeout(() => {
|
dismissTimeout.current = setTimeout(() => {
|
||||||
dispatch(hideMessage({ id }));
|
hideMessage({ id });
|
||||||
|
|
||||||
dismissTimeout.current = undefined;
|
dismissTimeout.current = undefined;
|
||||||
}, hideAfter * 1000);
|
}, hideAfter * 1000);
|
||||||
|
|
@ -60,7 +57,7 @@ function Message({ id, hideAfter, name, message, type }: MessageProps) {
|
||||||
clearTimeout(dismissTimeout.current);
|
clearTimeout(dismissTimeout.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [id, hideAfter, message, type, dispatch]);
|
}, [id, hideAfter, message, type]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.message, styles[type])}>
|
<div className={classNames(styles.message, styles[type])}>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { Message as MessageModel, useMessages } from 'App/messagesStore';
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import { Message as MessageModel } from 'App/State/MessagesAppState';
|
|
||||||
import Message from './Message';
|
import Message from './Message';
|
||||||
import styles from './Messages.css';
|
import styles from './Messages.css';
|
||||||
|
|
||||||
function Messages() {
|
function Messages() {
|
||||||
const items = useSelector((state: AppState) => state.app.messages.items);
|
const items = useMessages();
|
||||||
|
|
||||||
const messages = useMemo(() => {
|
const messages = useMemo(() => {
|
||||||
return items.reduce<MessageModel[]>((acc, item) => {
|
return items.reduce<MessageModel[]>((acc, item) => {
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,13 @@ import React, {
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
import QueueStatus from 'Activity/Queue/Status/QueueStatus';
|
import QueueStatus from 'Activity/Queue/Status/QueueStatus';
|
||||||
|
import {
|
||||||
|
setIsSidebarVisible,
|
||||||
|
useAppDimension,
|
||||||
|
useAppValue,
|
||||||
|
} from 'App/appStore';
|
||||||
import { IconName } from 'Components/Icon';
|
import { IconName } from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
|
@ -16,7 +20,6 @@ import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import { setIsSidebarVisible } from 'Store/Actions/appActions';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import HealthStatus from 'System/Status/Health/HealthStatus';
|
import HealthStatus from 'System/Status/Health/HealthStatus';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
@ -211,13 +214,9 @@ function hasActiveChildLink(link: SidebarItem, pathname: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageSidebarProps {
|
function PageSidebar() {
|
||||||
isSmallScreen: boolean;
|
const isSidebarVisible = useAppValue('isSidebarVisible');
|
||||||
isSidebarVisible: boolean;
|
const isSmallScreen = useAppDimension('isSmallScreen');
|
||||||
}
|
|
||||||
|
|
||||||
function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const sidebarRef = useRef(null);
|
const sidebarRef = useRef(null);
|
||||||
const touchStartX = useRef<number | null>(null);
|
const touchStartX = useRef<number | null>(null);
|
||||||
|
|
@ -286,15 +285,15 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) {
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
dispatch(setIsSidebarVisible({ isSidebarVisible: false }));
|
setIsSidebarVisible({ isSidebarVisible: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isSidebarVisible, dispatch]
|
[isSidebarVisible]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleItemPress = useCallback(() => {
|
const handleItemPress = useCallback(() => {
|
||||||
dispatch(setIsSidebarVisible({ isSidebarVisible: false }));
|
setIsSidebarVisible({ isSidebarVisible: false });
|
||||||
}, [dispatch]);
|
}, []);
|
||||||
|
|
||||||
const handleTouchStart = useCallback(
|
const handleTouchStart = useCallback(
|
||||||
(event: TouchEvent) => {
|
(event: TouchEvent) => {
|
||||||
|
|
@ -378,8 +377,8 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSidebarClosePress = useCallback(() => {
|
const handleSidebarClosePress = useCallback(() => {
|
||||||
dispatch(setIsSidebarVisible({ isSidebarVisible: false }));
|
setIsSidebarVisible({ isSidebarVisible: false });
|
||||||
}, [dispatch]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSmallScreen) {
|
if (isSmallScreen) {
|
||||||
|
|
@ -413,14 +412,14 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) {
|
||||||
transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1,
|
transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1,
|
||||||
});
|
});
|
||||||
} else if (sidebarTransform.transform === 0 && !isSidebarVisible) {
|
} else if (sidebarTransform.transform === 0 && !isSidebarVisible) {
|
||||||
dispatch(setIsSidebarVisible({ isSidebarVisible: true }));
|
setIsSidebarVisible({ isSidebarVisible: true });
|
||||||
} else if (
|
} else if (
|
||||||
sidebarTransform.transform === -SIDEBAR_WIDTH &&
|
sidebarTransform.transform === -SIDEBAR_WIDTH &&
|
||||||
isSidebarVisible
|
isSidebarVisible
|
||||||
) {
|
) {
|
||||||
dispatch(setIsSidebarVisible({ isSidebarVisible: false }));
|
setIsSidebarVisible({ isSidebarVisible: false });
|
||||||
}
|
}
|
||||||
}, [sidebarTransform, isSidebarVisible, wasSidebarVisible, dispatch]);
|
}, [sidebarTransform, isSidebarVisible, wasSidebarVisible]);
|
||||||
|
|
||||||
const containerStyle = useMemo(() => {
|
const containerStyle = useMemo(() => {
|
||||||
if (!isSmallScreen) {
|
if (!isSmallScreen) {
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import {
|
||||||
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { setAppValue, setVersion } from 'App/appStore';
|
||||||
import ModelBase from 'App/ModelBase';
|
import ModelBase from 'App/ModelBase';
|
||||||
import Command from 'Commands/Command';
|
import Command from 'Commands/Command';
|
||||||
import Episode from 'Episode/Episode';
|
import Episode from 'Episode/Episode';
|
||||||
import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
|
import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
|
||||||
import { PagedQueryResponse } from 'Helpers/Hooks/usePagedApiQuery';
|
import { PagedQueryResponse } from 'Helpers/Hooks/usePagedApiQuery';
|
||||||
import { setAppValue, setVersion } from 'Store/Actions/appActions';
|
|
||||||
import { removeItem, updateItem } from 'Store/Actions/baseActions';
|
import { removeItem, updateItem } from 'Store/Actions/baseActions';
|
||||||
import {
|
import {
|
||||||
fetchCommands,
|
fetchCommands,
|
||||||
|
|
@ -45,42 +45,36 @@ function SignalRListener() {
|
||||||
console.error('[signalR] failed to connect');
|
console.error('[signalR] failed to connect');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
dispatch(
|
|
||||||
setAppValue({
|
setAppValue({
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
isReconnecting: false,
|
isReconnecting: false,
|
||||||
isDisconnected: false,
|
isDisconnected: false,
|
||||||
isRestarting: false,
|
isRestarting: false,
|
||||||
})
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleStart = useRef(() => {
|
const handleStart = useRef(() => {
|
||||||
console.debug('[signalR] connected');
|
console.debug('[signalR] connected');
|
||||||
|
|
||||||
dispatch(
|
|
||||||
setAppValue({
|
setAppValue({
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
isReconnecting: false,
|
isReconnecting: false,
|
||||||
isDisconnected: false,
|
isDisconnected: false,
|
||||||
isRestarting: false,
|
isRestarting: false,
|
||||||
})
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleReconnecting = useRef(() => {
|
const handleReconnecting = useRef(() => {
|
||||||
dispatch(setAppValue({ isReconnecting: true }));
|
setAppValue({ isReconnecting: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleReconnected = useRef(() => {
|
const handleReconnected = useRef(() => {
|
||||||
dispatch(
|
|
||||||
setAppValue({
|
setAppValue({
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
isReconnecting: false,
|
isReconnecting: false,
|
||||||
isDisconnected: false,
|
isDisconnected: false,
|
||||||
isRestarting: false,
|
isRestarting: false,
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Repopulate the page (if a repopulator is set) to ensure things
|
// Repopulate the page (if a repopulator is set) to ensure things
|
||||||
// are in sync after reconnecting.
|
// are in sync after reconnecting.
|
||||||
|
|
@ -387,7 +381,7 @@ function SignalRListener() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'version') {
|
if (name === 'version') {
|
||||||
dispatch(setVersion({ version: body.version }));
|
setVersion({ version: body.version });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -435,7 +429,7 @@ function SignalRListener() {
|
||||||
.withAutomaticReconnect({
|
.withAutomaticReconnect({
|
||||||
nextRetryDelayInMilliseconds: (retryContext) => {
|
nextRetryDelayInMilliseconds: (retryContext) => {
|
||||||
if (retryContext.elapsedMilliseconds > 180000) {
|
if (retryContext.elapsedMilliseconds > 180000) {
|
||||||
dispatch(setAppValue({ isDisconnected: true }));
|
setAppValue({ isDisconnected: true });
|
||||||
}
|
}
|
||||||
return Math.min(retryContext.previousRetryCount, 10) * 1000;
|
return Math.min(retryContext.previousRetryCount, 10) * 1000;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import { useAppDimension } from 'App/appStore';
|
||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
|
|
@ -35,7 +36,6 @@ import { Statistics } from 'Series/Series';
|
||||||
import useSeries from 'Series/useSeries';
|
import useSeries from 'Series/useSeries';
|
||||||
import { toggleSeasonMonitored } from 'Store/Actions/seriesActions';
|
import { toggleSeasonMonitored } from 'Store/Actions/seriesActions';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import { TableOptionsChangePayload } from 'typings/Table';
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
||||||
import isAfter from 'Utilities/Date/isAfter';
|
import isAfter from 'Utilities/Date/isAfter';
|
||||||
|
|
@ -123,7 +123,7 @@ function SeriesDetailsSeason({
|
||||||
|
|
||||||
const { columns, sortKey, sortDirection } = useEpisodeOptions();
|
const { columns, sortKey, sortDirection } = useEpisodeOptions();
|
||||||
|
|
||||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
const isSmallScreen = useAppDimension('isSmallScreen');
|
||||||
const isSearching = useSelector(
|
const isSearching = useSelector(
|
||||||
createIsSearchingSelector(seriesId, seasonNumber)
|
createIsSearchingSelector(seriesId, seasonNumber)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import React, {
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
|
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||||
|
import { useAppDimension } from 'App/appStore';
|
||||||
import { SelectProvider } from 'App/Select/SelectContext';
|
import { SelectProvider } from 'App/Select/SelectContext';
|
||||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||||
import SeriesAppState, { SeriesIndexAppState } from 'App/State/SeriesAppState';
|
import SeriesAppState, { SeriesIndexAppState } from 'App/State/SeriesAppState';
|
||||||
|
|
@ -37,7 +38,6 @@ import {
|
||||||
} from 'Store/Actions/seriesIndexActions';
|
} from 'Store/Actions/seriesIndexActions';
|
||||||
import scrollPositions from 'Store/scrollPositions';
|
import scrollPositions from 'Store/scrollPositions';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import createSeriesClientSideCollectionItemsSelector from 'Store/Selectors/createSeriesClientSideCollectionItemsSelector';
|
import createSeriesClientSideCollectionItemsSelector from 'Store/Selectors/createSeriesClientSideCollectionItemsSelector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import SeriesIndexFilterMenu from './Menus/SeriesIndexFilterMenu';
|
import SeriesIndexFilterMenu from './Menus/SeriesIndexFilterMenu';
|
||||||
|
|
@ -95,7 +95,7 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
||||||
const isRssSyncExecuting = useSelector(
|
const isRssSyncExecuting = useSelector(
|
||||||
createCommandExecutingSelector(RSS_SYNC)
|
createCommandExecutingSelector(RSS_SYNC)
|
||||||
);
|
);
|
||||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
const isSmallScreen = useAppDimension('isSmallScreen');
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
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 { batchActions } from 'redux-batched-actions';
|
||||||
|
import { hideMessage, showMessage } from 'App/messagesStore';
|
||||||
import { messageTypes } from 'Helpers/Props';
|
import { messageTypes } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import { isSameCommand } from 'Utilities/Command';
|
import { isSameCommand } from 'Utilities/Command';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import { hideMessage, showMessage } from './appActions';
|
|
||||||
import { removeItem, updateItem } from './baseActions';
|
import { removeItem, updateItem } from './baseActions';
|
||||||
import createFetchHandler from './Creators/createFetchHandler';
|
import createFetchHandler from './Creators/createFetchHandler';
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
|
@ -85,13 +85,13 @@ function showCommandMessage(payload, dispatch) {
|
||||||
hideAfter = trigger === 'manual' ? 10 : 4;
|
hideAfter = trigger === 'manual' ? 10 : 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(showMessage({
|
showMessage({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
message,
|
message,
|
||||||
type,
|
type,
|
||||||
hideAfter
|
hideAfter
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleRemoveCommand(command, dispatch) {
|
function scheduleRemoveCommand(command, dispatch) {
|
||||||
|
|
@ -112,10 +112,11 @@ function scheduleRemoveCommand(command, dispatch) {
|
||||||
|
|
||||||
removeCommandTimeoutIds[id] = setTimeout(() => {
|
removeCommandTimeoutIds[id] = setTimeout(() => {
|
||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
removeCommand({ section: 'commands', id }),
|
removeCommand({ section: 'commands', id })
|
||||||
hideMessage({ id })
|
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
hideMessage({ id });
|
||||||
|
|
||||||
delete removeCommandTimeoutIds[id];
|
delete removeCommandTimeoutIds[id];
|
||||||
}, 60000 * 5);
|
}, 60000 * 5);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import * as app from './appActions';
|
|
||||||
import * as captcha from './captchaActions';
|
import * as captcha from './captchaActions';
|
||||||
import * as commands from './commandActions';
|
import * as commands from './commandActions';
|
||||||
import * as episodeHistory from './episodeHistoryActions';
|
import * as episodeHistory from './episodeHistoryActions';
|
||||||
|
|
@ -13,7 +12,6 @@ import * as seriesIndex from './seriesIndexActions';
|
||||||
import * as settings from './settingsActions';
|
import * as settings from './settingsActions';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
app,
|
|
||||||
captcha,
|
captcha,
|
||||||
commands,
|
commands,
|
||||||
episodeHistory,
|
episodeHistory,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1 @@
|
||||||
import { createSelector } from 'reselect';
|
// This file has been removed - use useAppDimensions or useAppDimension from App/appStore instead
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
|
|
||||||
function createDimensionsSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.app.dimensions,
|
|
||||||
(dimensions) => {
|
|
||||||
return dimensions;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createDimensionsSelector;
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
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 { Error } from 'App/State/AppSectionState';
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import Icon, { IconName, IconProps } from 'Components/Icon';
|
import Icon, { IconName, IconProps } from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
|
|
@ -79,7 +79,7 @@ function RestoreBackupModalContent({
|
||||||
name,
|
name,
|
||||||
onModalClose,
|
onModalClose,
|
||||||
}: RestoreBackupModalContentProps) {
|
}: RestoreBackupModalContentProps) {
|
||||||
const { isRestarting } = useSelector((state: AppState) => state.app);
|
const isRestarting = useAppValue('isRestarting');
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { restoreBackupById, isRestoringBackup, restoreBackupError } =
|
const { restoreBackupById, isRestoringBackup, restoreBackupError } =
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useAppValues } from 'App/appStore';
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
import useHealth from './useHealth';
|
import useHealth from './useHealth';
|
||||||
|
|
||||||
function HealthStatus() {
|
function HealthStatus() {
|
||||||
const { isConnected, isReconnecting } = useSelector(
|
const { isConnected, isReconnecting } = useAppValues(
|
||||||
(state: AppState) => state.app
|
'isConnected',
|
||||||
|
'isReconnecting'
|
||||||
);
|
);
|
||||||
const { data, refetch } = useHealth();
|
const { data, refetch } = useHealth();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import AppState from 'App/State/AppState';
|
import { useAppValue } from 'App/appStore';
|
||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
|
|
@ -29,7 +29,7 @@ import styles from './Updates.css';
|
||||||
const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i;
|
const VERSION_REGEX = /\d+\.\d+\.\d+\.\d+/i;
|
||||||
|
|
||||||
function Updates() {
|
function Updates() {
|
||||||
const currentVersion = useSelector((state: AppState) => state.app.version);
|
const currentVersion = useAppValue('version');
|
||||||
const { packageUpdateMechanismMessage } = useSystemStatusData();
|
const { packageUpdateMechanismMessage } = useSystemStatusData();
|
||||||
|
|
||||||
const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
|
const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,30 @@
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { pingServer, setAppValue } from 'App/appStore';
|
||||||
import { useDispatch } from 'react-redux';
|
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
||||||
import { pingServer, setAppValue } from 'Store/Actions/appActions';
|
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to ${endpoint}: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useRestart = () => {
|
export const useRestart = () => {
|
||||||
const dispatch = useDispatch();
|
const mutation = useApiMutation<void, void>({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/system/restart',
|
||||||
|
});
|
||||||
|
|
||||||
return useMutation<void, Error, void>({
|
const restart = () => {
|
||||||
mutationFn: createSystemMutationFn('restart'),
|
mutation.mutate(undefined, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
dispatch(setAppValue({ isRestarting: true }));
|
setAppValue({ isRestarting: true });
|
||||||
dispatch(pingServer());
|
pingServer();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...mutation,
|
||||||
|
mutate: restart,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const useShutdown = () => {
|
export const useShutdown = () => {
|
||||||
return useMutation<void, Error, void>({
|
return useApiMutation<void, void>({
|
||||||
mutationFn: createSystemMutationFn('shutdown'),
|
method: 'POST',
|
||||||
|
path: '/system/shutdown',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue