Use react-query for UI settings

This commit is contained in:
Mark McDowall 2025-12-28 16:35:16 -08:00
parent e9011011ed
commit 74e6ce4305
32 changed files with 264 additions and 263 deletions

View file

@ -1,11 +1,10 @@
import React from 'react';
import { useSelector } from 'react-redux';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
import Link from 'Components/Link/Link';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import {
DownloadFailedHistory,
DownloadFolderImportedHistory,
@ -33,9 +32,7 @@ interface HistoryDetailsProps {
function HistoryDetails(props: HistoryDetailsProps) {
const { eventType, sourceTitle, data, downloadId } = props;
const { shortDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
const { shortDateFormat, timeFormat } = useUiSettingsValues();
if (eventType === 'grabbed') {
const {

View file

@ -1,5 +1,4 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import { useSelect } from 'App/Select/SelectContext';
import IconButton from 'Components/Link/IconButton';
@ -22,7 +21,7 @@ import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import SeriesTitleLink from 'Series/SeriesTitleLink';
import { useSingleSeries } from 'Series/useSeries';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import CustomFormat from 'typings/CustomFormat';
import { SelectStateInputProps } from 'typings/props';
import Queue, {
@ -107,9 +106,8 @@ function QueueRow(props: QueueRowProps) {
const series = useSingleSeries(seriesId);
const episodes = useEpisodesWithIds(episodeIds);
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
const { showRelativeDates, shortDateFormat, timeFormat } =
useUiSettingsValues();
const { removeQueueItem, isRemoving } = useRemoveQueueItem(id);
const { grabQueueItem, isGrabbing, grabError } = useGrabQueueItem(id);
const { toggleSelected, useIsSelected } = useSelect<Queue>();

View file

@ -29,7 +29,6 @@ import NamingConfig from 'typings/Settings/NamingConfig';
import NamingExample from 'typings/Settings/NamingExample';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import RemotePathMapping from 'typings/Settings/RemotePathMapping';
import UiSettings from 'typings/Settings/UiSettings';
import MetadataAppState from './MetadataAppState';
type Presets<T> = T & {
@ -156,7 +155,6 @@ export interface RemotePathMappingsAppState
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type LanguageSettingsAppState = AppSectionState<Language>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
autoTaggings: AutoTaggingAppState;
@ -183,7 +181,6 @@ interface SettingsAppState {
qualityProfiles: QualityProfilesAppState;
releaseProfiles: ReleaseProfilesAppState;
remotePathMappings: RemotePathMappingsAppState;
ui: UiSettingsAppState;
}
export default SettingsAppState;

View file

@ -1,6 +1,5 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { useQueueItemForEpisode } from 'Activity/Queue/Details/QueueDetailsProvider';
import { useCalendarOptions } from 'Calendar/calendarOptionsStore';
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
@ -13,7 +12,7 @@ import getFinaleTypeName from 'Episode/getFinaleTypeName';
import { useEpisodeFile } from 'EpisodeFile/EpisodeFileProvider';
import { icons, kinds } from 'Helpers/Props';
import { useSingleSeries } from 'Series/useSeries';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import { convertToTimezone } from 'Utilities/Date/convertToTimezone';
import formatTime from 'Utilities/Date/formatTime';
import padNumber from 'Utilities/Number/padNumber';
@ -59,7 +58,7 @@ function AgendaEvent(props: AgendaEventProps) {
const episodeFile = useEpisodeFile(episodeFileId);
const queueItem = useQueueItemForEpisode(id);
const { timeFormat, longDateFormat, enableColorImpairedMode, timeZone } =
useSelector(createUISettingsSelector());
useUiSettingsValues();
const {
showEpisodeInformation,

View file

@ -1,10 +1,9 @@
import moment from 'moment';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCalendarOption } from 'Calendar/calendarOptionsStore';
import * as calendarViews from 'Calendar/calendarViews';
import { useCalendarDates } from 'Calendar/useCalendar';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import DayOfWeek from './DayOfWeek';
import styles from './DaysOfWeek.css';
@ -12,7 +11,7 @@ function DaysOfWeek() {
const view = useCalendarOption('view');
const dates = useCalendarDates();
const { calendarWeekColumnHeader, shortDateFormat, showRelativeDates } =
useSelector(createUISettingsSelector());
useUiSettingsValues();
const updateTimeout = useRef<ReturnType<typeof setTimeout>>();
const [todaysDate, setTodaysDate] = useState(

View file

@ -1,6 +1,5 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { useQueueItemForEpisode } from 'Activity/Queue/Details/QueueDetailsProvider';
import { useCalendarOptions } from 'Calendar/calendarOptionsStore';
import getStatusStyle from 'Calendar/getStatusStyle';
@ -12,7 +11,7 @@ import getFinaleTypeName from 'Episode/getFinaleTypeName';
import { useEpisodeFile } from 'EpisodeFile/EpisodeFileProvider';
import { icons, kinds } from 'Helpers/Props';
import { useSingleSeries } from 'Series/useSeries';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import { convertToTimezone } from 'Utilities/Date/convertToTimezone';
import formatTime from 'Utilities/Date/formatTime';
import padNumber from 'Utilities/Number/padNumber';
@ -60,9 +59,8 @@ function CalendarEvent(props: CalendarEventProps) {
const episodeFile = useEpisodeFile(episodeFileId);
const queueItem = useQueueItemForEpisode(id);
const { timeFormat, enableColorImpairedMode, timeZone } = useSelector(
createUISettingsSelector()
);
const { timeFormat, enableColorImpairedMode, timeZone } =
useUiSettingsValues();
const {
showEpisodeInformation,

View file

@ -1,6 +1,5 @@
import classNames from 'classnames';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useIsDownloadingEpisodes } from 'Activity/Queue/Details/QueueDetailsProvider';
import { useCalendarOptions } from 'Calendar/calendarOptionsStore';
import getStatusStyle from 'Calendar/getStatusStyle';
@ -9,7 +8,7 @@ import Link from 'Components/Link/Link';
import getFinaleTypeName from 'Episode/getFinaleTypeName';
import { icons, kinds } from 'Helpers/Props';
import { useSingleSeries } from 'Series/useSeries';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import { CalendarItem } from 'typings/Calendar';
import { convertToTimezone } from 'Utilities/Date/convertToTimezone';
import formatTime from 'Utilities/Date/formatTime';
@ -34,9 +33,8 @@ function CalendarEventGroup({
const isDownloading = useIsDownloadingEpisodes(episodeIds);
const series = useSingleSeries(seriesId)!;
const { timeFormat, enableColorImpairedMode, timeZone } = useSelector(
createUISettingsSelector()
);
const { timeFormat, enableColorImpairedMode, timeZone } =
useUiSettingsValues();
const { showEpisodeInformation, showFinaleIcon, fullColorEvents } =
useCalendarOptions();

View file

@ -1,6 +1,5 @@
import moment from 'moment';
import React, { useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { useAppDimensions } from 'App/appStore';
import {
setCalendarOption,
@ -22,7 +21,7 @@ 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 createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import translate from 'Utilities/String/translate';
import CalendarHeaderViewButton from './CalendarHeaderViewButton';
import styles from './CalendarHeader.css';
@ -35,7 +34,7 @@ function CalendarHeader() {
const { isSmallScreen, isLargeScreen } = useAppDimensions();
const { longDateFormat } = useSelector(createUISettingsSelector());
const { longDateFormat } = useUiSettingsValues();
const handleViewChange = useCallback((newView: string) => {
setCalendarOption('view', newView as CalendarView);

View file

@ -1,11 +1,10 @@
import React from 'react';
import { useSelector } from 'react-redux';
import {
useCalendarOption,
useCalendarOptions,
} from 'Calendar/calendarOptionsStore';
import { icons, kinds } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import translate from 'Utilities/String/translate';
import LegendIconItem from './LegendIconItem';
import LegendItem from './LegendItem';
@ -19,7 +18,7 @@ function Legend() {
showCutoffUnmetIcon,
fullColorEvents,
} = useCalendarOptions();
const { enableColorImpairedMode } = useSelector(createUISettingsSelector());
const { enableColorImpairedMode } = useUiSettingsValues();
const iconsToShow = [];
const isAgendaView = view === 'agenda';

View file

@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
CalendarOptions,
setCalendarOption,
@ -22,10 +21,12 @@ import {
timeFormatOptions,
weekColumnOptions,
} from 'Settings/UI/UISettings';
import { saveUISettings } from 'Store/Actions/settingsActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import {
UiSettingsModel,
useSaveUiSettings,
useUiSettingsValues,
} from 'Settings/UI/useUiSettings';
import { InputChanged } from 'typings/inputs';
import UiSettings from 'typings/Settings/UiSettings';
import translate from 'Utilities/String/translate';
interface CalendarOptionsModalContentProps {
@ -35,8 +36,6 @@ interface CalendarOptionsModalContentProps {
function CalendarOptionsModalContent({
onModalClose,
}: CalendarOptionsModalContentProps) {
const dispatch = useDispatch();
const {
collapseMultipleEpisodes,
showEpisodeInformation,
@ -46,9 +45,10 @@ function CalendarOptionsModalContent({
fullColorEvents,
} = useCalendarOptions();
const uiSettings = useSelector(createUISettingsSelector());
const uiSettings = useUiSettingsValues();
const saveUiSettings = useSaveUiSettings();
const [state, setState] = useState<Partial<UiSettings>>({
const [state, setState] = useState<Partial<UiSettingsModel>>({
firstDayOfWeek: uiSettings.firstDayOfWeek,
calendarWeekColumnHeader: uiSettings.calendarWeekColumnHeader,
timeFormat: uiSettings.timeFormat,
@ -73,9 +73,9 @@ function CalendarOptionsModalContent({
({ name, value }: InputChanged) => {
setState((prevState) => ({ ...prevState, [name]: value }));
dispatch(saveUISettings({ [name]: value }));
saveUiSettings({ [name]: value });
},
[dispatch]
[saveUiSettings]
);
useEffect(() => {

View file

@ -1,14 +1,13 @@
import { keepPreviousData } from '@tanstack/react-query';
import moment from 'moment';
import { useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { create } from 'zustand';
import AppState from 'App/State/AppState';
import { setEpisodeQueryKey } from 'Episode/useEpisode';
import { Filter, FilterBuilderProp } from 'Filters/Filter';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import { filterBuilderValueTypes } from 'Helpers/Props';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import { CalendarItem } from 'typings/Calendar';
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
import translate from 'Utilities/String/translate';
@ -156,9 +155,7 @@ export const useCalendarPage = () => {
const dayCount = useCalendarDayCount();
const time = useCalendarTime();
const view = useCalendarOption('view');
const firstDayOfWeek = useSelector(
(state: AppState) => state.settings.ui.item.firstDayOfWeek
);
const { firstDayOfWeek } = useUiSettingsValues();
useEffect(() => {
const { dates } = getDates(time, view, firstDayOfWeek, dayCount);

View file

@ -13,7 +13,7 @@ interface ErrorPageProps {
customFiltersError: ApiError | null;
tagsError: ApiError | null;
qualityProfilesError?: Error;
uiSettingsError?: Error;
uiSettingsError: ApiError | null;
systemStatusError: ApiError | null;
}

View file

@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { saveDimensions, useAppValue } from 'App/appStore';
import AppUpdatedModal from 'App/AppUpdatedModal';
import ColorImpairedContext from 'App/ColorImpairedContext';
@ -7,7 +6,7 @@ import ConnectionLostModal from 'App/ConnectionLostModal';
import SignalRListener from 'Components/SignalRListener';
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
import useAppPage from 'Helpers/Hooks/useAppPage';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import { useSystemStatusData } from 'System/Status/useSystemStatus';
import ErrorPage from './ErrorPage';
import PageHeader from './Header/PageHeader';
@ -29,7 +28,7 @@ function Page({ children }: PageProps) {
const [isConnectionLostModalOpen, setIsConnectionLostModalOpen] =
useState(false);
const { enableColorImpairedMode } = useSelector(createUISettingsSelector());
const { enableColorImpairedMode } = useUiSettingsValues();
const { authentication } = useSystemStatusData();
const authenticationEnabled = authentication !== 'none';

View file

@ -1,6 +1,5 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import TableRowCell from './TableRowCell';
@ -25,7 +24,7 @@ function RelativeDateCell(props: RelativeDateCellProps) {
} = props;
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
useSelector(createUISettingsSelector());
useUiSettingsValues();
if (!date) {
return <Component className={className} {...otherProps} />;

View file

@ -1,9 +1,8 @@
import moment from 'moment';
import React from 'react';
import { useSelector } from 'react-redux';
import Label from 'Components/Label';
import { kinds, sizes } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import formatTime from 'Utilities/Date/formatTime';
import isInNextWeek from 'Utilities/Date/isInNextWeek';
import isToday from 'Utilities/Date/isToday';
@ -18,9 +17,8 @@ interface EpisodeAiringProps {
function EpisodeAiring(props: EpisodeAiringProps) {
const { airDateUtc, network } = props;
const { shortDateFormat, showRelativeDates, timeFormat } = useSelector(
createUISettingsSelector()
);
const { shortDateFormat, showRelativeDates, timeFormat } =
useUiSettingsValues();
const networkLabel = (
<Label kind={kinds.INFO} size={sizes.MEDIUM}>

View file

@ -6,13 +6,13 @@ import { useTranslations } from 'App/useTranslations';
import useCommands from 'Commands/useCommands';
import useCustomFilters from 'Filters/useCustomFilters';
import useSeries from 'Series/useSeries';
import { useUiSettings } from 'Settings/UI/useUiSettings';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import {
fetchImportLists,
fetchIndexerFlags,
fetchLanguages,
fetchQualityProfiles,
fetchUISettings,
} from 'Store/Actions/settingsActions';
import useSystemStatus from 'System/Status/useSystemStatus';
import useTags from 'Tags/useTags';
@ -23,22 +23,22 @@ const createErrorsSelector = ({
systemStatusError,
tagsError,
translationsError,
uiSettingsError,
seriesError,
}: {
customFiltersError: ApiError | null;
systemStatusError: ApiError | null;
tagsError: ApiError | null;
translationsError: ApiError | null;
uiSettingsError: ApiError | null;
seriesError: ApiError | null;
}) =>
createSelector(
(state: AppState) => state.settings.ui.error,
(state: AppState) => state.settings.qualityProfiles.error,
(state: AppState) => state.settings.languages.error,
(state: AppState) => state.settings.importLists.error,
(state: AppState) => state.settings.indexerFlags.error,
(
uiSettingsError,
qualityProfilesError,
languagesError,
importListsError,
@ -54,7 +54,8 @@ const createErrorsSelector = ({
indexerFlagsError ||
systemStatusError ||
tagsError ||
translationsError
translationsError ||
uiSettingsError
);
return {
@ -93,9 +94,11 @@ const useAppPage = () => {
const { isFetched: isTranslationsFetched, error: translationsError } =
useTranslations();
const { isFetched: isUiSettingsFetched, error: uiSettingsError } =
useUiSettings();
const isAppStatePopulated = useSelector(
(state: AppState) =>
state.settings.ui.isPopulated &&
state.settings.qualityProfiles.isPopulated &&
state.settings.languages.isPopulated &&
state.settings.importLists.isPopulated &&
@ -108,7 +111,8 @@ const useAppPage = () => {
isSeriesFetched &&
isSystemStatusFetched &&
isTagsFetched &&
isTranslationsFetched;
isTranslationsFetched &&
isUiSettingsFetched;
const { hasError, errors } = useSelector(
createErrorsSelector({
@ -117,6 +121,7 @@ const useAppPage = () => {
systemStatusError,
tagsError,
translationsError,
uiSettingsError,
})
);
@ -139,7 +144,6 @@ const useAppPage = () => {
dispatch(fetchLanguages());
dispatch(fetchImportLists());
dispatch(fetchIndexerFlags());
dispatch(fetchUISettings());
}, [dispatch]);
return useMemo(() => {

View file

@ -1,4 +1,4 @@
import { useMemo, useRef } from 'react';
import { useMemo, useState } from 'react';
import { create, useStore } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
@ -14,25 +14,17 @@ interface PendingChangesStore<T extends object> {
export const usePendingChangesStore = <T extends object>(
initialPendingChanges: Partial<T>
) => {
const store = useRef(
create<PendingChangesStore<T>>()((_set) => {
// eslint-disable-next-line react/hook-use-state
const [store] = useState(() => {
return create<PendingChangesStore<T>>()((_set) => {
return {
pendingChanges: initialPendingChanges,
};
})
);
const usePendingChanges = () => {
return useStore(
store.current,
useShallow((state) => {
return state.pendingChanges as Partial<T>;
})
);
};
});
});
const setPendingChange = <K extends keyof T>(key: K, value: T[K]) => {
store.current.setState((state) => ({
store.setState((state) => ({
...state,
pendingChanges: {
...state.pendingChanges,
@ -41,21 +33,31 @@ export const usePendingChangesStore = <T extends object>(
}));
};
const setPendingChanges = (changes: Partial<T>) => {
store.current.setState((state) => ({
const unsetPendingChange = <K extends keyof T>(key: K) => {
store.setState((state) => {
const newPendingChanges = { ...state.pendingChanges };
delete newPendingChanges[key];
return {
...state,
pendingChanges: newPendingChanges,
};
});
};
const clearPendingChanges = () => {
store.setState((state) => ({
...state,
pendingChanges: {
...state.pendingChanges,
...changes,
},
pendingChanges: {},
}));
};
const discardPendingChanges = () => {
return setPendingChanges({} as Partial<T>);
};
const pendingChanges = usePendingChanges();
const pendingChanges = useStore(
store,
useShallow((state) => {
return state.pendingChanges as Partial<T>;
})
);
const hasPendingChanges = useMemo(() => {
return Object.keys(pendingChanges).length > 0;
@ -65,7 +67,8 @@ export const usePendingChangesStore = <T extends object>(
store,
pendingChanges,
setPendingChange,
discardPendingChanges,
unsetPendingChange,
clearPendingChanges,
hasPendingChanges,
};
};

View file

@ -1,18 +1,10 @@
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import themes from 'Styles/Themes';
function createThemeSelector() {
return createSelector(
(state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme,
(theme) => theme
);
}
const useTheme = () => {
const selectedTheme = useSelector(createThemeSelector());
const { theme } = useUiSettingsValues();
const selectedTheme = theme ?? window.Sonarr.theme;
const [resolvedTheme, setResolvedTheme] = useState(selectedTheme);
useEffect(() => {

View file

@ -1,5 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
@ -14,7 +13,7 @@ import EpisodeLanguages from 'Episode/EpisodeLanguages';
import EpisodeQuality from 'Episode/EpisodeQuality';
import IndexerFlags from 'Episode/IndexerFlags';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
@ -122,9 +121,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
protocol,
} = release;
const { longDateFormat, timeFormat, timeZone } = useSelector(
createUISettingsSelector()
);
const { longDateFormat, timeFormat, timeZone } = useUiSettingsValues();
const [isConfirmGrabModalOpen, setIsConfirmGrabModalOpen] = useState(false);
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);

View file

@ -1,11 +1,12 @@
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { IconName } from 'Components/Icon';
import { icons } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import {
UiSettingsModel,
useUiSettingsValues,
} from 'Settings/UI/useUiSettings';
import dimensions from 'Styles/Variables/dimensions';
import QualityProfile from 'typings/QualityProfile';
import UiSettings from 'typings/Settings/UiSettings';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
@ -95,7 +96,7 @@ const rows = [
function getInfoRowProps(
row: RowProps,
props: SeriesIndexOverviewInfoProps,
uiSettings: UiSettings
uiSettings: UiSettingsModel
): RowInfoProps | null {
const { name } = row;
@ -209,8 +210,7 @@ function getInfoRowProps(
function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) {
const { height, nextAiring } = props;
const uiSettings = useSelector(createUISettingsSelector());
const uiSettings = useUiSettingsValues();
const { shortDateFormat, showRelativeDates, longDateFormat, timeFormat } =
uiSettings;

View file

@ -1,6 +1,5 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import Label from 'Components/Label';
@ -16,7 +15,7 @@ import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect
import { Statistics } from 'Series/Series';
import { useSeriesPosterOptions } from 'Series/seriesOptionsStore';
import SeriesPoster from 'Series/SeriesPoster';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import translate from 'Utilities/String/translate';
@ -48,7 +47,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
} = useSeriesPosterOptions();
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
useSelector(createUISettingsSelector());
useUiSettingsValues();
const executeCommand = useExecuteCommand();
const [hasPosterError, setHasPosterError] = useState(false);

View file

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import React, { useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
@ -12,20 +12,13 @@ import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { inputTypes, kinds } from 'Helpers/Props';
import SettingsToolbar from 'Settings/SettingsToolbar';
import {
fetchUISettings,
saveUISettings,
setUISettingsValue,
} from 'Store/Actions/settingsActions';
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import themes from 'Styles/Themes';
import { InputChanged } from 'typings/inputs';
import timeZoneOptions from 'Utilities/Date/timeZoneOptions';
import titleCase from 'Utilities/String/titleCase';
import translate from 'Utilities/String/translate';
const SECTION = 'ui';
import { useManageUiSettings } from './useUiSettings';
export const firstDayOfWeekOptions: EnhancedSelectInputValue<number>[] = [
{
@ -69,8 +62,6 @@ export const timeFormatOptions: EnhancedSelectInputValue<string>[] = [
];
function UISettings() {
const dispatch = useDispatch();
const {
items,
isFetching: isLanguagesFetching,
@ -86,15 +77,17 @@ function UISettings() {
const {
isFetching: isSettingsFetching,
isPopulated: isSettingsPopulated,
isFetched: isSettingsPopulated,
error: settingsError,
hasPendingChanges,
hasSettings,
settings,
hasPendingChanges,
isSaving,
validationErrors,
validationWarnings,
} = useSelector(createSettingsSectionSelector(SECTION));
saveSettings,
updateSetting,
} = useManageUiSettings();
const isFetching = isLanguagesFetching || isSettingsFetching;
const isPopulated = isLanguagesPopulated && isSettingsPopulated;
@ -116,23 +109,15 @@ function UISettings() {
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions aren't typed
dispatch(setUISettingsValue(change));
// @ts-expect-error name needs to be keyof UiSettingsModel
updateSetting(change.name, change.value);
},
[dispatch]
[updateSetting]
);
const handleSavePress = useCallback(() => {
dispatch(saveUISettings());
}, [dispatch]);
useEffect(() => {
dispatch(fetchUISettings());
return () => {
// @ts-expect-error - actions aren't typed
dispatch(setUISettingsValue({ section: `settings.${SECTION}` }));
};
}, [dispatch]);
saveSettings();
}, [saveSettings]);
return (
<PageContent title={translate('UiSettings')}>

View file

@ -0,0 +1,54 @@
import { useCallback } from 'react';
import {
useManageSettings,
useSaveSettings,
useSettings,
} from 'Settings/useSettings';
export interface UiSettingsModel {
theme: 'auto' | 'dark' | 'light';
showRelativeDates: boolean;
shortDateFormat: string;
longDateFormat: string;
timeFormat: string;
timeZone: string;
firstDayOfWeek: number;
enableColorImpairedMode: boolean;
calendarWeekColumnHeader: string;
uiLanguage: number;
}
const PATH = '/settings/ui';
export const useUiSettingsValues = () => {
const { data } = useSettings<UiSettingsModel>(PATH);
return data;
};
export const useUiSettings = () => {
return useSettings<UiSettingsModel>(PATH);
};
export const useManageUiSettings = () => {
return useManageSettings<UiSettingsModel>(PATH);
};
export const useSaveUiSettings = () => {
const { data } = useSettings<UiSettingsModel>(PATH);
const { save } = useSaveSettings<UiSettingsModel>(PATH);
const saveSettings = useCallback(
(changes: Partial<UiSettingsModel>) => {
const updatedSettings = {
...data,
...changes,
};
save(updatedSettings);
},
[data, save]
);
return saveSettings;
};

View file

@ -0,0 +1,91 @@
import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import { usePendingChangesStore } from 'Helpers/Hooks/usePendingChangesStore';
import selectSettings from 'Store/Selectors/selectSettings';
export const useSettings = <T extends object>(path: string) => {
const result = useApiQuery<T>({
path,
});
return {
...result,
data: result.data ?? ({} as T),
};
};
export const useSaveSettings = <T extends object>(
path: string,
onSuccess?: () => void
) => {
const queryClient = useQueryClient();
const { mutate, isPending, error } = useApiMutation<T, T>({
path,
method: 'PUT',
mutationOptions: {
onSuccess: (updatedSettings: T) => {
queryClient.setQueryData<T>([path], updatedSettings);
onSuccess?.();
},
},
});
return {
save: mutate,
isSaving: isPending,
saveError: error,
};
};
export const useManageSettings = <T extends object>(path: string) => {
const { data, isFetching, isFetched, error } = useSettings<T>(path);
const {
pendingChanges,
setPendingChange,
unsetPendingChange,
clearPendingChanges,
} = usePendingChangesStore<T>({});
const { save, isSaving, saveError } = useSaveSettings<T>(
path,
clearPendingChanges
);
const settings = useMemo(() => {
return selectSettings<T>(data, pendingChanges, saveError);
}, [data, pendingChanges, saveError]);
const saveSettings = useCallback(() => {
const updatedSettings = {
...data,
...pendingChanges,
};
save(updatedSettings);
}, [data, pendingChanges, save]);
const updateSetting = useCallback(
<K extends keyof T>(key: K, value: T[K]) => {
if (data[key] === value) {
unsetPendingChange(key);
} else {
setPendingChange(key, value);
}
},
[data, setPendingChange, unsetPendingChange]
);
return {
...settings,
updateSetting,
saveSettings,
isFetching,
isFetched,
isSaving,
error,
saveError,
};
};

View file

@ -1,64 +0,0 @@
import { createAction } from 'redux-actions';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createSaveHandler from 'Store/Actions/Creators/createSaveHandler';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.ui';
//
// Actions Types
export const FETCH_UI_SETTINGS = 'settings/ui/fetchUiSettings';
export const SET_UI_SETTINGS_VALUE = 'SET_UI_SETTINGS_VALUE';
export const SAVE_UI_SETTINGS = 'SAVE_UI_SETTINGS';
//
// Action Creators
export const fetchUISettings = createThunk(FETCH_UI_SETTINGS);
export const saveUISettings = createThunk(SAVE_UI_SETTINGS);
export const setUISettingsValue = createAction(SET_UI_SETTINGS_VALUE, (payload) => {
return {
section,
...payload
};
});
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
pendingChanges: {},
isSaving: false,
saveError: null,
item: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_UI_SETTINGS]: createFetchHandler(section, '/config/ui'),
[SAVE_UI_SETTINGS]: createSaveHandler(section, '/config/ui')
},
//
// Reducers
reducers: {
[SET_UI_SETTINGS_VALUE]: createSetSettingValueReducer(section)
}
};

View file

@ -24,7 +24,6 @@ import qualityDefinitions from './Settings/qualityDefinitions';
import qualityProfiles from './Settings/qualityProfiles';
import releaseProfiles from './Settings/releaseProfiles';
import remotePathMappings from './Settings/remotePathMappings';
import ui from './Settings/ui';
export * from './Settings/autoTaggingSpecifications';
export * from './Settings/autoTaggings';
@ -50,7 +49,6 @@ export * from './Settings/qualityDefinitions';
export * from './Settings/qualityProfiles';
export * from './Settings/releaseProfiles';
export * from './Settings/remotePathMappings';
export * from './Settings/ui';
//
// Variables
@ -85,8 +83,7 @@ export const defaultState = {
qualityDefinitions: qualityDefinitions.defaultState,
qualityProfiles: qualityProfiles.defaultState,
releaseProfiles: releaseProfiles.defaultState,
remotePathMappings: remotePathMappings.defaultState,
ui: ui.defaultState
remotePathMappings: remotePathMappings.defaultState
};
export const persistState = [
@ -120,8 +117,7 @@ export const actionHandlers = handleThunks({
...qualityDefinitions.actionHandlers,
...qualityProfiles.actionHandlers,
...releaseProfiles.actionHandlers,
...remotePathMappings.actionHandlers,
...ui.actionHandlers
...remotePathMappings.actionHandlers
});
//
@ -151,7 +147,6 @@ export const reducers = createHandleActions({
...qualityDefinitions.reducers,
...qualityProfiles.reducers,
...releaseProfiles.reducers,
...remotePathMappings.reducers,
...ui.reducers
...remotePathMappings.reducers
}, defaultState, section);

View file

@ -1,13 +0,0 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createUISettingsSelector() {
return createSelector(
(state: AppState) => state.settings.ui,
(ui) => {
return ui.item;
}
);
}
export default createUISettingsSelector;

View file

@ -1,7 +1,6 @@
import moment from 'moment';
import React, { useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
@ -11,9 +10,7 @@ interface StartTimeProps {
function StartTime(props: StartTimeProps) {
const { startTime } = props;
const { timeFormat, longDateFormat } = useSelector(
createUISettingsSelector()
);
const { timeFormat, longDateFormat } = useUiSettingsValues();
const [time, setTime] = useState(Date.now());
const { formattedStartTime, uptime } = useMemo(() => {

View file

@ -1,6 +1,5 @@
import moment from 'moment';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { CommandBody } from 'Commands/Command';
import { useCancelCommand } from 'Commands/useCommands';
import Icon, { IconProps } from 'Components/Icon';
@ -10,7 +9,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
@ -120,7 +119,7 @@ export default function QueuedTaskRow(props: QueuedTaskRowProps) {
const { cancelCommand } = useCancelCommand(id);
const { longDateFormat, shortDateFormat, showRelativeDates, timeFormat } =
useSelector(createUISettingsSelector());
useUiSettingsValues();
const updateTimeTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(
null

View file

@ -1,12 +1,11 @@
import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useCommand, useExecuteCommand } from 'Commands/useCommands';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import { isCommandExecuting } from 'Utilities/Command';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime';
@ -35,7 +34,7 @@ function ScheduledTaskRow({
}: ScheduledTaskRowProps) {
const executeCommand = useExecuteCommand();
const { showRelativeDates, longDateFormat, shortDateFormat, timeFormat } =
useSelector(createUISettingsSelector());
useUiSettingsValues();
const command = useCommand(taskName);
const [time, setTime] = useState(Date.now());

View file

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { useAppValue } from 'App/appStore';
import CommandNames from 'Commands/CommandNames';
import { useCommandExecuting, useExecuteCommand } from 'Commands/useCommands';
@ -14,8 +14,8 @@ import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons, kinds } from 'Helpers/Props';
import useUpdateSettings from 'Settings/General/useUpdateSettings';
import { useUiSettingsValues } from 'Settings/UI/useUiSettings';
import { fetchGeneralSettings } from 'Store/Actions/settingsActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { useSystemStatusData } from 'System/Status/useSystemStatus';
import { UpdateMechanism } from 'typings/Settings/General';
import formatDate from 'Utilities/Date/formatDate';
@ -31,9 +31,7 @@ function Updates() {
const currentVersion = useAppValue('version');
const { packageUpdateMechanismMessage } = useSystemStatusData();
const { shortDateFormat, longDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
const { shortDateFormat, longDateFormat, timeFormat } = useUiSettingsValues();
const isInstallingUpdate = useCommandExecuting(
CommandNames.ApplicationUpdate
);

View file

@ -1,12 +0,0 @@
export default interface UiSettings {
theme: 'auto' | 'dark' | 'light';
showRelativeDates: boolean;
shortDateFormat: string;
longDateFormat: string;
timeFormat: string;
timeZone: string;
firstDayOfWeek: number;
enableColorImpairedMode: boolean;
calendarWeekColumnHeader: string;
uiLanguage: number;
}