Add react95 theme

This commit is contained in:
Stefano Brilli 2021-02-03 16:56:00 +01:00
parent a5ac3b2dc7
commit ad6427cb46
43 changed files with 1445 additions and 11 deletions

113
package-lock.json generated
View File

@ -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",

View File

@ -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"
},

View File

@ -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}

View File

@ -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>

View File

@ -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}>

View File

@ -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

View File

@ -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}

View File

@ -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}>

View File

@ -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}

View File

@ -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>

View File

@ -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}>

View File

@ -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}

View File

@ -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}>

View File

@ -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>
);
};

View File

@ -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">
&nbsp;
{' (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>
);
};

View File

@ -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;
}
`;

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
&nbsp;
<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 />
</>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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 />
</>
);
};

View File

@ -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
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

BIN
src/images/win95/delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

BIN
src/images/win95/device.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

BIN
src/images/win95/info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

BIN
src/images/win95/mic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
src/images/win95/move.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

BIN
src/images/win95/rename.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

BIN
src/images/win95/win95.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -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;
},

4
src/types.d.ts vendored
View File

@ -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';