mirror of
https://github.com/lidarr/Lidarr
synced 2025-02-23 22:40:54 +00:00
parent
b03f4d7d95
commit
71c1edd47c
8 changed files with 199 additions and 9 deletions
|
@ -42,6 +42,14 @@ export const defaultState = {
|
|||
},
|
||||
|
||||
columns: [
|
||||
{
|
||||
name: 'select',
|
||||
columnLabel: 'Select',
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
isModifiable: false,
|
||||
isHidden: true
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
label: translate('Path'),
|
||||
|
|
|
@ -10,7 +10,11 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
|||
import VirtualTable from 'Components/Table/VirtualTable';
|
||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import UnmappedFilesTableHeader from './UnmappedFilesTableHeader';
|
||||
import UnmappedFilesTableRow from './UnmappedFilesTableRow';
|
||||
|
||||
|
@ -23,10 +27,43 @@ class UnmappedFilesTable extends Component {
|
|||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
scroller: null
|
||||
scroller: null,
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = this.props;
|
||||
|
||||
if (sortKey !== prevProps.sortKey ||
|
||||
sortDirection !== prevProps.sortDirection ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||
) {
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
const hasFinishedDeleting = prevProps.isDeleting &&
|
||||
!isDeleting &&
|
||||
!deleteError;
|
||||
|
||||
if (hasFinishedDeleting) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
|
@ -34,6 +71,68 @@ class UnmappedFilesTable extends Component {
|
|||
this.setState({ scroller: ref });
|
||||
};
|
||||
|
||||
getSelectedIds = () => {
|
||||
if (this.state.allUnselected) {
|
||||
return [];
|
||||
}
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
setSelectedState() {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const newSelectedState = {};
|
||||
|
||||
items.forEach((file) => {
|
||||
const isItemSelected = selectedState[file.id];
|
||||
|
||||
if (isItemSelected) {
|
||||
newSelectedState[file.id] = isItemSelected;
|
||||
} else {
|
||||
newSelectedState[file.id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||
const newStateCount = Object.keys(newSelectedState).length;
|
||||
let isAllSelected = false;
|
||||
let isAllUnselected = false;
|
||||
|
||||
if (selectedCount === 0) {
|
||||
isAllUnselected = true;
|
||||
} else if (selectedCount === newStateCount) {
|
||||
isAllSelected = true;
|
||||
}
|
||||
|
||||
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||
}
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectAllPress = () => {
|
||||
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteUnmappedFilesPress = () => {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
this.props.deleteUnmappedFiles(selectedIds);
|
||||
};
|
||||
|
||||
rowRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
items,
|
||||
|
@ -41,6 +140,10 @@ class UnmappedFilesTable extends Component {
|
|||
deleteUnmappedFile
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const item = items[rowIndex];
|
||||
|
||||
return (
|
||||
|
@ -51,6 +154,8 @@ class UnmappedFilesTable extends Component {
|
|||
<UnmappedFilesTableRow
|
||||
key={item.id}
|
||||
columns={columns}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
deleteUnmappedFile={deleteUnmappedFile}
|
||||
{...item}
|
||||
/>
|
||||
|
@ -63,6 +168,7 @@ class UnmappedFilesTable extends Component {
|
|||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
isDeleting,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
|
@ -72,13 +178,19 @@ class UnmappedFilesTable extends Component {
|
|||
onSortPress,
|
||||
isScanningFolders,
|
||||
onAddMissingArtistsPress,
|
||||
deleteUnmappedFiles,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
scroller
|
||||
scroller,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const selectedTrackFileIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title={translate('UnmappedFiles')}>
|
||||
<PageToolbar>
|
||||
|
@ -90,6 +202,13 @@ class UnmappedFilesTable extends Component {
|
|||
isSpinning={isScanningFolders}
|
||||
onPress={onAddMissingArtistsPress}
|
||||
/>
|
||||
<PageToolbarButton
|
||||
label={translate('DeleteSelected')}
|
||||
iconName={icons.DELETE}
|
||||
isDisabled={selectedTrackFileIds.length === 0}
|
||||
isSpinning={isDeleting}
|
||||
onPress={this.onDeleteUnmappedFilesPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
|
@ -138,8 +257,12 @@ class UnmappedFilesTable extends Component {
|
|||
sortDirection={sortDirection}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
onSortPress={onSortPress}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
/>
|
||||
}
|
||||
selectedState={selectedState}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
/>
|
||||
|
@ -153,6 +276,8 @@ class UnmappedFilesTable extends Component {
|
|||
UnmappedFilesTable.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
@ -161,6 +286,7 @@ UnmappedFilesTable.propTypes = {
|
|||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
deleteUnmappedFile: PropTypes.func.isRequired,
|
||||
deleteUnmappedFiles: PropTypes.func.isRequired,
|
||||
isScanningFolders: PropTypes.bool.isRequired,
|
||||
onAddMissingArtistsPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ import { createSelector } from 'reselect';
|
|||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { deleteTrackFile, fetchTrackFiles, setTrackFilesSort, setTrackFilesTableOption } from 'Store/Actions/trackFileActions';
|
||||
import { deleteTrackFile, deleteTrackFiles, fetchTrackFiles, setTrackFilesSort, setTrackFilesTableOption } from 'Store/Actions/trackFileActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
|
@ -28,7 +28,9 @@ function createMapStateToProps() {
|
|||
items,
|
||||
...otherProps
|
||||
} = trackFiles;
|
||||
|
||||
const unmappedFiles = _.filter(items, { albumId: 0 });
|
||||
|
||||
return {
|
||||
items: unmappedFiles,
|
||||
...otherProps,
|
||||
|
@ -57,6 +59,10 @@ function createMapDispatchToProps(dispatch, props) {
|
|||
dispatch(deleteTrackFile({ id }));
|
||||
},
|
||||
|
||||
deleteUnmappedFiles(trackFileIds) {
|
||||
dispatch(deleteTrackFiles({ trackFileIds }));
|
||||
},
|
||||
|
||||
onAddMissingArtistsPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.RESCAN_FOLDERS,
|
||||
|
@ -105,7 +111,8 @@ UnmappedFilesTableConnector.propTypes = {
|
|||
onSortPress: PropTypes.func.isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
fetchUnmappedFiles: PropTypes.func.isRequired,
|
||||
deleteUnmappedFile: PropTypes.func.isRequired
|
||||
deleteUnmappedFile: PropTypes.func.isRequired,
|
||||
deleteUnmappedFiles: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
|
|
|
@ -4,6 +4,7 @@ import IconButton from 'Components/Link/IconButton';
|
|||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
// import hasGrowableColumns from './hasGrowableColumns';
|
||||
import styles from './UnmappedFilesTableHeader.css';
|
||||
|
@ -12,6 +13,9 @@ function UnmappedFilesTableHeader(props) {
|
|||
const {
|
||||
columns,
|
||||
onTableOptionChange,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
|
@ -30,6 +34,17 @@ function UnmappedFilesTableHeader(props) {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (name === 'select') {
|
||||
return (
|
||||
<VirtualTableSelectAllHeaderCell
|
||||
key={name}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
|
@ -71,6 +86,9 @@ function UnmappedFilesTableHeader(props) {
|
|||
|
||||
UnmappedFilesTableHeader.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -20,3 +20,9 @@
|
|||
|
||||
flex: 0 0 95px;
|
||||
}
|
||||
|
||||
.checkInput {
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import IconButton from 'Components/Link/IconButton';
|
|||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import FileDetailsModal from 'TrackFile/FileDetailsModal';
|
||||
|
@ -69,7 +70,9 @@ class UnmappedFilesTableRow extends Component {
|
|||
size,
|
||||
dateAdded,
|
||||
quality,
|
||||
columns
|
||||
columns,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
const folder = path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')));
|
||||
|
@ -93,6 +96,19 @@ class UnmappedFilesTableRow extends Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (name === 'select') {
|
||||
return (
|
||||
<VirtualTableSelectCell
|
||||
inputClassName={styles.checkInput}
|
||||
id={id}
|
||||
key={name}
|
||||
isSelected={isSelected}
|
||||
isDisabled={false}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
|
@ -208,6 +224,8 @@ UnmappedFilesTableRow.propTypes = {
|
|||
quality: PropTypes.object.isRequired,
|
||||
dateAdded: PropTypes.string.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
deleteUnmappedFile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -154,17 +154,23 @@ public void DeleteTrackFile(int id)
|
|||
}
|
||||
|
||||
[HttpDelete("bulk")]
|
||||
public IActionResult DeleteTrackFiles([FromBody] TrackFileListResource resource)
|
||||
public object DeleteTrackFiles([FromBody] TrackFileListResource resource)
|
||||
{
|
||||
var trackFiles = _mediaFileService.Get(resource.TrackFileIds);
|
||||
var artist = trackFiles.First().Artist.Value;
|
||||
|
||||
foreach (var trackFile in trackFiles)
|
||||
{
|
||||
_mediaFileDeletionService.DeleteTrackFile(artist, trackFile);
|
||||
if (trackFile.AlbumId > 0 && trackFile.Artist != null && trackFile.Artist.Value != null)
|
||||
{
|
||||
_mediaFileDeletionService.DeleteTrackFile(trackFile.Artist.Value, trackFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
_mediaFileDeletionService.DeleteTrackFile(trackFile, "Unmapped_Files");
|
||||
}
|
||||
}
|
||||
|
||||
return Ok();
|
||||
return new { };
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
|
|
|
@ -196,6 +196,7 @@
|
|||
"DeleteReleaseProfileMessageText": "Are you sure you want to delete this releaseProfile?",
|
||||
"DeleteRootFolder": "Delete Root Folder",
|
||||
"DeleteRootFolderMessageText": "Are you sure you want to delete the root folder '{0}'?",
|
||||
"DeleteSelected": "Delete Selected",
|
||||
"DeleteSelectedTrackFiles": "Delete Selected Track Files",
|
||||
"DeleteSelectedTrackFilesMessageText": "Are you sure you want to delete the selected track files?",
|
||||
"DeleteTag": "Delete Tag",
|
||||
|
|
Loading…
Reference in a new issue