mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-18 21:35:27 -04:00
Use react-query for parse
This commit is contained in:
@@ -14,7 +14,6 @@ import InteractiveImportAppState from './InteractiveImportAppState';
|
||||
import MessagesAppState from './MessagesAppState';
|
||||
import OAuthAppState from './OAuthAppState';
|
||||
import OrganizePreviewAppState from './OrganizePreviewAppState';
|
||||
import ParseAppState from './ParseAppState';
|
||||
import PathsAppState from './PathsAppState';
|
||||
import ProviderOptionsAppState from './ProviderOptionsAppState';
|
||||
import ReleasesAppState from './ReleasesAppState';
|
||||
@@ -91,7 +90,6 @@ interface AppState {
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
oAuth: OAuthAppState;
|
||||
organizePreview: OrganizePreviewAppState;
|
||||
parse: ParseAppState;
|
||||
paths: PathsAppState;
|
||||
providerOptions: ProviderOptionsAppState;
|
||||
releases: ReleasesAppState;
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
.inputContainer {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inputIconContainer {
|
||||
width: 58px;
|
||||
height: 46px;
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
border-right: none;
|
||||
border-radius: 4px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background-color: var(--inputIconContainerBackgroundColor);
|
||||
text-align: center;
|
||||
line-height: 46px;
|
||||
}
|
||||
|
||||
.input {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
height: 46px;
|
||||
border-radius: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.clearButton {
|
||||
border: 1px solid var(--inputBorderColor);
|
||||
border-left: none;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: $largeFontSize;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'clearButton': string;
|
||||
'helpText': string;
|
||||
'input': string;
|
||||
'inputContainer': string;
|
||||
'inputIconContainer': string;
|
||||
'message': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,110 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { clear, fetch } from 'Store/Actions/parseActions';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ParseResult from './ParseResult';
|
||||
import parseStateSelector from './parseStateSelector';
|
||||
import styles from './Parse.css';
|
||||
|
||||
function Parse() {
|
||||
const { isFetching, error, item } = useSelector(parseStateSelector());
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ value }: InputChanged<string>) => {
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
setTitle(value);
|
||||
|
||||
if (trimmedValue === '') {
|
||||
dispatch(clear());
|
||||
} else {
|
||||
dispatch(fetch({ title: trimmedValue }));
|
||||
}
|
||||
},
|
||||
[setTitle, dispatch]
|
||||
);
|
||||
|
||||
const onClearPress = useCallback(() => {
|
||||
setTitle('');
|
||||
dispatch(clear());
|
||||
}, [setTitle, dispatch]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
return () => {
|
||||
dispatch(clear());
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Parse')}>
|
||||
<PageContentBody>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.inputIconContainer}>
|
||||
<Icon name={icons.PARSE} size={20} />
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
className={styles.input}
|
||||
name="title"
|
||||
value={title}
|
||||
placeholder="eg. Series.Title.S01E05.720p.HDTV-RlsGroup"
|
||||
autoFocus={true}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
||||
<Button className={styles.clearButton} onPress={onClearPress}>
|
||||
<Icon name={icons.REMOVE} size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
{translate('ParseModalErrorParsing')}
|
||||
</div>
|
||||
<div>{getErrorMessage(error)}</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isFetching && title && !error && !item.parsedEpisodeInfo ? (
|
||||
<div className={styles.message}>
|
||||
{translate('ParseModalUnableToParse')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isFetching && !error && item.parsedEpisodeInfo ? (
|
||||
<ParseResult item={item} />
|
||||
) : null}
|
||||
|
||||
{title ? null : (
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
{translate('ParseModalHelpText')}
|
||||
</div>
|
||||
<div>{translate('ParseModalHelpTextDetails')}</div>
|
||||
</div>
|
||||
)}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default Parse;
|
||||
@@ -43,3 +43,15 @@
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.loading {
|
||||
composes: loading from '~Components/Loading/LoadingIndicator.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ interface CssExports {
|
||||
'input': string;
|
||||
'inputContainer': string;
|
||||
'inputIconContainer': string;
|
||||
'loading': string;
|
||||
'message': string;
|
||||
'modalFooter': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
@@ -8,13 +7,13 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import useDebounce from 'Helpers/Hooks/useDebounce';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { clear, fetch } from 'Store/Actions/parseActions';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ParseResult from './ParseResult';
|
||||
import parseStateSelector from './parseStateSelector';
|
||||
import useParse from './useParse';
|
||||
import styles from './ParseModalContent.css';
|
||||
|
||||
interface ParseModalContentProps {
|
||||
@@ -23,40 +22,21 @@ interface ParseModalContentProps {
|
||||
|
||||
function ParseModalContent(props: ParseModalContentProps) {
|
||||
const { onModalClose } = props;
|
||||
const { isFetching, error, item } = useSelector(parseStateSelector());
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
const queryTitle = useDebounce(title, title ? 300 : 0);
|
||||
const { isFetching, isLoading, error, data } = useParse(queryTitle);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ value }: InputChanged<string>) => {
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
setTitle(value);
|
||||
|
||||
if (trimmedValue === '') {
|
||||
dispatch(clear());
|
||||
} else {
|
||||
dispatch(fetch({ title: trimmedValue }));
|
||||
}
|
||||
},
|
||||
[setTitle, dispatch]
|
||||
[setTitle]
|
||||
);
|
||||
|
||||
const onClearPress = useCallback(() => {
|
||||
setTitle('');
|
||||
dispatch(clear());
|
||||
}, [setTitle, dispatch]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
return () => {
|
||||
dispatch(clear());
|
||||
};
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
}, [setTitle]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
@@ -82,9 +62,9 @@ function ParseModalContent(props: ParseModalContentProps) {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
{isLoading ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
{!isLoading && !!error ? (
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
{translate('ParseModalErrorParsing')}
|
||||
@@ -93,14 +73,14 @@ function ParseModalContent(props: ParseModalContentProps) {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isFetching && title && !error && !item.parsedEpisodeInfo ? (
|
||||
{!isLoading && title && !error && !data.parsedEpisodeInfo ? (
|
||||
<div className={styles.message}>
|
||||
{translate('ParseModalUnableToParse')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isFetching && !error && item.parsedEpisodeInfo ? (
|
||||
<ParseResult item={item} />
|
||||
{!isLoading && !error && data.parsedEpisodeInfo ? (
|
||||
<ParseResult item={data} />
|
||||
) : null}
|
||||
|
||||
{title ? null : (
|
||||
@@ -113,7 +93,13 @@ function ParseModalContent(props: ParseModalContentProps) {
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<div>
|
||||
{isFetching && !isLoading ? (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import { AppSectionItemState } from 'App/State/AppSectionState';
|
||||
import Episode from 'Episode/Episode';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
@@ -48,7 +47,3 @@ export interface ParseModel extends ModelBase {
|
||||
customFormats?: CustomFormat[];
|
||||
customFormatScore?: number;
|
||||
}
|
||||
|
||||
type ParseAppState = AppSectionItemState<ParseModel>;
|
||||
|
||||
export default ParseAppState;
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { ParseModel } from 'App/State/ParseAppState';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { ParseModel } from './ParseModel';
|
||||
import ParseResultItem from './ParseResultItem';
|
||||
import styles from './ParseResult.css';
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import ParseAppState from 'App/State/ParseAppState';
|
||||
|
||||
export default function parseStateSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.parse,
|
||||
(parse: ParseAppState) => {
|
||||
return parse;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { keepPreviousData } from '@tanstack/react-query';
|
||||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||
import { ParseModel } from './ParseModel';
|
||||
|
||||
const useParse = (title: string) => {
|
||||
const result = useApiQuery<ParseModel>({
|
||||
path: '/parse',
|
||||
queryParams: { title },
|
||||
queryOptions: {
|
||||
enabled: title.trim().length > 0,
|
||||
placeholderData: keepPreviousData,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?? ({} as ParseModel),
|
||||
};
|
||||
};
|
||||
|
||||
export default useParse;
|
||||
@@ -10,7 +10,6 @@ import * as importSeries from './importSeriesActions';
|
||||
import * as interactiveImportActions from './interactiveImportActions';
|
||||
import * as oAuth from './oAuthActions';
|
||||
import * as organizePreview from './organizePreviewActions';
|
||||
import * as parse from './parseActions';
|
||||
import * as paths from './pathActions';
|
||||
import * as providerOptions from './providerOptionActions';
|
||||
import * as releases from './releaseActions';
|
||||
@@ -35,7 +34,6 @@ export default [
|
||||
interactiveImportActions,
|
||||
oAuth,
|
||||
organizePreview,
|
||||
parse,
|
||||
paths,
|
||||
providerOptions,
|
||||
releases,
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { Dispatch } from 'redux';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import { set, update } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createClearReducer from './Creators/Reducers/createClearReducer';
|
||||
|
||||
interface FetchPayload {
|
||||
title: string;
|
||||
}
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'parse';
|
||||
let parseTimeout: number | null = null;
|
||||
let abortCurrentRequest: (() => void) | null = null;
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
item: {},
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH = 'parse/fetch';
|
||||
export const CLEAR = 'parse/clear';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetch = createThunk(FETCH);
|
||||
export const clear = createAction(CLEAR);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH]: function (
|
||||
_getState: () => AppState,
|
||||
payload: FetchPayload,
|
||||
dispatch: Dispatch
|
||||
) {
|
||||
if (parseTimeout) {
|
||||
clearTimeout(parseTimeout);
|
||||
}
|
||||
|
||||
parseTimeout = window.setTimeout(async () => {
|
||||
dispatch(set({ section, isFetching: true }));
|
||||
|
||||
if (abortCurrentRequest) {
|
||||
abortCurrentRequest();
|
||||
}
|
||||
|
||||
const { request, abortRequest } = createAjaxRequest({
|
||||
url: '/parse',
|
||||
data: {
|
||||
title: payload.title,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await request;
|
||||
|
||||
dispatch(
|
||||
batchActions([
|
||||
update({ section, data }),
|
||||
|
||||
set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null,
|
||||
}),
|
||||
])
|
||||
);
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
set({
|
||||
section,
|
||||
isAdding: false,
|
||||
isAdded: false,
|
||||
addError: error,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
abortCurrentRequest = abortRequest;
|
||||
}, 300);
|
||||
},
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions(
|
||||
{
|
||||
[CLEAR]: createClearReducer(section, defaultState),
|
||||
},
|
||||
defaultState,
|
||||
section
|
||||
);
|
||||
Reference in New Issue
Block a user