├── .env ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── apple-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── ms-icon-70x70.png ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── android-icon-144x144.png ├── android-icon-192x192.png ├── apple-icon-precomposed.png ├── manifest.json ├── dropbox │ └── index.html ├── index.html.old └── index.html ├── src ├── theme.js ├── components │ ├── common │ │ ├── ToolbarVerticalDivider.js │ │ ├── editor │ │ │ ├── EditorButton.js │ │ │ ├── EditorTabPanel.js │ │ │ ├── EditorTextField.js │ │ │ ├── EditorImage.js │ │ │ ├── EditorFileChooser.js │ │ │ ├── EditorSwitch.js │ │ │ ├── ThumbnailTab.js │ │ │ ├── EditorSelect.js │ │ │ ├── EditorMultiUrlField.js │ │ │ ├── EditorValidator.js │ │ │ ├── BackgroundTab.js │ │ │ ├── GeneralTab.js │ │ │ ├── EditorUrlField.js │ │ │ └── Editor.js │ │ ├── CommonTooltip.js │ │ ├── ImageLabel.js │ │ ├── ContentLabel.js │ │ └── CommonImage.js │ ├── SelectCategory.js │ ├── CategoriesTableMoreMenu.js │ ├── CloudMenu.js │ ├── cloud │ │ └── generate-manifest │ │ │ ├── GenerateManifestWrapper.js │ │ │ ├── TreeComponent.js │ │ │ ├── CustomTreeItem.js │ │ │ ├── CloudStorage.js │ │ │ ├── SelectCloudFolderDialog.js │ │ │ ├── TreeModel.js │ │ │ └── GenerateManifestDialog.js │ ├── Screens.js │ ├── item-editor │ │ ├── dos │ │ │ └── MouseSpeed.js │ │ ├── VolumeAdjust.js │ │ ├── ZoolLevel.js │ │ ├── SelectPlayerOrder.js │ │ ├── SelectType.js │ │ ├── quake │ │ │ └── WadSelector.js │ │ ├── a5200 │ │ │ ├── A5200DescriptionsTab.js │ │ │ └── A5200MappingsTab.js │ │ ├── c64 │ │ │ ├── C64MappingsTab.js │ │ │ └── C64MappingOptions.js │ │ ├── coleco │ │ │ ├── ColecoDescriptionsTab.js │ │ │ └── ColecoMappingsTab.js │ │ ├── gb │ │ │ ├── SelectPalette.js │ │ │ └── SelectPalette.css │ │ └── a2600 │ │ │ └── A2600ControllersTab.js │ ├── SelectedFeed.js │ ├── ItemsTab.js │ ├── ToolsMenu.js │ ├── load-dialog │ │ ├── LoadFeedDialog.js │ │ └── FeedsTable.js │ ├── ConfirmDialog.js │ ├── BusyScreen.js │ ├── FeedTabs.js │ ├── MainAppBar.js │ ├── DisplayMessage.js │ ├── NewMenu.js │ ├── DownloadFileDialog.js │ ├── CreateFromUrlDialog.js │ ├── ImportDialog.js │ ├── CategoryEditor.js │ ├── ItemsTableMoreMenu.js │ ├── ExportDialog.js │ ├── CopyLinkDialog.js │ └── tools │ │ └── repackage │ │ └── Repackage.js ├── index.js ├── Dropbox.js ├── Drop.js ├── App.css ├── Util.js ├── Prefs.js ├── Global.js └── App.js ├── config-overrides.js ├── .gitignore ├── package.json └── README.md /.env: -------------------------------------------------------------------------------- 1 | PORT=3200 2 | #HOST=192.168.1.161 3 | HOST=0.0.0.0 4 | #HTTPS=true -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/apple-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/android-icon-36x36.png -------------------------------------------------------------------------------- /public/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/android-icon-48x48.png -------------------------------------------------------------------------------- /public/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/android-icon-72x72.png -------------------------------------------------------------------------------- /public/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/android-icon-96x96.png -------------------------------------------------------------------------------- /public/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/android-icon-144x144.png -------------------------------------------------------------------------------- /public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/android-icon-192x192.png -------------------------------------------------------------------------------- /public/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webrcade/webrcade-editor/HEAD/public/apple-icon-precomposed.png -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@mui/material/styles'; 2 | 3 | const theme = createTheme({ 4 | palette: { 5 | mode: "dark" 6 | } 7 | }); 8 | 9 | export default theme; -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | module.exports = function override(config, env) { 2 | config.module.rules.push({ 3 | test: /\.js$/, 4 | use: { loader: require.resolve('@open-wc/webpack-import-meta-loader') } 5 | }); 6 | return config; 7 | } -------------------------------------------------------------------------------- /src/components/common/ToolbarVerticalDivider.js: -------------------------------------------------------------------------------- 1 | 2 | import Divider from '@mui/material/Divider'; 3 | 4 | export default function ToolbarVerticalDivider() { 5 | return ( 6 | 7 | ); 8 | } -------------------------------------------------------------------------------- /src/components/common/editor/EditorButton.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Button from '@mui/material/Button'; 3 | 4 | export default function EditorButton(props) { 5 | const { label, ...other } = props; 6 | 7 | return ( 8 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/common/editor/EditorTabPanel.js: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | 3 | export default function EditorTabPanel(props) { 4 | const { children, value, index } = props; 5 | return ( 6 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { ThemeProvider } from '@mui/material/styles'; 4 | 5 | import App from './App'; 6 | import theme from './theme'; 7 | import './App.css'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | -------------------------------------------------------------------------------- /src/components/common/CommonTooltip.js: -------------------------------------------------------------------------------- 1 | import Tooltip from '@mui/material/Tooltip'; 2 | 3 | export default function CommonTooltip(props) { 4 | const {title, children} = props; 5 | 6 | return ( 7 | 15 | {children} 16 | 17 | ); 18 | } -------------------------------------------------------------------------------- /src/Dropbox.js: -------------------------------------------------------------------------------- 1 | 2 | export function dropboxPicker(cb, multi=true) { 3 | const options = { 4 | linkType: "preview", 5 | multiselect: multi, 6 | folderselect: false, 7 | success: function (files) { 8 | const res = []; 9 | const names = []; 10 | for (let i = 0; i < files.length; i++) { 11 | const f = files[i]; 12 | res.push(f.link); 13 | names.push(f.name); 14 | } 15 | if (res.length > 0) { 16 | cb(res, names); 17 | } 18 | }, 19 | sizeLimit: 600 * 1024 * 1024 // 65mb 20 | }; 21 | window.Dropbox.choose(options); 22 | } -------------------------------------------------------------------------------- /src/Drop.js: -------------------------------------------------------------------------------- 1 | import { isValidString } from '@webrcade/app-common' 2 | 3 | const dropHandler = (e, cb) => { 4 | let resolved = [false]; 5 | const len = e.dataTransfer.items.length; 6 | 7 | for (let i = 0; i < len; i++) { 8 | const item = e.dataTransfer.items[i]; 9 | 10 | if (item.kind === 'string' && 11 | (item.type.match('^text/uri-list') || item.type.match('^text/plain'))) { 12 | item.getAsString((text) => { 13 | if (resolved[0]) return; 14 | if (isValidString(text)) { 15 | resolved[0] = true; 16 | cb(text) 17 | } 18 | }); 19 | } 20 | } 21 | } 22 | 23 | export { dropHandler } 24 | -------------------------------------------------------------------------------- /src/components/common/editor/EditorTextField.js: -------------------------------------------------------------------------------- 1 | import TextField from '@mui/material/TextField'; 2 | 3 | import { dropHandler } from '../../../Drop'; 4 | 5 | export default function EditorTextField(props) { 6 | const { 7 | sx, 8 | onDropText, 9 | ...other 10 | } = props; 11 | 12 | return ( 13 | { 15 | if (onDropText) { 16 | e.preventDefault(); 17 | dropHandler(e, (text) => { onDropText(text); }); 18 | } 19 | }} 20 | sx={{ 21 | m: 1.5, 22 | width: '35ch', 23 | maxWidth: '100%', 24 | ...sx 25 | }} 26 | {...other} 27 | /> 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* custom scrollbar */ 2 | ::-webkit-scrollbar { 3 | width: 22px; 4 | height: 22px; 5 | } 6 | 7 | ::-webkit-scrollbar-track { 8 | background-color: transparent; 9 | } 10 | 11 | ::-webkit-scrollbar-thumb { 12 | background-color: #d6dee1; 13 | border-radius: 20px; 14 | border: 6px solid transparent; 15 | background-clip: content-box; 16 | } 17 | 18 | ::-webkit-scrollbar-thumb:hover { 19 | background-color: #a8bbbf; 20 | } 21 | 22 | ::-webkit-scrollbar-corner { 23 | background-color: transparent; 24 | } 25 | 26 | .webrcade-app { 27 | position: fixed; 28 | left: 0; 29 | right: 0; 30 | bottom: 0; 31 | top: 0; 32 | background-color: black; 33 | z-index: 5000 34 | } 35 | -------------------------------------------------------------------------------- /src/components/common/editor/EditorImage.js: -------------------------------------------------------------------------------- 1 | import CommonImage from '../CommonImage' 2 | 3 | export default function EditorImage(props) { 4 | const { 5 | sx, 6 | src, 7 | defaultSrc, 8 | requiredSize, 9 | errorCallback, 10 | onDropText, 11 | ...other 12 | } = props; 13 | return ( 14 | 29 |   30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/common/editor/EditorFileChooser.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import Button from '@mui/material/Button'; 4 | 5 | export default function EditorFileChooser(props) { 6 | const fileInput = React.useRef(); 7 | const { label, sx, onChange, ...other } = props; 8 | 9 | return ( 10 | 17 | 23 | 29 | 30 | ); 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/components/common/ImageLabel.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Box from '@mui/material/Box'; 4 | import Stack from '@mui/material/Stack'; 5 | 6 | import { Global } from '../../Global'; 7 | import CommonImage from './CommonImage'; 8 | 9 | export default function ImageLabel(props) { 10 | const { label, imageSrc, defaultImageSrc } = props; 11 | 12 | return ( 13 | 14 | 20 | {label} 25 | 26 | ); 27 | } -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "icons": [ 3 | { 4 | "src": "android-icon-36x36.png", 5 | "sizes": "36x36", 6 | "type": "image\/png", 7 | "density": "0.75" 8 | }, 9 | { 10 | "src": "android-icon-48x48.png", 11 | "sizes": "48x48", 12 | "type": "image\/png", 13 | "density": "1.0" 14 | }, 15 | { 16 | "src": "android-icon-72x72.png", 17 | "sizes": "72x72", 18 | "type": "image\/png", 19 | "density": "1.5" 20 | }, 21 | { 22 | "src": "android-icon-96x96.png", 23 | "sizes": "96x96", 24 | "type": "image\/png", 25 | "density": "2.0" 26 | }, 27 | { 28 | "src": "android-icon-144x144.png", 29 | "sizes": "144x144", 30 | "type": "image\/png", 31 | "density": "3.0" 32 | }, 33 | { 34 | "src": "android-icon-192x192.png", 35 | "sizes": "192x192", 36 | "type": "image\/png", 37 | "density": "4.0" 38 | } 39 | ], 40 | "start_url": ".", 41 | "display": "fullscreen", 42 | "theme_color": "#000000", 43 | "background_color": "#000000" 44 | } -------------------------------------------------------------------------------- /src/components/common/ContentLabel.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Typography from '@mui/material/Typography'; 3 | 4 | import Box from '@mui/material/Box'; 5 | import Stack from '@mui/material/Stack'; 6 | import { useTheme } from '@mui/material/styles'; 7 | 8 | export default function ContentLabel(props) { 9 | const { label, content, onClick } = props; 10 | const theme = useTheme(); 11 | 12 | const sx = {} 13 | if (onClick) { 14 | sx.cursor = 'pointer'; 15 | } 16 | 17 | return ( 18 | 24 | {content} 25 | 31 | 36 | {label} 37 | 38 | 39 | 40 | ); 41 | } -------------------------------------------------------------------------------- /src/components/common/editor/EditorSwitch.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import FormControlLabel from '@mui/material/FormControlLabel'; 4 | import Switch from '@mui/material/Switch'; 5 | 6 | import CommonTooltip from '../CommonTooltip'; 7 | 8 | export default function EditorSwitch(props) { 9 | const { label, checked, sx, onChange, tooltip, ...other } = props; 10 | 11 | const switchControl = ( 12 | 17 | } label={label} /> 18 | ); 19 | 20 | 21 | return ( 22 | 29 | {tooltip !== undefined && tooltip.length > 0 ? ( 30 | 33 | {switchControl} 34 | 35 | ) : ( 36 | <> 37 | {switchControl} 38 | 39 | )} 40 | 41 | ); 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/components/SelectCategory.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import InputLabel from '@mui/material/InputLabel'; 3 | import MenuItem from '@mui/material/MenuItem'; 4 | import FormControl from '@mui/material/FormControl'; 5 | import Select from '@mui/material/Select'; 6 | 7 | export default function SelectCategory(props) { 8 | const { feed, category, setCategory } = props; 9 | 10 | const handleChange = (event) => { 11 | setCategory(event.target.value); 12 | }; 13 | 14 | return ( 15 |
16 | 17 | Category 18 | 32 | 33 |
34 | ); 35 | } -------------------------------------------------------------------------------- /src/components/CategoriesTableMoreMenu.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import FindInPageIcon from '@mui/icons-material/FindInPage'; 3 | import Menu from '@mui/material/Menu'; 4 | import MenuItem from '@mui/material/MenuItem'; 5 | import ListItemIcon from '@mui/material/ListItemIcon'; 6 | 7 | import * as UrlProcessor from '../UrlProcessor'; 8 | 9 | export default function CategoriesTableMoreMenu(props) { 10 | const { 11 | anchorEl, 12 | setAnchorEl, 13 | selected 14 | } = props; 15 | const open = Boolean(anchorEl); 16 | const handleClose = () => { 17 | setAnchorEl(null); 18 | }; 19 | 20 | return ( 21 | 27 | { 30 | handleClose(); 31 | UrlProcessor.analyzeCategories(selected); 32 | }}> 33 | 34 | 35 | 36 | Analyze 37 | 38 | 39 | ); 40 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrcade-feed-editor", 3 | "homepage": ".", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@emotion/react": "^11.5.0", 8 | "@emotion/styled": "^11.3.0", 9 | "@mui/icons-material": "^5.2.0", 10 | "@mui/lab": "^5.0.0-alpha.143", 11 | "@mui/material": "^5.2.2", 12 | "@testing-library/jest-dom": "^5.14.1", 13 | "@testing-library/react": "^11.2.7", 14 | "@testing-library/user-event": "^12.8.3", 15 | "autosuggest-highlight": "^3.3.4", 16 | "deepmerge": "^4.2.2", 17 | "react": "^17.0.2", 18 | "react-dom": "^17.0.2", 19 | "react-scripts": "4.0.3", 20 | "web-vitals": "^1.1.2" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/CloudMenu.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import ListItemIcon from '@mui/material/ListItemIcon'; 3 | import Menu from '@mui/material/Menu'; 4 | import MenuItem from '@mui/material/MenuItem'; 5 | // import ListRoundedIcon from '@mui/icons-material/ListRounded'; 6 | import SimCardDownloadRoundedIcon from '@mui/icons-material/SimCardDownloadRounded'; 7 | 8 | import { openManifestDialog } from './cloud/generate-manifest/GenerateManifestDialog'; 9 | 10 | 11 | export default function CloudMenu(props) { 12 | const { anchorEl, setAnchorEl, setOpen } = props; 13 | const isOpen = Boolean(anchorEl); 14 | 15 | const handleClose = () => { 16 | setAnchorEl(null); 17 | }; 18 | 19 | return ( 20 | 30 | { 31 | openManifestDialog(); 32 | setOpen(false); 33 | handleClose(); 34 | }}> 35 | 36 | 37 | 38 | Generate Package
Manifest File... 39 |
40 |
41 | ); 42 | } -------------------------------------------------------------------------------- /src/components/cloud/generate-manifest/GenerateManifestWrapper.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOG 3 | } from '@webrcade/app-common' 4 | 5 | import { Global } from '../../../Global'; 6 | 7 | export default class GenerateManifestWrapper { 8 | async generate(doGenerate, onComplete, showStatus) { 9 | if (showStatus) Global.openBusyScreen(true, "Generating manifest file..."); 10 | try { 11 | const manifestUrl = await doGenerate(); 12 | Global.displayMessage("Successfully created package manifest file.", "success"); 13 | onComplete(); 14 | setTimeout(() => { 15 | Global.openCopyLinkDialog( 16 | true, 17 | manifestUrl, 18 | "Package Manifest File URL", 19 | "Successfully copied the package manifest file URL to the clipboard.", 20 | true 21 | ) 22 | 23 | }, 0); 24 | } catch (e) { 25 | LOG.error(e); 26 | 27 | // Check for token scope error 28 | if (e?.error?.error?.required_scope) { 29 | Global.displayMessage("This operation requires an updated Dropbox token. Please use settings to unlink and relink to Dropbox.", "error"); 30 | } else { 31 | Global.displayMessage("An error occurred while attempting to generate the manifest.", "error"); 32 | } 33 | } finally { 34 | if (showStatus) Global.openBusyScreen(false); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Screens.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import CategoryEditor from './CategoryEditor'; 4 | import ConfirmDialog from './ConfirmDialog'; 5 | import CopyLinkDialog from './CopyLinkDialog'; 6 | import CreateFromUrlDialog from './CreateFromUrlDialog'; 7 | import DisplayMessage from './DisplayMessage'; 8 | import ExportDialog from './ExportDialog'; 9 | import FeedEditor from './FeedEditor'; 10 | import ImportDialog from './ImportDialog'; 11 | import ItemEditor from './item-editor/ItemEditor'; 12 | import LoadFeedDialog from './load-dialog/LoadFeedDialog'; 13 | import SettingsEditor from './SettingsEditor'; 14 | import { GenerateManifestDialog } from './cloud/generate-manifest/GenerateManifestDialog'; 15 | import { RepackageDialog } from './tools/repackage/RepackageDialog'; 16 | import { SelectCloudFolderDialog } from './cloud/generate-manifest/SelectCloudFolderDialog'; 17 | import { DownloadFileDialog } from './DownloadFileDialog'; 18 | 19 | export default function Screens() { 20 | 21 | return ( 22 | <> 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | } -------------------------------------------------------------------------------- /src/components/item-editor/dos/MouseSpeed.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Box from '@mui/material/Box' 4 | import Slider from '@mui/material/Slider'; 5 | import Stack from '@mui/material/Stack'; 6 | import Typography from '@mui/material/Typography'; 7 | 8 | const UNSET_TEMP = -9999; 9 | 10 | export default function MouseSpeed(props) { 11 | const { value, onChange, onChangeCommitted } = props; 12 | // Allows for smoother updated prior to change being committed 13 | const [tempValue, setTempValue] = React.useState(UNSET_TEMP); 14 | 15 | return ( 16 | <> 17 | 18 | Mouse speed 19 | 20 | 25 | {/* */} 26 | {/* Slower */} 27 | 28 | {onChange(e, val); setTempValue(val)}} 34 | onChangeCommitted={(e, val) => {onChangeCommitted(e, val); setTempValue(UNSET_TEMP)}} 35 | /> 36 | 37 | {/* Faster */} 38 | {/* */} 39 | 40 | 41 | ); 42 | } -------------------------------------------------------------------------------- /src/Util.js: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from 'react'; 2 | import * as WrcCommon from '@webrcade/app-common' 3 | 4 | export function cloneObject(obj) { 5 | return WrcCommon.cloneObject(obj); 6 | } 7 | 8 | export function asString(str) { 9 | return str ? str : ''; 10 | } 11 | 12 | export function isEmptyString(str) { 13 | return str.trim().length === 0; 14 | } 15 | 16 | export function asBoolean(obj) { 17 | if (obj === undefined) { 18 | return false; 19 | } 20 | return obj === true; 21 | } 22 | 23 | export function removeEmptyItems(arr) { 24 | const ret = []; 25 | for (let i = 0; i < arr.length; i++) { 26 | const item = arr[i].trim(); 27 | if (item.length > 0) { 28 | ret.push(item); 29 | } 30 | } 31 | return ret; 32 | } 33 | 34 | export function splitLines(str) { 35 | return str.split(/\r?\n|\r|\n/g); 36 | } 37 | 38 | export function useForceUpdate() { 39 | const [, setValue] = useState(0); // integer state 40 | return () => setValue(value => value + 1); // update the state to force render 41 | } 42 | 43 | export function usePrevious(value) { 44 | // The ref object is a generic container whose current property is mutable ... 45 | // ... and can hold any value, similar to an instance property on a class 46 | const ref = useRef(); 47 | // Store current value in ref 48 | useEffect(() => { 49 | ref.current = value; 50 | }, [value]); // Only re-run if value changes 51 | // Return previous value (happens before update in useEffect above) 52 | return ref.current; 53 | } -------------------------------------------------------------------------------- /src/components/SelectedFeed.js: -------------------------------------------------------------------------------- 1 | import Box from '@mui/material/Box'; 2 | import EditIcon from '@mui/icons-material/Edit'; 3 | import IconButton from '@mui/material/IconButton'; 4 | import Paper from '@mui/material/Paper'; 5 | import Toolbar from '@mui/material/Toolbar'; 6 | import Tooltip from '@mui/material/Tooltip'; 7 | import Typography from '@mui/material/Typography'; 8 | 9 | import * as WrcCommon from '@webrcade/app-common' 10 | 11 | import { Global } from '../Global'; 12 | import CommonImage from './common/CommonImage' 13 | 14 | function SelectedFeed(props) { 15 | const { feed } = props; 16 | return ( 17 | 18 | 19 | 20 | 26 | 27 | {feed.title} 28 | 29 | 30 | { 32 | e.stopPropagation(); 33 | Global.editFeed(Global.getFeed()); 34 | }} 35 | > 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default SelectedFeed; 46 | -------------------------------------------------------------------------------- /src/components/item-editor/VolumeAdjust.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Box from '@mui/material/Box' 4 | import Slider from '@mui/material/Slider'; 5 | import Stack from '@mui/material/Stack'; 6 | import Typography from '@mui/material/Typography'; 7 | import VolumeDown from '@mui/icons-material/VolumeDown'; 8 | import VolumeUp from '@mui/icons-material/VolumeUp'; 9 | 10 | const UNSET_TEMP = -9999; 11 | 12 | export default function SelectPlayerOrder(props) { 13 | const { value, onChange, onChangeCommitted } = props; 14 | // Allows for smoother updated prior to change being committed 15 | const [tempValue, setTempValue] = React.useState(UNSET_TEMP); 16 | 17 | return ( 18 | <> 19 | 20 | Volume adjustment 21 | 22 | 27 | 28 | 29 | {onChange(e, val); setTempValue(val)}} 35 | onChangeCommitted={(e, val) => {onChangeCommitted(e, val); setTempValue(UNSET_TEMP)}} 36 | /> 37 | 38 | 39 | 40 | 41 | ); 42 | } -------------------------------------------------------------------------------- /src/components/common/editor/ThumbnailTab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Global } from '../../../Global'; 4 | import * as Util from '../../../Util'; 5 | import EditorImage from '../../common/editor/EditorImage'; 6 | import EditorTabPanel from '../../common/editor/EditorTabPanel'; 7 | import EditorUrlField from '../../common/editor/EditorUrlField'; 8 | 9 | export default function ThumbnailTab(props) { 10 | const { 11 | tabValue, 12 | tabIndex, 13 | thumbSrc, 14 | defaultThumbSrc, 15 | setObject, 16 | object 17 | } = props; 18 | const [thumbnailError, setThumbnailError] = React.useState(null); 19 | 20 | return ( 21 | 22 |
23 | { setObject({ ...object, thumbnail: text }) }} 31 | onChange={(e) => { setObject({ ...object, thumbnail: e.target.value }) }} 32 | /> 33 |
34 |
35 | { setObject({ ...object, thumbnail: text }) }} 41 | /> 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/common/editor/EditorSelect.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import MenuItem from '@mui/material/MenuItem'; 4 | import FormControl from '@mui/material/FormControl'; 5 | import InputLabel from '@mui/material/InputLabel'; 6 | import Select from '@mui/material/Select'; 7 | 8 | import CommonTooltip from '../CommonTooltip'; 9 | 10 | export default function EditorSelect(props) { 11 | const { 12 | label, 13 | menuItems, 14 | value, 15 | sx, 16 | onChange, 17 | tooltip, 18 | children, 19 | ...other 20 | } = props; 21 | 22 | const selectControl = ( 23 | 24 | {label} 25 | 40 | 41 | ); 42 | 43 | return ( 44 | 50 | {tooltip !== undefined && tooltip.length > 0 ? ( 51 | 54 | {selectControl} 55 | 56 | ) : ( 57 | <> 58 | {selectControl} 59 | 60 | )} 61 | 62 | ); 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/components/ItemsTab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { GlobalHolder } from '../Global'; 4 | import * as Feed from '../Feed'; 5 | import * as Util from '../Util'; 6 | import ItemsTable from './ItemsTable'; 7 | import Prefs from '../Prefs'; 8 | import SelectCategory from './SelectCategory'; 9 | 10 | const PREF_CURRENT_CAT = "currentCategory"; 11 | 12 | export default function ItemsTab(props) { 13 | const { feed } = props; 14 | const [category, setCategory] = 15 | React.useState(Prefs.getPreference(PREF_CURRENT_CAT, "")); 16 | 17 | const prevFeed = Util.usePrevious(feed); 18 | const prevCategory = Util.usePrevious(category); 19 | 20 | GlobalHolder.setFeedCategoryId = setCategory; 21 | GlobalHolder.getFeedCategoryId = () => { 22 | return category; 23 | } 24 | 25 | // Reset page if key changes 26 | React.useEffect(() => { 27 | if (prevFeed !== feed && !Feed.getCategory(feed, category)) { 28 | setCategory( 29 | feed.categories && feed.categories.length > 0 ? 30 | feed.categories[0].id : ""); 31 | } 32 | }, [feed, prevFeed, category, setCategory]); 33 | 34 | // Update prefs if category changes 35 | React.useEffect(() => { 36 | if (prevCategory !== category) { 37 | Prefs.setPreference(PREF_CURRENT_CAT, category); 38 | Prefs.save(); 39 | } 40 | }, [category, prevCategory]); 41 | 42 | return ( 43 | <> 44 | 48 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/common/editor/EditorMultiUrlField.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import EditorUrlField from './EditorUrlField'; 3 | 4 | 5 | import { 6 | uuidv4, 7 | } from '@webrcade/app-common' 8 | 9 | export default function EditorMultiUrlField(props) { 10 | const [uuid, setUuid] = React.useState(uuidv4()); 11 | 12 | const { 13 | sx, 14 | rows, 15 | onDropText, 16 | onChange, 17 | ...other 18 | } = props; 19 | 20 | const updateUrls = (urls) => { 21 | if (urls && !Array.isArray(urls) && urls.toLowerCase().indexOf("drive.google.com") !== -1) { 22 | const driveUrls = urls.split(","); 23 | if (driveUrls.length > 1) { 24 | urls = ""; 25 | for (let i = 0; i < driveUrls.length; i++) { 26 | if (urls.length > 0) urls += "\n"; 27 | urls += driveUrls[i].trim(); 28 | } 29 | } 30 | } 31 | return urls; 32 | } 33 | 34 | return ( 35 | { 38 | if (onDropText) { 39 | onDropText(updateUrls(text)) 40 | setUuid(uuidv4()); 41 | } 42 | }} 43 | onChange={(e) => {if (onChange) { 44 | const urls = updateUrls(e.target.value); 45 | onChange({ 46 | // TODO: Hack, find a better way... 47 | target: { 48 | value: urls 49 | } 50 | }) 51 | }}} 52 | multiselect={true} 53 | multiline 54 | rows={rows ? rows : 5} 55 | sx={{ 56 | width: '50ch', 57 | ...sx 58 | }} 59 | inputProps={{ ref: input => { if (input) { input.style['white-space'] = 'nowrap' } } }} 60 | {...other} 61 | /> 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /public/dropbox/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | webЯcade 10 | 11 | 12 | 13 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/components/item-editor/ZoolLevel.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Box from '@mui/material/Box' 4 | import Slider from '@mui/material/Slider'; 5 | import Stack from '@mui/material/Stack'; 6 | import Typography from '@mui/material/Typography'; 7 | // import VolumeDown from '@mui/icons-material/VolumeDown'; 8 | // import VolumeUp from '@mui/icons-material/VolumeUp'; 9 | 10 | const UNSET_TEMP = -9999; 11 | 12 | export default function ZoomLevel(props) { 13 | const { value, onChange, onChangeCommitted } = props; 14 | // Allows for smoother updated prior to change being committed 15 | const [tempValue, setTempValue] = React.useState(UNSET_TEMP); 16 | 17 | return ( 18 | <> 19 | 20 | Zoom Level 21 | 22 | 27 | {/* */} 28 | 29 | {onChange(e, val); setTempValue(val)}} 41 | onChangeCommitted={(e, val) => {onChangeCommitted(e, val); setTempValue(UNSET_TEMP)}} 42 | /> 43 | 44 | {/* */} 45 | 46 | 47 | ); 48 | } -------------------------------------------------------------------------------- /public/index.html.old: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/ToolsMenu.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Divider from '@mui/material/Divider'; 3 | import ListItemIcon from '@mui/material/ListItemIcon'; 4 | import Menu from '@mui/material/Menu'; 5 | import MenuItem from '@mui/material/MenuItem'; 6 | import FolderZipRoundedIcon from '@mui/icons-material/FolderZipRounded'; 7 | import SimCardDownloadRoundedIcon from '@mui/icons-material/SimCardDownloadRounded'; 8 | 9 | import * as WrcCommon from '@webrcade/app-common' 10 | 11 | import { openManifestDialog } from './cloud/generate-manifest/GenerateManifestDialog'; 12 | import { openRepackageDialog } from './tools/repackage/RepackageDialog'; 13 | 14 | export default function ToolsMenu(props) { 15 | const { anchorEl, setAnchorEl, setOpen } = props; 16 | const isOpen = Boolean(anchorEl); 17 | 18 | const handleClose = () => { 19 | setAnchorEl(null); 20 | }; 21 | 22 | return ( 23 | 33 | { 34 | openRepackageDialog(); 35 | setOpen(false); 36 | handleClose(); 37 | }}> 38 | 39 | 40 | 41 | Repackage Archive... 42 | 43 | {WrcCommon.settings.isCloudStorageEnabled() && 44 | 45 | } 46 | {WrcCommon.settings.isCloudStorageEnabled() && 47 | { 48 | openManifestDialog(); 49 | setOpen(false); 50 | handleClose(); 51 | }}> 52 | 53 | 54 | 55 | Generate Package Manifest File... 56 | 57 | } 58 | 59 | ); 60 | } -------------------------------------------------------------------------------- /src/components/load-dialog/LoadFeedDialog.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Tab from '@mui/material/Tab'; 3 | 4 | import * as WrcCommon from '@webrcade/app-common' 5 | 6 | import { GlobalHolder } from '../../Global'; 7 | import Editor from '../common/editor/Editor'; 8 | import EditorTabPanel from '../common/editor/EditorTabPanel'; 9 | import FeedsTable from './FeedsTable'; 10 | 11 | const setTabValue = () => { }; 12 | 13 | export default function LoadFeedDialog(props) { 14 | const [isOpen, setOpen] = React.useState(false); 15 | const [feeds, setFeeds] = React.useState(null); 16 | 17 | GlobalHolder.setLoadFeedDialogOpen = setOpen; 18 | 19 | const loadTab = 0; 20 | 21 | const onOk = () => { 22 | return true; 23 | } 24 | 25 | return ( 26 | { 40 | const feeds = await WrcCommon.loadFeeds(0); 41 | setFeeds(feeds); 42 | }} 43 | onOk={onOk} 44 | onSubmit={() => { 45 | if (onOk()) { 46 | setOpen(false); 47 | } 48 | }} 49 | tabs={[ 50 | , 51 | ]} 52 | tabPanels={( 53 | <> 54 | 55 | { 59 | const feeds = await WrcCommon.loadFeeds(0); 60 | setFeeds(feeds); 61 | }} 62 | /> 63 | 64 | 65 | )} 66 | /> 67 | ); 68 | } -------------------------------------------------------------------------------- /src/components/ConfirmDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import Dialog from '@mui/material/Dialog'; 4 | import DialogActions from '@mui/material/DialogActions'; 5 | import DialogContent from '@mui/material/DialogContent'; 6 | import DialogTitle from '@mui/material/DialogTitle'; 7 | import useMediaQuery from '@mui/material/useMediaQuery'; 8 | import { useTheme } from '@mui/material/styles'; 9 | 10 | import { GlobalHolder } from '../Global'; 11 | import { enableDropHandler } from '../UrlProcessor'; 12 | import { usePrevious } from '../Util'; 13 | 14 | const ConfirmDialog = (props) => { 15 | const [isOpen, setOpen] = React.useState(false); 16 | const prevOpen = usePrevious(isOpen); 17 | const [confirmProps, setConfirmProps] = React.useState({ 18 | title: "Title", 19 | message: "Message", 20 | callback: () => {} 21 | }); 22 | const theme = useTheme(); 23 | const fullScreen = useMediaQuery(theme.breakpoints.down('md')); 24 | 25 | // Enable/disable the drop handler 26 | if (prevOpen && !isOpen) { 27 | enableDropHandler(true); 28 | } else if (!prevOpen && isOpen) { 29 | enableDropHandler(false); 30 | } 31 | 32 | GlobalHolder.setConfirmDialogOpen = setOpen; 33 | GlobalHolder.setConfirmDialogProps = setConfirmProps; 34 | 35 | return ( 36 | setOpen(false)} 39 | fullScreen={fullScreen} 40 | > 41 | {confirmProps.title} 42 | {confirmProps.message} 43 | 44 | 52 | 57 | 58 | 59 | ); 60 | }; 61 | 62 | export default ConfirmDialog; -------------------------------------------------------------------------------- /src/components/cloud/generate-manifest/TreeComponent.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | // import useTheme from '@mui/material/styles/useTheme'; 3 | 4 | import { TreeView } from "@mui/x-tree-view/TreeView"; 5 | import { ExpandMore } from "@mui/icons-material"; 6 | import { ChevronRight } from "@mui/icons-material"; 7 | import FolderRoundedIcon from '@mui/icons-material/FolderRounded'; 8 | import { Box } from "@mui/material"; 9 | import * as Util from '../../../Util'; 10 | import { Global } from '../../../Global'; 11 | 12 | import { 13 | LOG 14 | } from '@webrcade/app-common' 15 | 16 | export default function TreeComponent({ model, setNodeSelected }) { 17 | const [expanded, setExpanded] = React.useState([]); 18 | const [selected, setSelected] = React.useState([]); 19 | const forceRefresh = Util.useForceUpdate(); 20 | // const theme = useTheme(); 21 | 22 | return ( 23 | 24 | } 26 | defaultExpandIcon={} 27 | defaultParentIcon={} 28 | defaultEndIcon={} 29 | expanded={expanded} 30 | selected={selected} 31 | sx={{ height: 240, flexGrow: 1, overflowY: 'auto' }} 32 | onNodeToggle={(event, nodeIds) => { 33 | setExpanded(nodeIds); 34 | }} 35 | onNodeSelect={async (event, nodeId) => { 36 | const node = model.getNode(nodeId); 37 | try { 38 | await model.addNodes(node, node.getPath(), () => { 39 | setExpanded([nodeId, ...expanded]); 40 | }, () => { forceRefresh() }); 41 | } catch (e) { 42 | LOG.error(e); 43 | Global.displayMessage("An error occurred while attempting to read the folder.", "error"); 44 | } 45 | setSelected([nodeId]); 46 | setNodeSelected(node); 47 | }} 48 | > 49 | {model.renderTree()} 50 | 51 | 52 | ); 53 | } -------------------------------------------------------------------------------- /src/components/BusyScreen.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Backdrop from '@mui/material/Backdrop'; 3 | import Modal from '@mui/material/Modal'; 4 | import Box from '@mui/material/Box'; 5 | import CircularProgress from '@mui/material/CircularProgress'; 6 | import Grid from '@mui/material/Grid'; 7 | import Typography from '@mui/material/Typography'; 8 | 9 | import { usePrevious } from '../Util'; 10 | import { enableDropHandler } from '../UrlProcessor'; 11 | import { GlobalHolder } from '../Global'; 12 | 13 | export default function BusyScreen(props) { 14 | const [open, setOpen] = React.useState(false); 15 | const [message, setMessage] = React.useState(false); 16 | const [disableAutoFocus, setDisableAutoFocus] = React.useState(false); 17 | const [disableDrop, setDisableDrop] = React.useState(true); 18 | const prevOpen = usePrevious(open); 19 | 20 | GlobalHolder.setBusyScreenOpen = setOpen; 21 | GlobalHolder.setBusyScreenMessage = setMessage; 22 | GlobalHolder.setBusyScreenDisableAutoFocus = setDisableAutoFocus; 23 | GlobalHolder.setBusyScreenDisableDrop = setDisableDrop; 24 | 25 | if (disableDrop) { 26 | // Enable/disable the drop handler 27 | if (prevOpen && !open) { 28 | enableDropHandler(true); 29 | //console.log('enable drop'); 30 | } else if (!prevOpen && open) { 31 | //console.log('disable drop'); 32 | enableDropHandler(false); 33 | } 34 | } 35 | 36 | return ( 37 | 38 | 44 | 45 | 48 | 49 | 50 | {message ? ( 51 | 52 | 53 | {message} 54 | 55 | 56 | ) : null} 57 | 58 | 59 | 60 | ); 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/components/cloud/generate-manifest/CustomTreeItem.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import clsx from 'clsx'; 3 | import Typography from '@mui/material/Typography'; 4 | import { TreeItem, useTreeItem } from '@mui/x-tree-view/TreeItem'; 5 | 6 | const CustomContent = React.forwardRef(function CustomContent(props, ref) { 7 | const { 8 | classes, 9 | className, 10 | label, 11 | nodeId, 12 | icon: iconProp, 13 | expansionIcon, 14 | displayIcon, 15 | } = props; 16 | 17 | const { 18 | disabled, 19 | expanded, 20 | selected, 21 | focused, 22 | handleExpansion, 23 | handleSelection, 24 | preventSelection, 25 | } = useTreeItem(nodeId); 26 | 27 | const icon = iconProp || expansionIcon || displayIcon; 28 | 29 | const handleMouseDown = (event) => { 30 | preventSelection(event); 31 | }; 32 | 33 | const handleExpansionClick = (event) => { 34 | handleExpansion(event); 35 | }; 36 | 37 | const handleSelectionClick = (event) => { 38 | handleSelection(event); 39 | }; 40 | 41 | return ( 42 | // eslint-disable-next-line jsx-a11y/no-static-element-interactions 43 |
53 | {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */} 54 |
55 | {icon} 56 |
57 | 62 | {label} 63 | 64 |
65 | ); 66 | }); 67 | 68 | const CustomTreeItem = React.forwardRef(function CustomTreeItem(props, ref) { 69 | return ; 70 | }); 71 | 72 | export default CustomTreeItem; -------------------------------------------------------------------------------- /src/components/common/editor/EditorValidator.js: -------------------------------------------------------------------------------- 1 | export default class EditorValidator { 2 | 3 | errors = [] 4 | callbacks = {} 5 | 6 | updateErrors(tab, prop, valid) { 7 | let found = false; 8 | for (let i = 0; i < this.errors.length; i++) { 9 | const e = this.errors[i]; 10 | if (e.prop === prop && e.tab === tab) { 11 | found = true; 12 | if (valid) { 13 | this.errors.splice(i, 1); 14 | } 15 | break; 16 | } 17 | } 18 | if (!valid && !found) { 19 | this.errors.push({ 20 | prop: prop, 21 | tab: tab 22 | }); 23 | } 24 | 25 | // console.log("### UPDATE VALIDATION"); 26 | // for (let i = 0; i < this.errors.length; i++) { 27 | // const e = this.errors[i]; 28 | // console.log(e); 29 | // } 30 | } 31 | 32 | checkMinLength(tab, prop, str, minLen = 1) { 33 | const valid = (str !== undefined && (str.trim().length >= minLen)); 34 | this.updateErrors(tab, prop, valid); 35 | return valid; 36 | } 37 | 38 | reset() { 39 | // console.log('## RESET'); 40 | this.errors = []; 41 | this.callbacks = {}; 42 | } 43 | 44 | getMinInvalidTab() { 45 | let min = -1; 46 | for (let i = 0; i < this.errors.length; i++) { 47 | const e = this.errors[i]; 48 | if (min === -1 || e.tab < min ) { 49 | min = e.tab; 50 | } 51 | } 52 | return min; 53 | } 54 | 55 | isTabValid(tab) { 56 | for (let i = 0; i < this.errors.length; i++) { 57 | const e = this.errors[i]; 58 | if (e.tab === tab) { 59 | return false; 60 | } 61 | } 62 | return true; 63 | } 64 | 65 | isValid(tab, prop) { 66 | for (let i = 0; i < this.errors.length; i++) { 67 | const e = this.errors[i]; 68 | if (e.tab === tab && e.prop === prop) { 69 | return false; 70 | } 71 | } 72 | return true; 73 | } 74 | 75 | addCallback(key, cb) { 76 | this.callbacks[key] = cb; 77 | } 78 | 79 | executeCallbacks() { 80 | for(const id in this.callbacks) { 81 | this.callbacks[id](); 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/components/FeedTabs.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import Tab from '@mui/material/Tab'; 4 | import Tabs from '@mui/material/Tabs'; 5 | 6 | import { Global } from '../Global'; 7 | import CategoriesTable from './CategoriesTable'; 8 | import ItemsTab from './ItemsTab'; 9 | import Prefs from '../Prefs'; 10 | import { usePrevious } from '../Util'; 11 | 12 | function TabPanel(props) { 13 | const { children, value, index } = props; 14 | 15 | return ( 16 | 21 | ); 22 | } 23 | 24 | const PREF_FEED_TAB = "feedsTab.tab"; 25 | 26 | function FeedTabs(props) { 27 | const { feed } = props; 28 | const [tabValue, setTabValue] = 29 | React.useState(Prefs.getIntPreference(PREF_FEED_TAB, 0)); 30 | const prevTabValue = usePrevious(tabValue); 31 | 32 | // Store prefs (if applicable) 33 | React.useEffect(() => { 34 | // See if tab has changed 35 | if (prevTabValue !== tabValue) { 36 | Prefs.setPreference(PREF_FEED_TAB, tabValue); 37 | Prefs.save(); 38 | } 39 | }, [prevTabValue, tabValue]); 40 | 41 | function tabProps(index) { 42 | return { id: `feed-tab-${index}` }; 43 | } 44 | 45 | const handleTabChange = (event, newValue) => { 46 | setTabValue(newValue); 47 | }; 48 | 49 | const showCategoryItems = (catId) => { 50 | Global.setFeedCategoryId(catId); 51 | setTabValue(1); 52 | }; 53 | 54 | return ( 55 | <> 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | } 74 | 75 | export default FeedTabs; -------------------------------------------------------------------------------- /src/components/MainAppBar.js: -------------------------------------------------------------------------------- 1 | import AppBar from '@mui/material/AppBar'; 2 | import Avatar from '@mui/material/Avatar'; 3 | import HelpCenterIcon from '@mui/icons-material/HelpCenter'; 4 | import IconButton from '@mui/material/IconButton'; 5 | import MenuIcon from '@mui/icons-material/Menu'; 6 | import SettingsIcon from '@mui/icons-material/Settings'; 7 | import Toolbar from '@mui/material/Toolbar'; 8 | import Tooltip from '@mui/material/Tooltip'; 9 | import Typography from '@mui/material/Typography'; 10 | import { styled } from '@mui/material/styles'; 11 | 12 | import * as WrcCommon from '@webrcade/app-common' 13 | import { Global } from '../Global'; 14 | 15 | function MainAppBar(props) { 16 | const StyledAppBar = styled(AppBar)(({ theme }) => ({ 17 | zIndex: theme.zIndex.drawer + 1 18 | })); 19 | 20 | return ( 21 | 22 | 23 | 29 | 30 | 31 | 36 | 37 | webЯcade Feed Editor 38 | 39 | 40 | { Global.openSettingsEditor(true); }} 44 | sx={{ ml: 2 }} 45 | > 46 | 47 | 48 | 49 | 50 | { window.open('https://docs.webrcade.com/editor/'); }} 54 | sx={{ ml: .5 }} 55 | > 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | 64 | export default MainAppBar; -------------------------------------------------------------------------------- /src/components/common/editor/BackgroundTab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import * as Util from '../../../Util'; 4 | import EditorImage from '../../common/editor/EditorImage'; 5 | import EditorSwitch from '../../common/editor/EditorSwitch'; 6 | import EditorTabPanel from '../../common/editor/EditorTabPanel'; 7 | import EditorUrlField from '../../common/editor/EditorUrlField'; 8 | 9 | export default function BackgroundTab(props) { 10 | const { 11 | tabValue, 12 | tabIndex, 13 | thumbSrc, 14 | defaultThumbSrc, 15 | object, 16 | setObject 17 | } = props; 18 | const [backgroundError, setBackgroundError] = React.useState(null); 19 | 20 | return ( 21 | 22 |
23 | { setObject({ ...object, background: text }) }} 31 | onChange={(e) => { setObject({ ...object, background: e.target.value }) }} 32 | /> 33 |
34 |
35 | { 39 | setObject({ ...object, backgroundPixelated: e.target.checked }) 40 | }} 41 | checked={Util.asBoolean(object.backgroundPixelated)} 42 | sx={{marginTop: .5}} 43 | 44 | /> 45 |
46 |
47 | { setObject({ ...object, background: text }) }} 52 | sx={{ 53 | objectFit: 'cover', 54 | height: '100%' 55 | }} 56 | /> 57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/components/DisplayMessage.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { usePrevious } from '../Util'; 3 | import MuiAlert from '@mui/material/Alert'; 4 | import Slide from '@mui/material/Slide'; 5 | import Snackbar from '@mui/material/Snackbar'; 6 | 7 | import { GlobalHolder } from '../Global'; 8 | 9 | function SlideTransition(props) { 10 | return ; 11 | } 12 | 13 | const Alert = React.forwardRef(function Alert(props, ref) { 14 | return ; 15 | }); 16 | 17 | /* severity: error, warning, info, success */ 18 | export default function DisplayMessage(props) { 19 | const [open, setOpen] = React.useState(false); 20 | const [message, setMessage] = React.useState(null); 21 | const [severity, setMessageSeverity] = React.useState(null); 22 | 23 | GlobalHolder.setMessage = setMessage; 24 | GlobalHolder.setMessageSeverity = setMessageSeverity; 25 | 26 | // Display and clear the queued message 27 | if (GlobalHolder.queuedMessage) { 28 | const queuedMessage = GlobalHolder.queuedMessage; 29 | const queuedMessageSeverity = GlobalHolder.queuedMessageSeverity; 30 | GlobalHolder.queuedMessage = null; 31 | GlobalHolder.queuedMessageSeverity = null; 32 | setTimeout(() => { 33 | GlobalHolder.setMessage(queuedMessage); 34 | GlobalHolder.setMessageSeverity(queuedMessageSeverity); 35 | }, 50); 36 | } 37 | 38 | const previousMessage = usePrevious(message); 39 | 40 | React.useEffect(() => { 41 | if ((previousMessage !== message) && message) { 42 | setOpen(true); 43 | } 44 | }, [previousMessage, message]); 45 | 46 | const handleClose = (event, reason) => { 47 | setMessage(null); 48 | setOpen(false); 49 | }; 50 | 51 | return ( 52 | 61 | 62 | {message} 63 | 64 | 65 | ); 66 | } -------------------------------------------------------------------------------- /src/components/common/editor/GeneralTab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import * as Util from '../../../Util'; 4 | import EditorTabPanel from '../../common/editor/EditorTabPanel'; 5 | import EditorTextField from '../../common/editor/EditorTextField'; 6 | 7 | export default function GeneralTab(props) { 8 | const { 9 | tabValue, 10 | tabIndex, 11 | object, 12 | setObject, 13 | validator, 14 | addValidateCallback, 15 | otherFields, 16 | nameField 17 | } = props; 18 | 19 | React.useEffect(() => { 20 | if (addValidateCallback) { 21 | addValidateCallback( 22 | "GeneralTab-" + tabIndex, 23 | () => { validator.checkMinLength(tabIndex, "title", object.title); } 24 | ); 25 | } 26 | }, [addValidateCallback, object, tabIndex, validator]); 27 | 28 | return ( 29 | 30 | {nameField ? nameField : ( 31 |
32 | { setObject({ ...object, title: text }) }} 36 | onChange={(e) => { setObject({ ...object, title: e.target.value }) }} 37 | value={Util.asString(object.title)} 38 | error={!validator.isValid(tabIndex, "title")} 39 | /> 40 |
) 41 | } 42 |
43 | { setObject({ ...object, longTitle: text }) }} 47 | onChange={(e) => { setObject({ ...object, longTitle: e.target.value }) }} 48 | value={Util.asString(object.longTitle)} 49 | /> 50 |
51 |
52 | { setObject({ ...object, description: text }) }} 58 | onChange={(e) => { setObject({ ...object, description: e.target.value }) }} 59 | value={Util.asString(object.description)} 60 | /> 61 |
62 | {otherFields} 63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/NewMenu.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import FileCopyIcon from '@mui/icons-material/FileCopy'; 3 | import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'; 4 | import ListItemIcon from '@mui/material/ListItemIcon'; 5 | import Menu from '@mui/material/Menu'; 6 | import MenuItem from '@mui/material/MenuItem'; 7 | 8 | import * as Feed from '../Feed'; 9 | import { Global } from '../Global'; 10 | 11 | export default function NewMenu(props) { 12 | const { anchorEl, setAnchorEl, setOpen } = props; 13 | const isOpen = Boolean(anchorEl); 14 | 15 | const handleClose = () => { 16 | setAnchorEl(null); 17 | }; 18 | 19 | return ( 20 | 30 | { 31 | const feed = Feed.exampleFeed(); 32 | Global.setFeed({ ...feed }); 33 | setOpen(false); 34 | handleClose(); 35 | }}> 36 | 37 | 38 | 39 | Clone example feed 40 | 41 | { 42 | Global.openBusyScreen(true, "Cloning feed..."); 43 | Feed.loadFeedFromUrl(Feed.getDefaultFeedUrl(), false) 44 | .then((feed) => { 45 | Global.setFeed(feed); 46 | }) 47 | .catch(msg => { 48 | Global.displayMessage(msg); 49 | }) 50 | .finally(() => { 51 | Global.openBusyScreen(false); 52 | setOpen(false); 53 | handleClose(); 54 | }); 55 | }}> 56 | 57 | 58 | 59 | Clone default feed 60 | 61 | { 62 | const feed = Feed.newFeed(); 63 | Global.setFeed({ ...feed }); 64 | setOpen(false); 65 | handleClose(); 66 | }}> 67 | 68 | 69 | 70 | Empty feed 71 | 72 | 73 | ); 74 | } -------------------------------------------------------------------------------- /src/components/item-editor/SelectPlayerOrder.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import MenuItem from '@mui/material/MenuItem'; 3 | import Select from '@mui/material/Select'; 4 | import Stack from '@mui/material/Stack'; 5 | import Typography from '@mui/material/Typography'; 6 | 7 | const DEFAULT_VALUE = ["0", "1", "2", "3"]; 8 | 9 | const validate = (values) => { 10 | if (!values) return false; 11 | 12 | const vals = values.split(":"); 13 | if (vals.length !== 4) return false; 14 | 15 | return DEFAULT_VALUE.every(v => vals.includes(v)); 16 | } 17 | 18 | function PlayerSelect(props) { 19 | const { player, value, onChange } = props 20 | 21 | return ( 22 | 33 | ); 34 | } 35 | 36 | export default function SelectPlayerOrder(props) { 37 | const { value, onChange } = props; 38 | 39 | let values = [...DEFAULT_VALUE]; 40 | if (validate(value)) { 41 | values = value.split(":"); 42 | } 43 | 44 | const handleChange = (e) => { 45 | 46 | const target = parseInt(e.target.name); 47 | const value = e.target.value; 48 | 49 | for (let i = 0; i < values.length; i++) { 50 | if (values[i] === value) { 51 | values[i] = values[target]; 52 | values[target] = value; 53 | } 54 | } 55 | 56 | if (onChange) onChange(values.join(":")); 57 | }; 58 | 59 | return ( 60 | <> 61 | 62 | Player Order 63 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | ); 76 | } -------------------------------------------------------------------------------- /src/components/item-editor/SelectType.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import InputLabel from '@mui/material/InputLabel'; 3 | import FormControl from '@mui/material/FormControl'; 4 | import Select from '@mui/material/Select'; 5 | 6 | import { 7 | AppRegistry 8 | } from '@webrcade/app-common' 9 | 10 | export default function SelectType(props) { 11 | const { item, setItem, onChange } = props; 12 | 13 | const handleChange = (e) => { 14 | if (onChange) onChange(e); 15 | setItem({ ...item, type: e.target.value }); 16 | }; 17 | 18 | const aliasTypes = []; 19 | const specificTypes = []; 20 | 21 | const types = AppRegistry.instance.getAppTypes(); 22 | for (const key in types) { 23 | const type = types[key]; 24 | const isAlias = type.absoluteKey !== undefined; 25 | const name = AppRegistry.instance.getShortNameForType(key); 26 | 27 | if (isAlias) { 28 | aliasTypes.push({key, name}); 29 | } else { 30 | specificTypes.push({key, name}); 31 | } 32 | } 33 | 34 | aliasTypes.sort(function (a, b) { 35 | return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); 36 | }); 37 | specificTypes.sort(function (a, b) { 38 | return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); 39 | }); 40 | 41 | return ( 42 |
43 | 44 | Application 45 | 71 | 72 |
73 | ); 74 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webЯcade Feed Editor 2 | 3 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | 5 | The webЯcade feed editor, located at [editor.webrcade.com](https://editor.webrcade.com), provides a much simpler alternative to the [manual creation](https://docs.webrcade.com/feeds/tutorial/) of feeds. The editor allows for quickly creating webЯcade feeds without requiring any knowledge of the underlying [document format](https://docs.webrcade.com/feeds/format/). 6 | 7 |

8 | 9 | 10 | 11 |

12 | 13 | The editor also includes a ROM *analyzer* that is capable of determining the appropriate [Application](https://docs.webrcade.com/apps/) type (emulator, etc.) for ROMs as well as any associated meta-information (title, properties, description, and related artwork). The *analyze* operation is similar to *scraping* functionality found in other front-ends with the primary difference being that in addition to meta-information the analyzer also establishes the application type and related properties. 14 | 15 |

16 | 17 | 18 | 19 |

20 | 21 | The editor provides [several ways](https://docs.webrcade.com/editor/workspace/addingitems/) to create and add items (games, etc.) to feeds. The simplest way being to simply [drag and drop ROM-based links](https://docs.webrcade.com/editor/draganddrop/#drag-rom-urls) into the editor's [workspace](https://docs.webrcade.com/editor/workspace/). 22 | 23 | ## Documentation 24 | 25 | The [webЯcade Feed Editor Documentation](https://docs.webrcade.com/editor/) provides detailed information about the editor's interface, available actions, and overall functionality. 26 | 27 | ## LICENSE 28 | 29 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 30 | 31 | http://www.apache.org/licenses/LICENSE-2.0 32 | 33 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 34 | -------------------------------------------------------------------------------- /src/components/common/editor/EditorUrlField.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import ArrowDropDownCircleIcon from '@mui/icons-material/ArrowDropDownCircle'; 4 | import IconButton from '@mui/material/IconButton'; 5 | import ListItemIcon from '@mui/material/ListItemIcon'; 6 | import Menu from '@mui/material/Menu'; 7 | import MenuItem from '@mui/material/MenuItem'; 8 | import Stack from '@mui/material/Stack'; 9 | import Tooltip from '@mui/material/Tooltip'; 10 | 11 | import EditorTextField from './EditorTextField'; 12 | import { dropboxPicker } from '../../../Dropbox'; 13 | 14 | function AddMenu(props) { 15 | const { 16 | anchorEl, 17 | multiselect, 18 | onDropText, 19 | setAnchorEl, 20 | } = props; 21 | const open = Boolean(anchorEl); 22 | const handleClose = () => { 23 | setAnchorEl(null); 24 | }; 25 | 26 | return ( 27 | 33 | { 34 | handleClose(); 35 | dropboxPicker((res) => { 36 | if (onDropText) { 37 | onDropText(res.length > 1 ? res : res[0]); 38 | } 39 | }, multiselect ? true : false); 40 | }}> 41 | 42 | 49 | 50 | Select from Dropbox... 51 | 52 | 53 | ); 54 | } 55 | 56 | export default function EditorUrlField(props) { 57 | const [menuAnchor, setMenuAnchor] = React.useState(false); 58 | const { 59 | multiselect, 60 | ...other 61 | } = props; 62 | 63 | return ( 64 | <> 65 | 70 | 73 | 74 | { 77 | setMenuAnchor(e.target) 78 | }}> 79 | 80 | 81 | 82 | 83 | 89 | 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/components/DownloadFileDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import Dialog from '@mui/material/Dialog'; 4 | import DialogActions from '@mui/material/DialogActions'; 5 | import DialogContent from '@mui/material/DialogContent'; 6 | import DialogTitle from '@mui/material/DialogTitle'; 7 | import useMediaQuery from '@mui/material/useMediaQuery'; 8 | import { useTheme } from '@mui/material/styles'; 9 | import DownloadRoundedIcon from '@mui/icons-material/DownloadRounded'; 10 | import ContentLabel 11 | from './common/ContentLabel'; 12 | 13 | let _setId = null; 14 | let _setOpen = null; 15 | let _setFile = null; 16 | let _setName = null; 17 | let id = 0; 18 | 19 | export function openDownloadFileDialog(file, name) { 20 | _setId(id++); 21 | _setOpen(true); 22 | _setFile(file); 23 | _setName(name); 24 | } 25 | 26 | export function DownloadFileDialog() { 27 | const [isOpen, setOpen] = React.useState(false); 28 | const [id, setId] = React.useState(false); 29 | const [file, setFile] = React.useState(null); 30 | const [name, setName] = React.useState(null); 31 | 32 | _setOpen = setOpen; 33 | _setId = setId; 34 | _setFile = setFile; 35 | _setName = setName; 36 | 37 | if (!isOpen) return <>; 38 | 39 | return ( 40 | 47 | ); 48 | } 49 | 50 | const DownloadFileDialogInner = ({isOpen, file, setOpen, name}) => { 51 | const theme = useTheme(); 52 | const fullScreen = useMediaQuery(theme.breakpoints.down('md')); 53 | 54 | const resolvedName = name ? name : file ? file.name : "(unknown)"; 55 | 56 | const download = () => { 57 | const url = URL.createObjectURL(file); 58 | var element = document.createElement('a'); 59 | element.setAttribute('href', url); 60 | element.setAttribute('download', resolvedName); 61 | element.style.display = 'none'; 62 | document.body.appendChild(element); 63 | element.click(); 64 | document.body.removeChild(element); 65 | 66 | setOpen(false); 67 | } 68 | 69 | return ( 70 | setOpen(false)} 73 | fullScreen={fullScreen} 74 | > 75 | {"Download File"} 76 | 77 |
78 | } 80 | label={resolvedName} 81 | onClick={() => download()} 82 | /> 83 |
84 |
85 | 86 | 89 | 90 |
91 | ); 92 | }; 93 | 94 | -------------------------------------------------------------------------------- /src/components/item-editor/quake/WadSelector.js: -------------------------------------------------------------------------------- 1 | import * as Util from '../../../Util'; 2 | import EditorSelect from "../../common/editor/EditorSelect" 3 | import EditorTextField from "../../common/editor/EditorTextField"; 4 | 5 | const AUTO = 0; 6 | const QUAKE = 1; 7 | const SCOURGE = 2; 8 | const DISSOLUTION = 3; 9 | const DOPA = 4; 10 | const CUSTOM = 100; 11 | 12 | const getWadType = (t) => { 13 | return t ? t : 0; 14 | } 15 | 16 | const getWadPath = (t, path) => { 17 | switch (getWadType(t)) { 18 | case AUTO: 19 | return ""; 20 | case QUAKE: 21 | return "id1/"; 22 | case SCOURGE: 23 | return "hipnotic/"; 24 | case DISSOLUTION: 25 | return "rogue/"; 26 | case DOPA: 27 | return "dopa/"; 28 | case CUSTOM: 29 | return Util.asString(path); 30 | default: 31 | } 32 | } 33 | 34 | export default function WadSelector(props) { 35 | const { object, setObject } = props; 36 | return ( 37 | <> 38 |
39 | { 52 | const value = e.target.value; 53 | if (value !== CUSTOM) { 54 | object.props.wadPath = ''; 55 | } 56 | const props = { ...object.props, wadType: value } 57 | setObject({ ...object, props }) 58 | }} 59 | /> 60 |
61 | {getWadType(object.props.wadType) !== AUTO && ( 62 |
63 | { 68 | if (getWadType(object.props.wadType) === CUSTOM) { 69 | const props = { ...object.props, wadPath: text } 70 | setObject({ ...object, props }) 71 | } 72 | }} 73 | onChange={(e) => { 74 | if (getWadType(object.props.wadType) === CUSTOM) { 75 | const props = { ...object.props, wadPath: e.target.value } 76 | setObject({ ...object, props }) 77 | } 78 | }} 79 | value={getWadPath(object.props.wadType, object.props.wadPath)} 80 | /> 81 |
82 | )} 83 | 84 | ) 85 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | webЯcade Feed Editor 31 | 32 | 33 | 37 | 38 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /src/Prefs.js: -------------------------------------------------------------------------------- 1 | import { 2 | Storage, 3 | LOG 4 | } from '@webrcade/app-common' 5 | 6 | import * as Util from './Util'; 7 | 8 | class PrefsImpl { 9 | constructor() { 10 | this.storage = new Storage(); 11 | this.prefs = {}; 12 | this.feed = null; 13 | } 14 | 15 | PREFIX = "editor."; 16 | PREFERENCES_PROP = this.PREFIX + "prefs"; 17 | FEED_PROP = this.PREFIX + "feed"; 18 | TEST_FEED_PROP = this.PREFIX + "testFeed"; 19 | 20 | async load() { 21 | const { storage, PREFERENCES_PROP, FEED_PROP } = this; 22 | try { 23 | this.prefs = await storage.get(PREFERENCES_PROP); 24 | if (!this.prefs) { 25 | this.prefs = {}; 26 | } 27 | const feed = await storage.get(FEED_PROP); 28 | if (feed) { 29 | this.feed = feed; 30 | } 31 | 32 | } catch (e) { 33 | LOG.error("Error loading preferences: " + e); 34 | } 35 | } 36 | 37 | async setPreference(name, value) { 38 | this.prefs[name] = value; 39 | this.save(); 40 | } 41 | 42 | getPreference(name, defaultValue) { 43 | return this.prefs[name] ? 44 | this.prefs[name] : defaultValue; 45 | } 46 | 47 | getBoolPreference(name, defaultValue) { 48 | const val = this.prefs[name]; 49 | if (val === undefined) { 50 | return defaultValue; 51 | } else { 52 | return val === true; 53 | } 54 | } 55 | 56 | getIntPreference(name, defaultValue) { 57 | const val = this.getPreference(name, defaultValue); 58 | try { 59 | if (val) { 60 | return parseInt(val); 61 | } 62 | } catch(e) { 63 | LOG.error("Preferece is not an integer: " + name + ", " + val); 64 | } 65 | return defaultValue; 66 | } 67 | 68 | async setLastFeedUrl(lastUrl) { 69 | this.prefs.lastUrl = lastUrl; 70 | await this.save(); 71 | } 72 | 73 | getLastFeedUrl() { 74 | return Util.asString(this.prefs.lastUrl); 75 | } 76 | 77 | async setLastNewType(type) { 78 | this.prefs.lastNewType = type; 79 | await this.save(); 80 | } 81 | 82 | getLastNewType() { 83 | return Util.asString(this.prefs.lastNewType); 84 | } 85 | 86 | async setFeed(feed) { 87 | this.feed = feed; 88 | await this.saveFeed(this.FEED_PROP, feed); 89 | } 90 | 91 | async storeTestFeed(feed) { 92 | await this.saveFeed(this.TEST_FEED_PROP, feed); 93 | } 94 | 95 | getFeed() { 96 | return this.feed; 97 | } 98 | 99 | async save() { 100 | const { prefs, storage, PREFERENCES_PROP } = this; 101 | try { 102 | await storage.put(PREFERENCES_PROP, prefs); 103 | } catch (e) { 104 | LOG.error("Error saving preferences: " + e); 105 | } 106 | } 107 | 108 | async saveFeed(key, feed) { 109 | const {storage } = this; 110 | try { 111 | await storage.put(key, feed); 112 | } catch (e) { 113 | LOG.error("Error saving feed: " + e); 114 | } 115 | } 116 | } 117 | 118 | const Prefs = new PrefsImpl(); 119 | 120 | export default Prefs; -------------------------------------------------------------------------------- /src/components/CreateFromUrlDialog.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Tab from '@mui/material/Tab'; 3 | 4 | import * as UrlProcessor from '../UrlProcessor'; 5 | import * as Util from '../Util'; 6 | import { Global, GlobalHolder } from '../Global'; 7 | import Editor from './common/editor/Editor'; 8 | import EditorMultiUrlField from './common/editor/EditorMultiUrlField'; 9 | import EditorTabPanel from './common/editor/EditorTabPanel'; 10 | import EditorValidator from './common/editor/EditorValidator'; 11 | 12 | const validator = new EditorValidator(); 13 | 14 | const setTabValue = () => {}; 15 | 16 | export default function CreateFromUrlDialog(props) { 17 | const [isOpen, setOpen] = React.useState(false); 18 | const [url, setUrl] = React.useState(''); 19 | const forceUpdate = Util.useForceUpdate(); 20 | 21 | GlobalHolder.setCreateFromUrlDialogOpen = setOpen; 22 | 23 | const urlTab = 0; 24 | 25 | const onOk = () => { 26 | if (!validator.checkMinLength(urlTab, "url", url)) { 27 | forceUpdate(); 28 | return false; 29 | } 30 | const urls = Util.splitLines(url); 31 | const validatedUrls = []; 32 | for (let i = 0; i < urls.length; i++) { 33 | const curUrl = urls[i].trim(); 34 | if (curUrl.length > 0) { 35 | validatedUrls.push(curUrl); 36 | } 37 | } 38 | UrlProcessor.process(validatedUrls); 39 | return true; 40 | } 41 | 42 | let urlCount = 0; 43 | const urls = Util.splitLines(url); 44 | for (let i = 0; i < urls.length; i++) { 45 | const curUrl = urls[i].trim(); 46 | if (curUrl.length > 0) { 47 | urlCount++; 48 | } 49 | } 50 | 51 | return ( 52 | { 61 | setUrl(''); 62 | validator.reset(); 63 | forceUpdate(); 64 | }} 65 | onOk={onOk} 66 | onSubmit={() => { 67 | if (onOk()) { 68 | setOpen(false); 69 | } 70 | }} 71 | tabs={[ 72 | , 73 | ]} 74 | tabPanels={( 75 | <> 76 | 77 | { 81 | if (Array.isArray(text)) { 82 | text = text.join("\n"); 83 | } 84 | setUrl(url + (url.trim().length > 0 ? "\n" : "") + text); 85 | }} 86 | onChange={(e) => { setUrl(e.target.value); }} 87 | value={url} 88 | error={!validator.isValid(urlTab, "url")} 89 | helperText={ 90 | urlCount === 0 ? "" : 91 | urlCount === 1 ? "1 URL entered." : `${urlCount} URLs entered.` 92 | } 93 | /> 94 | 95 | 96 | )} 97 | /> 98 | ); 99 | } -------------------------------------------------------------------------------- /src/components/cloud/generate-manifest/CloudStorage.js: -------------------------------------------------------------------------------- 1 | import * as WrcCommon from '@webrcade/app-common' 2 | 3 | class CloudStorage { 4 | constructor() { 5 | this.cloudEnabled = null; 6 | this.dropbox = WrcCommon.dropbox; 7 | } 8 | 9 | async isCloudEnabled() { 10 | if (this.cloudEnabled === null) { 11 | this.cloudEnabled = await this.dropbox.testWrite(); 12 | if (!this.cloudEnabled) 13 | throw Error("Error attempting to write test file"); 14 | } 15 | return this.cloudEnabled; 16 | } 17 | 18 | async listFolder(path) { 19 | return await this.dropbox.listFolder(path); 20 | } 21 | 22 | async createSharedLink(path) { 23 | let url = null; 24 | try { 25 | const result = await this.dropbox.createSharedLink(path); 26 | url = result.result.url; 27 | } catch (e) { 28 | const error = e?.error?.error?.shared_link_already_exists; 29 | if (error) { 30 | url = error.metadata.url; 31 | } else { 32 | throw (e); 33 | } 34 | } 35 | console.log(url); 36 | return url; 37 | } 38 | 39 | async createManifestFile(path, manifest) { 40 | const str = JSON.stringify(manifest, null, 2); 41 | const bytes = new TextEncoder().encode(str); 42 | const blob = new Blob([bytes], { 43 | type: "application/json;charset=utf-8" 44 | }); 45 | return await this.dropbox.uploadFile(blob, path); 46 | } 47 | 48 | async uploadFile(blob, path) { 49 | return await this.dropbox.uploadFile(blob, path); 50 | } 51 | 52 | async addChildren(rootPath, path, files) { 53 | 54 | const result = await this.listFolder(path); 55 | console.log(result); 56 | 57 | if (result?.result?.entries) { 58 | const entries = result.result.entries; 59 | for (let i = 0; i < entries.length; i++) { 60 | const entry = entries[i]; 61 | const tag = entry[".tag"]; 62 | if (tag === "folder") { 63 | await this.addChildren(rootPath, entry.path_display, files); 64 | } else if (tag === "file" && entry.path_lower.substring(rootPath.length + 1) !== "wrc-manifest.json") { 65 | const url = await this.createSharedLink(entry.path_display); 66 | const f = { 67 | name: entry.path_display.substring(rootPath.length + 1), 68 | url: url 69 | } 70 | if (entry.path_lower.endsWith(".zip")) { 71 | f.extract = true; 72 | } 73 | files.push(f); 74 | } 75 | } 76 | } 77 | } 78 | 79 | async generateManifest(path, name) { 80 | const manifest = { 81 | props: { 82 | title: name 83 | } 84 | } 85 | 86 | const files = []; 87 | manifest.files = files; 88 | await this.addChildren(path, path, files); 89 | 90 | const mName = "WRC-MANIFEST.JSON"; 91 | const manifestFile = path + 92 | (path.endsWith("/") ? mName : "/" + mName); 93 | 94 | await this.createManifestFile(manifestFile, manifest); 95 | return await this.createSharedLink(manifestFile); 96 | } 97 | 98 | async generateManifestFromNode(node) { 99 | return await this.generateManifest(node.getPath(), node.getName()); 100 | } 101 | } 102 | 103 | export { CloudStorage } 104 | -------------------------------------------------------------------------------- /src/components/item-editor/a5200/A5200DescriptionsTab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | 4 | import * as Util from '../../../Util'; 5 | import EditorTabPanel from '../../common/editor/EditorTabPanel'; 6 | import EditorTextField from '../../common/editor/EditorTextField'; 7 | 8 | function KeypadField(props) { 9 | const { 10 | label, 11 | keyName, 12 | object, 13 | setObject 14 | } = props; 15 | 16 | let val = ""; 17 | if (object.props.descriptions) { 18 | val = object.props.descriptions[keyName]; 19 | if (!val) { 20 | val = ""; 21 | } 22 | } 23 | 24 | const setValue = (str) => { 25 | let descs = object.props.descriptions; 26 | if (!descs) { 27 | descs = {} 28 | } 29 | 30 | if (Util.isEmptyString(str)) { 31 | delete descs[keyName]; 32 | } else { 33 | descs[keyName] = str; 34 | } 35 | 36 | const props = { ...object.props, descriptions: descs } 37 | setObject({ ...object, props }) 38 | }; 39 | 40 | return ( 41 |
42 | { setValue(text); }} 46 | onChange={(e) => { setValue(e.target.value); }} 47 | value={Util.asString(val)} 48 | /> 49 |
50 | ); 51 | } 52 | 53 | export default function A5200DescriptionsTab(props) { 54 | const { 55 | tabValue, 56 | tabIndex, 57 | setObject, 58 | object 59 | } = props; 60 | 61 | return ( 62 | 63 | 64 | Provide game-specific descriptions for the Atari 5200 controller keys and buttons. 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/components/cloud/generate-manifest/SelectCloudFolderDialog.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Tab from '@mui/material/Tab'; 3 | 4 | import { 5 | LOG 6 | } from '@webrcade/app-common' 7 | 8 | import { Global } from '../../../Global'; 9 | import Editor from '../../common/editor/Editor'; 10 | import EditorTabPanel from '../../common/editor/EditorTabPanel'; 11 | import { CloudStorage } from './CloudStorage'; 12 | import TreeModel from './TreeModel'; 13 | import TreeComponent from './TreeComponent'; 14 | 15 | let _setId = null; 16 | let _setOpen = null; 17 | let _setModel = null; 18 | let _setNodeSelected = null; 19 | let _onSelect = null; 20 | let id = 0; 21 | 22 | export async function openSelectCloudFolderDialog(onSelect) { 23 | _onSelect = onSelect; 24 | 25 | try { 26 | Global.openBusyScreen(true, "Reading cloud storage..."); 27 | let cloudStorage = null; 28 | try { 29 | cloudStorage = new CloudStorage(); 30 | const enabled = await cloudStorage.isCloudEnabled(); 31 | if (!enabled) { 32 | LOG.info("Cloud is not enabled.") 33 | return; 34 | } 35 | 36 | const treeModel = new TreeModel(cloudStorage); 37 | await treeModel.init(); 38 | _setModel(treeModel); 39 | } catch (e) { 40 | LOG.error(e); 41 | Global.displayMessage("An error occurred attempting to connect to cloud storage.", "error"); 42 | return; 43 | } 44 | 45 | _setId(id++); 46 | _setNodeSelected(null); 47 | _setOpen(true); 48 | } finally { 49 | Global.openBusyScreen(false); 50 | } 51 | } 52 | 53 | export function SelectCloudFolderDialog() { 54 | const [isOpen, setOpen] = React.useState(false); 55 | const [id, setId] = React.useState(false); 56 | const [model, setModel] = React.useState(false); 57 | const [nodeSelected, setNodeSelected] = React.useState(null); 58 | 59 | _setOpen = setOpen; 60 | _setNodeSelected = setNodeSelected; 61 | _setModel = setModel; 62 | _setId = setId; 63 | 64 | if (!isOpen) return <>; 65 | 66 | return ( 67 | 75 | ); 76 | } 77 | 78 | const setTabValue = () => { }; 79 | 80 | function SelectCloudFolderDialogInner({ isOpen, setOpen, model, nodeSelected, setNodeSelected }) { 81 | const selectTab = 0; 82 | 83 | return ( 84 | { 93 | console.log("on show..."); 94 | }} 95 | okTitle={"Select"} 96 | onOk={() => { 97 | _onSelect(model, nodeSelected); 98 | return true; 99 | }} 100 | okDisabled={!nodeSelected} 101 | tabs={[ 102 | , 103 | ]} 104 | tabPanels={( 105 | <> 106 | 107 | 108 | 109 | 110 | )} 111 | /> 112 | ); 113 | } -------------------------------------------------------------------------------- /src/components/ImportDialog.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Tab from '@mui/material/Tab'; 3 | 4 | import * as Util from '../Util'; 5 | import { Global, GlobalHolder } from '../Global'; 6 | import Editor from './common/editor/Editor'; 7 | import EditorFileChooser from './common/editor/EditorFileChooser'; 8 | import EditorTabPanel from './common/editor/EditorTabPanel'; 9 | import EditorUrlField from './common/editor/EditorUrlField'; 10 | import EditorValidator from './common/editor/EditorValidator'; 11 | import Prefs from '../Prefs'; 12 | import * as Feed from '../Feed'; 13 | 14 | const validator = new EditorValidator(); 15 | 16 | function importFeed(obj, isFile = false) { 17 | Global.openBusyScreen(true, "Importing feed..."); 18 | (isFile ? 19 | Feed.loadFeedFromFile(obj) : 20 | Feed.loadFeedFromUrl(obj)) 21 | .then((feed) => { 22 | Global.setFeed(feed); 23 | }) 24 | .catch(msg => { 25 | Global.displayMessage(msg); 26 | }) 27 | .finally(() => { 28 | Global.openBusyScreen(false); 29 | }); 30 | } 31 | 32 | export default function ImportDialog(props) { 33 | const [isOpen, setOpen] = React.useState(false); 34 | const [tabValue, setTabValue] = React.useState(0); 35 | const [feedUrl, setFeedUrl] = React.useState(''); 36 | const forceUpdate = Util.useForceUpdate(); 37 | 38 | GlobalHolder.setImportDialogOpen = setOpen; 39 | 40 | const urlTab = 0; 41 | const fileTab = 1; 42 | 43 | const onOk = () => { 44 | if (tabValue === urlTab) { 45 | if (!validator.checkMinLength(urlTab, "feedUrl", feedUrl)) { 46 | forceUpdate(); 47 | return false; 48 | } 49 | Prefs.setLastFeedUrl(feedUrl); 50 | importFeed(feedUrl); 51 | } 52 | return true; 53 | } 54 | 55 | return ( 56 | { 65 | setFeedUrl(Prefs.getLastFeedUrl()); 66 | validator.reset(); 67 | forceUpdate(); 68 | }} 69 | onOk={onOk} 70 | onSubmit={() =>{ 71 | if (tabValue === urlTab && onOk()) { 72 | setOpen(false); 73 | } 74 | }} 75 | tabs={[ 76 | , 77 | , 78 | ]} 79 | tabPanels={( 80 | <> 81 | 82 | { setFeedUrl(text); }} 87 | onChange={(e) => { setFeedUrl(e.target.value); }} 88 | value={feedUrl} 89 | error={!validator.isValid(urlTab, "feedUrl")} 90 | /> 91 | 92 | 93 | { 97 | const files = e.target.files; 98 | if (files.length > 0) { 99 | const f = files[0]; 100 | setOpen(false); 101 | importFeed(f, true); 102 | } 103 | } 104 | } 105 | /> 106 | 107 | 108 | )} 109 | /> 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /src/components/common/editor/Editor.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import Button from '@mui/material/Button'; 4 | import Dialog from '@mui/material/Dialog'; 5 | import DialogActions from '@mui/material/DialogActions'; 6 | import DialogContent from '@mui/material/DialogContent'; 7 | import DialogTitle from '@mui/material/DialogTitle'; 8 | import Tabs from '@mui/material/Tabs'; 9 | import useMediaQuery from '@mui/material/useMediaQuery'; 10 | import { useTheme } from '@mui/material/styles'; 11 | 12 | import { enableDropHandler } from '../../../UrlProcessor'; 13 | import { usePrevious } from '../../../Util'; 14 | 15 | export default function Editor(props) { 16 | 17 | const [okCount, setOkCount] = React.useState(0); 18 | 19 | const { 20 | isOpen, 21 | setOpen, 22 | title, 23 | tabs, 24 | tabPanels, 25 | setTabValue, 26 | tabValue, 27 | onShow, 28 | onSubmit, 29 | onOk, 30 | height, 31 | maxWidth, 32 | closeButton, 33 | okTitle, 34 | okDisabled, 35 | ...other 36 | } = props; 37 | 38 | const prevOpen = usePrevious(isOpen); 39 | const theme = useTheme(); 40 | const fullScreen = useMediaQuery(theme.breakpoints.down('md')); 41 | 42 | // Enable/disable the drop handler 43 | if (prevOpen && !isOpen) { 44 | enableDropHandler(true); 45 | //console.log('enable drop'); 46 | } else if (!prevOpen && isOpen) { 47 | //console.log('disable drop'); 48 | enableDropHandler(false); 49 | } 50 | 51 | React.useEffect(() => { 52 | if (isOpen && !prevOpen) { 53 | // Reset here 54 | setTabValue(0); 55 | if (onShow) onShow(); 56 | } 57 | }, [isOpen, prevOpen, setTabValue, onShow]); 58 | 59 | const mwidth = 60 | maxWidth === false ? {} : 61 | { "maxWidth": maxWidth ? maxWidth : "md" } 62 | 63 | const onOkHandler = () => { 64 | if (onOk && onOk()) { 65 | setOpen(false) 66 | } else { 67 | setOkCount(okCount + 1); 68 | } 69 | } 70 | 71 | return ( 72 |
73 | { setOpen(false) }} 77 | fullWidth 78 | onSubmit={(event) => { 79 | if (onSubmit) onSubmit(event); 80 | event.preventDefault(); 81 | }} 82 | {...mwidth} 83 | {...other} 84 | > 85 | {title} 86 | 87 | 88 | { setTabValue(value) }} 89 | variant="scrollable" 90 | scrollButtons="auto" 91 | allowScrollButtonsMobile 92 | sx={{ display: tabs.length === 1 ? 'none' : undefined }} 93 | > 94 | {tabs} 95 | 96 | 97 | {tabPanels} 98 | 99 | 100 | { 101 | closeButton ? 102 | : 105 | <> 106 | 109 | 112 | 113 | } 114 | 115 | 116 |
117 | ); 118 | } -------------------------------------------------------------------------------- /src/components/CategoryEditor.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Tab from '@mui/material/Tab'; 3 | 4 | import * as Feed from '../Feed'; 5 | import * as Util from '../Util'; 6 | import BackgroundTab from './common/editor/BackgroundTab'; 7 | import GeneralTab from './common/editor/GeneralTab'; 8 | import Editor from './common/editor/Editor'; 9 | import EditorValidator from './common/editor/EditorValidator' 10 | import ThumbnailTab from './common/editor/ThumbnailTab'; 11 | import { GlobalHolder, Global } from '../Global'; 12 | 13 | import { 14 | CategoryBackgroundImage, 15 | CategoryThumbImage 16 | } from '@webrcade/app-common' 17 | 18 | const validator = new EditorValidator(); 19 | 20 | const addValidatorCallback = (id, cb) => { 21 | validator.addCallback(id, cb); 22 | } 23 | 24 | export default function CategoryEditor(props) { 25 | const [tabValue, setTabValue] = React.useState(0); 26 | const [category, setCategory] = React.useState({}); 27 | const [isOpen, setOpen] = React.useState(false); 28 | const [isCreate, setCreate] = React.useState(false); 29 | 30 | GlobalHolder.setCategoryEditorOpen = (open, isCreate = false) => { 31 | setCreate(isCreate); 32 | setOpen(open); 33 | } 34 | GlobalHolder.setEditCategory = setCategory; 35 | 36 | const forceUpdate = Util.useForceUpdate(); 37 | 38 | const genTab = 0; 39 | const thumbTab = 1; 40 | const bgTab = 2; 41 | 42 | return ( 43 | { 50 | validator.reset(); 51 | setCategory(Util.cloneObject(category)) 52 | forceUpdate(); 53 | }} 54 | onOk={() => { 55 | validator.executeCallbacks(); 56 | const minTab = validator.getMinInvalidTab(); 57 | if (minTab >= 0) { 58 | setTabValue(minTab); 59 | forceUpdate(); 60 | return false; 61 | } 62 | 63 | // Get the feed 64 | const feed = Global.getFeed(); 65 | // Replace the category in the feed 66 | if (isCreate) { 67 | Feed.addCategoryToFeed(feed, category); 68 | } else { 69 | Feed.replaceCategory(feed, category.id, category); 70 | } 71 | // Update the feed (shallow clone) 72 | Global.setFeed({ ...feed }); 73 | 74 | return true; 75 | }} 76 | tabs={[ 77 | , 78 | , 79 | , 80 | ]} 81 | tabPanels={( 82 | <> 83 | 91 | 99 | 107 | 108 | )} 109 | /> 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /src/components/item-editor/a5200/A5200MappingsTab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | 4 | import EditorTabPanel from '../../common/editor/EditorTabPanel'; 5 | import EditorSelect from '../../common/editor/EditorSelect'; 6 | 7 | function MappingField(props) { 8 | const { 9 | label, 10 | buttonName, 11 | object, 12 | setObject 13 | } = props; 14 | 15 | const NONE = "none"; 16 | 17 | let value = NONE; 18 | if (object.props.mappings) { 19 | value = object.props.mappings[buttonName]; 20 | if (!value) { 21 | value = NONE; 22 | } 23 | } 24 | 25 | const setValue = (str) => { 26 | let mappings = object.props.mappings; 27 | if (!mappings) { 28 | mappings = {} 29 | } 30 | 31 | if (str === NONE) { 32 | delete mappings[buttonName]; 33 | } else { 34 | mappings[buttonName] = str; 35 | } 36 | 37 | const props = { ...object.props, mappings: mappings } 38 | setObject({ ...object, props }) 39 | }; 40 | 41 | const menuItems = []; 42 | menuItems.push({value: NONE, name: "(none)"}); 43 | 44 | const getName = (key, def) => { 45 | let name = null; 46 | if (object.props.descriptions) { 47 | name = object.props.descriptions[key]; 48 | } 49 | if (!name) { 50 | name = def; 51 | } 52 | return name; 53 | } 54 | 55 | // Keypad buttons 56 | menuItems.push({value: "start", name: getName("start", "Start")}); 57 | menuItems.push({value: "pause", name: getName("pause", "Pause")}); 58 | menuItems.push({value: "reset", name: getName("reset", "Reset")}); 59 | 60 | const KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#"]; 61 | for (let i = 0; i < KEYS.length; i++) { 62 | const key = KEYS[i]; 63 | menuItems.push({value: key, name: getName(key, "Keypad " + key)}); 64 | } 65 | 66 | menuItems.push({value: "topfire", name: getName("topfire", "Top Fire")}); 67 | menuItems.push({value: "bottomfire", name: getName("bottomfire", "Bottom Fire")}); 68 | 69 | // Ensure value is in values 70 | let found = false; 71 | for (let i = 0; i < menuItems.length; i++) { 72 | const item = menuItems[i]; 73 | if (item.value === value) { 74 | found = true; 75 | break; 76 | } 77 | } 78 | 79 | if (!found) { 80 | value = NONE; 81 | } 82 | 83 | return ( 84 |
85 | { setValue(e.target.value) }} 90 | /> 91 |
92 | ); 93 | } 94 | 95 | export default function A5200MappingsTab(props) { 96 | const { 97 | tabValue, 98 | tabIndex, 99 | setObject, 100 | object 101 | } = props; 102 | 103 | return ( 104 | 105 | 106 | Create game-specific mappings from the Atari 5200 keys and buttons to the gamepad. 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /src/components/common/CommonImage.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Avatar from '@mui/material/Avatar'; 4 | import Link from '@mui/material/Link'; 5 | 6 | import * as WrcCommon from '@webrcade/app-common' 7 | import { dropHandler } from '../../Drop'; 8 | import { usePrevious } from '../../Util'; 9 | 10 | export default function CommonImage(props) { 11 | const { 12 | defaultImageSrc, 13 | requiredAspectRatio, 14 | errorCallback, 15 | onDropText, 16 | sx 17 | } = props; 18 | 19 | let imageSrc = props.imageSrc; 20 | if (imageSrc) imageSrc = WrcCommon.remapUrl(imageSrc); 21 | 22 | const [img, setImg] = React.useState(null); 23 | const prevValues = usePrevious({ 24 | imageSrc, defaultImageSrc, requiredAspectRatio 25 | }); 26 | 27 | React.useEffect(() => { 28 | // Only load image if one of the key values have changed 29 | if (!prevValues || ( 30 | (imageSrc !== prevValues.imageSrc) || 31 | (defaultImageSrc !== prevValues.defaultImageSrc) || 32 | (requiredAspectRatio !== prevValues.requiredAspectRatio))) { 33 | 34 | const tempImg = new Image(); 35 | tempImg.onload = (e) => { 36 | const target = e.target; 37 | 38 | 39 | let arTarget = 1; 40 | let arReq = 1; 41 | if (requiredAspectRatio) { 42 | arTarget = (target.naturalWidth/target.naturalHeight).toFixed(2); 43 | arReq = (requiredAspectRatio[0]/requiredAspectRatio[1]).toFixed(2); 44 | // console.log('target: ' + arTarget); 45 | // console.log('req: '+ arReq); 46 | } 47 | 48 | if (requiredAspectRatio && (arTarget !== arReq)) { 49 | const msg = ( 50 | <> 51 | {`Thumbnail has been sized to a ${requiredAspectRatio[0]}:${requiredAspectRatio[1]} aspect ratio.`}    52 | (Birme) 56 |   57 | (Rect Fitter) 61 | 62 | ); 63 | 64 | if (errorCallback) errorCallback(msg); 65 | 66 | // Attempt to use proxy 67 | // const proxyImg = new Image(); 68 | // proxyImg.onload = (e) => { 69 | // const proxyTarget = e.target; 70 | // setImg(proxyTarget.src); 71 | // } 72 | // proxyImg.onerror = (e) => { 73 | // setImg(defaultImageSrc); 74 | // } 75 | // const url = encodeURIComponent(target.src); 76 | //&fpy=0&a=focal 77 | //&fit=contain&cbg=00FFFFFF 78 | //proxyImg.src = `https://images.weserv.nl/?url=${url}&w=${requiredSize[0]}&h=${requiredSize[1]}&fit=cover&output=gif`; 79 | 80 | setImg(target.src); 81 | } else { 82 | if (errorCallback) errorCallback(null); 83 | setImg(target.src); 84 | } 85 | } 86 | tempImg.onerror = (e) => { 87 | if (errorCallback) errorCallback( 88 | "The specified image does not exist."); 89 | setImg(defaultImageSrc); 90 | } 91 | tempImg.src = WrcCommon.isValidString(imageSrc) ? 92 | imageSrc : defaultImageSrc; 93 | } 94 | }, [imageSrc, defaultImageSrc, setImg, errorCallback, requiredAspectRatio, prevValues]); 95 | 96 | return ( 97 | { 100 | if (onDropText) { 101 | e.preventDefault(); 102 | dropHandler(e, (text) => { onDropText(text); }); 103 | } 104 | }} 105 | sx={{ 106 | backgroundColor: 'black', 107 | ...sx 108 | }} 109 | src={img} 110 | > 111 |   112 | 113 | ); 114 | } -------------------------------------------------------------------------------- /src/components/item-editor/c64/C64MappingsTab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | 4 | import EditorTabPanel from '../../common/editor/EditorTabPanel'; 5 | import EditorSelect from '../../common/editor/EditorSelect'; 6 | import { OPTIONS } from './C64MappingOptions'; 7 | 8 | function MappingField(props) { 9 | const { 10 | label, 11 | buttonName, 12 | object, 13 | setObject 14 | } = props; 15 | 16 | const NONE = "none"; 17 | 18 | let value = NONE; 19 | if (object.props.mappings) { 20 | value = object.props.mappings[buttonName]; 21 | if (!value) { 22 | value = NONE; 23 | } 24 | } 25 | 26 | const setValue = (str) => { 27 | let mappings = object.props.mappings; 28 | if (!mappings) { 29 | mappings = {} 30 | } 31 | 32 | if (str === NONE) { 33 | delete mappings[buttonName]; 34 | } else { 35 | mappings[buttonName] = str; 36 | } 37 | 38 | const props = { ...object.props, mappings: mappings } 39 | setObject({ ...object, props }) 40 | }; 41 | 42 | const menuItems = []; 43 | menuItems.push({value: NONE, name: "(none)"}); 44 | 45 | for (let i = 0; i < OPTIONS.length; i++) { 46 | const opt = OPTIONS[i]; 47 | menuItems.push({value: opt.value, name: opt.label}); 48 | } 49 | 50 | // Ensure value is in values 51 | let found = false; 52 | for (let i = 0; i < menuItems.length; i++) { 53 | const item = menuItems[i]; 54 | if (item.value === value) { 55 | found = true; 56 | break; 57 | } 58 | } 59 | 60 | if (!found) { 61 | value = NONE; 62 | } 63 | 64 | return ( 65 |
66 | { setValue(e.target.value) }} 71 | /> 72 |
73 | ); 74 | } 75 | 76 | export default function C64MappingsTab(props) { 77 | const { 78 | tabValue, 79 | tabIndex, 80 | setObject, 81 | object 82 | } = props; 83 | 84 | return ( 85 | 86 | 87 | Create game-specific mappings from the Commodore keys and joystick to the gamepad. 88 | 89 | 90 | {/* 91 | 92 | 93 | */} 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /src/components/cloud/generate-manifest/TreeModel.js: -------------------------------------------------------------------------------- 1 | import CircularProgress from '@mui/material/CircularProgress'; 2 | import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined'; 3 | import CustomTreeItem from "./CustomTreeItem"; 4 | 5 | class TreeNode { 6 | constructor(id, name, path) { 7 | this.id = id; 8 | this.name = name; 9 | this.path = path; 10 | this.children = []; 11 | this.loading = false; 12 | } 13 | 14 | getId() { 15 | return this.id; 16 | } 17 | 18 | getName() { 19 | return this.name; 20 | } 21 | 22 | getPath() { 23 | return this.path; 24 | } 25 | 26 | getChildren() { 27 | return this.children; 28 | } 29 | 30 | addChild(node) { 31 | this.children.push(node); 32 | } 33 | 34 | setLoading(v) { 35 | this.loading = v; 36 | } 37 | 38 | isLoading() { 39 | return this.loading; 40 | } 41 | } 42 | 43 | export default class TreeModel { 44 | constructor(storage) { 45 | this.storage = storage; 46 | this.root = []; 47 | this.nodesById = {} 48 | this.nextId = 0; 49 | this.childrenLoaded = {} 50 | this.expanded = {} 51 | this.expandedArr = []; 52 | } 53 | 54 | getStorage() { 55 | return this.storage; 56 | } 57 | 58 | getExpanded() { 59 | console.log(this.expandedArr) 60 | return this.expandedArr; 61 | } 62 | 63 | toggleExpanded(nodeId) { 64 | if (this.expanded[nodeId]) { 65 | delete this.expanded[nodeId]; 66 | } else { 67 | this.expanded[nodeId] = true; 68 | } 69 | 70 | this.expandedArr = []; 71 | for (let key in this.expanded) { 72 | console.log('key: ' + key); 73 | this.expandedArr.push(key); 74 | } 75 | } 76 | 77 | getNode(nodeId) { 78 | return this.nodesById[nodeId]; 79 | } 80 | 81 | async addNodes(parentNode, path, cb, loadingCb) { 82 | if (parentNode && this.childrenLoaded[parentNode.getId()]) { 83 | return; 84 | } 85 | 86 | if (parentNode) { 87 | parentNode.setLoading(true); 88 | if (loadingCb) loadingCb(); 89 | } 90 | 91 | try { 92 | const results = await this.storage.listFolder(path); 93 | 94 | let count = 0; 95 | 96 | if (results?.result?.entries) { 97 | const entries = results.result.entries; 98 | entries.sort((a, b) => { 99 | return a.name.localeCompare(b.name); 100 | }); 101 | 102 | for (let i = 0; i < entries.length; i++) { 103 | const entry = entries[i]; 104 | if (entry[".tag"] === "folder") { 105 | count++; 106 | const node = new TreeNode(("node-" + (this.nextId++) + entry.name), entry.name, entry.path_display); 107 | this.nodesById[node.getId()] = node; 108 | if (!parentNode) { 109 | this.root.push(node) 110 | } else { 111 | parentNode.addChild(node); 112 | } 113 | } 114 | } 115 | } 116 | 117 | if (parentNode) { 118 | this.childrenLoaded[parentNode.getId()] = true; 119 | } 120 | 121 | if (cb && count > 0) cb(); 122 | } finally { 123 | if (parentNode) parentNode.setLoading(false); 124 | } 125 | } 126 | 127 | async init() { 128 | await this.addNodes(null, ""); 129 | } 130 | 131 | renderNode(node) { 132 | let icon = undefined; 133 | if (node.isLoading()) { 134 | icon = ; 135 | } else if (node.getChildren().length === 0 && this.childrenLoaded[node.getId()]) { 136 | icon = 137 | } 138 | 139 | return ( 140 | 146 | {node.getChildren().map((node) => this.renderNode(node))} 147 | 148 | ); 149 | } 150 | 151 | renderTree() { 152 | return ( 153 | <> 154 | {this.root.map((node) => this.renderNode(node))} 155 | 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/components/item-editor/coleco/ColecoDescriptionsTab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | 4 | import * as Util from '../../../Util'; 5 | import EditorTabPanel from '../../common/editor/EditorTabPanel'; 6 | import EditorTextField from '../../common/editor/EditorTextField'; 7 | 8 | function KeypadField(props) { 9 | const { 10 | label, 11 | keyName, 12 | object, 13 | setObject 14 | } = props; 15 | 16 | let val = ""; 17 | if (object.props.descriptions) { 18 | val = object.props.descriptions[keyName]; 19 | if (!val) { 20 | val = ""; 21 | } 22 | } 23 | 24 | const setValue = (str) => { 25 | let descs = object.props.descriptions; 26 | if (!descs) { 27 | descs = {} 28 | } 29 | 30 | if (Util.isEmptyString(str)) { 31 | delete descs[keyName]; 32 | } else { 33 | descs[keyName] = str; 34 | } 35 | 36 | const props = { ...object.props, descriptions: descs } 37 | setObject({ ...object, props }) 38 | }; 39 | 40 | return ( 41 |
42 | { setValue(text); }} 46 | onChange={(e) => { setValue(e.target.value); }} 47 | value={Util.asString(val)} 48 | /> 49 |
50 | ); 51 | } 52 | 53 | export default function ColecoDescriptionsTab(props) { 54 | const { 55 | tabValue, 56 | tabIndex, 57 | setObject, 58 | object 59 | } = props; 60 | 61 | const isRoller = object.props.controlsMode === 3; 62 | const isSuper = object.props.controlsMode === 1; 63 | const isDriving = object.props.controlsMode === 2; 64 | 65 | let firelLabel = "Left fire"; 66 | if (isDriving) { 67 | firelLabel = "Gas button"; 68 | } else if (isSuper) { 69 | firelLabel = "Yellow button"; 70 | } 71 | let firerLabel = "Right fire"; 72 | if (isDriving) { 73 | firerLabel = "Brake button"; 74 | } else if (isSuper) { 75 | firerLabel = "Orange button"; 76 | } 77 | 78 | return ( 79 | 80 | 81 | Provide game-specific descriptions for the ColecoVision controller keys and buttons. 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {isRoller && } 98 | {isRoller && } 99 | {isSuper && } 100 | {isSuper && } 101 | 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/components/ItemsTableMoreMenu.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; 3 | import Divider from '@mui/material/Divider'; 4 | import FindInPageIcon from '@mui/icons-material/FindInPage'; 5 | import Menu from '@mui/material/Menu'; 6 | import MenuItem from '@mui/material/MenuItem'; 7 | import LinkIcon from '@mui/icons-material/Link'; 8 | import ListItemIcon from '@mui/material/ListItemIcon'; 9 | import * as Feed from '../Feed'; 10 | 11 | import * as UrlProcessor from '../UrlProcessor'; 12 | import { Global } from '../Global'; 13 | import { dropboxPicker } from '../Dropbox'; 14 | 15 | import * as WrcCommon from '@webrcade/app-common'; 16 | 17 | export default function ItemsTableMoreMenu(props) { 18 | const { 19 | anchorEl, 20 | setAnchorEl, 21 | feed, 22 | category, 23 | selected, 24 | lastSelected, 25 | } = props; 26 | const open = Boolean(anchorEl); 27 | const handleClose = () => { 28 | setAnchorEl(null); 29 | }; 30 | 31 | return ( 32 | 38 | { 39 | handleClose(); 40 | Global.openCreateFromUrlDialog(true); 41 | }}> 42 | 43 | 44 | 45 | Create from URLs... 46 | 47 | { 48 | handleClose(); 49 | dropboxPicker((res, names) => { 50 | UrlProcessor.process(res, names); 51 | }); 52 | }}> 53 | 54 | 61 | 62 | Add from Dropbox... 63 | 64 | 65 | { 68 | handleClose(); 69 | 70 | const app = Feed.getItem(feed, category, lastSelected); 71 | if (!app) { 72 | return; 73 | } 74 | 75 | const feedProps = feed.props ? feed.props : {}; 76 | let location = WrcCommon.getStandaloneLocation(); 77 | 78 | if (WrcCommon.isDev()) { 79 | location += "/"; 80 | } else { 81 | let path = window.location.href; 82 | const index = path.toLowerCase().indexOf('app/editor'); 83 | location = path.substring(0, index) + location; 84 | } 85 | const reg = WrcCommon.AppRegistry.instance; 86 | let icon = reg.getThumbnail(app); 87 | 88 | // Hack for default icons 89 | if (icon.startsWith("images/app/")) { 90 | icon = "../../" + icon; 91 | } 92 | 93 | const appLocation = reg.getLocation( 94 | app, WrcCommon.AppProps.RV_CONTEXT_STANDALONE, feedProps, 95 | {icon: icon}); 96 | const qIndex = appLocation.indexOf("?") 97 | location += "?app=" + encodeURIComponent(appLocation.substring(0, qIndex)) + "&" + appLocation.substring(qIndex + 1); 98 | 99 | Global.openCopyLinkDialog(true, location); 100 | }}> 101 | 102 | 103 | 104 | Copy stand-alone link... 105 | 106 | 107 | { 110 | handleClose(); 111 | UrlProcessor.analyze(category, selected); 112 | }}> 113 | 114 | 115 | 116 | Analyze 117 | 118 | 119 | ); 120 | } -------------------------------------------------------------------------------- /src/components/cloud/generate-manifest/GenerateManifestDialog.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Tab from '@mui/material/Tab'; 3 | import Stack from '@mui/material/Stack'; 4 | import useTheme from '@mui/material/styles/useTheme'; 5 | 6 | import Editor from '../../common/editor/Editor'; 7 | import EditorButton from '../../common/editor/EditorButton'; 8 | import EditorTabPanel from '../../common/editor/EditorTabPanel'; 9 | import EditorTextField from '../../common/editor/EditorTextField'; 10 | import { Typography } from '@mui/material'; 11 | import { openSelectCloudFolderDialog } from '../../cloud/generate-manifest/SelectCloudFolderDialog'; 12 | import GenerateManifestWrapper from './GenerateManifestWrapper'; 13 | 14 | let _setId = null; 15 | let _setOpen = null; 16 | let _setModel = null; 17 | let _setNodeSelected = null; 18 | let id = 0; 19 | 20 | export async function openManifestDialog() { 21 | _setId(id++); 22 | _setModel(null); 23 | _setNodeSelected(null); 24 | _setOpen(true); 25 | } 26 | 27 | export function GenerateManifestDialog() { 28 | const [isOpen, setOpen] = React.useState(false); 29 | const [id, setId] = React.useState(false); 30 | const [model, setModel] = React.useState(false); 31 | const [nodeSelected, setNodeSelected] = React.useState(null); 32 | 33 | _setOpen = setOpen; 34 | _setNodeSelected = setNodeSelected; 35 | _setModel = setModel; 36 | _setId = setId; 37 | 38 | if (!isOpen) return <>; 39 | 40 | return ( 41 | 50 | ); 51 | } 52 | 53 | function GenerateManifestDialogInner({ isOpen, setOpen, model, setModel, nodeSelected, setNodeSelected }) { 54 | const theme = useTheme(); 55 | const generateTab = 0; 56 | 57 | const onOk = async () => { 58 | console.log("generate manifest."); 59 | console.log(nodeSelected); 60 | 61 | new GenerateManifestWrapper().generate( 62 | async () => { 63 | return await model.getStorage().generateManifestFromNode(nodeSelected); 64 | }, 65 | () => { 66 | setOpen(false); 67 | }, 68 | true 69 | ) 70 | } 71 | 72 | return ( 73 | { }} 81 | onShow={() => { }} 82 | okTitle={"Generate"} 83 | onOk={() => { 84 | onOk(); 85 | return false; 86 | }} 87 | okDisabled={!nodeSelected} 88 | tabs={[ 89 | , 90 | ]} 91 | tabPanels={( 92 | <> 93 | 94 | 95 | Select a folder that contains the contents to generate the manifest file for.
96 |
97 | 98 | (For example, the root folder of a game) 99 | 100 | 105 | 111 | { 114 | openSelectCloudFolderDialog((model, node) => { 115 | setModel(model); 116 | setNodeSelected(node) 117 | }); 118 | }} 119 | /> 120 | 121 | {/* */} 122 |
123 | 124 | )} 125 | /> 126 | ); 127 | } -------------------------------------------------------------------------------- /src/components/item-editor/coleco/ColecoMappingsTab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | 4 | import EditorTabPanel from '../../common/editor/EditorTabPanel'; 5 | import EditorSelect from '../../common/editor/EditorSelect'; 6 | 7 | function MappingField(props) { 8 | const { 9 | label, 10 | buttonName, 11 | object, 12 | setObject 13 | } = props; 14 | 15 | const NONE = "none"; 16 | 17 | let value = NONE; 18 | if (object.props.mappings) { 19 | value = object.props.mappings[buttonName]; 20 | if (!value) { 21 | value = NONE; 22 | } 23 | } 24 | 25 | const setValue = (str) => { 26 | let mappings = object.props.mappings; 27 | if (!mappings) { 28 | mappings = {} 29 | } 30 | 31 | if (str === NONE) { 32 | delete mappings[buttonName]; 33 | } else { 34 | mappings[buttonName] = str; 35 | } 36 | 37 | const props = { ...object.props, mappings: mappings } 38 | setObject({ ...object, props }) 39 | }; 40 | 41 | const menuItems = []; 42 | menuItems.push({value: NONE, name: "(none)"}); 43 | 44 | const getName = (key, def) => { 45 | let name = null; 46 | if (object.props.descriptions) { 47 | name = object.props.descriptions[key]; 48 | } 49 | if (!name) { 50 | name = def; 51 | } 52 | return name; 53 | } 54 | 55 | // Keypad buttons 56 | const KEYS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "0", "#"]; 57 | for (let i = 0; i < KEYS.length; i++) { 58 | const key = KEYS[i]; 59 | menuItems.push({value: key, name: getName(key, "Keypad " + key)}); 60 | } 61 | 62 | // Buttons 63 | const isRoller = object.props.controlsMode === 3; 64 | const isSuper = object.props.controlsMode === 1; 65 | 66 | 67 | menuItems.push({value: "firel", name: getName("firel", 68 | object.props.controlsMode === 2 ? "Gas" : 69 | object.props.controlsMode === 1 ? "Yellow button" : "Left fire")}); 70 | menuItems.push({value: "firer", name: getName("firer", 71 | object.props.controlsMode === 2 ? "Brake" : 72 | object.props.controlsMode === 1 ? "Orange button" :"Right fire")}); 73 | 74 | if (isRoller) { 75 | menuItems.push({value: "firel2", name: getName("firel2", "Left fire (2p)")}); 76 | menuItems.push({value: "firer2", name: getName("firer2", "Right fire (2p)")}); 77 | } else if (isSuper) { 78 | menuItems.push({value: "blue", name: getName("blue", "Blue button")}); 79 | menuItems.push({value: "purple", name: getName("purple", "Purple button")}); 80 | } 81 | 82 | // Ensure value is in values 83 | let found = false; 84 | for (let i = 0; i < menuItems.length; i++) { 85 | const item = menuItems[i]; 86 | if (item.value === value) { 87 | found = true; 88 | break; 89 | } 90 | } 91 | 92 | if (!found) { 93 | value = NONE; 94 | } 95 | 96 | return ( 97 |
98 | { setValue(e.target.value) }} 103 | /> 104 |
105 | ); 106 | } 107 | 108 | export default function ColecoMappingsTab(props) { 109 | const { 110 | tabValue, 111 | tabIndex, 112 | setObject, 113 | object 114 | } = props; 115 | 116 | return ( 117 | 118 | 119 | Create game-specific mappings from the ColecoVision keys and buttons to the gamepad. 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /src/components/ExportDialog.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Tab from '@mui/material/Tab'; 3 | 4 | import { Base64, Zip } from '@webrcade/app-common' 5 | 6 | import * as Util from '../Util'; 7 | import { Global, GlobalHolder } from '../Global'; 8 | import Editor from './common/editor/Editor'; 9 | import EditorSwitch from './common/editor/EditorSwitch'; 10 | import EditorTabPanel from './common/editor/EditorTabPanel'; 11 | import * as Feed from '../Feed'; 12 | import Prefs from '../Prefs'; 13 | 14 | const PREF_IS_ZIPPED = "exportFeed.isZipped"; 15 | const PREF_IS_BASE64 = "exportFeed.isBase64"; 16 | 17 | export default function ImportDialog(props) { 18 | const [isOpen, setOpen] = React.useState(false); 19 | const [tabValue, setTabValue] = React.useState(0); 20 | const [isZipped, setZipped] = 21 | React.useState(Prefs.getBoolPreference(PREF_IS_ZIPPED, false)); 22 | const [isBase64Encoded, setBase64Encoded] = 23 | React.useState(Prefs.getBoolPreference(PREF_IS_BASE64, false)); 24 | 25 | const forceUpdate = Util.useForceUpdate(); 26 | 27 | GlobalHolder.setExportDialogOpen = setOpen; 28 | 29 | const exportTab = 0; 30 | 31 | const download = async (isZip, isBase64Encoded) => { 32 | let extension = ".json"; 33 | let blob = null; 34 | 35 | let feed = Feed.exportFeed(Global.getFeed()); 36 | const title = feed.title; 37 | feed = JSON.stringify(feed, null, 2); 38 | 39 | if (isZip) { 40 | extension = ".zip"; 41 | 42 | const zip = new Zip(); 43 | const zipFiles = []; 44 | const b = new Blob([feed]); 45 | zipFiles.push({ 46 | name: "feed.json", 47 | content: b 48 | }); 49 | blob = await zip.zipFiles(zipFiles); 50 | 51 | if (isBase64Encoded) { 52 | const promise = new Promise((resolve, reject) => { 53 | const reader = new FileReader(); 54 | reader.readAsDataURL(blob); 55 | reader.onloadend = function () { 56 | var base64data = reader.result.split(',')[1]; 57 | resolve(base64data); 58 | } 59 | }); 60 | blob = new Blob([await promise]); 61 | extension = ".b64"; 62 | } 63 | } else if (isBase64Encoded) { 64 | feed = Base64.encode(feed); 65 | extension = ".b64"; 66 | blob = new Blob([feed], { type: "text/plain;base64" }); 67 | } else { 68 | blob = new Blob([feed], { type: "application/json" }); 69 | } 70 | 71 | const url = URL.createObjectURL(blob); 72 | var element = document.createElement('a'); 73 | element.setAttribute('href', url); 74 | element.setAttribute('download', title + extension); 75 | element.style.display = 'none'; 76 | document.body.appendChild(element); 77 | element.click(); 78 | document.body.removeChild(element); 79 | 80 | // Close the export dialog 81 | Global.openExportDialog(false); 82 | } 83 | 84 | const onOk = (isZip, isBase64Encoded) => { 85 | Prefs.setPreference(PREF_IS_ZIPPED, isZip); 86 | Prefs.setPreference(PREF_IS_BASE64, isBase64Encoded); 87 | download(isZip, isBase64Encoded); 88 | return false; 89 | } 90 | 91 | return ( 92 | { 101 | // setFeedUrl(Prefs.getLastFeedUrl()); 102 | forceUpdate(); 103 | }} 104 | onOk={() => onOk(isZipped, isBase64Encoded)} 105 | tabs={[ 106 | , 107 | ]} 108 | tabPanels={( 109 | <> 110 | 111 | { 115 | setZipped(e.target.checked); 116 | }} 117 | checked={isZipped} 118 | /> 119 | { 123 | setBase64Encoded(e.target.checked); 124 | }} 125 | checked={isBase64Encoded} 126 | /> 127 | 128 | 129 | )} 130 | /> 131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /src/components/item-editor/gb/SelectPalette.js: -------------------------------------------------------------------------------- 1 | import { MenuItem } from '@mui/material'; 2 | import * as React from 'react'; 3 | import EditorSelect from '../../common/editor/EditorSelect'; 4 | import './SelectPalette.css' 5 | 6 | function Palette(props) { 7 | const { palClass } = props; 8 | return ( 9 |
10 |
11 |
12 |
13 |
14 |
15 | ); 16 | } 17 | 18 | function getGrayscaleMenuItems() { 19 | return ([ 20 | , 21 | ]); 22 | } 23 | 24 | function getGreenscaleMenuItems() { 25 | return ([ 26 | , 27 | , 28 | , 29 | , 30 | , 31 | , 32 | ]); 33 | } 34 | 35 | function getSuperGameBoyMenuItems() { 36 | return ([ 37 | , 38 | , 39 | , 40 | , 41 | , 42 | , 43 | , 44 | , 45 | , 46 | , 47 | , 48 | , 49 | , 50 | , 51 | , 52 | , 53 | , 54 | , 55 | , 56 | , 57 | , 58 | , 59 | , 60 | , 61 | , 62 | , 63 | , 64 | , 65 | , 66 | , 67 | , 68 | , 69 | ]); 70 | } 71 | 72 | export default function SelectPalette(props) { 73 | const { colorsValue, value, onChange } = props; 74 | 75 | const handleChange = (e) => { 76 | if (onChange) onChange(e); 77 | }; 78 | 79 | let menuitems = getGrayscaleMenuItems(); 80 | let label = "Grayscale Palette"; 81 | let tooltip = "The particular grayscale palette to use."; 82 | if (colorsValue === 1) { 83 | menuitems = getGreenscaleMenuItems(); 84 | label = "Greenscale Palette"; 85 | tooltip = "The particular greenscale palette to use."; 86 | } else if (colorsValue === 2) { 87 | menuitems = getSuperGameBoyMenuItems(); 88 | label = "Super Game Boy Palette"; 89 | tooltip = "The particular Super Game Boy palette to use."; 90 | } 91 | 92 | return ( 93 | <> 94 | { handleChange(e) }} 99 | > 100 | {menuitems} 101 | 102 | 103 | ); 104 | } -------------------------------------------------------------------------------- /src/Global.js: -------------------------------------------------------------------------------- 1 | import { 2 | AppProps, 3 | UrlUtil 4 | } from '@webrcade/app-common' 5 | 6 | import { addId } from './Feed'; 7 | import Prefs from './Prefs'; 8 | import * as Util from './Util'; 9 | 10 | class Holder { 11 | static instance = Holder.instance || new Holder(); 12 | 13 | forceRefresh = null; 14 | setBusyScreenOpen = null; 15 | setBusyScreenMessage = null; 16 | setBusyScreenDisableAutoFocus = null; 17 | setEditItem = null; 18 | setFeed = null; 19 | setApp = null; 20 | getFeed = null; 21 | setFeedCategoryId = null; 22 | getFeedCategoryId = null; 23 | setMessage = null; 24 | setMessageSeverity = null; 25 | queuedMessage = null; 26 | queuedMessageSeverity = null; 27 | toggleDrawer = null; 28 | setItemEditorOpen = null; 29 | setImportDialogOpen = null; 30 | setExportDialogOpen = null; 31 | setCategoryEditorOpen = null; 32 | setEditCategory = null; 33 | setFeedEditorOpen = null; 34 | setEditFeed = null; 35 | setCreateFromUrlDialogOpen = null; 36 | setConfirmDialogOpen = null; 37 | setConfirmDialogProps = null; 38 | setCopyLinkDialogOpen = null; 39 | setCopyLinkDialogProps = null; 40 | setLoadFeedDialogOpen = null; 41 | setSettingsEditorOpen = null; 42 | setBusyScreenDisableDrop = null; 43 | isDebug = UrlUtil.getBoolParam(window.location.search, AppProps.RP_DEBUG); 44 | } 45 | 46 | const GlobalHolder = Holder.instance; 47 | 48 | const THUMB_AR = [4, 3]; 49 | 50 | const Global = { 51 | isDebug: () => { 52 | return GlobalHolder.isDebug 53 | }, 54 | forceRefresh: () => { 55 | GlobalHolder.forceRefresh(); 56 | }, 57 | openBusyScreen: (open, message = null, disableAutoFocus, disableDrop = true) => { 58 | GlobalHolder.setBusyScreenOpen(open); 59 | GlobalHolder.setBusyScreenDisableDrop(disableDrop); 60 | GlobalHolder.setBusyScreenDisableAutoFocus(disableAutoFocus ? true : false); 61 | if (open && message) { 62 | GlobalHolder.setBusyScreenMessage(message); 63 | } else { 64 | GlobalHolder.setBusyScreenMessage(""); 65 | } 66 | }, 67 | openItemEditor: (open, isCreate = false) => { 68 | GlobalHolder.setItemEditorOpen(open, isCreate); 69 | }, 70 | openLoadFeedDialog: (open) => { 71 | GlobalHolder.setLoadFeedDialogOpen(true); 72 | }, 73 | openConfirmDialog: (open, title, message, callback) => { 74 | if (title && message && callback) { 75 | GlobalHolder.setConfirmDialogProps({ 76 | title: title, 77 | message: message, 78 | callback: callback 79 | }); 80 | } 81 | GlobalHolder.setConfirmDialogOpen(open); 82 | }, 83 | openCopyLinkDialog: (open, link, title, success, disableShortened) => { 84 | if (link) { 85 | GlobalHolder.setCopyLinkDialogProps({ 86 | link: link, 87 | title: title, 88 | success: success, 89 | disableShortened: disableShortened 90 | }); 91 | } 92 | GlobalHolder.setCopyLinkDialogOpen(open); 93 | }, 94 | openImportDialog: (open) => { 95 | GlobalHolder.setImportDialogOpen(open); 96 | }, 97 | openExportDialog: (open) => { 98 | GlobalHolder.setExportDialogOpen(open); 99 | }, 100 | openCreateFromUrlDialog: (open) => { 101 | GlobalHolder.setCreateFromUrlDialogOpen(open); 102 | }, 103 | openSettingsEditor: (open) => { 104 | GlobalHolder.setSettingsEditorOpen(open); 105 | }, 106 | editItem: (item) => { 107 | Global.openItemEditor(true); 108 | GlobalHolder.setEditItem(Util.cloneObject(item)); 109 | }, 110 | createNewItem: () => { 111 | Global.openItemEditor(true, true); 112 | const newItem = { 113 | props: {}, type: "2600" 114 | }; 115 | addId(newItem); 116 | GlobalHolder.setEditItem(newItem); 117 | }, 118 | openCategoryEditor: (open, isCreate = false) => { 119 | GlobalHolder.setCategoryEditorOpen(open, isCreate); 120 | }, 121 | openFeedEditor: (open) => { 122 | GlobalHolder.setFeedEditorOpen(open); 123 | }, 124 | editCategory: (cat) => { 125 | Global.openCategoryEditor(true); 126 | GlobalHolder.setEditCategory(Util.cloneObject(cat)); 127 | }, 128 | createNewCategory: () => { 129 | Global.openCategoryEditor(true, true); 130 | const newCat = {}; 131 | addId(newCat); 132 | GlobalHolder.setEditCategory(newCat); 133 | }, 134 | editFeed: (feed) => { 135 | Global.openFeedEditor(true); 136 | GlobalHolder.setEditFeed(Util.cloneObject(feed)); 137 | }, 138 | toggleDrawer: () => { 139 | GlobalHolder.toggleDrawer(); 140 | }, 141 | displayMessage(message, severity) { 142 | if (GlobalHolder.setMessage && GlobalHolder.setMessageSeverity) { 143 | GlobalHolder.setMessage(message); 144 | GlobalHolder.setMessageSeverity(severity); 145 | } else { 146 | GlobalHolder.queuedMessage = message; 147 | GlobalHolder.queuedMessageSeverity = severity; 148 | } 149 | }, 150 | setFeed: (feed) => { 151 | GlobalHolder.setFeed(feed); 152 | Prefs.setFeed(feed); 153 | }, 154 | getFeed: () => { 155 | return GlobalHolder.getFeed(); 156 | }, 157 | getFeedCategoryId: () => { 158 | return GlobalHolder.getFeedCategoryId(); 159 | }, 160 | setFeedCategoryId: (id) => { 161 | return GlobalHolder.setFeedCategoryId(id); 162 | }, 163 | getThumbAspectRatio: () => { 164 | return THUMB_AR; 165 | }, 166 | setApp: (app) => { 167 | GlobalHolder.setApp(app); 168 | } 169 | } 170 | 171 | export { Global, GlobalHolder }; -------------------------------------------------------------------------------- /src/components/CopyLinkDialog.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import Dialog from '@mui/material/Dialog'; 4 | import DialogActions from '@mui/material/DialogActions'; 5 | import DialogContent from '@mui/material/DialogContent'; 6 | import DialogTitle from '@mui/material/DialogTitle'; 7 | import FormControlLabel from '@mui/material/FormControlLabel'; 8 | import Switch from '@mui/material/Switch'; 9 | import TextField from '@mui/material/TextField'; 10 | import useMediaQuery from '@mui/material/useMediaQuery'; 11 | 12 | import { useTheme } from '@mui/material/styles'; 13 | 14 | import { Global, GlobalHolder } from '../Global'; 15 | import { enableDropHandler } from '../UrlProcessor'; 16 | import { usePrevious } from '../Util'; 17 | 18 | import * as WrcCommon from '@webrcade/app-common'; 19 | 20 | const copyToClipboard = (text) => { 21 | const input = document.createElement('input'); 22 | input.value = text; 23 | 24 | // Critical for iOS Safari: 25 | input.setAttribute('readonly', ''); 26 | input.style.position = 'fixed'; 27 | input.style.top = '0'; 28 | input.style.left = '0'; 29 | input.style.opacity = '1'; // Not hidden 30 | input.style.zIndex = '-1'; // Visually non-disruptive 31 | input.style.height = '1px'; // Minimal size 32 | input.style.fontSize = '16px'; // iOS Safari bug: small font sizes can break selection 33 | 34 | document.body.appendChild(input); 35 | input.focus(); 36 | input.select(); 37 | 38 | try { 39 | const successful = document.execCommand('copy'); 40 | if (!successful) { 41 | throw new Error('Copy command failed'); 42 | } 43 | } catch (err) { 44 | console.warn('Copy failed', err); 45 | } 46 | 47 | document.body.removeChild(input); 48 | }; 49 | 50 | const minimizeLink = (location, copyLinkProps, setCopyLinkProps) => { 51 | const originalLocation = location; 52 | 53 | if (copyLinkProps.minLink) { 54 | return; 55 | } 56 | 57 | // Global.openBusyScreen(true, "Shortening URL...", true, false); 58 | new WrcCommon.FetchAppData(`https://tinyurl.com/api-create.php?url=${encodeURIComponent(location)}`).fetch() 59 | .then((res) => { 60 | if (res.ok) { 61 | return res.text(); 62 | } else { 63 | throw Error("Error attempting to shorten URL"); 64 | } 65 | }) 66 | .then((text) => { 67 | if (text.toLowerCase().indexOf("//tinyurl.com") !== -1) { 68 | location = text; 69 | } else { 70 | throw Error("Invalid response from tinyurl"); 71 | } 72 | }) 73 | .catch((err) => { 74 | WrcCommon.LOG.error(err); 75 | Global.displayMessage("An error occurred while attempting to shorten the URL.", "error"); 76 | }) 77 | .finally(() => { 78 | // Global.openBusyScreen(false); 79 | if (location !== originalLocation) { 80 | setCopyLinkProps({...copyLinkProps, minLink: location}); 81 | } 82 | }); 83 | } 84 | 85 | const CopyLinkDialog = (props) => { 86 | const [isOpen, setOpen] = React.useState(false); 87 | const prevOpen = usePrevious(isOpen); 88 | const [copyLinkProps, setCopyLinkProps] = React.useState({ link: "", checked: false }); 89 | const theme = useTheme(); 90 | const fullScreen = useMediaQuery(theme.breakpoints.down('md')); 91 | 92 | // Enable/disable the drop handler 93 | if (prevOpen && !isOpen) { 94 | enableDropHandler(true); 95 | } else if (!prevOpen && isOpen) { 96 | enableDropHandler(false); 97 | } 98 | 99 | GlobalHolder.setCopyLinkDialogOpen = setOpen; 100 | GlobalHolder.setCopyLinkDialogProps = setCopyLinkProps; 101 | 102 | const title = copyLinkProps.title; 103 | const success = copyLinkProps.success; 104 | const disabledShortened = copyLinkProps.disableShortened; 105 | 106 | const getLink = () => { 107 | return copyLinkProps.checked && copyLinkProps.minLink ? copyLinkProps.minLink : copyLinkProps.link; 108 | } 109 | 110 | return ( 111 | setOpen(false)} 114 | fullScreen={fullScreen} 115 | > 116 | {title ? title : "Copy Stand-alone Link"} 117 | 118 | {!disabledShortened && ( 119 |
120 | { 123 | const updatedProps = {...copyLinkProps, checked: e.target.checked}; 124 | setCopyLinkProps(updatedProps); 125 | if (e.target.checked) { 126 | minimizeLink(copyLinkProps.link, updatedProps, setCopyLinkProps) 127 | } 128 | }} 129 | /> 130 | } 131 | label={"Shortened URL"} 132 | /> 133 |
134 | )} 135 |
136 | {}} 139 | InputProps={{ 140 | disabled: true, 141 | }} 142 | sx={{ m: 1.5, width: { xs: '35ch', sm: '40ch' }, }} 143 | /> 144 |
145 |
146 | 147 | 159 | 162 | 163 |
164 | ); 165 | }; 166 | 167 | export default CopyLinkDialog; 168 | -------------------------------------------------------------------------------- /src/components/item-editor/c64/C64MappingOptions.js: -------------------------------------------------------------------------------- 1 | export const OPTIONS = [ 2 | { 3 | label: "Joystick Left", 4 | value: "joy-left" 5 | }, 6 | { 7 | label: "Joystick Right", 8 | value: "joy-right" 9 | }, 10 | { 11 | label: "Joystick Up", 12 | value: "joy-up" 13 | }, 14 | { 15 | label: "Joystick Down", 16 | value: "joy-down" 17 | }, 18 | { 19 | label: "Fire 1", 20 | value: "fire1" 21 | }, 22 | { 23 | label: "Fire 2", 24 | value: "fire2" 25 | }, 26 | { 27 | label: "F1", 28 | value: "f1" 29 | }, 30 | { 31 | label: "F2", 32 | value: "f2" 33 | }, 34 | { 35 | label: "F3", 36 | value: "f3" 37 | }, 38 | { 39 | label: "F4", 40 | value: "f4" 41 | }, 42 | { 43 | label: "F5", 44 | value: "f5" 45 | }, 46 | { 47 | label: "F6", 48 | value: "f6" 49 | }, 50 | { 51 | label: "F7", 52 | value: "f7" 53 | }, 54 | { 55 | label: "F8", 56 | value: "f8" 57 | }, 58 | { 59 | label: "Run/Stop", 60 | value: "runstop" 61 | }, 62 | { 63 | label: "Home", 64 | value: "home" 65 | }, 66 | { 67 | label: "Delete", 68 | value: "delete" 69 | }, 70 | { 71 | label: "Control", 72 | value: "control" 73 | }, 74 | { 75 | label: "Restore", 76 | value: "restore" 77 | }, 78 | { 79 | label: "Commodore Key", 80 | value: "commodore" 81 | }, 82 | { 83 | label: "Left Shift", 84 | value: "leftshift" 85 | }, 86 | { 87 | label: "Right Shift", 88 | value: "rightshift" 89 | }, 90 | { 91 | label: "Return", 92 | value: "return" 93 | }, 94 | { 95 | label: "Cursor Up", 96 | value: "cursorup" 97 | }, 98 | { 99 | label: "Cursor Down", 100 | value: "cursordown" 101 | }, 102 | { 103 | label: "Cursor Left", 104 | value: "cursorleft" 105 | }, 106 | { 107 | label: "Cursor Right", 108 | value: "cursorright" 109 | }, 110 | { 111 | label: "Space Bar", 112 | value: "space" 113 | }, 114 | { 115 | label: "A", 116 | value: "a" 117 | }, 118 | { 119 | label: "B", 120 | value: "b" 121 | }, 122 | { 123 | label: "C", 124 | value: "c" 125 | }, 126 | { 127 | label: "D", 128 | value: "d" 129 | }, 130 | { 131 | label: "E", 132 | value: "e" 133 | }, 134 | { 135 | label: "F", 136 | value: "f" 137 | }, 138 | { 139 | label: "G", 140 | value: "g" 141 | }, 142 | { 143 | label: "H", 144 | value: "h" 145 | }, 146 | { 147 | label: "I", 148 | value: "i" 149 | }, 150 | { 151 | label: "J", 152 | value: "j" 153 | }, 154 | { 155 | label: "K", 156 | value: "k" 157 | }, 158 | { 159 | label: "L", 160 | value: "l" 161 | }, 162 | { 163 | label: "M", 164 | value: "m" 165 | }, 166 | { 167 | label: "N", 168 | value: "n" 169 | }, 170 | { 171 | label: "O", 172 | value: "o" 173 | }, 174 | { 175 | label: "P", 176 | value: "p" 177 | }, 178 | { 179 | label: "Q", 180 | value: "q" 181 | }, 182 | { 183 | label: "R", 184 | value: "r" 185 | }, 186 | { 187 | label: "S", 188 | value: "s" 189 | }, 190 | { 191 | label: "T", 192 | value: "t" 193 | }, 194 | { 195 | label: "U", 196 | value: "u" 197 | }, 198 | { 199 | label: "V", 200 | value: "v" 201 | }, 202 | { 203 | label: "W", 204 | value: "w" 205 | }, 206 | { 207 | label: "X", 208 | value: "x" 209 | }, 210 | { 211 | label: "Y", 212 | value: "y" 213 | }, 214 | { 215 | label: "Z", 216 | value: "z" 217 | }, 218 | { 219 | label: "1", 220 | value: "1" 221 | }, 222 | { 223 | label: "2", 224 | value: "2" 225 | }, 226 | { 227 | label: "3", 228 | value: "3" 229 | }, 230 | { 231 | label: "4", 232 | value: "4" 233 | }, 234 | { 235 | label: "5", 236 | value: "5" 237 | }, 238 | { 239 | label: "6", 240 | value: "6" 241 | }, 242 | { 243 | label: "7", 244 | value: "7" 245 | }, 246 | { 247 | label: "8", 248 | value: "8" 249 | }, 250 | { 251 | label: "9", 252 | value: "9" 253 | }, 254 | { 255 | label: "0", 256 | value: "0" 257 | }, 258 | { 259 | label: "+", 260 | value: "plus" 261 | }, 262 | { 263 | label: "-", 264 | value: "minus" 265 | }, 266 | { 267 | label: "£", 268 | value: "pound" 269 | }, 270 | { 271 | label: "←", 272 | value: "leftarrow" 273 | }, 274 | { 275 | label: "↑", 276 | value: "uparrow" 277 | }, 278 | { 279 | label: "@", 280 | value: "at" 281 | }, 282 | { 283 | label: "*", 284 | value: "asterick" 285 | }, 286 | { 287 | label: ":", 288 | value: "colon" 289 | }, 290 | { 291 | label: ";", 292 | value: "semicolon" 293 | }, 294 | { 295 | label: "=", 296 | value: "equal" 297 | }, 298 | { 299 | label: ",", 300 | value: "comma" 301 | }, 302 | { 303 | label: ".", 304 | value: "period" 305 | }, 306 | { 307 | label: "/", 308 | value: "slash" 309 | }, 310 | ]; 311 | 312 | 313 | -------------------------------------------------------------------------------- /src/components/load-dialog/FeedsTable.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import DeleteIcon from '@mui/icons-material/Delete'; 3 | import FileOpenIcon from '@mui/icons-material/FileOpen'; 4 | import IconButton from '@mui/material/IconButton'; 5 | import TableCell from '@mui/material/TableCell'; 6 | import Tooltip from '@mui/material/Tooltip'; 7 | 8 | import * as WrcCommon from '@webrcade/app-common' 9 | import { Global } from '../../Global'; 10 | import CommonTable from '../common/CommonTable'; 11 | import * as Feed from '../../Feed'; 12 | import ImageLabel from '../common/ImageLabel'; 13 | import Prefs from '../../Prefs'; 14 | import ToolbarVerticalDivider from '../common/ToolbarVerticalDivider'; 15 | 16 | import EditorSwitch from '../common/editor/EditorSwitch'; 17 | 18 | function createData(id, title, thumbSrc, location, localId) { 19 | return { 20 | id, title, thumbSrc, location, localId 21 | }; 22 | } 23 | 24 | const PREF_SHOW_REMOTE = "loadFeeds.showRemote"; 25 | 26 | export default function FeedsTable(props) { 27 | const {feeds, setOpen, onDelete} = props; 28 | const [showRemote, setShowRemote] = 29 | React.useState(Prefs.getBoolPreference(PREF_SHOW_REMOTE, true)); 30 | 31 | const rows = []; 32 | if (feeds) { 33 | const distinctFeeds = feeds.getDistinctFeeds(); 34 | distinctFeeds.forEach((f) => { 35 | if (showRemote || !f.url) { 36 | rows.push(createData( 37 | f.feedId, f.title, f.thumbnail, f.url ? f.url : "", f.localId 38 | )) 39 | } 40 | }); 41 | } 42 | 43 | return ( 44 | { 77 | return ( 78 | <> 79 | 86 | 91 | 92 | 93 | 94 | { 95 | e.stopPropagation(); 96 | Global.openBusyScreen(true, "Loading feed..."); 97 | try { 98 | let feed = null; 99 | if (row.localId) { 100 | const localFeed = await feeds.getLocalFeed(row.localId); 101 | if (localFeed) { 102 | feed = await Feed.loadFeed(localFeed); 103 | } 104 | } else if (row.location && row.location.trim().length > 0) { 105 | feed = await Feed.loadFeedFromUrl(row.location); 106 | } 107 | if (feed) { 108 | Global.setFeed({...feed}); 109 | setOpen(false); 110 | } else { 111 | Global.displayMessage("The feed no longer exists.", "error"); 112 | } 113 | } catch (e) { 114 | Global.displayMessage("An error occurrred attempting to load the feed.", "error"); 115 | } finally { 116 | Global.openBusyScreen(false); 117 | } 118 | }} 119 | > 120 | 121 | 122 | 123 | 124 | {row.location ? row.location : "(Local)"} 125 | 126 | ); 127 | }} 128 | renderToolbarItems={(selection, selected) => { 129 | return ( 130 | <> 131 |
132 | { 136 | const checked = e.target.checked; 137 | setShowRemote(checked); 138 | Prefs.setPreference(PREF_SHOW_REMOTE, checked); 139 | }} 140 | checked={showRemote} 141 | /> 142 |
143 | 144 | 145 |
146 | { 149 | const single = selected.length === 1; 150 | Global.openConfirmDialog( 151 | true, 152 | `Delete Feed${single ? "" : "s"}`, 153 | `Are you sure you want to delete the selected feed${single ? "" : "s"}?`, 154 | async () => { 155 | try { 156 | Global.openBusyScreen(true, `Deleting feed${single ? "" : "s"}...`); 157 | for (let i = 0; i < selected.length; i++) { 158 | const id = selected[i]; 159 | await feeds.removeFeed(id); 160 | } 161 | if (onDelete) await onDelete(); 162 | } finally { 163 | Global.openBusyScreen(false); 164 | } 165 | } 166 | ); 167 | }} 168 | > 169 | 170 | 171 |
172 |
173 | 174 | 175 | ); 176 | }} 177 | /> 178 | ); 179 | } -------------------------------------------------------------------------------- /src/components/item-editor/gb/SelectPalette.css: -------------------------------------------------------------------------------- 1 | .palette { 2 | display: flex; 3 | flex-direction: row; 4 | flex-wrap: nowrap; 5 | align-items: middle; 6 | margin-top: 3px; 7 | margin-bottom: 3px; 8 | } 9 | 10 | .c1, .c2, .c3, .c4 { 11 | width: 16px; 12 | height: 16px; 13 | margin-right: 1px; 14 | } 15 | 16 | .gray0 > .c1 { background: #ffffff} 17 | .gray0 > .c2 { background: #acacac} 18 | .gray0 > .c3 { background: #626262} 19 | .gray0 > .c4 { background: #000000} 20 | 21 | .green0 > .c1 { background: #31E673 } 22 | .green0 > .c2 { background: #00B400 } 23 | .green0 > .c3 { background: #186A00 } 24 | .green0 > .c4 { background: #205200 } 25 | 26 | .green1 > .c1 { background: #EFFFE0 } 27 | .green1 > .c2 { background: #AED798 } 28 | .green1 > .c3 { background: #539274 } 29 | .green1 > .c4 { background: #193441 } 30 | 31 | .green2 > .c1 { background: #e0f8a0 } 32 | .green2 > .c2 { background: #78c838 } 33 | .green2 > .c3 { background: #488818 } 34 | .green2 > .c4 { background: #081800 } 35 | 36 | .green3 > .c1 { background: #9bbc0f } 37 | .green3 > .c2 { background: #8bac0f } 38 | .green3 > .c3 { background: #306230 } 39 | .green3 > .c4 { background: #0f380f } 40 | 41 | .green4 > .c1 { background: #00b581 } 42 | .green4 > .c2 { background: #009a71 } 43 | .green4 > .c3 { background: #00694a } 44 | .green4 > .c4 { background: #004f3b } 45 | 46 | .green5 > .c1 { background: #F8F8C8 } 47 | .green5 > .c2 { background: #B8C058 } 48 | .green5 > .c3 { background: #808840 } 49 | .green5 > .c4 { background: #405028 } 50 | 51 | .sgb1a > .c1 { background: #f8e8c8 } 52 | .sgb1a > .c2 { background: #d89048 } 53 | .sgb1a > .c3 { background: #a82820 } 54 | .sgb1a > .c4 { background: #301850 } 55 | 56 | .sgb1b > .c1 { background: #d8d8c0 } 57 | .sgb1b > .c2 { background: #c8b070 } 58 | .sgb1b > .c3 { background: #b05010 } 59 | .sgb1b > .c4 { background: #000000 } 60 | 61 | .sgb1c > .c1 { background: #f8c0f8 } 62 | .sgb1c > .c2 { background: #e89850 } 63 | .sgb1c > .c3 { background: #983860 } 64 | .sgb1c > .c4 { background: #383898 } 65 | 66 | .sgb1d > .c1 { background: #f8f8a8 } 67 | .sgb1d > .c2 { background: #c08048 } 68 | .sgb1d > .c3 { background: #f80000 } 69 | .sgb1d > .c4 { background: #501800 } 70 | 71 | .sgb1e > .c1 { background: #f8d8b0 } 72 | .sgb1e > .c2 { background: #78c078 } 73 | .sgb1e > .c3 { background: #688840 } 74 | .sgb1e > .c4 { background: #583820 } 75 | 76 | .sgb1f > .c1 { background: #d8e8f8 } 77 | .sgb1f > .c2 { background: #e08850 } 78 | .sgb1f > .c3 { background: #a80000 } 79 | .sgb1f > .c4 { background: #004010 } 80 | 81 | .sgb1g > .c1 { background: #000050 } 82 | .sgb1g > .c2 { background: #00a0e8 } 83 | .sgb1g > .c3 { background: #787800 } 84 | .sgb1g > .c4 { background: #f8f858 } 85 | 86 | .sgb1h > .c1 { background: #f8e8e0 } 87 | .sgb1h > .c2 { background: #f8b888 } 88 | .sgb1h > .c3 { background: #804000 } 89 | .sgb1h > .c4 { background: #301800 } 90 | 91 | .sgb2a > .c1 { background: #f0c8a0 } 92 | .sgb2a > .c2 { background: #c08848 } 93 | .sgb2a > .c3 { background: #287800 } 94 | .sgb2a > .c4 { background: #000000 } 95 | 96 | .sgb2b > .c1 { background: #f8f8f8 } 97 | .sgb2b > .c2 { background: #f8e850 } 98 | .sgb2b > .c3 { background: #f83000 } 99 | .sgb2b > .c4 { background: #500058 } 100 | 101 | .sgb2c > .c1 { background: #f8c0f8 } 102 | .sgb2c > .c2 { background: #e88888 } 103 | .sgb2c > .c3 { background: #7830e8 } 104 | .sgb2c > .c4 { background: #282898 } 105 | 106 | .sgb2d > .c1 { background: #f8f8a0 } 107 | .sgb2d > .c2 { background: #00f800 } 108 | .sgb2d > .c3 { background: #f83000 } 109 | .sgb2d > .c4 { background: #000050 } 110 | 111 | .sgb2e > .c1 { background: #f8c880 } 112 | .sgb2e > .c2 { background: #90b0e0 } 113 | .sgb2e > .c3 { background: #281060 } 114 | .sgb2e > .c4 { background: #100810 } 115 | 116 | .sgb2f > .c1 { background: #d0f8f8 } 117 | .sgb2f > .c2 { background: #f89050 } 118 | .sgb2f > .c3 { background: #a00000 } 119 | .sgb2f > .c4 { background: #180000 } 120 | 121 | .sgb2g > .c1 { background: #68b838 } 122 | .sgb2g > .c2 { background: #e05040 } 123 | .sgb2g > .c3 { background: #e0b880 } 124 | .sgb2g > .c4 { background: #001800 } 125 | 126 | .sgb2h > .c1 { background: #f8f8f8 } 127 | .sgb2h > .c2 { background: #b8b8b8 } 128 | .sgb2h > .c3 { background: #707070 } 129 | .sgb2h > .c4 { background: #000000 } 130 | 131 | .sgb3a > .c1 { background: #f8d098 } 132 | .sgb3a > .c2 { background: #70c0c0 } 133 | .sgb3a > .c3 { background: #f86028 } 134 | .sgb3a > .c4 { background: #304860 } 135 | 136 | .sgb3b > .c1 { background: #d8d8c0 } 137 | .sgb3b > .c2 { background: #e08020 } 138 | .sgb3b > .c3 { background: #005000 } 139 | .sgb3b > .c4 { background: #001010 } 140 | 141 | .sgb3c > .c1 { background: #e0a0c8 } 142 | .sgb3c > .c2 { background: #f8f878 } 143 | .sgb3c > .c3 { background: #00b8f8 } 144 | .sgb3c > .c4 { background: #202058 } 145 | 146 | .sgb3d > .c1 { background: #f0f8b8 } 147 | .sgb3d > .c2 { background: #e0a878 } 148 | .sgb3d > .c3 { background: #08c800 } 149 | .sgb3d > .c4 { background: #000000 } 150 | 151 | .sgb3e > .c1 { background: #f8f8c0 } 152 | .sgb3e > .c2 { background: #e0b068 } 153 | .sgb3e > .c3 { background: #b07820 } 154 | .sgb3e > .c4 { background: #504870 } 155 | 156 | .sgb3f > .c1 { background: #7878c8 } 157 | .sgb3f > .c2 { background: #f868f8 } 158 | .sgb3f > .c3 { background: #f8d000 } 159 | .sgb3f > .c4 { background: #404040 } 160 | 161 | .sgb3g > .c1 { background: #60d850 } 162 | .sgb3g > .c2 { background: #f8f8f8 } 163 | .sgb3g > .c3 { background: #c83038 } 164 | .sgb3g > .c4 { background: #380000 } 165 | 166 | .sgb3h > .c1 { background: #e0f8a0 } 167 | .sgb3h > .c2 { background: #78c838 } 168 | .sgb3h > .c3 { background: #488818 } 169 | .sgb3h > .c4 { background: #081800 } 170 | 171 | .sgb4a > .c1 { background: #f0a868 } 172 | .sgb4a > .c2 { background: #78a8f8 } 173 | .sgb4a > .c3 { background: #d000d0 } 174 | .sgb4a > .c4 { background: #000078 } 175 | 176 | .sgb4b > .c1 { background: #f0e8f0 } 177 | .sgb4b > .c2 { background: #e8a060 } 178 | .sgb4b > .c3 { background: #407838 } 179 | .sgb4b > .c4 { background: #180808 } 180 | 181 | .sgb4c > .c1 { background: #f8e0e0 } 182 | .sgb4c > .c2 { background: #d8a0d0 } 183 | .sgb4c > .c3 { background: #98a0e0 } 184 | .sgb4c > .c4 { background: #080000 } 185 | 186 | .sgb4d > .c1 { background: #f8f8b8 } 187 | .sgb4d > .c2 { background: #90c8c8 } 188 | .sgb4d > .c3 { background: #486878 } 189 | .sgb4d > .c4 { background: #082048 } 190 | 191 | .sgb4e > .c1 { background: #f8d8a8 } 192 | .sgb4e > .c2 { background: #e0a878 } 193 | .sgb4e > .c3 { background: #785888 } 194 | .sgb4e > .c4 { background: #002030 } 195 | 196 | .sgb4f > .c1 { background: #b8d0d0 } 197 | .sgb4f > .c2 { background: #d880d8 } 198 | .sgb4f > .c3 { background: #8000a0 } 199 | .sgb4f > .c4 { background: #380000 } 200 | 201 | .sgb4g > .c1 { background: #b0e018 } 202 | .sgb4g > .c2 { background: #b82058 } 203 | .sgb4g > .c3 { background: #281000 } 204 | .sgb4g > .c4 { background: #008060 } 205 | 206 | .sgb4h > .c1 { background: #F8F8C8 } 207 | .sgb4h > .c2 { background: #B8C058 } 208 | .sgb4h > .c3 { background: #808840 } 209 | .sgb4h > .c4 { background: #405028 } 210 | -------------------------------------------------------------------------------- /src/components/item-editor/a2600/A2600ControllersTab.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import MenuItem from '@mui/material/MenuItem'; 4 | import Select from '@mui/material/Select'; 5 | import Slider from '@mui/material/Slider'; 6 | import Stack from '@mui/material/Stack'; 7 | import Typography from '@mui/material/Typography'; 8 | 9 | import EditorTabPanel from '../../common/editor/EditorTabPanel'; 10 | import EditorSwitch from '../../common/editor/EditorSwitch'; 11 | import * as Util from '../../../Util'; 12 | 13 | const UNSET_TEMP = -9999; 14 | 15 | export function PaddleSensitivity(props) { 16 | const { value, onChange, onChangeCommitted } = props; 17 | // Allows for smoother updated prior to change being committed 18 | const [tempValue, setTempValue] = React.useState(UNSET_TEMP); 19 | 20 | return ( 21 | <> 22 | 23 | Paddle Sensitivity 24 | 25 | 30 | {onChange(e, val); setTempValue(val)}} 36 | onChangeCommitted={(e, val) => {onChangeCommitted(e, val); setTempValue(UNSET_TEMP)}} 37 | /> 38 | 39 | 40 | ); 41 | } 42 | 43 | export function PaddleCenter(props) { 44 | const { value, onChange, onChangeCommitted } = props; 45 | // Allows for smoother updated prior to change being committed 46 | const [tempValue, setTempValue] = React.useState(UNSET_TEMP); 47 | 48 | return ( 49 | <> 50 | 51 | Paddle Center 52 | 53 | 58 | {onChange(e, val); setTempValue(val)}} 64 | onChangeCommitted={(e, val) => {onChangeCommitted(e, val); setTempValue(UNSET_TEMP)}} 65 | /> 66 | 67 | 68 | ); 69 | } 70 | 71 | function ControllerType(props) { 72 | const { port, value, onChange } = props 73 | 74 | const valueOut = (value ? value : 0); 75 | 76 | return ( 77 | 87 | ); 88 | } 89 | 90 | export default function A2600ControllersTab(props) { 91 | const { 92 | tabValue, 93 | tabIndex, 94 | setObject, 95 | object 96 | } = props; 97 | 98 | // console.log(object) 99 | 100 | const port0 = object.props.port0; 101 | const port1 = object.props.port1; 102 | const hasPaddles = ((port0 && (port0 > 0)) || (port1 && (port1 > 0))); 103 | 104 | return ( 105 | 106 |
107 | 108 | Controller Ports 109 | 110 | 115 | { 117 | const props = { ...object.props, port0: parseInt(e.target.value) } 118 | setObject({ ...object, props }) 119 | }} 120 | /> 121 | { 123 | const props = { ...object.props, port1: parseInt(e.target.value) } 124 | setObject({ ...object, props }) 125 | }} 126 | /> 127 | 128 |
129 | {hasPaddles === true && ( 130 |
131 | { 134 | // Allows for smoother updated prior to change being committed 135 | object.props.paddleSensitivity = parseInt(val); 136 | }} 137 | onChangeCommitted={(e, val) => { 138 | const props = { ...object.props, paddleSensitivity: parseInt(val) } 139 | setObject({ ...object, props }) 140 | }} /> 141 |
142 | )} 143 | {hasPaddles === true && ( 144 |
145 | { 148 | // Allows for smoother updated prior to change being committed 149 | object.props.paddleCenter = parseInt(val); 150 | }} 151 | onChangeCommitted={(e, val) => { 152 | const props = { ...object.props, paddleCenter: parseInt(val) } 153 | setObject({ ...object, props }) 154 | }} /> 155 |
156 | )} 157 | {hasPaddles === true && ( 158 |
159 | { 163 | const props = { ...object.props, paddleVertical: e.target.checked } 164 | setObject({ ...object, props }) 165 | }} 166 | checked={Util.asBoolean(object.props.paddleVertical)} 167 | /> 168 |
169 | )} 170 | {hasPaddles === true && ( 171 |
172 | { 176 | const props = { ...object.props, paddleInverted: e.target.checked } 177 | setObject({ ...object, props }) 178 | }} 179 | checked={Util.asBoolean(object.props.paddleInverted)} 180 | /> 181 |
182 | )} 183 |
184 | { 188 | const props = { ...object.props, swap: e.target.checked } 189 | setObject({ ...object, props }) 190 | }} 191 | checked={Util.asBoolean(object.props.swap)} 192 | /> 193 |
194 |
195 | ); 196 | } 197 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import CssBaseline from '@mui/material/CssBaseline'; 4 | import Toolbar from '@mui/material/Toolbar'; 5 | 6 | import { 7 | applyIosNavBarHack, 8 | dropbox, 9 | isDev, 10 | removeIosNavBarHack, 11 | storagePersist, 12 | settings, 13 | AppProps, 14 | AppRegistry, 15 | AppScreen, 16 | Feed as CommonFeed, 17 | config, 18 | APP_FRAME_ID, 19 | LOG, 20 | isLocalhostOrHttps 21 | } from '@webrcade/app-common' 22 | 23 | import { Global, GlobalHolder } from './Global'; 24 | import { dropHandler } from './UrlProcessor'; 25 | import * as Feed from './Feed'; 26 | import * as Util from './Util'; 27 | import BusyScreen from './components/BusyScreen'; 28 | import Dialogs from './components/Screens'; 29 | import EditorDrawer from './components/EditorDrawer'; 30 | import FeedTabs from './components/FeedTabs'; 31 | import GameRegistry from './GameRegistry'; 32 | import MainAppBar from './components/MainAppBar'; 33 | import Prefs from './Prefs'; 34 | import SelectedFeed from './components/SelectedFeed'; 35 | import { buildFieldMap } from './components/item-editor/PropertiesTab'; 36 | 37 | const HASH_PLAY = "play"; 38 | const drawerWidth = 190; 39 | 40 | let currentApp = null; 41 | let popstateListener = null; 42 | let messageListener = null; 43 | 44 | function ignore(event) { event.preventDefault(); } 45 | 46 | const createPopstateHandler = () => { 47 | return (e) => { 48 | if (currentApp) { 49 | const iframe = document.getElementById(APP_FRAME_ID); 50 | if (iframe) { 51 | try { 52 | const content = iframe.contentWindow; 53 | if (content) { 54 | content.postMessage("exit", "*"); 55 | } 56 | } catch (e) { 57 | LOG.error(e); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | let popstateGlobal = null; 65 | 66 | const createMessageListener = (setApp) => { 67 | return (e) => { 68 | if (e.data === 'exitComplete') { 69 | setApp(null); 70 | } 71 | if (e.data === 'appExiting') { 72 | if (popstateGlobal) { 73 | popstateGlobal(); 74 | } 75 | } 76 | } 77 | }; 78 | 79 | function App(props) { 80 | const [feed, setFeed] = React.useState({}); 81 | const [app, setApp] = React.useState(null); 82 | const [editorHidden, setEditorHidden] = React.useState(false); 83 | const [started, setStarted] = React.useState(false); 84 | const previousApp = Util.usePrevious(app); 85 | const forceRefresh = Util.useForceUpdate(); 86 | 87 | GlobalHolder.forceRefresh = forceRefresh; 88 | 89 | if (previousApp && !app) { 90 | document.body.style.removeProperty('overflow'); 91 | removeIosNavBarHack(); 92 | } 93 | 94 | if (app && !previousApp) { 95 | setTimeout(() => { 96 | document.body.style.overflow = 'hidden'; 97 | }, 0); 98 | applyIosNavBarHack(); 99 | } 100 | 101 | currentApp = app; 102 | 103 | React.useEffect(() => { 104 | AppRegistry.instance.setAllowMultiThreaded(true); 105 | 106 | // Ask for long term storage 107 | storagePersist(); 108 | 109 | console.log("Application was loaded"); 110 | Global.openBusyScreen(true, "Preparing editor...", true); 111 | 112 | document.addEventListener("drop", dropHandler); 113 | document.addEventListener("dragenter", ignore); 114 | document.addEventListener("dragover", ignore); 115 | 116 | popstateListener = createPopstateHandler(); 117 | popstateGlobal = popstateListener; 118 | window.addEventListener("popstate", popstateListener, false); 119 | messageListener = createMessageListener(setApp); 120 | window.addEventListener("message", messageListener); 121 | 122 | // Clear hash if an app is not loaded 123 | const hash = window.location.href.indexOf('#'); 124 | 125 | if (!app && hash >= 0) { 126 | window.history.pushState(null, "", window.location.href.substring(0, hash)); 127 | } 128 | 129 | // Load settings 130 | settings.load().finally(() => { 131 | dropbox.checkLinkResult() 132 | .catch(e => { Global.displayMessage(e, "error") }) 133 | .finally(() => { 134 | // TODO: Hack for now. We need to build the field map after settings are 135 | // loaded. Maybe better to have an event sent when settings change, etc. 136 | // so that various components can react without direct bindings. 137 | buildFieldMap(); 138 | 139 | // Load prefs 140 | Prefs.load() 141 | .then(() => GameRegistry.init()) // Init the game registry 142 | .then(() => { 143 | const feed = Prefs.getFeed() 144 | if (feed) { 145 | // return feed from prefs 146 | const feedObj = new CommonFeed(feed, 0, false); 147 | const result = feedObj.getClonedFeed(); 148 | // Add ids? 149 | return result; 150 | } else { 151 | // load default feed (if applicable) 152 | return config.isPublicServer() ? 153 | Feed.loadFeedFromUrl(Feed.getDefaultFeedUrl(), false) : 154 | Feed.newFeed(); 155 | } 156 | }) 157 | .then((feed) => { 158 | setFeed(feed); 159 | }) 160 | .catch(e => { 161 | LOG.error("Error during startup: " + e); 162 | setFeed(Feed.newFeed()); // Set a new feed 163 | }) 164 | .finally(() => { 165 | // Mark as started 166 | setStarted(true); 167 | 168 | Global.openBusyScreen(false); 169 | }) 170 | }) 171 | }); 172 | 173 | return () => { 174 | console.log("Application was unloaded"); 175 | 176 | document.removeEventListener("drop", dropHandler); 177 | document.removeEventListener("dragenter", ignore); 178 | document.removeEventListener("dragover", ignore); 179 | 180 | window.removeEventListener("popstate", popstateListener, false); 181 | window.removeEventListener("message", messageListener); 182 | } 183 | // eslint-disable-next-line 184 | }, []); 185 | 186 | 187 | GlobalHolder.setApp = (app) => { 188 | const context = AppProps.RV_CONTEXT_EDITOR; 189 | const reg = AppRegistry.instance; 190 | let location = reg.getLocation(app, context, feed.props); 191 | if (!isDev() && context && context === AppProps.RV_CONTEXT_EDITOR) { 192 | location = "../../" + location; 193 | } 194 | if (reg.isMultiThreaded(app.type)) { 195 | if (!isLocalhostOrHttps()) { 196 | Global.displayMessage(`The ${reg.getName(app)} application requires HTTPS and the proper headers.`); 197 | return; 198 | } 199 | // multi-threaded 200 | window.open(location, "_blank"); 201 | } else { 202 | window.location.hash = HASH_PLAY; 203 | setEditorHidden(true); 204 | setApp(app); 205 | } 206 | } 207 | GlobalHolder.setFeed = setFeed; 208 | GlobalHolder.getFeed = () => { 209 | return feed; 210 | } 211 | 212 | return ( 213 | <> 214 | 215 | 216 | {started ? ( 217 | <> 218 | 223 | 224 | 225 | 226 | 227 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | {app ? ( 239 | { 244 | setEditorHidden(false); 245 | }} 246 | /> 247 | ) : null} 248 | 249 | ) : null} 250 | 251 | ); 252 | } 253 | 254 | export default App; -------------------------------------------------------------------------------- /src/components/tools/repackage/Repackage.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOG 3 | } from '@webrcade/app-common' 4 | 5 | import { GlobalHolder } from '../../../Global'; 6 | import GenerateManifestWrapper from '../../cloud/generate-manifest/GenerateManifestWrapper'; 7 | 8 | window.zip.configure({ useWebWorkers: false }); 9 | 10 | class BaseComponent { 11 | isPackage() { 12 | return false; 13 | } 14 | 15 | isFile() { 16 | return false; 17 | } 18 | } 19 | 20 | class PackageFile extends BaseComponent { 21 | constructor(entry) { 22 | super(); 23 | this.entry = entry; 24 | this.inPackage = false; 25 | } 26 | 27 | isFile() { 28 | return true; 29 | } 30 | 31 | getSize() { 32 | return this.entry.uncompressedSize; 33 | } 34 | 35 | getName() { 36 | return this.entry.filename; 37 | } 38 | 39 | isPackaged() { 40 | return this.inPackage; 41 | } 42 | 43 | setPackaged(v) { 44 | this.inPackage = v; 45 | } 46 | } 47 | 48 | class Package extends BaseComponent { 49 | constructor() { 50 | super(); 51 | this.files = []; 52 | } 53 | 54 | isPackage() { 55 | return true; 56 | } 57 | 58 | addFile(file) { 59 | this.files.push(file); 60 | } 61 | 62 | getFileCount() { 63 | return this.files.length; 64 | } 65 | 66 | getTotalSize() { 67 | let size = 0; 68 | for (let i = 0; i < this.files.length; i++) { 69 | size += this.files[i].getSize(); 70 | } 71 | return size; 72 | } 73 | 74 | async createPackage() { 75 | const { zip } = window; 76 | const blobWriter = new zip.BlobWriter("application/zip"); 77 | const zipWriter = new zip.ZipWriter(blobWriter); 78 | for (let i = 0; i < this.files.length; i++) { 79 | const f = this.files[i]; 80 | const writer = new zip.BlobWriter(); 81 | const blob = await f.entry.getData(writer, {}); 82 | await zipWriter.add(f.getName(), new zip.BlobReader(blob)); 83 | } 84 | return await zipWriter.close(); 85 | } 86 | } 87 | 88 | export default class Repackage { 89 | 90 | TARGET_SIZE = 50 * 1024 * 1024; // TODO: move to 50 91 | 92 | constructor() { 93 | this.files = []; 94 | this.packages = []; 95 | } 96 | 97 | buildPackages() { 98 | const { files, packages, TARGET_SIZE } = this; 99 | 100 | while (true) { 101 | let curPackage = new Package(); 102 | let curTotal = 0; 103 | for (let i = 0; i < files.length; i++) { 104 | const f = files[i]; 105 | if (f.isPackaged()) { 106 | continue; 107 | } 108 | const s = f.getSize(); 109 | if (s <= (TARGET_SIZE - curTotal)) { 110 | curTotal += s; 111 | curPackage.addFile(f); 112 | f.setPackaged(true); 113 | } 114 | } 115 | 116 | if (curPackage.getFileCount() === 0) { 117 | break; 118 | } else { 119 | packages.push(curPackage); 120 | curPackage = new Package(); 121 | curTotal = 0; 122 | } 123 | } 124 | } 125 | 126 | async repackage(file) { 127 | const { zip } = window; 128 | 129 | LOG.info("## Repackage: " + file.name); 130 | 131 | // Read entries 132 | const reader = new zip.ZipReader(new zip.BlobReader(file)); 133 | try { 134 | const entries = await reader.getEntries(); 135 | for (let i = 0; i < entries.length; i++) { 136 | const entry = entries[i]; 137 | if (!entry.directory) { 138 | const f = new PackageFile(entry); 139 | this.files.push(f); 140 | } 141 | } 142 | } finally { 143 | await reader.close(); 144 | } 145 | 146 | // Sort by size 147 | this.files.sort((a, b) => { 148 | return -(a.getSize() - b.getSize()); 149 | }); 150 | 151 | for (let i = 0; i < this.files.length; i++) { 152 | const f = this.files[i]; 153 | LOG.info(`${f.getName()} : ${Math.round((f.getSize() / (1024 * 1024)) * 100) / 100} mb`); 154 | } 155 | 156 | this.buildPackages(); 157 | 158 | const results = []; 159 | 160 | for (let i = 0; i < this.packages.length; i++) { 161 | const p = this.packages[i]; 162 | LOG.info(`Package ${i}: ${p.getTotalSize()}, ${p.getFileCount()} files.`); 163 | results.push(p); 164 | } 165 | 166 | for (let i = 0; i < this.files.length; i++) { 167 | const f = this.files[i]; 168 | if (!f.isPackaged()) { 169 | results.push(f); 170 | } 171 | } 172 | 173 | return results; 174 | } 175 | 176 | async createArchive(name, results) { 177 | const { zip } = window; 178 | 179 | const manifest = { 180 | props: { 181 | title: name 182 | } 183 | } 184 | 185 | const files = []; 186 | manifest.files = files; 187 | 188 | const zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip")); 189 | let packageIndex = 1; 190 | for (let i = 0; i < results.length; i++) { 191 | const r = results[i]; 192 | 193 | const f = { 194 | url: "" 195 | } 196 | if (r.isPackage()) { 197 | f.extract = true; 198 | const blob = await r.createPackage(); 199 | const name = `pak${packageIndex++}.zip`; 200 | f.name = name; 201 | await zipWriter.add(name, new zip.BlobReader(blob)); 202 | } else { 203 | f.name = r.getName(); 204 | const blob = await r.entry.getData(new zip.BlobWriter(), {}); 205 | await zipWriter.add(r.getName(), new zip.BlobReader(blob)); 206 | } 207 | files.push(f); 208 | } 209 | 210 | const manifestStr = JSON.stringify(manifest, null, 2); 211 | const manifestBytes = new TextEncoder().encode(manifestStr); 212 | const manifestBlob = new Blob([manifestBytes], { 213 | type: "application/json;charset=utf-8" 214 | }); 215 | await zipWriter.add("WRC-MANIFEST.JSON", new zip.BlobReader(manifestBlob)); 216 | 217 | return await zipWriter.close(); 218 | } 219 | 220 | // TODO: Method that returns path and file 221 | // TODO: Cloud method that will create dir, walks dirs and attempts to create 222 | // TODO: Cloud method that will write blob to destination 223 | // TODO: Once written, run the manifest generator on the root path 224 | 225 | getPath(rootPath, name) { 226 | let n = ""; 227 | let f = name; 228 | let idx = name.lastIndexOf("/"); 229 | if (idx !== -1) { 230 | n = name.substring(0, idx); 231 | f = name.substring(idx + 1); 232 | } 233 | 234 | if (n.length > 0) { 235 | if (!rootPath.endsWith("/")) { 236 | rootPath += "/"; 237 | } 238 | 239 | if (n.startsWith("/")) { 240 | n = n.substring(1); 241 | } 242 | } 243 | 244 | return [rootPath + n, f]; 245 | } 246 | 247 | async writeToCloud(model, results, rootPath, name, onSuccess) { 248 | const { zip } = window; 249 | const { storage } = model; 250 | 251 | let packageIndex = 1; 252 | for (let i = 0; i < results.length; i++) { 253 | const r = results[i]; 254 | 255 | let blob = null; 256 | let name = null; 257 | if (r.isPackage()) { 258 | blob = await r.createPackage(); 259 | name = `pak${packageIndex++}.zip`; 260 | } else { 261 | let retry = 5; 262 | while (retry-- > 0) { 263 | try { 264 | blob = await r.entry.getData(new zip.BlobWriter(), {}); 265 | break; 266 | } catch (e) { 267 | console.log(e); 268 | console.log("Retrying..."); 269 | } 270 | } 271 | name = r.getName(); 272 | } 273 | const path = this.getPath(rootPath, name); 274 | const fullPath = path[0] + (path[0].endsWith("/") ? "" : "/") + path[1]; 275 | console.log(fullPath); 276 | GlobalHolder.setBusyScreenMessage(`Uploading file ${i + 1} of ${results.length}...`); 277 | await storage.uploadFile(blob, fullPath); 278 | } 279 | 280 | GlobalHolder.setBusyScreenMessage(`Generating manifest file...`); 281 | 282 | await new GenerateManifestWrapper().generate( 283 | async () => { 284 | return await storage.generateManifest(rootPath, name); 285 | }, 286 | () => { 287 | onSuccess(); 288 | }, 289 | false 290 | ) 291 | } 292 | } 293 | --------------------------------------------------------------------------------