Merge pull request #53 from cybercase/fix/various_fixes_and_improvements

Fix/various fixes and improvements
This commit is contained in:
Stefano Brilli 2021-07-30 16:57:24 +02:00 committed by GitHub
commit 3453528afe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 550 additions and 366 deletions

View File

@ -121,9 +121,11 @@ const App = () => {
</Typography>
</main>
{loading ? (
<Backdrop className={classes.backdrop} open={loading}>
<CircularProgress color="inherit" />
</Backdrop>
) : null}
</ThemeProvider>
</React.Fragment>
);

View File

@ -0,0 +1,233 @@
import React, { useCallback } from 'react';
import clsx from 'clsx';
import { EncodingName } from '../utils';
import { formatTimeFromFrames, Track, Group } from 'netmd-js';
import { makeStyles } from '@material-ui/core/styles';
import TableCell from '@material-ui/core/TableCell';
import TableRow from '@material-ui/core/TableRow';
import * as BadgeImpl from '@material-ui/core/Badge/Badge';
import DragIndicator from '@material-ui/icons/DragIndicator';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import PauseIcon from '@material-ui/icons/Pause';
import IconButton from '@material-ui/core/IconButton';
import FolderIcon from '@material-ui/icons/Folder';
import DeleteIcon from '@material-ui/icons/Delete';
import { DraggableProvided } from 'react-beautiful-dnd';
const useStyles = makeStyles(theme => ({
currentTrackRow: {
color: theme.palette.primary.main,
'& > td': {
color: 'inherit',
},
},
inGroupTrackRow: {
'& > $indexCell': {
transform: `translateX(${theme.spacing(3)}px)`,
},
'& > $titleCell': {
transform: `translateX(${theme.spacing(3)}px)`,
},
},
playButtonInTrackList: {
display: 'none',
},
trackRow: {
'&:hover': {
'& $playButtonInTrackList': {
display: 'inline-flex',
},
'& $trackIndex': {
display: 'none',
},
},
},
controlButtonInTrackCommon: {
width: theme.spacing(2),
height: theme.spacing(2),
verticalAlign: 'middle',
marginLeft: theme.spacing(-0.5),
},
formatBadge: {
...(BadgeImpl as any).styles(theme).badge,
...(BadgeImpl as any).styles(theme).colorPrimary,
position: 'static',
display: 'inline-flex',
border: `2px solid ${theme.palette.background.paper}`,
padding: '0 4px',
verticalAlign: 'middle',
width: theme.spacing(4.5),
marginRight: theme.spacing(0.5),
},
durationCell: {
whiteSpace: 'nowrap',
},
durationCellSecondary: {
whiteSpace: 'nowrap',
color: theme.palette.text.secondary,
},
durationCellTime: {
verticalAlign: 'middle',
},
titleCell: {
overflow: 'hidden',
maxWidth: '40ch',
textOverflow: 'ellipsis',
// whiteSpace: 'nowrap',
},
deleteGroupButton: {
display: 'none',
},
indexCell: {
whiteSpace: 'nowrap',
paddingRight: 0,
width: theme.spacing(4),
},
trackIndex: {
display: 'inline-block',
height: '16px',
width: '16px',
},
dragHandle: {
width: 20,
padding: `${theme.spacing(0.5)}px 0 0 0`,
},
dragHandleEmpty: {
width: 20,
padding: `${theme.spacing(0.5)}px 0 0 0`,
},
groupFolderIcon: {},
groupHeadRow: {
'&:hover': {
'& $deleteGroupButton': {
display: 'inline-flex',
},
'& $groupFolderIcon': {
display: 'none',
},
},
},
}));
interface TrackRowProps {
track: Track;
inGroup: boolean;
isSelected: boolean;
trackStatus: 'playing' | 'paused' | 'none';
draggableProvided: DraggableProvided;
onSelect: (event: React.MouseEvent, trackIdx: number) => void;
onRename: (event: React.MouseEvent, trackIdx: number) => void;
onTogglePlayPause: (event: React.MouseEvent, trackIdx: number) => void;
}
export function TrackRow({
track,
inGroup,
isSelected,
draggableProvided,
trackStatus,
onSelect,
onRename,
onTogglePlayPause,
}: TrackRowProps) {
const classes = useStyles();
const handleRename = useCallback(event => onRename(event, track.index), [track.index, onRename]);
const handleSelect = useCallback(event => onSelect(event, track.index), [track.index, onSelect]);
const handlePlayPause: React.MouseEventHandler = useCallback(
event => {
event.stopPropagation();
onTogglePlayPause(event, track.index);
},
[track.index, onTogglePlayPause]
);
const handleDoubleClickOnPlayButton: React.MouseEventHandler = useCallback(event => event.stopPropagation(), []);
const isPlayingOrPaused = trackStatus === 'playing' || trackStatus === 'paused';
return (
<TableRow
{...draggableProvided.draggableProps}
ref={draggableProvided.innerRef}
hover
selected={isSelected}
onDoubleClick={handleRename}
onClick={handleSelect}
color="inherit"
className={clsx(classes.trackRow, { [classes.inGroupTrackRow]: inGroup, [classes.currentTrackRow]: isPlayingOrPaused })}
>
<TableCell className={classes.dragHandle} {...draggableProvided.dragHandleProps} onClick={event => event.stopPropagation()}>
<DragIndicator fontSize="small" color="disabled" />
</TableCell>
<TableCell className={classes.indexCell}>
<span className={classes.trackIndex}>{track.index + 1}</span>
<IconButton
aria-label="delete"
className={clsx(classes.controlButtonInTrackCommon, classes.playButtonInTrackList)}
size="small"
onClick={handlePlayPause}
onDoubleClick={handleDoubleClickOnPlayButton}
>
{trackStatus === 'paused' || trackStatus === 'none' ? (
<PlayArrowIcon fontSize="inherit" />
) : (
<PauseIcon fontSize="inherit" />
)}
</IconButton>
</TableCell>
<TableCell className={classes.titleCell} title={track.title ?? ''}>
{track.fullWidthTitle ? `${track.fullWidthTitle} / ` : ``}
{track.title || `No Title`}
</TableCell>
<TableCell align="right" className={classes.durationCell}>
<span className={classes.formatBadge}>{EncodingName[track.encoding]}</span>
<span className={classes.durationCellTime}>{formatTimeFromFrames(track.duration, false)}</span>
</TableCell>
</TableRow>
);
}
interface GroupRowProps {
group: Group;
onRename: (event: React.MouseEvent, groupIdx: number) => void;
onDelete: (event: React.MouseEvent, groupIdx: number) => void;
}
export function GroupRow({ group, onRename, onDelete }: GroupRowProps) {
const classes = useStyles();
const handleDelete = useCallback((event: React.MouseEvent) => onDelete(event, group.index), [onDelete, group]);
const handleRename = useCallback((event: React.MouseEvent) => onRename(event, group.index), [onRename, group]);
return (
<TableRow hover className={classes.groupHeadRow} onDoubleClick={handleRename}>
<TableCell className={classes.dragHandleEmpty}></TableCell>
<TableCell className={classes.indexCell}>
<FolderIcon className={clsx(classes.controlButtonInTrackCommon, classes.groupFolderIcon)} />
<IconButton
aria-label="delete"
className={clsx(classes.controlButtonInTrackCommon, classes.deleteGroupButton)}
size="small"
onClick={handleDelete}
>
<DeleteIcon fontSize="inherit" />
</IconButton>
</TableCell>
<TableCell className={classes.titleCell} title={group.title!}>
{group.fullWidthTitle ? `${group.fullWidthTitle} / ` : ``}
{group.title || `No Name`}
</TableCell>
<TableCell align="right" className={classes.durationCellSecondary}>
<span className={classes.durationCellTime}>
{formatTimeFromFrames(
group.tracks.map(n => n.duration).reduce((a, b) => a + b),
false
)}
</span>
</TableCell>
</TableRow>
);
}

View File

@ -17,18 +17,10 @@ import { actions as renameDialogActions } from '../redux/rename-dialog-feature';
import { actions as convertDialogActions } from '../redux/convert-dialog-feature';
import { actions as dumpDialogActions } from '../redux/dump-dialog-feature';
import { formatTimeFromFrames, Track } from 'netmd-js';
import { DeviceStatus, formatTimeFromFrames, Track } from 'netmd-js';
import { control } from '../redux/actions';
import {
belowDesktop,
forAnyDesktop,
getGroupedTracks,
getSortedTracks,
isSequential,
useShallowEqualSelector,
EncodingName,
} from '../utils';
import { belowDesktop, forAnyDesktop, getGroupedTracks, getSortedTracks, isSequential, useShallowEqualSelector } from '../utils';
import { lighten, makeStyles } from '@material-ui/core/styles';
import { fade } from '@material-ui/core/styles/colorManipulator';
@ -39,9 +31,6 @@ import AddIcon from '@material-ui/icons/Add';
import DeleteIcon from '@material-ui/icons/Delete';
import EditIcon from '@material-ui/icons/Edit';
import Backdrop from '@material-ui/core/Backdrop';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import PauseIcon from '@material-ui/icons/Pause';
import FolderIcon from '@material-ui/icons/Folder';
import CreateNewFolderIcon from '@material-ui/icons/CreateNewFolder';
import Table from '@material-ui/core/Table';
@ -55,6 +44,7 @@ import Toolbar from '@material-ui/core/Toolbar';
import Tooltip from '@material-ui/core/Tooltip';
import { batchActions } from 'redux-batched-actions';
import { GroupRow, TrackRow } from './main-rows';
import { RenameDialog } from './rename-dialog';
import { UploadDialog } from './upload-dialog';
import { RecordDialog } from './record-dialog';
@ -65,7 +55,6 @@ import { AboutDialog } from './about-dialog';
import { DumpDialog } from './dump-dialog';
import { TopMenu } from './topmenu';
import Checkbox from '@material-ui/core/Checkbox';
import * as BadgeImpl from '@material-ui/core/Badge/Badge';
import Button from '@material-ui/core/Button';
import { W95Main } from './win95/main';
import { useMemo } from 'react';
@ -120,33 +109,10 @@ const useStyles = makeStyles(theme => ({
spacing: {
marginTop: theme.spacing(1),
},
formatBadge: {
...(BadgeImpl as any).styles(theme).badge,
...(BadgeImpl as any).styles(theme).colorPrimary,
position: 'static',
display: 'inline-flex',
border: `2px solid ${theme.palette.background.paper}`,
padding: '0 4px',
verticalAlign: 'middle',
width: theme.spacing(4.5),
marginRight: theme.spacing(0.5),
},
titleCell: {
overflow: 'hidden',
maxWidth: '40ch',
textOverflow: 'ellipsis',
// whiteSpace: 'nowrap',
},
durationCell: {
whiteSpace: 'nowrap',
},
durationCellTime: {
verticalAlign: 'middle',
},
indexCell: {
whiteSpace: 'nowrap',
paddingRight: 0,
width: `16px`,
width: theme.spacing(4),
},
backdrop: {
zIndex: theme.zIndex.drawer + 1,
@ -156,91 +122,41 @@ const useStyles = makeStyles(theme => ({
textDecoration: 'underline',
textDecorationStyle: 'dotted',
},
controlButtonInTrackCommon: {
width: `16px`,
height: `16px`,
verticalAlign: 'middle',
cursor: 'pointer',
marginLeft: theme.spacing(-0.5),
},
playButtonInTrackListPlaying: {
color: theme.palette.primary.main,
display: 'none',
},
pauseButtonInTrackListPlaying: {
color: theme.palette.primary.main,
display: 'none',
},
currentControlButton: {
display: 'inline-block',
},
playButtonInTrackListNotPlaying: {
width: `0px`,
},
trackRow: {
userSelect: 'none',
'&:hover': {
/* For the tracks that aren't currently playing */
'& $playButtonInTrackListNotPlaying': {
width: `16px`,
},
'& $trackIndex': {
width: `0px`,
display: 'none',
},
/* For the current track */
'& svg:not($currentControlButton)': {
display: 'inline-block',
},
'& $currentControlButton': {
display: 'none',
},
},
},
inGroupTrackRow: {
'& > $indexCell': {
transform: `translateX(${theme.spacing(3)}px)`,
},
'& > $titleCell': {
transform: `translateX(${theme.spacing(3)}px)`,
},
},
deleteGroupButton: {
display: 'none',
},
groupHeadRow: {
'&:hover': {
'& svg:not($deleteGroupButton)': {
display: 'none',
},
'& $deleteGroupButton': {
display: 'inline-block',
},
},
},
hoveringOverGroup: {
backgroundColor: `${fade(theme.palette.secondary.dark, 0.4)}`,
},
trackIndex: {
display: 'inline-block',
height: '16px',
width: '16px',
dragHandleEmpty: {
width: 20,
padding: `${theme.spacing(0.5)}px 0 0 0`,
},
}));
function getTrackStatus(track: Track, deviceStatus: DeviceStatus | null): 'playing' | 'paused' | 'none' {
if (!deviceStatus || track.index !== deviceStatus.track) {
return 'none';
}
if (deviceStatus.state === 'playing') {
return 'playing';
} else if (deviceStatus.state === 'paused') {
return 'paused';
} else {
return 'none';
}
}
export const Main = (props: {}) => {
let dispatch = useDispatch();
let disc = useShallowEqualSelector(state => state.main.disc);
let deviceName = useShallowEqualSelector(state => state.main.deviceName);
const disc = useShallowEqualSelector(state => state.main.disc);
const deviceName = useShallowEqualSelector(state => state.main.deviceName);
const deviceStatus = useShallowEqualSelector(state => state.main.deviceStatus);
const { vintageMode } = useShallowEqualSelector(state => state.appState);
const [selected, setSelected] = React.useState<number[]>([]);
const [uploadedFiles, setUploadedFiles] = React.useState<File[]>([]);
const [lastClicked, setLastClicked] = useState(-1);
const selectedCount = selected.length;
const [moveMenuAnchorEl, setMoveMenuAnchorEl] = React.useState<null | HTMLElement>(null);
const handleShowMoveMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
setMoveMenuAnchorEl(event.currentTarget);
@ -250,6 +166,7 @@ export const Main = (props: {}) => {
const handleCloseMoveMenu = useCallback(() => {
setMoveMenuAnchorEl(null);
}, [setMoveMenuAnchorEl]);
const handleMoveSelectedTrack = useCallback(
(destIndex: number) => {
dispatch(moveTrack(selected[0], destIndex));
@ -282,7 +199,6 @@ export const Main = (props: {}) => {
setSelected([]); // Reset selection if disc changes
}, [disc]);
let [uploadedFiles, setUploadedFiles] = React.useState<File[]>([]);
const onDrop = useCallback(
(acceptedFiles: File[], rejectedFiles: File[]) => {
setUploadedFiles(acceptedFiles);
@ -290,6 +206,7 @@ export const Main = (props: {}) => {
},
[dispatch]
);
const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
onDrop,
accept: [`audio/*`, `video/mp4`],
@ -301,7 +218,8 @@ export const Main = (props: {}) => {
const groupedTracks = useMemo(() => getGroupedTracks(disc), [disc]);
// Action Handlers
const handleSelectClick = (event: React.MouseEvent, item: number) => {
const handleSelectTrackClick = useCallback(
(event: React.MouseEvent, item: number) => {
if (event.shiftKey && selected.length && lastClicked !== -1) {
let rangeBegin = Math.min(lastClicked + 1, item),
rangeEnd = Math.max(lastClicked - 1, item);
@ -319,70 +237,106 @@ export const Main = (props: {}) => {
setSelected([...selected, item]);
}
setLastClicked(item);
};
},
[selected, setSelected, lastClicked, setLastClicked]
);
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
const handleSelectAllClick = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
if (selected.length < tracks.length) {
setSelected(tracks.map(t => t.index));
} else {
setSelected([]);
}
};
},
[selected, tracks]
);
const handleRenameDoubleClick = (event: React.MouseEvent, index: number, renameGroup?: boolean) => {
let group, track;
if (renameGroup) {
group = groupedTracks.find(n => n.tracks[0]?.index === index);
if (!group) return;
} else {
track = tracks.find(n => n.index === index);
if (!track) return;
const handleRenameTrack = useCallback(
(event: React.MouseEvent, index: number) => {
let track = tracks.find(t => t.index === index);
if (!track) {
return;
}
let currentName = (track ? track.title : group?.title) ?? '';
let currentFullWidthName = (track ? track.fullWidthTitle : group?.fullWidthTitle) ?? '';
dispatch(
batchActions([
renameDialogActions.setVisible(true),
renameDialogActions.setGroupIndex(group ? index : null),
renameDialogActions.setCurrentName(currentName),
renameDialogActions.setCurrentFullWidthName(currentFullWidthName),
renameDialogActions.setIndex(track?.index ?? -1),
renameDialogActions.setGroupIndex(null),
renameDialogActions.setCurrentName(track.title),
renameDialogActions.setCurrentFullWidthName(track.fullWidthTitle),
renameDialogActions.setIndex(track.index),
])
);
};
},
[dispatch, tracks]
);
const handleRenameActionClick = (event: React.MouseEvent) => {
if(event.detail !== 1) return; //Event retriggering when hitting enter in the dialog
handleRenameDoubleClick(event, selected[0]);
};
const handleDeleteSelected = (event: React.MouseEvent) => {
dispatch(deleteTracks(selected));
};
const handleGroupTracks = (event: React.MouseEvent) => {
dispatch(groupTracks(selected));
};
const handleGroupRemoval = (event: React.MouseEvent, groupBegin: number) => {
dispatch(deleteGroup(groupBegin));
};
const handlePlayTrack = async (event: React.MouseEvent, track: number) => {
if (deviceStatus?.track !== track) {
dispatch(control('goto', track));
const handleRenameGroup = useCallback(
(event: React.MouseEvent, index: number) => {
let group = groupedTracks.find(g => g.index === index);
if (!group) {
return;
}
if (deviceStatus?.state !== 'playing') {
dispatch(
batchActions([
renameDialogActions.setVisible(true),
renameDialogActions.setGroupIndex(index),
renameDialogActions.setCurrentName(group.title ?? ''),
renameDialogActions.setCurrentFullWidthName(group.fullWidthTitle ?? ''),
renameDialogActions.setIndex(-1),
])
);
},
[dispatch, groupedTracks]
);
const handleRenameActionClick = useCallback(
(event: React.MouseEvent) => {
if (event.detail !== 1) return; //Event retriggering when hitting enter in the dialog
handleRenameTrack(event, selected[0]);
},
[handleRenameTrack, selected]
);
const handleDeleteSelected = useCallback(
(event: React.MouseEvent) => {
dispatch(deleteTracks(selected));
},
[dispatch, selected]
);
const handleGroupTracks = useCallback(
(event: React.MouseEvent) => {
dispatch(groupTracks(selected));
},
[dispatch, selected]
);
const handleDeleteGroup = useCallback(
(event: React.MouseEvent, index: number) => {
dispatch(deleteGroup(index));
},
[dispatch]
);
const handleTogglePlayPauseTrack = useCallback(
(event: React.MouseEvent, track: number) => {
if (!deviceStatus) {
return;
}
if (deviceStatus.track !== track) {
dispatch(control('goto', track));
if (deviceStatus.state !== 'playing') {
dispatch(control('play'));
}
};
const handleCurrentClick = async (event: React.MouseEvent) => {
if (deviceStatus?.state === 'playing') {
} else if (deviceStatus.state === 'playing') {
dispatch(control('pause'));
} else dispatch(control('play'));
};
}
},
[dispatch, deviceStatus]
);
const canGroup = useMemo(() => {
return (
@ -390,6 +344,7 @@ export const Main = (props: {}) => {
isSequential(selected.sort((a, b) => a - b))
);
}, [tracks, selected]);
const selectedCount = selected.length;
if (vintageMode) {
const p = {
@ -419,71 +374,13 @@ export const Main = (props: {}) => {
handleShowDumpDialog,
handleDeleteSelected,
handleRenameActionClick,
handleRenameDoubleClick,
handleRenameTrack,
handleSelectAllClick,
handleSelectClick,
handleSelectTrackClick,
};
return <W95Main {...p} />;
}
const getTrackRow = (track: Track, inGroup: boolean, additional: {}) => {
const child = (
<TableRow
{...additional}
hover
selected={selected.includes(track.index)}
onDoubleClick={event => handleRenameDoubleClick(event, track.index)}
onClick={event => handleSelectClick(event, track.index)}
className={clsx(classes.trackRow, { [classes.inGroupTrackRow]: inGroup })}
>
<TableCell className={classes.indexCell}>
{track.index === deviceStatus?.track && ['playing', 'paused'].includes(deviceStatus?.state) ? (
<span>
<PlayArrowIcon
className={clsx(classes.controlButtonInTrackCommon, classes.playButtonInTrackListPlaying, {
[classes.currentControlButton]: deviceStatus?.state === 'playing',
})}
onClick={event => {
handleCurrentClick(event);
event.stopPropagation();
}}
/>
<PauseIcon
className={clsx(classes.controlButtonInTrackCommon, classes.pauseButtonInTrackListPlaying, {
[classes.currentControlButton]: deviceStatus?.state === 'paused',
})}
onClick={event => {
handleCurrentClick(event);
event.stopPropagation();
}}
/>
</span>
) : (
<span>
<span className={classes.trackIndex}>{track.index + 1}</span>
<PlayArrowIcon
className={clsx(classes.controlButtonInTrackCommon, classes.playButtonInTrackListNotPlaying)}
onClick={event => {
handlePlayTrack(event, track.index);
event.stopPropagation();
}}
/>
</span>
)}
</TableCell>
<TableCell className={classes.titleCell} title={track.title ?? ''}>
{track.fullWidthTitle ? `${track.fullWidthTitle} / ` : ``}
{track.title || `No Title`}
</TableCell>
<TableCell align="right" className={classes.durationCell}>
<span className={classes.formatBadge}>{EncodingName[track.encoding]}</span>
<span className={classes.durationCellTime}>{formatTimeFromFrames(track.duration, false)}</span>
</TableCell>
</TableRow>
);
return child;
};
return (
<React.Fragment>
<Box className={classes.headBox}>
@ -575,6 +472,7 @@ export const Main = (props: {}) => {
<Table size="small">
<TableHead>
<TableRow>
<TableCell className={classes.dragHandleEmpty}></TableCell>
<TableCell className={classes.indexCell}>#</TableCell>
<TableCell>Title</TableCell>
<TableCell align="right">Duration</TableCell>
@ -584,7 +482,7 @@ export const Main = (props: {}) => {
<TableBody>
{groupedTracks.map((group, index) => (
<TableRow key={`${index}`}>
<TableCell colSpan={3} style={{ padding: '0' }}>
<TableCell colSpan={4} style={{ padding: '0' }}>
<Table size="small">
<Droppable droppableId={`${index}`} key={`${index}`}>
{(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (
@ -594,56 +492,33 @@ export const Main = (props: {}) => {
className={clsx({ [classes.hoveringOverGroup]: snapshot.isDraggingOver })}
>
{group.title !== null && (
<TableRow
hover
className={classes.groupHeadRow}
key={`${index}-head`}
onDoubleClick={event =>
handleRenameDoubleClick(event, group.tracks[0].index, true)
}
>
<TableCell className={classes.indexCell}>
<FolderIcon className={classes.controlButtonInTrackCommon} />
<DeleteIcon
className={clsx(
classes.controlButtonInTrackCommon,
classes.deleteGroupButton
)}
onClick={event => {
handleGroupRemoval(event, group.tracks[0].index);
}}
<GroupRow
group={group}
onRename={handleRenameGroup}
onDelete={handleDeleteGroup}
/>
</TableCell>
<TableCell className={classes.titleCell} title={group.title!}>
{group.fullWidthTitle ? `${group.fullWidthTitle} / ` : ``}
{group.title || `No Name`}
</TableCell>
<TableCell align="right" className={classes.durationCell}>
<span className={classes.durationCellTime}>
{formatTimeFromFrames(
group.tracks.map(n => n.duration).reduce((a, b) => a + b),
false
)}
</span>
</TableCell>
</TableRow>
)}
{group.title === null && group.tracks.length === 0 && (
<TableRow style={{ height: '1px' }} />
)}
{group.tracks.map(n => (
{group.tracks.map((t, tidx) => (
<Draggable
draggableId={`${group.index}-${n.index}`}
key={`${n.index - group.tracks[0].index}`}
index={n.index - group.tracks[0].index}
draggableId={`${group.index}-${t.index}`}
key={`t-${t.index}`}
index={tidx}
>
{(providedInGroup: DraggableProvided) =>
getTrackRow(n, group.title !== null, {
ref: providedInGroup.innerRef,
...providedInGroup.draggableProps,
...providedInGroup.dragHandleProps,
})
}
{(provided: DraggableProvided) => (
<TrackRow
track={t}
draggableProvided={provided}
inGroup={group.title !== null}
isSelected={selected.includes(t.index)}
trackStatus={getTrackStatus(t, deviceStatus)}
onSelect={handleSelectTrackClick}
onRename={handleRenameTrack}
onTogglePlayPause={handleTogglePlayPauseTrack}
/>
)}
</Draggable>
))}
{provided.placeholder}
@ -657,9 +532,11 @@ export const Main = (props: {}) => {
</TableBody>
</DragDropContext>
</Table>
{isDragActive ? (
<Backdrop className={classes.backdrop} open={isDragActive}>
Drop your Music to Upload
</Backdrop>
) : null}
</Box>
<Fab color="primary" aria-label="add" className={classes.add} onClick={open}>
<AddIcon />

View File

@ -42,11 +42,12 @@ export const RenameDialog = (props: {}) => {
const what = renameDialogGroupIndex !== null ? `Group` : renameDialogIndex < 0 ? `Disc` : `Track`;
const handleCancelRename = () => {
const handleCancelRename = useCallback(() => {
dispatch(renameDialogActions.setVisible(false));
};
}, [dispatch]);
const handleDoRename = () => {
const handleDoRename = useCallback(() => {
console.log(renameDialogGroupIndex, renameDialogIndex);
if (renameDialogGroupIndex !== null) {
// Just rename the group with this range
dispatch(
@ -73,7 +74,7 @@ export const RenameDialog = (props: {}) => {
);
}
handleCancelRename(); // Close the dialog
};
}, [dispatch, handleCancelRename, renameDialogFullWidthTitle, renameDialogGroupIndex, renameDialogIndex, renameDialogTitle]);
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
@ -89,6 +90,17 @@ export const RenameDialog = (props: {}) => {
[dispatch]
);
const handleEnterKeyEvent = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === `Enter`) {
event.stopPropagation();
event.preventDefault();
handleDoRename();
}
},
[handleDoRename]
);
const { vintageMode } = useShallowEqualSelector(state => state.appState);
if (vintageMode) {
const p = {
@ -121,9 +133,7 @@ export const RenameDialog = (props: {}) => {
type="text"
fullWidth
value={renameDialogTitle}
onKeyDown={event => {
event.key === `Enter` && handleDoRename();
}}
onKeyDown={handleEnterKeyEvent}
onChange={handleChange}
/>
{allowFullWidth && (
@ -134,9 +144,7 @@ export const RenameDialog = (props: {}) => {
fullWidth
className={classes.marginUpDown}
value={renameDialogFullWidthTitle}
onKeyDown={event => {
event.key === `Enter` && handleDoRename();
}}
onKeyDown={handleEnterKeyEvent}
onChange={handleFullWidthChange}
/>
)}

View File

@ -170,7 +170,7 @@ export const TopMenu = function(props: { onClick?: () => void }) {
);
}
if (mainView === 'MAIN') {
menuItems.push(<Divider />);
menuItems.push(<Divider key="action-divider" />);
menuItems.push(
<MenuItem key="allowFullWidth" onClick={handleAllowFullWidth}>
<ListItemIcon className={classes.listItemIcon}>
@ -208,7 +208,7 @@ export const TopMenu = function(props: { onClick?: () => void }) {
);
}
if (mainView === 'MAIN') {
menuItems.push(<Divider />);
menuItems.push(<Divider key="feature-divider" />);
}
menuItems.push(
<MenuItem key="about" onClick={handleShowAbout}>

View File

@ -106,9 +106,9 @@ export const W95Main = (props: {
handleShowDumpDialog: () => void;
handleDeleteSelected: (event: React.MouseEvent) => void;
handleRenameActionClick: (event: React.MouseEvent) => void;
handleRenameDoubleClick: (event: React.MouseEvent, item: number) => void;
handleRenameTrack: (event: React.MouseEvent, item: number) => void;
handleSelectAllClick: (event: React.ChangeEvent<HTMLInputElement>) => void;
handleSelectClick: (event: React.MouseEvent, item: number) => void;
handleSelectTrackClick: (event: React.MouseEvent, item: number) => void;
}) => {
const classes = useStyles();
const themeContext = useContext(ThemeContext);
@ -199,12 +199,15 @@ export const W95Main = (props: {
<CustomTableRow
style={props.selected.includes(track.index) ? themeContext.selectedTableRow : {}}
key={track.index}
onDoubleClick={(event: React.MouseEvent) => props.handleRenameDoubleClick(event, track.index)}
onClick={(event: React.MouseEvent) => props.handleSelectClick(event, track.index)}
onDoubleClick={(event: React.MouseEvent) => props.handleRenameTrack(event, track.index)}
onClick={(event: React.MouseEvent) => props.handleSelectTrackClick(event, track.index)}
>
<TableDataCell style={{ textAlign: 'center', width: '2ch' }}>{track.index + 1}</TableDataCell>
<TableDataCell style={{ width: '80%' }}>
<div>{track.fullWidthTitle && `${track.fullWidthTitle} / `}{track.title || `No Title`}</div>
<div>
{track.fullWidthTitle && `${track.fullWidthTitle} / `}
{track.title || `No Title`}
</div>
</TableDataCell>
<TableDataCell style={{ textAlign: 'right', width: '20%' }}>
<span>{track.encoding}</span>

View File

@ -76,10 +76,10 @@ export function groupTracks(indexes: number[]) {
};
}
export function deleteGroup(groupBegin: number) {
export function deleteGroup(index: number) {
return async function(dispatch: AppDispatch) {
const { netmdService } = serviceRegistry;
netmdService!.deleteGroup(groupBegin);
netmdService!.deleteGroup(index);
listContent()(dispatch);
};
}

View File

@ -55,21 +55,26 @@ class NetMDMockService implements NetMDService {
channel: Channels.stereo,
protected: TrackFlag.unprotected,
title: 'Mock Track 5',
fullWidthTitle: '',
fullWidthTitle: 'スコット と リバース',
},
];
public _groups: Group[] = [
public _groupsDef: {
index: number;
title: string | null;
fullWidthTitle: string | null;
tracksIdx: number[];
}[] = [
{
title: null,
index: 0,
tracks: this._tracks.slice(2),
fullWidthTitle: '',
index: 0,
tracksIdx: [2, 3, 4],
},
{
title: 'Test',
fullWidthTitle: '',
index: 0,
tracks: [this._tracks[0], this._tracks[1]],
index: 1,
tracksIdx: [0, 1],
},
];
@ -80,6 +85,15 @@ class NetMDMockService implements NetMDService {
state: 'ready',
};
public _getGroups(): Group[] {
return this._groupsDef.map(g => ({
title: g.title,
index: g.index,
tracks: this._tracks.filter(t => g.tracksIdx.includes(t.index)),
fullWidthTitle: g.fullWidthTitle,
}));
}
_updateTrackIndexes() {
for (let i = 0; i < this._tracks.length; i++) {
this._tracks[i].index = i;
@ -108,7 +122,7 @@ class NetMDMockService implements NetMDService {
used: this._getUsed(),
total: this._discCapacity,
trackCount: this._tracks.length,
groups: this._groups,
groups: this._getGroups(),
};
}
@ -126,55 +140,72 @@ class NetMDMockService implements NetMDService {
return JSON.parse(JSON.stringify(this._getDisc()));
}
async renameGroup(groupBegin: number, newName: string, newFullWidth?: string) {
let group = this._groups.slice(1).find(n => n.index === groupBegin);
if (!group) return;
async renameGroup(gropuIndex: number, newName: string, newFullWidth?: string) {
let group = this._groupsDef.find(n => n.index === gropuIndex);
if (!group) {
return;
}
group.title = newName;
if (newFullWidth !== undefined) group.fullWidthTitle = newFullWidth;
if (newFullWidth !== undefined) {
group.fullWidthTitle = newFullWidth;
}
}
async addGroup(groupBegin: number, groupLength: number, newName: string) {
let ungrouped = this._groups.find(n => n.title === null);
if (!ungrouped) return; // You can only group tracks that aren't already in a different group, if there's no such tracks, there's no point to continue
let ungroupedLengthBeforeGroup = ungrouped.tracks.length;
let thisGroupTracks = ungrouped.tracks.filter(n => n.index >= groupBegin && n.index < groupBegin + groupLength);
ungrouped.tracks = ungrouped.tracks.filter(n => !thisGroupTracks.includes(n));
if (ungroupedLengthBeforeGroup - ungrouped.tracks.length !== groupLength) {
throw new Error('A track cannot be in 2 groups!');
let ungroupedDefs = this._groupsDef.find(g => g.title === null);
if (!ungroupedDefs) {
return; // You can only group tracks that aren't already in a different group, if there's no such tracks, there's no point to continue
}
let ungroupedLengthBeforeGroup = ungroupedDefs.tracksIdx.length;
if (!isSequential(thisGroupTracks.map(n => n.index))) {
const newGroupTracks = ungroupedDefs.tracksIdx.filter(idx => idx >= groupBegin && idx < groupBegin + groupLength);
if (!isSequential(newGroupTracks)) {
throw new Error('Invalid sequence of tracks!');
}
this._groups.push({
const newGroupDef = {
title: newName,
fullWidthTitle: '',
index: groupBegin,
tracks: thisGroupTracks,
});
tracksIdx: newGroupTracks,
};
this._groupsDef.push(newGroupDef);
this._groupsDef = this._groupsDef.filter(g => g.tracksIdx.length !== 0).sort((a, b) => a.tracksIdx[0] - b.tracksIdx[0]);
ungroupedDefs.tracksIdx = ungroupedDefs.tracksIdx.filter(idx => !newGroupTracks.includes(idx));
if (ungroupedLengthBeforeGroup - ungroupedDefs.tracksIdx.length !== groupLength) {
throw new Error('A track cannot be in 2 groups!');
}
}
async deleteGroup(groupBegin: number) {
const thisGroup = this._groups.slice(1).find(n => n.tracks[0].index === groupBegin);
if (!thisGroup) return;
let ungroupedGroup = this._groups.find(n => n.title === null);
async deleteGroup(index: number) {
const groups = this._getGroups();
const group = groups.find(g => g.index === index);
if (!group) {
return;
}
let ungroupedGroup = this._groupsDef.find(n => n.title === null);
if (!ungroupedGroup) {
ungroupedGroup = {
title: null,
fullWidthTitle: null,
tracks: [],
tracksIdx: [],
index: 0,
};
this._groups.unshift(ungroupedGroup);
this._groupsDef.unshift(ungroupedGroup);
}
ungroupedGroup.tracks = ungroupedGroup.tracks.concat(thisGroup.tracks).sort((a, b) => a.index - b.index);
this._groups.splice(this._groups.indexOf(thisGroup), 1);
ungroupedGroup.tracksIdx = ungroupedGroup.tracksIdx.concat(group.tracks.map(t => t.index)).sort();
this._groupsDef.splice(groups.indexOf(group), 1);
}
async rewriteGroups(groups: Group[]) {
this._groups = [...groups];
this._groupsDef = groups.map(g => ({
title: g.title,
fullWidthTitle: g.fullWidthTitle,
index: g.index,
tracksIdx: g.tracks.map(t => t.index),
}));
}
async getDeviceStatus() {
@ -207,9 +238,16 @@ class NetMDMockService implements NetMDService {
indexes = indexes.sort();
indexes.reverse();
for (let index of indexes) {
this._groups = recomputeGroupsAfterTrackMove(this._getDisc(), index, -1).groups;
this._groupsDef = recomputeGroupsAfterTrackMove(this._getDisc(), index, -1).groups.map(g => ({
title: g.title,
fullWidthTitle: g.fullWidthTitle,
index: g.index,
tracksIdx: g.tracks.map(t => t.index),
}));
this._tracks.splice(index, 1);
this._groups.forEach(n => (n.tracks = n.tracks.filter(n => this._tracks.includes(n))));
this._groupsDef.forEach(
g => (g.tracksIdx = g.tracksIdx.filter(tidx => this._tracks.find(t => t.index === tidx) !== undefined))
);
}
this._updateTrackIndexes();
}
@ -219,7 +257,14 @@ class NetMDMockService implements NetMDService {
assert(t.length === 1);
this._tracks.splice(dst, 0, t[0]);
this._updateTrackIndexes();
if (updateGroups || updateGroups === undefined) this._groups = recomputeGroupsAfterTrackMove(this._getDisc(), src, dst).groups;
if (updateGroups || updateGroups === undefined) {
this._groupsDef = recomputeGroupsAfterTrackMove(this._getDisc(), src, dst).groups.map(g => ({
title: g.title,
fullWidthTitle: g.fullWidthTitle,
index: g.index,
tracksIdx: g.tracks.map(t => t.index),
}));
}
}
async wipeDisc() {
@ -227,12 +272,12 @@ class NetMDMockService implements NetMDService {
}
async wipeDiscTitleInfo() {
this._groups = [
this._groupsDef = [
{
index: 0,
title: null,
fullWidthTitle: null,
tracks: this._tracks,
tracksIdx: this._tracks.map(t => t.index),
},
];
this._discTitle = '';
@ -262,7 +307,7 @@ class NetMDMockService implements NetMDService {
await sleep(1000);
}
this._tracks.push({
const newTrack = {
title: halfWidthTitle,
duration: 5 * 60 * 512,
encoding: Encoding.sp,
@ -270,7 +315,9 @@ class NetMDMockService implements NetMDService {
protected: TrackFlag.unprotected,
channel: 0,
fullWidthTitle: fullWidthTitle,
});
};
this._tracks.push(newTrack);
this._groupsDef[0].tracksIdx.push(newTrack.index);
await sleep(1000);
progressCallback({ written: 100, encrypted: 100, total: 100 });
@ -283,7 +330,7 @@ class NetMDMockService implements NetMDService {
@asyncMutex
async pause() {
console.log('pause');
this._status.state = 'paused';
}
@asyncMutex

View File

@ -36,7 +36,7 @@ export interface NetMDService {
finalize(): Promise<void>;
renameTrack(index: number, newTitle: string, newFullWidthTitle?: string): Promise<void>;
renameDisc(newName: string, newFullWidthName?: string): Promise<void>;
renameGroup(groupBegin: number, newTitle: string, newFullWidthTitle?: string): Promise<void>;
renameGroup(groupIndex: number, newTitle: string, newFullWidthTitle?: string): Promise<void>;
addGroup(groupBegin: number, groupLength: number, name: string): Promise<void>;
deleteGroup(groupIndex: number): Promise<void>;
rewriteGroups(groups: Group[]): Promise<void>;
@ -96,7 +96,6 @@ export class NetMDUSBService implements NetMDService {
}
private async listContentUsingCache() {
// listContent takes a long time to execute (>3000ms), so I think caching it should speed up the app
if (!this.cachedContentList) {
console.log("There's no cached version of the TOC, caching");
this.cachedContentList = await listContent(this.netmdInterface!);
@ -173,13 +172,17 @@ export class NetMDUSBService implements NetMDService {
}
@asyncMutex
async renameGroup(groupBegin: number, newName: string, newFullWidthName?: string) {
async renameGroup(groupIndex: number, newName: string, newFullWidthName?: string) {
const disc = await this.listContentUsingCache();
let thisGroup = disc.groups.find(n => n.tracks[0].index === groupBegin);
if (!thisGroup) return;
let thisGroup = disc.groups.find(g => g.index === groupIndex);
if (!thisGroup) {
return;
}
thisGroup.title = newName;
if (newFullWidthName !== undefined) thisGroup.fullWidthTitle = newFullWidthName;
if (newFullWidthName !== undefined) {
thisGroup.fullWidthTitle = newFullWidthName;
}
await this.writeRawTitles(compileDiscTitles(disc));
}
@ -187,7 +190,10 @@ export class NetMDUSBService implements NetMDService {
async addGroup(groupBegin: number, groupLength: number, title: string) {
const disc = await this.listContentUsingCache();
let ungrouped = disc.groups.find(n => n.title === null);
if (!ungrouped) return; // You can only group tracks that aren't already in a different group, if there's no such tracks, there's no point to continue
if (!ungrouped) {
return; // You can only group tracks that aren't already in a different group, if there's no such tracks, there's no point to continue
}
let ungroupedLengthBeforeGroup = ungrouped.tracks.length;
let thisGroupTracks = ungrouped.tracks.filter(n => n.index >= groupBegin && n.index < groupBegin + groupLength);
@ -200,21 +206,25 @@ export class NetMDUSBService implements NetMDService {
if (!isSequential(thisGroupTracks.map(n => n.index))) {
throw new Error('Invalid sequence of tracks!');
}
disc.groups.push({
title,
fullWidthTitle: '',
index: groupBegin,
index: disc.groups.length,
tracks: thisGroupTracks,
});
disc.groups = disc.groups.filter(g => g.tracks.length !== 0).sort((a, b) => a.tracks[0].index - b.tracks[0].index);
await this.writeRawTitles(compileDiscTitles(disc));
}
@asyncMutex
async deleteGroup(groupBegin: number) {
async deleteGroup(index: number) {
const disc = await this.listContentUsingCache();
let thisGroup = disc.groups.find(n => n.tracks[0].index === groupBegin);
if (thisGroup) disc.groups.splice(disc.groups.indexOf(thisGroup), 1);
let groupIndex = disc.groups.findIndex(g => g.index === index);
if (groupIndex >= 0) {
disc.groups.splice(groupIndex, 1);
}
await this.writeRawTitles(compileDiscTitles(disc));
}

View File

@ -171,14 +171,18 @@ export function getSortedTracks(disc: Disc | null) {
}
export function getGroupedTracks(disc: Disc | null) {
if (!disc) return [];
if (!disc) {
return [];
}
let groupedList: Group[] = [];
let ungroupedTracks = [...(disc.groups.find(n => n.title === null)?.tracks ?? [])];
let lastIndex = 0;
for (let group of disc.groups) {
if (group.title === null) continue; // Ungrouped tracks
if (group.title === null) {
continue; // Ungrouped tracks
}
let toCopy = group.tracks[0].index - lastIndex;
groupedList.push({
index: -1,