Added support for full-width titles (full-width katakana, hiragana and kanji). Thank you for testing, @is-that-a-thing-now
This commit is contained in:
parent
01686324ed
commit
206e92b495
|
@ -142,9 +142,10 @@ export const Controls = () => {
|
||||||
} else if (tracks.length === 0) {
|
} else if (tracks.length === 0) {
|
||||||
message = `BLANKDISC`;
|
message = `BLANKDISC`;
|
||||||
} else if (deviceStatus && deviceStatus.track !== null && tracks[deviceStatus.track]) {
|
} else if (deviceStatus && deviceStatus.track !== null && tracks[deviceStatus.track]) {
|
||||||
|
let title = tracks[deviceStatus.track].fullWidthTitle || tracks[deviceStatus.track].title;
|
||||||
message =
|
message =
|
||||||
(deviceStatus.track + 1).toString().padStart(3, '0') +
|
(deviceStatus.track + 1).toString().padStart(3, '0') +
|
||||||
(tracks[deviceStatus.track].title ? ' - ' + tracks[deviceStatus.track].title : '');
|
(title ? ' - ' + title : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [lcdScroll, setLcdScroll] = useState(0);
|
const [lcdScroll, setLcdScroll] = useState(0);
|
||||||
|
|
|
@ -86,13 +86,13 @@ const useStyles = makeStyles(theme => ({
|
||||||
toolbarHighlight:
|
toolbarHighlight:
|
||||||
theme.palette.type === 'light'
|
theme.palette.type === 'light'
|
||||||
? {
|
? {
|
||||||
color: theme.palette.secondary.main,
|
color: theme.palette.secondary.main,
|
||||||
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
|
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
backgroundColor: theme.palette.secondary.dark,
|
backgroundColor: theme.palette.secondary.dark,
|
||||||
},
|
},
|
||||||
headBox: {
|
headBox: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
@ -255,12 +255,15 @@ export const Main = (props: {}) => {
|
||||||
|
|
||||||
const handleRenameDoubleClick = (event: React.MouseEvent, item: number) => {
|
const handleRenameDoubleClick = (event: React.MouseEvent, item: number) => {
|
||||||
let selectedIndex = item;
|
let selectedIndex = item;
|
||||||
let currentName = getTracks(disc!).find(track => track.index === selectedIndex)?.title ?? '';
|
let track = getTracks(disc!).find(track => track.index === selectedIndex);
|
||||||
|
let currentName = track?.title ?? '';
|
||||||
|
let currentFullWidthName = track?.fullWidthTitle ?? '';
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
batchActions([
|
batchActions([
|
||||||
renameDialogActions.setVisible(true),
|
renameDialogActions.setVisible(true),
|
||||||
renameDialogActions.setCurrentName(currentName),
|
renameDialogActions.setCurrentName(currentName),
|
||||||
|
renameDialogActions.setCurrentFullWidthName(currentFullWidthName),
|
||||||
renameDialogActions.setIndex(selectedIndex),
|
renameDialogActions.setIndex(selectedIndex),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
@ -372,6 +375,7 @@ export const Main = (props: {}) => {
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Typography component="h3" variant="h6" className={classes.toolbarLabel}>
|
<Typography component="h3" variant="h6" className={classes.toolbarLabel}>
|
||||||
|
{disc?.fullWidthTitle && `${disc.fullWidthTitle} / `}
|
||||||
{disc?.title || `Untitled Disc`}
|
{disc?.title || `Untitled Disc`}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
@ -491,6 +495,7 @@ export const Main = (props: {}) => {
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={classes.titleCell} title={track.title}>
|
<TableCell className={classes.titleCell} title={track.title}>
|
||||||
|
{track.fullWidthTitle ? `${track.fullWidthTitle} / ` : ``}
|
||||||
{track.title || `No Title`}
|
{track.title || `No Title`}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right" className={classes.durationCell}>
|
<TableCell align="right" className={classes.durationCell}>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useShallowEqualSelector } from '../utils';
|
||||||
import { actions as renameDialogActions } from '../redux/rename-dialog-feature';
|
import { actions as renameDialogActions } from '../redux/rename-dialog-feature';
|
||||||
import { renameTrack, renameDisc } from '../redux/actions';
|
import { renameTrack, renameDisc } from '../redux/actions';
|
||||||
|
|
||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
import Dialog from '@material-ui/core/Dialog';
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
import DialogActions from '@material-ui/core/DialogActions';
|
import DialogActions from '@material-ui/core/DialogActions';
|
||||||
import DialogContent from '@material-ui/core/DialogContent';
|
import DialogContent from '@material-ui/core/DialogContent';
|
||||||
|
@ -21,12 +22,22 @@ const Transition = React.forwardRef(function Transition(
|
||||||
return <Slide direction="up" ref={ref} {...props} />;
|
return <Slide direction="up" ref={ref} {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const useStyles = makeStyles(theme => ({
|
||||||
|
marginUpDown: {
|
||||||
|
marginTop: theme.spacing(3),
|
||||||
|
marginBottom: theme.spacing(3),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
export const RenameDialog = (props: {}) => {
|
export const RenameDialog = (props: {}) => {
|
||||||
let dispatch = useDispatch();
|
let dispatch = useDispatch();
|
||||||
|
let classes = useStyles();
|
||||||
|
|
||||||
let renameDialogVisible = useShallowEqualSelector(state => state.renameDialog.visible);
|
let renameDialogVisible = useShallowEqualSelector(state => state.renameDialog.visible);
|
||||||
let renameDialogTitle = useShallowEqualSelector(state => state.renameDialog.title);
|
let renameDialogTitle = useShallowEqualSelector(state => state.renameDialog.title);
|
||||||
|
let renameDialogFullWidthTitle = useShallowEqualSelector(state => state.renameDialog.fullWidthTitle);
|
||||||
let renameDialogIndex = useShallowEqualSelector(state => state.renameDialog.index);
|
let renameDialogIndex = useShallowEqualSelector(state => state.renameDialog.index);
|
||||||
|
let allowFullWidth = useShallowEqualSelector(state => state.appState.fullWidthSupport);
|
||||||
|
|
||||||
const what = renameDialogIndex < 0 ? `Disc` : `Track`;
|
const what = renameDialogIndex < 0 ? `Disc` : `Track`;
|
||||||
|
|
||||||
|
@ -36,15 +47,27 @@ export const RenameDialog = (props: {}) => {
|
||||||
|
|
||||||
const handleDoRename = () => {
|
const handleDoRename = () => {
|
||||||
if (renameDialogIndex < 0) {
|
if (renameDialogIndex < 0) {
|
||||||
dispatch(renameDisc({ newName: renameDialogTitle }));
|
dispatch(renameDisc({
|
||||||
|
newName: renameDialogTitle,
|
||||||
|
newFullWidthName: renameDialogFullWidthTitle,
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
dispatch(renameTrack({ index: renameDialogIndex, newName: renameDialogTitle }));
|
dispatch(renameTrack({
|
||||||
|
index: renameDialogIndex,
|
||||||
|
newName: renameDialogTitle,
|
||||||
|
newFullWidthName: renameDialogFullWidthTitle,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
handleCancelRename(); //Close the dialog
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
(event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||||
dispatch(renameDialogActions.setCurrentName(event.target.value.substring(0, 120))); // MAX title length
|
if (event.target.id === "name") {
|
||||||
|
dispatch(renameDialogActions.setCurrentName(event.target.value.substring(0, 120))); // MAX title length
|
||||||
|
} else {
|
||||||
|
dispatch(renameDialogActions.setCurrentFullWidthName(event.target.value.substring(0, 64)));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
@ -86,6 +109,21 @@ export const RenameDialog = (props: {}) => {
|
||||||
}}
|
}}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
|
{allowFullWidth && (
|
||||||
|
<TextField
|
||||||
|
id="fullWidthTitle"
|
||||||
|
label={`Full-Width ${what} Name`}
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
className={classes.marginUpDown}
|
||||||
|
value={renameDialogFullWidthTitle}
|
||||||
|
onKeyDown={event => {
|
||||||
|
event.key === `Enter` && handleDoRename();
|
||||||
|
}}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={handleCancelRename}>Cancel</Button>
|
<Button onClick={handleCancelRename}>Cancel</Button>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { useShallowEqualSelector } from '../utils';
|
||||||
import Link from '@material-ui/core/Link';
|
import Link from '@material-ui/core/Link';
|
||||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||||
import ListItemText from '@material-ui/core/ListItemText';
|
import ListItemText from '@material-ui/core/ListItemText';
|
||||||
|
import Tooltip from '@material-ui/core/Tooltip';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||||
|
@ -33,14 +34,19 @@ const useStyles = makeStyles(theme => ({
|
||||||
listItemIcon: {
|
listItemIcon: {
|
||||||
minWidth: theme.spacing(5),
|
minWidth: theme.spacing(5),
|
||||||
},
|
},
|
||||||
|
toolTippedText: {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
textDecorationStyle: 'dotted',
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const TopMenu = function(props: { onClick?: () => void }) {
|
export const TopMenu = function (props: { onClick?: () => void }) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
let { mainView, darkMode, vintageMode } = useShallowEqualSelector(state => state.appState);
|
let { mainView, darkMode, vintageMode, fullWidthSupport } = useShallowEqualSelector(state => state.appState);
|
||||||
let discTitle = useShallowEqualSelector(state => state.main.disc?.title ?? ``);
|
let discTitle = useShallowEqualSelector(state => state.main.disc?.title ?? ``);
|
||||||
|
let fullWidthDiscTitle = useShallowEqualSelector(state => state.main.disc?.fullWidthTitle ?? ``);
|
||||||
|
|
||||||
const githubLinkRef = React.useRef<null | HTMLAnchorElement>(null);
|
const githubLinkRef = React.useRef<null | HTMLAnchorElement>(null);
|
||||||
const helpLinkRef = React.useRef<null | HTMLAnchorElement>(null);
|
const helpLinkRef = React.useRef<null | HTMLAnchorElement>(null);
|
||||||
|
@ -71,6 +77,10 @@ export const TopMenu = function(props: { onClick?: () => void }) {
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
}, [dispatch, handleMenuClose]);
|
}, [dispatch, handleMenuClose]);
|
||||||
|
|
||||||
|
const handleAllowFullWidth = useCallback(() => {
|
||||||
|
dispatch(appActions.setFullWidthSupport(!fullWidthSupport));
|
||||||
|
}, [dispatch, fullWidthSupport]);
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
dispatch(listContent());
|
dispatch(listContent());
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
|
@ -81,11 +91,12 @@ export const TopMenu = function(props: { onClick?: () => void }) {
|
||||||
batchActions([
|
batchActions([
|
||||||
renameDialogActions.setVisible(true),
|
renameDialogActions.setVisible(true),
|
||||||
renameDialogActions.setCurrentName(discTitle),
|
renameDialogActions.setCurrentName(discTitle),
|
||||||
|
renameDialogActions.setCurrentFullWidthName(fullWidthDiscTitle),
|
||||||
renameDialogActions.setIndex(-1),
|
renameDialogActions.setIndex(-1),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
}, [dispatch, handleMenuClose, discTitle]);
|
}, [dispatch, handleMenuClose, discTitle, fullWidthDiscTitle]);
|
||||||
|
|
||||||
const handleExit = useCallback(() => {
|
const handleExit = useCallback(() => {
|
||||||
dispatch(appActions.setMainView('WELCOME'));
|
dispatch(appActions.setMainView('WELCOME'));
|
||||||
|
@ -147,6 +158,15 @@ export const TopMenu = function(props: { onClick?: () => void }) {
|
||||||
<ListItemText>Wipe Disc</ListItemText>
|
<ListItemText>Wipe Disc</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
menuItems.push(
|
||||||
|
<MenuItem key="allowFullWidth" onClick={handleAllowFullWidth}>
|
||||||
|
<ListItemIcon className={classes.listItemIcon}>
|
||||||
|
{fullWidthSupport ? <ToggleOnIcon fontSize="small" /> : <ToggleOffIcon fontSize="small" />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText>
|
||||||
|
Allow <Tooltip title="Minidiscs have 2 slots for titles - the default half-width one used for standard alphabet and half-width katakana, and full-width for hiragana and kanji." arrow><span className={classes.toolTippedText}>Full-Width</span></Tooltip> Title Editing</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
menuItems.push(
|
menuItems.push(
|
||||||
<MenuItem key="exit" onClick={handleExit}>
|
<MenuItem key="exit" onClick={handleExit}>
|
||||||
<ListItemIcon className={classes.listItemIcon}>
|
<ListItemIcon className={classes.listItemIcon}>
|
||||||
|
|
|
@ -105,12 +105,12 @@ export function listContent() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renameTrack({ index, newName }: { index: number; newName: string }) {
|
export function renameTrack({ index, newName, newFullWidthName }: { index: number; newName: string, newFullWidthName?: string }) {
|
||||||
return async function(dispatch: AppDispatch) {
|
return async function(dispatch: AppDispatch) {
|
||||||
const { netmdService } = serviceRegistry;
|
const { netmdService } = serviceRegistry;
|
||||||
dispatch(renameDialogActions.setVisible(false));
|
dispatch(renameDialogActions.setVisible(false));
|
||||||
try {
|
try {
|
||||||
await netmdService!.renameTrack(index, newName);
|
await netmdService!.renameTrack(index, newName, newFullWidthName);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
dispatch(batchActions([errorDialogAction.setVisible(true), errorDialogAction.setErrorMessage(`Rename failed.`)]));
|
dispatch(batchActions([errorDialogAction.setVisible(true), errorDialogAction.setErrorMessage(`Rename failed.`)]));
|
||||||
|
@ -119,10 +119,13 @@ export function renameTrack({ index, newName }: { index: number; newName: string
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renameDisc({ newName }: { newName: string }) {
|
export function renameDisc({ newName, newFullWidthName }: { newName: string, newFullWidthName?: string }) {
|
||||||
return async function(dispatch: AppDispatch) {
|
return async function(dispatch: AppDispatch) {
|
||||||
const { netmdService } = serviceRegistry;
|
const { netmdService } = serviceRegistry;
|
||||||
await netmdService!.renameDisc(newName);
|
await netmdService!.renameDisc(
|
||||||
|
newName.replace(/\/\//g, ' /'), //Make sure the title doesn't interfere with the groups
|
||||||
|
newFullWidthName?.replace(////g, '/')
|
||||||
|
);
|
||||||
dispatch(renameDialogActions.setVisible(false));
|
dispatch(renameDialogActions.setVisible(false));
|
||||||
listContent()(dispatch);
|
listContent()(dispatch);
|
||||||
};
|
};
|
||||||
|
@ -387,8 +390,12 @@ export function convertAndUpload(files: File[], format: UploadFormat, titleForma
|
||||||
};
|
};
|
||||||
|
|
||||||
let disc = getState().main.disc;
|
let disc = getState().main.disc;
|
||||||
let maxTitleLength = disc ? getAvailableCharsForTrackTitle(getTracks(disc).map(track => track.title || ``)) : -1;
|
let useFullWidth = getState().appState.fullWidthSupport;
|
||||||
|
let tracks = disc && getTracks(disc);
|
||||||
|
let maxTitleLength = tracks ? getAvailableCharsForTrackTitle(tracks.map(track => track.title || ``)) : -1;
|
||||||
|
let maxFullWidthTitleLength = tracks ? getAvailableCharsForTrackTitle(tracks.map(track => track.fullWidthTitle || ``)) : -1;
|
||||||
maxTitleLength = Math.floor(maxTitleLength / files.length);
|
maxTitleLength = Math.floor(maxTitleLength / files.length);
|
||||||
|
maxFullWidthTitleLength = Math.floor(maxFullWidthTitleLength / files.length);
|
||||||
|
|
||||||
let error: any;
|
let error: any;
|
||||||
let errorMessage = ``;
|
let errorMessage = ``;
|
||||||
|
@ -408,7 +415,7 @@ export function convertAndUpload(files: File[], format: UploadFormat, titleForma
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxTitleLength > -1) {
|
if (maxTitleLength > -1) {
|
||||||
title = title.substring(0, maxTitleLength);
|
title = title.substring(0, useFullWidth ? Math.min(maxTitleLength, maxFullWidthTitleLength) : maxTitleLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackUpdate.current = i++;
|
trackUpdate.current = i++;
|
||||||
|
@ -416,7 +423,7 @@ export function convertAndUpload(files: File[], format: UploadFormat, titleForma
|
||||||
updateTrack();
|
updateTrack();
|
||||||
updateProgressCallback({ written: 0, encrypted: 0, total: 100 });
|
updateProgressCallback({ written: 0, encrypted: 0, total: 100 });
|
||||||
try {
|
try {
|
||||||
await netmdService?.upload(title, data, wireformat, updateProgressCallback);
|
await netmdService?.upload(title, data, wireformat, useFullWidth, updateProgressCallback);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error = err;
|
error = err;
|
||||||
errorMessage = `${file.name}: Error uploading to device. There might not be enough space left.`;
|
errorMessage = `${file.name}: Error uploading to device. There might not be enough space left.`;
|
||||||
|
|
|
@ -15,6 +15,7 @@ export interface AppState {
|
||||||
aboutDialogVisible: boolean;
|
aboutDialogVisible: boolean;
|
||||||
notifyWhenFinished: boolean;
|
notifyWhenFinished: boolean;
|
||||||
hasNotificationSupport: boolean;
|
hasNotificationSupport: boolean;
|
||||||
|
fullWidthSupport: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildInitialState = (): AppState => {
|
export const buildInitialState = (): AppState => {
|
||||||
|
@ -29,6 +30,7 @@ export const buildInitialState = (): AppState => {
|
||||||
aboutDialogVisible: false,
|
aboutDialogVisible: false,
|
||||||
notifyWhenFinished: loadPreference('notifyWhenFinished', false),
|
notifyWhenFinished: loadPreference('notifyWhenFinished', false),
|
||||||
hasNotificationSupport: true,
|
hasNotificationSupport: true,
|
||||||
|
fullWidthSupport: loadPreference('fullWidthSupport', false),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -72,6 +74,10 @@ export const slice = createSlice({
|
||||||
showAboutDialog: (state, action: PayloadAction<boolean>) => {
|
showAboutDialog: (state, action: PayloadAction<boolean>) => {
|
||||||
state.aboutDialogVisible = action.payload;
|
state.aboutDialogVisible = action.payload;
|
||||||
},
|
},
|
||||||
|
setFullWidthSupport: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.fullWidthSupport = action.payload;
|
||||||
|
savePreference('fullWidthSupport', state.fullWidthSupport);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,14 @@ import { enableBatching } from 'redux-batched-actions';
|
||||||
export interface RenameDialogState {
|
export interface RenameDialogState {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
|
fullWidthTitle: string;
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: RenameDialogState = {
|
const initialState: RenameDialogState = {
|
||||||
visible: false,
|
visible: false,
|
||||||
title: '',
|
title: '',
|
||||||
|
fullWidthTitle: '',
|
||||||
index: -1,
|
index: -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -23,6 +25,9 @@ export const slice = createSlice({
|
||||||
setCurrentName: (state: RenameDialogState, action: PayloadAction<string>) => {
|
setCurrentName: (state: RenameDialogState, action: PayloadAction<string>) => {
|
||||||
state.title = action.payload;
|
state.title = action.payload;
|
||||||
},
|
},
|
||||||
|
setCurrentFullWidthName: (state: RenameDialogState, action: PayloadAction<string>) => {
|
||||||
|
state.fullWidthTitle = action.payload;
|
||||||
|
},
|
||||||
setIndex: (state: RenameDialogState, action: PayloadAction<number>) => {
|
setIndex: (state: RenameDialogState, action: PayloadAction<number>) => {
|
||||||
state.index = action.payload;
|
state.index = action.payload;
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Track, Channels, Encoding, Wireformat, TrackFlag, DeviceStatus } from 'netmd-js';
|
import { Track, Channels, Encoding, Wireformat, TrackFlag, DeviceStatus } from 'netmd-js';
|
||||||
import { NetMDService } from './netmd';
|
import { NetMDService } from './netmd';
|
||||||
import { sleep, sanitizeHalfWidthTitle, asyncMutex } from '../utils';
|
import { sleep, sanitizeFullWidthTitle, sanitizeHalfWidthTitle, asyncMutex } from '../utils';
|
||||||
import { assert } from 'netmd-js/dist/utils';
|
import { assert } from 'netmd-js/dist/utils';
|
||||||
import { Mutex } from 'async-mutex';
|
import { Mutex } from 'async-mutex';
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ class NetMDMockService implements NetMDService {
|
||||||
public mutex = new Mutex();
|
public mutex = new Mutex();
|
||||||
public _tracksTitlesMaxLength = 1700;
|
public _tracksTitlesMaxLength = 1700;
|
||||||
public _discTitle: string = 'Mock Disc';
|
public _discTitle: string = 'Mock Disc';
|
||||||
|
public _fullWidthDiscTitle: string = '';
|
||||||
public _discCapacity: number = 80 * 60 * 512;
|
public _discCapacity: number = 80 * 60 * 512;
|
||||||
public _tracks: Track[] = [
|
public _tracks: Track[] = [
|
||||||
{
|
{
|
||||||
|
@ -18,6 +19,7 @@ class NetMDMockService implements NetMDService {
|
||||||
channel: Channels.stereo,
|
channel: Channels.stereo,
|
||||||
protected: TrackFlag.unprotected,
|
protected: TrackFlag.unprotected,
|
||||||
title: 'Long name for - Mock Track 1 - by some artist -12398729837198723',
|
title: 'Long name for - Mock Track 1 - by some artist -12398729837198723',
|
||||||
|
fullWidthTitle: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 5 * 60 * 512,
|
duration: 5 * 60 * 512,
|
||||||
|
@ -26,6 +28,7 @@ class NetMDMockService implements NetMDService {
|
||||||
channel: Channels.stereo,
|
channel: Channels.stereo,
|
||||||
protected: TrackFlag.unprotected,
|
protected: TrackFlag.unprotected,
|
||||||
title: 'Mock Track 2',
|
title: 'Mock Track 2',
|
||||||
|
fullWidthTitle: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 5 * 60 * 512,
|
duration: 5 * 60 * 512,
|
||||||
|
@ -34,6 +37,7 @@ class NetMDMockService implements NetMDService {
|
||||||
channel: Channels.stereo,
|
channel: Channels.stereo,
|
||||||
protected: TrackFlag.unprotected,
|
protected: TrackFlag.unprotected,
|
||||||
title: 'Mock Track 3',
|
title: 'Mock Track 3',
|
||||||
|
fullWidthTitle: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
duration: 5 * 60 * 512,
|
duration: 5 * 60 * 512,
|
||||||
|
@ -42,6 +46,7 @@ class NetMDMockService implements NetMDService {
|
||||||
channel: Channels.stereo,
|
channel: Channels.stereo,
|
||||||
protected: TrackFlag.unprotected,
|
protected: TrackFlag.unprotected,
|
||||||
title: 'Mock Track 4',
|
title: 'Mock Track 4',
|
||||||
|
fullWidthTitle: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
public _status: DeviceStatus = {
|
public _status: DeviceStatus = {
|
||||||
|
@ -108,18 +113,22 @@ class NetMDMockService implements NetMDService {
|
||||||
return `Generic MD Unit`;
|
return `Generic MD Unit`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async finalize() {}
|
async finalize() { }
|
||||||
|
|
||||||
async renameTrack(index: number, newTitle: string) {
|
async renameTrack(index: number, newTitle: string, fullWidthTitle?: string) {
|
||||||
newTitle = sanitizeHalfWidthTitle(newTitle);
|
newTitle = sanitizeHalfWidthTitle(newTitle);
|
||||||
if (this._getTracksTitlesLength() + newTitle.length > this._tracksTitlesMaxLength) {
|
if (this._getTracksTitlesLength() + newTitle.length > this._tracksTitlesMaxLength) {
|
||||||
throw new Error(`Track's title too long`);
|
throw new Error(`Track's title too long`);
|
||||||
}
|
}
|
||||||
|
if (fullWidthTitle !== undefined){
|
||||||
|
this._tracks[index].fullWidthTitle = sanitizeFullWidthTitle(fullWidthTitle);
|
||||||
|
}
|
||||||
this._tracks[index].title = newTitle;
|
this._tracks[index].title = newTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
async renameDisc(newName: string) {
|
async renameDisc(newName: string, fullWidthName?: string) {
|
||||||
this._discTitle = newName;
|
this._discTitle = sanitizeHalfWidthTitle(newName);
|
||||||
|
if (fullWidthName !== undefined) this._fullWidthDiscTitle = sanitizeFullWidthTitle(fullWidthName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteTrack(index: number) {
|
async deleteTrack(index: number) {
|
||||||
|
@ -142,11 +151,13 @@ class NetMDMockService implements NetMDService {
|
||||||
title: string,
|
title: string,
|
||||||
data: ArrayBuffer,
|
data: ArrayBuffer,
|
||||||
format: Wireformat,
|
format: Wireformat,
|
||||||
|
useFullWidth: boolean,
|
||||||
progressCallback: (progress: { written: number; encrypted: number; total: number }) => void
|
progressCallback: (progress: { written: number; encrypted: number; total: number }) => void
|
||||||
) {
|
) {
|
||||||
progressCallback({ written: 0, encrypted: 0, total: 100 });
|
progressCallback({ written: 0, encrypted: 0, total: 100 });
|
||||||
|
|
||||||
title = sanitizeHalfWidthTitle(title);
|
let halfWidthTitle = sanitizeHalfWidthTitle(title);
|
||||||
|
let fullWidthTitle = sanitizeFullWidthTitle(title);
|
||||||
|
|
||||||
if (this._getTracksTitlesLength() + title.length > this._tracksTitlesMaxLength) {
|
if (this._getTracksTitlesLength() + title.length > this._tracksTitlesMaxLength) {
|
||||||
throw new Error(`Track's title too long`); // Simulates reject from device
|
throw new Error(`Track's title too long`); // Simulates reject from device
|
||||||
|
@ -160,12 +171,13 @@ class NetMDMockService implements NetMDService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this._tracks.push({
|
this._tracks.push({
|
||||||
title,
|
title: halfWidthTitle,
|
||||||
duration: 5 * 60 * 512,
|
duration: 5 * 60 * 512,
|
||||||
encoding: Encoding.sp,
|
encoding: Encoding.sp,
|
||||||
index: this._tracks.length,
|
index: this._tracks.length,
|
||||||
protected: TrackFlag.unprotected,
|
protected: TrackFlag.unprotected,
|
||||||
channel: 0,
|
channel: 0,
|
||||||
|
fullWidthTitle: useFullWidth ? fullWidthTitle : '',
|
||||||
});
|
});
|
||||||
|
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {
|
||||||
} from 'netmd-js';
|
} from 'netmd-js';
|
||||||
import { makeGetAsyncPacketIteratorOnWorkerThread } from 'netmd-js/dist/web-encrypt-worker';
|
import { makeGetAsyncPacketIteratorOnWorkerThread } from 'netmd-js/dist/web-encrypt-worker';
|
||||||
import { Logger } from 'netmd-js/dist/logger';
|
import { Logger } from 'netmd-js/dist/logger';
|
||||||
import { asyncMutex, sanitizeHalfWidthTitle, sleep } from '../utils';
|
import { asyncMutex, sanitizeHalfWidthTitle, sanitizeFullWidthTitle, sleep } from '../utils';
|
||||||
import { Mutex } from 'async-mutex';
|
import { Mutex } from 'async-mutex';
|
||||||
|
|
||||||
const Worker = require('worker-loader!netmd-js/dist/web-encrypt-worker.js'); // eslint-disable-line import/no-webpack-loader-syntax
|
const Worker = require('worker-loader!netmd-js/dist/web-encrypt-worker.js'); // eslint-disable-line import/no-webpack-loader-syntax
|
||||||
|
@ -25,8 +25,8 @@ export interface NetMDService {
|
||||||
listContent(): Promise<Disc>;
|
listContent(): Promise<Disc>;
|
||||||
getDeviceName(): Promise<string>;
|
getDeviceName(): Promise<string>;
|
||||||
finalize(): Promise<void>;
|
finalize(): Promise<void>;
|
||||||
renameTrack(index: number, newTitle: string): Promise<void>;
|
renameTrack(index: number, newTitle: string, newFullWidthTitle?: string): Promise<void>;
|
||||||
renameDisc(newName: string): Promise<void>;
|
renameDisc(newName: string, newFullWidthName?: string): Promise<void>;
|
||||||
deleteTrack(index: number): Promise<void>;
|
deleteTrack(index: number): Promise<void>;
|
||||||
moveTrack(src: number, dst: number): Promise<void>;
|
moveTrack(src: number, dst: number): Promise<void>;
|
||||||
wipeDisc(): Promise<void>;
|
wipeDisc(): Promise<void>;
|
||||||
|
@ -34,6 +34,7 @@ export interface NetMDService {
|
||||||
title: string,
|
title: string,
|
||||||
data: ArrayBuffer,
|
data: ArrayBuffer,
|
||||||
format: Wireformat,
|
format: Wireformat,
|
||||||
|
useFullWidth: boolean,
|
||||||
progressCallback: (progress: { written: number; encrypted: number; total: number }) => void
|
progressCallback: (progress: { written: number; encrypted: number; total: number }) => void
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
|
@ -109,22 +110,46 @@ export class NetMDUSBService implements NetMDService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@asyncMutex
|
@asyncMutex
|
||||||
async renameTrack(index: number, title: string) {
|
async renameTrack(index: number, title: string, fullWidthTitle?: string) {
|
||||||
title = sanitizeHalfWidthTitle(title);
|
title = sanitizeHalfWidthTitle(title);
|
||||||
await this.netmdInterface!.cacheTOC();
|
await this.netmdInterface!.cacheTOC();
|
||||||
await this.netmdInterface!.setTrackTitle(index, title);
|
await this.netmdInterface!.setTrackTitle(index, title);
|
||||||
|
if (fullWidthTitle !== undefined) {
|
||||||
|
await this.netmdInterface!.setTrackTitle(index, sanitizeFullWidthTitle(fullWidthTitle), true);
|
||||||
|
}
|
||||||
await this.netmdInterface!.syncTOC();
|
await this.netmdInterface!.syncTOC();
|
||||||
}
|
}
|
||||||
|
|
||||||
@asyncMutex
|
@asyncMutex
|
||||||
async renameDisc(newName: string) {
|
async renameDisc(newName: string, newFullWidthName?: string) {
|
||||||
// TODO: This whole function should be moved in netmd-js
|
// TODO: This whole function should be moved in netmd-js
|
||||||
const oldName = await this.netmdInterface!.getDiscTitle();
|
const oldName = await this.netmdInterface!.getDiscTitle();
|
||||||
|
const oldFullWidthName = await this.netmdInterface!.getDiscTitle(true);
|
||||||
const oldRawName = await this.netmdInterface!._getDiscTitle();
|
const oldRawName = await this.netmdInterface!._getDiscTitle();
|
||||||
|
const oldRawFullWidthName = await this.netmdInterface!._getDiscTitle(true);
|
||||||
const hasGroups = oldRawName.indexOf('//') >= 0;
|
const hasGroups = oldRawName.indexOf('//') >= 0;
|
||||||
|
const hasFullWidthGroups = oldRawName.indexOf('//') >= 0;
|
||||||
const hasGroupsAndTitle = oldRawName.startsWith('0;');
|
const hasGroupsAndTitle = oldRawName.startsWith('0;');
|
||||||
|
const hasFullWidthGroupsAndTitle = oldRawName.startsWith('0;');
|
||||||
|
|
||||||
newName = sanitizeHalfWidthTitle(newName);
|
newName = sanitizeHalfWidthTitle(newName);
|
||||||
|
newFullWidthName = newFullWidthName && sanitizeFullWidthTitle(newFullWidthName);
|
||||||
|
|
||||||
|
if(newFullWidthName !== oldFullWidthName && newFullWidthName !== undefined){
|
||||||
|
let newFullWidthNameWithGroups;
|
||||||
|
if (hasFullWidthGroups) {
|
||||||
|
if (hasFullWidthGroupsAndTitle) {
|
||||||
|
newFullWidthNameWithGroups = oldRawFullWidthName.replace(/^0;.*?///, newFullWidthName !== '' ? `0;${newFullWidthName}//` : ``);
|
||||||
|
} else {
|
||||||
|
newFullWidthNameWithGroups = `0;${newFullWidthName}//${oldRawFullWidthName}`; // Add the new title
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newFullWidthNameWithGroups = newFullWidthName;
|
||||||
|
}
|
||||||
|
await this.netmdInterface!.cacheTOC();
|
||||||
|
await this.netmdInterface!.setDiscTitle(newFullWidthNameWithGroups, true);
|
||||||
|
await this.netmdInterface!.syncTOC();
|
||||||
|
}
|
||||||
|
|
||||||
if (newName === oldName) {
|
if (newName === oldName) {
|
||||||
return;
|
return;
|
||||||
|
@ -167,6 +192,7 @@ export class NetMDUSBService implements NetMDService {
|
||||||
title: string,
|
title: string,
|
||||||
data: ArrayBuffer,
|
data: ArrayBuffer,
|
||||||
format: Wireformat,
|
format: Wireformat,
|
||||||
|
useFullWidth: boolean,
|
||||||
progressCallback: (progress: { written: number; encrypted: number; total: number }) => void
|
progressCallback: (progress: { written: number; encrypted: number; total: number }) => void
|
||||||
) {
|
) {
|
||||||
let total = data.byteLength;
|
let total = data.byteLength;
|
||||||
|
@ -183,8 +209,9 @@ export class NetMDUSBService implements NetMDService {
|
||||||
updateProgress();
|
updateProgress();
|
||||||
});
|
});
|
||||||
|
|
||||||
title = sanitizeHalfWidthTitle(title);
|
let halfWidthTitle = sanitizeHalfWidthTitle(title);
|
||||||
let mdTrack = new MDTrack(title, format, data, 0x80000, webWorkerAsyncPacketIterator);
|
let fullWidthTitle = sanitizeFullWidthTitle(title);
|
||||||
|
let mdTrack = new MDTrack(halfWidthTitle, format, data, 0x80000, useFullWidth ? fullWidthTitle : '', webWorkerAsyncPacketIterator);
|
||||||
|
|
||||||
await download(this.netmdInterface!, mdTrack, ({ writtenBytes }) => {
|
await download(this.netmdInterface!, mdTrack, ({ writtenBytes }) => {
|
||||||
written = writtenBytes;
|
written = writtenBytes;
|
||||||
|
|
30
src/utils.ts
30
src/utils.ts
|
@ -66,20 +66,37 @@ export function sanitizeTitle(title: string) {
|
||||||
return title.normalize('NFD').replace(/[^\x00-\x7F]/g, '');
|
return title.normalize('NFD').replace(/[^\x00-\x7F]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeHalfWidthTitle(title: string){
|
export function sanitizeHalfWidthTitle(title: string) {
|
||||||
const mappings: {[key: string]: string} = {'ァ': 'ァ', 'ア': 'ア', 'ィ': 'ィ', 'イ': 'イ', 'ゥ': 'ゥ', 'ウ': 'ウ', 'ェ': 'ェ', 'エ': 'エ', 'ォ': 'ォ', 'オ': 'オ', 'カ': 'カ', 'ガ': 'ガ', 'キ': 'キ', 'ギ': 'ギ', 'ク': 'ク', 'グ': 'グ', 'ケ': 'ケ', 'ゲ': 'ゲ', 'コ': 'コ', 'ゴ': 'ゴ', 'サ': 'サ', 'ザ': 'ザ', 'シ': 'シ', 'ジ': 'ジ', 'ス': 'ス', 'ズ': 'ズ', 'セ': 'セ', 'ゼ': 'ゼ', 'ソ': 'ソ', 'ゾ': 'ゾ', 'タ': 'タ', 'ダ': 'ダ', 'チ': 'チ', 'ヂ': 'ヂ', 'ッ': 'ッ', 'ツ': 'ツ', 'ヅ': 'ヅ', 'テ': 'テ', 'デ': 'デ', 'ト': 'ト', 'ド': 'ド', 'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', 'ノ': 'ノ', 'ハ': 'ハ', 'バ': 'バ', 'パ': 'パ', 'ヒ': 'ヒ', 'ビ': 'ビ', 'ピ': 'ピ', 'フ': 'フ', 'ブ': 'ブ', 'プ': 'プ', 'ヘ': 'ヘ', 'ベ': 'ベ', 'ペ': 'ペ', 'ホ': 'ホ', 'ボ': 'ボ', 'ポ': 'ポ', 'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ', 'ャ': 'ャ', 'ヤ': 'ヤ', 'ュ': 'ュ', 'ユ': 'ユ', 'ョ': 'ョ', 'ヨ': 'ヨ', 'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ', 'ワ': 'ワ', 'ヲ': 'ヲ', 'ン': 'ン', 'ー': 'ー', 'ヮ': 'ヮ', 'ヰ': 'ヰ', 'ヱ': 'ヱ', 'ヵ': 'ヵ', 'ヶ': 'ヶ', 'ヴ': 'ヴ', 'ヽ': 'ヽ', 'ヾ': 'ヾ', '・': '・', '「': '「', '」': '」', '。': '。', '、': '、', '!': '!', '"': '"', '#': '#', '$': '$', '%': '%', '&': '&', ''': "'", '(': '(', ')': ')', '*': '*', '+': '+', ',': ',', '-': '-', '.': '.', '/': '/', ':': ':', ';': ';', '<': '<', '=': '=', '>': '>', '?': '?', '@': '@', 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z', '[': '[', '\': '\\', ']': ']', '^': '^', '_': '_', '`': '`', 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', '{': '{', '|': '|', '}': '}', '~': '~', '\u3000': ' ', '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9'};
|
if (title.length > 120) title = title.substring(0, 120);
|
||||||
let multiByteChars: {[key: string]: number} = {'ガ': 1, 'ギ': 1, 'グ': 1, 'ゲ': 1, 'ゴ': 1, 'ザ': 1, 'ジ': 1, 'ズ': 1, 'ゼ': 1, 'ゾ': 1, 'ダ': 1, 'ヂ': 1, 'ヅ': 1, 'デ': 1, 'ド': 1, 'バ': 1, 'パ': 1, 'ビ': 1, 'ピ': 1, 'ブ': 1, 'プ': 1, 'ベ': 1, 'ペ': 1, 'ボ': 1, 'ポ': 1, 'ヮ': 1, 'ヰ': 1, 'ヱ': 1, 'ヵ': 1, 'ヶ': 1, 'ヴ': 1, 'ヽ': 1, 'ヾ': 1}; //Dakuten kana are encoded as 2 bytes
|
|
||||||
|
const mappings: { [key: string]: string } = { 'ァ': 'ァ', 'ア': 'ア', 'ィ': 'ィ', 'イ': 'イ', 'ゥ': 'ゥ', 'ウ': 'ウ', 'ェ': 'ェ', 'エ': 'エ', 'ォ': 'ォ', 'オ': 'オ', 'カ': 'カ', 'ガ': 'ガ', 'キ': 'キ', 'ギ': 'ギ', 'ク': 'ク', 'グ': 'グ', 'ケ': 'ケ', 'ゲ': 'ゲ', 'コ': 'コ', 'ゴ': 'ゴ', 'サ': 'サ', 'ザ': 'ザ', 'シ': 'シ', 'ジ': 'ジ', 'ス': 'ス', 'ズ': 'ズ', 'セ': 'セ', 'ゼ': 'ゼ', 'ソ': 'ソ', 'ゾ': 'ゾ', 'タ': 'タ', 'ダ': 'ダ', 'チ': 'チ', 'ヂ': 'ヂ', 'ッ': 'ッ', 'ツ': 'ツ', 'ヅ': 'ヅ', 'テ': 'テ', 'デ': 'デ', 'ト': 'ト', 'ド': 'ド', 'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', 'ノ': 'ノ', 'ハ': 'ハ', 'バ': 'バ', 'パ': 'パ', 'ヒ': 'ヒ', 'ビ': 'ビ', 'ピ': 'ピ', 'フ': 'フ', 'ブ': 'ブ', 'プ': 'プ', 'ヘ': 'ヘ', 'ベ': 'ベ', 'ペ': 'ペ', 'ホ': 'ホ', 'ボ': 'ボ', 'ポ': 'ポ', 'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ', 'ャ': 'ャ', 'ヤ': 'ヤ', 'ュ': 'ュ', 'ユ': 'ユ', 'ョ': 'ョ', 'ヨ': 'ヨ', 'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ', 'ワ': 'ワ', 'ヲ': 'ヲ', 'ン': 'ン', 'ー': 'ー', 'ヮ': 'ヮ', 'ヰ': 'ヰ', 'ヱ': 'ヱ', 'ヵ': 'ヵ', 'ヶ': 'ヶ', 'ヴ': 'ヴ', 'ヽ': 'ヽ', 'ヾ': 'ヾ', '・': '・', '「': '「', '」': '」', '。': '。', '、': '、', '!': '!', '"': '"', '#': '#', '$': '$', '%': '%', '&': '&', ''': "'", '(': '(', ')': ')', '*': '*', '+': '+', ',': ',', '-': '-', '.': '.', '/': '/', ':': ':', ';': ';', '<': '<', '=': '=', '>': '>', '?': '?', '@': '@', 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z', '[': '[', '\': '\\', ']': ']', '^': '^', '_': '_', '`': '`', 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', '{': '{', '|': '|', '}': '}', '~': '~', '\u3000': ' ', '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', 'ぁ': 'ァ', 'あ': 'ア', 'ぃ': 'ィ', 'い': 'イ', 'ぅ': 'ゥ', 'う': 'ウ', 'ぇ': 'ェ', 'え': 'エ', 'ぉ': 'ォ', 'お': 'オ', 'か': 'カ', 'が': 'ガ', 'き': 'キ', 'ぎ': 'ギ', 'く': 'ク', 'ぐ': 'グ', 'け': 'ケ', 'げ': 'ゲ', 'こ': 'コ', 'ご': 'ゴ', 'さ': 'サ', 'ざ': 'ザ', 'し': 'シ', 'じ': 'ジ', 'す': 'ス', 'ず': 'ズ', 'せ': 'セ', 'ぜ': 'ゼ', 'そ': 'ソ', 'ぞ': 'ゾ', 'た': 'タ', 'だ': 'ダ', 'ち': 'チ', 'ぢ': 'ヂ', 'っ': 'ッ', 'つ': 'ツ', 'づ': 'ヅ', 'て': 'テ', 'で': 'デ', 'と': 'ト', 'ど': 'ド', 'な': 'ナ', 'に': 'ニ', 'ぬ': 'ヌ', 'ね': 'ネ', 'の': 'ノ', 'は': 'ハ', 'ば': 'バ', 'ぱ': 'パ', 'ひ': 'ヒ', 'び': 'ビ', 'ぴ': 'ピ', 'ふ': 'フ', 'ぶ': 'ブ', 'ぷ': 'プ', 'へ': 'ヘ', 'べ': 'ベ', 'ぺ': 'ペ', 'ほ': 'ホ', 'ぼ': 'ボ', 'ぽ': 'ポ', 'ま': 'マ', 'み': 'ミ', 'む': 'ム', 'め': 'メ', 'も': 'モ', 'ゃ': 'ャ', 'や': 'ヤ', 'ゅ': 'ュ', 'ゆ': 'ユ', 'ょ': 'ョ', 'よ': 'ヨ', 'ら': 'ラ', 'り': 'リ', 'る': 'ル', 'れ': 'レ', 'ろ': 'ロ', 'わ': 'ワ', 'を': 'ヲ', 'ん': 'ン', 'ゎ': 'ヮ', 'ゐ': 'ヰ', 'ゑ': 'ヱ', 'ゕ': 'ヵ', 'ゖ': 'ヶ', 'ゔ': 'ヴ', 'ゝ': 'ヽ', 'ゞ': 'ヾ' };
|
||||||
|
const multiByteChars: { [key: string]: number } = { "ガ": 1, "ギ": 1, "グ": 1, "ゲ": 1, "ゴ": 1, "ザ": 1, "ジ": 1, "ズ": 1, "ゼ": 1, "ゾ": 1, "ダ": 1, "ヂ": 1, "ヅ": 1, "デ": 1, "ド": 1, "バ": 1, "パ": 1, "ビ": 1, "ピ": 1, "ブ": 1, "プ": 1, "ベ": 1, "ペ": 1, "ボ": 1, "ポ": 1, "ヮ": 1, "ヰ": 1, "ヱ": 1, "ヵ": 1, "ヶ": 1, "ヴ": 1, "ヽ": 1, "ヾ": 1, "が": 1, "ぎ": 1, "ぐ": 1, "げ": 1, "ご": 1, "ざ": 1, "じ": 1, "ず": 1, "ぜ": 1, "ぞ": 1, "だ": 1, "ぢ": 1, "づ": 1, "で": 1, "ど": 1, "ば": 1, "ぱ": 1, "び": 1, "ぴ": 1, "ぶ": 1, "ぷ": 1, "べ": 1, "ぺ": 1, "ぼ": 1, "ぽ": 1, "ゎ": 1, "ゐ": 1, "ゑ": 1, "ゕ": 1, "ゖ": 1, "ゔ": 1, "ゝ": 1, "ゞ": 1 }; //Dakuten kana are encoded as 2 bytes
|
||||||
|
const allowedHalfWidthKana: string[] = Object.values(mappings);
|
||||||
|
|
||||||
let allowedAdditionalChars = 0;
|
let allowedAdditionalChars = 0;
|
||||||
const newTitle = title.split('').map(n => {
|
const newTitle = title.split('').map(n => {
|
||||||
allowedAdditionalChars += (multiByteChars[n] ?? 0);
|
allowedAdditionalChars += (multiByteChars[n] ?? 0);
|
||||||
return mappings[n] ?? n;
|
if (mappings[n]) return mappings[n];
|
||||||
|
if (n.charCodeAt(0) < 0x7f || allowedHalfWidthKana.includes(n)) return n;
|
||||||
|
return ' ';
|
||||||
}).join('');
|
}).join('');
|
||||||
//Check if the amount of characters is the same as the amount of encoded bytes (when accounting for dakuten). Otherwise the disc might end up corrupted
|
//Check if the amount of characters is the same as the amount of encoded bytes (when accounting for dakuten). Otherwise the disc might end up corrupted
|
||||||
const sjisEncoded = jconv.encode(newTitle, 'SJIS');
|
const sjisEncoded = jconv.encode(newTitle, 'SJIS');
|
||||||
if(sjisEncoded.length - allowedAdditionalChars !== title.length) return sanitizeTitle(title); //Fallback
|
if (sjisEncoded.length - allowedAdditionalChars !== title.length) return sanitizeTitle(title); //Fallback
|
||||||
return newTitle;
|
return newTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sanitizeFullWidthTitle(title: string) {
|
||||||
|
if (title.length > 100) title = title.substring(0, 100);
|
||||||
|
const mappings: { [key: string]: string} = {'!': '!', '"': '"', '#': '#', '$': '$', '%': '%', '&': '&', "'": ''', '(': '(', ')': ')', '*': '*', '+': '+', ',': ',', '-': '-', '.': '.', '/': '/', ':': ':', ';': ';', '<': '<', '=': '=', '>': '>', '?': '?', '@': '@', 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z', '[': '[', '\\': '\', ']': ']', '^': '^', '_': '_', '`': '`', 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', '{': '{', '|': '|', '}': '}', '~': '~', ' ': '\u3000', '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', 'ァ': 'ァ', 'ア': 'ア', 'ィ': 'ィ', 'イ': 'イ', 'ゥ': 'ゥ', 'ウ': 'ウ', 'ェ': 'ェ', 'エ': 'エ', 'ォ': 'ォ', 'オ': 'オ', 'カ': 'カ', 'ガ': 'ガ', 'キ': 'キ', 'ギ': 'ギ', 'ク': 'ク', 'グ': 'グ', 'ケ': 'ケ', 'ゲ': 'ゲ', 'コ': 'コ', 'ゴ': 'ゴ', 'サ': 'サ', 'ザ': 'ザ', 'シ': 'シ', 'ジ': 'ジ', 'ス': 'ス', 'ズ': 'ズ', 'セ': 'セ', 'ゼ': 'ゼ', 'ソ': 'ソ', 'ゾ': 'ゾ', 'タ': 'タ', 'ダ': 'ダ', 'チ': 'チ', 'ヂ': 'ヂ', 'ッ': 'ッ', 'ツ': 'ツ', 'ヅ': 'ヅ', 'テ': 'テ', 'デ': 'デ', 'ト': 'ト', 'ド': 'ド', 'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', 'ノ': 'ノ', 'ハ': 'ハ', 'バ': 'バ', 'パ': 'パ', 'ヒ': 'ヒ', 'ビ': 'ビ', 'ピ': 'ピ', 'フ': 'フ', 'ブ': 'ブ', 'プ': 'プ', 'ヘ': 'ヘ', 'ベ': 'ベ', 'ペ': 'ペ', 'ホ': 'ホ', 'ボ': 'ボ', 'ポ': 'ポ', 'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ', 'ャ': 'ャ', 'ヤ': 'ヤ', 'ュ': 'ュ', 'ユ': 'ユ', 'ョ': 'ョ', 'ヨ': 'ヨ', 'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ', 'ワ': 'ワ', 'ヲ': 'ヲ', 'ン': 'ン', 'ー': 'ー', 'ヮ': 'ヮ', 'ヰ': 'ヰ', 'ヱ': 'ヱ', 'ヵ': 'ヵ', 'ヶ': 'ヶ', 'ヴ': 'ヴ', 'ヽ': 'ヽ', 'ヾ': 'ヾ', '・': '・', '「': '「', '」': '」', '。': '。', '、': '、'};
|
||||||
|
|
||||||
|
const newTitle = title.split('').map(n => mappings[n] ?? n).join('');
|
||||||
|
|
||||||
|
const sjisEncoded = jconv.encode(newTitle, 'SJIS');
|
||||||
|
if(jconv.decode(sjisEncoded, 'SJIS') !== newTitle) return sanitizeTitle(title); //Fallback
|
||||||
|
return newTitle;
|
||||||
|
}
|
||||||
|
|
||||||
const EncodingName: { [k: number]: string } = {
|
const EncodingName: { [k: number]: string } = {
|
||||||
[Encoding.sp]: 'SP',
|
[Encoding.sp]: 'SP',
|
||||||
[Encoding.lp2]: 'LP2',
|
[Encoding.lp2]: 'LP2',
|
||||||
|
@ -87,13 +104,14 @@ const EncodingName: { [k: number]: string } = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getSortedTracks(disc: Disc | null) {
|
export function getSortedTracks(disc: Disc | null) {
|
||||||
let tracks: { index: number; title: string; group: string; duration: string; encoding: string }[] = [];
|
let tracks: { index: number; title: string; fullWidthTitle: string; group: string; duration: string; encoding: string }[] = [];
|
||||||
if (disc !== null) {
|
if (disc !== null) {
|
||||||
for (let group of disc.groups) {
|
for (let group of disc.groups) {
|
||||||
for (let track of group.tracks) {
|
for (let track of group.tracks) {
|
||||||
tracks.push({
|
tracks.push({
|
||||||
index: track.index,
|
index: track.index,
|
||||||
title: track.title ?? `Unknown Title`,
|
title: track.title ?? `Unknown Title`,
|
||||||
|
fullWidthTitle: track.fullWidthTitle ?? ``,
|
||||||
group: group.title ?? ``,
|
group: group.title ?? ``,
|
||||||
encoding: EncodingName[track.encoding],
|
encoding: EncodingName[track.encoding],
|
||||||
duration: formatTimeFromFrames(track.duration, false),
|
duration: formatTimeFromFrames(track.duration, false),
|
||||||
|
|
Loading…
Reference in New Issue