1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-15 15:54:35 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Mark McDowall
b9d46dd3ea New: Differentiate Proper and Repack when renaming files
Closes #7455
2025-03-24 21:16:58 -07:00
245 changed files with 1931 additions and 5071 deletions

113
distribution/debian/install.sh Executable file → Normal file
View File

@@ -6,8 +6,6 @@
### Version V1.0.1 2024-01-02 - StevieTV - remove UTF8-BOM
### Version V1.0.2 2024-01-03 - markus101 - Get user input from /dev/tty
### Version V1.0.3 2024-01-06 - StevieTV - exit script when it is ran from install directory
### Version V1.0.4 2025-04-05 - kaecyra - Allow user/group to be supplied via CLI, add unattended mode
### Version V1.0.5 2025-07-08 - bparkin1283 - use systemctl instead of service for stopping app
### Boilerplate Warning
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
@@ -18,8 +16,8 @@
#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
scriptversion="1.0.4"
scriptdate="2025-04-05"
scriptversion="1.0.3"
scriptdate="2024-01-06"
set -euo pipefail
@@ -51,106 +49,18 @@ if [ "$installdir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ] || [ "$bindi
exit
fi
show_help() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Options:
--user <name> What user will $app run under?
User will be created if it doesn't already exist.
--group <name> What group will $app run under?
Group will be created if it doesn't already exist.
-u Unattended mode
The installer will not prompt or pause, making it suitable for automated installations.
This option requires the use of --user and --group to supply those inputs for the script.
-h, --help Show this help message and exit
EOF
}
# Default values for command-line arguments
arg_user=""
arg_group=""
arg_unattended=false
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--user=*)
arg_user="${1#*=}"
shift
;;
--user)
if [[ -n "$2" && "$2" != -* ]]; then
arg_user="$2"
shift 2
else
echo "Error: --user requires a value." >&2
exit 1
fi
;;
--group=*)
arg_group="${1#*=}"
shift
;;
--group)
if [[ -n "$2" && "$2" != -* ]]; then
arg_group="$2"
shift 2
else
echo "Error: --group requires a value." >&2
exit 1
fi
;;
-u)
arg_unattended=true
shift
;;
-h|--help)
show_help
exit 0
;;
*)
echo "Unknown option: $1" >&2
echo "Use --help to see valid options." >&2
exit 1
;;
esac
done
# If unattended mode is requested, require user and group
if $arg_unattended; then
if [[ -z "$arg_user" || -z "$arg_group" ]]; then
echo "Error: --user and --group are required when using -u (unattended mode)." >&2
exit 1
fi
fi
# Prompt User if necessary
if [ -n "$arg_user" ]; then
app_uid="$arg_user"
else
read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty
fi
# Prompt User
read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty
app_uid=$(echo "$app_uid" | tr -d ' ')
app_uid=${app_uid:-$app}
# Prompt Group if necessary
if [ -n "$arg_group" ]; then
app_guid="$arg_group"
else
read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty
fi
# Prompt Group
read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty
app_guid=$(echo "$app_guid" | tr -d ' ')
app_guid=${app_guid:-media}
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that the selected user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
if ! $arg_unattended; then
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
fi
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
# Create User / Group as needed
if [ "$app_guid" != "$app_uid" ]; then
@@ -168,10 +78,11 @@ if ! getent group "$app_guid" | grep -qw "$app_uid"; then
echo "Added User [$app_uid] to Group [$app_guid]"
fi
# Stop and disable the App if running
if [ $(systemctl is-active "$app") = "active" ]; then
systemctl disable --now -q "$app"
echo "Stopped and disabled existing $app"
# Stop the App if running
if service --status-all | grep -Fq "$app"; then
systemctl stop "$app"
systemctl disable "$app".service
echo "Stopped existing $app"
fi
# Create Appdata Directory

View File

@@ -176,7 +176,7 @@ module.exports = (env) => {
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: '3.42'
corejs: '3.39'
}
]
]

View File

@@ -1,10 +1,7 @@
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import React, { useCallback, useMemo, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
@@ -12,8 +9,6 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import { setQueueRemovalOption } from 'Store/Actions/queueActions';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemModal.css';
@@ -35,6 +30,12 @@ interface RemoveQueueItemModalProps {
onModalClose: () => void;
}
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
type BlocklistMethod =
| 'doNotBlocklist'
| 'blocklistAndSearch'
| 'blocklistOnly';
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
const {
isOpen,
@@ -47,13 +48,12 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
onModalClose,
} = props;
const dispatch = useDispatch();
const multipleSelected = selectedCount && selectedCount > 1;
const { removalMethod, blocklistMethod } = useSelector(
(state: AppState) => state.queue.removalOptions
);
const [removalMethod, setRemovalMethod] =
useState<RemovalMethod>('removeFromClient');
const [blocklistMethod, setBlocklistMethod] =
useState<BlocklistMethod>('doNotBlocklist');
const { title, message } = useMemo(() => {
if (!selectedCount) {
@@ -79,7 +79,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
}, [sourceTitle, selectedCount]);
const removalMethodOptions = useMemo(() => {
const options: EnhancedSelectInputValue<string>[] = [
return [
{
key: 'removeFromClient',
value: translate('RemoveFromDownloadClient'),
@@ -106,12 +106,10 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
: translate('IgnoreDownloadHint'),
},
];
return options;
}, [canChangeCategory, canIgnore, multipleSelected]);
const blocklistMethodOptions = useMemo(() => {
const options: EnhancedSelectInputValue<string>[] = [
return [
{
key: 'doNotBlocklist',
value: translate('DoNotBlocklist'),
@@ -133,15 +131,20 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
: translate('BlocklistOnlyHint'),
},
];
return options;
}, [isPending, multipleSelected]);
const handleRemovalOptionInputChange = useCallback(
({ name, value }: InputChanged) => {
dispatch(setQueueRemovalOption({ [name]: value }));
const handleRemovalMethodChange = useCallback(
({ value }: { value: RemovalMethod }) => {
setRemovalMethod(value);
},
[dispatch]
[setRemovalMethod]
);
const handleBlocklistMethodChange = useCallback(
({ value }: { value: BlocklistMethod }) => {
setBlocklistMethod(value);
},
[setBlocklistMethod]
);
const handleConfirmRemove = useCallback(() => {
@@ -151,11 +154,23 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
blocklist: blocklistMethod !== 'doNotBlocklist',
skipRedownload: blocklistMethod === 'blocklistOnly',
});
}, [removalMethod, blocklistMethod, onRemovePress]);
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
}, [
removalMethod,
blocklistMethod,
setRemovalMethod,
setBlocklistMethod,
onRemovePress,
]);
const handleModalClose = useCallback(() => {
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
onModalClose();
}, [onModalClose]);
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
@@ -178,7 +193,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
helpTextWarning={translate(
'RemoveQueueItemRemovalMethodHelpTextWarning'
)}
onChange={handleRemovalOptionInputChange}
onChange={handleRemovalMethodChange}
/>
</FormGroup>
)}
@@ -196,7 +211,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
value={blocklistMethod}
values={blocklistMethodOptions}
helpText={translate('BlocklistReleaseHelpText')}
onChange={handleRemovalOptionInputChange}
onChange={handleBlocklistMethodChange}
/>
</FormGroup>
</ModalBody>

View File

@@ -32,17 +32,6 @@ export interface QueuePagedAppState
removeError: Error;
}
export type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
export type BlocklistMethod =
| 'doNotBlocklist'
| 'blocklistAndSearch'
| 'blocklistOnly';
interface RemovalOptions {
removalMethod: RemovalMethod;
blocklistMethod: BlocklistMethod;
}
interface QueueAppState {
status: AppSectionItemState<QueueStatus>;
details: QueueDetailsAppState;
@@ -50,7 +39,6 @@ interface QueueAppState {
options: {
includeUnknownSeriesItems: boolean;
};
removalOptions: RemovalOptions;
}
export default QueueAppState;

View File

@@ -22,9 +22,9 @@ function createIsDownloadingSelector(episodeIds: number[]) {
return createSelector(
(state: AppState) => state.queue.details,
(details) => {
return details.items.some(
(item) => item.episodeId && episodeIds.includes(item.episodeId)
);
return details.items.some((item) => {
return !!(item.episodeId && episodeIds.includes(item.episodeId));
});
}
);
}
@@ -61,10 +61,10 @@ function CalendarEventGroup({
const endTime = moment(lastEpisode.airDateUtc).add(series.runtime, 'minutes');
const seasonNumber = firstEpisode.seasonNumber;
const { allDownloaded, anyGrabbed, anyMonitored, allAbsoluteEpisodeNumbers } =
const { allDownloaded, anyQueued, anyMonitored, allAbsoluteEpisodeNumbers } =
useMemo(() => {
let files = 0;
let grabbed = 0;
let queued = 0;
let monitored = 0;
let absoluteEpisodeNumbers = 0;
@@ -73,8 +73,8 @@ function CalendarEventGroup({
files++;
}
if (event.grabbed) {
grabbed++;
if (event.queued) {
queued++;
}
if (series.monitored && event.monitored) {
@@ -88,13 +88,13 @@ function CalendarEventGroup({
return {
allDownloaded: files === events.length,
anyGrabbed: grabbed > 0,
anyQueued: queued > 0,
anyMonitored: monitored > 0,
allAbsoluteEpisodeNumbers: absoluteEpisodeNumbers === events.length,
};
}, [series, events]);
const anyDownloading = isDownloading || anyGrabbed;
const anyDownloading = isDownloading || anyQueued;
const statusStyle = getStatusStyle(
allDownloaded,

View File

@@ -22,12 +22,7 @@ interface CalendarLinkModalContentProps {
function CalendarLinkModalContent({
onModalClose,
}: CalendarLinkModalContentProps) {
const [state, setState] = useState<{
unmonitored: boolean;
premieresOnly: boolean;
asAllDay: boolean;
tags: number[];
}>({
const [state, setState] = useState({
unmonitored: false,
premieresOnly: false,
asAllDay: false,

View File

@@ -93,10 +93,9 @@ function AutoSuggestInput<T = any>(props: AutoSuggestInputProps<T>) {
mainAxis: true,
}),
size({
apply({ availableHeight, elements, rects }) {
apply({ rects, elements }) {
Object.assign(elements.floating.style, {
minWidth: `${rects.reference.width}px`,
maxHeight: `${Math.max(0, availableHeight)}px`,
width: `${rects.reference.width}px`,
});
},
}),

View File

@@ -139,11 +139,11 @@ type PickProps<V, C extends InputType> = C extends 'text'
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
EnhancedSelectInputProps<any, V>
: C extends 'seriesTag'
? SeriesTagInputProps<V>
? SeriesTagInputProps
: C extends 'seriesTypeSelect'
? SeriesTypeSelectInputProps
: C extends 'tag'
? SeriesTagInputProps<V>
? SeriesTagInputProps
: C extends 'tagSelect'
? TagSelectInputProps
: C extends 'text'
@@ -222,7 +222,7 @@ function FormInputGroup<T, C extends InputType>(
<div className={containerClassName}>
<div className={className}>
<div className={styles.inputContainer}>
{/* @ts-expect-error - types are validated already */}
{/* @ts-expect-error - tpyes are validated already */}
<InputComponent
className={inputClassName}
helpText={helpText}

View File

@@ -120,7 +120,7 @@ function ProviderFieldFormGroup<T>({
helpTextWarning={helpTextWarning}
helpLink={helpLink}
placeholder={placeholder}
// @ts-expect-error - this isn't available on all types
// @ts-expect-error - this isn;'t available on all types
selectOptionsProviderAction={selectOptionsProviderAction}
value={value}
values={selectValues}

View File

@@ -42,10 +42,15 @@
color: var(--disabledInputColor);
}
.optionsContainer {
z-index: $popperZIndex;
max-height: vh(50);
width: auto;
}
.options {
composes: scroller from '~Components/Scroller/Scroller.css';
z-index: $popperZIndex;
border: 1px solid var(--inputBorderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);

View File

@@ -13,6 +13,7 @@ interface CssExports {
'mobileCloseButton': string;
'mobileCloseButtonContainer': string;
'options': string;
'optionsContainer': string;
'optionsInnerModalBody': string;
'optionsModal': string;
'optionsModalBody': string;

View File

@@ -14,7 +14,6 @@ import React, {
KeyboardEvent,
ReactNode,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
@@ -181,21 +180,15 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
mainAxis: true,
}),
size({
apply({ availableHeight, elements, rects }) {
apply({ rects, elements }) {
Object.assign(elements.floating.style, {
minWidth: `${rects.reference.width}px`,
maxHeight: `${Math.max(
0,
Math.min(window.innerHeight / 2, availableHeight)
)}px`,
'min-width': `${rects.reference.width}px`,
});
},
}),
],
open: isOpen,
placement: 'bottom-start',
whileElementsMounted: autoUpdate,
onOpenChange: setIsOpen,
});
const click = useClick(context);
@@ -221,8 +214,12 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
}, [value, values, isMultiSelect]);
const handlePress = useCallback(() => {
setIsOpen((prevIsOpen) => !prevIsOpen);
}, []);
if (!isOpen && onOpen) {
onOpen();
}
setIsOpen(!isOpen);
}, [isOpen, setIsOpen, onOpen]);
const handleSelect = useCallback(
(newValue: ArrayElement<V>) => {
@@ -375,12 +372,6 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
[onChange]
);
useEffect(() => {
if (isOpen) {
onOpen?.();
}
}, [isOpen, onOpen]);
return (
<>
<div ref={refs.setReference} {...getReferenceProps()}>
@@ -452,43 +443,46 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
</Link>
)}
</div>
{!isMobile && isOpen ? (
{isOpen ? (
<FloatingPortal id="portal-root">
<Scroller
<div
ref={refs.setFloating}
className={styles.options}
className={styles.optionsContainer}
style={floatingStyles}
{...getFloatingProps()}
>
{values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected =
v.parentKey !== undefined &&
Array.isArray(value) &&
value.includes(v.parentKey);
{isOpen && !isMobile ? (
<Scroller className={styles.options}>
{values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected =
v.parentKey !== undefined &&
Array.isArray(value) &&
value.includes(v.parentKey);
const { key, ...other } = v;
const { key, ...other } = v;
return (
<OptionComponent
key={v.key}
id={v.key}
depth={depth}
isSelected={isSelectedItem(index, value, values)}
isDisabled={parentSelected}
isMultiSelect={isMultiSelect}
{...valueOptions}
{...other}
isMobile={false}
onSelect={handleSelect}
>
{v.value}
</OptionComponent>
);
})}
</Scroller>
return (
<OptionComponent
key={v.key}
id={v.key}
depth={depth}
isSelected={isSelectedItem(index, value, values)}
isDisabled={parentSelected}
isMultiSelect={isMultiSelect}
{...valueOptions}
{...other}
isMobile={false}
onSelect={handleSelect}
>
{v.value}
</OptionComponent>
);
})}
</Scroller>
) : null}
</div>
</FloatingPortal>
) : null}

View File

@@ -88,10 +88,13 @@ function QualityProfileSelectInput({
);
const handleChange = useCallback(
({ value }: EnhancedSelectInputChanged<string | number>) => {
onChange({ name, value });
({ value: newValue }: EnhancedSelectInputChanged<string | number>) => {
onChange({
name,
value: newValue === 'noChange' ? value : newValue,
});
},
[name, onChange]
[name, value, onChange]
);
useEffect(() => {

View File

@@ -21,7 +21,6 @@ const ADD_NEW_KEY = 'addNew';
export interface RootFolderSelectInputValue
extends EnhancedSelectInputValue<string> {
freeSpace?: number;
isMissing?: boolean;
}
@@ -43,58 +42,66 @@ function createRootFolderOptionsSelector(
includeNoChange: boolean,
includeNoChangeDisabled: boolean
) {
return createSelector(createRootFoldersSelector(), (rootFolders) => {
const values: RootFolderSelectInputValue[] = rootFolders.items.map(
(rootFolder) => {
return {
key: rootFolder.path,
value: rootFolder.path,
freeSpace: rootFolder.freeSpace,
return createSelector(
createRootFoldersSelector(),
(rootFolders) => {
const values: RootFolderSelectInputValue[] = rootFolders.items.map(
(rootFolder) => {
return {
key: rootFolder.path,
value: rootFolder.path,
freeSpace: rootFolder.freeSpace,
isMissing: false,
};
}
);
if (includeNoChange) {
values.unshift({
key: 'noChange',
get value() {
return translate('NoChange');
},
isDisabled: includeNoChangeDisabled,
isMissing: false,
};
});
}
);
if (includeNoChange) {
values.unshift({
key: 'noChange',
get value() {
return translate('NoChange');
},
isDisabled: includeNoChangeDisabled,
isMissing: false,
});
}
if (!values.length) {
values.push({
key: '',
value: '',
isDisabled: true,
isHidden: true,
});
}
if (
includeMissingValue &&
value &&
!values.find((v) => v.key === value)
) {
values.push({
key: value,
value,
isMissing: true,
isDisabled: true,
});
}
if (!values.length) {
values.push({
key: '',
value: '',
isDisabled: true,
isHidden: true,
key: ADD_NEW_KEY,
value: translate('AddANewPath'),
});
return {
values,
isSaving: rootFolders.isSaving,
saveError: rootFolders.saveError,
};
}
if (includeMissingValue && value && !values.find((v) => v.key === value)) {
values.push({
key: value,
value,
isMissing: true,
isDisabled: true,
});
}
values.push({
key: ADD_NEW_KEY,
value: translate('AddANewPath'),
});
return {
values,
isSaving: rootFolders.isSaving,
saveError: rootFolders.saveError,
};
});
);
}
function RootFolderSelectInput({

View File

@@ -18,16 +18,18 @@ interface RootFolderSelectInputOptionProps
isWindows?: boolean;
}
function RootFolderSelectInputOption({
id,
value,
freeSpace,
isMissing,
seriesFolder,
isMobile,
isWindows,
...otherProps
}: RootFolderSelectInputOptionProps) {
function RootFolderSelectInputOption(props: RootFolderSelectInputOptionProps) {
const {
id,
value,
freeSpace,
isMissing,
seriesFolder,
isMobile,
isWindows,
...otherProps
} = props;
const slashCharacter = isWindows ? '\\' : '/';
return (

View File

@@ -30,11 +30,3 @@
text-align: right;
font-size: $smallFontSize;
}
.isMissing {
flex: 0 0 auto;
margin-left: 15px;
color: var(--dangerColor);
text-align: right;
font-size: $smallFontSize;
}

View File

@@ -2,7 +2,6 @@
// Please do not change this file!
interface CssExports {
'freeSpace': string;
'isMissing': string;
'path': string;
'pathContainer': string;
'selectedValue': string;

View File

@@ -8,23 +8,27 @@ import styles from './RootFolderSelectInputSelectedValue.css';
interface RootFolderSelectInputSelectedValueProps {
selectedValue: string;
values: RootFolderSelectInputValue[];
freeSpace?: number;
seriesFolder?: string;
isWindows?: boolean;
includeFreeSpace?: boolean;
}
function RootFolderSelectInputSelectedValue({
selectedValue,
values,
seriesFolder,
includeFreeSpace = true,
isWindows,
...otherProps
}: RootFolderSelectInputSelectedValueProps) {
function RootFolderSelectInputSelectedValue(
props: RootFolderSelectInputSelectedValueProps
) {
const {
selectedValue,
values,
freeSpace,
seriesFolder,
includeFreeSpace = true,
isWindows,
...otherProps
} = props;
const slashCharacter = isWindows ? '\\' : '/';
const { value, freeSpace, isMissing } =
values.find((v) => v.key === selectedValue) ||
({} as RootFolderSelectInputValue);
const value = values.find((v) => v.key === selectedValue)?.value;
return (
<EnhancedSelectInputSelectedValue
@@ -49,10 +53,6 @@ function RootFolderSelectInputSelectedValue({
})}
</div>
) : null}
{isMissing ? (
<div className={styles.isMissing}>{translate('Missing')}</div>
) : null}
</EnhancedSelectInputSelectedValue>
);
}

View File

@@ -2,12 +2,10 @@
import React, { SyntheticEvent } from 'react';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput, {
EnhancedSelectInputValue,
} from './EnhancedSelectInput';
import EnhancedSelectInput from './EnhancedSelectInput';
import styles from './UMaskInput.css';
const umaskOptions: EnhancedSelectInputValue<string>[] = [
const umaskOptions = [
{
key: '755',
get value() {

View File

@@ -1,15 +1,9 @@
import classNames from 'classnames';
import React, {
ChangeEvent,
ComponentProps,
SyntheticEvent,
useCallback,
} from 'react';
import React, { ChangeEvent, SyntheticEvent, useCallback } from 'react';
import { InputChanged } from 'typings/inputs';
import styles from './SelectInput.css';
export interface SelectInputOption
extends Pick<ComponentProps<'option'>, 'disabled'> {
interface SelectInputOption {
key: string | number;
value: string | number | (() => string | number);
}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { addTag } from 'Store/Actions/tagActions';
@@ -12,10 +12,10 @@ interface SeriesTag extends TagBase {
name: string;
}
export interface SeriesTagInputProps<V> {
export interface SeriesTagInputProps {
name: string;
value: V;
onChange: (change: InputChanged<V>) => void;
value: number[];
onChange: (change: InputChanged<number[]>) => void;
}
const VALID_TAG_REGEX = new RegExp('[^-_a-z0-9]', 'i');
@@ -59,48 +59,28 @@ function createSeriesTagsSelector(tags: number[]) {
});
}
export default function SeriesTagInput<V extends number | number[]>({
export default function SeriesTagInput({
name,
value,
onChange,
}: SeriesTagInputProps<V>) {
}: SeriesTagInputProps) {
const dispatch = useDispatch();
const isArray = Array.isArray(value);
const arrayValue = useMemo(() => {
if (isArray) {
return value as number[];
}
return value === 0 ? [] : [value as number];
}, [isArray, value]);
const { tags, tagList, allTags } = useSelector(
createSeriesTagsSelector(arrayValue)
createSeriesTagsSelector(value)
);
const handleTagCreated = useCallback(
(tag: SeriesTag) => {
if (isArray) {
onChange({ name, value: [...value, tag.id] as V });
} else {
onChange({
name,
value: tag.id as V,
});
}
onChange({ name, value: [...value, tag.id] });
},
[name, value, isArray, onChange]
[name, value, onChange]
);
const handleTagAdd = useCallback(
(newTag: SeriesTag) => {
if (newTag.id) {
if (isArray) {
onChange({ name, value: [...value, newTag.id] as V });
} else {
onChange({ name, value: newTag.id as V });
}
onChange({ name, value: [...value, newTag.id] });
return;
}
@@ -116,21 +96,17 @@ export default function SeriesTagInput<V extends number | number[]>({
);
}
},
[name, value, isArray, allTags, handleTagCreated, onChange, dispatch]
[name, value, allTags, handleTagCreated, onChange, dispatch]
);
const handleTagDelete = useCallback(
({ index }: { index: number }) => {
if (isArray) {
const newValue = value.slice();
newValue.splice(index, 1);
const newValue = value.slice();
newValue.splice(index, 1);
onChange({ name, value: newValue as V });
} else {
onChange({ name, value: 0 as V });
}
onChange({ name, value: newValue });
},
[name, value, isArray, onChange]
[name, value, onChange]
);
return (

View File

@@ -14,7 +14,7 @@ import {
RenderSuggestion,
SuggestionsFetchRequestedParams,
} from 'react-autosuggest';
import { useDebouncedCallback } from 'use-debounce';
import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback';
import { Kind } from 'Helpers/Props/kinds';
import { InputChanged } from 'typings/inputs';
import AutoSuggestInput from '../AutoSuggestInput';

View File

@@ -58,6 +58,16 @@ function Menu({
onPress: handleMenuButtonPress,
});
const handleFloaterPress = useCallback((_event: MouseEvent) => {
// TODO: Menu items should handle closing when they are clicked.
// This is handled before the menu item click event is handled, so wait 100ms before closing.
setTimeout(() => {
setIsMenuOpen(false);
}, 100);
return true;
}, []);
const handleWindowResize = useCallback(() => {
updateMaxHeight();
}, [updateMaxHeight]);
@@ -108,31 +118,8 @@ function Menu({
onOpenChange: setIsMenuOpen,
});
const handleFloaterPress = useCallback(
(event: MouseEvent) => {
if (
refs.reference &&
(refs.reference.current as HTMLElement).contains(
event.target as HTMLElement
)
) {
return false;
}
// TODO: Menu items should handle closing when they are clicked.
// This is handled before the menu item click event is handled, so wait 100ms before closing.
setTimeout(() => {
setIsMenuOpen(false);
}, 100);
return true;
},
[refs.reference]
);
const click = useClick(context);
const dismiss = useDismiss(context, {
outsidePressEvent: 'click',
outsidePress: handleFloaterPress,
});

View File

@@ -13,10 +13,10 @@ import React, {
import Autosuggest from 'react-autosuggest';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { useDebouncedCallback } from 'use-debounce';
import { Tag } from 'App/State/TagsAppState';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback';
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
import { icons } from 'Helpers/Props';
import Series from 'Series/Series';
@@ -316,7 +316,7 @@ function SeriesSearchInput() {
return;
}
// If a suggestion is not selected go to the first series,
// If an suggestion is not selected go to the first series,
// otherwise go to the selected series.
const selectedSuggestion =

View File

@@ -359,37 +359,34 @@ function PageSidebar({ isSidebarVisible, isSmallScreen }: PageSidebarProps) {
});
}, []);
const handleTouchEnd = useCallback(
(event: TouchEvent) => {
const touches = event.changedTouches;
const currentTouch = touches[0].pageX;
const handleTouchEnd = useCallback((event: TouchEvent) => {
const touches = event.changedTouches;
const currentTouch = touches[0].pageX;
if (!touchStartX.current) {
return;
}
if (!touchStartX.current) {
return;
}
if (currentTouch > touchStartX.current && currentTouch > 50) {
setSidebarTransform({
transition: 'none',
transform: 0,
});
} else if (currentTouch < touchStartX.current && currentTouch < 80) {
setSidebarTransform({
transition: 'transform 50ms ease-in-out',
transform: SIDEBAR_WIDTH * -1,
});
} else {
setSidebarTransform({
transition: 'none',
transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1,
});
}
if (currentTouch > touchStartX.current && currentTouch > 50) {
setSidebarTransform({
transition: 'none',
transform: 0,
});
} else if (currentTouch < touchStartX.current && currentTouch < 80) {
setSidebarTransform({
transition: 'transform 50ms ease-in-out',
transform: SIDEBAR_WIDTH * -1,
});
} else {
setSidebarTransform({
transition: 'none',
transform: 0,
});
}
touchStartX.current = null;
touchStartY.current = null;
},
[isSidebarVisible]
);
touchStartX.current = null;
touchStartY.current = null;
}, []);
const handleTouchCancel = useCallback(() => {
touchStartX.current = null;

View File

@@ -80,12 +80,8 @@ function PageToolbarSection({
if (buttonCount - 1 === maxButtons) {
const overflowItems: PageToolbarButtonProps[] = [];
const buttonsWithoutSeparators = validChildren.filter(
(child) => Object.keys(child.props).length > 0
);
return {
buttons: buttonsWithoutSeparators,
buttons: validChildren,
buttonCount,
overflowItems,
};

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import React, { useCallback, useMemo, useState } from 'react';
import SelectInput, { SelectInputOption } from 'Components/Form/SelectInput';
import SelectInput from 'Components/Form/SelectInput';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -34,7 +34,7 @@ function TablePager({
const isLastPage = page === totalPages;
const pages = useMemo(() => {
return Array.from(new Array(totalPages), (_x, i): SelectInputOption => {
return Array.from(new Array(totalPages), (_x, i) => {
const pageNumber = i + 1;
return {

View File

@@ -22,6 +22,10 @@ interface Episode extends ModelBase {
monitored: boolean;
grabbed?: boolean;
unverifiedSceneNumbering: boolean;
endTime?: string;
grabDate?: string;
seriesTitle?: string;
queued?: boolean;
series?: Series;
finaleType?: string;
}

View File

@@ -0,0 +1,16 @@
import { debounce, DebouncedFunc, DebounceSettings } from 'lodash';
import { useCallback } from 'react';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function useDebouncedCallback<T extends (...args: any) => any>(
callback: T,
delay: number,
options?: DebounceSettings
): DebouncedFunc<T> {
// eslint-disable-next-line react-hooks/exhaustive-deps
return useCallback(debounce(callback, delay, options), [
callback,
delay,
options,
]);
}

View File

@@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import * as commandNames from 'Commands/commandNames';
import SelectInput, { SelectInputOption } from 'Components/Form/SelectInput';
import SelectInput from 'Components/Form/SelectInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
@@ -164,7 +164,7 @@ const COLUMNS = [
},
];
const importModeOptions: SelectInputOption[] = [
const importModeOptions = [
{
key: 'chooseImportMode',
value: () => translate('ChooseImportMode'),
@@ -343,7 +343,7 @@ function InteractiveImportModalContent(
}
);
const options: SelectInputOption[] = [
const options = [
{
key: 'select',
value: translate('SelectDropdown'),

View File

@@ -7,7 +7,6 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
@@ -70,7 +69,7 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
);
const qualityOptions = useMemo(() => {
return items.map(({ id, name }): EnhancedSelectInputValue<number> => {
return items.map(({ id, name }) => {
return {
key: id,
value: name,

View File

@@ -82,9 +82,9 @@ function RootFolderRow(props: RootFolderRowProps) {
<ConfirmModal
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('RemoveRootFolder')}
message={translate('RemoveRootFolderWithSeriesMessageText', { path })}
confirmLabel={translate('Remove')}
title={translate('DeleteRootFolder')}
message={translate('DeleteRootFolderMessageText', { path })}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}
/>

View File

@@ -5,6 +5,7 @@
.header {
position: relative;
width: 100%;
height: 425px;
}
.backdrop {
@@ -29,18 +30,20 @@
width: 100%;
height: 100%;
color: var(--white);
gap: 35px;
}
.poster {
flex-shrink: 0;
margin-right: 35px;
width: 250px;
height: 368px;
}
.info {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
width: 100%;
}
.titleRow {
@@ -56,13 +59,10 @@
}
.title {
overflow: auto;
max-height: calc(3 * 50px);
text-wrap: balance;
font-weight: 300;
font-size: 50px;
line-height: 50px;
line-clamp: 3;
}
.toggleMonitoredContainer {
@@ -170,8 +170,6 @@
}
.title {
overflow: hidden;
max-height: calc(3 * 30px);
font-weight: 300;
font-size: 30px;
line-height: 30px;

View File

@@ -1,6 +1,7 @@
import moment from 'moment';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import TextTruncate from 'react-text-truncate';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
@@ -20,6 +21,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import useMeasure from 'Helpers/Hooks/useMeasure';
import usePrevious from 'Helpers/Hooks/usePrevious';
import {
align,
@@ -54,6 +56,7 @@ import {
import { toggleSeriesMonitored } from 'Store/Actions/seriesActions';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import fonts from 'Styles/Variables/fonts';
import sortByProp from 'Utilities/Array/sortByProp';
import { findCommand, isCommandExecuting } from 'Utilities/Command';
import formatBytes from 'Utilities/Number/formatBytes';
@@ -72,6 +75,9 @@ import SeriesProgressLabel from './SeriesProgressLabel';
import SeriesTags from './SeriesTags';
import styles from './SeriesDetails.css';
const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images: Image[]) {
return images.find((image) => image.coverType === 'fanart')?.url;
}
@@ -240,6 +246,7 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
allCollapsed: false,
seasons: {},
});
const [overviewRef, { height: overviewHeight }] = useMeasure();
const wasRefreshing = usePrevious(isRefreshing);
const wasRenaming = usePrevious(isRenaming);
@@ -516,7 +523,7 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
<PageToolbarSeparator />
<PageToolbarButton
label={translate('EpisodeMonitoring')}
label={translate('SeriesMonitoring')}
iconName={icons.MONITORED}
onPress={handleMonitorOptionsPress}
/>
@@ -789,7 +796,16 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
/>
</div>
<div className={styles.overview}>{overview}</div>
<div ref={overviewRef} className={styles.overview}>
<TextTruncate
line={
Math.floor(
overviewHeight / (defaultFontSize * lineHeight)
) - 1
}
text={overview}
/>
</div>
<MetadataAttribution />
</div>

View File

@@ -562,7 +562,6 @@ function SeriesDetailsSeason({
<SeasonInteractiveSearchModal
isOpen={isInteractiveSearchModalOpen}
episodeCount={totalEpisodeCount}
seriesId={seriesId}
seasonNumber={seasonNumber}
onModalClose={handleInteractiveSearchModalClose}

View File

@@ -4,7 +4,6 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -15,7 +14,7 @@ import { setSeriesOverviewOption } from 'Store/Actions/seriesIndexActions';
import translate from 'Utilities/String/translate';
import selectOverviewOptions from '../selectOverviewOptions';
const posterSizeOptions: EnhancedSelectInputValue<string>[] = [
const posterSizeOptions = [
{
key: 'small',
get value() {

View File

@@ -0,0 +1,11 @@
.grid {
flex: 1 0 auto;
}
.container {
&:hover {
.content {
background-color: var(--tableRowHoverBackgroundColor);
}
}
}

View File

@@ -80,17 +80,17 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) {
const [size, setSize] = useState({ width: 0, height: 0 });
const posterWidth = useMemo(() => {
const maximumPosterWidth = isSmallScreen ? 152 : 162;
const maxiumPosterWidth = isSmallScreen ? 152 : 162;
if (posterSize === 'large') {
return maximumPosterWidth;
return maxiumPosterWidth;
}
if (posterSize === 'medium') {
return Math.floor(maximumPosterWidth * 0.75);
return Math.floor(maxiumPosterWidth * 0.75);
}
return Math.floor(maximumPosterWidth * 0.5);
return Math.floor(maxiumPosterWidth * 0.5);
}, [posterSize, isSmallScreen]);
const posterHeight = useMemo(() => {

View File

@@ -4,7 +4,6 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -15,7 +14,7 @@ import selectPosterOptions from 'Series/Index/Posters/selectPosterOptions';
import { setSeriesPosterOption } from 'Store/Actions/seriesIndexActions';
import translate from 'Utilities/String/translate';
const posterSizeOptions: EnhancedSelectInputValue<string>[] = [
const posterSizeOptions = [
{
key: 'small',
get value() {

View File

@@ -1,7 +1,6 @@
import classNames from 'classnames';
import React, { SyntheticEvent, useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
@@ -123,31 +122,8 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
setIsDeleteSeriesModalOpen(false);
}, [setIsDeleteSeriesModalOpen]);
const [selectState, selectDispatch] = useSelect();
const onSelectPress = useCallback(
(event: SyntheticEvent<HTMLElement, MouseEvent>) => {
if (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey) {
window.open(`/series/${titleSlug}`, '_blank');
return;
}
const shiftKey = event.nativeEvent.shiftKey;
selectDispatch({
type: 'toggleSelected',
id: seriesId,
isSelected: !selectState.selectedState[seriesId],
shiftKey,
});
},
[seriesId, selectState.selectedState, selectDispatch, titleSlug]
);
const link = `/series/${titleSlug}`;
const linkProps = isSelectMode ? { onPress: onSelectPress } : { to: link };
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
@@ -199,7 +175,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
/>
) : null}
<Link className={styles.link} style={elementStyle} {...linkProps}>
<Link className={styles.link} style={elementStyle} to={link}>
<SeriesPoster
style={elementStyle}
images={images}

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -32,7 +31,7 @@ interface EditSeriesModalContentProps {
const NO_CHANGE = 'noChange';
const monitoredOptions: EnhancedSelectInputValue<string>[] = [
const monitoredOptions = [
{
key: NO_CHANGE,
get value() {
@@ -54,7 +53,7 @@ const monitoredOptions: EnhancedSelectInputValue<string>[] = [
},
];
const seasonFolderOptions: EnhancedSelectInputValue<string>[] = [
const seasonFolderOptions = [
{
key: NO_CHANGE,
get value() {

View File

@@ -1,6 +1,5 @@
import React, { useCallback, useState } from 'react';
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -12,7 +11,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import { icons, inputTypes, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ChangeMonitoringModalContent.css';
@@ -47,12 +46,9 @@ function ChangeMonitoringModalContent(
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('MonitorEpisodes')}</ModalHeader>
<ModalHeader>{translate('MonitorSeries')}</ModalHeader>
<ModalBody>
<Alert kind={kinds.INFO}>
<div>{translate('MonitorEpisodesModalInfo')}</div>
</Alert>
<Form {...otherProps}>
<FormGroup>
<FormLabel>

View File

@@ -6,7 +6,6 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
@@ -67,7 +66,7 @@ function TagsModalContent(props: TagsModalContentProps) {
onApplyTagsPress(tags, applyTags);
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions: EnhancedSelectInputValue<string>[] = [
const applyTagsOptions = [
{
key: 'add',
value: translate('Add'),

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -16,7 +15,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import { icons, tooltipPositions } from 'Helpers/Props';
import { updateSeriesMonitor } from 'Store/Actions/seriesActions';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
@@ -67,12 +66,9 @@ function MonitoringOptionsModalContent({
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('MonitorEpisodes')}</ModalHeader>
<ModalHeader>{translate('MonitorSeries')}</ModalHeader>
<ModalBody>
<Alert kind={kinds.INFO}>
<div>{translate('MonitorEpisodesModalInfo')}</div>
</Alert>
<Form>
<FormGroup>
<FormLabel>

View File

@@ -6,19 +6,19 @@ import {
cancelFetchReleases,
clearReleases,
} from 'Store/Actions/releaseActions';
import SeasonInteractiveSearchModalContent, {
SeasonInteractiveSearchModalContentProps,
} from './SeasonInteractiveSearchModalContent';
import SeasonInteractiveSearchModalContent from './SeasonInteractiveSearchModalContent';
interface SeasonInteractiveSearchModalProps
extends SeasonInteractiveSearchModalContentProps {
interface SeasonInteractiveSearchModalProps {
isOpen: boolean;
seriesId: number;
seasonNumber: number;
onModalClose(): void;
}
function SeasonInteractiveSearchModal(
props: SeasonInteractiveSearchModalProps
) {
const { isOpen, episodeCount, seriesId, seasonNumber, onModalClose } = props;
const { isOpen, seriesId, seasonNumber, onModalClose } = props;
const dispatch = useDispatch();
@@ -44,7 +44,6 @@ function SeasonInteractiveSearchModal(
onModalClose={handleModalClose}
>
<SeasonInteractiveSearchModalContent
episodeCount={episodeCount}
seriesId={seriesId}
seasonNumber={seasonNumber}
onModalClose={handleModalClose}

View File

@@ -1,5 +0,0 @@
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}

View File

@@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'modalFooter': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -8,21 +8,18 @@ import { scrollDirections } from 'Helpers/Props';
import InteractiveSearch from 'InteractiveSearch/InteractiveSearch';
import formatSeason from 'Season/formatSeason';
import translate from 'Utilities/String/translate';
import styles from './SeasonInteractiveSearchModalContent.css';
export interface SeasonInteractiveSearchModalContentProps {
episodeCount: number;
interface SeasonInteractiveSearchModalContentProps {
seriesId: number;
seasonNumber: number;
onModalClose(): void;
}
function SeasonInteractiveSearchModalContent({
episodeCount,
seriesId,
seasonNumber,
onModalClose,
}: SeasonInteractiveSearchModalContentProps) {
function SeasonInteractiveSearchModalContent(
props: SeasonInteractiveSearchModalContentProps
) {
const { seriesId, seasonNumber, onModalClose } = props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
@@ -43,13 +40,7 @@ function SeasonInteractiveSearchModalContent({
/>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div>
{translate('EpisodesInSeason', {
episodeCount,
})}
</div>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -24,7 +23,7 @@ interface ManageCustomFormatsEditModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions: EnhancedSelectInputValue<string>[] = [
const enableOptions = [
{
key: NO_CHANGE,
get value() {

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -28,7 +27,7 @@ interface ManageDownloadClientsEditModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions: EnhancedSelectInputValue<string>[] = [
const enableOptions = [
{
key: NO_CHANGE,
get value() {

View File

@@ -8,7 +8,6 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
@@ -72,7 +71,7 @@ function TagsModalContent(props: TagsModalContentProps) {
onApplyTagsPress(tags, applyTags);
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions: EnhancedSelectInputValue<string>[] = [
const applyTagsOptions = [
{
key: 'add',
get value() {

View File

@@ -3,7 +3,6 @@ import FieldSet from 'Components/FieldSet';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes } from 'Helpers/Props';
import { InputChanged } from 'typings/inputs';
@@ -11,7 +10,7 @@ import { PendingSection } from 'typings/pending';
import General from 'typings/Settings/General';
import translate from 'Utilities/String/translate';
const logLevelOptions: EnhancedSelectInputValue<string>[] = [
const logLevelOptions = [
{
key: 'info',
get value() {

View File

@@ -3,7 +3,6 @@ import FieldSet from 'Components/FieldSet';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import { inputTypes, sizes } from 'Helpers/Props';
import { InputChanged } from 'typings/inputs';
import { PendingSection } from 'typings/pending';
@@ -33,7 +32,7 @@ function ProxySettings({
proxyBypassLocalAddresses,
onInputChange,
}: ProxySettingsProps) {
const proxyTypeOptions: EnhancedSelectInputValue<string>[] = [
const proxyTypeOptions = [
{
key: 'http',
value: translate('HttpHttps'),

View File

@@ -6,7 +6,6 @@ import FormGroup from 'Components/Form/FormGroup';
import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Icon from 'Components/Icon';
import ClipboardButton from 'Components/Link/ClipboardButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
@@ -17,7 +16,7 @@ import { PendingSection } from 'typings/pending';
import General from 'typings/Settings/General';
import translate from 'Utilities/String/translate';
export const authenticationMethodOptions: EnhancedSelectInputValue<string>[] = [
export const authenticationMethodOptions = [
{
key: 'none',
get value() {
@@ -48,23 +47,22 @@ export const authenticationMethodOptions: EnhancedSelectInputValue<string>[] = [
},
];
export const authenticationRequiredOptions: EnhancedSelectInputValue<string>[] =
[
{
key: 'enabled',
get value() {
return translate('Enabled');
},
export const authenticationRequiredOptions = [
{
key: 'enabled',
get value() {
return translate('Enabled');
},
{
key: 'disabledForLocalAddresses',
get value() {
return translate('DisabledForLocalAddresses');
},
},
{
key: 'disabledForLocalAddresses',
get value() {
return translate('DisabledForLocalAddresses');
},
];
},
];
const certificateValidationOptions: EnhancedSelectInputValue<string>[] = [
const certificateValidationOptions = [
{
key: 'enabled',
get value() {

View File

@@ -3,7 +3,6 @@ import FieldSet from 'Components/FieldSet';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, sizes } from 'Helpers/Props';
import useSystemStatus from 'System/useSystemStatus';
@@ -39,7 +38,7 @@ function UpdateSettings({
const usingExternalUpdateMechanism = packageUpdateMechanism !== 'builtIn';
const updateOptions: EnhancedSelectInputValue<string>[] = [];
const updateOptions = [];
if (usingExternalUpdateMechanism) {
updateOptions.push({

View File

@@ -4,11 +4,6 @@
width: 290px;
}
.nameContainer {
display: flex;
justify-content: space-between;
}
.name {
@add-mixin truncate;
@@ -17,12 +12,6 @@
font-size: 24px;
}
.cloneButton {
composes: button from '~Components/Link/IconButton.css';
height: 36px;
}
.enabled {
display: flex;
flex-wrap: wrap;

View File

@@ -1,11 +1,9 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'cloneButton': string;
'enabled': string;
'list': string;
'name': string;
'nameContainer': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -2,10 +2,9 @@ import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { icons, kinds } from 'Helpers/Props';
import { kinds } from 'Helpers/Props';
import { deleteImportList } from 'Store/Actions/settingsActions';
import useTags from 'Tags/useTags';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
@@ -19,7 +18,6 @@ interface ImportListProps {
enableAutomaticAdd: boolean;
tags: number[];
minRefreshInterval: string;
onCloneImportListPress: (id: number) => void;
}
function ImportList({
@@ -28,7 +26,6 @@ function ImportList({
enableAutomaticAdd,
tags,
minRefreshInterval,
onCloneImportListPress,
}: ImportListProps) {
const dispatch = useDispatch();
const tagList = useTags();
@@ -60,26 +57,13 @@ function ImportList({
dispatch(deleteImportList({ id }));
}, [id, dispatch]);
const handleCloneImportListPress = useCallback(() => {
onCloneImportListPress(id);
}, [id, onCloneImportListPress]);
return (
<Card
className={styles.list}
overlayContent={true}
onPress={handleEditImportListPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>{name}</div>
<IconButton
className={styles.cloneButton}
title={translate('CloneImportList')}
name={icons.CLONE}
onPress={handleCloneImportListPress}
/>
</div>
<div className={styles.name}>{name}</div>
<div className={styles.enabled}>
{enableAutomaticAdd ? (

View File

@@ -7,10 +7,7 @@ import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import {
cloneImportList,
fetchImportLists,
} from 'Store/Actions/settingsActions';
import { fetchImportLists } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import ImportListModel from 'typings/ImportList';
import sortByProp from 'Utilities/Array/sortByProp';
@@ -52,14 +49,6 @@ function ImportLists() {
setIsEditImportListModalOpen(false);
}, []);
const handleCloneImportListPress = useCallback(
(id: number) => {
dispatch(cloneImportList({ id }));
setIsEditImportListModalOpen(true);
},
[dispatch]
);
useEffect(() => {
dispatch(fetchImportLists());
dispatch(fetchRootFolders());
@@ -75,13 +64,7 @@ function ImportLists() {
>
<div className={styles.lists}>
{items.map((item) => {
return (
<ImportList
key={item.id}
{...item}
onCloneImportListPress={handleCloneImportListPress}
/>
);
return <ImportList key={item.id} {...item} />;
})}
<Card className={styles.addList} onPress={handleAddImportListPress}>

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -27,7 +26,7 @@ interface ManageImportListsEditModalContentProps {
const NO_CHANGE = 'noChange';
const autoAddOptions: EnhancedSelectInputValue<string>[] = [
const autoAddOptions = [
{
key: NO_CHANGE,
get value() {

View File

@@ -8,7 +8,6 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
@@ -70,7 +69,7 @@ function TagsModalContent(props: TagsModalContentProps) {
onApplyTagsPress(tags, applyTags);
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions: EnhancedSelectInputValue<string>[] = [
const applyTagsOptions = [
{
key: 'add',
get value() {

View File

@@ -6,7 +6,6 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, kinds } from 'Helpers/Props';
@@ -20,7 +19,7 @@ import createSettingsSectionSelector from 'Store/Selectors/createSettingsSection
import translate from 'Utilities/String/translate';
const SECTION = 'importListOptions';
const cleanLibraryLevelOptions: EnhancedSelectInputValue<string>[] = [
const cleanLibraryLevelOptions = [
{
key: 'disabled',
get value() {

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
@@ -29,7 +28,7 @@ interface ManageIndexersEditModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions: EnhancedSelectInputValue<string>[] = [
const enableOptions = [
{
key: NO_CHANGE,
get value() {

View File

@@ -8,7 +8,6 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
@@ -70,7 +69,7 @@ function TagsModalContent(props: TagsModalContentProps) {
onApplyTagsPress(tags, applyTags);
}, [tags, applyTags, onApplyTagsPress]);
const applyTagsOptions: EnhancedSelectInputValue<string>[] = [
const applyTagsOptions = [
{
key: 'add',
get value() {

View File

@@ -7,7 +7,6 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -32,7 +31,7 @@ import AddRootFolder from './RootFolder/AddRootFolder';
const SECTION = 'mediaManagement';
const episodeTitleRequiredOptions: EnhancedSelectInputValue<string>[] = [
const episodeTitleRequiredOptions = [
{
key: 'always',
get value() {
@@ -53,7 +52,7 @@ const episodeTitleRequiredOptions: EnhancedSelectInputValue<string>[] = [
},
];
const rescanAfterRefreshOptions: EnhancedSelectInputValue<string>[] = [
const rescanAfterRefreshOptions = [
{
key: 'always',
get value() {
@@ -74,7 +73,7 @@ const rescanAfterRefreshOptions: EnhancedSelectInputValue<string>[] = [
},
];
const downloadPropersAndRepacksOptions: EnhancedSelectInputValue<string>[] = [
const downloadPropersAndRepacksOptions = [
{
key: 'preferAndUpgrade',
get value() {
@@ -95,7 +94,7 @@ const downloadPropersAndRepacksOptions: EnhancedSelectInputValue<string>[] = [
},
];
const fileDateOptions: EnhancedSelectInputValue<string>[] = [
const fileDateOptions = [
{
key: 'none',
get value() {
@@ -361,24 +360,6 @@ function MediaManagement() {
/>
</FormGroup>
) : null}
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('UserRejectedExtensions')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="userRejectedExtensions"
helpTexts={[
translate('UserRejectedExtensionsHelpText'),
translate('UserRejectedExtensionsTextsExamples'),
]}
onChange={handleInputChange}
{...settings.userRejectedExtensions}
/>
</FormGroup>
</FieldSet>
) : null}

View File

@@ -9,7 +9,6 @@ import FormGroup from 'Components/Form/FormGroup';
import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
@@ -170,7 +169,7 @@ function Naming() {
const replaceIllegalCharacters =
hasSettings && settings.replaceIllegalCharacters.value;
const multiEpisodeStyleOptions: EnhancedSelectInputValue<number>[] = [
const multiEpisodeStyleOptions = [
{ key: 0, value: translate('Extend'), hint: 'S01E01-02-03' },
{ key: 1, value: translate('Duplicate'), hint: 'S01E01.S01E02' },
{ key: 2, value: translate('Repeat'), hint: 'S01E01E02E03' },
@@ -179,7 +178,7 @@ function Naming() {
{ key: 5, value: translate('PrefixedRange'), hint: 'S01E01-E03' },
];
const colonReplacementOptions: EnhancedSelectInputValue<number>[] = [
const colonReplacementOptions = [
{ key: 0, value: translate('Delete') },
{ key: 1, value: translate('ReplaceWithDash') },
{ key: 2, value: translate('ReplaceWithSpaceDash') },

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react';
import FieldSet from 'Components/FieldSet';
import SelectInput, { SelectInputOption } from 'Components/Form/SelectInput';
import SelectInput from 'Components/Form/SelectInput';
import TextInput from 'Components/Form/TextInput';
import Button from 'Components/Link/Button';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
@@ -17,15 +17,7 @@ import TokenCase from './TokenCase';
import TokenSeparator from './TokenSeparator';
import styles from './NamingModal.css';
type SeparatorInputOption = Omit<SelectInputOption, 'key'> & {
key: TokenSeparator;
};
type CaseInputOption = Omit<SelectInputOption, 'key'> & {
key: TokenCase;
};
const separatorOptions: SeparatorInputOption[] = [
const separatorOptions: { key: TokenSeparator; value: string }[] = [
{
key: ' ',
get value() {
@@ -52,7 +44,7 @@ const separatorOptions: SeparatorInputOption[] = [
},
];
const caseOptions: CaseInputOption[] = [
const caseOptions: { key: TokenCase; value: string }[] = [
{
key: 'title',
get value() {

View File

@@ -7,7 +7,6 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -42,7 +41,7 @@ const newDelayProfile: DelayProfile & { [key: string]: unknown } = {
tags: [],
};
const protocolOptions: EnhancedSelectInputValue<string>[] = [
const protocolOptions = [
{
key: 'preferUsenet',
get value() {

View File

@@ -289,7 +289,7 @@ function EditQualityProfileModalContent({
});
// @ts-expect-error - actions are not typed
dispatch(setQualityProfileValue({ name: 'items', value: newItems }));
dispatch(setQualityProfileValue({ name: 'items', newItems }));
},
[items, dispatch]
);

View File

@@ -54,7 +54,7 @@
}
.createGroupButton {
composes: button from '~Components/Link/IconButton.css';
composes: buton from '~Components/Link/IconButton.css';
display: flex;
align-items: center;

View File

@@ -36,7 +36,7 @@ interface ItemProps {
preferredSize: number | null;
isInGroup?: boolean;
onCreateGroupPress?: (qualityId: number) => void;
onItemAllowedChange: (id: number, allowed: boolean) => void;
onItemAllowedChange: (id: number, allowd: boolean) => void;
}
interface GroupProps {
@@ -45,8 +45,8 @@ interface GroupProps {
items: QualityProfileQualityItem[];
qualityIndex: string;
onDeleteGroupPress: (groupId: number) => void;
onItemAllowedChange: (id: number, allowed: boolean) => void;
onGroupAllowedChange: (id: number, allowed: boolean) => void;
onItemAllowedChange: (id: number, allowd: boolean) => void;
onGroupAllowedChange: (id: number, allowd: boolean) => void;
onItemGroupNameChange: (groupId: number, name: string) => void;
}
@@ -67,9 +67,9 @@ export type QualityProfileItemDragSourceProps = CommonProps &
export interface QualityProfileItemDragSourceActionProps {
onCreateGroupPress?: (qualityId: number) => void;
onItemAllowedChange: (id: number, allowed: boolean) => void;
onItemAllowedChange: (id: number, allowd: boolean) => void;
onDeleteGroupPress: (groupId: number) => void;
onGroupAllowedChange: (id: number, allowed: boolean) => void;
onGroupAllowedChange: (id: number, allowd: boolean) => void;
onItemGroupNameChange: (groupId: number, name: string) => void;
onDragMove: (move: DragMoveState) => void;
onDragEnd: (didDrop: boolean) => void;

View File

@@ -79,7 +79,7 @@
}
.deleteGroupButton {
composes: button from '~Components/Link/IconButton.css';
composes: buton from '~Components/Link/IconButton.css';
display: flex;
align-items: center;

View File

@@ -183,7 +183,6 @@ export default function QualityProfileItemSize({
// @ts-ignore allowCross is still available in the version currently used
allowCross={false}
snapDragDisabled={true}
pearling={true}
renderThumb={thumbRenderer}
renderTrack={trackRenderer}
onChange={handleSliderChange}
@@ -244,7 +243,7 @@ export default function QualityProfileItemSize({
max={preferredSize ? preferredSize - 5 : MAX - 5}
step={0.1}
isFloat={true}
// @ts-expect-error - Typings are too loose
// @ts-expect-error - Typngs are too loose
onChange={handleMinSizeChange}
/>
<Label kind={kinds.INFO}>
@@ -262,7 +261,7 @@ export default function QualityProfileItemSize({
max={maxSize ? maxSize - 5 : MAX - 5}
step={0.1}
isFloat={true}
// @ts-expect-error - Typings are too loose
// @ts-expect-error - Typngs are too loose
onChange={handlePreferredSizeChange}
/>
@@ -281,7 +280,7 @@ export default function QualityProfileItemSize({
max={MAX}
step={0.1}
isFloat={true}
// @ts-expect-error - Typings are too loose
// @ts-expect-error - Typngs are too loose
onChange={handleMaxSizeChange}
/>

View File

@@ -55,13 +55,13 @@ function Tag({ id, label }: TagProps) {
}, []);
const handleConfirmDeleteTag = useCallback(() => {
dispatch(deleteTag({ id }));
}, [id, dispatch]);
const handleDeleteTagModalClose = useCallback(() => {
setIsDeleteTagModalOpen(false);
}, []);
const handleDeleteTagModalClose = useCallback(() => {
dispatch(deleteTag({ id }));
}, [id, dispatch]);
return (
<Card
className={styles.tag}

View File

@@ -6,7 +6,6 @@ import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
@@ -26,7 +25,7 @@ import translate from 'Utilities/String/translate';
const SECTION = 'ui';
export const firstDayOfWeekOptions: EnhancedSelectInputValue<number>[] = [
export const firstDayOfWeekOptions = [
{
key: 0,
get value() {
@@ -41,14 +40,14 @@ export const firstDayOfWeekOptions: EnhancedSelectInputValue<number>[] = [
},
];
export const weekColumnOptions: EnhancedSelectInputValue<string>[] = [
export const weekColumnOptions = [
{ key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' },
{ key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' },
{ key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' },
{ key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' },
];
const shortDateFormatOptions: EnhancedSelectInputValue<string>[] = [
const shortDateFormatOptions = [
{ key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' },
{ key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' },
{ key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' },
@@ -57,12 +56,12 @@ const shortDateFormatOptions: EnhancedSelectInputValue<string>[] = [
{ key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' },
];
const longDateFormatOptions: EnhancedSelectInputValue<string>[] = [
const longDateFormatOptions = [
{ key: 'dddd, MMMM D YYYY', value: 'Tuesday, March 25, 2014' },
{ key: 'dddd, D MMMM YYYY', value: 'Tuesday, 25 March, 2014' },
];
export const timeFormatOptions: EnhancedSelectInputValue<string>[] = [
export const timeFormatOptions = [
{ key: 'h(:mm)a', value: '5pm/5:30pm' },
{ key: 'HH:mm', value: '17:00/17:30' },
];

View File

@@ -10,10 +10,7 @@ import createTestProviderHandler, { createCancelTestProviderHandler } from 'Stor
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import getSectionState from 'Utilities/State/getSectionState';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
import updateSectionState from 'Utilities/State/updateSectionState';
import translate from 'Utilities/String/translate';
//
// Variables
@@ -36,7 +33,6 @@ export const CANCEL_TEST_IMPORT_LIST = 'settings/importLists/cancelTestImportLis
export const TEST_ALL_IMPORT_LISTS = 'settings/importLists/testAllImportLists';
export const BULK_EDIT_IMPORT_LISTS = 'settings/importLists/bulkEditImportLists';
export const BULK_DELETE_IMPORT_LISTS = 'settings/importLists/bulkDeleteImportLists';
export const CLONE_IMPORT_LIST = 'settings/importLists/cloneImportList';
//
// Action Creators
@@ -68,8 +64,6 @@ export const setImportListFieldValue = createAction(SET_IMPORT_LIST_FIELD_VALUE,
};
});
export const cloneImportList = createAction(CLONE_IMPORT_LIST);
//
// Details
@@ -133,37 +127,6 @@ export default {
return selectedSchema;
});
},
[CLONE_IMPORT_LIST]: (state, { payload }) => {
const id = payload.id;
const newState = getSectionState(state, section);
const item = newState.items.find((i) => i.id === id);
const selectedSchema = { ...item };
delete selectedSchema.id;
delete selectedSchema.name;
// Use selectedSchema so `createProviderSettingsSelector` works properly
selectedSchema.fields = selectedSchema.fields.map((field) => {
const newField = { ...field };
if (newField.privacy === 'apiKey' || newField.privacy === 'password') {
newField.value = '';
}
return newField;
});
newState.selectedSchema = selectedSchema;
const pendingChanges = { ...item, id: 0 };
delete pendingChanges.id;
pendingChanges.name = translate('DefaultNameCopiedImportList', { name: pendingChanges.name });
newState.pendingChanges = pendingChanges;
return updateSectionState(state, section, newState);
}
}

View File

@@ -31,11 +31,6 @@ export const defaultState = {
includeUnknownSeriesItems: true
},
removalOptions: {
removalMethod: 'removeFromClient',
blocklistMethod: 'doNotBlocklist'
},
status: {
isFetching: false,
isPopulated: false,
@@ -230,7 +225,6 @@ export const defaultState = {
export const persistState = [
'queue.options',
'queue.removalOptions',
'queue.paged.pageSize',
'queue.paged.sortKey',
'queue.paged.sortDirection',
@@ -263,7 +257,6 @@ export const SET_QUEUE_SORT = 'queue/setQueueSort';
export const SET_QUEUE_FILTER = 'queue/setQueueFilter';
export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption';
export const SET_QUEUE_OPTION = 'queue/setQueueOption';
export const SET_QUEUE_REMOVAL_OPTION = 'queue/setQueueRemoveOption';
export const CLEAR_QUEUE = 'queue/clearQueue';
export const GRAB_QUEUE_ITEM = 'queue/grabQueueItem';
@@ -289,7 +282,6 @@ export const setQueueSort = createThunk(SET_QUEUE_SORT);
export const setQueueFilter = createThunk(SET_QUEUE_FILTER);
export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION);
export const setQueueOption = createAction(SET_QUEUE_OPTION);
export const setQueueRemovalOption = createAction(SET_QUEUE_REMOVAL_OPTION);
export const clearQueue = createAction(CLEAR_QUEUE);
export const grabQueueItem = createThunk(GRAB_QUEUE_ITEM);
@@ -537,18 +529,6 @@ export const reducers = createHandleActions({
};
},
[SET_QUEUE_REMOVAL_OPTION]: function(state, { payload }) {
const queueRemovalOptions = state.removalOptions;
return {
...state,
removalOptions: {
...queueRemovalOptions,
...payload
}
};
},
[CLEAR_QUEUE]: createClearReducer(paged, {
isFetching: false,
isPopulated: false,

View File

@@ -377,7 +377,7 @@ export const reducers = createHandleActions({
const items = newState.items;
const index = items.findIndex((item) => item.guid === guid);
// Don't try to update if there isn't a matching item (the user closed the modal)
// Don't try to update if there isnt a matching item (the user closed the modal)
if (index >= 0) {
const item = Object.assign({}, items[index], payload);

View File

@@ -2,8 +2,8 @@ import { createSelector } from 'reselect';
import { isCommandExecuting } from 'Utilities/Command';
import createCommandSelector from './createCommandSelector';
function createCommandExecutingSelector(name: string, constraints = {}) {
return createSelector(createCommandSelector(name, constraints), (command) => {
function createCommandExecutingSelector(name: string, contraints = {}) {
return createSelector(createCommandSelector(name, contraints), (command) => {
return command ? isCommandExecuting(command) : false;
});
}

View File

@@ -2,9 +2,9 @@ import { createSelector } from 'reselect';
import { findCommand } from 'Utilities/Command';
import createCommandsSelector from './createCommandsSelector';
function createCommandSelector(name: string, constraints = {}) {
function createCommandSelector(name: string, contraints = {}) {
return createSelector(createCommandsSelector(), (commands) => {
return findCommand(commands, { name, ...constraints });
return findCommand(commands, { name, ...contraints });
});
}

View File

@@ -7,9 +7,8 @@ function formatBitrate(input: string | number) {
return '';
}
const { value, symbol } = filesize(size / 8, {
const { value, symbol } = filesize(size, {
base: 10,
bits: true,
round: 1,
output: 'object',
});

View File

@@ -1,7 +1,6 @@
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import translate from 'Utilities/String/translate';
const monitorNewItemsOptions: EnhancedSelectInputValue<string>[] = [
const monitorNewItemsOptions = [
{
key: 'all',
get value() {

View File

@@ -72,7 +72,7 @@ function Missing() {
} = useSelector((state: AppState) => state.wanted.missing);
const isSearchingForAllEpisodes = useSelector(
createCommandExecutingSelector(commandNames.MISSING_EPISODE_SEARCH)
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_EPISODE_SEARCH)
);
const isSearchingForSelectedEpisodes = useSelector(
createCommandExecutingSelector(commandNames.EPISODE_SEARCH)
@@ -155,7 +155,7 @@ function Missing() {
const handleSearchAllMissingConfirmed = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.MISSING_EPISODE_SEARCH,
name: commandNames.CUTOFF_UNMET_EPISODE_SEARCH,
commandFinished: () => {
dispatch(fetchMissing());
},
@@ -353,11 +353,11 @@ function Missing() {
<ConfirmModal
isOpen={isConfirmSearchAllModalOpen}
kind={kinds.DANGER}
title={translate('SearchForAllMissingEpisodes')}
title={translate('SearchForMissingEpisodes')}
message={
<div>
<div>
{translate('SearchForAllMissingEpisodesConfirmationCount', {
{translate('SearchForMissingEpisodesConfirmationCount', {
totalRecords,
})}
</div>

View File

@@ -8,7 +8,7 @@ window.console.debug = window.console.debug || function() {};
window.console.warn = window.console.warn || function() {};
window.console.assert = window.console.assert || function() {};
// TODO: Remove in v5, well supported in browsers
// TODO: Remove in v5, well suppoprted in browsers
if (!String.prototype.startsWith) {
Object.defineProperty(String.prototype, 'startsWith', {
enumerable: false,
@@ -21,7 +21,7 @@ if (!String.prototype.startsWith) {
});
}
// TODO: Remove in v5, well supported in browsers
// TODO: Remove in v5, well suppoprted in browsers
if (!String.prototype.endsWith) {
Object.defineProperty(String.prototype, 'endsWith', {
enumerable: false,

View File

@@ -18,6 +18,5 @@ export default interface MediaManagement {
scriptImportPath: string;
importExtraFiles: boolean;
extraFileExtensions: string;
userRejectedExtensions: string;
enableMediaInfo: boolean;
}

View File

@@ -22,24 +22,24 @@
],
"dependencies": {
"@floating-ui/react": "0.27.5",
"@fortawesome/fontawesome-free": "6.7.2",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/fontawesome-free": "6.7.1",
"@fortawesome/fontawesome-svg-core": "6.7.1",
"@fortawesome/free-regular-svg-icons": "6.7.1",
"@fortawesome/free-solid-svg-icons": "6.7.1",
"@fortawesome/react-fontawesome": "0.2.2",
"@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "8.0.7",
"@sentry/browser": "7.119.1",
"@tanstack/react-query": "5.61.0",
"@types/node": "20.16.11",
"@types/react": "18.3.21",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"classnames": "2.5.1",
"connected-react-router": "6.9.3",
"copy-to-clipboard": "3.3.3",
"element-class": "0.2.2",
"filesize": "10.1.6",
"fuse.js": "7.1.0",
"fuse.js": "7.0.0",
"history": "4.10.1",
"jdu": "1.0.0",
"jquery": "3.7.1",
@@ -64,7 +64,7 @@
"react-dom": "18.3.1",
"react-focus-lock": "2.9.4",
"react-google-recaptcha": "2.1.0",
"react-lazyload": "3.2.1",
"react-lazyload": "3.2.0",
"react-measure": "1.4.7",
"react-redux": "7.2.4",
"react-router": "5.2.0",
@@ -72,8 +72,8 @@
"react-slider": "1.1.4",
"react-tabs": "4.3.0",
"react-text-truncate": "0.19.0",
"react-use-measure": "2.1.7",
"react-window": "1.8.11",
"react-use-measure": "2.1.1",
"react-window": "1.8.10",
"redux": "4.2.1",
"redux-actions": "2.6.5",
"redux-batched-actions": "0.5.0",
@@ -82,17 +82,16 @@
"reselect": "4.1.8",
"stacktrace-js": "2.0.2",
"typescript": "5.7.2",
"use-debounce": "10.0.4",
"zustand": "5.0.3"
},
"devDependencies": {
"@babel/core": "7.27.1",
"@babel/eslint-parser": "7.27.1",
"@babel/plugin-proposal-export-default-from": "7.27.1",
"@babel/core": "7.26.0",
"@babel/eslint-parser": "7.25.9",
"@babel/plugin-proposal-export-default-from": "7.25.9",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.27.2",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1",
"@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.26.3",
"@babel/preset-typescript": "7.26.0",
"@types/lodash": "4.14.195",
"@types/mousetrap": "1.6.15",
"@types/qs": "6.9.16",
@@ -112,7 +111,7 @@
"babel-loader": "9.2.1",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.42.0",
"core-js": "3.39.0",
"css-loader": "6.7.3",
"css-modules-typescript-loader": "4.0.1",
"eslint": "8.57.1",

View File

@@ -5,7 +5,7 @@
<ItemGroup>
<PackageReference Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="134.0.6998.16500" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="111.0.5563.6400" />
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -15,7 +15,7 @@ namespace NzbDrone.Common.EnvironmentInfo
var attributes = assembly.GetCustomAttributes(true);
Branch = "unknown";
Branch = "unknow";
var config = attributes.OfType<AssemblyConfigurationAttribute>().FirstOrDefault();
if (config != null)

View File

@@ -141,7 +141,7 @@ namespace NzbDrone.Common.Http.Dispatchers
}
catch (OperationCanceledException ex) when (cts.IsCancellationRequested)
{
throw new WebException("Http request timed out", ex, WebExceptionStatus.Timeout, null);
throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null);
}
}

View File

@@ -4,27 +4,27 @@ namespace NzbDrone.Common.Instrumentation.Extensions
{
public static class LoggerExtensions
{
[MessageTemplateFormatMethod("message")]
public static void ProgressInfo(this Logger logger, string message, params object[] args)
{
LogProgressMessage(logger, LogLevel.Info, message, args);
var formattedMessage = string.Format(message, args);
LogProgressMessage(logger, LogLevel.Info, formattedMessage);
}
[MessageTemplateFormatMethod("message")]
public static void ProgressDebug(this Logger logger, string message, params object[] args)
{
LogProgressMessage(logger, LogLevel.Debug, message, args);
var formattedMessage = string.Format(message, args);
LogProgressMessage(logger, LogLevel.Debug, formattedMessage);
}
[MessageTemplateFormatMethod("message")]
public static void ProgressTrace(this Logger logger, string message, params object[] args)
{
LogProgressMessage(logger, LogLevel.Trace, message, args);
var formattedMessage = string.Format(message, args);
LogProgressMessage(logger, LogLevel.Trace, formattedMessage);
}
private static void LogProgressMessage(Logger logger, LogLevel level, string message, object[] parameters)
private static void LogProgressMessage(Logger logger, LogLevel level, string message)
{
var logEvent = new LogEventInfo(level, logger.Name, null, message, parameters);
var logEvent = new LogEventInfo(level, logger.Name, message);
logEvent.Properties.Add("Status", "");
logger.Log(logEvent);

View File

@@ -215,7 +215,6 @@ namespace NzbDrone.Common.Instrumentation
c.ForLogger("Microsoft.*").WriteToNil(LogLevel.Warn);
c.ForLogger("Microsoft.Hosting.Lifetime*").WriteToNil(LogLevel.Info);
c.ForLogger("Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware").WriteToNil(LogLevel.Fatal);
c.ForLogger("Sonarr.Http.Authentication.ApiKeyAuthenticationHandler").WriteToNil(LogLevel.Info);
});
}

View File

@@ -6,7 +6,6 @@ using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Model;
@@ -118,9 +117,7 @@ namespace NzbDrone.Common.Processes
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
RedirectStandardInput = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
RedirectStandardInput = true
};
if (environmentVariables != null)

View File

@@ -1,88 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class nzb_su_url_to_nzb_lifeFixture : MigrationTest<nzb_su_url_to_nzb_life>
{
[TestCase("Newznab", "https://api.nzb.su")]
[TestCase("Newznab", "http://api.nzb.su")]
public void should_replace_old_url(string impl, string baseUrl)
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Indexers").Row(new
{
Name = "Nzb.su",
Implementation = impl,
Settings = new NewznabSettings219
{
BaseUrl = baseUrl,
ApiPath = "/api"
}.ToJson(),
ConfigContract = impl + "Settings",
EnableInteractiveSearch = false
});
});
var items = db.Query<IndexerDefinition219>("SELECT * FROM \"Indexers\"");
items.Should().HaveCount(1);
items.First().Settings.ToObject<NewznabSettings219>().BaseUrl.Should().Be(baseUrl.Replace("su", "life"));
}
[TestCase("Newznab", "https://api.indexer.com")]
public void should_not_replace_different_url(string impl, string baseUrl)
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Indexers").Row(new
{
Name = "Indexer.com",
Implementation = impl,
Settings = new NewznabSettings219
{
BaseUrl = baseUrl,
ApiPath = "/api"
}.ToJson(),
ConfigContract = impl + "Settings",
EnableInteractiveSearch = false
});
});
var items = db.Query<IndexerDefinition219>("SELECT * FROM \"Indexers\"");
items.Should().HaveCount(1);
items.First().Settings.ToObject<NewznabSettings219>().BaseUrl.Should().Be(baseUrl);
}
}
internal class IndexerDefinition219
{
public int Id { get; set; }
public string Name { get; set; }
public JObject Settings { get; set; }
public int Priority { get; set; }
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public bool EnableRss { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
public HashSet<int> Tags { get; set; }
public int DownloadClientId { get; set; }
public int SeasonSearchMaximumSingleEpisodeAge { get; set; }
}
internal class NewznabSettings219
{
public string BaseUrl { get; set; }
public string ApiPath { get; set; }
}
}

View File

@@ -26,7 +26,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
_parsedEpisodeInfo = Builder<ParsedEpisodeInfo>.CreateNew()
.With(p => p.Quality = new QualityModel(Quality.SDTV,
new Revision(2, 0, false)))
new Revision(2, 0, 0, false)))
.With(p => p.ReleaseGroup = "Sonarr")
.Build();

View File

@@ -115,7 +115,6 @@ namespace NzbDrone.Core.Test.DiskSpace
[TestCase("/var/lib/docker")]
[TestCase("/some/place/docker/aufs")]
[TestCase("/etc/network")]
[TestCase("/Volumes/.timemachine/ABC123456-A1BC-12A3B45678C9/2025-05-13-181401.backup")]
public void should_not_check_diskspace_for_irrelevant_mounts(string path)
{
var mount = new Mock<IMount>();

View File

@@ -1,121 +0,0 @@
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Extras.Others;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class CleanupOrphanedExtraFilesFixture : DbTest<CleanupOrphanedExtraFiles, OtherExtraFile>
{
[Test]
public void should_delete_extra_files_that_dont_have_a_coresponding_series()
{
var episodeFile = Builder<EpisodeFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildNew();
Db.Insert(episodeFile);
var extraFile = Builder<OtherExtraFile>.CreateNew()
.With(m => m.EpisodeFileId = episodeFile.Id)
.BuildNew();
Db.Insert(extraFile);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_extra_files_that_have_a_coresponding_series()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
var episodeFile = Builder<EpisodeFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildNew();
Db.Insert(series);
Db.Insert(episodeFile);
var extraFile = Builder<OtherExtraFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = episodeFile.Id)
.BuildNew();
Db.Insert(extraFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
[Test]
public void should_delete_extra_files_that_dont_have_a_coresponding_episode_file()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
Db.Insert(series);
var extraFile = Builder<OtherExtraFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = 10)
.BuildNew();
Db.Insert(extraFile);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_extra_files_that_have_a_coresponding_episode_file()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
var episodeFile = Builder<EpisodeFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildNew();
Db.Insert(series);
Db.Insert(episodeFile);
var extraFile = Builder<OtherExtraFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = episodeFile.Id)
.BuildNew();
Db.Insert(extraFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
[Test]
public void should_delete_extra_files_that_have_episodefileid_of_zero()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
Db.Insert(series);
var extraFile = Builder<OtherExtraFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = 0)
.BuildNew();
Db.Insert(extraFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(0);
}
}
}

View File

@@ -1,126 +0,0 @@
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Extras.Subtitles;
using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
{
[TestFixture]
public class CleanupOrphanedSubtitleFilesFixture : DbTest<CleanupOrphanedSubtitleFiles, SubtitleFile>
{
[Test]
public void should_delete_subtitle_files_that_dont_have_a_coresponding_series()
{
var episodeFile = Builder<EpisodeFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildNew();
Db.Insert(episodeFile);
var subtitleFile = Builder<SubtitleFile>.CreateNew()
.With(m => m.EpisodeFileId = episodeFile.Id)
.With(m => m.Language = Language.English)
.BuildNew();
Db.Insert(subtitleFile);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_subtitle_files_that_have_a_coresponding_series()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
var episodeFile = Builder<EpisodeFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildNew();
Db.Insert(series);
Db.Insert(episodeFile);
var subtitleFile = Builder<SubtitleFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = episodeFile.Id)
.With(m => m.Language = Language.English)
.BuildNew();
Db.Insert(subtitleFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
[Test]
public void should_delete_subtitle_files_that_dont_have_a_coresponding_episode_file()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
Db.Insert(series);
var subtitleFile = Builder<SubtitleFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = 10)
.With(m => m.Language = Language.English)
.BuildNew();
Db.Insert(subtitleFile);
Subject.Clean();
AllStoredModels.Should().BeEmpty();
}
[Test]
public void should_not_delete_subtitle_files_that_have_a_coresponding_episode_file()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
var episodeFile = Builder<EpisodeFile>.CreateNew()
.With(h => h.Quality = new QualityModel())
.With(h => h.Languages = new List<Language> { Language.English })
.BuildNew();
Db.Insert(series);
Db.Insert(episodeFile);
var subtitleFile = Builder<SubtitleFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = episodeFile.Id)
.With(m => m.Language = Language.English)
.BuildNew();
Db.Insert(subtitleFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(1);
}
[Test]
public void should_delete_subtitle_files_that_have_episodefileid_of_zero()
{
var series = Builder<Series>.CreateNew()
.BuildNew();
Db.Insert(series);
var subtitleFile = Builder<SubtitleFile>.CreateNew()
.With(m => m.SeriesId = series.Id)
.With(m => m.EpisodeFileId = 0)
.With(m => m.Language = Language.English)
.BuildNew();
Db.Insert(subtitleFile);
Subject.Clean();
AllStoredModels.Should().HaveCount(0);
}
}
}

View File

@@ -59,7 +59,6 @@ namespace NzbDrone.Core.Test.IndexerTests
public void should_return_season_time_for_season_packs()
{
var settings = new TorznabSettings();
settings.SeedCriteria.SeasonPackSeedGoal = (int)SeasonPackSeedGoal.UseSeasonPackSeedGoal;
settings.SeedCriteria.SeasonPackSeedTime = 10;
Mocker.GetMock<ICachedIndexerSettingsProvider>()
@@ -86,71 +85,5 @@ namespace NzbDrone.Core.Test.IndexerTests
result.Should().NotBeNull();
result.SeedTime.Should().Be(TimeSpan.FromMinutes(10));
}
[Test]
public void should_return_season_ratio_for_season_packs_when_set()
{
var settings = new TorznabSettings();
settings.SeedCriteria.SeasonPackSeedGoal = (int)SeasonPackSeedGoal.UseSeasonPackSeedGoal;
settings.SeedCriteria.SeedRatio = 1.0;
settings.SeedCriteria.SeasonPackSeedRatio = 10.0;
Mocker.GetMock<ICachedIndexerSettingsProvider>()
.Setup(v => v.GetSettings(It.IsAny<int>()))
.Returns(new CachedIndexerSettings
{
FailDownloads = new HashSet<FailDownloads> { FailDownloads.Executables },
SeedCriteriaSettings = settings.SeedCriteria
});
var result = Subject.GetSeedConfiguration(new RemoteEpisode
{
Release = new ReleaseInfo
{
DownloadProtocol = DownloadProtocol.Torrent,
IndexerId = 1
},
ParsedEpisodeInfo = new ParsedEpisodeInfo
{
FullSeason = true
}
});
result.Should().NotBeNull();
result.Ratio.Should().Be(10.0);
}
[Test]
public void should_return_standard_ratio_for_season_packs_when_not_set()
{
var settings = new TorznabSettings();
settings.SeedCriteria.SeasonPackSeedGoal = (int)SeasonPackSeedGoal.UseStandardSeedGoal;
settings.SeedCriteria.SeedRatio = 1.0;
settings.SeedCriteria.SeasonPackSeedRatio = 10.0;
Mocker.GetMock<ICachedIndexerSettingsProvider>()
.Setup(v => v.GetSettings(It.IsAny<int>()))
.Returns(new CachedIndexerSettings
{
FailDownloads = new HashSet<FailDownloads> { FailDownloads.Executables },
SeedCriteriaSettings = settings.SeedCriteria
});
var result = Subject.GetSeedConfiguration(new RemoteEpisode
{
Release = new ReleaseInfo
{
DownloadProtocol = DownloadProtocol.Torrent,
IndexerId = 1
},
ParsedEpisodeInfo = new ParsedEpisodeInfo
{
FullSeason = true
}
});
result.Should().NotBeNull();
result.Ratio.Should().Be(1.0);
}
}
}

View File

@@ -43,24 +43,7 @@ namespace NzbDrone.Core.Test.Languages
new object[] { 31, Language.Slovak },
new object[] { 32, Language.Thai },
new object[] { 33, Language.PortugueseBrazil },
new object[] { 34, Language.SpanishLatino },
new object[] { 35, Language.Romanian },
new object[] { 36, Language.Latvian },
new object[] { 37, Language.Persian },
new object[] { 38, Language.Catalan },
new object[] { 39, Language.Croatian },
new object[] { 40, Language.Serbian },
new object[] { 41, Language.Bosnian },
new object[] { 42, Language.Estonian },
new object[] { 43, Language.Tamil },
new object[] { 44, Language.Indonesian },
new object[] { 45, Language.Macedonian },
new object[] { 46, Language.Slovenian },
new object[] { 47, Language.Azerbaijani },
new object[] { 48, Language.Uzbek },
new object[] { 49, Language.Malay },
new object[] { 50, Language.Urdu },
new object[] { 51, Language.Romansh }
new object[] { 34, Language.SpanishLatino }
};
public static object[] ToIntCases =
@@ -98,24 +81,7 @@ namespace NzbDrone.Core.Test.Languages
new object[] { Language.Slovak, 31 },
new object[] { Language.Thai, 32 },
new object[] { Language.PortugueseBrazil, 33 },
new object[] { Language.SpanishLatino, 34 },
new object[] { Language.Romanian, 35 },
new object[] { Language.Latvian, 36 },
new object[] { Language.Persian, 37 },
new object[] { Language.Catalan, 38 },
new object[] { Language.Croatian, 39 },
new object[] { Language.Serbian, 40 },
new object[] { Language.Bosnian, 41 },
new object[] { Language.Estonian, 42 },
new object[] { Language.Tamil, 43 },
new object[] { Language.Indonesian, 44 },
new object[] { Language.Macedonian, 45 },
new object[] { Language.Slovenian, 46 },
new object[] { Language.Azerbaijani, 47 },
new object[] { Language.Uzbek, 48 },
new object[] { Language.Malay, 49 },
new object[] { Language.Urdu, 50 },
new object[] { Language.Romansh, 51 }
new object[] { Language.SpanishLatino, 34 }
};
[Test]

View File

@@ -213,16 +213,5 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport
Subject.IsSample(_localEpisode).Should().Be(DetectSampleResult.Sample);
}
[Test]
public void should_default_to_45_minutes_if_runtime_is_zero()
{
GivenRuntime(120);
_localEpisode.Series.Runtime = 0;
_localEpisode.Episodes.First().Runtime = 0;
Subject.IsSample(_localEpisode).Should().Be(DetectSampleResult.Sample);
}
}
}

Some files were not shown because too many files have changed in this diff Show More