Add and reorder tracks in convert dialog

This commit is contained in:
Stefano Brilli 2021-01-23 15:10:43 +01:00
parent b80f72b051
commit 0ff27de811
2 changed files with 227 additions and 37 deletions

View File

@ -1,6 +1,6 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useShallowEqualSelector } from '../utils';
import { belowDesktop, useShallowEqualSelector } from '../utils';
import { actions as convertDialogActions } from '../redux/convert-dialog-feature';
import { convertAndUpload } from '../redux/actions';
@ -20,6 +20,22 @@ import { Typography } from '@material-ui/core';
import Select from '@material-ui/core/Select';
import Input from '@material-ui/core/Input';
import MenuItem from '@material-ui/core/MenuItem';
import Accordion from '@material-ui/core/Accordion';
import AccordionDetails from '@material-ui/core/AccordionDetails';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import ExpandLessIcon from '@material-ui/icons/ExpandLess';
import AddIcon from '@material-ui/icons/Add';
import RemoveIcon from '@material-ui/icons/Remove';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import IconButton from '@material-ui/core/IconButton';
import Toolbar from '@material-ui/core/Toolbar';
import { lighten } from '@material-ui/core/styles';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import Radio from '@material-ui/core/Radio';
import { useDropzone } from 'react-dropzone';
import Backdrop from '@material-ui/core/Backdrop';
const Transition = React.forwardRef(function Transition(
props: TransitionProps & { children?: React.ReactElement<any, any> },
@ -40,6 +56,11 @@ const useStyles = makeStyles(theme => ({
minWidth: 40,
},
dialogContent: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'stretch',
},
formatAndTitle: {
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-between',
@ -51,6 +72,43 @@ const useStyles = makeStyles(theme => ({
titleFormControl: {
minWidth: 170,
marginTop: 4,
[belowDesktop(theme)]: {
width: 114,
minWidth: 0,
},
},
spacer: {
display: 'flex',
flex: '1 1 auto',
},
showTracksOrderBtn: {
marginLeft: theme.spacing(1),
},
tracksOrderAccordion: {
'&:before': {
opacity: 0,
},
},
tracksOrderAccordionDetail: {
maxHeight: '40vh',
overflow: 'auto',
},
toolbarHighlight:
theme.palette.type === 'light'
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
trackList: {
flex: '1 1 auto',
},
backdrop: {
zIndex: theme.zIndex.drawer + 1,
color: '#fff',
},
}));
@ -60,6 +118,38 @@ export const ConvertDialog = (props: { files: File[] }) => {
let { visible, format, titleFormat } = useShallowEqualSelector(state => state.convertDialog);
// Track reodering
const [files, setFiles] = useState(props.files);
const [selectedTrackIndex, setSelectedTrack] = useState(-1);
const moveFile = useCallback(
(offset: number) => {
const targetIndex = selectedTrackIndex + offset;
if (targetIndex >= files.length || targetIndex < 0) {
return; // This should not be allowed by the UI
}
const newFileArray = files.slice();
// Swap trakcs
let tmp = newFileArray[selectedTrackIndex];
newFileArray[selectedTrackIndex] = newFileArray[targetIndex];
newFileArray[targetIndex] = tmp;
setFiles(newFileArray);
setSelectedTrack(targetIndex);
},
[files, selectedTrackIndex]
);
const moveFileUp = useCallback(() => {
moveFile(-1);
}, [moveFile]);
const moveFileDown = useCallback(() => {
moveFile(1);
}, [moveFile]);
const handleClose = useCallback(() => {
dispatch(convertDialogActions.setVisible(false));
}, [dispatch]);
@ -83,8 +173,71 @@ export const ConvertDialog = (props: { files: File[] }) => {
const handleConvert = useCallback(() => {
handleClose();
dispatch(convertAndUpload(props.files, format, titleFormat));
}, [dispatch, props, format, titleFormat, handleClose]);
dispatch(convertAndUpload(files, format, titleFormat));
}, [dispatch, files, format, titleFormat, handleClose]);
const [tracksOrderVisible, setTracksOrderVisible] = useState(false);
const handleToggleTracksOrder = useCallback(() => {
setTracksOrderVisible(!tracksOrderVisible);
}, [tracksOrderVisible, setTracksOrderVisible]);
// Dialog init on new files
useEffect(() => {
const newFiles = Array.from(props.files);
setFiles(newFiles);
setSelectedTrack(-1);
setTracksOrderVisible(false);
}, [props.files, setSelectedTrack, setTracksOrderVisible]);
// scroll selected track into view
const selectedTrackRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
selectedTrackRef.current?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}, [selectedTrackRef, selectedTrackIndex]);
const renderTracks = useCallback(() => {
return files.map((file, i) => {
const isSelected = selectedTrackIndex === i;
const ref = isSelected ? selectedTrackRef : null;
return (
<ListItem key={`${i}`} disableGutters={true} onClick={() => setSelectedTrack(i)} ref={ref} button>
<ListItemIcon>
<Radio checked={isSelected} value={`track-${i}`} size="small" />
</ListItemIcon>
<ListItemText primary={file.name} />
</ListItem>
);
});
}, [files, selectedTrackIndex, setSelectedTrack, selectedTrackRef]);
// Add/Remove tracks
const onDrop = useCallback(
(acceptedFiles: File[], rejectedFiles: File[]) => {
const newFileArray = files.slice().concat(acceptedFiles);
setFiles(newFileArray);
},
[files, setFiles]
);
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
onDrop,
accept: [`audio/*`, `video/mp4`],
noClick: true,
});
const disableRemove = selectedTrackIndex < 0 || selectedTrackIndex >= files.length;
const handleRemoveSelectedTrack = useCallback(() => {
const newFileArray = files.filter((f, i) => i !== selectedTrackIndex);
setFiles(newFileArray);
if (selectedTrackIndex >= newFileArray.length) {
setSelectedTrack(newFileArray.length - 1);
}
}, [selectedTrackIndex, files, setFiles]);
const dialogVisible = useShallowEqualSelector(state => state.convertDialog.visible);
useEffect(() => {
if (dialogVisible && files.length === 0) {
handleClose();
}
}, [files, dialogVisible]);
return (
<Dialog
@ -97,6 +250,7 @@ export const ConvertDialog = (props: { files: File[] }) => {
>
<DialogTitle id="convert-dialog-slide-title">Upload Settings</DialogTitle>
<DialogContent className={classes.dialogContent}>
<div className={classes.formatAndTitle}>
<FormControl>
<Typography component="label" variant="caption" color="textSecondary">
Recording Mode
@ -129,8 +283,42 @@ export const ConvertDialog = (props: { files: File[] }) => {
</FormControl>
</FormControl>
</div>
</div>
<Accordion expanded={tracksOrderVisible} className={classes.tracksOrderAccordion} square={true}>
<div></div>
<div {...getRootProps()} style={{ outline: 'none' }}>
<Toolbar variant="dense" className={classes.toolbarHighlight}>
<IconButton edge="start" aria-label="add track" onClick={open}>
<AddIcon />
</IconButton>
<IconButton edge="start" aria-label="remove track" onClick={handleRemoveSelectedTrack} disabled={disableRemove}>
<RemoveIcon />
</IconButton>
<div className={classes.spacer}></div>
<IconButton edge="end" aria-label="move up" onClick={moveFileDown}>
<ExpandMoreIcon />
</IconButton>
<IconButton edge="end" aria-label="move down" onClick={moveFileUp}>
<ExpandLessIcon />
</IconButton>
</Toolbar>
<AccordionDetails className={classes.tracksOrderAccordionDetail}>
<List dense={true} disablePadding={false} className={classes.trackList}>
{renderTracks()}
</List>
</AccordionDetails>
<Backdrop className={classes.backdrop} open={isDragActive}>
Drop your Music to add it to the queue
</Backdrop>
<input {...getInputProps()} />
</div>
</Accordion>
</DialogContent>
<DialogActions>
<Button onClick={handleToggleTracksOrder} className={classes.showTracksOrderBtn}>
{`${tracksOrderVisible ? 'Hide' : 'Show'} Tracks`}
</Button>
<div className={classes.spacer}></div>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleConvert}>Ok</Button>
</DialogActions>

View File

@ -262,7 +262,13 @@ async function getTrackNameFromMediaTags(file: File, titleFormat: TitleFormatTyp
return `${artist} - ${album} - ${title}`;
}
case 'filename': {
return file.name;
let title = file.name;
// Remove file extension
const extStartIndex = title.lastIndexOf('.');
if (extStartIndex > 0) {
title = title.substring(0, extStartIndex);
}
return title;
}
}
}
@ -365,10 +371,6 @@ export function convertAndUpload(files: File[], format: UploadFormat, titleForma
console.error(err);
}
const extStartIndex = title.lastIndexOf('.');
if (extStartIndex > 0) {
title = title.substring(0, extStartIndex);
}
if (maxTitleLength > -1) {
title = title.substring(0, maxTitleLength);
}