Add react95 theme
|
@ -1082,6 +1082,29 @@
|
|||
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
|
||||
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
|
||||
},
|
||||
"@emotion/is-prop-valid": {
|
||||
"version": "0.8.8",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
|
||||
"integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==",
|
||||
"requires": {
|
||||
"@emotion/memoize": "0.7.4"
|
||||
}
|
||||
},
|
||||
"@emotion/memoize": {
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz",
|
||||
"integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="
|
||||
},
|
||||
"@emotion/stylis": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz",
|
||||
"integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ=="
|
||||
},
|
||||
"@emotion/unitless": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
|
||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
|
||||
},
|
||||
"@ffmpeg/core": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffmpeg/core/-/core-0.6.0.tgz",
|
||||
|
@ -1890,6 +1913,23 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
||||
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw=="
|
||||
},
|
||||
"@types/styled-components": {
|
||||
"version": "5.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.7.tgz",
|
||||
"integrity": "sha512-BJzPhFygYspyefAGFZTZ/8lCEY4Tk+Iqktvnko3xmJf9LrLqs3+grxPeU3O0zLl6yjbYBopD0/VikbHgXDbJtA==",
|
||||
"requires": {
|
||||
"@types/hoist-non-react-statics": "*",
|
||||
"@types/react": "*",
|
||||
"csstype": "^3.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"csstype": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz",
|
||||
"integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/testing-library__dom": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-6.14.0.tgz",
|
||||
|
@ -2835,6 +2875,22 @@
|
|||
"resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.6.tgz",
|
||||
"integrity": "sha512-1aGDUfL1qOOIoqk9QKGIo2lANk+C7ko/fqH0uIyC71x3PEGz0uVP8ISgfEsFuG+FKmjHTvFK/nNM8dowpmUxLA=="
|
||||
},
|
||||
"babel-plugin-styled-components": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.12.0.tgz",
|
||||
"integrity": "sha512-FEiD7l5ZABdJPpLssKXjBUJMYqzbcNzBowfXDCdJhOpbhWiewapUaY+LZGT8R4Jg2TwOjGjG4RKeyrO5p9sBkA==",
|
||||
"requires": {
|
||||
"@babel/helper-annotate-as-pure": "^7.0.0",
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"babel-plugin-syntax-jsx": "^6.18.0",
|
||||
"lodash": "^4.17.11"
|
||||
}
|
||||
},
|
||||
"babel-plugin-syntax-jsx": {
|
||||
"version": "6.18.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
|
||||
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
|
||||
},
|
||||
"babel-plugin-syntax-object-rest-spread": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz",
|
||||
|
@ -3370,6 +3426,11 @@
|
|||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
|
||||
},
|
||||
"camelize": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
|
||||
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
|
||||
},
|
||||
"caniuse-api": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
|
||||
|
@ -4060,6 +4121,11 @@
|
|||
"postcss": "^7.0.5"
|
||||
}
|
||||
},
|
||||
"css-color-keywords": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||
"integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
|
||||
},
|
||||
"css-color-names": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
|
||||
|
@ -4143,6 +4209,16 @@
|
|||
"resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz",
|
||||
"integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w=="
|
||||
},
|
||||
"css-to-react-native": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz",
|
||||
"integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==",
|
||||
"requires": {
|
||||
"camelize": "^1.0.0",
|
||||
"css-color-keywords": "^1.0.0",
|
||||
"postcss-value-parser": "^4.0.2"
|
||||
}
|
||||
},
|
||||
"css-tree": {
|
||||
"version": "1.0.0-alpha.37",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz",
|
||||
|
@ -12307,6 +12383,11 @@
|
|||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"react95": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react95/-/react95-3.5.0.tgz",
|
||||
"integrity": "sha512-R287skjv9nGgy6a2WhTiKtzNAbKxx2gOyCNzA3JQ07oAfOdQE+oCiarMEntrrH1eun2aoAJgRzmMG2P9YictrQ=="
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
|
||||
|
@ -13146,6 +13227,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
|
@ -13831,6 +13917,33 @@
|
|||
"schema-utils": "^2.6.4"
|
||||
}
|
||||
},
|
||||
"styled-components": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.2.1.tgz",
|
||||
"integrity": "sha512-sBdgLWrCFTKtmZm/9x7jkIabjFNVzCUeKfoQsM6R3saImkUnjx0QYdLwJHBjY9ifEcmjDamJDVfknWm1yxZPxQ==",
|
||||
"requires": {
|
||||
"@babel/helper-module-imports": "^7.0.0",
|
||||
"@babel/traverse": "^7.4.5",
|
||||
"@emotion/is-prop-valid": "^0.8.8",
|
||||
"@emotion/stylis": "^0.8.4",
|
||||
"@emotion/unitless": "^0.7.4",
|
||||
"babel-plugin-styled-components": ">= 1",
|
||||
"css-to-react-native": "^3.0.0",
|
||||
"hoist-non-react-statics": "^3.0.0",
|
||||
"shallowequal": "^1.1.0",
|
||||
"supports-color": "^5.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"stylehacks": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz",
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
"@types/react": "^16.14.2",
|
||||
"@types/react-dom": "^16.9.10",
|
||||
"@types/react-redux": "^7.1.15",
|
||||
"@types/styled-components": "^5.1.7",
|
||||
"@types/w3c-web-usb": "^1.0.4",
|
||||
"clsx": "^1.1.1",
|
||||
"husky": "^4.3.8",
|
||||
|
@ -29,8 +30,10 @@
|
|||
"react-dropzone": "^10.2.2",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-scripts": "3.3.1",
|
||||
"react95": "^3.5.0",
|
||||
"recorderjs": "^1.0.1",
|
||||
"redux-batched-actions": "^0.4.1",
|
||||
"styled-components": "^5.2.1",
|
||||
"typescript": "^4.1.3",
|
||||
"worker-loader": "^2.0.0"
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@ import Slide from '@material-ui/core/Slide';
|
|||
import Button from '@material-ui/core/Button';
|
||||
import Link from '@material-ui/core/Link';
|
||||
import { TransitionProps } from '@material-ui/core/transitions';
|
||||
import { W95AboutDialog } from './win95/about-dialog';
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement<any, any> },
|
||||
|
@ -25,11 +26,20 @@ export const AboutDialog = (props: {}) => {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
let visible = useShallowEqualSelector(state => state.appState.aboutDialogVisible);
|
||||
const vintageMode = useShallowEqualSelector(state => state.appState.vintageMode);
|
||||
|
||||
const handleClose = () => {
|
||||
dispatch(appActions.showAboutDialog(false));
|
||||
};
|
||||
|
||||
if (vintageMode) {
|
||||
const p = {
|
||||
visible,
|
||||
handleClose,
|
||||
};
|
||||
return <W95AboutDialog {...p} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={visible}
|
||||
|
|
|
@ -13,7 +13,7 @@ import Paper from '@material-ui/core/Paper';
|
|||
import Typography from '@material-ui/core/Typography';
|
||||
import Link from '@material-ui/core/Link';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { W95App } from './win95/app';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
layout: {
|
||||
|
@ -94,9 +94,11 @@ const lightTheme = createMuiTheme({
|
|||
|
||||
const App = () => {
|
||||
const classes = useStyles();
|
||||
const { mainView, loading, darkMode, vintageMode } = useShallowEqualSelector(state => state.appState);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
let { mainView, loading, darkMode } = useShallowEqualSelector(state => state.appState);
|
||||
if (vintageMode) {
|
||||
return <W95App></W95App>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
|
|
@ -17,6 +17,7 @@ import { ReactComponent as MDIcon0 } from '../images/md0.svg';
|
|||
import { ReactComponent as MDIcon1 } from '../images/md1.svg';
|
||||
import { ReactComponent as MDIcon2 } from '../images/md2.svg';
|
||||
import { ReactComponent as MDIcon3 } from '../images/md3.svg';
|
||||
import { W95Controls } from './win95/controls';
|
||||
|
||||
const frames = [MDIcon0, MDIcon1, MDIcon2, MDIcon3];
|
||||
|
||||
|
@ -179,6 +180,26 @@ export const Controls = () => {
|
|||
}, [deviceState, lcdIconFrame]);
|
||||
|
||||
const DiscFrame = frames[lcdIconFrame];
|
||||
|
||||
const vintageMode = useShallowEqualSelector(state => state.appState.vintageMode);
|
||||
if (vintageMode) {
|
||||
const p = {
|
||||
handlePrev,
|
||||
handlePlay,
|
||||
handleStop,
|
||||
handleNext,
|
||||
|
||||
message,
|
||||
discPresent,
|
||||
lcdScroll,
|
||||
lcdRef,
|
||||
lcdScrollDuration,
|
||||
|
||||
classes,
|
||||
};
|
||||
return <W95Controls {...p} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={classes.container}>
|
||||
<IconButton aria-label="prev" onClick={handlePrev} className={classes.button}>
|
||||
|
|
|
@ -36,6 +36,7 @@ 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';
|
||||
import { W95ConvertDialog } from './win95/convert-dialog';
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement<any, any> },
|
||||
|
@ -237,7 +238,44 @@ export const ConvertDialog = (props: { files: File[] }) => {
|
|||
if (dialogVisible && files.length === 0) {
|
||||
handleClose();
|
||||
}
|
||||
}, [files, dialogVisible]);
|
||||
}, [files, dialogVisible, handleClose]);
|
||||
|
||||
const vintageMode = useShallowEqualSelector(state => state.appState.vintageMode);
|
||||
if (vintageMode) {
|
||||
const p = {
|
||||
visible,
|
||||
format,
|
||||
titleFormat,
|
||||
|
||||
files,
|
||||
setFiles,
|
||||
selectedTrackIndex,
|
||||
setSelectedTrack,
|
||||
|
||||
moveFileUp,
|
||||
moveFileDown,
|
||||
|
||||
handleClose,
|
||||
handleChangeFormat,
|
||||
handleChangeTitleFormat,
|
||||
handleConvert,
|
||||
|
||||
tracksOrderVisible,
|
||||
setTracksOrderVisible,
|
||||
handleToggleTracksOrder,
|
||||
selectedTrackRef,
|
||||
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive,
|
||||
open,
|
||||
|
||||
disableRemove,
|
||||
handleRemoveSelectedTrack,
|
||||
dialogVisible,
|
||||
};
|
||||
return <W95ConvertDialog {...p} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
|
|
@ -21,6 +21,7 @@ import { Controls } from './controls';
|
|||
import Box from '@material-ui/core/Box';
|
||||
import serviceRegistry from '../services/registry';
|
||||
import { TransitionProps } from '@material-ui/core/transitions';
|
||||
import { W95DumpDialog } from './win95/dump-dialog';
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement<any, any> },
|
||||
|
@ -96,6 +97,20 @@ export const DumpDialog = ({ trackIndexes }: { trackIndexes: number[] }) => {
|
|||
}
|
||||
}, [visible, setDevices]);
|
||||
|
||||
const vintageMode = useShallowEqualSelector(state => state.appState.vintageMode);
|
||||
|
||||
if (vintageMode) {
|
||||
const p = {
|
||||
handleClose,
|
||||
handleChange,
|
||||
handleStartTransfer,
|
||||
visible,
|
||||
devices,
|
||||
inputDeviceId,
|
||||
};
|
||||
return <W95DumpDialog {...p} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={visible}
|
||||
|
|
|
@ -45,6 +45,7 @@ import * as BadgeImpl from '@material-ui/core/Badge/Badge';
|
|||
import Button from '@material-ui/core/Button';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import { W95Main } from './win95/main';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
add: {
|
||||
|
@ -228,6 +229,41 @@ export const Main = (props: {}) => {
|
|||
dispatch(deleteTracks(selected));
|
||||
};
|
||||
|
||||
if (vintageMode) {
|
||||
const p = {
|
||||
disc,
|
||||
deviceName,
|
||||
|
||||
selected,
|
||||
setSelected,
|
||||
selectedCount,
|
||||
|
||||
tracks,
|
||||
uploadedFiles,
|
||||
setUploadedFiles,
|
||||
|
||||
onDrop,
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive,
|
||||
open,
|
||||
|
||||
moveMenuAnchorEl,
|
||||
setMoveMenuAnchorEl,
|
||||
|
||||
handleShowMoveMenu,
|
||||
handleCloseMoveMenu,
|
||||
handleMoveSelectedTrack,
|
||||
handleShowDumpDialog,
|
||||
handleDeleteSelected,
|
||||
handleRenameActionClick,
|
||||
handleRenameDoubleClick,
|
||||
handleSelectAllClick,
|
||||
handleSelectClick,
|
||||
};
|
||||
return <W95Main {...p} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Box className={classes.headBox}>
|
||||
|
|
|
@ -11,6 +11,7 @@ import LinearProgress from '@material-ui/core/LinearProgress';
|
|||
import Box from '@material-ui/core/Box';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { TransitionProps } from '@material-ui/core/transitions';
|
||||
import { W95RecordDialog } from './win95/record-dialog';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
progressPerc: {
|
||||
|
@ -34,6 +35,20 @@ export const RecordDialog = (props: {}) => {
|
|||
let { visible, trackTotal, trackDone, trackCurrent, titleCurrent } = useShallowEqualSelector(state => state.recordDialog);
|
||||
|
||||
let progressValue = Math.round(trackCurrent);
|
||||
|
||||
const vintageMode = useShallowEqualSelector(state => state.appState.vintageMode);
|
||||
if (vintageMode) {
|
||||
const p = {
|
||||
visible,
|
||||
trackTotal,
|
||||
trackDone,
|
||||
trackCurrent,
|
||||
titleCurrent,
|
||||
progressValue,
|
||||
};
|
||||
return <W95RecordDialog {...p} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={visible}
|
||||
|
|
|
@ -12,6 +12,7 @@ import TextField from '@material-ui/core/TextField';
|
|||
import Slide from '@material-ui/core/Slide';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { TransitionProps } from '@material-ui/core/transitions';
|
||||
import { W95RenameDialog } from './win95/rename-dialog';
|
||||
|
||||
const Transition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & { children?: React.ReactElement<any, any> },
|
||||
|
@ -41,6 +42,27 @@ export const RenameDialog = (props: {}) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
|
||||
dispatch(renameDialogActions.setCurrentName(event.target.value.substring(0, 120))); // MAX title length
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const { vintageMode } = useShallowEqualSelector(state => state.appState);
|
||||
if (vintageMode) {
|
||||
const p = {
|
||||
renameDialogVisible,
|
||||
renameDialogTitle,
|
||||
renameDialogIndex,
|
||||
what,
|
||||
handleCancelRename,
|
||||
handleDoRename,
|
||||
handleChange,
|
||||
};
|
||||
return <W95RenameDialog {...p} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={renameDialogVisible}
|
||||
|
@ -62,9 +84,7 @@ export const RenameDialog = (props: {}) => {
|
|||
onKeyDown={event => {
|
||||
event.key === `Enter` && handleDoRename();
|
||||
}}
|
||||
onChange={event => {
|
||||
dispatch(renameDialogActions.setCurrentName(event.target.value.substring(0, 120))); // MAX title length
|
||||
}}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
|
|
@ -24,6 +24,9 @@ import ExitToAppIcon from '@material-ui/icons/ExitToApp';
|
|||
import InfoIcon from '@material-ui/icons/Info';
|
||||
import ToggleOffIcon from '@material-ui/icons/ToggleOff';
|
||||
import ToggleOnIcon from '@material-ui/icons/ToggleOn';
|
||||
import Win95Icon from '../images/win95/win95.png';
|
||||
|
||||
import { W95TopMenu } from './win95/topmenu';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
listItemIcon: {
|
||||
|
@ -31,11 +34,11 @@ const useStyles = makeStyles(theme => ({
|
|||
},
|
||||
}));
|
||||
|
||||
export const TopMenu = function() {
|
||||
export const TopMenu = function(props: { onClick?: () => void }) {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
let { mainView, darkMode } = useShallowEqualSelector(state => state.appState);
|
||||
let { mainView, darkMode, vintageMode } = useShallowEqualSelector(state => state.appState);
|
||||
let discTitle = useShallowEqualSelector(state => state.main.disc?.title ?? ``);
|
||||
|
||||
const githubLinkRef = React.useRef<null | HTMLAnchorElement>(null);
|
||||
|
@ -53,6 +56,10 @@ export const TopMenu = function() {
|
|||
dispatch(appActions.setDarkMode(!darkMode));
|
||||
}, [dispatch, darkMode]);
|
||||
|
||||
const handleVintageMode = useCallback(() => {
|
||||
dispatch(appActions.setVintageMode(!vintageMode));
|
||||
}, [dispatch, vintageMode]);
|
||||
|
||||
const handleMenuClose = useCallback(() => {
|
||||
setMenuAnchorEl(null);
|
||||
}, [setMenuAnchorEl]);
|
||||
|
@ -144,6 +151,16 @@ export const TopMenu = function() {
|
|||
<ListItemText>Dark Mode</ListItemText>
|
||||
</MenuItem>
|
||||
);
|
||||
if (mainView === 'MAIN') {
|
||||
menuItems.push(
|
||||
<MenuItem key="vintageMode" onClick={handleVintageMode}>
|
||||
<ListItemIcon className={classes.listItemIcon}>
|
||||
<img alt="Windows 95" src={Win95Icon} width="24px" height="24px" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Retro Mode (beta)</ListItemText>
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
menuItems.push(
|
||||
<MenuItem key="about" onClick={handleShowAbout}>
|
||||
<ListItemIcon className={classes.listItemIcon}>
|
||||
|
@ -171,6 +188,19 @@ export const TopMenu = function() {
|
|||
</MenuItem>
|
||||
);
|
||||
|
||||
if (vintageMode) {
|
||||
const p = {
|
||||
mainView,
|
||||
onClick: props.onClick,
|
||||
handleWipeDisc,
|
||||
handleRefresh,
|
||||
handleRenameDisc,
|
||||
handleExit,
|
||||
handleShowAbout,
|
||||
handleVintageMode,
|
||||
};
|
||||
return <W95TopMenu {...p} />;
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<IconButton aria-label="actions" aria-controls="actions-menu" aria-haspopup="true" onClick={handleMenuOpen}>
|
||||
|
|
|
@ -15,6 +15,7 @@ import Box from '@material-ui/core/Box';
|
|||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { TransitionProps } from '@material-ui/core/transitions';
|
||||
import { Button } from '@material-ui/core';
|
||||
import { W95UploadDialog } from './win95/upload-dialog';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
progressPerc: {
|
||||
|
@ -60,6 +61,29 @@ export const UploadDialog = (props: {}) => {
|
|||
let progressValue = Math.floor((writtenProgress / totalProgress) * 100);
|
||||
let bufferValue = Math.floor((encryptedProgress / totalProgress) * 100);
|
||||
let convertedValue = Math.floor((trackConverting / trackTotal) * 100);
|
||||
|
||||
const vintageMode = useShallowEqualSelector(state => state.appState.vintageMode);
|
||||
if (vintageMode) {
|
||||
const p = {
|
||||
visible,
|
||||
cancelled,
|
||||
writtenProgress,
|
||||
encryptedProgress,
|
||||
totalProgress,
|
||||
|
||||
trackTotal,
|
||||
trackCurrent,
|
||||
trackConverting,
|
||||
titleCurrent,
|
||||
titleConverting,
|
||||
|
||||
handleCancelUpload,
|
||||
progressValue,
|
||||
bufferValue,
|
||||
convertedValue,
|
||||
};
|
||||
return <W95UploadDialog {...p} />;
|
||||
}
|
||||
return (
|
||||
<Dialog
|
||||
open={visible}
|
||||
|
|
|
@ -15,6 +15,7 @@ import Link from '@material-ui/core/Link';
|
|||
import { AboutDialog } from './about-dialog';
|
||||
import { TopMenu } from './topmenu';
|
||||
import ChromeIconPath from '../images/chrome-icon.svg';
|
||||
import { W95Welcome } from './win95/welcome';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
main: {
|
||||
|
@ -49,9 +50,8 @@ const useStyles = makeStyles(theme => ({
|
|||
|
||||
export const Welcome = (props: {}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { browserSupported, pairingFailed, pairingMessage } = useShallowEqualSelector(state => state.appState);
|
||||
const { browserSupported, pairingFailed, pairingMessage, vintageMode } = useShallowEqualSelector(state => state.appState);
|
||||
if (pairingMessage.toLowerCase().match(/denied/)) {
|
||||
// show linux instructions
|
||||
}
|
||||
|
@ -63,6 +63,15 @@ export const Welcome = (props: {}) => {
|
|||
setWhyUnsupported(true);
|
||||
};
|
||||
|
||||
if (vintageMode) {
|
||||
const p = {
|
||||
dispatch,
|
||||
pairingFailed,
|
||||
pairingMessage,
|
||||
};
|
||||
return <W95Welcome {...p}></W95Welcome>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Box className={classes.headBox}>
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
import { Button, WindowHeader, Anchor } from 'react95';
|
||||
import { FooterButton, DialogOverlay, DialogWindow, DialogFooter, DialogWindowContent, WindowCloseIcon } from './common';
|
||||
|
||||
export const W95AboutDialog = (props: { visible: boolean; handleClose: () => void }) => {
|
||||
if (!props.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogOverlay>
|
||||
<DialogWindow>
|
||||
<WindowHeader style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ flex: '1 1 auto' }}>About Web MiniDisc</span>
|
||||
<Button onClick={props.handleClose}>
|
||||
<WindowCloseIcon />
|
||||
</Button>
|
||||
</WindowHeader>
|
||||
<DialogWindowContent>
|
||||
<ul>
|
||||
<li>
|
||||
<Anchor rel="noopener noreferrer" href="https://www.ffmpeg.org/" target="_blank">
|
||||
FFmpeg
|
||||
</Anchor>{' '}
|
||||
and{' '}
|
||||
<Anchor rel="noopener noreferrer" href="https://github.com/ffmpegjs/FFmpeg" target="_blank">
|
||||
ffmpegjs
|
||||
</Anchor>
|
||||
, to read your audio files (wav, mp3, ogg, mp4, etc...).
|
||||
</li>
|
||||
<li>
|
||||
<Anchor rel="noopener noreferrer" href="https://github.com/dcherednik/atracdenc/" target="_blank">
|
||||
Atracdenc
|
||||
</Anchor>
|
||||
, to support atrac3 encoding (lp2, lp4 audio formats).
|
||||
</li>
|
||||
<li>
|
||||
<Anchor rel="noopener noreferrer" href="https://emscripten.org/" target="_blank">
|
||||
Emscripten
|
||||
</Anchor>
|
||||
, to run both FFmpeg and Atracdenc in the browser.
|
||||
</li>
|
||||
<li>
|
||||
<Anchor rel="noopener noreferrer" href="https://github.com/cybercase/netmd-js" target="_blank">
|
||||
netmd-js
|
||||
</Anchor>
|
||||
, to send commands to NetMD devices using Javascript.
|
||||
</li>
|
||||
<li>
|
||||
<Anchor rel="noopener noreferrer" href="https://github.com/glaubitz/linux-minidisc" target="_blank">
|
||||
linux-minidisc
|
||||
</Anchor>
|
||||
, to make the netmd-js project possible.
|
||||
</li>
|
||||
<li>
|
||||
<Anchor rel="noopener noreferrer" href="https://material-ui.com/" target="_blank">
|
||||
material-ui
|
||||
</Anchor>
|
||||
, to build the user interface.
|
||||
</li>
|
||||
</ul>
|
||||
<DialogFooter>
|
||||
<FooterButton onClick={props.handleClose}>OK</FooterButton>
|
||||
</DialogFooter>
|
||||
</DialogWindowContent>
|
||||
</DialogWindow>
|
||||
</DialogOverlay>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,133 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { forAnyDesktop, forWideDesktop, useShallowEqualSelector } from '../../utils';
|
||||
|
||||
import { Welcome } from '../welcome';
|
||||
import { Main } from '../main';
|
||||
import { actions as appActions } from '../../redux/app-feature';
|
||||
|
||||
import { Window, WindowHeader, Button, Toolbar, Panel, Hourglass, styleReset, Anchor } from 'react95';
|
||||
import { createGlobalStyle, ThemeProvider as StyledThemeProvider } from 'styled-components';
|
||||
import original from 'react95/dist/themes/original';
|
||||
import { TopMenu } from '../topmenu';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import CDPlayerIconUrl from '../../images/win95/cdplayer.png';
|
||||
import { WindowCloseIcon } from './common';
|
||||
|
||||
const GlobalStyles = createGlobalStyle`
|
||||
${styleReset}
|
||||
body {
|
||||
font-family: 'ms_sans_serif';
|
||||
}
|
||||
img {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
`;
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
desktop: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'teal',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
window: {
|
||||
display: 'flex !important', // This is needed to override the styledComponent prop :(
|
||||
flexDirection: 'column',
|
||||
width: 'auto',
|
||||
height: '100%',
|
||||
[forAnyDesktop(theme)]: {
|
||||
width: 600,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
height: 600,
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
[forWideDesktop(theme)]: {
|
||||
width: 700,
|
||||
height: 700,
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
loading: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
export const W95App = () => {
|
||||
const classes = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const { mainView, loading } = useShallowEqualSelector(state => state.appState);
|
||||
const [isMenuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const handleExit = useCallback(() => {
|
||||
dispatch(appActions.setState('WELCOME'));
|
||||
}, [dispatch]);
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
setMenuOpen(false);
|
||||
}, [setMenuOpen]);
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
setMenuOpen(!isMenuOpen);
|
||||
}, [isMenuOpen, setMenuOpen]);
|
||||
|
||||
const currentTheme = original;
|
||||
const theme = {
|
||||
...currentTheme,
|
||||
selectedTableRow: {
|
||||
background: currentTheme.hoverBackground,
|
||||
color: currentTheme.canvasTextInvert,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.desktop}>
|
||||
<GlobalStyles />
|
||||
<StyledThemeProvider theme={theme}>
|
||||
<Window className={classes.window}>
|
||||
<WindowHeader style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<img alt="CD Player" src={CDPlayerIconUrl} />
|
||||
<span style={{ flex: '1 1 auto', marginLeft: '4px' }}>Web MiniDisc</span>
|
||||
{mainView === 'MAIN' ? (
|
||||
<Button onClick={handleExit}>
|
||||
<WindowCloseIcon />
|
||||
</Button>
|
||||
) : null}
|
||||
</WindowHeader>
|
||||
<Toolbar>
|
||||
<Button variant="menu" size="sm" active={isMenuOpen} onClick={toggleMenu}>
|
||||
File
|
||||
</Button>
|
||||
{isMenuOpen ? <TopMenu onClick={closeMenu} /> : null}
|
||||
</Toolbar>
|
||||
<>
|
||||
{mainView === 'WELCOME' ? <Welcome /> : null}
|
||||
{mainView === 'MAIN' ? <Main /> : null}
|
||||
</>
|
||||
<Panel variant="well">
|
||||
|
||||
{' (c) '}
|
||||
<Anchor rel="noopener noreferrer" color="inherit" target="_blank" href="https://stefano.brilli.me/">
|
||||
Stefano Brilli
|
||||
</Anchor>{' '}
|
||||
{new Date().getFullYear()}
|
||||
{'.'}
|
||||
</Panel>
|
||||
{loading ? (
|
||||
<div className={classes.loading}>
|
||||
<Hourglass size={32} />
|
||||
</div>
|
||||
) : null}
|
||||
</Window>
|
||||
</StyledThemeProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,110 @@
|
|||
import { Button, Window, WindowContent, TableRow } from 'react95';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const DialogOverlay = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
export const DialogWindow = styled(Window)`
|
||||
width: 80%;
|
||||
left: 10%;
|
||||
top: 20%;
|
||||
`;
|
||||
|
||||
export const DialogFooter = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 16px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const DialogWindowContent = styled(WindowContent)`
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const FooterButton = styled(Button)`
|
||||
margin-left: 16px;
|
||||
min-width: 90px;
|
||||
`;
|
||||
|
||||
export const CustomTableRow = styled(TableRow)`
|
||||
cursor: default;
|
||||
&:hover {
|
||||
color: ${(styled: any) => styled.theme.canvasText};
|
||||
background-color: initial;
|
||||
}
|
||||
`;
|
||||
|
||||
export const WindowCloseIcon = styled.span`
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
transform: rotateZ(45deg);
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 3px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: #0a0a0a;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 3px;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background-color: #0a0a0a;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FloatingButton = styled.button`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
right: 24px;
|
||||
z-index: 1;
|
||||
border-radius: 50%;
|
||||
background: rgb(185, 106, 201);
|
||||
border-width: 4px;
|
||||
border-style: solid;
|
||||
border-color: rgb(233, 128, 252) rgb(111, 45, 189) rgb(111, 45, 189) rgb(233, 128, 252);
|
||||
box-shadow: rgb(0 0 0 / 45%) 4px 4px 10px 0px;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAADKgAAAyoBEJdYGAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAHWSURBVHic7d3BbdYwGIDhz1wYAakXNmOGdhKY4d+MC1JH4BQurcQ1EmmE3+cZwLbkV04OjrKO45idrLWeZ+b7RcO/HMfx46Kxb/Hp7gVwLwHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAuB0DePpPx77F2umXMWutzzPzc2a+XDTF68x8PY7j90Xjf7g1M893L+IfeZqZb3Pd5r97nZnHzPy6eJ4PsWZmnyOA03Z8B+AEAcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAohzLfy8va6F+zDklO0+DNnqEfC2MY8Lp3jstPkzmwXw5sqjeYtj/287BsAJAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxAogTQJwA4gQQJ4A4AcQJIE4AcQKIE0CcAOIEECeAOAHECSBOAHECiBNAnADiBBAngDgBxAkgTgBxfwAw2y4BcmRzJgAAAABJRU5ErkJggg==');
|
||||
background-size: 30px;
|
||||
background-repeat: no-repeat;
|
||||
filter: drop-shadow(rgb(233, 128, 252) 1px 1px 0px) drop-shadow(rgb(111, 45, 189) -1px -1px 0px);
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
&:active {
|
||||
border-width: 4px;
|
||||
border-style: solid;
|
||||
border-color: rgb(111, 45, 189) rgb(233, 128, 252) rgb(233, 128, 252) rgb(111, 45, 189);
|
||||
box-shadow: rgb(0 0 0 / 55%) 3px 3px 5px 0px;
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,80 @@
|
|||
import React from 'react';
|
||||
import { Button, Panel } from 'react95';
|
||||
import { belowDesktop } from '../../utils';
|
||||
|
||||
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
|
||||
import StopIcon from '@material-ui/icons/Stop';
|
||||
import SkipNextIcon from '@material-ui/icons/SkipNext';
|
||||
import SkipPreviousIcon from '@material-ui/icons/SkipPrevious';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
alignItems: 'center',
|
||||
[belowDesktop(theme)]: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
},
|
||||
lcd: {
|
||||
backgroundColor: 'black !important',
|
||||
flex: '1 1 auto',
|
||||
margin: '0 80px 0 0px',
|
||||
minWidth: 150,
|
||||
height: 48,
|
||||
color: 'white !important',
|
||||
fontFamily: 'LCDDot',
|
||||
},
|
||||
}));
|
||||
|
||||
export const W95Controls = (props: {
|
||||
handlePrev: () => void;
|
||||
handlePlay: () => void;
|
||||
handleStop: () => void;
|
||||
handleNext: () => void;
|
||||
message: string;
|
||||
discPresent: boolean;
|
||||
classes: any;
|
||||
lcdScroll: number;
|
||||
lcdRef: React.RefObject<HTMLParagraphElement>;
|
||||
lcdScrollDuration: number;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<Button onClick={props.handlePrev}>
|
||||
<SkipPreviousIcon />
|
||||
</Button>
|
||||
<Button onClick={props.handlePlay}>
|
||||
<PlayArrowIcon />
|
||||
</Button>
|
||||
<Button onClick={props.handleStop}>
|
||||
<StopIcon />
|
||||
</Button>
|
||||
<Button onClick={props.handleNext} style={{ marginRight: 16 }}>
|
||||
<SkipNextIcon />
|
||||
</Button>
|
||||
|
||||
<Panel variant="well" className={classes.lcd}>
|
||||
<div className={props.classes.lcdText} style={{ left: 16, width: 'calc(100% - 16px)' }}>
|
||||
<span
|
||||
className={props.lcdScroll ? props.classes.scrollingStatusMessage : props.classes.statusMessage}
|
||||
ref={props.lcdRef}
|
||||
style={
|
||||
props.message && props.lcdScroll > 0
|
||||
? {
|
||||
animationDuration: `${props.lcdScrollDuration}s`,
|
||||
transform: `translate(-${props.lcdScroll}%)`,
|
||||
top: 12,
|
||||
}
|
||||
: { top: 12 }
|
||||
}
|
||||
>
|
||||
{props.message}
|
||||
</span>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,144 @@
|
|||
import React, { useCallback, useContext } from 'react';
|
||||
import { Button, WindowHeader, Fieldset, Select, Table, TableBody, TableDataCell, Divider, Toolbar } from 'react95';
|
||||
import { DialogOverlay, DialogWindow, DialogFooter, DialogWindowContent, WindowCloseIcon, FooterButton, CustomTableRow } from './common';
|
||||
import { TitleFormatType, UploadFormat } from '../../redux/convert-dialog-feature';
|
||||
import { DropzoneInputProps, DropzoneRootProps } from 'react-dropzone';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import ArrowUpIconUrl from '../../images/win95/arrowup.png';
|
||||
import ArrowDownIconUrl from '../../images/win95/arrowdown.png';
|
||||
import DeleteIconUrl from '../../images/win95/delete.png';
|
||||
|
||||
const trackTitleOptions = [
|
||||
{ value: 'filename', label: 'Filename' },
|
||||
{ value: 'title', label: 'Title' },
|
||||
{ value: 'album-title', label: 'Album - Title' },
|
||||
{ value: 'artist-title', label: 'Artist - Title' },
|
||||
{ value: 'artist-album-title', label: 'Artist - Album - Title' },
|
||||
];
|
||||
|
||||
const recordModeOptions = [
|
||||
{ value: 'SP', label: 'SP' },
|
||||
{ value: 'LP2', label: 'LP2' },
|
||||
{ value: 'LP4', label: 'LP4' },
|
||||
];
|
||||
|
||||
export const W95ConvertDialog = (props: {
|
||||
visible: boolean;
|
||||
format: UploadFormat;
|
||||
titleFormat: TitleFormatType;
|
||||
files: File[];
|
||||
setFiles: React.Dispatch<React.SetStateAction<File[]>>;
|
||||
selectedTrackIndex: number;
|
||||
setSelectedTrack: React.Dispatch<React.SetStateAction<number>>;
|
||||
moveFileUp: () => void;
|
||||
moveFileDown: () => void;
|
||||
handleClose: () => void;
|
||||
handleChangeFormat: (ev: any, newFormat: any) => void;
|
||||
handleChangeTitleFormat: (
|
||||
event: React.ChangeEvent<{
|
||||
value: any;
|
||||
}>
|
||||
) => void;
|
||||
handleConvert: () => void;
|
||||
tracksOrderVisible: boolean;
|
||||
setTracksOrderVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleToggleTracksOrder: () => void;
|
||||
selectedTrackRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
getRootProps: (props?: DropzoneRootProps | undefined) => DropzoneRootProps;
|
||||
getInputProps: (props?: DropzoneInputProps | undefined) => DropzoneInputProps;
|
||||
isDragActive: boolean;
|
||||
open: () => void;
|
||||
disableRemove: boolean;
|
||||
handleRemoveSelectedTrack: () => void;
|
||||
dialogVisible: boolean;
|
||||
}) => {
|
||||
const themeContext = useContext(ThemeContext);
|
||||
|
||||
const renderTracks = useCallback(() => {
|
||||
return props.files.map((file, i) => {
|
||||
const isSelected = props.selectedTrackIndex === i;
|
||||
const ref = isSelected ? props.selectedTrackRef : null;
|
||||
return (
|
||||
<CustomTableRow
|
||||
key={`${i}`}
|
||||
onClick={() => props.setSelectedTrack(i)}
|
||||
ref={ref}
|
||||
style={isSelected ? themeContext.selectedTableRow : {}}
|
||||
>
|
||||
<TableDataCell>{file.name}</TableDataCell>
|
||||
</CustomTableRow>
|
||||
);
|
||||
});
|
||||
}, [props, themeContext]);
|
||||
|
||||
if (!props.dialogVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogOverlay>
|
||||
<DialogWindow>
|
||||
<WindowHeader style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ flex: '1 1 auto' }}>Upload Settings</span>
|
||||
<Button onClick={props.handleClose}>
|
||||
<WindowCloseIcon />
|
||||
</Button>
|
||||
</WindowHeader>
|
||||
<DialogWindowContent>
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
<Fieldset label="Recording Mode" style={{ display: 'flex', flex: '1 1 auto' }}>
|
||||
<Select
|
||||
defaultValue={props.format}
|
||||
options={recordModeOptions}
|
||||
width={90}
|
||||
onChange={(ev: any, format: any) => props.handleChangeFormat(ev, format.value)}
|
||||
/>
|
||||
</Fieldset>
|
||||
<Fieldset label="Track title" style={{ flex: '1 1 auto', marginLeft: 16 }}>
|
||||
<Select
|
||||
defaultValue={props.titleFormat}
|
||||
options={trackTitleOptions}
|
||||
width={180}
|
||||
onChange={props.handleChangeTitleFormat}
|
||||
/>
|
||||
</Fieldset>
|
||||
</div>
|
||||
{props.tracksOrderVisible ? (
|
||||
<div {...props.getRootProps()} style={{ width: '100%', marginTop: 16 }}>
|
||||
<Divider style={{ marginTop: 16 }} />
|
||||
<Toolbar style={{ display: 'flex' }}>
|
||||
<Button variant="menu" onClick={props.open}>
|
||||
Add...
|
||||
</Button>
|
||||
<Button variant="menu" disabled={props.disableRemove} onClick={props.handleRemoveSelectedTrack}>
|
||||
<img alt="delete" src={DeleteIconUrl} style={{ marginRight: 4 }} />
|
||||
Remove
|
||||
</Button>
|
||||
<div style={{ flex: '1 1 auto' }}></div>
|
||||
<Button variant="menu" disabled={props.disableRemove} onClick={props.moveFileDown}>
|
||||
<img alt="Move Down" src={ArrowDownIconUrl} />
|
||||
</Button>
|
||||
<Button variant="menu" disabled={props.disableRemove} onClick={props.moveFileUp}>
|
||||
<img alt="Move Up" src={ArrowUpIconUrl} />
|
||||
</Button>
|
||||
</Toolbar>
|
||||
<div style={{ maxHeight: '30vh', overflow: 'scroll' }}>
|
||||
<Table>
|
||||
<TableBody>{renderTracks()}</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<input {...props.getInputProps()} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={props.handleToggleTracksOrder}>{`${props.tracksOrderVisible ? 'Hide' : 'Show'} Tracks`}</Button>
|
||||
<div style={{ flex: '1 1 auto' }}></div>
|
||||
<FooterButton onClick={props.handleConvert}>OK</FooterButton>
|
||||
<FooterButton onClick={props.handleClose}>Cancel</FooterButton>
|
||||
</DialogFooter>
|
||||
</DialogWindowContent>
|
||||
</DialogWindow>
|
||||
</DialogOverlay>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import { Button, WindowHeader, Fieldset, Select } from 'react95';
|
||||
import { Controls } from '../controls';
|
||||
import { DialogOverlay, DialogWindow, DialogFooter, DialogWindowContent, WindowCloseIcon, FooterButton } from './common';
|
||||
|
||||
export const W95DumpDialog = (props: {
|
||||
handleClose: () => void;
|
||||
handleChange: (
|
||||
ev: React.ChangeEvent<{
|
||||
value: unknown;
|
||||
}>
|
||||
) => void;
|
||||
handleStartTransfer: () => void;
|
||||
visible: boolean;
|
||||
devices: {
|
||||
deviceId: string;
|
||||
label: string;
|
||||
}[];
|
||||
inputDeviceId: string;
|
||||
}) => {
|
||||
if (!props.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogOverlay>
|
||||
<DialogWindow>
|
||||
<WindowHeader style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ flex: '1 1 auto' }}>Record Selected Tracks</span>
|
||||
<Button onClick={props.handleClose}>
|
||||
<WindowCloseIcon />
|
||||
</Button>
|
||||
</WindowHeader>
|
||||
<DialogWindowContent>
|
||||
<div style={{ width: '100%', display: 'flex', alignItems: 'flex-Start', flexDirection: 'column' }}>
|
||||
<p>1. Connect your MD Player line-out to your PC audio line-in.</p>
|
||||
<p>2. Use the controls at the bottom right to play some tracks.</p>
|
||||
<p>3. Select the input source. You should hear the tracks playing on your PC.</p>
|
||||
<p>4. Adjust the input gain and the line-out volume of your device.</p>
|
||||
<Fieldset label="Input Source" style={{ display: 'flex', flex: '1 1 auto', margin: '32px 0' }}>
|
||||
<Select
|
||||
defaultValue={props.inputDeviceId || ''}
|
||||
options={props.devices
|
||||
.concat([{ deviceId: '', label: 'None' }])
|
||||
.map(({ deviceId, label }) => ({ value: deviceId, label }))}
|
||||
onChange={props.handleChange}
|
||||
width={200}
|
||||
/>
|
||||
</Fieldset>
|
||||
<Controls />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<div style={{ flex: '1 1 auto' }}></div>
|
||||
<FooterButton onClick={props.handleClose}>Cancel</FooterButton>
|
||||
<FooterButton onClick={props.handleStartTransfer} disabled={props.inputDeviceId === ''}>
|
||||
Start Record
|
||||
</FooterButton>
|
||||
</DialogFooter>
|
||||
</DialogWindowContent>
|
||||
</DialogWindow>
|
||||
</DialogOverlay>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,231 @@
|
|||
import React, { useContext } from 'react';
|
||||
import {
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableHeadCell,
|
||||
TableBody,
|
||||
TableDataCell,
|
||||
Divider,
|
||||
Toolbar,
|
||||
Bar,
|
||||
Button,
|
||||
WindowContent,
|
||||
Tooltip,
|
||||
List,
|
||||
ListItem,
|
||||
} from 'react95';
|
||||
import { Disc, formatTimeFromFrames } from 'netmd-js';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { DropzoneRootProps, DropzoneInputProps } from 'react-dropzone';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import { Controls } from '../controls';
|
||||
import { useShallowEqualSelector } from '../../utils';
|
||||
|
||||
import DeleteIconUrl from '../../images/win95/delete.png';
|
||||
import MicIconUrl from '../../images/win95/mic.png';
|
||||
import MoveIconUrl from '../../images/win95/move.png';
|
||||
import RenameIconUrl from '../../images/win95/rename.png';
|
||||
import DeviceIconUrl from '../../images/win95/device.png';
|
||||
import { RenameDialog } from '../rename-dialog';
|
||||
import { AboutDialog } from '../about-dialog';
|
||||
|
||||
import MDIconUrl from '../../images/win95/minidisc32.png';
|
||||
import { FloatingButton, CustomTableRow } from './common';
|
||||
import { ConvertDialog } from '../convert-dialog';
|
||||
import { UploadDialog } from '../upload-dialog';
|
||||
import { ErrorDialog } from '../error-dialog';
|
||||
import { RecordDialog } from '../record-dialog';
|
||||
import { DumpDialog } from '../dump-dialog';
|
||||
import { PanicDialog } from '../panic-dialog';
|
||||
|
||||
const useStyles = makeStyles((theme: any) => ({
|
||||
container: {
|
||||
width: '100%',
|
||||
flex: '1 1 auto',
|
||||
display: 'flex',
|
||||
minHeight: 0,
|
||||
'& > div': {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
table: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'flex !important',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
windowContent: {
|
||||
flex: '1 1 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: 0,
|
||||
},
|
||||
controlsContainer: {
|
||||
width: '100%',
|
||||
marginTop: 16,
|
||||
},
|
||||
toolbarIcon: {
|
||||
marginRight: 4,
|
||||
},
|
||||
toolbarItem: {
|
||||
padding: '6px 10px',
|
||||
},
|
||||
}));
|
||||
|
||||
export const W95Main = (props: {
|
||||
disc: Disc | null;
|
||||
deviceName: string;
|
||||
selected: number[];
|
||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||
selectedCount: number;
|
||||
tracks: {
|
||||
index: number;
|
||||
title: string;
|
||||
group: string;
|
||||
duration: string;
|
||||
encoding: string;
|
||||
}[];
|
||||
uploadedFiles: File[];
|
||||
setUploadedFiles: React.Dispatch<React.SetStateAction<File[]>>;
|
||||
onDrop: (acceptedFiles: File[], rejectedFiles: File[]) => void;
|
||||
getRootProps: (props?: DropzoneRootProps | undefined) => DropzoneRootProps;
|
||||
getInputProps: (props?: DropzoneInputProps | undefined) => DropzoneInputProps;
|
||||
isDragActive: boolean;
|
||||
open: () => void;
|
||||
moveMenuAnchorEl: HTMLElement | null;
|
||||
setMoveMenuAnchorEl: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
|
||||
handleShowMoveMenu: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
handleCloseMoveMenu: () => void;
|
||||
handleMoveSelectedTrack: (destIndex: number) => void;
|
||||
handleShowDumpDialog: () => void;
|
||||
handleDeleteSelected: (event: React.MouseEvent) => void;
|
||||
handleRenameActionClick: (event: React.MouseEvent) => void;
|
||||
handleRenameDoubleClick: (event: React.MouseEvent, item: number) => void;
|
||||
handleSelectAllClick: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
handleSelectClick: (event: React.MouseEvent, item: number) => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const themeContext = useContext(ThemeContext);
|
||||
const { mainView } = useShallowEqualSelector(state => state.appState);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Divider />
|
||||
<Toolbar style={{ flexWrap: 'wrap', position: 'relative' }}>
|
||||
{props.selectedCount === 0 ? (
|
||||
<>
|
||||
<img alt="device" src={DeviceIconUrl} style={{ marginTop: -10, marginLeft: 10 }} />
|
||||
<div className={classes.toolbarItem}>
|
||||
{`${props.deviceName}: (` || `Loading...`}
|
||||
{props.disc?.title || `Untitled Disc`}
|
||||
{`)`}
|
||||
</div>
|
||||
<Bar size={35} />
|
||||
<img alt="minidisc" src={MDIconUrl} style={{ width: 32, marginLeft: 10 }} />
|
||||
{props.disc !== null ? (
|
||||
<Tooltip
|
||||
text={`${formatTimeFromFrames(props.disc.left * 2, false)} in LP2 or ${formatTimeFromFrames(
|
||||
props.disc.left * 4,
|
||||
false
|
||||
)} in LP4`}
|
||||
enterDelay={100}
|
||||
leaveDelay={500}
|
||||
>
|
||||
<div className={classes.toolbarItem}>{`${formatTimeFromFrames(
|
||||
props.disc.left,
|
||||
false
|
||||
)} left of ${formatTimeFromFrames(props.disc.total, false)} `}</div>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{props.selectedCount > 0 ? (
|
||||
<>
|
||||
<Button variant="menu" disabled={props.selectedCount !== 1} onClick={props.handleShowMoveMenu}>
|
||||
<img alt="move" src={MoveIconUrl} className={classes.toolbarIcon} />
|
||||
Move
|
||||
</Button>
|
||||
<Button variant="menu" onClick={props.handleShowDumpDialog}>
|
||||
<img alt="record" src={MicIconUrl} className={classes.toolbarIcon} />
|
||||
Record
|
||||
</Button>
|
||||
<Button variant="menu" onClick={props.handleDeleteSelected}>
|
||||
<img alt="delete" src={DeleteIconUrl} className={classes.toolbarIcon} />
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant="menu" onClick={props.handleRenameActionClick} disabled={props.selectedCount > 1}>
|
||||
<img alt="rename" src={RenameIconUrl} className={classes.toolbarIcon} />
|
||||
Rename
|
||||
</Button>
|
||||
{!!props.moveMenuAnchorEl ? (
|
||||
<List style={{ position: 'absolute', left: 16, top: 32, zIndex: 2 }}>
|
||||
{Array(props.tracks.length)
|
||||
.fill(null)
|
||||
.map((_, i) => {
|
||||
return (
|
||||
<ListItem key={`pos-${i}`} onClick={() => props.handleMoveSelectedTrack(i)}>
|
||||
{i + 1}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
<Bar size={35} />
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
<WindowContent className={classes.windowContent}>
|
||||
<div className={classes.container} {...props.getRootProps()} style={{ outline: 'none' }}>
|
||||
<input {...props.getInputProps()} />
|
||||
<Table className={classes.table}>
|
||||
<TableHead>
|
||||
<TableRow head style={{ display: 'flex' }}>
|
||||
<TableHeadCell style={{ width: '2ch' }}>#</TableHeadCell>
|
||||
<TableHeadCell style={{ textAlign: 'left', flex: '1 1 auto' }}>Title</TableHeadCell>
|
||||
<TableHeadCell style={{ textAlign: 'right', width: '20%' }}>Duration</TableHeadCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.tracks.map(track => (
|
||||
<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)}
|
||||
>
|
||||
<TableDataCell style={{ textAlign: 'center', width: '2ch' }}>{track.index + 1}</TableDataCell>
|
||||
<TableDataCell style={{ width: '80%' }}>
|
||||
<div>{track.title || `No Title`}</div>
|
||||
</TableDataCell>
|
||||
<TableDataCell style={{ textAlign: 'right', width: '20%' }}>
|
||||
<span>{track.encoding}</span>
|
||||
|
||||
<span>{track.duration}</span>
|
||||
</TableDataCell>
|
||||
</CustomTableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className={classes.controlsContainer}>{mainView === 'MAIN' ? <Controls /> : null}</div>
|
||||
</WindowContent>
|
||||
<FloatingButton onClick={props.open} />
|
||||
|
||||
<UploadDialog />
|
||||
<RenameDialog />
|
||||
<ErrorDialog />
|
||||
<ConvertDialog files={props.uploadedFiles} />
|
||||
<RecordDialog />
|
||||
<DumpDialog trackIndexes={props.selected} />
|
||||
<AboutDialog />
|
||||
<PanicDialog />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import { WindowHeader, Progress } from 'react95';
|
||||
import { DialogOverlay, DialogWindow, DialogWindowContent } from './common';
|
||||
|
||||
export const W95RecordDialog = (props: {
|
||||
visible: boolean;
|
||||
trackTotal: number;
|
||||
trackDone: number;
|
||||
trackCurrent: number;
|
||||
titleCurrent: string;
|
||||
progressValue: number;
|
||||
}) => {
|
||||
if (!props.visible) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<DialogOverlay>
|
||||
<DialogWindow>
|
||||
<WindowHeader>
|
||||
<span>Recording...</span>
|
||||
</WindowHeader>
|
||||
<DialogWindowContent>
|
||||
<p style={{ marginBottom: 16, width: '100%' }}>{`Recording track ${props.trackDone + 1} of ${props.trackTotal}: ${
|
||||
props.titleCurrent
|
||||
}`}</p>
|
||||
<Progress value={props.progressValue} hideValue={props.progressValue < 0} />
|
||||
</DialogWindowContent>
|
||||
</DialogWindow>
|
||||
</DialogOverlay>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import { WindowHeader, WindowContent, TextField } from 'react95';
|
||||
import { DialogOverlay, DialogFooter, DialogWindow, FooterButton } from './common';
|
||||
|
||||
export const W95RenameDialog = (props: {
|
||||
renameDialogVisible: boolean;
|
||||
renameDialogTitle: string;
|
||||
renameDialogIndex: number;
|
||||
what: string;
|
||||
handleCancelRename: () => void;
|
||||
handleDoRename: () => void;
|
||||
handleChange: (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
|
||||
}) => {
|
||||
if (!props.renameDialogVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogOverlay>
|
||||
<DialogWindow>
|
||||
<WindowHeader>
|
||||
<span>Rename {props.what}</span>
|
||||
</WindowHeader>
|
||||
<WindowContent>
|
||||
<p style={{ marginBottom: 4 }}>Track Name:</p>
|
||||
<TextField
|
||||
style={{ marginBottom: 16 }}
|
||||
value={props.renameDialogTitle}
|
||||
placeholder="Type here..."
|
||||
onChange={props.handleChange}
|
||||
onKeyDown={(event: any) => {
|
||||
event.key === `Enter` && props.handleDoRename();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<DialogFooter>
|
||||
<FooterButton onClick={props.handleDoRename}>OK</FooterButton>
|
||||
<FooterButton onClick={props.handleCancelRename}>Cancel</FooterButton>
|
||||
</DialogFooter>
|
||||
</WindowContent>
|
||||
</DialogWindow>
|
||||
</DialogOverlay>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
import React from 'react';
|
||||
import { List, ListItem, Checkbox, Divider } from 'react95';
|
||||
import { Views } from '../../redux/app-feature';
|
||||
|
||||
export const W95TopMenu = (props: {
|
||||
mainView: Views;
|
||||
onClick?: () => void;
|
||||
handleWipeDisc: () => void;
|
||||
handleRefresh: () => void;
|
||||
handleRenameDisc: () => void;
|
||||
handleExit: () => void;
|
||||
handleShowAbout: () => void;
|
||||
handleVintageMode: () => void;
|
||||
}) => {
|
||||
const items = [];
|
||||
|
||||
if (props.mainView === 'MAIN') {
|
||||
items.push(
|
||||
<ListItem key="update" onClick={props.handleRefresh}>
|
||||
Reload TOC
|
||||
</ListItem>
|
||||
);
|
||||
items.push(
|
||||
<ListItem key="title" onClick={props.handleRenameDisc}>
|
||||
Rename Disc
|
||||
</ListItem>
|
||||
);
|
||||
items.push(
|
||||
<ListItem key="wipe" onClick={props.handleWipeDisc}>
|
||||
Wipe Disc
|
||||
</ListItem>
|
||||
);
|
||||
items.push(
|
||||
<ListItem key="vintage" onClick={props.handleVintageMode}>
|
||||
<Checkbox checked name="vintageMode" variant="menu" value="vintageMode" label="Retro Mode (beta)" defaultChecked />
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
items.push(<Divider key="d1" />);
|
||||
items.push(
|
||||
<ListItem key="exit" onClick={props.handleExit}>
|
||||
Exit
|
||||
</ListItem>
|
||||
);
|
||||
items.push(<Divider key="d2" />);
|
||||
}
|
||||
items.push(
|
||||
<ListItem key="about" onClick={props.handleShowAbout}>
|
||||
About...
|
||||
</ListItem>
|
||||
);
|
||||
items.push(
|
||||
<ListItem key={`menu-gh`}>
|
||||
<a rel="noopener noreferrer" href="https://github.com/cybercase/webminidisc" target="_blank">
|
||||
Fork me on GitHub
|
||||
</a>
|
||||
</ListItem>
|
||||
);
|
||||
return (
|
||||
<List
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '100%',
|
||||
zIndex: '9999',
|
||||
}}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{items}
|
||||
</List>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react';
|
||||
import { WindowHeader, Button, Progress } from 'react95';
|
||||
import { DialogOverlay, DialogWindow, DialogFooter, DialogWindowContent } from './common';
|
||||
|
||||
export const W95UploadDialog = (props: {
|
||||
visible: boolean;
|
||||
cancelled: boolean;
|
||||
writtenProgress: number;
|
||||
encryptedProgress: number;
|
||||
totalProgress: number;
|
||||
trackTotal: number;
|
||||
trackCurrent: number;
|
||||
trackConverting: number;
|
||||
titleCurrent: string;
|
||||
titleConverting: string;
|
||||
handleCancelUpload: () => void;
|
||||
progressValue: number;
|
||||
bufferValue: number;
|
||||
convertedValue: number;
|
||||
}) => {
|
||||
if (!props.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogOverlay>
|
||||
<DialogWindow>
|
||||
<WindowHeader>
|
||||
<span>Recording...</span>
|
||||
</WindowHeader>
|
||||
<DialogWindowContent>
|
||||
<div style={{ width: '100%' }}>
|
||||
{props.convertedValue === 100 && props.trackConverting === props.trackTotal
|
||||
? `Conversion completed`
|
||||
: `Converting ${props.trackConverting + 1} of ${props.trackTotal}: ${props.titleConverting}`}
|
||||
</div>
|
||||
<Progress value={Math.floor(props.convertedValue)} />
|
||||
|
||||
<div style={{ width: '100%', marginTop: 16 }}>
|
||||
Uploading {props.trackCurrent} of {props.trackTotal}: {props.titleCurrent}
|
||||
</div>
|
||||
<Progress value={props.progressValue} />
|
||||
|
||||
<DialogFooter>
|
||||
<Button disabled={props.cancelled} onClick={props.handleCancelUpload}>
|
||||
{props.cancelled ? `Stopping after current track...` : `Cancel Recording`}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogWindowContent>
|
||||
</DialogWindow>
|
||||
</DialogOverlay>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
import React from 'react';
|
||||
import { Button, WindowContent } from 'react95';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { pair } from '../../redux/actions';
|
||||
import { Dispatch } from '@reduxjs/toolkit';
|
||||
import { AboutDialog } from '../about-dialog';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
pairingMessage: {
|
||||
color: 'red',
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
windowContent: {
|
||||
flex: '1 1 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
export interface W95WelcomeProps {
|
||||
dispatch: Dispatch<any>;
|
||||
pairingFailed: boolean;
|
||||
pairingMessage: string;
|
||||
}
|
||||
|
||||
export const W95Welcome = (props: W95WelcomeProps) => {
|
||||
let { dispatch, pairingFailed, pairingMessage } = props;
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<>
|
||||
<WindowContent className={classes.windowContent}>
|
||||
<p style={{ paddingBottom: 8 }}>Press the button to connect to a NetMD device</p>
|
||||
<Button style={{ minWidth: 90 }} onClick={() => dispatch(pair())}>
|
||||
Connect
|
||||
</Button>
|
||||
<p style={{ visibility: pairingFailed ? 'visible' : 'hidden' }} className={classes.pairingMessage}>
|
||||
{pairingMessage}
|
||||
</p>
|
||||
</WindowContent>
|
||||
<AboutDialog />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -9,3 +9,16 @@
|
|||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'ms_sans_serif';
|
||||
src: url('./ms_sans_serif.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'ms_sans_serif';
|
||||
src: url('./ms_sans_serif_bold.woff2') format('woff2');
|
||||
font-weight: bold;
|
||||
font-style: normal
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 193 B |
After Width: | Height: | Size: 194 B |
After Width: | Height: | Size: 683 B |
After Width: | Height: | Size: 161 B |
After Width: | Height: | Size: 279 B |
After Width: | Height: | Size: 470 B |
After Width: | Height: | Size: 262 B |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 182 B |
After Width: | Height: | Size: 240 B |
After Width: | Height: | Size: 4.1 KiB |
|
@ -11,6 +11,7 @@ export interface AppState {
|
|||
pairingMessage: string;
|
||||
browserSupported: boolean;
|
||||
darkMode: boolean;
|
||||
vintageMode: boolean;
|
||||
aboutDialogVisible: boolean;
|
||||
}
|
||||
|
||||
|
@ -21,6 +22,7 @@ const initialState: AppState = {
|
|||
pairingMessage: ``,
|
||||
browserSupported: true,
|
||||
darkMode: loadPreference('darkMode', false),
|
||||
vintageMode: loadPreference('vintageMode', false),
|
||||
aboutDialogVisible: false,
|
||||
};
|
||||
|
||||
|
@ -47,6 +49,10 @@ export const slice = createSlice({
|
|||
state.darkMode = action.payload;
|
||||
savePreference('darkMode', state.darkMode);
|
||||
},
|
||||
setVintageMode: (state, action: PayloadAction<boolean>) => {
|
||||
state.vintageMode = action.payload;
|
||||
savePreference('vintageMode', state.vintageMode);
|
||||
},
|
||||
showAboutDialog: (state, action: PayloadAction<boolean>) => {
|
||||
state.aboutDialogVisible = action.payload;
|
||||
},
|
||||
|
|
|
@ -5,3 +5,7 @@ declare module '*.svg' {
|
|||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
declare module 'react95';
|
||||
declare module 'react95/dist/themes/original';
|
||||
declare module 'react95/dist/fonts/ms_sans_serif.woff2';
|
||||
declare module 'react95/dist/fonts/ms_sans_serif_bold.woff2';
|
||||
|
|