Fixed: Quality Groups and Profiles

This commit is contained in:
Qstick
2019-06-10 22:59:39 -04:00
parent 6275737ced
commit 16ff1176f7
55 changed files with 1229 additions and 216 deletions

View File

@@ -17,7 +17,7 @@ import Settings from 'Settings/Settings';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import Profiles from 'Settings/Profiles/Profiles';
import Quality from 'Settings/Quality/Quality';
import CustomFormatsConnector from 'Settings/CustomFormats/CustomFormatsConnector';
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import NetImportSettingsConnector from 'Settings/NetImport/NetImportSettingsConnector';
@@ -142,7 +142,7 @@ function AppRoutes(props) {
<Route
path="/settings/customformats"
component={CustomFormatsConnector}
component={CustomFormatSettingsConnector}
/>
<Route

View File

@@ -0,0 +1,30 @@
import React, { Component } from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
class CustomFormatSettingsConnector extends Component {
//
// Render
render() {
return (
<PageContent title="Custom Formats Settings">
<SettingsToolbarConnector
showSave={false}
/>
<PageContentBodyConnector>
<CustomFormatsConnector />
</PageContentBodyConnector>
</PageContent>
);
}
}
export default DragDropContext(HTML5Backend)(CustomFormatSettingsConnector);

View File

@@ -0,0 +1,38 @@
.customFormat {
composes: card from '~Components/Card.css';
width: 300px;
}
.nameContainer {
display: flex;
justify-content: space-between;
}
.name {
@add-mixin truncate;
margin-bottom: 20px;
font-weight: 300;
font-size: 24px;
}
.cloneButton {
composes: button from '~Components/Link/IconButton.css';
height: 36px;
}
.formats {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
pointer-events: all;
}
.tooltipLabel {
composes: label from '~Components/Label.css';
margin: 0;
border: none;
}

View File

@@ -0,0 +1,141 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds } from 'Helpers/Props';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
// import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
import styles from './CustomFormat.css';
class CustomFormat extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditCustomFormatModalOpen: false,
isDeleteCustomFormatModalOpen: false
};
}
//
// Listeners
onEditCustomFormatPress = () => {
this.setState({ isEditCustomFormatModalOpen: true });
}
onEditCustomFormatModalClose = () => {
this.setState({ isEditCustomFormatModalOpen: false });
}
onDeleteCustomFormatPress = () => {
this.setState({
isEditCustomFormatModalOpen: false,
isDeleteCustomFormatModalOpen: true
});
}
onDeleteCustomFormatModalClose = () => {
this.setState({ isDeleteCustomFormatModalOpen: false });
}
onConfirmDeleteCustomFormat = () => {
this.props.onConfirmDeleteCustomFormat(this.props.id);
}
onCloneCustomFormatPress = () => {
const {
id,
onCloneCustomFormatPress
} = this.props;
onCloneCustomFormatPress(id);
}
//
// Render
render() {
const {
// id,
name,
items,
isDeleting
} = this.props;
return (
<Card
className={styles.CustomFormat}
overlayContent={true}
onPress={this.onEditCustomFormatPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>
{name}
</div>
<IconButton
className={styles.cloneButton}
title="Clone Profile"
name={icons.CLONE}
onPress={this.onCloneCustomFormatPress}
/>
</div>
<div className={styles.formats}>
{
items.map((item) => {
if (!item.allowed) {
return null;
}
return (
<Label
key={item.quality.id}
kind={kinds.default}
title={null}
>
{item.quality.name}
</Label>
);
})
}
</div>
{/* <EditCustomFormatModalConnector
id={id}
isOpen={this.state.isEditCustomFormatModalOpen}
onModalClose={this.onEditCustomFormatModalClose}
onDeleteCustomFormatPress={this.onDeleteCustomFormatPress}
/> */}
<ConfirmModal
isOpen={this.state.isDeleteCustomFormatModalOpen}
kind={kinds.DANGER}
title="Delete Custom Format"
message={`Are you sure you want to delete the custom format '${name}'?`}
confirmLabel="Delete"
isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteCustomFormat}
onCancel={this.onDeleteCustomFormatModalClose}
/>
</Card>
);
}
}
CustomFormat.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
onConfirmDeleteCustomFormat: PropTypes.func.isRequired,
onCloneCustomFormatPress: PropTypes.func.isRequired
};
export default CustomFormat;

View File

@@ -0,0 +1,21 @@
.customFormats {
display: flex;
flex-wrap: wrap;
}
.addCustomFormat {
composes: customFormat from '~./CustomFormat.css';
background-color: $cardAlternateBackgroundColor;
color: $gray;
text-align: center;
font-size: 45px;
}
.center {
display: inline-block;
padding: 5px 20px 0;
border: 1px solid $borderColor;
border-radius: 4px;
background-color: $white;
}

View File

@@ -0,0 +1,109 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import sortByName from 'Utilities/Array/sortByName';
import { icons } from 'Helpers/Props';
import FieldSet from 'Components/FieldSet';
import Card from 'Components/Card';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import CustomFormat from './CustomFormat';
// import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
import styles from './CustomFormats.css';
class CustomFormats extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isCustomFormatModalOpen: false
};
}
//
// Listeners
onCloneCustomFormatPress = (id) => {
this.props.onCloneCustomFormatPress(id);
this.setState({ isCustomFormatModalOpen: true });
}
onEditCustomFormatPress = () => {
this.setState({ isCustomFormatModalOpen: true });
}
onModalClose = () => {
this.setState({ isCustomFormatModalOpen: false });
}
//
// Render
render() {
const {
items,
isDeleting,
onConfirmDeleteCustomFormat,
onCloneCustomFormatPress,
...otherProps
} = this.props;
return (
<FieldSet legend="Custom Formats">
<PageSectionContent
errorMessage="Unable to load Custom Formats"
{...otherProps}c={true}
>
<div className={styles.CustomFormats}>
{
items.sort(sortByName).map((item) => {
return (
<CustomFormat
key={item.id}
{...item}
isDeleting={isDeleting}
onConfirmDeleteCustomFormat={onConfirmDeleteCustomFormat}
onCloneCustomFormatPress={this.onCloneCustomFormatPress}
/>
);
})
}
<Card
className={styles.addCustomFormat}
onPress={this.onEditCustomFormatPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
</div>
{/*
<EditCustomFormatModalConnector
isOpen={this.state.isCustomFormatModalOpen}
onModalClose={this.onModalClose}
/> */}
</PageSectionContent>
</FieldSet>
);
}
}
CustomFormats.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteCustomFormat: PropTypes.func.isRequired,
onCloneCustomFormatPress: PropTypes.func.isRequired
};
export default CustomFormats;

View File

@@ -0,0 +1,65 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchCustomFormats, deleteCustomFormat, cloneCustomFormat } from 'Store/Actions/settingsActions';
import CustomFormats from './CustomFormats';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.customFormats,
(customFormats) => {
return {
...customFormats
};
}
);
}
const mapDispatchToProps = {
dispatchFetchCustomFormats: fetchCustomFormats,
dispatchDeleteCustomFormat: deleteCustomFormat,
dispatchCloneCustomFormat: cloneCustomFormat
};
class CustomFormatsConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchCustomFormats();
}
//
// Listeners
onConfirmDeleteCustomFormat = (id) => {
this.props.dispatchDeleteCustomFormat({ id });
}
onCloneCustomFormatPress = (id) => {
this.props.dispatchCloneCustomFormat({ id });
}
//
// Render
render() {
return (
<CustomFormats
onConfirmDeleteCustomFormat={this.onConfirmDeleteCustomFormat}
onCloneCustomFormatPress={this.onCloneCustomFormatPress}
{...this.props}
/>
);
}
}
CustomFormatsConnector.propTypes = {
dispatchFetchCustomFormats: PropTypes.func.isRequired,
dispatchDeleteCustomFormat: PropTypes.func.isRequired,
dispatchCloneCustomFormat: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(CustomFormatsConnector);

View File

@@ -1,17 +0,0 @@
import React from 'react';
import PageContent from 'Components/Page/PageContent';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
function CustomFormatsConnector() {
return (
<PageContent title="Custom Formats Settings">
<SettingsToolbarConnector
showSave={false}
/>
</PageContent>
);
}
export default CustomFormatsConnector;

View File

@@ -121,7 +121,7 @@ class EditQualityProfileModalContentConnector extends Component {
return false;
}
return i.id === cutoff.id || (i.quality && i.quality.id === cutoff.id);
return i.id === cutoff || (i.quality && i.quality.id === cutoff);
});
// If the cutoff isn't allowed anymore or there isn't a cutoff set one

View File

@@ -97,20 +97,20 @@ class QualityProfile extends Component {
}
if (item.quality) {
const isCutoff = item.quality.id === cutoff.id;
const isCutoff = item.quality.id === cutoff;
return (
<Label
key={item.quality.id}
kind={isCutoff ? kinds.INFO : kinds.default}
title={isCutoff ? 'Cutoff' : null}
title={isCutoff ? 'Upgrade until this quality is met or exceeded' : null}
>
{item.quality.name}
</Label>
);
}
const isCutoff = item.id === cutoff.id;
const isCutoff = item.id === cutoff;
return (
<Tooltip
@@ -174,7 +174,7 @@ class QualityProfile extends Component {
QualityProfile.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
cutoff: PropTypes.object.isRequired,
cutoff: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
onConfirmDeleteQualityProfile: PropTypes.func.isRequired,

View File

@@ -0,0 +1,97 @@
import { createAction } from 'redux-actions';
import { createThunk } from 'Store/thunks';
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
//
// Variables
const section = 'settings.customFormats';
//
// Actions Types
export const FETCH_CUSTOM_FORMATS = 'settings/customFormats/fetchCustomFormats';
export const FETCH_CUSTOM_FORMAT_SCHEMA = 'settings/customFormats/fetchCustomFormatSchema';
export const SAVE_CUSTOM_FORMAT = 'settings/customFormats/saveCustomFormat';
export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat';
export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
//
// Action Creators
export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
export const fetchCustomFormatSchema = createThunk(FETCH_CUSTOM_FORMAT_SCHEMA);
export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT);
export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => {
return {
section,
...payload
};
});
export const cloneCustomFormat = createAction(CLONE_CUSTOM_FORMAT);
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
isDeleting: false,
deleteError: null,
isSchemaFetching: false,
isSchemaPopulated: false,
schemaError: null,
schema: {},
isSaving: false,
saveError: null,
items: [],
pendingChanges: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_CUSTOM_FORMATS]: createFetchHandler(section, '/customformat'),
[FETCH_CUSTOM_FORMAT_SCHEMA]: createFetchSchemaHandler(section, '/customformat/schema'),
[SAVE_CUSTOM_FORMAT]: createSaveProviderHandler(section, '/customformat'),
[DELETE_CUSTOM_FORMAT]: createRemoveItemHandler(section, '/customformat')
},
//
// Reducers
reducers: {
[SET_CUSTOM_FORMAT_VALUE]: createSetSettingValueReducer(section),
[CLONE_CUSTOM_FORMAT]: function(state, { payload }) {
const id = payload.id;
const newState = getSectionState(state, section);
const item = newState.items.find((i) => i.id === id);
const pendingChanges = { ...item, id: 0 };
delete pendingChanges.id;
pendingChanges.name = `${pendingChanges.name} - Copy`;
newState.pendingChanges = pendingChanges;
return updateSectionState(state, section, newState);
}
}
};

View File

@@ -1,6 +1,7 @@
import { createAction } from 'redux-actions';
import { handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions';
import customFormats from './Settings/customFormats';
import delayProfiles from './Settings/delayProfiles';
import downloadClients from './Settings/downloadClients';
import downloadClientOptions from './Settings/downloadClientOptions';
@@ -20,6 +21,7 @@ import remotePathMappings from './Settings/remotePathMappings';
import restrictions from './Settings/restrictions';
import ui from './Settings/ui';
export * from './Settings/customFormats';
export * from './Settings/delayProfiles';
export * from './Settings/downloadClients';
export * from './Settings/downloadClientOptions';
@@ -50,6 +52,7 @@ export const section = 'settings';
export const defaultState = {
advancedSettings: false,
customFormats: customFormats.defaultState,
delayProfiles: delayProfiles.defaultState,
downloadClients: downloadClients.defaultState,
downloadClientOptions: downloadClientOptions.defaultState,
@@ -88,6 +91,7 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
// Action Handlers
export const actionHandlers = handleThunks({
...customFormats.actionHandlers,
...delayProfiles.actionHandlers,
...downloadClients.actionHandlers,
...downloadClientOptions.actionHandlers,
@@ -117,6 +121,7 @@ export const reducers = createHandleActions({
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
},
...customFormats.reducers,
...delayProfiles.reducers,
...downloadClients.reducers,
...downloadClientOptions.reducers,