mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-05 13:20:20 -05:00
Compare commits
117 Commits
differenti
...
newznab-su
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78913a3e9f | ||
|
|
7fdc4d6638 | ||
|
|
309b55fe38 | ||
|
|
d6f265c7b5 | ||
|
|
e757dca038 | ||
|
|
9ebe043bd9 | ||
|
|
f055e8a3e5 | ||
|
|
8c697afa67 | ||
|
|
8d68879edd | ||
|
|
e9c82078da | ||
|
|
f0798550af | ||
|
|
d9c7838329 | ||
|
|
b00229e53c | ||
|
|
880628fb68 | ||
|
|
b09c6f0811 | ||
|
|
b376b63c9e | ||
|
|
99feaa34d2 | ||
|
|
d7f82a72c2 | ||
|
|
bd20ebfad7 | ||
|
|
71553ad67b | ||
|
|
41c39f1f28 | ||
|
|
d0066358eb | ||
|
|
6f1d461dad | ||
|
|
6ccab3cfc8 | ||
|
|
5e47cc3baa | ||
|
|
78ca30d1f8 | ||
|
|
f9d0abada3 | ||
|
|
4bdb0408f1 | ||
|
|
40ea6ce4e5 | ||
|
|
ccf33033dc | ||
|
|
996c0e9f50 | ||
|
|
8b7f9daab0 | ||
|
|
dfb6fdfbeb | ||
|
|
29d0073ee6 | ||
|
|
9cf6be32fa | ||
|
|
fee3f8150e | ||
|
|
010bbbd222 | ||
|
|
d3c3a6ebce | ||
|
|
f26344ae75 | ||
|
|
034f731308 | ||
|
|
4b50861a6b | ||
|
|
f977b8ba1b | ||
|
|
8374ebc25b | ||
|
|
71851d038c | ||
|
|
9ffcd141a5 | ||
|
|
a6f50408f2 | ||
|
|
6e43b08dab | ||
|
|
90c4791d5f | ||
|
|
030910babc | ||
|
|
59af86cea4 | ||
|
|
4cb25228b6 | ||
|
|
bf34b43094 | ||
|
|
1cdca8ef3e | ||
|
|
103b1335b9 | ||
|
|
b3d830c475 | ||
|
|
a279240335 | ||
|
|
3eed84c679 | ||
|
|
51c17fd312 | ||
|
|
70c74fc176 | ||
|
|
cfda24536c | ||
|
|
14e324ee30 | ||
|
|
32ba06ecd0 | ||
|
|
61807fede0 | ||
|
|
2a1efe5f59 | ||
|
|
0f43f8c9f6 | ||
|
|
a853c537db | ||
|
|
f9dccd6ec7 | ||
|
|
72b3b825eb | ||
|
|
818ae02a7a | ||
|
|
5ba3ff5987 | ||
|
|
e38deb3422 | ||
|
|
f35888e053 | ||
|
|
a50d256264 | ||
|
|
70165bddc8 | ||
|
|
4258e94e90 | ||
|
|
066b39032b | ||
|
|
728df146ad | ||
|
|
751a07bb40 | ||
|
|
d4ce60bd41 | ||
|
|
4b868d3f06 | ||
|
|
817d13e85c | ||
|
|
fae014c8be | ||
|
|
2fa02472ee | ||
|
|
7c3c577811 | ||
|
|
9fdf545f47 | ||
|
|
e537a2dc8f | ||
|
|
1047e71b7d | ||
|
|
415498efb3 | ||
|
|
cf08e947c4 | ||
|
|
bb872ee35b | ||
|
|
ab0d8352e8 | ||
|
|
9683b0af35 | ||
|
|
76b1130b68 | ||
|
|
5be58249f8 | ||
|
|
4d67b8ae2b | ||
|
|
66633b9b07 | ||
|
|
4728fa29ef | ||
|
|
9cb9c711be | ||
|
|
d62eea604a | ||
|
|
3185315343 | ||
|
|
e52b68ee7d | ||
|
|
f7eece32e7 | ||
|
|
c96c47af9e | ||
|
|
a5999b1410 | ||
|
|
ac1bb497ef | ||
|
|
9bd619ccfe | ||
|
|
dfbf12b711 | ||
|
|
0ae07898ba | ||
|
|
2314d0b506 | ||
|
|
2093f08a57 | ||
|
|
0a7ffb64f0 | ||
|
|
41b65abd1d | ||
|
|
0f904e0917 | ||
|
|
f8e57b0985 | ||
|
|
9e774f4026 | ||
|
|
2acc4c8865 | ||
|
|
0fcd92e441 |
113
distribution/debian/install.sh
Normal file → Executable file
113
distribution/debian/install.sh
Normal file → Executable file
@@ -6,6 +6,8 @@
|
||||
### 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,
|
||||
@@ -16,8 +18,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.3"
|
||||
scriptdate="2024-01-06"
|
||||
scriptversion="1.0.4"
|
||||
scriptdate="2025-04-05"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -49,18 +51,106 @@ if [ "$installdir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ] || [ "$bindi
|
||||
exit
|
||||
fi
|
||||
|
||||
# Prompt User
|
||||
read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty
|
||||
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
|
||||
app_uid=$(echo "$app_uid" | tr -d ' ')
|
||||
app_uid=${app_uid:-$app}
|
||||
# Prompt Group
|
||||
read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty
|
||||
|
||||
# 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
|
||||
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"
|
||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||
if ! $arg_unattended; then
|
||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||
fi
|
||||
|
||||
# Create User / Group as needed
|
||||
if [ "$app_guid" != "$app_uid" ]; then
|
||||
@@ -78,11 +168,10 @@ if ! getent group "$app_guid" | grep -qw "$app_uid"; then
|
||||
echo "Added User [$app_uid] to Group [$app_guid]"
|
||||
fi
|
||||
|
||||
# Stop the App if running
|
||||
if service --status-all | grep -Fq "$app"; then
|
||||
systemctl stop "$app"
|
||||
systemctl disable "$app".service
|
||||
echo "Stopped existing $app"
|
||||
# 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"
|
||||
fi
|
||||
|
||||
# Create Appdata Directory
|
||||
|
||||
@@ -176,7 +176,7 @@ module.exports = (env) => {
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: '3.39'
|
||||
corejs: '3.42'
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
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';
|
||||
@@ -9,6 +12,8 @@ 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';
|
||||
|
||||
@@ -30,12 +35,6 @@ interface RemoveQueueItemModalProps {
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
|
||||
type BlocklistMethod =
|
||||
| 'doNotBlocklist'
|
||||
| 'blocklistAndSearch'
|
||||
| 'blocklistOnly';
|
||||
|
||||
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
@@ -48,12 +47,13 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const multipleSelected = selectedCount && selectedCount > 1;
|
||||
|
||||
const [removalMethod, setRemovalMethod] =
|
||||
useState<RemovalMethod>('removeFromClient');
|
||||
const [blocklistMethod, setBlocklistMethod] =
|
||||
useState<BlocklistMethod>('doNotBlocklist');
|
||||
const { removalMethod, blocklistMethod } = useSelector(
|
||||
(state: AppState) => state.queue.removalOptions
|
||||
);
|
||||
|
||||
const { title, message } = useMemo(() => {
|
||||
if (!selectedCount) {
|
||||
@@ -79,7 +79,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
}, [sourceTitle, selectedCount]);
|
||||
|
||||
const removalMethodOptions = useMemo(() => {
|
||||
return [
|
||||
const options: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'removeFromClient',
|
||||
value: translate('RemoveFromDownloadClient'),
|
||||
@@ -106,10 +106,12 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
: translate('IgnoreDownloadHint'),
|
||||
},
|
||||
];
|
||||
|
||||
return options;
|
||||
}, [canChangeCategory, canIgnore, multipleSelected]);
|
||||
|
||||
const blocklistMethodOptions = useMemo(() => {
|
||||
return [
|
||||
const options: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'doNotBlocklist',
|
||||
value: translate('DoNotBlocklist'),
|
||||
@@ -131,20 +133,15 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
: translate('BlocklistOnlyHint'),
|
||||
},
|
||||
];
|
||||
|
||||
return options;
|
||||
}, [isPending, multipleSelected]);
|
||||
|
||||
const handleRemovalMethodChange = useCallback(
|
||||
({ value }: { value: RemovalMethod }) => {
|
||||
setRemovalMethod(value);
|
||||
const handleRemovalOptionInputChange = useCallback(
|
||||
({ name, value }: InputChanged) => {
|
||||
dispatch(setQueueRemovalOption({ [name]: value }));
|
||||
},
|
||||
[setRemovalMethod]
|
||||
);
|
||||
|
||||
const handleBlocklistMethodChange = useCallback(
|
||||
({ value }: { value: BlocklistMethod }) => {
|
||||
setBlocklistMethod(value);
|
||||
},
|
||||
[setBlocklistMethod]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleConfirmRemove = useCallback(() => {
|
||||
@@ -154,23 +151,11 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
blocklist: blocklistMethod !== 'doNotBlocklist',
|
||||
skipRedownload: blocklistMethod === 'blocklistOnly',
|
||||
});
|
||||
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
}, [
|
||||
removalMethod,
|
||||
blocklistMethod,
|
||||
setRemovalMethod,
|
||||
setBlocklistMethod,
|
||||
onRemovePress,
|
||||
]);
|
||||
}, [removalMethod, blocklistMethod, onRemovePress]);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
|
||||
onModalClose();
|
||||
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
|
||||
}, [onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
|
||||
@@ -193,7 +178,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
helpTextWarning={translate(
|
||||
'RemoveQueueItemRemovalMethodHelpTextWarning'
|
||||
)}
|
||||
onChange={handleRemovalMethodChange}
|
||||
onChange={handleRemovalOptionInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
@@ -211,7 +196,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
value={blocklistMethod}
|
||||
values={blocklistMethodOptions}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={handleBlocklistMethodChange}
|
||||
onChange={handleRemovalOptionInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
@@ -32,6 +32,17 @@ 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;
|
||||
@@ -39,6 +50,7 @@ interface QueueAppState {
|
||||
options: {
|
||||
includeUnknownSeriesItems: boolean;
|
||||
};
|
||||
removalOptions: RemovalOptions;
|
||||
}
|
||||
|
||||
export default QueueAppState;
|
||||
|
||||
@@ -22,9 +22,9 @@ function createIsDownloadingSelector(episodeIds: number[]) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.queue.details,
|
||||
(details) => {
|
||||
return details.items.some((item) => {
|
||||
return !!(item.episodeId && episodeIds.includes(item.episodeId));
|
||||
});
|
||||
return details.items.some(
|
||||
(item) => 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, anyQueued, anyMonitored, allAbsoluteEpisodeNumbers } =
|
||||
const { allDownloaded, anyGrabbed, anyMonitored, allAbsoluteEpisodeNumbers } =
|
||||
useMemo(() => {
|
||||
let files = 0;
|
||||
let queued = 0;
|
||||
let grabbed = 0;
|
||||
let monitored = 0;
|
||||
let absoluteEpisodeNumbers = 0;
|
||||
|
||||
@@ -73,8 +73,8 @@ function CalendarEventGroup({
|
||||
files++;
|
||||
}
|
||||
|
||||
if (event.queued) {
|
||||
queued++;
|
||||
if (event.grabbed) {
|
||||
grabbed++;
|
||||
}
|
||||
|
||||
if (series.monitored && event.monitored) {
|
||||
@@ -88,13 +88,13 @@ function CalendarEventGroup({
|
||||
|
||||
return {
|
||||
allDownloaded: files === events.length,
|
||||
anyQueued: queued > 0,
|
||||
anyGrabbed: grabbed > 0,
|
||||
anyMonitored: monitored > 0,
|
||||
allAbsoluteEpisodeNumbers: absoluteEpisodeNumbers === events.length,
|
||||
};
|
||||
}, [series, events]);
|
||||
|
||||
const anyDownloading = isDownloading || anyQueued;
|
||||
const anyDownloading = isDownloading || anyGrabbed;
|
||||
|
||||
const statusStyle = getStatusStyle(
|
||||
allDownloaded,
|
||||
|
||||
@@ -22,7 +22,12 @@ interface CalendarLinkModalContentProps {
|
||||
function CalendarLinkModalContent({
|
||||
onModalClose,
|
||||
}: CalendarLinkModalContentProps) {
|
||||
const [state, setState] = useState({
|
||||
const [state, setState] = useState<{
|
||||
unmonitored: boolean;
|
||||
premieresOnly: boolean;
|
||||
asAllDay: boolean;
|
||||
tags: number[];
|
||||
}>({
|
||||
unmonitored: false,
|
||||
premieresOnly: false,
|
||||
asAllDay: false,
|
||||
|
||||
@@ -93,9 +93,10 @@ function AutoSuggestInput<T = any>(props: AutoSuggestInputProps<T>) {
|
||||
mainAxis: true,
|
||||
}),
|
||||
size({
|
||||
apply({ rects, elements }) {
|
||||
apply({ availableHeight, elements, rects }) {
|
||||
Object.assign(elements.floating.style, {
|
||||
width: `${rects.reference.width}px`,
|
||||
minWidth: `${rects.reference.width}px`,
|
||||
maxHeight: `${Math.max(0, availableHeight)}px`,
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
? SeriesTagInputProps<V>
|
||||
: C extends 'seriesTypeSelect'
|
||||
? SeriesTypeSelectInputProps
|
||||
: C extends 'tag'
|
||||
? SeriesTagInputProps
|
||||
? SeriesTagInputProps<V>
|
||||
: 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 - tpyes are validated already */}
|
||||
{/* @ts-expect-error - types are validated already */}
|
||||
<InputComponent
|
||||
className={inputClassName}
|
||||
helpText={helpText}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -42,15 +42,10 @@
|
||||
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);
|
||||
|
||||
@@ -13,7 +13,6 @@ interface CssExports {
|
||||
'mobileCloseButton': string;
|
||||
'mobileCloseButtonContainer': string;
|
||||
'options': string;
|
||||
'optionsContainer': string;
|
||||
'optionsInnerModalBody': string;
|
||||
'optionsModal': string;
|
||||
'optionsModalBody': string;
|
||||
|
||||
@@ -14,6 +14,7 @@ import React, {
|
||||
KeyboardEvent,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
@@ -180,15 +181,21 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
|
||||
mainAxis: true,
|
||||
}),
|
||||
size({
|
||||
apply({ rects, elements }) {
|
||||
apply({ availableHeight, elements, rects }) {
|
||||
Object.assign(elements.floating.style, {
|
||||
'min-width': `${rects.reference.width}px`,
|
||||
minWidth: `${rects.reference.width}px`,
|
||||
maxHeight: `${Math.max(
|
||||
0,
|
||||
Math.min(window.innerHeight / 2, availableHeight)
|
||||
)}px`,
|
||||
});
|
||||
},
|
||||
}),
|
||||
],
|
||||
open: isOpen,
|
||||
placement: 'bottom-start',
|
||||
whileElementsMounted: autoUpdate,
|
||||
onOpenChange: setIsOpen,
|
||||
});
|
||||
|
||||
const click = useClick(context);
|
||||
@@ -214,12 +221,8 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
|
||||
}, [value, values, isMultiSelect]);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (!isOpen && onOpen) {
|
||||
onOpen();
|
||||
}
|
||||
|
||||
setIsOpen(!isOpen);
|
||||
}, [isOpen, setIsOpen, onOpen]);
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
}, []);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(newValue: ArrayElement<V>) => {
|
||||
@@ -372,6 +375,12 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
|
||||
[onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
onOpen?.();
|
||||
}
|
||||
}, [isOpen, onOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={refs.setReference} {...getReferenceProps()}>
|
||||
@@ -443,46 +452,43 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{isOpen ? (
|
||||
|
||||
{!isMobile && isOpen ? (
|
||||
<FloatingPortal id="portal-root">
|
||||
<div
|
||||
<Scroller
|
||||
ref={refs.setFloating}
|
||||
className={styles.optionsContainer}
|
||||
className={styles.options}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
{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);
|
||||
{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>
|
||||
) : null}
|
||||
</div>
|
||||
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>
|
||||
</FloatingPortal>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -88,13 +88,10 @@ function QualityProfileSelectInput({
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ value: newValue }: EnhancedSelectInputChanged<string | number>) => {
|
||||
onChange({
|
||||
name,
|
||||
value: newValue === 'noChange' ? value : newValue,
|
||||
});
|
||||
({ value }: EnhancedSelectInputChanged<string | number>) => {
|
||||
onChange({ name, value });
|
||||
},
|
||||
[name, value, onChange]
|
||||
[name, onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -21,6 +21,7 @@ const ADD_NEW_KEY = 'addNew';
|
||||
|
||||
export interface RootFolderSelectInputValue
|
||||
extends EnhancedSelectInputValue<string> {
|
||||
freeSpace?: number;
|
||||
isMissing?: boolean;
|
||||
}
|
||||
|
||||
@@ -42,66 +43,58 @@ 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,
|
||||
isMissing: false,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
isDisabled: includeNoChangeDisabled,
|
||||
return createSelector(createRootFoldersSelector(), (rootFolders) => {
|
||||
const values: RootFolderSelectInputValue[] = rootFolders.items.map(
|
||||
(rootFolder) => {
|
||||
return {
|
||||
key: rootFolder.path,
|
||||
value: rootFolder.path,
|
||||
freeSpace: rootFolder.freeSpace,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
values.push({
|
||||
key: ADD_NEW_KEY,
|
||||
value: translate('AddANewPath'),
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
isDisabled: includeNoChangeDisabled,
|
||||
isMissing: false,
|
||||
});
|
||||
|
||||
return {
|
||||
values,
|
||||
isSaving: rootFolders.isSaving,
|
||||
saveError: rootFolders.saveError,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
values.push({
|
||||
key: ADD_NEW_KEY,
|
||||
value: translate('AddANewPath'),
|
||||
});
|
||||
|
||||
return {
|
||||
values,
|
||||
isSaving: rootFolders.isSaving,
|
||||
saveError: rootFolders.saveError,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function RootFolderSelectInput({
|
||||
|
||||
@@ -18,18 +18,16 @@ interface RootFolderSelectInputOptionProps
|
||||
isWindows?: boolean;
|
||||
}
|
||||
|
||||
function RootFolderSelectInputOption(props: RootFolderSelectInputOptionProps) {
|
||||
const {
|
||||
id,
|
||||
value,
|
||||
freeSpace,
|
||||
isMissing,
|
||||
seriesFolder,
|
||||
isMobile,
|
||||
isWindows,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
function RootFolderSelectInputOption({
|
||||
id,
|
||||
value,
|
||||
freeSpace,
|
||||
isMissing,
|
||||
seriesFolder,
|
||||
isMobile,
|
||||
isWindows,
|
||||
...otherProps
|
||||
}: RootFolderSelectInputOptionProps) {
|
||||
const slashCharacter = isWindows ? '\\' : '/';
|
||||
|
||||
return (
|
||||
|
||||
@@ -30,3 +30,11 @@
|
||||
text-align: right;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
||||
.isMissing {
|
||||
flex: 0 0 auto;
|
||||
margin-left: 15px;
|
||||
color: var(--dangerColor);
|
||||
text-align: right;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'freeSpace': string;
|
||||
'isMissing': string;
|
||||
'path': string;
|
||||
'pathContainer': string;
|
||||
'selectedValue': string;
|
||||
|
||||
@@ -8,27 +8,23 @@ import styles from './RootFolderSelectInputSelectedValue.css';
|
||||
interface RootFolderSelectInputSelectedValueProps {
|
||||
selectedValue: string;
|
||||
values: RootFolderSelectInputValue[];
|
||||
freeSpace?: number;
|
||||
seriesFolder?: string;
|
||||
isWindows?: boolean;
|
||||
includeFreeSpace?: boolean;
|
||||
}
|
||||
|
||||
function RootFolderSelectInputSelectedValue(
|
||||
props: RootFolderSelectInputSelectedValueProps
|
||||
) {
|
||||
const {
|
||||
selectedValue,
|
||||
values,
|
||||
freeSpace,
|
||||
seriesFolder,
|
||||
includeFreeSpace = true,
|
||||
isWindows,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
function RootFolderSelectInputSelectedValue({
|
||||
selectedValue,
|
||||
values,
|
||||
seriesFolder,
|
||||
includeFreeSpace = true,
|
||||
isWindows,
|
||||
...otherProps
|
||||
}: RootFolderSelectInputSelectedValueProps) {
|
||||
const slashCharacter = isWindows ? '\\' : '/';
|
||||
const value = values.find((v) => v.key === selectedValue)?.value;
|
||||
const { value, freeSpace, isMissing } =
|
||||
values.find((v) => v.key === selectedValue) ||
|
||||
({} as RootFolderSelectInputValue);
|
||||
|
||||
return (
|
||||
<EnhancedSelectInputSelectedValue
|
||||
@@ -53,6 +49,10 @@ function RootFolderSelectInputSelectedValue(
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isMissing ? (
|
||||
<div className={styles.isMissing}>{translate('Missing')}</div>
|
||||
) : null}
|
||||
</EnhancedSelectInputSelectedValue>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
import React, { SyntheticEvent } from 'react';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import EnhancedSelectInput, {
|
||||
EnhancedSelectInputValue,
|
||||
} from './EnhancedSelectInput';
|
||||
import styles from './UMaskInput.css';
|
||||
|
||||
const umaskOptions = [
|
||||
const umaskOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: '755',
|
||||
get value() {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { ChangeEvent, SyntheticEvent, useCallback } from 'react';
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
ComponentProps,
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import styles from './SelectInput.css';
|
||||
|
||||
interface SelectInputOption {
|
||||
export interface SelectInputOption
|
||||
extends Pick<ComponentProps<'option'>, 'disabled'> {
|
||||
key: string | number;
|
||||
value: string | number | (() => string | number);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } 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 {
|
||||
export interface SeriesTagInputProps<V> {
|
||||
name: string;
|
||||
value: number[];
|
||||
onChange: (change: InputChanged<number[]>) => void;
|
||||
value: V;
|
||||
onChange: (change: InputChanged<V>) => void;
|
||||
}
|
||||
|
||||
const VALID_TAG_REGEX = new RegExp('[^-_a-z0-9]', 'i');
|
||||
@@ -59,28 +59,48 @@ function createSeriesTagsSelector(tags: number[]) {
|
||||
});
|
||||
}
|
||||
|
||||
export default function SeriesTagInput({
|
||||
export default function SeriesTagInput<V extends number | number[]>({
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
}: SeriesTagInputProps) {
|
||||
}: SeriesTagInputProps<V>) {
|
||||
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(value)
|
||||
createSeriesTagsSelector(arrayValue)
|
||||
);
|
||||
|
||||
const handleTagCreated = useCallback(
|
||||
(tag: SeriesTag) => {
|
||||
onChange({ name, value: [...value, tag.id] });
|
||||
if (isArray) {
|
||||
onChange({ name, value: [...value, tag.id] as V });
|
||||
} else {
|
||||
onChange({
|
||||
name,
|
||||
value: tag.id as V,
|
||||
});
|
||||
}
|
||||
},
|
||||
[name, value, onChange]
|
||||
[name, value, isArray, onChange]
|
||||
);
|
||||
|
||||
const handleTagAdd = useCallback(
|
||||
(newTag: SeriesTag) => {
|
||||
if (newTag.id) {
|
||||
onChange({ name, value: [...value, newTag.id] });
|
||||
if (isArray) {
|
||||
onChange({ name, value: [...value, newTag.id] as V });
|
||||
} else {
|
||||
onChange({ name, value: newTag.id as V });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -96,17 +116,21 @@ export default function SeriesTagInput({
|
||||
);
|
||||
}
|
||||
},
|
||||
[name, value, allTags, handleTagCreated, onChange, dispatch]
|
||||
[name, value, isArray, allTags, handleTagCreated, onChange, dispatch]
|
||||
);
|
||||
|
||||
const handleTagDelete = useCallback(
|
||||
({ index }: { index: number }) => {
|
||||
const newValue = value.slice();
|
||||
newValue.splice(index, 1);
|
||||
if (isArray) {
|
||||
const newValue = value.slice();
|
||||
newValue.splice(index, 1);
|
||||
|
||||
onChange({ name, value: newValue });
|
||||
onChange({ name, value: newValue as V });
|
||||
} else {
|
||||
onChange({ name, value: 0 as V });
|
||||
}
|
||||
},
|
||||
[name, value, onChange]
|
||||
[name, value, isArray, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
RenderSuggestion,
|
||||
SuggestionsFetchRequestedParams,
|
||||
} from 'react-autosuggest';
|
||||
import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import AutoSuggestInput from '../AutoSuggestInput';
|
||||
|
||||
@@ -58,16 +58,6 @@ 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]);
|
||||
@@ -118,8 +108,31 @@ 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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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 an suggestion is not selected go to the first series,
|
||||
// If a suggestion is not selected go to the first series,
|
||||
// otherwise go to the selected series.
|
||||
|
||||
const selectedSuggestion =
|
||||
|
||||
@@ -359,34 +359,37 @@ 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: 0,
|
||||
});
|
||||
}
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
touchStartX.current = null;
|
||||
touchStartY.current = null;
|
||||
}, []);
|
||||
touchStartX.current = null;
|
||||
touchStartY.current = null;
|
||||
},
|
||||
[isSidebarVisible]
|
||||
);
|
||||
|
||||
const handleTouchCancel = useCallback(() => {
|
||||
touchStartX.current = null;
|
||||
|
||||
@@ -80,8 +80,12 @@ function PageToolbarSection({
|
||||
if (buttonCount - 1 === maxButtons) {
|
||||
const overflowItems: PageToolbarButtonProps[] = [];
|
||||
|
||||
const buttonsWithoutSeparators = validChildren.filter(
|
||||
(child) => Object.keys(child.props).length > 0
|
||||
);
|
||||
|
||||
return {
|
||||
buttons: validChildren,
|
||||
buttons: buttonsWithoutSeparators,
|
||||
buttonCount,
|
||||
overflowItems,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import SelectInput, { SelectInputOption } 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) => {
|
||||
return Array.from(new Array(totalPages), (_x, i): SelectInputOption => {
|
||||
const pageNumber = i + 1;
|
||||
|
||||
return {
|
||||
|
||||
@@ -22,10 +22,6 @@ interface Episode extends ModelBase {
|
||||
monitored: boolean;
|
||||
grabbed?: boolean;
|
||||
unverifiedSceneNumbering: boolean;
|
||||
endTime?: string;
|
||||
grabDate?: string;
|
||||
seriesTitle?: string;
|
||||
queued?: boolean;
|
||||
series?: Series;
|
||||
finaleType?: string;
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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,
|
||||
]);
|
||||
}
|
||||
@@ -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 from 'Components/Form/SelectInput';
|
||||
import SelectInput, { SelectInputOption } 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 = [
|
||||
const importModeOptions: SelectInputOption[] = [
|
||||
{
|
||||
key: 'chooseImportMode',
|
||||
value: () => translate('ChooseImportMode'),
|
||||
@@ -343,7 +343,7 @@ function InteractiveImportModalContent(
|
||||
}
|
||||
);
|
||||
|
||||
const options = [
|
||||
const options: SelectInputOption[] = [
|
||||
{
|
||||
key: 'select',
|
||||
value: translate('SelectDropdown'),
|
||||
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
@@ -69,7 +70,7 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
|
||||
);
|
||||
|
||||
const qualityOptions = useMemo(() => {
|
||||
return items.map(({ id, name }) => {
|
||||
return items.map(({ id, name }): EnhancedSelectInputValue<number> => {
|
||||
return {
|
||||
key: id,
|
||||
value: name,
|
||||
|
||||
@@ -82,9 +82,9 @@ function RootFolderRow(props: RootFolderRowProps) {
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteRootFolder')}
|
||||
message={translate('DeleteRootFolderMessageText', { path })}
|
||||
confirmLabel={translate('Delete')}
|
||||
title={translate('RemoveRootFolder')}
|
||||
message={translate('RemoveRootFolderWithSeriesMessageText', { path })}
|
||||
confirmLabel={translate('Remove')}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={onDeleteModalClose}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
.header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 425px;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
@@ -30,20 +29,18 @@
|
||||
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 {
|
||||
@@ -59,10 +56,13 @@
|
||||
}
|
||||
|
||||
.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,6 +170,8 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
max-height: calc(3 * 30px);
|
||||
font-weight: 300;
|
||||
font-size: 30px;
|
||||
line-height: 30px;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -21,7 +20,6 @@ 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,
|
||||
@@ -56,7 +54,6 @@ 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';
|
||||
@@ -75,9 +72,6 @@ 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;
|
||||
}
|
||||
@@ -246,7 +240,6 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
||||
allCollapsed: false,
|
||||
seasons: {},
|
||||
});
|
||||
const [overviewRef, { height: overviewHeight }] = useMeasure();
|
||||
const wasRefreshing = usePrevious(isRefreshing);
|
||||
const wasRenaming = usePrevious(isRenaming);
|
||||
|
||||
@@ -523,7 +516,7 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('SeriesMonitoring')}
|
||||
label={translate('EpisodeMonitoring')}
|
||||
iconName={icons.MONITORED}
|
||||
onPress={handleMonitorOptionsPress}
|
||||
/>
|
||||
@@ -796,16 +789,7 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div ref={overviewRef} className={styles.overview}>
|
||||
<TextTruncate
|
||||
line={
|
||||
Math.floor(
|
||||
overviewHeight / (defaultFontSize * lineHeight)
|
||||
) - 1
|
||||
}
|
||||
text={overview}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.overview}>{overview}</div>
|
||||
|
||||
<MetadataAttribution />
|
||||
</div>
|
||||
|
||||
@@ -562,6 +562,7 @@ function SeriesDetailsSeason({
|
||||
|
||||
<SeasonInteractiveSearchModal
|
||||
isOpen={isInteractiveSearchModalOpen}
|
||||
episodeCount={totalEpisodeCount}
|
||||
seriesId={seriesId}
|
||||
seasonNumber={seasonNumber}
|
||||
onModalClose={handleInteractiveSearchModalClose}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
@@ -14,7 +15,7 @@ import { setSeriesOverviewOption } from 'Store/Actions/seriesIndexActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectOverviewOptions from '../selectOverviewOptions';
|
||||
|
||||
const posterSizeOptions = [
|
||||
const posterSizeOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'small',
|
||||
get value() {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.grid {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
&:hover {
|
||||
.content {
|
||||
background-color: var(--tableRowHoverBackgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,17 +80,17 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) {
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
const posterWidth = useMemo(() => {
|
||||
const maxiumPosterWidth = isSmallScreen ? 152 : 162;
|
||||
const maximumPosterWidth = isSmallScreen ? 152 : 162;
|
||||
|
||||
if (posterSize === 'large') {
|
||||
return maxiumPosterWidth;
|
||||
return maximumPosterWidth;
|
||||
}
|
||||
|
||||
if (posterSize === 'medium') {
|
||||
return Math.floor(maxiumPosterWidth * 0.75);
|
||||
return Math.floor(maximumPosterWidth * 0.75);
|
||||
}
|
||||
|
||||
return Math.floor(maxiumPosterWidth * 0.5);
|
||||
return Math.floor(maximumPosterWidth * 0.5);
|
||||
}, [posterSize, isSmallScreen]);
|
||||
|
||||
const posterHeight = useMemo(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ 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';
|
||||
@@ -14,7 +15,7 @@ import selectPosterOptions from 'Series/Index/Posters/selectPosterOptions';
|
||||
import { setSeriesPosterOption } from 'Store/Actions/seriesIndexActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const posterSizeOptions = [
|
||||
const posterSizeOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'small',
|
||||
get value() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { SyntheticEvent, 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';
|
||||
@@ -122,8 +123,31 @@ 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`,
|
||||
@@ -175,7 +199,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Link className={styles.link} style={elementStyle} to={link}>
|
||||
<Link className={styles.link} style={elementStyle} {...linkProps}>
|
||||
<SeriesPoster
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
@@ -31,7 +32,7 @@ interface EditSeriesModalContentProps {
|
||||
|
||||
const NO_CHANGE = 'noChange';
|
||||
|
||||
const monitoredOptions = [
|
||||
const monitoredOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: NO_CHANGE,
|
||||
get value() {
|
||||
@@ -53,7 +54,7 @@ const monitoredOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
const seasonFolderOptions = [
|
||||
const seasonFolderOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: NO_CHANGE,
|
||||
get value() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -11,7 +12,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, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ChangeMonitoringModalContent.css';
|
||||
|
||||
@@ -46,9 +47,12 @@ function ChangeMonitoringModalContent(
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('MonitorSeries')}</ModalHeader>
|
||||
<ModalHeader>{translate('MonitorEpisodes')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>{translate('MonitorEpisodesModalInfo')}</div>
|
||||
</Alert>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
@@ -66,7 +67,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
onApplyTagsPress(tags, applyTags);
|
||||
}, [tags, applyTags, onApplyTagsPress]);
|
||||
|
||||
const applyTagsOptions = [
|
||||
const applyTagsOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'add',
|
||||
value: translate('Add'),
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
@@ -15,7 +16,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, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { updateSeriesMonitor } from 'Store/Actions/seriesActions';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -66,9 +67,12 @@ function MonitoringOptionsModalContent({
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('MonitorSeries')}</ModalHeader>
|
||||
<ModalHeader>{translate('MonitorEpisodes')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>{translate('MonitorEpisodesModalInfo')}</div>
|
||||
</Alert>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
|
||||
@@ -6,19 +6,19 @@ import {
|
||||
cancelFetchReleases,
|
||||
clearReleases,
|
||||
} from 'Store/Actions/releaseActions';
|
||||
import SeasonInteractiveSearchModalContent from './SeasonInteractiveSearchModalContent';
|
||||
import SeasonInteractiveSearchModalContent, {
|
||||
SeasonInteractiveSearchModalContentProps,
|
||||
} from './SeasonInteractiveSearchModalContent';
|
||||
|
||||
interface SeasonInteractiveSearchModalProps {
|
||||
interface SeasonInteractiveSearchModalProps
|
||||
extends SeasonInteractiveSearchModalContentProps {
|
||||
isOpen: boolean;
|
||||
seriesId: number;
|
||||
seasonNumber: number;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function SeasonInteractiveSearchModal(
|
||||
props: SeasonInteractiveSearchModalProps
|
||||
) {
|
||||
const { isOpen, seriesId, seasonNumber, onModalClose } = props;
|
||||
const { isOpen, episodeCount, seriesId, seasonNumber, onModalClose } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -44,6 +44,7 @@ function SeasonInteractiveSearchModal(
|
||||
onModalClose={handleModalClose}
|
||||
>
|
||||
<SeasonInteractiveSearchModalContent
|
||||
episodeCount={episodeCount}
|
||||
seriesId={seriesId}
|
||||
seasonNumber={seasonNumber}
|
||||
onModalClose={handleModalClose}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.modalFooter {
|
||||
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||
|
||||
justify-content: space-between;
|
||||
}
|
||||
7
frontend/src/Series/Search/SeasonInteractiveSearchModalContent.css.d.ts
vendored
Normal file
7
frontend/src/Series/Search/SeasonInteractiveSearchModalContent.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'modalFooter': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -8,18 +8,21 @@ 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';
|
||||
|
||||
interface SeasonInteractiveSearchModalContentProps {
|
||||
export interface SeasonInteractiveSearchModalContentProps {
|
||||
episodeCount: number;
|
||||
seriesId: number;
|
||||
seasonNumber: number;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function SeasonInteractiveSearchModalContent(
|
||||
props: SeasonInteractiveSearchModalContentProps
|
||||
) {
|
||||
const { seriesId, seasonNumber, onModalClose } = props;
|
||||
|
||||
function SeasonInteractiveSearchModalContent({
|
||||
episodeCount,
|
||||
seriesId,
|
||||
seasonNumber,
|
||||
onModalClose,
|
||||
}: SeasonInteractiveSearchModalContentProps) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
@@ -40,7 +43,13 @@ function SeasonInteractiveSearchModalContent(
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<div>
|
||||
{translate('EpisodesInSeason', {
|
||||
episodeCount,
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
@@ -23,7 +24,7 @@ interface ManageCustomFormatsEditModalContentProps {
|
||||
|
||||
const NO_CHANGE = 'noChange';
|
||||
|
||||
const enableOptions = [
|
||||
const enableOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: NO_CHANGE,
|
||||
get value() {
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 +28,7 @@ interface ManageDownloadClientsEditModalContentProps {
|
||||
|
||||
const NO_CHANGE = 'noChange';
|
||||
|
||||
const enableOptions = [
|
||||
const enableOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: NO_CHANGE,
|
||||
get value() {
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -71,7 +72,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
onApplyTagsPress(tags, applyTags);
|
||||
}, [tags, applyTags, onApplyTagsPress]);
|
||||
|
||||
const applyTagsOptions = [
|
||||
const applyTagsOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'add',
|
||||
get value() {
|
||||
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
@@ -10,7 +11,7 @@ import { PendingSection } from 'typings/pending';
|
||||
import General from 'typings/Settings/General';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const logLevelOptions = [
|
||||
const logLevelOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'info',
|
||||
get value() {
|
||||
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
@@ -32,7 +33,7 @@ function ProxySettings({
|
||||
proxyBypassLocalAddresses,
|
||||
onInputChange,
|
||||
}: ProxySettingsProps) {
|
||||
const proxyTypeOptions = [
|
||||
const proxyTypeOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'http',
|
||||
value: translate('HttpHttps'),
|
||||
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
@@ -16,7 +17,7 @@ import { PendingSection } from 'typings/pending';
|
||||
import General from 'typings/Settings/General';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
export const authenticationMethodOptions = [
|
||||
export const authenticationMethodOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'none',
|
||||
get value() {
|
||||
@@ -47,22 +48,23 @@ export const authenticationMethodOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
export const authenticationRequiredOptions = [
|
||||
{
|
||||
key: 'enabled',
|
||||
get value() {
|
||||
return translate('Enabled');
|
||||
export const authenticationRequiredOptions: EnhancedSelectInputValue<string>[] =
|
||||
[
|
||||
{
|
||||
key: 'enabled',
|
||||
get value() {
|
||||
return translate('Enabled');
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'disabledForLocalAddresses',
|
||||
get value() {
|
||||
return translate('DisabledForLocalAddresses');
|
||||
{
|
||||
key: 'disabledForLocalAddresses',
|
||||
get value() {
|
||||
return translate('DisabledForLocalAddresses');
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
];
|
||||
|
||||
const certificateValidationOptions = [
|
||||
const certificateValidationOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'enabled',
|
||||
get value() {
|
||||
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
@@ -38,7 +39,7 @@ function UpdateSettings({
|
||||
|
||||
const usingExternalUpdateMechanism = packageUpdateMechanism !== 'builtIn';
|
||||
|
||||
const updateOptions = [];
|
||||
const updateOptions: EnhancedSelectInputValue<string>[] = [];
|
||||
|
||||
if (usingExternalUpdateMechanism) {
|
||||
updateOptions.push({
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
width: 290px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
@@ -12,6 +17,12 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cloneButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.enabled {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// 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;
|
||||
|
||||
@@ -2,9 +2,10 @@ 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 { kinds } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { deleteImportList } from 'Store/Actions/settingsActions';
|
||||
import useTags from 'Tags/useTags';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
@@ -18,6 +19,7 @@ interface ImportListProps {
|
||||
enableAutomaticAdd: boolean;
|
||||
tags: number[];
|
||||
minRefreshInterval: string;
|
||||
onCloneImportListPress: (id: number) => void;
|
||||
}
|
||||
|
||||
function ImportList({
|
||||
@@ -26,6 +28,7 @@ function ImportList({
|
||||
enableAutomaticAdd,
|
||||
tags,
|
||||
minRefreshInterval,
|
||||
onCloneImportListPress,
|
||||
}: ImportListProps) {
|
||||
const dispatch = useDispatch();
|
||||
const tagList = useTags();
|
||||
@@ -57,13 +60,26 @@ 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.name}>{name}</div>
|
||||
<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.enabled}>
|
||||
{enableAutomaticAdd ? (
|
||||
|
||||
@@ -7,7 +7,10 @@ import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchImportLists } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
cloneImportList,
|
||||
fetchImportLists,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import ImportListModel from 'typings/ImportList';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
@@ -49,6 +52,14 @@ function ImportLists() {
|
||||
setIsEditImportListModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleCloneImportListPress = useCallback(
|
||||
(id: number) => {
|
||||
dispatch(cloneImportList({ id }));
|
||||
setIsEditImportListModalOpen(true);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchImportLists());
|
||||
dispatch(fetchRootFolders());
|
||||
@@ -64,7 +75,13 @@ function ImportLists() {
|
||||
>
|
||||
<div className={styles.lists}>
|
||||
{items.map((item) => {
|
||||
return <ImportList key={item.id} {...item} />;
|
||||
return (
|
||||
<ImportList
|
||||
key={item.id}
|
||||
{...item}
|
||||
onCloneImportListPress={handleCloneImportListPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Card className={styles.addList} onPress={handleAddImportListPress}>
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
@@ -26,7 +27,7 @@ interface ManageImportListsEditModalContentProps {
|
||||
|
||||
const NO_CHANGE = 'noChange';
|
||||
|
||||
const autoAddOptions = [
|
||||
const autoAddOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: NO_CHANGE,
|
||||
get value() {
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -69,7 +70,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
onApplyTagsPress(tags, applyTags);
|
||||
}, [tags, applyTags, onApplyTagsPress]);
|
||||
|
||||
const applyTagsOptions = [
|
||||
const applyTagsOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'add',
|
||||
get value() {
|
||||
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
@@ -19,7 +20,7 @@ import createSettingsSectionSelector from 'Store/Selectors/createSettingsSection
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const SECTION = 'importListOptions';
|
||||
const cleanLibraryLevelOptions = [
|
||||
const cleanLibraryLevelOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'disabled',
|
||||
get value() {
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 +29,7 @@ interface ManageIndexersEditModalContentProps {
|
||||
|
||||
const NO_CHANGE = 'noChange';
|
||||
|
||||
const enableOptions = [
|
||||
const enableOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: NO_CHANGE,
|
||||
get value() {
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -69,7 +70,7 @@ function TagsModalContent(props: TagsModalContentProps) {
|
||||
onApplyTagsPress(tags, applyTags);
|
||||
}, [tags, applyTags, onApplyTagsPress]);
|
||||
|
||||
const applyTagsOptions = [
|
||||
const applyTagsOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'add',
|
||||
get value() {
|
||||
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
@@ -31,7 +32,7 @@ import AddRootFolder from './RootFolder/AddRootFolder';
|
||||
|
||||
const SECTION = 'mediaManagement';
|
||||
|
||||
const episodeTitleRequiredOptions = [
|
||||
const episodeTitleRequiredOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'always',
|
||||
get value() {
|
||||
@@ -52,7 +53,7 @@ const episodeTitleRequiredOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
const rescanAfterRefreshOptions = [
|
||||
const rescanAfterRefreshOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'always',
|
||||
get value() {
|
||||
@@ -73,7 +74,7 @@ const rescanAfterRefreshOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
const downloadPropersAndRepacksOptions = [
|
||||
const downloadPropersAndRepacksOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'preferAndUpgrade',
|
||||
get value() {
|
||||
@@ -94,7 +95,7 @@ const downloadPropersAndRepacksOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
const fileDateOptions = [
|
||||
const fileDateOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'none',
|
||||
get value() {
|
||||
@@ -360,6 +361,24 @@ 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}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ 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';
|
||||
@@ -169,7 +170,7 @@ function Naming() {
|
||||
const replaceIllegalCharacters =
|
||||
hasSettings && settings.replaceIllegalCharacters.value;
|
||||
|
||||
const multiEpisodeStyleOptions = [
|
||||
const multiEpisodeStyleOptions: EnhancedSelectInputValue<number>[] = [
|
||||
{ key: 0, value: translate('Extend'), hint: 'S01E01-02-03' },
|
||||
{ key: 1, value: translate('Duplicate'), hint: 'S01E01.S01E02' },
|
||||
{ key: 2, value: translate('Repeat'), hint: 'S01E01E02E03' },
|
||||
@@ -178,7 +179,7 @@ function Naming() {
|
||||
{ key: 5, value: translate('PrefixedRange'), hint: 'S01E01-E03' },
|
||||
];
|
||||
|
||||
const colonReplacementOptions = [
|
||||
const colonReplacementOptions: EnhancedSelectInputValue<number>[] = [
|
||||
{ key: 0, value: translate('Delete') },
|
||||
{ key: 1, value: translate('ReplaceWithDash') },
|
||||
{ key: 2, value: translate('ReplaceWithSpaceDash') },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import SelectInput, { SelectInputOption } from 'Components/Form/SelectInput';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
@@ -17,7 +17,15 @@ import TokenCase from './TokenCase';
|
||||
import TokenSeparator from './TokenSeparator';
|
||||
import styles from './NamingModal.css';
|
||||
|
||||
const separatorOptions: { key: TokenSeparator; value: string }[] = [
|
||||
type SeparatorInputOption = Omit<SelectInputOption, 'key'> & {
|
||||
key: TokenSeparator;
|
||||
};
|
||||
|
||||
type CaseInputOption = Omit<SelectInputOption, 'key'> & {
|
||||
key: TokenCase;
|
||||
};
|
||||
|
||||
const separatorOptions: SeparatorInputOption[] = [
|
||||
{
|
||||
key: ' ',
|
||||
get value() {
|
||||
@@ -44,7 +52,7 @@ const separatorOptions: { key: TokenSeparator; value: string }[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const caseOptions: { key: TokenCase; value: string }[] = [
|
||||
const caseOptions: CaseInputOption[] = [
|
||||
{
|
||||
key: 'title',
|
||||
get value() {
|
||||
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
@@ -41,7 +42,7 @@ const newDelayProfile: DelayProfile & { [key: string]: unknown } = {
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const protocolOptions = [
|
||||
const protocolOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'preferUsenet',
|
||||
get value() {
|
||||
|
||||
@@ -289,7 +289,7 @@ function EditQualityProfileModalContent({
|
||||
});
|
||||
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setQualityProfileValue({ name: 'items', newItems }));
|
||||
dispatch(setQualityProfileValue({ name: 'items', value: newItems }));
|
||||
},
|
||||
[items, dispatch]
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
}
|
||||
|
||||
.createGroupButton {
|
||||
composes: buton from '~Components/Link/IconButton.css';
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -36,7 +36,7 @@ interface ItemProps {
|
||||
preferredSize: number | null;
|
||||
isInGroup?: boolean;
|
||||
onCreateGroupPress?: (qualityId: number) => void;
|
||||
onItemAllowedChange: (id: number, allowd: boolean) => void;
|
||||
onItemAllowedChange: (id: number, allowed: boolean) => void;
|
||||
}
|
||||
|
||||
interface GroupProps {
|
||||
@@ -45,8 +45,8 @@ interface GroupProps {
|
||||
items: QualityProfileQualityItem[];
|
||||
qualityIndex: string;
|
||||
onDeleteGroupPress: (groupId: number) => void;
|
||||
onItemAllowedChange: (id: number, allowd: boolean) => void;
|
||||
onGroupAllowedChange: (id: number, allowd: boolean) => void;
|
||||
onItemAllowedChange: (id: number, allowed: boolean) => void;
|
||||
onGroupAllowedChange: (id: number, allowed: 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, allowd: boolean) => void;
|
||||
onItemAllowedChange: (id: number, allowed: boolean) => void;
|
||||
onDeleteGroupPress: (groupId: number) => void;
|
||||
onGroupAllowedChange: (id: number, allowd: boolean) => void;
|
||||
onGroupAllowedChange: (id: number, allowed: boolean) => void;
|
||||
onItemGroupNameChange: (groupId: number, name: string) => void;
|
||||
onDragMove: (move: DragMoveState) => void;
|
||||
onDragEnd: (didDrop: boolean) => void;
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
}
|
||||
|
||||
.deleteGroupButton {
|
||||
composes: buton from '~Components/Link/IconButton.css';
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -183,6 +183,7 @@ 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}
|
||||
@@ -243,7 +244,7 @@ export default function QualityProfileItemSize({
|
||||
max={preferredSize ? preferredSize - 5 : MAX - 5}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
// @ts-expect-error - Typngs are too loose
|
||||
// @ts-expect-error - Typings are too loose
|
||||
onChange={handleMinSizeChange}
|
||||
/>
|
||||
<Label kind={kinds.INFO}>
|
||||
@@ -261,7 +262,7 @@ export default function QualityProfileItemSize({
|
||||
max={maxSize ? maxSize - 5 : MAX - 5}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
// @ts-expect-error - Typngs are too loose
|
||||
// @ts-expect-error - Typings are too loose
|
||||
onChange={handlePreferredSizeChange}
|
||||
/>
|
||||
|
||||
@@ -280,7 +281,7 @@ export default function QualityProfileItemSize({
|
||||
max={MAX}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
// @ts-expect-error - Typngs are too loose
|
||||
// @ts-expect-error - Typings are too loose
|
||||
onChange={handleMaxSizeChange}
|
||||
/>
|
||||
|
||||
|
||||
@@ -55,13 +55,13 @@ function Tag({ id, label }: TagProps) {
|
||||
}, []);
|
||||
|
||||
const handleConfirmDeleteTag = useCallback(() => {
|
||||
setIsDeleteTagModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleDeleteTagModalClose = useCallback(() => {
|
||||
dispatch(deleteTag({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
const handleDeleteTagModalClose = useCallback(() => {
|
||||
setIsDeleteTagModalOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.tag}
|
||||
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
@@ -25,7 +26,7 @@ import translate from 'Utilities/String/translate';
|
||||
|
||||
const SECTION = 'ui';
|
||||
|
||||
export const firstDayOfWeekOptions = [
|
||||
export const firstDayOfWeekOptions: EnhancedSelectInputValue<number>[] = [
|
||||
{
|
||||
key: 0,
|
||||
get value() {
|
||||
@@ -40,14 +41,14 @@ export const firstDayOfWeekOptions = [
|
||||
},
|
||||
];
|
||||
|
||||
export const weekColumnOptions = [
|
||||
export const weekColumnOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{ 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 = [
|
||||
const shortDateFormatOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{ 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' },
|
||||
@@ -56,12 +57,12 @@ const shortDateFormatOptions = [
|
||||
{ key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' },
|
||||
];
|
||||
|
||||
const longDateFormatOptions = [
|
||||
const longDateFormatOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{ key: 'dddd, MMMM D YYYY', value: 'Tuesday, March 25, 2014' },
|
||||
{ key: 'dddd, D MMMM YYYY', value: 'Tuesday, 25 March, 2014' },
|
||||
];
|
||||
|
||||
export const timeFormatOptions = [
|
||||
export const timeFormatOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{ key: 'h(:mm)a', value: '5pm/5:30pm' },
|
||||
{ key: 'HH:mm', value: '17:00/17:30' },
|
||||
];
|
||||
|
||||
@@ -10,7 +10,10 @@ 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
|
||||
@@ -33,6 +36,7 @@ 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
|
||||
@@ -64,6 +68,8 @@ export const setImportListFieldValue = createAction(SET_IMPORT_LIST_FIELD_VALUE,
|
||||
};
|
||||
});
|
||||
|
||||
export const cloneImportList = createAction(CLONE_IMPORT_LIST);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
@@ -127,6 +133,37 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,11 @@ export const defaultState = {
|
||||
includeUnknownSeriesItems: true
|
||||
},
|
||||
|
||||
removalOptions: {
|
||||
removalMethod: 'removeFromClient',
|
||||
blocklistMethod: 'doNotBlocklist'
|
||||
},
|
||||
|
||||
status: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
@@ -225,6 +230,7 @@ export const defaultState = {
|
||||
|
||||
export const persistState = [
|
||||
'queue.options',
|
||||
'queue.removalOptions',
|
||||
'queue.paged.pageSize',
|
||||
'queue.paged.sortKey',
|
||||
'queue.paged.sortDirection',
|
||||
@@ -257,6 +263,7 @@ 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';
|
||||
@@ -282,6 +289,7 @@ 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);
|
||||
@@ -529,6 +537,18 @@ 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,
|
||||
|
||||
@@ -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 isnt a matching item (the user closed the modal)
|
||||
// Don't try to update if there isn't a matching item (the user closed the modal)
|
||||
|
||||
if (index >= 0) {
|
||||
const item = Object.assign({}, items[index], payload);
|
||||
|
||||
@@ -2,8 +2,8 @@ import { createSelector } from 'reselect';
|
||||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
import createCommandSelector from './createCommandSelector';
|
||||
|
||||
function createCommandExecutingSelector(name: string, contraints = {}) {
|
||||
return createSelector(createCommandSelector(name, contraints), (command) => {
|
||||
function createCommandExecutingSelector(name: string, constraints = {}) {
|
||||
return createSelector(createCommandSelector(name, constraints), (command) => {
|
||||
return command ? isCommandExecuting(command) : false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import { createSelector } from 'reselect';
|
||||
import { findCommand } from 'Utilities/Command';
|
||||
import createCommandsSelector from './createCommandsSelector';
|
||||
|
||||
function createCommandSelector(name: string, contraints = {}) {
|
||||
function createCommandSelector(name: string, constraints = {}) {
|
||||
return createSelector(createCommandsSelector(), (commands) => {
|
||||
return findCommand(commands, { name, ...contraints });
|
||||
return findCommand(commands, { name, ...constraints });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,9 @@ function formatBitrate(input: string | number) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { value, symbol } = filesize(size, {
|
||||
const { value, symbol } = filesize(size / 8, {
|
||||
base: 10,
|
||||
bits: true,
|
||||
round: 1,
|
||||
output: 'object',
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const monitorNewItemsOptions = [
|
||||
const monitorNewItemsOptions: EnhancedSelectInputValue<string>[] = [
|
||||
{
|
||||
key: 'all',
|
||||
get value() {
|
||||
|
||||
@@ -72,7 +72,7 @@ function Missing() {
|
||||
} = useSelector((state: AppState) => state.wanted.missing);
|
||||
|
||||
const isSearchingForAllEpisodes = useSelector(
|
||||
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_EPISODE_SEARCH)
|
||||
createCommandExecutingSelector(commandNames.MISSING_EPISODE_SEARCH)
|
||||
);
|
||||
const isSearchingForSelectedEpisodes = useSelector(
|
||||
createCommandExecutingSelector(commandNames.EPISODE_SEARCH)
|
||||
@@ -155,7 +155,7 @@ function Missing() {
|
||||
const handleSearchAllMissingConfirmed = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.CUTOFF_UNMET_EPISODE_SEARCH,
|
||||
name: commandNames.MISSING_EPISODE_SEARCH,
|
||||
commandFinished: () => {
|
||||
dispatch(fetchMissing());
|
||||
},
|
||||
@@ -353,11 +353,11 @@ function Missing() {
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmSearchAllModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('SearchForMissingEpisodes')}
|
||||
title={translate('SearchForAllMissingEpisodes')}
|
||||
message={
|
||||
<div>
|
||||
<div>
|
||||
{translate('SearchForMissingEpisodesConfirmationCount', {
|
||||
{translate('SearchForAllMissingEpisodesConfirmationCount', {
|
||||
totalRecords,
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -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 suppoprted in browsers
|
||||
// TODO: Remove in v5, well supported 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 suppoprted in browsers
|
||||
// TODO: Remove in v5, well supported in browsers
|
||||
if (!String.prototype.endsWith) {
|
||||
Object.defineProperty(String.prototype, 'endsWith', {
|
||||
enumerable: false,
|
||||
|
||||
@@ -18,5 +18,6 @@ export default interface MediaManagement {
|
||||
scriptImportPath: string;
|
||||
importExtraFiles: boolean;
|
||||
extraFileExtensions: string;
|
||||
userRejectedExtensions: string;
|
||||
enableMediaInfo: boolean;
|
||||
}
|
||||
|
||||
33
package.json
33
package.json
@@ -22,24 +22,24 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "0.27.5",
|
||||
"@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/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/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.12",
|
||||
"@types/react": "18.3.21",
|
||||
"@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.0.0",
|
||||
"fuse.js": "7.1.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.0",
|
||||
"react-lazyload": "3.2.1",
|
||||
"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.1",
|
||||
"react-window": "1.8.10",
|
||||
"react-use-measure": "2.1.7",
|
||||
"react-window": "1.8.11",
|
||||
"redux": "4.2.1",
|
||||
"redux-actions": "2.6.5",
|
||||
"redux-batched-actions": "0.5.0",
|
||||
@@ -82,16 +82,17 @@
|
||||
"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.26.0",
|
||||
"@babel/eslint-parser": "7.25.9",
|
||||
"@babel/plugin-proposal-export-default-from": "7.25.9",
|
||||
"@babel/core": "7.27.1",
|
||||
"@babel/eslint-parser": "7.27.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.27.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.26.0",
|
||||
"@babel/preset-react": "7.26.3",
|
||||
"@babel/preset-typescript": "7.26.0",
|
||||
"@babel/preset-env": "7.27.2",
|
||||
"@babel/preset-react": "7.27.1",
|
||||
"@babel/preset-typescript": "7.27.1",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/mousetrap": "1.6.15",
|
||||
"@types/qs": "6.9.16",
|
||||
@@ -111,7 +112,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.39.0",
|
||||
"core-js": "3.42.0",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.57.1",
|
||||
|
||||
@@ -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="111.0.5563.6400" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="134.0.6998.16500" />
|
||||
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.556">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
|
||||
var attributes = assembly.GetCustomAttributes(true);
|
||||
|
||||
Branch = "unknow";
|
||||
Branch = "unknown";
|
||||
|
||||
var config = attributes.OfType<AssemblyConfigurationAttribute>().FirstOrDefault();
|
||||
if (config != null)
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace NzbDrone.Common.Extensions
|
||||
{
|
||||
public static class DateTimeExtensions
|
||||
{
|
||||
public static readonly DateTime EpochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
public static bool InNextDays(this DateTime dateTime, int days)
|
||||
{
|
||||
return InNext(dateTime, new TimeSpan(days, 0, 0, 0));
|
||||
@@ -43,5 +45,10 @@ namespace NzbDrone.Common.Extensions
|
||||
{
|
||||
return dateTime.AddTicks(-(dateTime.Ticks % TimeSpan.TicksPerSecond));
|
||||
}
|
||||
|
||||
public static DateTime WithTicksFrom(this DateTime dateTime, DateTime other)
|
||||
{
|
||||
return dateTime.WithoutTicks().AddTicks(other.Ticks % TimeSpan.TicksPerSecond);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
}
|
||||
catch (OperationCanceledException ex) when (cts.IsCancellationRequested)
|
||||
{
|
||||
throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null);
|
||||
throw new WebException("Http request timed out", ex, WebExceptionStatus.Timeout, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -390,5 +390,12 @@ namespace NzbDrone.Common.Http
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public virtual HttpRequestBuilder AllowRedirect(bool allowAutoRedirect = true)
|
||||
{
|
||||
AllowAutoRedirect = allowAutoRedirect;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
var formattedMessage = string.Format(message, args);
|
||||
LogProgressMessage(logger, LogLevel.Info, formattedMessage);
|
||||
LogProgressMessage(logger, LogLevel.Info, message, args);
|
||||
}
|
||||
|
||||
[MessageTemplateFormatMethod("message")]
|
||||
public static void ProgressDebug(this Logger logger, string message, params object[] args)
|
||||
{
|
||||
var formattedMessage = string.Format(message, args);
|
||||
LogProgressMessage(logger, LogLevel.Debug, formattedMessage);
|
||||
LogProgressMessage(logger, LogLevel.Debug, message, args);
|
||||
}
|
||||
|
||||
[MessageTemplateFormatMethod("message")]
|
||||
public static void ProgressTrace(this Logger logger, string message, params object[] args)
|
||||
{
|
||||
var formattedMessage = string.Format(message, args);
|
||||
LogProgressMessage(logger, LogLevel.Trace, formattedMessage);
|
||||
LogProgressMessage(logger, LogLevel.Trace, message, args);
|
||||
}
|
||||
|
||||
private static void LogProgressMessage(Logger logger, LogLevel level, string message)
|
||||
private static void LogProgressMessage(Logger logger, LogLevel level, string message, object[] parameters)
|
||||
{
|
||||
var logEvent = new LogEventInfo(level, logger.Name, message);
|
||||
var logEvent = new LogEventInfo(level, logger.Name, null, message, parameters);
|
||||
logEvent.Properties.Add("Status", "");
|
||||
|
||||
logger.Log(logEvent);
|
||||
|
||||
@@ -215,6 +215,7 @@ 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -117,7 +118,9 @@ namespace NzbDrone.Common.Processes
|
||||
UseShellExecute = false,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardInput = true
|
||||
RedirectStandardInput = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8
|
||||
};
|
||||
|
||||
if (environmentVariables != null)
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
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; }
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,7 @@ 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>();
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"TvrageID":"4055",
|
||||
"ImdbID":"0320037",
|
||||
"InfoHash":"123",
|
||||
"Tags": ["Subtitles"],
|
||||
"DownloadURL":"https:\/\/broadcasthe.net\/torrents.php?action=download&id=123&authkey=123&torrent_pass=123"
|
||||
},
|
||||
"1234":{
|
||||
@@ -54,8 +55,9 @@
|
||||
"TvrageID":"38472",
|
||||
"ImdbID":"2377081",
|
||||
"InfoHash":"1234",
|
||||
"Tags": [],
|
||||
"DownloadURL":"https:\/\/broadcasthe.net\/torrents.php?action=download&id=1234&authkey=1234&torrent_pass=1234"
|
||||
}},
|
||||
"results":"117927"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,5 +124,34 @@
|
||||
<newznab:attr name="nuked" value="0"/>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>title</title>
|
||||
<guid isPermaLink="true">subs=eng</guid>
|
||||
<link>link</link>
|
||||
<comments>comments</comments>
|
||||
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
||||
<category>category</category>
|
||||
<description>description</description>
|
||||
<enclosure url="url" length="500" type="application/x-nzb"/>
|
||||
|
||||
<newznab:attr name="haspretime" value="0"/>
|
||||
<newznab:attr name="nuked" value="0"/>
|
||||
<newznab:attr name="subs" value="Eng"/>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>title</title>
|
||||
<guid isPermaLink="true">subs=''</guid>
|
||||
<link>link</link>
|
||||
<comments>comments</comments>
|
||||
<pubDate>Sat, 31 Aug 2024 12:28:40 +0300</pubDate>
|
||||
<category>category</category>
|
||||
<description>description</description>
|
||||
<enclosure url="url" length="500" type="application/x-nzb"/>
|
||||
|
||||
<newznab:attr name="haspretime" value="0"/>
|
||||
<newznab:attr name="nuked" value="0"/>
|
||||
<newznab:attr name="subs" value=""/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -0,0 +1,121 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user