Initial commit
This commit is contained in:
commit
e2860fde14
|
@ -0,0 +1,10 @@
|
|||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1 @@
|
|||
/src/**/*.js
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
settings:
|
||||
import/core-modules:
|
||||
- electron
|
||||
import/resolver:
|
||||
node:
|
||||
extensions:
|
||||
- ".js"
|
||||
- ".jsx"
|
||||
- ".ts"
|
||||
- ".tsx"
|
||||
extends:
|
||||
- react-app
|
||||
- airbnb
|
||||
- plugin:jsx-a11y/recommended
|
||||
- prettier
|
||||
plugins:
|
||||
- jsx-a11y
|
||||
- prettier
|
||||
rules:
|
||||
semi:
|
||||
- error
|
||||
- always
|
||||
comma-dangle:
|
||||
- error
|
||||
- always-multiline
|
||||
react/jsx-filename-extension:
|
||||
- 1
|
||||
- extensions:
|
||||
- ".jsx"
|
||||
- ".tsx"
|
||||
import/extensions: 0
|
||||
react/react-in-jsx-scope: 0
|
||||
react/jsx-props-no-spreading: 0
|
||||
object-curly-spacing:
|
||||
- error
|
||||
- always
|
||||
quotes:
|
||||
- error
|
||||
- single
|
||||
no-console: 0
|
||||
no-plusplus: 0
|
||||
import/no-extraneous-dependencies: 0
|
||||
no-undef: 1
|
||||
react/require-default-props: 0
|
||||
no-unused-vars: 0
|
||||
'@typescript-eslint/no-unused-vars': 1
|
||||
no-shadow: 0
|
||||
'@typescript-eslint/no-shadow': 2
|
|
@ -0,0 +1,11 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
|
@ -0,0 +1,29 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production build
|
||||
/build
|
||||
/out
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.eslintcache
|
||||
|
||||
# custom ts builds
|
||||
/src/**/*.js
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"sonarsource.sonarlint-vscode",
|
||||
"davidanson.vscode-markdownlint"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Piotr Rogowski
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,20 @@
|
|||
# SpeedyTuner Cloud
|
||||
|
||||
Share your Speeduino tune and logs.
|
||||
|
||||
## Development
|
||||
|
||||
Recommended dev environment:
|
||||
|
||||
- [Node LTS](https://nodejs.org/) 14.x.x
|
||||
|
||||
```bash
|
||||
# install packages
|
||||
npm install
|
||||
|
||||
# run development server
|
||||
npm start
|
||||
|
||||
# open in browser
|
||||
open http://localhost:3000
|
||||
```
|
|
@ -0,0 +1,140 @@
|
|||
const path = require('path');
|
||||
const { getLoader, loaderByName, throwUnexpectedConfigError } = require('@craco/craco');
|
||||
|
||||
const overrideWebpackConfig = ({ context, webpackConfig, pluginOptions }) => {
|
||||
const throwError = (message, githubIssueQuery) =>
|
||||
throwUnexpectedConfigError({
|
||||
packageName: 'craco-less',
|
||||
githubRepo: 'DocSpring/craco-less',
|
||||
message,
|
||||
githubIssueQuery,
|
||||
});
|
||||
|
||||
const lessExtension = /\.less$/;
|
||||
const options = pluginOptions || {};
|
||||
const pathSep = path.sep;
|
||||
|
||||
const oneOfRule = webpackConfig.module.rules.find(rule => rule.oneOf);
|
||||
if (!oneOfRule) {
|
||||
throwError(
|
||||
'Can\'t find a \'oneOf\' rule under module.rules in the ' +
|
||||
`${context.env} webpack config!`,
|
||||
'webpack+rules+oneOf',
|
||||
);
|
||||
}
|
||||
|
||||
const sassRule = oneOfRule.oneOf.find(
|
||||
rule => rule.test && rule.test.toString().includes('scss|sass'),
|
||||
);
|
||||
if (!sassRule) {
|
||||
throwError(
|
||||
'Can\'t find the webpack rule to match scss/sass files in the ' +
|
||||
`${context.env} webpack config!`,
|
||||
'webpack+rules+scss+sass',
|
||||
);
|
||||
}
|
||||
let lessRule = {
|
||||
exclude: /\.module\.(less)$/,
|
||||
test: lessExtension,
|
||||
use: [],
|
||||
};
|
||||
|
||||
const loaders = sassRule.use;
|
||||
loaders.forEach(ruleOrLoader => {
|
||||
let rule;
|
||||
if (typeof ruleOrLoader === 'string') {
|
||||
rule = {
|
||||
loader: ruleOrLoader,
|
||||
options: {},
|
||||
};
|
||||
} else {
|
||||
rule = ruleOrLoader;
|
||||
}
|
||||
|
||||
if (
|
||||
(context.env === 'development' || context.env === 'test') &&
|
||||
rule.loader.includes(`${pathSep}style-loader${pathSep}`)
|
||||
) {
|
||||
lessRule.use.push({
|
||||
loader: rule.loader,
|
||||
options: {
|
||||
...rule.options,
|
||||
...(options.styleLoaderOptions || {}),
|
||||
},
|
||||
});
|
||||
} else if (rule.loader.includes(`${pathSep}css-loader${pathSep}`)) {
|
||||
lessRule.use.push({
|
||||
loader: rule.loader,
|
||||
options: {
|
||||
...rule.options,
|
||||
...(options.cssLoaderOptions || {}),
|
||||
},
|
||||
});
|
||||
} else if (rule.loader.includes(`${pathSep}postcss-loader${pathSep}`)) {
|
||||
lessRule.use.push({
|
||||
loader: rule.loader,
|
||||
options: {
|
||||
...rule.options,
|
||||
...(options.postcssLoaderOptions || {}),
|
||||
},
|
||||
});
|
||||
} else if (rule.loader.includes(`${pathSep}resolve-url-loader${pathSep}`)) {
|
||||
lessRule.use.push({
|
||||
loader: rule.loader,
|
||||
options: {
|
||||
...rule.options,
|
||||
...(options.resolveUrlLoaderOptions || {}),
|
||||
},
|
||||
});
|
||||
} else if (
|
||||
context.env === 'production' &&
|
||||
rule.loader.includes(`${pathSep}mini-css-extract-plugin${pathSep}`)
|
||||
) {
|
||||
lessRule.use.push({
|
||||
loader: rule.loader,
|
||||
options: {
|
||||
...rule.options,
|
||||
...(options.miniCssExtractPluginOptions || {}),
|
||||
},
|
||||
});
|
||||
} else if (rule.loader.includes(`${pathSep}sass-loader${pathSep}`)) {
|
||||
const defaultLessLoaderOptions =
|
||||
context.env === 'production' ? { sourceMap: true } : {};
|
||||
lessRule.use.push({
|
||||
loader: require.resolve('less-loader'),
|
||||
options: {
|
||||
...defaultLessLoaderOptions,
|
||||
...options.lessLoaderOptions,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throwError(
|
||||
`Found an unhandled loader in the ${context.env} webpack config: ${rule.loader}`,
|
||||
'webpack+unknown+rule',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (options.modifyLessRule) {
|
||||
lessRule = options.modifyLessRule(lessRule, context);
|
||||
}
|
||||
oneOfRule.oneOf.push(lessRule);
|
||||
|
||||
const { isFound, match: fileLoaderMatch } = getLoader(
|
||||
webpackConfig,
|
||||
loaderByName('file-loader'),
|
||||
);
|
||||
if (!isFound) {
|
||||
throwError(
|
||||
`Can't find file-loader in the ${context.env} webpack config!`,
|
||||
'webpack+file-loader',
|
||||
);
|
||||
}
|
||||
fileLoaderMatch.loader.exclude.push(lessExtension);
|
||||
|
||||
return webpackConfig;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
overrideWebpackConfig,
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
const CracoLessPlugin = require('./craco-less');
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
{
|
||||
plugin: CracoLessPlugin,
|
||||
options: {
|
||||
lessLoaderOptions: {
|
||||
lessOptions: {
|
||||
javascriptEnabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
makers: [
|
||||
{
|
||||
name: '@electron-forge/maker-dmg'
|
||||
},
|
||||
],
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"name": "speedy-tuner",
|
||||
"description": "Speeduino Tuning Software",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"homepage": "./",
|
||||
"main": "src/electron.js",
|
||||
"scripts": {
|
||||
"start": "craco start",
|
||||
"dev": "concurrently \"npm start\" \"wait-on http://localhost:3000 && electron .\"",
|
||||
"web:build": "craco build",
|
||||
"electron:compile": "tsc -p tsconfig.custom.json",
|
||||
"electron:start": "npm run electron:compile && electron-forge start",
|
||||
"electron:package": "npm run web:build && npm run electron:compile && electron-forge package",
|
||||
"electron:make": "npm run web:build && npm run electron:compile && electron-forge make",
|
||||
"ini:compile": "tsc -p tsconfig.custom.json",
|
||||
"ini:start": "npm run ini:compile && node src/parser/ini.js",
|
||||
"ini:watch": "npx onchange src/parser/ini.ts -- npm run ini:start",
|
||||
"test": "craco test",
|
||||
"lint": "eslint src/",
|
||||
"lint-fix": "eslint --fix src/",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.5.0",
|
||||
"antd": "^4.14.0",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"js-yaml": "^4.0.0 ",
|
||||
"mlg-converter": "^0.1.5",
|
||||
"parsimmon": "^1.16.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-perfect-scrollbar": "^1.5.8",
|
||||
"react-redux": "^7.2.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-table-drag-select": "^0.3.1",
|
||||
"recharts": "^2.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^6.1.1",
|
||||
"@electron-forge/cli": "^6.0.0-beta.54",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.54",
|
||||
"@electron-forge/maker-dmg": "^6.0.0-beta.54",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.54",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.54",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.54",
|
||||
"@types/js-yaml": "^4.0.0",
|
||||
"@types/node": "^14.14.35",
|
||||
"@types/parsimmon": "^1.10.6",
|
||||
"@types/react": "^17.0.3",
|
||||
"@types/react-dom": "^17.0.2",
|
||||
"@types/react-redux": "^7.1.16",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"concurrently": "^6.0.0",
|
||||
"electron": "^12.0.1",
|
||||
"eslint": "^7.22.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"less-loader": "^6.1.0",
|
||||
"prettier": "^2.2.1",
|
||||
"typescript": "^4.1.5",
|
||||
"wait-on": "^5.3.0"
|
||||
},
|
||||
"config": {
|
||||
"forge": {
|
||||
"packagerConfig": {
|
||||
"name": "SpeedyTuner",
|
||||
"icon": "./public/icons/icon"
|
||||
},
|
||||
"makers": [
|
||||
{
|
||||
"name": "@electron-forge/maker-squirrel",
|
||||
"config": {
|
||||
"name": "SpeedyTuner"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-zip",
|
||||
"platforms": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-deb",
|
||||
"config": {}
|
||||
},
|
||||
{
|
||||
"name": "@electron-forge/maker-rpm",
|
||||
"config": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 353 KiB |
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/icons/icon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#222629" />
|
||||
<meta name="description" content="SpeedyTuner" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/icons/icon.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>SpeedyTuner</title>
|
||||
</head>
|
||||
<body style="background-color: #222629;">
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"short_name": "SpeedyTuner",
|
||||
"name": "Speeduino Tuning Software",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon.ico",
|
||||
"type": "image/x-icon",
|
||||
"sizes": "256x256"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#222629",
|
||||
"background_color": "#222629"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow: /
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
declare module 'react-table-drag-select';
|
|
@ -0,0 +1,100 @@
|
|||
@import '~antd/dist/antd.less';
|
||||
// @import '~antd/dist/antd.compact.less';
|
||||
// @import './themes/light.less';
|
||||
@import './themes/dark.less';
|
||||
@import './themes/common.less';
|
||||
@import './themes/ant.less';
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.border-right {
|
||||
border-right-width: 1px;
|
||||
border-right-color: @border-color-split;
|
||||
border-right-style: solid;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.electron-draggable {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.electron-not-draggable {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.app-top-bar {
|
||||
.electron-draggable;
|
||||
// border-bottom-width: 1px;
|
||||
// border-bottom-color: @border-color-split;
|
||||
// border-bottom-style: solid;
|
||||
box-shadow: 0px 0px 20px 0px #0000001c, 5px 5px 15px 5px rgb(0 0 0 e('/') 4%);
|
||||
z-index: @bars-z-index;
|
||||
}
|
||||
|
||||
.app-sidebar {
|
||||
height: calc(100vh - @layout-header-height - @layout-footer-height);
|
||||
position: fixed;
|
||||
left: 0;
|
||||
user-select: none;
|
||||
// .border-right;
|
||||
}
|
||||
|
||||
.app-status-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: @layout-footer-height;
|
||||
font-size: @font-size-sm;
|
||||
color: @text-color;
|
||||
z-index: @bars-z-index;
|
||||
// border-top-width: 1px;
|
||||
// border-top-color: @border-color-split;
|
||||
// border-top-style: solid;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
height: calc(100vh - @layout-header-height - @layout-footer-height);
|
||||
}
|
||||
|
||||
.table {
|
||||
margin: 20px;
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
border: 1px solid @main-light;
|
||||
height: 50px;
|
||||
user-select: none;
|
||||
// transition: all 0.1s;
|
||||
|
||||
&.value {
|
||||
color: rgba(0, 0, 0, 1);
|
||||
width: 5%;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&.title-curve {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
&.title-map {
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
&.cell-selected {
|
||||
box-shadow: inset 0 0 25px 2px @info-color;
|
||||
}
|
||||
|
||||
&.cell-being-selected {
|
||||
box-shadow: inset 0 0 25px 2px @warning-color;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import {
|
||||
useLocation,
|
||||
Switch,
|
||||
Route,
|
||||
matchPath,
|
||||
Redirect,
|
||||
generatePath,
|
||||
} from 'react-router-dom';
|
||||
import { Layout, Result } from 'antd';
|
||||
import { connect } from 'react-redux';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import Dialog from './components/Dialog';
|
||||
import { loadAll } from './utils/api';
|
||||
import SideBar, { DialogMatchedPathType } from './components/SideBar';
|
||||
import { AppState, UIState } from './types/state';
|
||||
import BurnButton from './components/BurnButton';
|
||||
import TopBar from './components/TopBar';
|
||||
import StatusBar from './components/StatusBar';
|
||||
import { isDesktop } from './utils/env';
|
||||
import 'react-perfect-scrollbar/dist/css/styles.css';
|
||||
import './App.less';
|
||||
import { Routes } from './routes';
|
||||
import { Config as ConfigType } from './types/config';
|
||||
import { storageGet } from './utils/storage';
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
ui: state.ui,
|
||||
status: state.status,
|
||||
config: state.config,
|
||||
});
|
||||
|
||||
const App = ({ ui, config }: { ui: UIState, config: ConfigType }) => {
|
||||
const margin = ui.sidebarCollapsed ? 80 : 250;
|
||||
const { pathname } = useLocation();
|
||||
const dialogMatchedPath: DialogMatchedPathType = useMemo(() => matchPath(pathname, {
|
||||
path: Routes.DIALOG,
|
||||
exact: true,
|
||||
}) || { url: '', params: { category: '', dialog: '' } }, [pathname]);
|
||||
|
||||
const lastDialogPath = storageGet('lastDialog');
|
||||
const firstDialogPath = useMemo(() => {
|
||||
if (!config.menus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstCategory = Object.keys(config.menus)[0];
|
||||
const firstDialog = Object.keys(config.menus[firstCategory].subMenus)[0];
|
||||
return generatePath(Routes.DIALOG, { category: firstCategory, dialog: firstDialog });
|
||||
}, [config.menus]);
|
||||
|
||||
console.log({
|
||||
firstDialogPath,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadAll();
|
||||
// window.addEventListener('beforeunload', beforeUnload);
|
||||
|
||||
// return () => {
|
||||
// window.removeEventListener('beforeunload', beforeUnload);
|
||||
// };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Layout>
|
||||
<TopBar />
|
||||
<Switch>
|
||||
<Route path={Routes.ROOT} exact>
|
||||
<Redirect to="/tune" />
|
||||
</Route>
|
||||
<Route path={Routes.TUNE}>
|
||||
<Route path={Routes.TUNE} exact>
|
||||
{firstDialogPath && <Redirect to={lastDialogPath || firstDialogPath} />}
|
||||
</Route>
|
||||
<Layout style={{ marginLeft: margin }}>
|
||||
<SideBar matchedPath={dialogMatchedPath} />
|
||||
<Layout className="app-content">
|
||||
<Content>
|
||||
<PerfectScrollbar options={{ suppressScrollX: true }}>
|
||||
<Dialog
|
||||
name={dialogMatchedPath.params.dialog}
|
||||
url={dialogMatchedPath.url}
|
||||
burnButton={isDesktop && <BurnButton />}
|
||||
/>
|
||||
</PerfectScrollbar>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Route>
|
||||
<Route>
|
||||
<Result
|
||||
status="warning"
|
||||
title="There is nothing here"
|
||||
style={{ marginTop: 50 }}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Layout>
|
||||
<StatusBar />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(App);
|
|
@ -0,0 +1,23 @@
|
|||
import { Button, Grid } from 'antd';
|
||||
import { FireOutlined } from '@ant-design/icons';
|
||||
|
||||
const { useBreakpoint } = Grid;
|
||||
|
||||
const BurnButton = () => {
|
||||
const { md } = useBreakpoint();
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
danger
|
||||
htmlType="submit"
|
||||
icon={<FireOutlined />}
|
||||
style={{ position: 'fixed', right: 35, bottom: 45 }}
|
||||
>
|
||||
{md && 'Burn'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default BurnButton;
|
|
@ -0,0 +1,421 @@
|
|||
import { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
Form,
|
||||
Skeleton,
|
||||
Divider,
|
||||
Col,
|
||||
Row,
|
||||
Popover,
|
||||
Space,
|
||||
Result,
|
||||
} from 'antd';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { AppState } from '../types/state';
|
||||
import SmartSelect from './Dialog/SmartSelect';
|
||||
import SmartNumber from './Dialog/SmartNumber';
|
||||
import TextField from './Dialog/TextField';
|
||||
import Curve from './Dialog/Curve';
|
||||
import {
|
||||
Dialogs as DialogsType,
|
||||
Dialog as DialogType,
|
||||
Config as ConfigType,
|
||||
Field as FieldType,
|
||||
Curve as CurveType,
|
||||
Table as TableType,
|
||||
ScalarConstant as ScalarConstantType,
|
||||
ConstantTypes,
|
||||
} from '../types/config';
|
||||
import {
|
||||
Tune as TuneType,
|
||||
} from '../types/tune';
|
||||
import { findOnPage } from '../utils/config/find';
|
||||
import { parseXy, parseZ } from '../utils/tune/table';
|
||||
import Map from './Dialog/Map';
|
||||
import { evaluateExpression, isExpression } from '../utils/tune/expression';
|
||||
import { storageSet } from '../utils/storage';
|
||||
|
||||
interface DialogsAndCurves {
|
||||
[name: string]: DialogType | CurveType | TableType,
|
||||
}
|
||||
|
||||
interface RenderedPanel {
|
||||
type: string,
|
||||
name: string,
|
||||
title: string;
|
||||
labels: string[];
|
||||
xAxis: number[];
|
||||
yAxis: number[];
|
||||
xBins: string[];
|
||||
yBins: string[];
|
||||
size: number[];
|
||||
gauge?: string;
|
||||
fields: FieldType[],
|
||||
map: string;
|
||||
page: number;
|
||||
help?: string;
|
||||
xyLabels: string[];
|
||||
zBins: string[];
|
||||
gridHeight: number;
|
||||
gridOrient: number[];
|
||||
upDownLabel: string[];
|
||||
}
|
||||
|
||||
enum PanelTypes {
|
||||
FIELDS = 'fields',
|
||||
CURVE = 'curve',
|
||||
TABLE = 'table',
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
config: state.config,
|
||||
tune: state.tune,
|
||||
});
|
||||
|
||||
const containerStyle = {
|
||||
padding: 20,
|
||||
};
|
||||
|
||||
const skeleton = (<div style={containerStyle}>
|
||||
<Skeleton /><Skeleton />
|
||||
</div>);
|
||||
|
||||
// TODO: refactor this
|
||||
const Dialog = ({
|
||||
config,
|
||||
tune,
|
||||
url,
|
||||
name,
|
||||
burnButton,
|
||||
}: {
|
||||
config: ConfigType,
|
||||
tune: TuneType,
|
||||
name: string,
|
||||
url: string,
|
||||
burnButton: any
|
||||
}) => {
|
||||
const isDataReady = Object.keys(tune.constants).length && Object.keys(config.constants).length;
|
||||
|
||||
useEffect(() => {
|
||||
storageSet('lastDialog', url);
|
||||
}, [url]);
|
||||
|
||||
const renderHelp = (link?: string) => (link &&
|
||||
<Popover
|
||||
content={
|
||||
<a
|
||||
href={`${link}`}
|
||||
target="__blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{link}
|
||||
</a>
|
||||
}
|
||||
placement="right"
|
||||
>
|
||||
<QuestionCircleOutlined
|
||||
style={{ position: 'sticky', top: 15, zIndex: 1 }}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
const renderCurve = (curve: CurveType) => {
|
||||
const x = tune.constants[curve.xBins[0]];
|
||||
const y = tune.constants[curve.yBins[0]];
|
||||
const xConstant = findOnPage(config, curve.xBins[0]) as ScalarConstantType;
|
||||
const yConstant = findOnPage(config, curve.yBins[0]) as ScalarConstantType;
|
||||
|
||||
return (
|
||||
<Curve
|
||||
name={curve.yBins[0]}
|
||||
key={curve.yBins[0]}
|
||||
disabled={false} // TODO: evaluate condition
|
||||
help={config.help[curve.yBins[0]]}
|
||||
xLabel={curve.labels[0]}
|
||||
yLabel={curve.labels[1]}
|
||||
xUnits={xConstant.units}
|
||||
yUnits={yConstant.units}
|
||||
xMin={xConstant.min as number}
|
||||
xMax={xConstant.max as number}
|
||||
yMin={yConstant.min as number}
|
||||
yMax={yConstant.max as number}
|
||||
xData={parseXy(x.value as string)}
|
||||
yData={parseXy(y.value as string)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTable = (table: TableType | RenderedPanel) => {
|
||||
const x = tune.constants[table.xBins[0]];
|
||||
const y = tune.constants[table.yBins[0]];
|
||||
const z = tune.constants[table.zBins[0]];
|
||||
const zConstant = findOnPage(config, table.zBins[0]) as ScalarConstantType;
|
||||
|
||||
let max = zConstant.max as number;
|
||||
if (isExpression(zConstant.max)) {
|
||||
max = evaluateExpression(zConstant.max as string, tune.constants, config);
|
||||
}
|
||||
|
||||
let min = zConstant.min as number;
|
||||
if (isExpression(zConstant.min)) {
|
||||
min = evaluateExpression(zConstant.min as string, tune.constants, config);
|
||||
}
|
||||
|
||||
return <div>
|
||||
{renderHelp(table.help)}
|
||||
<Map
|
||||
name={table.map}
|
||||
key={table.map}
|
||||
xData={parseXy(x.value as string)}
|
||||
yData={parseXy(y.value as string).reverse()}
|
||||
zData={parseZ(z.value as string)}
|
||||
disabled={false}
|
||||
zMin={min}
|
||||
zMax={max}
|
||||
digits={zConstant.digits as number}
|
||||
xUnits={x.units as string}
|
||||
yUnits={y.units as string}
|
||||
/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
if (!isDataReady) {
|
||||
return skeleton;
|
||||
}
|
||||
|
||||
const dialogConfig = config.dialogs[name];
|
||||
const curveConfig = config.curves[name];
|
||||
const tableConfig = config.tables[name];
|
||||
|
||||
// standalone dialog / page
|
||||
if (!dialogConfig) {
|
||||
if (curveConfig) {
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<Divider>{curveConfig.title}</Divider>
|
||||
{renderCurve(curveConfig)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tableConfig) {
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
{renderHelp(tableConfig.help)}
|
||||
<Divider>{tableConfig.title}</Divider>
|
||||
{renderTable(tableConfig)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Result
|
||||
status="warning"
|
||||
title="Dialog not found"
|
||||
style={{ marginTop: 50 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const calculateSpan = (type: PanelTypes, dialogsCount: number) => {
|
||||
let xxl = 24;
|
||||
const xl = 24;
|
||||
|
||||
if (dialogsCount > 1 && type === PanelTypes.FIELDS) {
|
||||
xxl = 12;
|
||||
}
|
||||
|
||||
return {
|
||||
span: 24,
|
||||
xxl,
|
||||
xl,
|
||||
};
|
||||
};
|
||||
|
||||
const resolvedDialogs: DialogsAndCurves = {};
|
||||
|
||||
const resolveDialogs = (source: DialogsType, dialogName: string) => {
|
||||
if (!source[dialogName]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// resolve root dialog
|
||||
resolvedDialogs[dialogName] = source[dialogName];
|
||||
|
||||
Object.keys(source[dialogName].panels).forEach((panelName: string) => {
|
||||
const currentDialog = source[panelName];
|
||||
|
||||
if (!currentDialog) {
|
||||
// resolve 2D map / curve panel
|
||||
if (config.curves[panelName]) {
|
||||
resolvedDialogs[panelName] = {
|
||||
...config.curves[panelName],
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// resolve 3D map / table panel
|
||||
if (config.tables[panelName]) {
|
||||
resolvedDialogs[panelName] = {
|
||||
...config.tables[panelName],
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.info('Unable to resolve panel:', panelName);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentDialog.fields.length > 0) {
|
||||
// resolve in root scope
|
||||
resolvedDialogs[panelName] = config.dialogs[panelName];
|
||||
}
|
||||
|
||||
// recursion
|
||||
resolveDialogs(config.dialogs, panelName);
|
||||
});
|
||||
};
|
||||
|
||||
// TODO: refactor this
|
||||
resolveDialogs(config.dialogs, name);
|
||||
|
||||
// remove dummy dialogs and flatten to array
|
||||
const panels = Object.keys(resolvedDialogs).map((dialogName: string): RenderedPanel => {
|
||||
const currentDialog: DialogType | CurveType | TableType = resolvedDialogs[dialogName];
|
||||
let type = PanelTypes.CURVE;
|
||||
let fields: FieldType[] = [];
|
||||
|
||||
if ('fields' in currentDialog) {
|
||||
type = PanelTypes.FIELDS;
|
||||
fields = (currentDialog as DialogType)
|
||||
.fields
|
||||
.filter((field) => field.title !== '' );
|
||||
} else if ('zBins' in currentDialog) {
|
||||
type = PanelTypes.TABLE;
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
name: dialogName,
|
||||
title: currentDialog.title,
|
||||
fields,
|
||||
labels: (currentDialog as CurveType).labels,
|
||||
xAxis: (currentDialog as CurveType).xAxis,
|
||||
yAxis: (currentDialog as CurveType).yAxis,
|
||||
xBins: (currentDialog as CurveType).xBins,
|
||||
yBins: (currentDialog as CurveType).yBins,
|
||||
size: (currentDialog as CurveType).size,
|
||||
gauge: (currentDialog as CurveType).gauge,
|
||||
map: (currentDialog as TableType).map,
|
||||
page: (currentDialog as TableType).page,
|
||||
help: (currentDialog as TableType).help,
|
||||
xyLabels: (currentDialog as TableType).xyLabels,
|
||||
zBins: (currentDialog as TableType).zBins,
|
||||
gridHeight: (currentDialog as TableType).gridHeight,
|
||||
gridOrient: (currentDialog as TableType).gridOrient,
|
||||
upDownLabel: (currentDialog as TableType).upDownLabel,
|
||||
};
|
||||
});
|
||||
|
||||
const panelsComponents = () => panels.map((panel: RenderedPanel) => {
|
||||
if (panel.type === PanelTypes.FIELDS && panel.fields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Col key={panel.name} {...calculateSpan(panel.type as PanelTypes, panels.length)}>
|
||||
<Divider>{panel.title}</Divider>
|
||||
{(panel.fields).map((field: FieldType) => {
|
||||
const constant = findOnPage(config, field.name);
|
||||
const tuneField = tune.constants[field.name];
|
||||
const help = config.help[field.name];
|
||||
let input;
|
||||
let enabled = true;
|
||||
|
||||
if (field.condition) {
|
||||
enabled = evaluateExpression(field.condition, tune.constants, config);
|
||||
}
|
||||
|
||||
if (field.name === '_fieldText_' && enabled) {
|
||||
return <TextField key={`${panel.name}-${field.title}`} title={field.title} />;
|
||||
}
|
||||
|
||||
if (!tuneField) {
|
||||
// TODO: handle this?
|
||||
// name: "rpmwarn", title: "Warning",
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (constant.type) {
|
||||
// case ConstantTypes.ARRAY: // TODO: arrays
|
||||
case ConstantTypes.BITS:
|
||||
input = <SmartSelect
|
||||
defaultValue={`${tuneField.value}`}
|
||||
values={constant.values as string[]}
|
||||
disabled={!enabled}
|
||||
/>;
|
||||
break;
|
||||
|
||||
case ConstantTypes.SCALAR:
|
||||
input = <SmartNumber
|
||||
defaultValue={Number(tuneField.value)}
|
||||
digits={(constant as ScalarConstantType).digits}
|
||||
min={((constant as ScalarConstantType).min as number) || 0}
|
||||
max={(constant as ScalarConstantType).max as number}
|
||||
disabled={!enabled}
|
||||
units={(constant as ScalarConstantType).units}
|
||||
/>;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item
|
||||
key={field.name}
|
||||
label={
|
||||
<Space>
|
||||
{field.title}
|
||||
{help && (<Popover content={
|
||||
help.split('\\n').map((line) => <div key={line}>{line}</div>)
|
||||
}>
|
||||
<QuestionCircleOutlined />
|
||||
</Popover>)}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{input}
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
|
||||
{panel.type === PanelTypes.CURVE && renderCurve(panel)}
|
||||
|
||||
{panel.type === PanelTypes.TABLE && renderTable(panel)}
|
||||
</Col>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
{renderHelp(dialogConfig?.help)}
|
||||
<Form
|
||||
labelCol={{ span: 10 }}
|
||||
wrapperCol={{ span: 10 }}
|
||||
>
|
||||
<Row gutter={20}>
|
||||
{isDataReady && panelsComponents()}
|
||||
</Row>
|
||||
<Form.Item>
|
||||
{burnButton}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(Dialog);
|
|
@ -0,0 +1,128 @@
|
|||
import { Typography } from 'antd';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Label,
|
||||
} from 'recharts';
|
||||
import Table from './Table';
|
||||
|
||||
const Curve = ({
|
||||
name,
|
||||
xLabel,
|
||||
yLabel,
|
||||
xData,
|
||||
yData,
|
||||
disabled,
|
||||
help,
|
||||
xMin,
|
||||
xMax,
|
||||
yMin,
|
||||
yMax,
|
||||
xUnits = '',
|
||||
yUnits = '',
|
||||
}: {
|
||||
name: string,
|
||||
xLabel: string,
|
||||
yLabel: string,
|
||||
xData: number[],
|
||||
yData: number[],
|
||||
disabled: boolean,
|
||||
help: string,
|
||||
xMin: number,
|
||||
xMax: number,
|
||||
yMin: number,
|
||||
yMax: number,
|
||||
xUnits?: string,
|
||||
yUnits?: string,
|
||||
}) => {
|
||||
const mapData = (rawData: number[][]) => rawData[1].map((val, i) => ({
|
||||
x: val,
|
||||
y: rawData[0][i],
|
||||
}));
|
||||
const [data, setData] = useState(mapData([yData, xData]));
|
||||
const margin = 15;
|
||||
const mainColor = '#ccc';
|
||||
const tooltipBg = '#2E3338';
|
||||
const animationDuration = 500;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography.Paragraph>
|
||||
<Typography.Text type="secondary">{help}</Typography.Text>
|
||||
</Typography.Paragraph>
|
||||
<ResponsiveContainer height={450}>
|
||||
<LineChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: margin,
|
||||
right: margin,
|
||||
left: margin,
|
||||
bottom: margin + 5,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid
|
||||
strokeDasharray="4 4"
|
||||
strokeOpacity={0.1}
|
||||
/>
|
||||
<XAxis dataKey="x">
|
||||
<Label
|
||||
value={`${xLabel} (${xUnits})`}
|
||||
position="bottom"
|
||||
style={{ fill: mainColor }}
|
||||
/>
|
||||
</XAxis>
|
||||
<YAxis domain={['auto', 'auto']}>
|
||||
<Label
|
||||
value={`${yLabel} (${yUnits})`}
|
||||
position="left"
|
||||
angle={-90}
|
||||
style={{ fill: mainColor }}
|
||||
/>
|
||||
</YAxis>
|
||||
<Tooltip
|
||||
labelFormatter={(value) => `${xLabel} : ${value} ${xUnits}`}
|
||||
formatter={(value: number) => [`${value} ${yUnits}`, yLabel]}
|
||||
contentStyle={{
|
||||
backgroundColor: tooltipBg,
|
||||
border: 0,
|
||||
boxShadow: '0 3px 6px -4px rgb(0 0 0 / 12%), 0 6px 16px 0 rgb(0 0 0 / 8%), 0 9px 28px 8px rgb(0 0 0 / 5%)',
|
||||
borderRadius: 5,
|
||||
}}
|
||||
animationDuration={animationDuration}
|
||||
/>
|
||||
<Line
|
||||
strokeWidth={3}
|
||||
type="linear"
|
||||
dataKey="y"
|
||||
stroke="#1e88ea"
|
||||
animationDuration={animationDuration}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
<Table
|
||||
name={name}
|
||||
key={name}
|
||||
xLabel={xLabel}
|
||||
yLabel={yLabel}
|
||||
xData={xData}
|
||||
yData={yData}
|
||||
disabled={disabled}
|
||||
xMin={xMin}
|
||||
xMax={xMax}
|
||||
yMin={yMin}
|
||||
yMax={yMax}
|
||||
xUnits={xUnits}
|
||||
yUnits={yUnits}
|
||||
onChange={(newData: number[][]) => setData(mapData(newData))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Curve;
|
|
@ -0,0 +1,276 @@
|
|||
/* eslint-disable react/no-array-index-key */
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Button,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popover,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusCircleOutlined,
|
||||
MinusCircleOutlined,
|
||||
EditOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import TableDragSelect from 'react-table-drag-select';
|
||||
import {
|
||||
isDecrement,
|
||||
isEscape,
|
||||
isIncrement,
|
||||
isReplace,
|
||||
} from '../../utils/keyboard/shortcuts';
|
||||
|
||||
type CellsType = boolean[][];
|
||||
type DataType = number[][];
|
||||
type OnChangeType = (data: DataType) => void;
|
||||
enum Operations {
|
||||
INC,
|
||||
DEC,
|
||||
REPLACE,
|
||||
}
|
||||
type HslType = [number, number, number];
|
||||
|
||||
const Map = ({
|
||||
name,
|
||||
xData,
|
||||
yData,
|
||||
zData,
|
||||
disabled,
|
||||
onChange,
|
||||
zMin,
|
||||
zMax,
|
||||
digits,
|
||||
xUnits,
|
||||
yUnits,
|
||||
}: {
|
||||
name: string,
|
||||
xData: number[],
|
||||
yData: number[],
|
||||
zData: number[][],
|
||||
disabled: boolean,
|
||||
onChange?: OnChangeType,
|
||||
zMin: number,
|
||||
zMax: number,
|
||||
digits: number,
|
||||
xUnits: string,
|
||||
yUnits: string,
|
||||
}) => {
|
||||
const titleProps = { disabled: true };
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [modalValue, setModalValue] = useState<number | undefined>();
|
||||
const [data, _setData] = useState<DataType>(zData);
|
||||
const generateCells = () => Array.from(
|
||||
Array(yData.length + 1).fill(false),
|
||||
() => new Array(xData.length + 1).fill(false),
|
||||
);
|
||||
const [cells, _setCells] = useState<CellsType>(generateCells());
|
||||
const cellsRef = useRef(cells);
|
||||
const dataRef = useRef(data);
|
||||
const modalInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const setCells = (currentCells: CellsType) => {
|
||||
cellsRef.current = currentCells;
|
||||
_setCells(currentCells);
|
||||
};
|
||||
const setData = (currentData: DataType) => {
|
||||
dataRef.current = currentData;
|
||||
_setData(currentData);
|
||||
if (onChange) {
|
||||
onChange(currentData);
|
||||
}
|
||||
};
|
||||
const modifyData = (operation: Operations, currentCells: CellsType, currentData: DataType, value = 0): DataType => {
|
||||
const newData = [...currentData.map((row) => [...row])];
|
||||
currentCells.forEach((_, rowIndex) => {
|
||||
currentCells[rowIndex].forEach((selected, valueIndex) => {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = newData[rowIndex][valueIndex - 1];
|
||||
switch (operation) {
|
||||
case Operations.INC:
|
||||
if (current < zMax) {
|
||||
newData[rowIndex][valueIndex - 1] = Number((newData[rowIndex][valueIndex - 1] + 10**-digits).toFixed(digits));
|
||||
}
|
||||
break;
|
||||
case Operations.DEC:
|
||||
if (current > zMin) {
|
||||
newData[rowIndex][valueIndex - 1] = Number((newData[rowIndex][valueIndex - 1] - 10**-digits).toFixed(digits));
|
||||
}
|
||||
break;
|
||||
case Operations.REPLACE:
|
||||
if (value > zMax) {
|
||||
newData[rowIndex][valueIndex - 1] = zMax;
|
||||
break;
|
||||
}
|
||||
if (value < zMin) {
|
||||
newData[rowIndex][valueIndex - 1] = zMin;
|
||||
break;
|
||||
}
|
||||
newData[rowIndex][valueIndex - 1] = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return [...newData];
|
||||
};
|
||||
|
||||
const oneModalOk = () => {
|
||||
setData(modifyData(Operations.REPLACE, cellsRef.current, dataRef.current, modalValue));
|
||||
setIsModalVisible(false);
|
||||
setModalValue(undefined);
|
||||
};
|
||||
const onModalCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
setModalValue(undefined);
|
||||
};
|
||||
const resetCells = () => setCells(generateCells());
|
||||
const increment = () => setData(modifyData(Operations.INC, cellsRef.current, dataRef.current));
|
||||
const decrement = () => setData(modifyData(Operations.DEC, cellsRef.current, dataRef.current));
|
||||
const replace = () => {
|
||||
// don't show modal when no cell is selected
|
||||
if (cellsRef.current.flat().find((val) => val === true)) {
|
||||
setModalValue(undefined);
|
||||
setIsModalVisible(true);
|
||||
setInterval(() => modalInputRef.current?.focus(), 1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const keyboardListener = (e: KeyboardEvent) => {
|
||||
|
||||
if (isIncrement(e)) {
|
||||
increment();
|
||||
}
|
||||
if (isDecrement(e)) {
|
||||
decrement();
|
||||
}
|
||||
if (isReplace(e)) {
|
||||
e.preventDefault();
|
||||
replace();
|
||||
}
|
||||
if (isEscape(e)) {
|
||||
resetCells();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', keyboardListener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyboardListener);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const colorHsl = (min: number, max: number, value: number): HslType => {
|
||||
const saturation = 60;
|
||||
const lightness = 40;
|
||||
const coldDeg = 220;
|
||||
const hotDeg = 0;
|
||||
const remap = (x: number, inMin: number, inMax: number, outMin: number, outMax: number) => (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
|
||||
|
||||
let hue = remap(value, min, max, coldDeg, hotDeg);
|
||||
|
||||
// fallback to cold temp
|
||||
if (Number.isNaN(hue)) {
|
||||
hue = coldDeg;
|
||||
}
|
||||
|
||||
return [hue, saturation, lightness];
|
||||
};
|
||||
|
||||
const min = Math.min(...data.map((row) => Math.min(...row)));
|
||||
const max = Math.max(...data.map((row) => Math.max(...row)));
|
||||
|
||||
const renderRow = (rowIndex: number, input: number[]) => input
|
||||
.map((value, index) => {
|
||||
const [hue, sat, light] = colorHsl(min, max, value);
|
||||
const yValue = yData[rowIndex];
|
||||
const result = [];
|
||||
|
||||
if (index === 0) {
|
||||
result.push((
|
||||
<td {...titleProps} className="title-map" key={`y-${yValue}`}>
|
||||
{`${yValue}`}
|
||||
</td>
|
||||
));
|
||||
}
|
||||
|
||||
result.push((
|
||||
<td
|
||||
className="value"
|
||||
key={`${rowIndex}-${index}-${value}-${hue}${sat}${light}`}
|
||||
style={{ backgroundColor: `hsl(${hue}, ${sat}%, ${light}%)` }}
|
||||
>
|
||||
{`${value}`}
|
||||
</td>
|
||||
));
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="table">
|
||||
<Popover
|
||||
visible={cells.flat().find((val) => val === true) === true}
|
||||
content={
|
||||
<Space>
|
||||
<Button onClick={decrement} icon={<MinusCircleOutlined />} />
|
||||
<Button onClick={increment} icon={<PlusCircleOutlined />} />
|
||||
<Button onClick={replace} icon={<EditOutlined />} />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<TableDragSelect
|
||||
key={name}
|
||||
value={cells}
|
||||
onChange={setCells}
|
||||
>
|
||||
{data.map((row, i) => (
|
||||
<tr key={`row-${i}`}>
|
||||
{renderRow(i, row)}
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
<td {...titleProps} className="title-map">
|
||||
{yUnits} / {xUnits}
|
||||
</td>
|
||||
{xData.map((xValue) => (
|
||||
<td {...titleProps} key={`x-${xValue}`}>
|
||||
{`${xValue}`}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
</TableDragSelect>
|
||||
</Popover>
|
||||
</div>
|
||||
<Modal
|
||||
title="Set cell values"
|
||||
visible={isModalVisible}
|
||||
onOk={oneModalOk}
|
||||
onCancel={onModalCancel}
|
||||
centered
|
||||
forceRender
|
||||
>
|
||||
<InputNumber
|
||||
ref={modalInputRef}
|
||||
value={modalValue}
|
||||
onChange={(val) => setModalValue(Number(val))}
|
||||
onPressEnter={oneModalOk}
|
||||
style={{ width: '20%' }}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Map;
|
|
@ -0,0 +1,65 @@
|
|||
import {
|
||||
InputNumber,
|
||||
Slider,
|
||||
} from 'antd';
|
||||
|
||||
const SmartNumber = ({
|
||||
defaultValue,
|
||||
min,
|
||||
max,
|
||||
units,
|
||||
digits,
|
||||
disabled,
|
||||
}: {
|
||||
defaultValue: number,
|
||||
min: number,
|
||||
max: number,
|
||||
units: string | undefined,
|
||||
digits: number,
|
||||
disabled: boolean,
|
||||
}) => {
|
||||
const isSlider = (u: string) => ['%', 'C']
|
||||
.includes(`${u}`.toUpperCase());
|
||||
const sliderMarks: { [value: number]: string } = {};
|
||||
sliderMarks[min] = `${min}${units}`;
|
||||
|
||||
if (min <= 0) {
|
||||
sliderMarks[0] = `0${units}`;
|
||||
}
|
||||
|
||||
if (max) {
|
||||
sliderMarks[max] = `${max}${units}`;
|
||||
}
|
||||
|
||||
if (isSlider(units || '')) {
|
||||
return (
|
||||
<Slider
|
||||
defaultValue={defaultValue}
|
||||
min={min}
|
||||
max={max}
|
||||
step={10**-digits}
|
||||
disabled={disabled}
|
||||
marks={sliderMarks}
|
||||
tipFormatter={(val) => `${val}${units}`}
|
||||
// tooltipVisible
|
||||
// tooltipPlacement="bottom"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InputNumber
|
||||
defaultValue={defaultValue}
|
||||
precision={digits}
|
||||
min={min}
|
||||
max={max}
|
||||
step={10**-digits}
|
||||
disabled={disabled}
|
||||
style={{ minWidth: 150 }}
|
||||
formatter={(val) => units ? `${val} ${units}` : `${val}`}
|
||||
parser={(val) => Number(`${val}`.replace(/[^\d.]/g, ''))}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmartNumber;
|
|
@ -0,0 +1,68 @@
|
|||
import {
|
||||
Radio,
|
||||
Select,
|
||||
Switch,
|
||||
} from 'antd';
|
||||
import {
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Switches } from '../../types/config';
|
||||
|
||||
const SmartSelect = ({
|
||||
values,
|
||||
defaultValue,
|
||||
disabled,
|
||||
}: {
|
||||
values: string[],
|
||||
defaultValue: string,
|
||||
disabled: boolean,
|
||||
}) => {
|
||||
|
||||
if (values.length === 2
|
||||
&& (
|
||||
(values.includes(Switches.YES) && values.includes(Switches.NO)) ||
|
||||
(values.includes(Switches.ON) && values.includes(Switches.OFF))
|
||||
)
|
||||
) {
|
||||
return <Switch
|
||||
defaultChecked={[Switches.ON, Switches.YES].includes(defaultValue as Switches)}
|
||||
checkedChildren={<CheckOutlined />}
|
||||
unCheckedChildren={<CloseOutlined />}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (values.length < 3) {
|
||||
return (
|
||||
<Radio.Group
|
||||
defaultValue={values.indexOf(defaultValue)}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
disabled={disabled}
|
||||
>
|
||||
{values.map((val: string, index) =>
|
||||
<Radio key={val} value={index}>{val}</Radio>,
|
||||
)}
|
||||
</Radio.Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Select
|
||||
defaultValue={values.indexOf(defaultValue)}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
disabled={disabled}
|
||||
style={{ maxWidth: 250 }}
|
||||
>
|
||||
{/* we need to preserve indexes here, skip INVALID option */}
|
||||
{values.map((val: string, index) =>
|
||||
val === 'INVALID'
|
||||
? null
|
||||
: <Select.Option key={val} value={index} label={val}>{val}</Select.Option>,
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmartSelect;
|
|
@ -0,0 +1,277 @@
|
|||
/* eslint-disable react/no-array-index-key */
|
||||
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Button,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popover,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusCircleOutlined,
|
||||
MinusCircleOutlined,
|
||||
EditOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import TableDragSelect from 'react-table-drag-select';
|
||||
import {
|
||||
isDecrement,
|
||||
isEscape,
|
||||
isIncrement,
|
||||
isReplace,
|
||||
} from '../../utils/keyboard/shortcuts';
|
||||
|
||||
type AxisType = 'x' | 'y';
|
||||
type CellsType = boolean[][];
|
||||
type DataType = number[][];
|
||||
type OnChangeType = (data: DataType) => void;
|
||||
enum Operations {
|
||||
INC,
|
||||
DEC,
|
||||
REPLACE,
|
||||
}
|
||||
type HslType = [number, number, number];
|
||||
|
||||
const Table = ({
|
||||
name,
|
||||
xLabel,
|
||||
yLabel,
|
||||
xData,
|
||||
yData,
|
||||
disabled,
|
||||
onChange,
|
||||
xMin,
|
||||
xMax,
|
||||
yMin,
|
||||
yMax,
|
||||
xUnits,
|
||||
yUnits,
|
||||
}: {
|
||||
name: string,
|
||||
xLabel: string,
|
||||
yLabel: string,
|
||||
xData: number[],
|
||||
yData: number[],
|
||||
disabled: boolean,
|
||||
onChange?: OnChangeType,
|
||||
xMin: number,
|
||||
xMax: number,
|
||||
yMin: number,
|
||||
yMax: number,
|
||||
xUnits: string,
|
||||
yUnits: string,
|
||||
}) => {
|
||||
const titleProps = { disabled: true };
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [modalValue, setModalValue] = useState<number | undefined>();
|
||||
const [data, _setData] = useState<DataType>([yData, xData]);
|
||||
// data starts from `1` index, 0 is title / name
|
||||
const rowsCount = data[1].length + 1;
|
||||
const generateCells = () => [
|
||||
Array(rowsCount).fill(false),
|
||||
Array(rowsCount).fill(false),
|
||||
];
|
||||
const [cells, _setCells] = useState<CellsType>(generateCells());
|
||||
const cellsRef = useRef(cells);
|
||||
const dataRef = useRef(data);
|
||||
const modalInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const setCells = (currentCells: CellsType) => {
|
||||
cellsRef.current = currentCells;
|
||||
_setCells(currentCells);
|
||||
};
|
||||
const setData = (currentData: DataType) => {
|
||||
dataRef.current = currentData;
|
||||
_setData(currentData);
|
||||
if (onChange) {
|
||||
onChange(currentData);
|
||||
}
|
||||
};
|
||||
const modifyData = useCallback((operation: Operations, currentCells: CellsType, currentData: DataType, value = 0): DataType => {
|
||||
const newData = [...currentData.map((row) => [...row])];
|
||||
// rowIndex: [0 => Y, 1 => X]
|
||||
const isY = (row: number) => row === 0;
|
||||
const isX = (row: number) => row === 1;
|
||||
const isNotGreater = (row: number, val: number) => (isY(row) && val < yMax) || (isX(row) && val < xMax);
|
||||
const isNotLess = (row: number, val: number) => (isY(row) && val > yMin) || (isX(row) && val > xMin);
|
||||
|
||||
currentCells.forEach((_, rowIndex) => {
|
||||
currentCells[rowIndex].forEach((selected, valueIndex) => {
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = newData[rowIndex][valueIndex - 1];
|
||||
switch (operation) {
|
||||
case Operations.INC:
|
||||
if (isNotGreater(rowIndex, current)) {
|
||||
newData[rowIndex][valueIndex - 1] += 1;
|
||||
}
|
||||
break;
|
||||
case Operations.DEC:
|
||||
if (isNotLess(rowIndex, current)) {
|
||||
newData[rowIndex][valueIndex - 1] -= 1;
|
||||
}
|
||||
break;
|
||||
case Operations.REPLACE:
|
||||
if (isX(rowIndex) && value > xMax) {
|
||||
newData[rowIndex][valueIndex - 1] = xMax;
|
||||
break;
|
||||
}
|
||||
if (isX(rowIndex) && value < xMin) {
|
||||
newData[rowIndex][valueIndex - 1] = xMin;
|
||||
break;
|
||||
}
|
||||
if (isY(rowIndex) && value < yMin) {
|
||||
newData[rowIndex][valueIndex - 1] = yMin;
|
||||
break;
|
||||
}
|
||||
if (isY(rowIndex) && value > yMax) {
|
||||
newData[rowIndex][valueIndex - 1] = yMax;
|
||||
break;
|
||||
}
|
||||
|
||||
newData[rowIndex][valueIndex - 1] = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return [...newData];
|
||||
}, [xMax, xMin, yMax, yMin]);
|
||||
|
||||
const oneModalOk = () => {
|
||||
setData(modifyData(Operations.REPLACE, cellsRef.current, dataRef.current, modalValue));
|
||||
setIsModalVisible(false);
|
||||
setModalValue(undefined);
|
||||
};
|
||||
const onModalCancel = () => {
|
||||
setIsModalVisible(false);
|
||||
setModalValue(undefined);
|
||||
};
|
||||
const resetCells = () => setCells(generateCells());
|
||||
const increment = () => setData(modifyData(Operations.INC, cellsRef.current, dataRef.current));
|
||||
const decrement = () => setData(modifyData(Operations.DEC, cellsRef.current, dataRef.current));
|
||||
const replace = () => {
|
||||
// don't show modal when no cell is selected
|
||||
if (cellsRef.current.flat().find((val) => val === true)) {
|
||||
setModalValue(undefined);
|
||||
setIsModalVisible(true);
|
||||
setInterval(() => modalInputRef.current?.focus(), 1);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const keyboardListener = (e: KeyboardEvent) => {
|
||||
|
||||
if (isIncrement(e)) {
|
||||
increment();
|
||||
}
|
||||
if (isDecrement(e)) {
|
||||
decrement();
|
||||
}
|
||||
if (isReplace(e)) {
|
||||
e.preventDefault();
|
||||
replace();
|
||||
}
|
||||
if (isEscape(e)) {
|
||||
resetCells();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', keyboardListener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', keyboardListener);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const colorHsl = (min: number, max: number, value: number): HslType => {
|
||||
const saturation = 60;
|
||||
const lightness = 40;
|
||||
const coldDeg = 220;
|
||||
const hotDeg = 0;
|
||||
const remap = (x: number, inMin: number, inMax: number, outMin: number, outMax: number) => (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
|
||||
|
||||
let hue = remap(value, min, max, coldDeg, hotDeg);
|
||||
|
||||
// fallback to cold temp
|
||||
if (Number.isNaN(hue)) {
|
||||
hue = coldDeg;
|
||||
}
|
||||
|
||||
return [hue, saturation, lightness];
|
||||
};
|
||||
|
||||
const renderRow = (axis: AxisType, input: number[]) => input
|
||||
.map((value, index) => {
|
||||
const [hue, sat, light] = colorHsl(Math.min(...input), Math.max(...input), value);
|
||||
|
||||
return (
|
||||
<td
|
||||
className="value"
|
||||
key={`${axis}-${index}-${value}-${hue}${sat}${light}`}
|
||||
style={{ backgroundColor: `hsl(${hue}, ${sat}%, ${light}%)` }}
|
||||
>
|
||||
{`${value}`}
|
||||
</td>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="table table-2d">
|
||||
<Popover
|
||||
visible={cells.flat().find((val) => val === true) === true}
|
||||
content={
|
||||
<Space>
|
||||
<Button onClick={decrement} icon={<MinusCircleOutlined />} />
|
||||
<Button onClick={increment} icon={<PlusCircleOutlined />} />
|
||||
<Button onClick={replace} icon={<EditOutlined />} />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<TableDragSelect
|
||||
key={name}
|
||||
value={cells}
|
||||
onChange={setCells}
|
||||
>
|
||||
<tr>
|
||||
<td {...titleProps} className="title-curve" key={yLabel}>{`${yLabel} (${yUnits})`}</td>
|
||||
{renderRow('y', data[0])}
|
||||
</tr>
|
||||
<tr>
|
||||
<td {...titleProps} className="title-curve" key={xLabel}>{`${xLabel} (${xUnits})`}</td>
|
||||
{renderRow('x', data[1])}
|
||||
</tr>
|
||||
</TableDragSelect>
|
||||
</Popover>
|
||||
</div>
|
||||
<Modal
|
||||
title="Set cell values"
|
||||
visible={isModalVisible}
|
||||
onOk={oneModalOk}
|
||||
onCancel={onModalCancel}
|
||||
centered
|
||||
forceRender
|
||||
>
|
||||
<InputNumber
|
||||
ref={modalInputRef}
|
||||
value={modalValue}
|
||||
onChange={(val) => setModalValue(Number(val))}
|
||||
onPressEnter={oneModalOk}
|
||||
style={{ width: '20%' }}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
|
@ -0,0 +1,27 @@
|
|||
import {
|
||||
Typography,
|
||||
Alert,
|
||||
} from 'antd';
|
||||
|
||||
const TextField = ({ title }: { title: string }) => {
|
||||
const types: { [char: string]: 'info' | 'warning' } = {
|
||||
'#': 'info',
|
||||
'!': 'warning',
|
||||
};
|
||||
const type = types[title.charAt(0)];
|
||||
|
||||
return (
|
||||
<Typography.Paragraph
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
>
|
||||
{type ? <Alert
|
||||
message={type ? title.substring(1) : title}
|
||||
type={type}
|
||||
showIcon
|
||||
style={{ width: '100%', maxWidth: 700 }}
|
||||
/> : <Typography.Text type="secondary">{title}</Typography.Text>}
|
||||
</Typography.Paragraph>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextField;
|
|
@ -0,0 +1,152 @@
|
|||
import { Layout, Menu, Skeleton } from 'antd';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
generatePath,
|
||||
Link,
|
||||
} from 'react-router-dom';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import { useCallback } from 'react';
|
||||
import store from '../store';
|
||||
import {
|
||||
AppState,
|
||||
UIState,
|
||||
} from '../types/state';
|
||||
import {
|
||||
Config as ConfigType,
|
||||
Menus as MenusType,
|
||||
} from '../types/config';
|
||||
import {
|
||||
Tune as TuneType,
|
||||
} from '../types/tune';
|
||||
import Icon from './SideBar/Icon';
|
||||
import { evaluateExpression } from '../utils/tune/expression';
|
||||
import { Routes } from '../routes';
|
||||
|
||||
const { Sider } = Layout;
|
||||
const { SubMenu } = Menu;
|
||||
|
||||
export interface DialogMatchedPathType {
|
||||
url: string;
|
||||
params: {
|
||||
category: string;
|
||||
dialog: string;
|
||||
};
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
config: state.config,
|
||||
tune: state.tune,
|
||||
ui: state.ui,
|
||||
});
|
||||
|
||||
const SKIP_MENUS = [
|
||||
'help',
|
||||
'hardwareTesting',
|
||||
'3dTuningMaps',
|
||||
'dataLogging',
|
||||
'tools',
|
||||
];
|
||||
|
||||
const SKIP_SUB_MENUS = [
|
||||
'settings/gaugeLimits',
|
||||
'settings/io_summary',
|
||||
'tuning/std_realtime',
|
||||
];
|
||||
|
||||
const SideBar = ({
|
||||
config,
|
||||
tune,
|
||||
ui,
|
||||
matchedPath,
|
||||
}: {
|
||||
config: ConfigType,
|
||||
tune: TuneType,
|
||||
ui: UIState,
|
||||
matchedPath: DialogMatchedPathType,
|
||||
}) => {
|
||||
const sidebarWidth = 250;
|
||||
const siderProps = {
|
||||
width: sidebarWidth,
|
||||
collapsible: true,
|
||||
breakpoint: 'xl',
|
||||
collapsed: ui.sidebarCollapsed,
|
||||
onCollapse: (collapsed: boolean) => store.dispatch({ type: 'ui/sidebarCollapsed', payload: collapsed }),
|
||||
} as any;
|
||||
const checkCondition = useCallback((condition: string) => evaluateExpression(condition, tune.constants, config), [tune.constants, config]);
|
||||
const buildLink = useCallback((main: string, sub: string) => generatePath(Routes.DIALOG, {
|
||||
category: main,
|
||||
dialog: sub,
|
||||
}), []);
|
||||
|
||||
const menusList = useCallback((menus: MenusType) => (
|
||||
Object.keys(menus).map((menuName: string) => {
|
||||
if (SKIP_MENUS.includes(menuName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SubMenu
|
||||
key={`/${menuName}`}
|
||||
icon={<Icon name={menuName} />}
|
||||
title={menus[menuName].title}
|
||||
>
|
||||
{Object.keys(menus[menuName].subMenus).map((subMenuName: string) => {
|
||||
if (subMenuName === 'std_separator') {
|
||||
return <Menu.Divider key={buildLink(menuName, subMenuName)} />;
|
||||
}
|
||||
|
||||
if (SKIP_SUB_MENUS.includes(`${menuName}/${subMenuName}`)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const subMenu = menus[menuName].subMenus[subMenuName];
|
||||
let enabled = true;
|
||||
|
||||
// TODO: optimize this!
|
||||
if (subMenu.condition) {
|
||||
enabled = checkCondition(subMenu.condition);
|
||||
}
|
||||
|
||||
return (<Menu.Item
|
||||
key={buildLink(menuName, subMenuName)}
|
||||
icon={<Icon name={subMenuName} />}
|
||||
disabled={!enabled}
|
||||
>
|
||||
<Link to={buildLink(menuName, subMenuName)}>
|
||||
{subMenu.title}
|
||||
</Link>
|
||||
</Menu.Item>);
|
||||
})}
|
||||
</SubMenu>
|
||||
);
|
||||
})
|
||||
), [buildLink, checkCondition]);
|
||||
|
||||
if (!config || !config.constants) {
|
||||
return (
|
||||
<Sider {...siderProps} className="app-sidebar" >
|
||||
<div style={{ paddingLeft: 10 }}>
|
||||
<Skeleton /><Skeleton /><Skeleton /><Skeleton /><Skeleton />
|
||||
</div>
|
||||
</Sider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Sider {...siderProps} className="app-sidebar">
|
||||
<PerfectScrollbar options={{ suppressScrollX: true }}>
|
||||
<Menu
|
||||
defaultSelectedKeys={[matchedPath.url]}
|
||||
defaultOpenKeys={[`/${matchedPath.params.category}`]}
|
||||
mode="inline"
|
||||
style={{ height: '100%' }}
|
||||
key={matchedPath.url}
|
||||
>
|
||||
{Object.keys(tune.constants).length && menusList(config.menus)}
|
||||
</Menu>
|
||||
</PerfectScrollbar>
|
||||
</Sider>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(SideBar);
|
|
@ -0,0 +1,127 @@
|
|||
import {
|
||||
ApartmentOutlined,
|
||||
ApiOutlined,
|
||||
CarOutlined,
|
||||
ControlOutlined,
|
||||
DashboardOutlined,
|
||||
DotChartOutlined,
|
||||
ExperimentOutlined,
|
||||
FieldTimeOutlined,
|
||||
FireOutlined,
|
||||
FundOutlined,
|
||||
FundProjectionScreenOutlined,
|
||||
PoweroffOutlined,
|
||||
QuestionCircleOutlined,
|
||||
RocketOutlined,
|
||||
SafetyOutlined,
|
||||
SettingOutlined,
|
||||
TableOutlined,
|
||||
ThunderboltOutlined,
|
||||
ToolOutlined,
|
||||
UnorderedListOutlined,
|
||||
UpCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const Icon = ({ name }: { name: string }) => {
|
||||
switch (name) {
|
||||
// main menu
|
||||
case 'settings':
|
||||
return <ControlOutlined />;
|
||||
case 'tuning':
|
||||
return <CarOutlined />;
|
||||
case 'spark':
|
||||
return <FireOutlined />;
|
||||
case 'startupIdle':
|
||||
return <PoweroffOutlined />;
|
||||
case 'accessories':
|
||||
return <ApiOutlined />;
|
||||
case 'tools':
|
||||
return <ToolOutlined />;
|
||||
case '3dTuningMaps':
|
||||
return <DotChartOutlined />;
|
||||
case 'hardwareTesting':
|
||||
return <ExperimentOutlined />;
|
||||
case 'help':
|
||||
return <QuestionCircleOutlined />;
|
||||
|
||||
// common, 2D table
|
||||
case 'injChars':
|
||||
case 'airdensity_curve':
|
||||
case 'baroFuel_curve':
|
||||
case 'dwellCompensation':
|
||||
case 'iatRetard':
|
||||
case 'clt_advance_curve':
|
||||
case 'rotary_ignition':
|
||||
case 'accelEnrichments':
|
||||
case 'flexFueling':
|
||||
case 'dwell_correction_curve':
|
||||
case 'iat_retard_curve':
|
||||
case 'crankPW':
|
||||
case 'primePW':
|
||||
case 'warmup':
|
||||
case 'ASE':
|
||||
case 'iacClosedLoop_curve':
|
||||
case 'iacPwm_curve':
|
||||
case 'iacPwmCrank_curve':
|
||||
case 'iacStep_curve':
|
||||
case 'iacStepCrank_curve':
|
||||
case 'idleAdvanceSettings':
|
||||
return <FundOutlined />;
|
||||
|
||||
// common 3D table / map
|
||||
case 'sparkTbl':
|
||||
case 'veTableDialog':
|
||||
case 'afrTable1Tbl':
|
||||
case 'fuelTable2Dialog':
|
||||
case 'sparkTable2Dialog':
|
||||
case 'inj_trimad':
|
||||
case 'stagingTableDialog':
|
||||
case 'stagedInjection':
|
||||
case 'fuelTemp_curve':
|
||||
case 'boostLoad':
|
||||
return <TableOutlined />;
|
||||
|
||||
// rest
|
||||
case 'triggerSettings':
|
||||
return <SettingOutlined />;
|
||||
case 'reset_control':
|
||||
return <PoweroffOutlined />;
|
||||
case 'engine_constants':
|
||||
return <ControlOutlined />;
|
||||
case 'io_summary':
|
||||
return <UnorderedListOutlined />;
|
||||
case 'prgm_out_config':
|
||||
return <ApartmentOutlined />;
|
||||
case 'std_realtime':
|
||||
return <FundProjectionScreenOutlined />;
|
||||
case 'sparkSettings':
|
||||
return <FireOutlined />;
|
||||
case 'dwellSettings':
|
||||
return <FieldTimeOutlined />;
|
||||
case 'RevLimiterS':
|
||||
return <SafetyOutlined />;
|
||||
case 'idleUpSettings':
|
||||
return <UpCircleOutlined />;
|
||||
case 'LaunchControl':
|
||||
return <ThunderboltOutlined />;
|
||||
case 'NitrousControl':
|
||||
return <RocketOutlined />;
|
||||
case 'vssSettings':
|
||||
return <SettingOutlined />;
|
||||
case 'Auxin_config':
|
||||
return <ApiOutlined />;
|
||||
case 'tacho':
|
||||
case 'pressureSensors':
|
||||
return <DashboardOutlined />;
|
||||
|
||||
// default / not found
|
||||
default:
|
||||
// return null;
|
||||
// return <BarsOutlined />;
|
||||
// return <SettingOutlined />;
|
||||
// return <MenuOutlined />;
|
||||
return <ControlOutlined />;
|
||||
}
|
||||
};
|
||||
|
||||
export default Icon;
|
|
@ -0,0 +1,39 @@
|
|||
|
||||
import { Layout, Space, Row, Col } from 'antd';
|
||||
import { CarOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState, ConfigState, StatusState } from '../types/state';
|
||||
|
||||
const { Footer } = Layout;
|
||||
|
||||
const mapStateToProps = (state: AppState) => ({
|
||||
status: state.status,
|
||||
config: state.config,
|
||||
});
|
||||
|
||||
const firmware = (signature: string) => (
|
||||
<Space>
|
||||
<InfoCircleOutlined />{signature}
|
||||
</Space>
|
||||
);
|
||||
|
||||
const StatusBar = ({ status, config }: { status: StatusState, config: ConfigState }) => (
|
||||
<Footer className="app-status-bar">
|
||||
<Row>
|
||||
<Col span={8}>
|
||||
<Space>
|
||||
<CarOutlined />
|
||||
default
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={8} style={{ textAlign: 'center' }}>
|
||||
{config.megaTune && firmware(config.megaTune.signature)}
|
||||
</Col>
|
||||
<Col span={8} style={{ textAlign: 'right' }}>
|
||||
{status.text}
|
||||
</Col>
|
||||
</Row>
|
||||
</Footer>
|
||||
);
|
||||
|
||||
export default connect(mapStateToProps)(StatusBar);
|
|
@ -0,0 +1,187 @@
|
|||
import {
|
||||
matchPath,
|
||||
useLocation,
|
||||
useHistory,
|
||||
} from 'react-router';
|
||||
import {
|
||||
Layout,
|
||||
Space,
|
||||
Button,
|
||||
Input,
|
||||
Row,
|
||||
Col,
|
||||
Tooltip,
|
||||
Grid,
|
||||
Menu,
|
||||
Dropdown,
|
||||
Typography,
|
||||
Radio,
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
ShareAltOutlined,
|
||||
CloudUploadOutlined,
|
||||
CloudDownloadOutlined,
|
||||
SettingOutlined,
|
||||
LoginOutlined,
|
||||
LineChartOutlined,
|
||||
SlidersOutlined,
|
||||
GithubOutlined,
|
||||
FileExcelOutlined,
|
||||
FileTextOutlined,
|
||||
FileZipOutlined,
|
||||
SaveOutlined,
|
||||
DesktopOutlined,
|
||||
GlobalOutlined,
|
||||
LinkOutlined,
|
||||
DownOutlined,
|
||||
SearchOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import store from '../store';
|
||||
import { isMac } from '../utils/env';
|
||||
import {
|
||||
isCommand,
|
||||
isToggleSidebar,
|
||||
} from '../utils/keyboard/shortcuts';
|
||||
import { Routes } from '../routes';
|
||||
|
||||
const { Header } = Layout;
|
||||
const { useBreakpoint } = Grid;
|
||||
const { SubMenu } = Menu;
|
||||
|
||||
const TopBar = () => {
|
||||
const { sm } = useBreakpoint();
|
||||
const { pathname } = useLocation();
|
||||
const history = useHistory();
|
||||
const matchedTabPath = useMemo(() => matchPath(pathname, { path: Routes.TAB }), [pathname]);
|
||||
|
||||
const userMenu = (
|
||||
<Menu>
|
||||
<Menu.Item disabled icon={<LoginOutlined />}>
|
||||
Login / Sign-up
|
||||
</Menu.Item>
|
||||
<Menu.Item icon={<GithubOutlined />}>
|
||||
<a href="https://github.com/speedy-tuner/speedy-tuner-cloud" target="__blank" rel="noopener noreferrer">
|
||||
GitHub
|
||||
</a>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item disabled icon={<SettingOutlined />}>
|
||||
Preferences
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const shareMenu = (
|
||||
<Menu>
|
||||
<Menu.Item disabled icon={<CloudUploadOutlined />}>
|
||||
Upload
|
||||
</Menu.Item>
|
||||
<SubMenu title="Download" icon={<CloudDownloadOutlined />}>
|
||||
<SubMenu title="Tune" icon={<SlidersOutlined />}>
|
||||
<Menu.Item icon={<SaveOutlined />}>
|
||||
<a href="/tunes/202103.msq" target="__blank" rel="noopener noreferrer">
|
||||
Download
|
||||
</a>
|
||||
</Menu.Item>
|
||||
<Menu.Item disabled icon={<DesktopOutlined />}>
|
||||
Open in app
|
||||
</Menu.Item>
|
||||
</SubMenu>
|
||||
<SubMenu title="Logs" icon={<LineChartOutlined />}>
|
||||
<Menu.Item disabled icon={<FileZipOutlined />}>
|
||||
MLG
|
||||
</Menu.Item>
|
||||
<Menu.Item disabled icon={<FileTextOutlined />}>
|
||||
MSL
|
||||
</Menu.Item>
|
||||
<Menu.Item disabled icon={<FileExcelOutlined />}>
|
||||
CSV
|
||||
</Menu.Item>
|
||||
</SubMenu>
|
||||
</SubMenu>
|
||||
<Menu.Item disabled icon={<LinkOutlined />}>
|
||||
Create link
|
||||
</Menu.Item>
|
||||
<Menu.Item disabled icon={<GlobalOutlined />}>
|
||||
Publish to Hub
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const searchInput = useRef<Input | null>(null);
|
||||
const handleGlobalKeyboard = (e: KeyboardEvent) => {
|
||||
if (isCommand(e)) {
|
||||
if (searchInput) {
|
||||
e.preventDefault();
|
||||
searchInput.current!.focus();
|
||||
}
|
||||
}
|
||||
|
||||
if (isToggleSidebar(e)) {
|
||||
e.preventDefault();
|
||||
store.dispatch({ type: 'ui/toggleSidebar' });
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleGlobalKeyboard);
|
||||
|
||||
return () => document.removeEventListener('keydown', handleGlobalKeyboard);
|
||||
});
|
||||
|
||||
return (
|
||||
<Header className="app-top-bar">
|
||||
<Row>
|
||||
<Col span={0} md={8} sm={0} />
|
||||
<Col span={12} md={8} sm={16} style={{ textAlign: 'center' }}>
|
||||
<Radio.Group
|
||||
key={pathname}
|
||||
defaultValue={matchedTabPath?.url}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
onChange={(e) => history.push(e.target.value)}
|
||||
>
|
||||
<Radio.Button value={Routes.TUNE}>Tune</Radio.Button>
|
||||
<Radio.Button value={Routes.LOG}>Log</Radio.Button>
|
||||
<Radio.Button value={Routes.DIAGNOSE}>Diagnose</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Col>
|
||||
<Col span={12} md={8} sm={8} style={{ textAlign: 'right' }}>
|
||||
<Space className="electron-not-draggable">
|
||||
<Tooltip title={
|
||||
<>
|
||||
<Typography.Text keyboard>{isMac ? '⌘' : 'CTRL'}</Typography.Text>
|
||||
<Typography.Text keyboard>P</Typography.Text>
|
||||
</>
|
||||
}>
|
||||
{sm && <Button icon={<SearchOutlined />} />}
|
||||
</Tooltip>
|
||||
<Dropdown
|
||||
overlay={shareMenu}
|
||||
placement="bottomCenter"
|
||||
>
|
||||
<Button icon={<ShareAltOutlined />}>
|
||||
<DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
overlay={userMenu}
|
||||
placement="bottomCenter"
|
||||
>
|
||||
<Button icon={<UserOutlined />}>
|
||||
<DownOutlined />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopBar;
|
|
@ -0,0 +1,17 @@
|
|||
import {
|
||||
ConstantTypes,
|
||||
ScalarConstant,
|
||||
} from '../types/config';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const divider: ScalarConstant = {
|
||||
type: ConstantTypes.SCALAR,
|
||||
size: 'U08',
|
||||
offset: 25,
|
||||
units: '',
|
||||
scale: 1,
|
||||
transform: 0,
|
||||
min: 1,
|
||||
max: 8,
|
||||
digits: 0,
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
import {
|
||||
Help as HelpType,
|
||||
} from '../types/config';
|
||||
|
||||
export const help: HelpType = {
|
||||
reqFuel: 'The base reference pulse width required to achieve stoichiometric at 100% VE and a manifold absolute pressure (MAP) of 100kPa using current settings.',
|
||||
algorithm: 'Fueling calculation algorithm',
|
||||
alternate: 'Whether or not the injectors should be fired at the same time.\nThis setting is ignored when Sequential is selected below, however it will still affect req_fuel value.',
|
||||
twoStroke: 'Four-stroke (most engines) / Two-stroke',
|
||||
nCylinders: 'Cylinder count',
|
||||
injType: 'Port Injection (one injector for each cylinder) or Throttle Body (injectors shared by cylinders)',
|
||||
nInjectors: 'Number of primary injectors',
|
||||
engineType: 'Engines with an equal number of degrees between all firings (This is most engines) should select Even fire.\nSome 2 and 6 cylinder engines are Odd fire however',
|
||||
};
|
||||
|
||||
export default help;
|
|
@ -0,0 +1,58 @@
|
|||
import {
|
||||
Dialogs as DialogsType,
|
||||
} from '../types/config';
|
||||
|
||||
const standardDialogs: DialogsType = {
|
||||
std_injection: {
|
||||
title: 'Engine Constants',
|
||||
help: 'https://wiki.speeduino.com/en/configuration/Engine_Constants',
|
||||
layout: '',
|
||||
panels: {
|
||||
engine_constants_southwest: {
|
||||
layout: '',
|
||||
fields: [], // overridden by ini file
|
||||
condition: '',
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'reqFuel',
|
||||
title: 'Required Fuel',
|
||||
},
|
||||
{
|
||||
name: 'algorithm',
|
||||
title: 'Control Algorithm',
|
||||
},
|
||||
{
|
||||
name: 'divider',
|
||||
title: 'Squirts Per Engine Cycle',
|
||||
},
|
||||
{
|
||||
name: 'alternate',
|
||||
title: 'Injector Staging',
|
||||
},
|
||||
{
|
||||
name: 'twoStroke',
|
||||
title: 'Engine Stroke',
|
||||
},
|
||||
{
|
||||
name: 'nCylinders',
|
||||
title: 'Number of Cylinders',
|
||||
},
|
||||
{
|
||||
name: 'injType',
|
||||
title: 'Injector Port Type',
|
||||
},
|
||||
{
|
||||
name: 'nInjectors',
|
||||
title: 'Number of Injectors',
|
||||
},
|
||||
{
|
||||
name: 'engineType',
|
||||
title: 'Engine Type',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default standardDialogs;
|
|
@ -0,0 +1,62 @@
|
|||
import { app, BrowserWindow } from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
const isDev = !app.isPackaged;
|
||||
|
||||
let main: BrowserWindow;
|
||||
// let splash: BrowserWindow | null;
|
||||
|
||||
// const createSplash = () => {
|
||||
// splash = new BrowserWindow({
|
||||
// width: 200,
|
||||
// height: 400,
|
||||
// frame: false,
|
||||
// transparent: true,
|
||||
// resizable: false,
|
||||
// });
|
||||
// splash.loadURL(`file://${path.join(__dirname, '../public/splash.html')}`);
|
||||
|
||||
// splash.on('closed', () => {
|
||||
// splash = null;
|
||||
// });
|
||||
|
||||
// splash.webContents.on('did-finish-load', () => {
|
||||
// splash.show();
|
||||
// });
|
||||
// };
|
||||
|
||||
function createMain() {
|
||||
main = new BrowserWindow({
|
||||
title: 'SpeedyTuner',
|
||||
width: 1400,
|
||||
height: 1000,
|
||||
show: false,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
backgroundColor: '#222629',
|
||||
});
|
||||
main.setMenuBarVisibility(false);
|
||||
|
||||
const startURL = isDev
|
||||
? 'http://localhost:3000'
|
||||
: `file://${path.join(__dirname, '../build/index.html')}`;
|
||||
|
||||
main.loadURL(startURL);
|
||||
|
||||
main.on('closed', () => {
|
||||
main = null as any;
|
||||
});
|
||||
|
||||
main.once('ready-to-show', () => main.show());
|
||||
// main.webContents.on('did-finish-load', () => {
|
||||
// if (splash) {
|
||||
// splash.close();
|
||||
// }
|
||||
// main.show();
|
||||
// });
|
||||
}
|
||||
|
||||
app.on('ready', () => {
|
||||
// createSplash();
|
||||
createMain();
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import store from './store';
|
||||
|
||||
ReactDOM.render(
|
||||
<HashRouter>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</HashRouter>,
|
||||
document.getElementById('root'),
|
||||
);
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
/// <reference types="react-scripts" />
|
|
@ -0,0 +1,9 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export enum Routes {
|
||||
ROOT = '/',
|
||||
TAB = '/:tab',
|
||||
TUNE = '/tune',
|
||||
DIALOG = '/tune/:category/:dialog',
|
||||
LOG = '/log',
|
||||
DIAGNOSE = '/diagnose',
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
|
||||
import { configureStore, createAction, createReducer } from '@reduxjs/toolkit';
|
||||
import * as Types from './types/state';
|
||||
|
||||
// tune and config
|
||||
const updateTune = createAction<Types.UpdateTunePayload>('tune/update');
|
||||
const loadTune = createAction<Types.TuneState>('tune/load');
|
||||
const loadConfig = createAction<Types.ConfigState>('config/load');
|
||||
|
||||
// status bar
|
||||
const setStatus = createAction<string>('status');
|
||||
|
||||
// ui
|
||||
const setSidebarCollapsed = createAction<boolean>('ui/sidebarCollapsed');
|
||||
const toggleSidebar = createAction('ui/toggleSidebar');
|
||||
|
||||
const initialState: Types.AppState = {
|
||||
tune: {
|
||||
constants: {},
|
||||
},
|
||||
config: {} as any,
|
||||
ui: {
|
||||
sidebarCollapsed: false,
|
||||
},
|
||||
status: {
|
||||
text: null,
|
||||
},
|
||||
};
|
||||
|
||||
const rootReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
.addCase(loadConfig, (state: Types.AppState, action) => {
|
||||
state.config = action.payload;
|
||||
})
|
||||
.addCase(loadTune, (state: Types.AppState, action) => {
|
||||
state.tune = action.payload;
|
||||
})
|
||||
.addCase(updateTune, (state: Types.AppState, action) => {
|
||||
state.tune.constants[action.payload.name].value = action.payload.value;
|
||||
})
|
||||
.addCase(setSidebarCollapsed, (state: Types.AppState, action) => {
|
||||
state.ui.sidebarCollapsed = action.payload;
|
||||
})
|
||||
.addCase(toggleSidebar, (state: Types.AppState) => {
|
||||
state.ui.sidebarCollapsed = !state.ui.sidebarCollapsed;
|
||||
})
|
||||
.addCase(setStatus, (state: Types.AppState, action) => {
|
||||
state.status.text = action.payload;
|
||||
});
|
||||
});
|
||||
|
||||
const store = configureStore({
|
||||
reducer: rootReducer,
|
||||
});
|
||||
|
||||
export default store;
|
|
@ -0,0 +1,16 @@
|
|||
.ant-menu {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.ant-layout-sider-trigger {
|
||||
bottom: @layout-footer-height;
|
||||
// .border-right;
|
||||
}
|
||||
|
||||
.ant-empty {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ant-popover-content {
|
||||
box-shadow: @box-shadow;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
@primary-color: #126ec3;
|
||||
@text-light: #fff;
|
||||
|
||||
@border-radius-base: 5px;
|
||||
|
||||
@layout-header-padding: 0 15px;
|
||||
@layout-header-height: 45px;
|
||||
|
||||
@layout-footer-padding: 2px 10px;
|
||||
@layout-footer-height: 24px;
|
||||
|
||||
@bars-z-index: 5;
|
||||
@zindex-modal: 1080;
|
||||
@box-shadow: 0 3px 6px -4px rgb(0 0 0 e('/') 12%), 0 6px 16px 0 rgb(0 0 0 e('/') 8%), 0 9px 28px 8px rgb(0 0 0 e('/') 5%);
|
|
@ -0,0 +1,102 @@
|
|||
// darker
|
||||
@main: #222629;
|
||||
@main-dark: #191C1E;
|
||||
@main-light: #2E3338;
|
||||
|
||||
@text: #ddd;
|
||||
|
||||
// lighter
|
||||
// @main: #272c30;
|
||||
// @main-dark: #212527;
|
||||
// @main-light: #394046;
|
||||
|
||||
@text-dark: #222;
|
||||
@text-disabled: #999;
|
||||
|
||||
// First
|
||||
// @primary-color: #26547C;
|
||||
@info: #FFD166;
|
||||
@success: #06D6A0;
|
||||
@error: #EF476F;
|
||||
@warn: #E55934;
|
||||
|
||||
// Second
|
||||
// @primary-color: #004E64;
|
||||
// @info: #F2C14E;
|
||||
// @success: #499F68;
|
||||
// @error: #DA2C38;
|
||||
// @warn: #ED5A31;
|
||||
|
||||
// new ant
|
||||
@primary-1: @primary-color;
|
||||
|
||||
@processing-color: @info;
|
||||
@info-color: @info;
|
||||
@success-color: @success;
|
||||
@error-color: @error;
|
||||
@highlight-color: @error;
|
||||
@warning-color: @warn;
|
||||
@text-color: @text;
|
||||
@text-color-dark: @text-dark;
|
||||
@disabled-color: @text-disabled;
|
||||
@text-color-secondary: @text-disabled;
|
||||
@heading-color: @text-light;
|
||||
@heading-color-dark: @text-dark;
|
||||
@link-color: lighten(@primary-color, 10%);
|
||||
|
||||
@body-background: @main;
|
||||
@component-background: @main-light;
|
||||
@item-active-bg: @primary-color;
|
||||
@item-hover-bg: @primary-color;
|
||||
@menu-highlight-color: @text;
|
||||
@table-row-hover-bg: darken(@primary-color, 10%);
|
||||
|
||||
@descriptions-bg: @main-light;
|
||||
@menu-bg: @main;
|
||||
|
||||
@border-color-base: @main-dark;
|
||||
@border-color-split: lighten(@main-light, 2%);
|
||||
@background-color-light: @main-light;
|
||||
@background-color-base: @main-light;
|
||||
|
||||
@checkbox-check-color: @main-light;
|
||||
@layout-body-background: @main;
|
||||
|
||||
// Alert
|
||||
@alert-success-border-color: @success;
|
||||
@alert-success-bg-color: darken(@success, 20%);
|
||||
@alert-success-icon-color: @success;
|
||||
|
||||
@alert-info-border-color: @info;
|
||||
@alert-info-bg-color: darken(@info, 30%);
|
||||
@alert-info-icon-color: @info;
|
||||
|
||||
@alert-warning-border-color: @warn;
|
||||
@alert-warning-bg-color: darken(@warn, 20%);
|
||||
@alert-warning-icon-color: @warn;
|
||||
|
||||
@alert-error-border-color: @error;
|
||||
@alert-error-bg-color: darken(@error, 20%);
|
||||
@alert-error-icon-color: @error;
|
||||
|
||||
// Divider
|
||||
@divider-color: @border-color-split;
|
||||
|
||||
// Header
|
||||
@layout-header-background: @main;
|
||||
|
||||
// Side
|
||||
@layout-sider-background: @main;
|
||||
@layout-trigger-background: @main;
|
||||
@layout-trigger-color: @text-color;
|
||||
|
||||
// Tooltip
|
||||
@tooltip-bg: rgb(46, 51, 56, 0.8);
|
||||
@zindex-tooltip: 1;
|
||||
|
||||
// Modal
|
||||
@modal-header-bg: @main;
|
||||
@modal-content-bg: @main;
|
||||
|
||||
// Radio buttons
|
||||
@radio-solid-checked-color: @white;
|
|
@ -0,0 +1,12 @@
|
|||
@main: @white;
|
||||
@text: @white;
|
||||
@main-light: #181818;
|
||||
|
||||
@body-background: @main;
|
||||
@menu-bg: @main;
|
||||
@layout-body-background: @main;
|
||||
@layout-header-background: @main;
|
||||
|
||||
@layout-sider-background: @main;
|
||||
@layout-trigger-background: @main;
|
||||
@layout-trigger-color: @text-color;
|
|
@ -0,0 +1,200 @@
|
|||
export enum Switches {
|
||||
YES = 'Yes',
|
||||
NO = 'No',
|
||||
ON = 'On',
|
||||
OFF = 'Off',
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
title: string;
|
||||
name: string;
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export interface Panel {
|
||||
layout: string;
|
||||
panels?: {
|
||||
[name: string]: Panel;
|
||||
};
|
||||
fields?: Field[];
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export interface Dialog {
|
||||
title: string;
|
||||
layout: string;
|
||||
help?: string;
|
||||
panels: {
|
||||
[name: string]: Panel;
|
||||
};
|
||||
fields: Field[];
|
||||
// TODO:
|
||||
// settingSelector
|
||||
// commandButton
|
||||
}
|
||||
|
||||
export interface Dialogs {
|
||||
[name: string]: Dialog;
|
||||
}
|
||||
|
||||
export interface SubMenu {
|
||||
title: string;
|
||||
page: number;
|
||||
condition: string;
|
||||
}
|
||||
|
||||
export interface Menu {
|
||||
title: string;
|
||||
subMenus: {
|
||||
[name: string]: SubMenu;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Menus {
|
||||
[name: string]: Menu;
|
||||
}
|
||||
|
||||
export interface ArrayShape {
|
||||
columns: number;
|
||||
rows: number;
|
||||
}
|
||||
|
||||
export enum ConstantTypes {
|
||||
SCALAR = 'scalar',
|
||||
BITS = 'bits',
|
||||
ARRAY = 'array',
|
||||
STRING = 'string',
|
||||
}
|
||||
|
||||
export type ConstantSize = 'U08' | 'S08' | 'U16' | 'S16' | 'U32' | 'S32' | 'S64' | 'F32' | 'ASCII';
|
||||
|
||||
export interface ScalarConstant {
|
||||
type: ConstantTypes.SCALAR;
|
||||
size: ConstantSize;
|
||||
offset?: number;
|
||||
units: string;
|
||||
scale: number | string;
|
||||
transform: number | string;
|
||||
min: number | string;
|
||||
max: number | string;
|
||||
digits: number;
|
||||
}
|
||||
|
||||
export interface BitsConstant {
|
||||
type: ConstantTypes.BITS;
|
||||
size: ConstantSize;
|
||||
offset?: number;
|
||||
address: number[];
|
||||
values: string[];
|
||||
}
|
||||
|
||||
export interface ArrayConstant {
|
||||
type: ConstantTypes.ARRAY;
|
||||
size: ConstantSize;
|
||||
offset?: number;
|
||||
shape: ArrayShape;
|
||||
units: string;
|
||||
scale: number | string;
|
||||
transform: number | string;
|
||||
min: number | string;
|
||||
max: number | string;
|
||||
digits: number;
|
||||
}
|
||||
|
||||
export interface StringConstant {
|
||||
type: ConstantTypes.SCALAR;
|
||||
size: ConstantSize;
|
||||
length: number;
|
||||
}
|
||||
|
||||
export interface OutputChannel {
|
||||
type: ConstantTypes.SCALAR | ConstantTypes.BITS;
|
||||
size: ConstantSize;
|
||||
offset: number;
|
||||
units: string;
|
||||
scale: number | string;
|
||||
transform: number | string;
|
||||
}
|
||||
|
||||
export interface SimpleConstant {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type Constant = ScalarConstant | BitsConstant | ArrayConstant | StringConstant;
|
||||
|
||||
export interface Constants {
|
||||
[name: string]: Constant;
|
||||
}
|
||||
|
||||
export interface Page {
|
||||
number: number;
|
||||
size: number;
|
||||
data: Constants;
|
||||
}
|
||||
|
||||
export interface Help {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface Curve {
|
||||
title: string;
|
||||
labels: string[];
|
||||
xAxis: number[];
|
||||
yAxis: number[];
|
||||
xBins: string[];
|
||||
yBins: string[];
|
||||
size: number[];
|
||||
gauge?: string;
|
||||
}
|
||||
|
||||
export interface Table {
|
||||
map: string;
|
||||
title: string;
|
||||
page: number;
|
||||
help?: string;
|
||||
xBins: string[];
|
||||
yBins: string[];
|
||||
xyLabels: string[];
|
||||
zBins: string[];
|
||||
gridHeight: number;
|
||||
gridOrient: number[];
|
||||
upDownLabel: string[];
|
||||
}
|
||||
|
||||
export interface OutputChannels {
|
||||
[name: string]: OutputChannel | SimpleConstant;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
[key: string]: any;
|
||||
megaTune: {
|
||||
[key: string]: any;
|
||||
signature: string;
|
||||
MTversion: number;
|
||||
queryCommand: string;
|
||||
versionInfo: string;
|
||||
};
|
||||
tunerStudio: {
|
||||
[key: string]: any;
|
||||
iniSpecVersion: number;
|
||||
};
|
||||
pcVariables: Constants;
|
||||
constants: {
|
||||
pages: Page[];
|
||||
};
|
||||
defines: {
|
||||
[name: string]: string[];
|
||||
};
|
||||
menus: Menus;
|
||||
help: Help;
|
||||
dialogs: {
|
||||
[name: string]: Dialog;
|
||||
};
|
||||
curves: {
|
||||
[name: string]: Curve;
|
||||
};
|
||||
tables: {
|
||||
[name: string]: Table;
|
||||
};
|
||||
outputChannels: OutputChannels;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { Config } from './config';
|
||||
import { Tune } from './tune';
|
||||
|
||||
export interface ConfigState extends Config {}
|
||||
|
||||
export interface TuneState extends Tune {}
|
||||
|
||||
export interface UIState {
|
||||
sidebarCollapsed: boolean;
|
||||
}
|
||||
|
||||
export interface StatusState {
|
||||
text: string | null;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
tune: TuneState;
|
||||
config: ConfigState;
|
||||
ui: UIState;
|
||||
status: StatusState;
|
||||
}
|
||||
|
||||
export interface UpdateTunePayload {
|
||||
name: string;
|
||||
value: string | number;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
export interface Constant {
|
||||
units?: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface Constants {
|
||||
[name: string]: Constant;
|
||||
}
|
||||
|
||||
export interface Tune {
|
||||
constants: Constants;
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import store from '../store';
|
||||
import {
|
||||
Config as ConfigType,
|
||||
} from '../types/config';
|
||||
import stdDialogs from '../data/standardDialogs';
|
||||
import help from '../data/help';
|
||||
import { divider } from '../data/constants';
|
||||
|
||||
export const loadAll = () => {
|
||||
const started = new Date();
|
||||
// const version = 202012;
|
||||
const version = 202103;
|
||||
|
||||
fetch(`./tunes/${version}.json`)
|
||||
.then((response) => response.json())
|
||||
.then((json: ConfigType) => {
|
||||
|
||||
fetch(`./tunes/${version}.msq`)
|
||||
.then((response) => response.text())
|
||||
.then((tune) => {
|
||||
const xml = (new DOMParser()).parseFromString(tune, 'text/xml');
|
||||
const xmlPages = xml.getElementsByTagName('page');
|
||||
const constants: any = {};
|
||||
|
||||
Object.keys(xmlPages).forEach((key: any) => {
|
||||
const page = xmlPages[key];
|
||||
const pageElements = page.children;
|
||||
|
||||
Object.keys(pageElements).forEach((item: any) => {
|
||||
const element = pageElements[item];
|
||||
|
||||
if (element.tagName === 'constant') {
|
||||
const attributes: any = {};
|
||||
|
||||
Object.keys(element.attributes).forEach((attr: any) => {
|
||||
attributes[element.attributes[attr].name] = element.attributes[attr].value;
|
||||
});
|
||||
|
||||
const val = element.textContent?.replace(/"/g, '').toString();
|
||||
|
||||
constants[attributes.name] = {
|
||||
value: Number.isNaN(Number(val)) ? val : Number(val),
|
||||
// digits: Number.isNaN(Number(attributes.digits)) ? attributes.digits : Number(attributes.digits),
|
||||
// cols: Number.isNaN(Number(attributes.cols)) ? attributes.cols : Number(attributes.cols),
|
||||
// rows: Number.isNaN(Number(attributes.rows)) ? attributes.rows : Number(attributes.rows),
|
||||
units: attributes.units ?? null,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!Object.keys(constants).length) {
|
||||
console.error('Invalid tune');
|
||||
}
|
||||
|
||||
const config = json;
|
||||
|
||||
// override / merge standard dialogs, constants and help
|
||||
config.dialogs = {
|
||||
...config.dialogs,
|
||||
...stdDialogs,
|
||||
};
|
||||
config.help = {
|
||||
...config.help,
|
||||
...help,
|
||||
};
|
||||
config.constants.pages[0].data.divider = divider;
|
||||
|
||||
const loadingTimeInfo = `Tune loaded in ${(new Date().getTime() - started.getTime())}ms`;
|
||||
console.log(loadingTimeInfo);
|
||||
|
||||
store.dispatch({ type: 'config/load', payload: config });
|
||||
store.dispatch({ type: 'tune/load', payload: { constants } });
|
||||
store.dispatch({
|
||||
type: 'status',
|
||||
payload: loadingTimeInfo,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const test = () => 'test';
|
|
@ -0,0 +1,16 @@
|
|||
import {
|
||||
Config as ConfigType,
|
||||
Page as PageType,
|
||||
Constant,
|
||||
} from '../../types/config';
|
||||
|
||||
export const findOnPage = (config: ConfigType, fieldName: string): Constant => {
|
||||
const foundPage = config
|
||||
.constants
|
||||
.pages
|
||||
.find((page: PageType) => fieldName in page.data) || { data: {} } as PageType;
|
||||
|
||||
return foundPage.data[fieldName];
|
||||
};
|
||||
|
||||
export const todo = '';
|
|
@ -0,0 +1,4 @@
|
|||
export const isWeb = process.env.APP_PLATFORM === 'web';
|
||||
export const isDesktop = process.env.APP_PLATFORM === 'desktop';
|
||||
export const isMac = `${window.navigator.platform}`.includes('Mac');
|
||||
export const platform = `${window.navigator.platform}`;
|
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
|
||||
type KeyEvent = KeyboardEvent | React.KeyboardEvent<HTMLInputElement>;
|
||||
|
||||
enum Keys {
|
||||
INCREMENT = '.',
|
||||
DECREMENT = ',',
|
||||
COMMAND = 'p',
|
||||
SIDEBAR = '\\',
|
||||
ESCAPE = 'Escape',
|
||||
REPLACE = '=',
|
||||
}
|
||||
|
||||
const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
export const isCommand = (e: KeyEvent) => (e.metaKey || e.ctrlKey) && e.key === Keys.COMMAND;
|
||||
|
||||
export const isToggleSidebar = (e: KeyEvent) => (e.metaKey || e.ctrlKey) && e.key === Keys.SIDEBAR;
|
||||
|
||||
export const isIncrement = (e: KeyEvent) => e.key === Keys.INCREMENT;
|
||||
|
||||
export const isDecrement = (e: KeyEvent) => e.key === Keys.DECREMENT;
|
||||
|
||||
export const isReplace = (e: KeyEvent) => e.key === Keys.REPLACE;
|
||||
|
||||
export const isEscape = (e: KeyEvent) => e.key === Keys.ESCAPE;
|
||||
|
||||
export const useDigits = (e: KeyEvent): [boolean, number] => [digits.includes(Number(e.key)), Number(e.key)];
|
|
@ -0,0 +1,3 @@
|
|||
export const storageGet = (key: string) => window.localStorage.getItem(key);
|
||||
|
||||
export const storageSet = (key: string, value: string) => window.localStorage.setItem(key, value);
|
|
@ -0,0 +1,8 @@
|
|||
export const camelToSnakeCase = (str: string) => str
|
||||
.replace(/[A-Z]/g, (letter: string) => `_${letter.toLowerCase()}`);
|
||||
|
||||
export const camelToUrlCase = (str: string) => str
|
||||
.replace(/[A-Z]/g, (letter: string) => `-${letter.toLowerCase()}`);
|
||||
|
||||
export const snakeToCamelCase = (str: string) => str
|
||||
.replace(/([-_]\w)/g, (g) => g[1].toUpperCase());
|
|
@ -0,0 +1,113 @@
|
|||
import {
|
||||
Constants as TuneConstantsType,
|
||||
} from '../../types/tune';
|
||||
|
||||
import {
|
||||
Config as ConfigType,
|
||||
OutputChannels as OutputChannelsType,
|
||||
Page as ConfigPageType,
|
||||
SimpleConstant as SimpleConstantType,
|
||||
} from '../../types/config';
|
||||
|
||||
export const isExpression = (val: any) => `${val}`.startsWith('{') && `${val}`.endsWith('}');
|
||||
|
||||
export const stripExpression = (val: string) => val.slice(1).slice(0, -1).trim();
|
||||
|
||||
export const isNumber = (val: any) => !Number.isNaN(Number(val));
|
||||
|
||||
// export const isNumber
|
||||
// ochGetCommand
|
||||
export const prepareConstDeclarations = (tuneConstants: TuneConstantsType, configPages: ConfigPageType[]) => (
|
||||
Object.keys(tuneConstants).map((constName: string) => {
|
||||
let val = tuneConstants[constName].value;
|
||||
|
||||
// TODO: skip 2D and 3D maps for now
|
||||
if (typeof val === 'string' && val.includes('\n')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: check whether we can limit this to a single page
|
||||
const constant = configPages
|
||||
.find((page: ConfigPageType) => constName in page.data)
|
||||
?.data[constName];
|
||||
|
||||
// we need array index instead of a display value
|
||||
if (constant?.type === 'bits') {
|
||||
val = (constant.values as string[]).indexOf(`${val}`);
|
||||
}
|
||||
|
||||
// escape string values
|
||||
if (typeof val === 'string') {
|
||||
val = `'${val}'`;
|
||||
}
|
||||
|
||||
return `const ${constName} = ${val};`;
|
||||
})
|
||||
);
|
||||
|
||||
const prepareChannelsDeclarations = (configOutputChannels: OutputChannelsType) => (
|
||||
Object.keys(configOutputChannels).map((channelName: string) => {
|
||||
const current = configOutputChannels[channelName] as SimpleConstantType;
|
||||
if (!current.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let val = current.value;
|
||||
|
||||
if (isExpression(val)) {
|
||||
val = stripExpression(val);
|
||||
} else if (!isNumber(val)) {
|
||||
val = `"${val}"`;
|
||||
}
|
||||
|
||||
return `const ${channelName} = ${val};`;
|
||||
}).filter((val) => val !== null)
|
||||
);
|
||||
|
||||
export const evaluateExpression = (expression: string, tuneConstants: TuneConstantsType, config: ConfigType) => {
|
||||
const constDeclarations = prepareConstDeclarations(tuneConstants, config.constants.pages);
|
||||
const channelsDeclarations = prepareChannelsDeclarations(config.outputChannels);
|
||||
|
||||
try {
|
||||
// TODO: strip eval from `command` etc
|
||||
// https://www.electronjs.org/docs/tutorial/security
|
||||
// eslint-disable-next-line no-eval
|
||||
return eval(`
|
||||
'use strict';
|
||||
const arrayValue = (number, layout) => number;
|
||||
const array = {
|
||||
boardFuelOutputs: 4,
|
||||
boardIgnOutputs: 4,
|
||||
};
|
||||
|
||||
const coolantRaw = 21;
|
||||
const iatRaw = 21;
|
||||
const fuelTempRaw = 21;
|
||||
const timeNow = new Date().getTime();
|
||||
const secl = 0;
|
||||
const tps = 0;
|
||||
const rpm = 0;
|
||||
const nSquirts = 1;
|
||||
const boostCutFuel = 0;
|
||||
const boostCutSpark = 0;
|
||||
const afr = 14.7;
|
||||
const afrTarget = 14.7;
|
||||
const map = 0;
|
||||
const loopsPerSecond = 0;
|
||||
const batCorrection = 0;
|
||||
const ASECurr = 0;
|
||||
const baro = 0;
|
||||
const vss = 0;
|
||||
const CLIdleTarget = 0;
|
||||
|
||||
${constDeclarations.join('')}
|
||||
${channelsDeclarations.join('')}
|
||||
${stripExpression(expression)};
|
||||
`);
|
||||
} catch (error) {
|
||||
console.info('Condition evaluation failed with:', error.message);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
export const parseXy = (value: string) => value
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((val) => val.trim())
|
||||
.filter((val) => val !== '')
|
||||
.map(Number);
|
||||
|
||||
export const parseZ = (value: string) => value
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((val) => val.trim().split(' ').map(Number))
|
||||
.reverse();
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"target": "ES2019",
|
||||
"lib": [
|
||||
"ES2019"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"noEmit": false
|
||||
},
|
||||
"files": [
|
||||
"src/electron.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/parser"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"removeComments": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
}
|
Loading…
Reference in New Issue