mirror of https://github.com/lidarr/Lidarr
Compare commits
94 Commits
v2.2.1.407
...
develop
Author | SHA1 | Date |
---|---|---|
Bogdan | d27b062d6a | |
Bogdan | febb3ef485 | |
sillock1 | 30d9891bf0 | |
Bogdan | 2621acdae5 | |
Bogdan | 18772553f2 | |
Weblate | 0300bf2dd2 | |
Bogdan | 77861e4303 | |
Bogdan | 3545a7451e | |
Mark McDowall | 9f8c4530ca | |
Mickaël Thomas | 9da690f807 | |
Bogdan | 31f342b8ad | |
Bogdan | 596a36d45f | |
Bogdan | 94bb8a436b | |
Weblate | 94d2a20b6a | |
Bogdan | a25e5aae10 | |
Mark McDowall | f4a02ffc83 | |
Bogdan | 1bdcf91014 | |
Bogdan | 4d28d3f25a | |
Bogdan | 9660ec37cd | |
Bogdan | 66c7521f4b | |
Stevie Robinson | 8b57b33c99 | |
Mark McDowall | 580e4becbe | |
Christopher | 5f248aa25e | |
Bogdan | a735eccb65 | |
Weblate | d11ed42830 | |
Bogdan | b0038dd143 | |
Bogdan | 2e242aeb7b | |
Bogdan | 416d505316 | |
Bogdan | 4816f35256 | |
Josh McKinney | e42e0a72eb | |
Bogdan | db9e62f79d | |
Bogdan | bc69fa4842 | |
Bogdan | 86dad72c49 | |
Bogdan | 4a8d6c367d | |
Bogdan | c1926f8758 | |
Bogdan | 7820bcf91f | |
Servarr | 431ad0a028 | |
Bogdan | 59cf7a95c3 | |
Bogdan | e17e3633f8 | |
Bogdan | 46da2b49c6 | |
Josh McKinney | 3071977284 | |
Mark McDowall | b14e2bb618 | |
Mark McDowall | 8c09c0cb5c | |
Weblate | 8cebb21c2d | |
Bogdan | 74ac263b74 | |
Bogdan | adcec90ef8 | |
Qstick | daf8b94c8e | |
Bogdan | 7c4f0c597e | |
Bogdan | 1d2af2aab4 | |
Stevie Robinson | 5d537689fb | |
Cuki | ca6beea62b | |
Mark McDowall | a82c919093 | |
Mark McDowall | 2941e0c4b7 | |
Bogdan | ca0b900d92 | |
Weblate | 72f1b2075b | |
Bogdan | e847828191 | |
Bogdan | 2a10505dff | |
Mark McDowall | 28f2eb974d | |
Servarr | 13ce040e4d | |
Weblate | f477f9b287 | |
Bogdan | 0e84008669 | |
Louis R | 52b5ff6fdd | |
Carlos Gustavo Sarmiento | 1d0de51917 | |
Stevie Robinson | a8648fdb71 | |
Bogdan | f890a8c18f | |
Mark McDowall | e730cf6307 | |
Bogdan | 9f4d821a2d | |
Weblate | ce6e4555ec | |
Mark McDowall | 55eaecb3c8 | |
Bogdan | 63e36f71d2 | |
Bogdan | 89e184e768 | |
Mark McDowall | 873a225f0c | |
Stevie Robinson | b81170d911 | |
Servarr | 5ffde40320 | |
Mark McDowall | ebfa68087d | |
Bogdan | 1db0eb1029 | |
Bogdan | 967b58017a | |
Bogdan | 3754d611c7 | |
Weblate | 8035d4202f | |
Bogdan | 468f3acf85 | |
Bogdan | 29c77ec3a1 | |
bpoxy | d04bb5333a | |
Bogdan | 0d76fbcf0d | |
Weblate | 3df140b1f0 | |
Bogdan | 340ae78f46 | |
Helvio Pedreschi | 881fabad93 | |
Mark McDowall | be8f7e5618 | |
Bogdan | c9743448fd | |
Bogdan | 47e647ddb1 | |
Mark McDowall | f6529d5ad3 | |
Mark McDowall | fb1b7274d0 | |
Louis R | 33b12a532c | |
nopoz | cea5ee503f | |
Bogdan | 475590a21b |
|
@ -0,0 +1,13 @@
|
|||
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||
// the frontend has vscode settings that are distinct from the backend
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "../frontend"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||
{
|
||||
"name": "Lidarr",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [8686],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for more information:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# https://containers.dev/guide/dependabot
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
|
@ -126,6 +126,7 @@ coverage*.xml
|
|||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
# VS outout folders
|
||||
bin
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||
"name": "Run Lidarr",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build dotnet",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/_output/net6.0/Lidarr",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build dotnet",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"msbuild",
|
||||
"-restore",
|
||||
"${workspaceFolder}/src/Lidarr.sln",
|
||||
"-p:GenerateFullPaths=true",
|
||||
"-p:Configuration=Debug",
|
||||
"-p:Platform=Posix",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/src/Lidarr.sln",
|
||||
"-property:GenerateFullPaths=true",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Lidarr.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -9,13 +9,13 @@ variables:
|
|||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '2.2.1'
|
||||
majorVersion: '2.4.0'
|
||||
minorVersion: $[counter('minorVersion', 1076)]
|
||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.417'
|
||||
dotnetVersion: '6.0.421'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
|
@ -166,10 +166,10 @@ stages:
|
|||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
- task: UseNode@1
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
version: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
|
@ -1093,10 +1093,10 @@ stages:
|
|||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
- task: UseNode@1
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: $(nodeVersion)
|
||||
version: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
|
|
|
@ -28,7 +28,8 @@ module.exports = {
|
|||
globals: {
|
||||
expect: false,
|
||||
chai: false,
|
||||
sinon: false
|
||||
sinon: false,
|
||||
JSX: true
|
||||
},
|
||||
|
||||
parserOptions: {
|
||||
|
|
|
@ -53,7 +53,7 @@ class DeleteAlbumModalContent extends Component {
|
|||
render() {
|
||||
const {
|
||||
title,
|
||||
statistics,
|
||||
statistics = {},
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
|
|
|
@ -35,3 +35,9 @@
|
|||
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
.indexerFlags {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 50px;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ interface CssExports {
|
|||
'audio': string;
|
||||
'customFormatScore': string;
|
||||
'duration': string;
|
||||
'indexerFlags': string;
|
||||
'monitored': string;
|
||||
'size': string;
|
||||
'status': string;
|
||||
|
|
|
@ -2,15 +2,19 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import AlbumFormats from 'Album/AlbumFormats';
|
||||
import EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
|
||||
import IndexerFlags from 'Album/IndexerFlags';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
|
||||
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import TrackActionsCell from './TrackActionsCell';
|
||||
import styles from './TrackRow.css';
|
||||
|
||||
|
@ -32,6 +36,7 @@ class TrackRow extends Component {
|
|||
trackFileSize,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
indexerFlags,
|
||||
columns,
|
||||
deleteTrackFile
|
||||
} = this.props;
|
||||
|
@ -141,12 +146,30 @@ class TrackRow extends Component {
|
|||
customFormats.length
|
||||
)}
|
||||
tooltip={<AlbumFormats formats={customFormats} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexerFlags') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.indexerFlags}
|
||||
>
|
||||
{indexerFlags ? (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||
title={translate('IndexerFlags')}
|
||||
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
@ -208,12 +231,14 @@ TrackRow.propTypes = {
|
|||
trackFileSize: PropTypes.number,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
indexerFlags: PropTypes.number.isRequired,
|
||||
mediaInfo: PropTypes.object,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
TrackRow.defaultProps = {
|
||||
customFormats: []
|
||||
customFormats: [],
|
||||
indexerFlags: 0
|
||||
};
|
||||
|
||||
export default TrackRow;
|
||||
|
|
|
@ -13,7 +13,8 @@ function createMapStateToProps() {
|
|||
trackFilePath: trackFile ? trackFile.path : null,
|
||||
trackFileSize: trackFile ? trackFile.size : null,
|
||||
customFormats: trackFile ? trackFile.customFormats : [],
|
||||
customFormatScore: trackFile ? trackFile.customFormatScore : 0
|
||||
customFormatScore: trackFile ? trackFile.customFormatScore : 0,
|
||||
indexerFlags: trackFile ? trackFile.indexerFlags : 0
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -35,7 +35,7 @@ class EditAlbumModalContent extends Component {
|
|||
title,
|
||||
artistName,
|
||||
albumType,
|
||||
statistics,
|
||||
statistics = {},
|
||||
item,
|
||||
isSaving,
|
||||
onInputChange,
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import createIndexerFlagsSelector from 'Store/Selectors/createIndexerFlagsSelector';
|
||||
|
||||
interface IndexerFlagsProps {
|
||||
indexerFlags: number;
|
||||
}
|
||||
|
||||
function IndexerFlags({ indexerFlags = 0 }: IndexerFlagsProps) {
|
||||
const allIndexerFlags = useSelector(createIndexerFlagsSelector);
|
||||
|
||||
const flags = allIndexerFlags.items.filter(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
(item) => (indexerFlags & item.id) === item.id
|
||||
);
|
||||
|
||||
return flags.length ? (
|
||||
<ul>
|
||||
{flags.map((flag, index) => {
|
||||
return <li key={index}>{flag.name}</li>;
|
||||
})}
|
||||
</ul>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default IndexerFlags;
|
|
@ -15,7 +15,7 @@ function AlbumInteractiveSearchModal(props) {
|
|||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
|
|
@ -7,6 +7,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function AlbumInteractiveSearchModalContent(props) {
|
||||
const {
|
||||
|
@ -18,7 +19,10 @@ function AlbumInteractiveSearchModalContent(props) {
|
|||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Interactive Search {albumId != null && `- ${albumTitle}`}
|
||||
{albumTitle === undefined ?
|
||||
translate('InteractiveSearchModalHeader') :
|
||||
translate('InteractiveSearchModalHeaderTitle', { title: albumTitle })
|
||||
}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
|
@ -32,7 +36,7 @@ function AlbumInteractiveSearchModalContent(props) {
|
|||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
|
|
@ -12,11 +12,10 @@ function App({ store, history }) {
|
|||
<DocumentTitle title={window.Lidarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<ApplyTheme>
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ApplyTheme>
|
||||
<ApplyTheme />
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</DocumentTitle>
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.ui.item.theme || window.Lidarr.theme,
|
||||
(
|
||||
theme
|
||||
) => {
|
||||
return {
|
||||
theme
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ theme, children }) {
|
||||
// Update the CSS Variables
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
||||
|
||||
// Loop through each array key and set the CSS Variables
|
||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
||||
// Based on our snippet from MDN
|
||||
document.documentElement.style.setProperty(
|
||||
`--${cssVariableKey}`,
|
||||
arrayOfVariableValues[index]
|
||||
);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables(theme);
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
ApplyTheme.propTypes = {
|
||||
theme: PropTypes.string.isRequired,
|
||||
children: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(ApplyTheme);
|
|
@ -0,0 +1,37 @@
|
|||
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
import AppState from './State/AppState';
|
||||
|
||||
interface ApplyThemeProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function createThemeSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.ui.item.theme || window.Lidarr.theme,
|
||||
(theme) => {
|
||||
return theme;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ children }: ApplyThemeProps) {
|
||||
const theme = useSelector(createThemeSelector());
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
Object.entries(themes[theme]).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables();
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
export default ApplyTheme;
|
|
@ -1,9 +1,11 @@
|
|||
import AlbumAppState from './AlbumAppState';
|
||||
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
import QueueAppState from './QueueAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
import TrackFilesAppState from './TrackFilesAppState';
|
||||
import TracksAppState from './TracksAppState';
|
||||
|
@ -54,12 +56,14 @@ interface AppState {
|
|||
artist: ArtistAppState;
|
||||
artistIndex: ArtistIndexAppState;
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
history: HistoryAppState;
|
||||
queue: QueueAppState;
|
||||
settings: SettingsAppState;
|
||||
tags: TagsAppState;
|
||||
trackFiles: TrackFilesAppState;
|
||||
tracksSelection: TracksAppState;
|
||||
system: SystemAppState;
|
||||
}
|
||||
|
||||
export default AppState;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Command from 'Commands/Command';
|
||||
|
||||
export type CommandAppState = AppSectionState<Command>;
|
||||
|
||||
export default CommandAppState;
|
|
@ -1,11 +1,13 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import IndexerFlag from 'typings/IndexerFlag';
|
||||
import MetadataProfile from 'typings/MetadataProfile';
|
||||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
|
@ -44,17 +46,19 @@ export interface RootFolderAppState
|
|||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
downloadClients: DownloadClientAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
metadataProfiles: MetadataProfilesAppState;
|
||||
notifications: NotificationAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
rootFolders: RootFolderAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import SystemStatus from 'typings/SystemStatus';
|
||||
import { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
|
||||
interface SystemAppState {
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
|
@ -1,12 +1,32 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
||||
export interface Tag extends ModelBase {
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
|
||||
export interface TagDetail extends ModelBase {
|
||||
label: string;
|
||||
autoTagIds: number[];
|
||||
delayProfileIds: number[];
|
||||
downloadClientIds: [];
|
||||
importListIds: number[];
|
||||
indexerIds: number[];
|
||||
notificationIds: number[];
|
||||
restrictionIds: number[];
|
||||
artistIds: number[];
|
||||
}
|
||||
|
||||
export interface TagDetailAppState
|
||||
extends AppSectionState<TagDetail>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
|
||||
details: TagDetailAppState;
|
||||
}
|
||||
|
||||
export default TagsAppState;
|
||||
|
|
|
@ -135,14 +135,14 @@ class DeleteArtistModalContent extends Component {
|
|||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onDeleteArtistConfirmed}
|
||||
>
|
||||
Delete
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
@ -161,9 +161,7 @@ DeleteArtistModalContent.propTypes = {
|
|||
};
|
||||
|
||||
DeleteArtistModalContent.defaultProps = {
|
||||
statistics: {
|
||||
trackFileCount: 0
|
||||
}
|
||||
statistics: {}
|
||||
};
|
||||
|
||||
export default DeleteArtistModalContent;
|
||||
|
|
|
@ -192,7 +192,7 @@ class ArtistDetails extends Component {
|
|||
artistName,
|
||||
ratings,
|
||||
path,
|
||||
statistics,
|
||||
statistics = {},
|
||||
qualityProfileId,
|
||||
monitored,
|
||||
genres,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import _ from 'lodash';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Statistics } from 'Album/Album';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
@ -56,7 +57,7 @@ function AlbumDetails(props: AlbumDetailsProps) {
|
|||
disambiguation,
|
||||
albumType,
|
||||
monitored,
|
||||
statistics,
|
||||
statistics = {} as Statistics,
|
||||
isSaving = false,
|
||||
} = album;
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ const monitoredOptions = [
|
|||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'monitored',
|
||||
|
|
|
@ -14,7 +14,7 @@ function ArtistInteractiveSearchModal(props) {
|
|||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
|
|
@ -13,6 +13,7 @@ export interface CommandBody {
|
|||
trigger: string;
|
||||
suppressMessages: boolean;
|
||||
artistId?: number;
|
||||
artistIds?: number[];
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import TagInputConnector from './TagInputConnector';
|
||||
|
||||
interface ArtistTagInputProps {
|
||||
name: string;
|
||||
value: number | number[];
|
||||
onChange: ({
|
||||
name,
|
||||
value,
|
||||
}: {
|
||||
name: string;
|
||||
value: number | number[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export default function ArtistTagInput(props: ArtistTagInputProps) {
|
||||
const { value, onChange, ...otherProps } = props;
|
||||
const isArray = Array.isArray(value);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ name, value: newValue }: { name: string; value: number[] }) => {
|
||||
if (isArray) {
|
||||
onChange({ name, value: newValue });
|
||||
} else {
|
||||
onChange({
|
||||
name,
|
||||
value: newValue.length ? newValue[newValue.length - 1] : 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
[isArray, onChange]
|
||||
);
|
||||
|
||||
let finalValue: number[] = [];
|
||||
|
||||
if (isArray) {
|
||||
finalValue = value;
|
||||
} else if (value === 0) {
|
||||
finalValue = [];
|
||||
} else {
|
||||
finalValue = [value];
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore 2786 'TagInputConnector' isn't typed yet
|
||||
<TagInputConnector
|
||||
{...otherProps}
|
||||
value={finalValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
.isDisabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.dropdownArrowContainer {
|
||||
|
|
|
@ -4,6 +4,7 @@ import Link from 'Components/Link/Link';
|
|||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AlbumReleaseSelectInputConnector from './AlbumReleaseSelectInputConnector';
|
||||
import ArtistTagInput from './ArtistTagInput';
|
||||
import AutoCompleteInput from './AutoCompleteInput';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
|
@ -12,6 +13,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
|
|||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||
import FormInputHelpText from './FormInputHelpText';
|
||||
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
|
||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
|
||||
|
@ -83,6 +85,9 @@ function getComponent(type) {
|
|||
case inputTypes.INDEXER_SELECT:
|
||||
return IndexerSelectInputConnector;
|
||||
|
||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||
return IndexerFlagsSelectInput;
|
||||
|
||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
||||
return DownloadClientSelectInputConnector;
|
||||
|
||||
|
@ -95,6 +100,9 @@ function getComponent(type) {
|
|||
case inputTypes.DYNAMIC_SELECT:
|
||||
return EnhancedSelectInputConnector;
|
||||
|
||||
case inputTypes.ARTIST_TAG:
|
||||
return ArtistTagInput;
|
||||
|
||||
case inputTypes.SERIES_TYPE_SELECT:
|
||||
return SeriesTypeSelectInput;
|
||||
|
||||
|
@ -292,6 +300,7 @@ FormInputGroup.propTypes = {
|
|||
includeNoChangeDisabled: PropTypes.bool,
|
||||
includeNone: PropTypes.bool,
|
||||
selectedValueOptions: PropTypes.object,
|
||||
indexerFlags: PropTypes.number,
|
||||
pending: PropTypes.bool,
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
const selectIndexerFlagsValues = (selectedFlags: number) =>
|
||||
createSelector(
|
||||
(state: AppState) => state.settings.indexerFlags,
|
||||
(indexerFlags) => {
|
||||
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if ((selectedFlags & id) === id) {
|
||||
acc.push(id);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const values = indexerFlags.items.map(({ id, name }) => ({
|
||||
key: id,
|
||||
value: name,
|
||||
}));
|
||||
|
||||
return {
|
||||
value,
|
||||
values,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
interface IndexerFlagsSelectInputProps {
|
||||
name: string;
|
||||
indexerFlags: number;
|
||||
onChange(payload: object): void;
|
||||
}
|
||||
|
||||
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
|
||||
const { indexerFlags, onChange } = props;
|
||||
|
||||
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
|
||||
|
||||
const onChangeWrapper = useCallback(
|
||||
({ name, value }: { name: string; value: number[] }) => {
|
||||
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
|
||||
|
||||
onChange({ name, value: indexerFlags });
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...props}
|
||||
value={value}
|
||||
values={values}
|
||||
onChange={onChangeWrapper}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default IndexerFlagsSelectInput;
|
|
@ -38,7 +38,7 @@ function createMapStateToProps() {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled
|
||||
isDisabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ function createMapStateToProps() {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ function MonitorAlbumsSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled
|
||||
isDisabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ function MonitorAlbumsSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ function MonitorNewItemsSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled
|
||||
isDisabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ function MonitorNewItemsSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
.input {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
font-family: $passwordFamily;
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './PasswordInput.css';
|
||||
|
||||
// Prevent a user from copying (or cutting) the password from the input
|
||||
function onCopy(e) {
|
||||
|
@ -13,17 +11,14 @@ function PasswordInput(props) {
|
|||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
type="password"
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
PasswordInput.propTypes = {
|
||||
className: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
PasswordInput.defaultProps = {
|
||||
className: styles.input
|
||||
...TextInput.props
|
||||
};
|
||||
|
||||
export default PasswordInput;
|
||||
|
|
|
@ -29,6 +29,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||
return inputTypes.DYNAMIC_SELECT;
|
||||
}
|
||||
return inputTypes.SELECT;
|
||||
case 'artistTag':
|
||||
return inputTypes.ARTIST_TAG;
|
||||
case 'tag':
|
||||
return inputTypes.TEXT_TAG;
|
||||
case 'tagSelect':
|
||||
|
|
|
@ -26,7 +26,7 @@ function createMapStateToProps() {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled
|
||||
isDisabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ function createMapStateToProps() {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ function SeriesTypeSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: includeNoChangeDisabled
|
||||
isDisabled: includeNoChangeDisabled
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ function SeriesTypeSelectInput(props) {
|
|||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,13 @@
|
|||
width: 1280px;
|
||||
}
|
||||
|
||||
|
||||
.extraExtraLarge {
|
||||
composes: modal;
|
||||
|
||||
width: 1600px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointExtraLarge) {
|
||||
.modal.extraLarge {
|
||||
width: 90%;
|
||||
|
@ -90,7 +97,8 @@
|
|||
.modal.small,
|
||||
.modal.medium,
|
||||
.modal.large,
|
||||
.modal.extraLarge {
|
||||
.modal.extraLarge,
|
||||
.modal.extraExtraLarge {
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'extraExtraLarge': string;
|
||||
'extraLarge': string;
|
||||
'large': string;
|
||||
'medium': string;
|
||||
|
|
|
@ -6,7 +6,14 @@ import { createSelector } from 'reselect';
|
|||
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||
import { fetchArtist } from 'Store/Actions/artistActions';
|
||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||
import { fetchImportLists, fetchLanguages, fetchMetadataProfiles, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
fetchImportLists,
|
||||
fetchIndexerFlags,
|
||||
fetchLanguages,
|
||||
fetchMetadataProfiles,
|
||||
fetchQualityProfiles,
|
||||
fetchUISettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import { fetchTags } from 'Store/Actions/tagActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
|
@ -44,6 +51,7 @@ const selectAppProps = createSelector(
|
|||
);
|
||||
|
||||
const selectIsPopulated = createSelector(
|
||||
(state) => state.artist.isPopulated,
|
||||
(state) => state.customFilters.isPopulated,
|
||||
(state) => state.tags.isPopulated,
|
||||
(state) => state.settings.ui.isPopulated,
|
||||
|
@ -51,9 +59,11 @@ const selectIsPopulated = createSelector(
|
|||
(state) => state.settings.qualityProfiles.isPopulated,
|
||||
(state) => state.settings.metadataProfiles.isPopulated,
|
||||
(state) => state.settings.importLists.isPopulated,
|
||||
(state) => state.settings.indexerFlags.isPopulated,
|
||||
(state) => state.system.status.isPopulated,
|
||||
(state) => state.app.translations.isPopulated,
|
||||
(
|
||||
artistsIsPopulated,
|
||||
customFiltersIsPopulated,
|
||||
tagsIsPopulated,
|
||||
uiSettingsIsPopulated,
|
||||
|
@ -61,10 +71,12 @@ const selectIsPopulated = createSelector(
|
|||
qualityProfilesIsPopulated,
|
||||
metadataProfilesIsPopulated,
|
||||
importListsIsPopulated,
|
||||
indexerFlagsIsPopulated,
|
||||
systemStatusIsPopulated,
|
||||
translationsIsPopulated
|
||||
) => {
|
||||
return (
|
||||
artistsIsPopulated &&
|
||||
customFiltersIsPopulated &&
|
||||
tagsIsPopulated &&
|
||||
uiSettingsIsPopulated &&
|
||||
|
@ -72,6 +84,7 @@ const selectIsPopulated = createSelector(
|
|||
qualityProfilesIsPopulated &&
|
||||
metadataProfilesIsPopulated &&
|
||||
importListsIsPopulated &&
|
||||
indexerFlagsIsPopulated &&
|
||||
systemStatusIsPopulated &&
|
||||
translationsIsPopulated
|
||||
);
|
||||
|
@ -79,6 +92,7 @@ const selectIsPopulated = createSelector(
|
|||
);
|
||||
|
||||
const selectErrors = createSelector(
|
||||
(state) => state.artist.error,
|
||||
(state) => state.customFilters.error,
|
||||
(state) => state.tags.error,
|
||||
(state) => state.settings.ui.error,
|
||||
|
@ -86,9 +100,11 @@ const selectErrors = createSelector(
|
|||
(state) => state.settings.qualityProfiles.error,
|
||||
(state) => state.settings.metadataProfiles.error,
|
||||
(state) => state.settings.importLists.error,
|
||||
(state) => state.settings.indexerFlags.error,
|
||||
(state) => state.system.status.error,
|
||||
(state) => state.app.translations.error,
|
||||
(
|
||||
artistsError,
|
||||
customFiltersError,
|
||||
tagsError,
|
||||
uiSettingsError,
|
||||
|
@ -96,10 +112,12 @@ const selectErrors = createSelector(
|
|||
qualityProfilesError,
|
||||
metadataProfilesError,
|
||||
importListsError,
|
||||
indexerFlagsError,
|
||||
systemStatusError,
|
||||
translationsError
|
||||
) => {
|
||||
const hasError = !!(
|
||||
artistsError ||
|
||||
customFiltersError ||
|
||||
tagsError ||
|
||||
uiSettingsError ||
|
||||
|
@ -107,6 +125,7 @@ const selectErrors = createSelector(
|
|||
qualityProfilesError ||
|
||||
metadataProfilesError ||
|
||||
importListsError ||
|
||||
indexerFlagsError ||
|
||||
systemStatusError ||
|
||||
translationsError
|
||||
);
|
||||
|
@ -120,6 +139,7 @@ const selectErrors = createSelector(
|
|||
qualityProfilesError,
|
||||
metadataProfilesError,
|
||||
importListsError,
|
||||
indexerFlagsError,
|
||||
systemStatusError,
|
||||
translationsError
|
||||
};
|
||||
|
@ -177,6 +197,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||
dispatchFetchImportLists() {
|
||||
dispatch(fetchImportLists());
|
||||
},
|
||||
dispatchFetchIndexerFlags() {
|
||||
dispatch(fetchIndexerFlags());
|
||||
},
|
||||
dispatchFetchUISettings() {
|
||||
dispatch(fetchUISettings());
|
||||
},
|
||||
|
@ -217,6 +240,7 @@ class PageConnector extends Component {
|
|||
this.props.dispatchFetchQualityProfiles();
|
||||
this.props.dispatchFetchMetadataProfiles();
|
||||
this.props.dispatchFetchImportLists();
|
||||
this.props.dispatchFetchIndexerFlags();
|
||||
this.props.dispatchFetchUISettings();
|
||||
this.props.dispatchFetchStatus();
|
||||
this.props.dispatchFetchTranslations();
|
||||
|
@ -243,6 +267,7 @@ class PageConnector extends Component {
|
|||
dispatchFetchQualityProfiles,
|
||||
dispatchFetchMetadataProfiles,
|
||||
dispatchFetchImportLists,
|
||||
dispatchFetchIndexerFlags,
|
||||
dispatchFetchUISettings,
|
||||
dispatchFetchStatus,
|
||||
dispatchFetchTranslations,
|
||||
|
@ -284,6 +309,7 @@ PageConnector.propTypes = {
|
|||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchMetadataProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
|
||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||
dispatchFetchTranslations: PropTypes.func.isRequired,
|
||||
|
|
|
@ -25,14 +25,3 @@
|
|||
font-family: 'Ubuntu Mono';
|
||||
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
||||
}
|
||||
|
||||
/*
|
||||
* text-security-disc
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-family: 'text-security-disc';
|
||||
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -15,5 +15,5 @@
|
|||
"start_url": "../../../../",
|
||||
"theme_color": "#3a3f51",
|
||||
"background_color": "#3a3f51",
|
||||
"display": "minimal-ui"
|
||||
"display": "standalone"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
|
||||
export default function useModalOpenState(
|
||||
initialState: boolean
|
||||
): [boolean, () => void, () => void] {
|
||||
const [isOpen, setOpen] = useState(initialState);
|
||||
|
||||
const setModalOpen = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, [setOpen]);
|
||||
|
||||
const setModalClosed = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
return [isOpen, setModalOpen, setModalClosed];
|
||||
}
|
|
@ -60,6 +60,7 @@ import {
|
|||
faFileImport as fasFileImport,
|
||||
faFileInvoice as farFileInvoice,
|
||||
faFilter as fasFilter,
|
||||
faFlag as fasFlag,
|
||||
faFolderOpen as fasFolderOpen,
|
||||
faForward as fasForward,
|
||||
faHeart as fasHeart,
|
||||
|
@ -158,6 +159,7 @@ export const FILE = farFile;
|
|||
export const FILE_IMPORT = fasFileImport;
|
||||
export const FILE_MISSING = fasFileCircleQuestion;
|
||||
export const FILTER = fasFilter;
|
||||
export const FLAG = fasFlag;
|
||||
export const FOLDER = farFolder;
|
||||
export const FOLDER_OPEN = fasFolderOpen;
|
||||
export const GROUP = farObjectGroup;
|
||||
|
|
|
@ -15,10 +15,12 @@ export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
|||
export const METADATA_PROFILE_SELECT = 'metadataProfileSelect';
|
||||
export const ALBUM_RELEASE_SELECT = 'albumReleaseSelect';
|
||||
export const INDEXER_SELECT = 'indexerSelect';
|
||||
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
||||
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
export const SELECT = 'select';
|
||||
export const SERIES_TYPE_SELECT = 'artistTypeSelect';
|
||||
export const ARTIST_TAG = 'artistTag';
|
||||
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||
export const TAG = 'tag';
|
||||
export const TAG_SELECT = 'tagSelect';
|
||||
|
@ -48,6 +50,7 @@ export const all = [
|
|||
DOWNLOAD_CLIENT_SELECT,
|
||||
ROOT_FOLDER_SELECT,
|
||||
SELECT,
|
||||
ARTIST_TAG,
|
||||
DYNAMIC_SELECT,
|
||||
SERIES_TYPE_SELECT,
|
||||
TAG,
|
||||
|
|
|
@ -3,5 +3,5 @@ export const SMALL = 'small';
|
|||
export const MEDIUM = 'medium';
|
||||
export const LARGE = 'large';
|
||||
export const EXTRA_LARGE = 'extraLarge';
|
||||
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
|
||||
export const EXTRA_EXTRA_LARGE = 'extraExtraLarge';
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE];
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import shortenList from 'Utilities/String/shortenList';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
|
||||
|
@ -56,8 +55,7 @@ class SelectAlbumReleaseRow extends Component {
|
|||
if (name === 'release') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
<SelectInput
|
||||
name={id.toString()}
|
||||
values={_.map(releases, (r) => ({
|
||||
key: r.id,
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import SelectIndexerFlagsModalContentConnector from './SelectIndexerFlagsModalContentConnector';
|
||||
|
||||
class SelectIndexerFlagsModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SelectIndexerFlagsModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectIndexerFlagsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectIndexerFlagsModal;
|
|
@ -0,0 +1,7 @@
|
|||
.modalBody {
|
||||
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'input': string;
|
||||
'modalBody': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
|
@ -0,0 +1,106 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
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 Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectIndexerFlagsModalContent.css';
|
||||
|
||||
class SelectIndexerFlagsModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
indexerFlags
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
indexerFlags
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerFlagsChange = ({ value }) => {
|
||||
this.setState({ indexerFlags: value });
|
||||
};
|
||||
|
||||
onIndexerFlagsSelect = () => {
|
||||
this.props.onIndexerFlagsSelect(this.state);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
indexerFlags
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Set indexer Flags
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('IndexerFlags')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.INDEXER_FLAGS_SELECT}
|
||||
name="indexerFlags"
|
||||
indexerFlags={indexerFlags}
|
||||
autoFocus={true}
|
||||
onChange={this.onIndexerFlagsChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.SUCCESS}
|
||||
onPress={this.onIndexerFlagsSelect}
|
||||
>
|
||||
{translate('SetIndexerFlags')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectIndexerFlagsModalContent.propTypes = {
|
||||
indexerFlags: PropTypes.number.isRequired,
|
||||
onIndexerFlagsSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectIndexerFlagsModalContent;
|
|
@ -0,0 +1,54 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { saveInteractiveImportItem, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
|
||||
import SelectIndexerFlagsModalContent from './SelectIndexerFlagsModalContent';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
|
||||
dispatchSaveInteractiveImportItems: saveInteractiveImportItem
|
||||
};
|
||||
|
||||
class SelectIndexerFlagsModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onIndexerFlagsSelect = ({ indexerFlags }) => {
|
||||
const {
|
||||
ids,
|
||||
dispatchUpdateInteractiveImportItems,
|
||||
dispatchSaveInteractiveImportItems
|
||||
} = this.props;
|
||||
|
||||
dispatchUpdateInteractiveImportItems({
|
||||
ids,
|
||||
indexerFlags
|
||||
});
|
||||
|
||||
dispatchSaveInteractiveImportItems({ ids });
|
||||
|
||||
this.props.onModalClose(true);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectIndexerFlagsModalContent
|
||||
{...this.props}
|
||||
onIndexerFlagsSelect={this.onIndexerFlagsSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectIndexerFlagsModalContentConnector.propTypes = {
|
||||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
|
||||
dispatchSaveInteractiveImportItems: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(SelectIndexerFlagsModalContentConnector);
|
|
@ -20,6 +20,7 @@ import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal';
|
|||
import SelectAlbumReleaseModal from 'InteractiveImport/AlbumRelease/SelectAlbumReleaseModal';
|
||||
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
|
||||
import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal';
|
||||
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
|
@ -30,7 +31,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
|
|||
import InteractiveImportRow from './InteractiveImportRow';
|
||||
import styles from './InteractiveImportModalContent.css';
|
||||
|
||||
const columns = [
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: 'path',
|
||||
label: () => translate('Path'),
|
||||
|
@ -79,11 +80,21 @@ const columns = [
|
|||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
kind: kinds.DANGER
|
||||
kind: kinds.DANGER,
|
||||
title: () => translate('Rejections')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
|
@ -107,6 +118,7 @@ const ALBUM = 'album';
|
|||
const ALBUM_RELEASE = 'albumRelease';
|
||||
const RELEASE_GROUP = 'releaseGroup';
|
||||
const QUALITY = 'quality';
|
||||
const INDEXER_FLAGS = 'indexerFlags';
|
||||
|
||||
const replaceExistingFilesOptions = {
|
||||
COMBINE: 'combine',
|
||||
|
@ -301,6 +313,21 @@ class InteractiveImportModalContent extends Component {
|
|||
inconsistentAlbumReleases
|
||||
} = this.state;
|
||||
|
||||
const allColumns = _.cloneDeep(COLUMNS);
|
||||
const columns = allColumns.map((column) => {
|
||||
const showIndexerFlags = items.some((item) => item.indexerFlags);
|
||||
|
||||
if (!showIndexerFlags) {
|
||||
const indexerFlagsColumn = allColumns.find((c) => c.name === 'indexerFlags');
|
||||
|
||||
if (indexerFlagsColumn) {
|
||||
indexerFlagsColumn.isVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
return column;
|
||||
});
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null;
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load manual import items');
|
||||
|
@ -310,7 +337,8 @@ class InteractiveImportModalContent extends Component {
|
|||
{ key: ALBUM, value: translate('SelectAlbum') },
|
||||
{ key: ALBUM_RELEASE, value: translate('SelectAlbumRelease') },
|
||||
{ key: QUALITY, value: translate('SelectQuality') },
|
||||
{ key: RELEASE_GROUP, value: translate('SelectReleaseGroup') }
|
||||
{ key: RELEASE_GROUP, value: translate('SelectReleaseGroup') },
|
||||
{ key: INDEXER_FLAGS, value: translate('SelectIndexerFlags') }
|
||||
];
|
||||
|
||||
if (allowArtistChange) {
|
||||
|
@ -433,6 +461,7 @@ class InteractiveImportModalContent extends Component {
|
|||
isSaving={isSaving}
|
||||
{...item}
|
||||
allowArtistChange={allowArtistChange}
|
||||
columns={columns}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onValidRowChange={this.onValidRowChange}
|
||||
/>
|
||||
|
@ -547,6 +576,13 @@ class InteractiveImportModalContent extends Component {
|
|||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectIndexerFlagsModal
|
||||
isOpen={selectModalOpen === INDEXER_FLAGS}
|
||||
ids={selectedIds}
|
||||
indexerFlags={0}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmImportModal
|
||||
isOpen={isConfirmImportModalOpen}
|
||||
albums={albumsImported}
|
||||
|
|
|
@ -135,6 +135,7 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
albumReleaseId,
|
||||
tracks,
|
||||
quality,
|
||||
indexerFlags,
|
||||
disableReleaseSwitching
|
||||
} = item;
|
||||
|
||||
|
@ -165,6 +166,7 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
albumReleaseId,
|
||||
trackIds: _.map(tracks, 'id'),
|
||||
quality,
|
||||
indexerFlags,
|
||||
downloadId: this.props.downloadId,
|
||||
disableReleaseSwitching
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AlbumFormats from 'Album/AlbumFormats';
|
||||
import IndexerFlags from 'Album/IndexerFlags';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
|
@ -13,6 +14,7 @@ import Tooltip from 'Components/Tooltip/Tooltip';
|
|||
import { icons, kinds, sortDirections, tooltipPositions } from 'Helpers/Props';
|
||||
import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal';
|
||||
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
|
||||
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||
import SelectTrackModal from 'InteractiveImport/Track/SelectTrackModal';
|
||||
|
@ -35,7 +37,8 @@ class InteractiveImportRow extends Component {
|
|||
isSelectAlbumModalOpen: false,
|
||||
isSelectTrackModalOpen: false,
|
||||
isSelectReleaseGroupModalOpen: false,
|
||||
isSelectQualityModalOpen: false
|
||||
isSelectQualityModalOpen: false,
|
||||
isSelectIndexerFlagsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -45,14 +48,16 @@ class InteractiveImportRow extends Component {
|
|||
artist,
|
||||
album,
|
||||
tracks,
|
||||
quality
|
||||
quality,
|
||||
size
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
artist &&
|
||||
album != null &&
|
||||
tracks.length &&
|
||||
quality
|
||||
quality &&
|
||||
size > 0
|
||||
) {
|
||||
this.props.onSelectedChange({ id, value: true });
|
||||
}
|
||||
|
@ -130,6 +135,10 @@ class InteractiveImportRow extends Component {
|
|||
this.setState({ isSelectQualityModalOpen: true });
|
||||
};
|
||||
|
||||
onSelectIndexerFlagsPress = () => {
|
||||
this.setState({ isSelectIndexerFlagsModalOpen: true });
|
||||
};
|
||||
|
||||
onSelectArtistModalClose = (changed) => {
|
||||
this.setState({ isSelectArtistModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
|
@ -155,6 +164,11 @@ class InteractiveImportRow extends Component {
|
|||
this.selectRowAfterChange(changed);
|
||||
};
|
||||
|
||||
onSelectIndexerFlagsModalClose = (changed) => {
|
||||
this.setState({ isSelectIndexerFlagsModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -171,7 +185,9 @@ class InteractiveImportRow extends Component {
|
|||
releaseGroup,
|
||||
size,
|
||||
customFormats,
|
||||
indexerFlags,
|
||||
rejections,
|
||||
columns,
|
||||
isReprocessing,
|
||||
audioTags,
|
||||
additionalFile,
|
||||
|
@ -184,7 +200,8 @@ class InteractiveImportRow extends Component {
|
|||
isSelectAlbumModalOpen,
|
||||
isSelectTrackModalOpen,
|
||||
isSelectReleaseGroupModalOpen,
|
||||
isSelectQualityModalOpen
|
||||
isSelectQualityModalOpen,
|
||||
isSelectIndexerFlagsModalOpen
|
||||
} = this.state;
|
||||
|
||||
const artistName = artist ? artist.artistName : '';
|
||||
|
@ -204,6 +221,7 @@ class InteractiveImportRow extends Component {
|
|||
const showTrackNumbersLoading = isReprocessing && !tracks.length;
|
||||
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
|
||||
const showQualityPlaceholder = isSelected && !quality;
|
||||
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
|
||||
|
||||
const pathCellContents = (
|
||||
<div>
|
||||
|
@ -219,6 +237,8 @@ class InteractiveImportRow extends Component {
|
|||
/>
|
||||
) : pathCellContents;
|
||||
|
||||
const isIndexerFlagsColumnVisible = columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
className={additionalFile ? styles.additionalFile : undefined}
|
||||
|
@ -322,6 +342,28 @@ class InteractiveImportRow extends Component {
|
|||
}
|
||||
</TableRowCell>
|
||||
|
||||
{isIndexerFlagsColumnVisible ? (
|
||||
<TableRowCellButton
|
||||
title={translate('ClickToChangeIndexerFlags')}
|
||||
onPress={this.onSelectIndexerFlagsPress}
|
||||
>
|
||||
{showIndexerFlagsPlaceholder ? (
|
||||
<InteractiveImportRowCellPlaceholder isOptional={true} />
|
||||
) : (
|
||||
<>
|
||||
{indexerFlags ? (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||
title={translate('IndexerFlags')}
|
||||
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</TableRowCellButton>
|
||||
) : null}
|
||||
|
||||
<TableRowCell>
|
||||
{
|
||||
rejections.length ?
|
||||
|
@ -395,6 +437,13 @@ class InteractiveImportRow extends Component {
|
|||
real={quality ? quality.revision.real > 0 : false}
|
||||
onModalClose={this.onSelectQualityModalClose}
|
||||
/>
|
||||
|
||||
<SelectIndexerFlagsModal
|
||||
isOpen={isSelectIndexerFlagsModalOpen}
|
||||
ids={[id]}
|
||||
indexerFlags={indexerFlags ?? 0}
|
||||
onModalClose={this.onSelectIndexerFlagsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
@ -413,7 +462,9 @@ InteractiveImportRow.propTypes = {
|
|||
quality: PropTypes.object,
|
||||
size: PropTypes.number.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
indexerFlags: PropTypes.number.isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
audioTags: PropTypes.object.isRequired,
|
||||
additionalFile: PropTypes.bool.isRequired,
|
||||
isReprocessing: PropTypes.bool,
|
||||
|
|
|
@ -48,7 +48,7 @@ class InteractiveImportModal extends Component {
|
|||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
|
|
@ -65,6 +65,15 @@ const columns = [
|
|||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
}
|
||||
|
||||
.rejected,
|
||||
.indexerFlags,
|
||||
.download {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ interface CssExports {
|
|||
'customFormatScore': string;
|
||||
'download': string;
|
||||
'indexer': string;
|
||||
'indexerFlags': string;
|
||||
'peers': string;
|
||||
'protocol': string;
|
||||
'quality': string;
|
||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import AlbumFormats from 'Album/AlbumFormats';
|
||||
import IndexerFlags from 'Album/IndexerFlags';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
@ -48,12 +49,12 @@ function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
|||
if (isGrabbing) {
|
||||
return '';
|
||||
} else if (isGrabbed) {
|
||||
return 'Added to downloaded queue';
|
||||
return translate('AddedToDownloadQueue');
|
||||
} else if (grabError) {
|
||||
return grabError;
|
||||
}
|
||||
|
||||
return 'Add to downloaded queue';
|
||||
return translate('AddToDownloadQueue');
|
||||
}
|
||||
|
||||
class InteractiveSearchRow extends Component {
|
||||
|
@ -129,6 +130,7 @@ class InteractiveSearchRow extends Component {
|
|||
quality,
|
||||
customFormatScore,
|
||||
customFormats,
|
||||
indexerFlags = 0,
|
||||
rejections,
|
||||
downloadAllowed,
|
||||
isGrabbing,
|
||||
|
@ -187,10 +189,21 @@ class InteractiveSearchRow extends Component {
|
|||
formatCustomFormatScore(customFormatScore, customFormats.length)
|
||||
}
|
||||
tooltip={<AlbumFormats formats={customFormats} />}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexerFlags}>
|
||||
{indexerFlags ? (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||
title={translate('IndexerFlags')}
|
||||
body={<IndexerFlags indexerFlags={indexerFlags} />}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.rejected}>
|
||||
{
|
||||
!!rejections.length &&
|
||||
|
@ -236,7 +249,9 @@ class InteractiveSearchRow extends Component {
|
|||
isOpen={this.state.isConfirmGrabModalOpen}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('GrabRelease')}
|
||||
message={translate('GrabReleaseMessageText', [title])}
|
||||
message={translate('GrabReleaseUnknownArtistOrAlbumMessageText', {
|
||||
title
|
||||
})}
|
||||
confirmLabel={translate('Grab')}
|
||||
onConfirm={this.onGrabConfirm}
|
||||
onCancel={this.onGrabCancel}
|
||||
|
@ -263,6 +278,7 @@ InteractiveSearchRow.propTypes = {
|
|||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
indexerFlags: PropTypes.number.isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
downloadAllowed: PropTypes.bool.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
|
@ -275,6 +291,7 @@ InteractiveSearchRow.propTypes = {
|
|||
};
|
||||
|
||||
InteractiveSearchRow.defaultProps = {
|
||||
indexerFlags: 0,
|
||||
rejections: [],
|
||||
isGrabbing: false,
|
||||
isGrabbed: false
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
|
@ -150,6 +151,11 @@ class EditCustomFormatModalContent extends Component {
|
|||
</Form>
|
||||
|
||||
<FieldSet legend={translate('Conditions')}>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
{translate('CustomFormatsSettingsTriggerInfo')}
|
||||
</div>
|
||||
</Alert>
|
||||
<div className={styles.customFormats}>
|
||||
{
|
||||
specifications.map((tag) => {
|
||||
|
|
|
@ -15,6 +15,7 @@ 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 AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditDownloadClientModalContent.css';
|
||||
|
||||
|
@ -37,6 +38,7 @@ class EditDownloadClientModalContent extends Component {
|
|||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteDownloadClientPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
@ -206,6 +208,12 @@ class EditDownloadClientModalContent extends Component {
|
|||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
|
@ -246,6 +254,7 @@ EditDownloadClientModalContent.propTypes = {
|
|||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteDownloadClientPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveDownloadClient, setDownloadClientFieldValue, setDownloadClientValue, testDownloadClient } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveDownloadClient,
|
||||
setDownloadClientFieldValue,
|
||||
setDownloadClientValue,
|
||||
testDownloadClient,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||
|
||||
|
@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
|||
setDownloadClientValue,
|
||||
setDownloadClientFieldValue,
|
||||
saveDownloadClient,
|
||||
testDownloadClient
|
||||
testDownloadClient,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditDownloadClientModalContentConnector extends Component {
|
||||
|
@ -56,6 +63,10 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||
this.props.testDownloadClient({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -65,6 +76,7 @@ class EditDownloadClientModalContentConnector extends Component {
|
|||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
|
@ -82,6 +94,7 @@ EditDownloadClientModalContentConnector.propTypes = {
|
|||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||
saveDownloadClient: PropTypes.func.isRequired,
|
||||
testDownloadClient: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ const enableOptions = [
|
|||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
|
|
@ -20,6 +20,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditImportListModalContent.css';
|
||||
|
@ -66,6 +67,7 @@ function EditImportListModalContent(props) {
|
|||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteImportListPress,
|
||||
showMetadataProfile,
|
||||
...otherProps
|
||||
|
@ -333,6 +335,12 @@ function EditImportListModalContent(props) {
|
|||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
|
@ -373,6 +381,7 @@ EditImportListModalContent.propTypes = {
|
|||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteImportListPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveImportList, setImportListFieldValue, setImportListValue, testImportList } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveImportList,
|
||||
setImportListFieldValue,
|
||||
setImportListValue,
|
||||
testImportList,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditImportListModalContent from './EditImportListModalContent';
|
||||
|
||||
|
@ -25,7 +31,8 @@ const mapDispatchToProps = {
|
|||
setImportListValue,
|
||||
setImportListFieldValue,
|
||||
saveImportList,
|
||||
testImportList
|
||||
testImportList,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditImportListModalContentConnector extends Component {
|
||||
|
@ -58,6 +65,10 @@ class EditImportListModalContentConnector extends Component {
|
|||
this.props.testImportList({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -67,6 +78,7 @@ class EditImportListModalContentConnector extends Component {
|
|||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
|
@ -84,6 +96,7 @@ EditImportListModalContentConnector.propTypes = {
|
|||
setImportListFieldValue: PropTypes.func.isRequired,
|
||||
saveImportList: PropTypes.func.isRequired,
|
||||
testImportList: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ const autoAddOptions = [
|
|||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
|
|
@ -160,7 +160,7 @@ function EditIndexerModalContent(props) {
|
|||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="priority"
|
||||
helpText={translate('PriorityHelpText')}
|
||||
helpText={translate('IndexerPriorityHelpText')}
|
||||
min={1}
|
||||
max={50}
|
||||
{...priority}
|
||||
|
|
|
@ -32,7 +32,7 @@ const enableOptions = [
|
|||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
margin: 3px;
|
||||
border: 1px solid var(--borderColor);
|
||||
|
@ -17,7 +17,7 @@
|
|||
}
|
||||
|
||||
.small {
|
||||
width: 460px;
|
||||
width: 490px;
|
||||
}
|
||||
|
||||
.large {
|
||||
|
@ -26,7 +26,7 @@
|
|||
|
||||
.token {
|
||||
flex: 0 0 50%;
|
||||
padding: 6px 16px;
|
||||
padding: 6px;
|
||||
background-color: var(--popoverTitleBackgroundColor);
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
@ -34,9 +34,9 @@
|
|||
.example {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 50%;
|
||||
padding: 6px 16px;
|
||||
padding: 6px;
|
||||
background-color: var(--popoverBodyBackgroundColor);
|
||||
|
||||
.footNote {
|
||||
|
|
|
@ -14,6 +14,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import NotificationEventItems from './NotificationEventItems';
|
||||
import styles from './EditNotificationModalContent.css';
|
||||
|
@ -32,6 +33,7 @@ function EditNotificationModalContent(props) {
|
|||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onAdvancedSettingsPress,
|
||||
onDeleteNotificationPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
@ -140,6 +142,12 @@ function EditNotificationModalContent(props) {
|
|||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
advancedSettings={advancedSettings}
|
||||
onAdvancedSettingsPress={onAdvancedSettingsPress}
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
|
@ -179,6 +187,7 @@ EditNotificationModalContent.propTypes = {
|
|||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onAdvancedSettingsPress: PropTypes.func.isRequired,
|
||||
onDeleteNotificationPress: PropTypes.func
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,13 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveNotification, setNotificationFieldValue, setNotificationValue, testNotification } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
saveNotification,
|
||||
setNotificationFieldValue,
|
||||
setNotificationValue,
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditNotificationModalContent from './EditNotificationModalContent';
|
||||
|
||||
|
@ -23,7 +29,8 @@ const mapDispatchToProps = {
|
|||
setNotificationValue,
|
||||
setNotificationFieldValue,
|
||||
saveNotification,
|
||||
testNotification
|
||||
testNotification,
|
||||
toggleAdvancedSettings
|
||||
};
|
||||
|
||||
class EditNotificationModalContentConnector extends Component {
|
||||
|
@ -56,6 +63,10 @@ class EditNotificationModalContentConnector extends Component {
|
|||
this.props.testNotification({ id: this.props.id });
|
||||
};
|
||||
|
||||
onAdvancedSettingsPress = () => {
|
||||
this.props.toggleAdvancedSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -65,6 +76,7 @@ class EditNotificationModalContentConnector extends Component {
|
|||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onAdvancedSettingsPress={this.onAdvancedSettingsPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
|
@ -82,6 +94,7 @@ EditNotificationModalContentConnector.propTypes = {
|
|||
setNotificationFieldValue: PropTypes.func.isRequired,
|
||||
saveNotification: PropTypes.func.isRequired,
|
||||
testNotification: PropTypes.func.isRequired,
|
||||
toggleAdvancedSettings: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ export default function TagInUse(props) {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (count > 1 && labelPlural ) {
|
||||
if (count > 1 && labelPlural) {
|
||||
return (
|
||||
<div>
|
||||
{count} {labelPlural.toLowerCase()}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
import { set } from '../baseActions';
|
||||
|
||||
const abortCurrentRequests = {};
|
||||
let lastTestData = null;
|
||||
|
||||
export function createCancelTestProviderHandler(section) {
|
||||
return function(getState, payload, dispatch) {
|
||||
|
@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) {
|
|||
return function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isTesting: true }));
|
||||
|
||||
const testData = getProviderState(payload, getState, section);
|
||||
const {
|
||||
queryParams = {},
|
||||
...otherPayload
|
||||
} = payload;
|
||||
|
||||
const testData = getProviderState({ ...otherPayload }, getState, section);
|
||||
const params = { ...queryParams };
|
||||
|
||||
// If the user is re-testing the same provider without changes
|
||||
// force it to be tested.
|
||||
|
||||
if (_.isEqual(testData, lastTestData)) {
|
||||
params.forceTest = true;
|
||||
}
|
||||
|
||||
lastTestData = testData;
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}/test`,
|
||||
url: `${url}/test?${$.param(params, true)}`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
|
@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) {
|
|||
abortCurrentRequests[section] = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
lastTestData = null;
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
isTesting: false,
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.indexerFlags';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag')
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
|
||||
}
|
||||
|
||||
};
|
|
@ -208,6 +208,7 @@ export const actionHandlers = handleThunks({
|
|||
trackIds: (item.tracks || []).map((e) => e.id),
|
||||
quality: item.quality,
|
||||
releaseGroup: item.releaseGroup,
|
||||
indexerFlags: item.indexerFlags,
|
||||
downloadId: item.downloadId,
|
||||
additionalFile: item.additionalFile,
|
||||
replaceExistingFiles: item.replaceExistingFiles,
|
||||
|
|
|
@ -11,6 +11,7 @@ import downloadClients from './Settings/downloadClients';
|
|||
import general from './Settings/general';
|
||||
import importListExclusions from './Settings/importListExclusions';
|
||||
import importLists from './Settings/importLists';
|
||||
import indexerFlags from './Settings/indexerFlags';
|
||||
import indexerOptions from './Settings/indexerOptions';
|
||||
import indexers from './Settings/indexers';
|
||||
import languages from './Settings/languages';
|
||||
|
@ -38,6 +39,7 @@ export * from './Settings/downloadClientOptions';
|
|||
export * from './Settings/general';
|
||||
export * from './Settings/importLists';
|
||||
export * from './Settings/importListExclusions';
|
||||
export * from './Settings/indexerFlags';
|
||||
export * from './Settings/indexerOptions';
|
||||
export * from './Settings/indexers';
|
||||
export * from './Settings/languages';
|
||||
|
@ -73,6 +75,7 @@ export const defaultState = {
|
|||
downloadClients: downloadClients.defaultState,
|
||||
downloadClientOptions: downloadClientOptions.defaultState,
|
||||
general: general.defaultState,
|
||||
indexerFlags: indexerFlags.defaultState,
|
||||
indexerOptions: indexerOptions.defaultState,
|
||||
indexers: indexers.defaultState,
|
||||
importLists: importLists.defaultState,
|
||||
|
@ -119,6 +122,7 @@ export const actionHandlers = handleThunks({
|
|||
...downloadClients.actionHandlers,
|
||||
...downloadClientOptions.actionHandlers,
|
||||
...general.actionHandlers,
|
||||
...indexerFlags.actionHandlers,
|
||||
...indexerOptions.actionHandlers,
|
||||
...indexers.actionHandlers,
|
||||
...importLists.actionHandlers,
|
||||
|
@ -156,6 +160,7 @@ export const reducers = createHandleActions({
|
|||
...downloadClients.reducers,
|
||||
...downloadClientOptions.reducers,
|
||||
...general.reducers,
|
||||
...indexerFlags.reducers,
|
||||
...indexerOptions.reducers,
|
||||
...indexers.reducers,
|
||||
...importLists.reducers,
|
||||
|
|
|
@ -77,6 +77,15 @@ export const defaultState = {
|
|||
}),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
columnLabel: () => translate('IndexerFlags'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags')
|
||||
}),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: () => translate('Status'),
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createAllArtistSelector() {
|
||||
return createSelector(
|
||||
(state) => state.artist,
|
||||
(state: AppState) => state.artist,
|
||||
(artist) => {
|
||||
return artist.items;
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import createAllArtistSelector from './createAllArtistSelector';
|
||||
|
||||
function createArtistCountSelector() {
|
||||
return createSelector(
|
||||
createAllArtistSelector(),
|
||||
(state) => state.artist.error,
|
||||
(state) => state.artist.isFetching,
|
||||
(state) => state.artist.isPopulated,
|
||||
(state: AppState) => state.artist.error,
|
||||
(state: AppState) => state.artist.isFetching,
|
||||
(state: AppState) => state.artist.isPopulated,
|
||||
(artists, error, isFetching, isPopulated) => {
|
||||
return {
|
||||
count: artists.length,
|
||||
error,
|
||||
isFetching,
|
||||
isPopulated
|
||||
isPopulated,
|
||||
};
|
||||
}
|
||||
);
|
|
@ -2,13 +2,10 @@ import { createSelector } from 'reselect';
|
|||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
import createCommandSelector from './createCommandSelector';
|
||||
|
||||
function createCommandExecutingSelector(name, contraints = {}) {
|
||||
return createSelector(
|
||||
createCommandSelector(name, contraints),
|
||||
(command) => {
|
||||
return isCommandExecuting(command);
|
||||
}
|
||||
);
|
||||
function createCommandExecutingSelector(name: string, contraints = {}) {
|
||||
return createSelector(createCommandSelector(name, contraints), (command) => {
|
||||
return isCommandExecuting(command);
|
||||
});
|
||||
}
|
||||
|
||||
export default createCommandExecutingSelector;
|
|
@ -1,14 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import { findCommand } from 'Utilities/Command';
|
||||
import createCommandsSelector from './createCommandsSelector';
|
||||
|
||||
function createCommandSelector(name, contraints = {}) {
|
||||
return createSelector(
|
||||
createCommandsSelector(),
|
||||
(commands) => {
|
||||
return findCommand(commands, { name, ...contraints });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createCommandSelector;
|
|
@ -0,0 +1,11 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import { findCommand } from 'Utilities/Command';
|
||||
import createCommandsSelector from './createCommandsSelector';
|
||||
|
||||
function createCommandSelector(name: string, contraints = {}) {
|
||||
return createSelector(createCommandsSelector(), (commands) => {
|
||||
return findCommand(commands, { name, ...contraints });
|
||||
});
|
||||
}
|
||||
|
||||
export default createCommandSelector;
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createCommandsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.commands,
|
||||
(state: AppState) => state.commands,
|
||||
(commands) => {
|
||||
return commands.items;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { createSelectorCreator, defaultMemoize } from 'reselect';
|
||||
|
||||
const createDeepEqualSelector = createSelectorCreator(
|
||||
defaultMemoize,
|
||||
_.isEqual
|
||||
);
|
||||
|
||||
export default createDeepEqualSelector;
|
|
@ -0,0 +1,6 @@
|
|||
import { isEqual } from 'lodash';
|
||||
import { createSelectorCreator, defaultMemoize } from 'reselect';
|
||||
|
||||
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual);
|
||||
|
||||
export default createDeepEqualSelector;
|
|
@ -1,9 +1,10 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
|
||||
function createExecutingCommandsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.commands.items,
|
||||
(state: AppState) => state.commands.items,
|
||||
(commands) => {
|
||||
return commands.filter((command) => isCommandExecuting(command));
|
||||
}
|
|
@ -1,13 +1,15 @@
|
|||
import _ from 'lodash';
|
||||
import { some } from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import createAllArtistSelector from './createAllArtistSelector';
|
||||
|
||||
function createExistingArtistSelector() {
|
||||
return createSelector(
|
||||
(state, { foreignArtistId }) => foreignArtistId,
|
||||
(_: AppState, { foreignArtistId }: { foreignArtistId: string }) =>
|
||||
foreignArtistId,
|
||||
createAllArtistSelector(),
|
||||
(foreignArtistId, artist) => {
|
||||
return _.some(artist, { foreignArtistId });
|
||||
return some(artist, { foreignArtistId });
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
const createIndexerFlagsSelector = createSelector(
|
||||
(state: AppState) => state.settings.indexerFlags,
|
||||
(indexerFlags) => indexerFlags
|
||||
);
|
||||
|
||||
export default createIndexerFlagsSelector;
|
|
@ -1,15 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
function createMetadataProfileSelector() {
|
||||
return createSelector(
|
||||
(state, { metadataProfileId }) => metadataProfileId,
|
||||
(state) => state.settings.metadataProfiles.items,
|
||||
(metadataProfileId, metadataProfiles) => {
|
||||
return metadataProfiles.find((profile) => {
|
||||
return profile.id === metadataProfileId;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createMetadataProfileSelector;
|
|
@ -0,0 +1,17 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createMetadataProfileSelector() {
|
||||
return createSelector(
|
||||
(_: AppState, { metadataProfileId }: { metadataProfileId: number }) =>
|
||||
metadataProfileId,
|
||||
(state: AppState) => state.settings.metadataProfiles.items,
|
||||
(metadataProfileId, metadataProfiles) => {
|
||||
return metadataProfiles.find(
|
||||
(profile) => profile.id === metadataProfileId
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createMetadataProfileSelector;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue