Improve repo directory structure
50
.eslintignore
Normal file
|
@ -0,0 +1,50 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
.eslintcache
|
||||
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# App packaged
|
||||
release
|
||||
build
|
||||
dist
|
||||
dll
|
||||
main.js
|
||||
main.js.map
|
||||
|
||||
.idea
|
||||
npm-debug.log.*
|
||||
__snapshots__
|
||||
|
||||
# Package.json
|
||||
package.json
|
||||
.travis.yml
|
||||
*.css.d.ts
|
||||
*.sass.d.ts
|
||||
*.scss.d.ts
|
49
.eslintrc.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
extends: ["plugin:react/recommended", "airbnb"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
ecmaVersion: 12,
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["react", "@typescript-eslint"],
|
||||
rules: {
|
||||
"no-use-before-define": "off",
|
||||
"@typescript-eslint/no-use-before-define": ["error"],
|
||||
"react/jsx-filename-extension": [1, { extensions: [".tsx", ".ts"] }],
|
||||
"consistent-return": ["error", { treatUndefinedAsUnspecified: false }],
|
||||
quotes: ["error", "double"],
|
||||
"import/no-extraneous-dependencies": ["error", { devDependencies: true }],
|
||||
"no-console": "off",
|
||||
"max-len": ["error", { code: 300 }],
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": ["error"],
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
js: "never",
|
||||
jsx: "never",
|
||||
ts: "never",
|
||||
tsx: "never",
|
||||
},
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
node: {
|
||||
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
};
|
40
.gitignore
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
npm-debug.log
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
.eslintcache
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# App packaged
|
||||
release
|
||||
dist
|
||||
dll
|
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"editor.formatOnSave": false
|
||||
}
|
22
README.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Heliox - Dashboard
|
||||
|
||||
[![Electron Build](https://github.com/GHOSCHT/light-control/actions/workflows/Electron.yml/badge.svg)](https://github.com/GHOSCHT/light-control/actions/workflows/Electron.yml)
|
||||
[![CodeQL](https://github.com/GHOSCHT/light-control/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/GHOSCHT/light-control/actions/workflows/codeql-analysis.yml)
|
||||
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/bdb8a994396345efab8271307f1ea155)](https://www.codacy.com/gh/GHOSCHT/heliox/dashboard?utm_source=github.com&utm_medium=referral&utm_content=GHOSCHT/heliox&utm_campaign=Badge_Grade)
|
||||
<a href="https://www.figma.com/file/fK5tEIw4Zx8AivuVbu79Lw/Light-Control">
|
||||
<img src="https://img.shields.io/badge/Figma-F24E1E?style=flat&logo=figma&logoColor=white" />
|
||||
</a>
|
||||
|
||||
Uses: <https://github.com/GHOSCHT/simple-electron-react-boilerplate>
|
||||
|
||||
Dashboard: node-gyp fails on sqlite3 build -> set default pyhton version to python 2
|
||||
|
||||
```shell
|
||||
node-gyp --python /path/to/python2.7/python.exe
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```shell
|
||||
npm config set python /path/to/executable/python2.7
|
||||
```
|
4
assets/assets.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
declare module "*.svg" {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
6630
assets/icons/Icon.ai
Normal file
BIN
assets/icons/Icon.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
assets/icons/mac/icon.icns
Normal file
BIN
assets/icons/png/1024x1024.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
assets/icons/png/128x128.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
assets/icons/png/16x16.png
Normal file
After Width: | Height: | Size: 424 B |
BIN
assets/icons/png/24x24.png
Normal file
After Width: | Height: | Size: 597 B |
BIN
assets/icons/png/256x256.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
assets/icons/png/32x32.png
Normal file
After Width: | Height: | Size: 795 B |
BIN
assets/icons/png/48x48.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/icons/png/512x512.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/icons/png/64x64.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/icons/win/icon.ico
Normal file
After Width: | Height: | Size: 353 KiB |
6605
assets/knob/Knob.ai
Normal file
1
assets/knob/Knob.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 141.73 141.73"><defs><style>.cls-1{fill:#545353}.cls-2{font-size:20px;fill:#fff;font-family:SegoeUI-Bold,Segoe UI;font-weight:700}.cls-3{fill:#2f6087}</style></defs><title>Knob</title><g id="Base"><path d="M352.12,273A70.87,70.87,0,1,0,423,343.84,70.86,70.86,0,0,0,352.12,273Zm0,120.48a49.61,49.61,0,1,1,49.61-49.61A49.61,49.61,0,0,1,352.12,393.45Z" class="cls-1" transform="translate(-281.26 -272.97)"/></g><g id="Status"><text class="cls-2" transform="translate(53.61 77.41)">255</text></g><g id="Overlay"><g><path d="M363.54,295.57" class="cls-3" transform="translate(-281.26 -272.97)"/><path d="M363.54,295.57a49.44,49.44,0,0,0-11.42-1.34" class="cls-3" transform="translate(-281.26 -272.97)"/><path d="M352.12,294.23" class="cls-3" transform="translate(-281.26 -272.97)"/><path d="M368.76,275l-.33-.1v0Z" class="cls-3" transform="translate(-281.26 -272.97)"/><polygon points="87.18 1.89 87.18 1.89 87.17 1.91 87.17 1.91 87.18 1.89" class="cls-3"/><polygon points="82.28 22.58 87.17 1.91 87.17 1.91 82.28 22.58 82.28 22.58" class="cls-3"/><path d="M368.76,275l-.33-.08A71.24,71.24,0,0,0,352.12,273a10.71,10.71,0,0,0-10.53,10.73,10.53,10.53,0,0,0,10.53,10.53,49.44,49.44,0,0,1,11.42,1.34v0a10.33,10.33,0,0,0,12.56-7.68A10.79,10.79,0,0,0,368.76,275Z" class="cls-3" transform="translate(-281.26 -272.97)"/></g></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
367
assets/tray/Swatch.ai
Normal file
1879
assets/tray/Template.ai
Normal file
BIN
assets/tray/tray-default-32.png
Normal file
After Width: | Height: | Size: 451 B |
BIN
assets/tray/tray-green-32.png
Normal file
After Width: | Height: | Size: 612 B |
BIN
assets/tray/tray-red-32.png
Normal file
After Width: | Height: | Size: 614 B |
BIN
assets/tray/tray-yellow-32.png
Normal file
After Width: | Height: | Size: 601 B |
375
assets/tray/tray.ai
Normal file
4
babel.config.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": ["@babel/env", "@babel/react", "@babel/preset-typescript"],
|
||||
"plugins": ["@babel/plugin-proposal-class-properties"]
|
||||
}
|
121
package.json
Normal file
|
@ -0,0 +1,121 @@
|
|||
{
|
||||
"name": "dashboard",
|
||||
"author": {
|
||||
"name": "GHOSCHT"
|
||||
},
|
||||
"description": "",
|
||||
"version": "2.2.1",
|
||||
"main": "src/electron.js",
|
||||
"private": true,
|
||||
"build": {
|
||||
"productName": "Heliox",
|
||||
"appId": "heliox.dashboard",
|
||||
"buildDependenciesFromSource": true,
|
||||
"npmRebuild": false,
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis"
|
||||
],
|
||||
"icon": "./assets/icons/win/icon.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"include": "./scripts/installer.nsh"
|
||||
},
|
||||
"directories": {
|
||||
"buildResources": "assets",
|
||||
"output": "release"
|
||||
},
|
||||
"files": [
|
||||
"node_modules/**/*",
|
||||
"src/*",
|
||||
"package.json",
|
||||
"build/**"
|
||||
],
|
||||
"extraResources": [
|
||||
"./assets/**"
|
||||
]
|
||||
},
|
||||
"homepage": "./src",
|
||||
"scripts": {
|
||||
"web": "webpack serve --port 8420",
|
||||
"build": "webpack --mode=production",
|
||||
"start": "concurrently -k \"yarn web\" \"npm:electron\"",
|
||||
"electron": "wait-on tcp:8420 && electron .",
|
||||
"package": "yarn build && electron-builder --publish never",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"lint": "cross-env NODE_ENV=development eslint . --cache --ext .js,.jsx,.ts,.tsx",
|
||||
"lint:fix": "cross-env NODE_ENV=development eslint . --cache --fix --ext .js,.jsx,.ts,.tsx",
|
||||
"remotedev": "redux-devtools --hostname=localhost --port=8000"
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fluentui/react": "^8.61.0",
|
||||
"@fluentui/react-icons-mdl2": "^1.3.4",
|
||||
"@reduxjs/toolkit": "^1.7.1",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/node": "^15.14.9",
|
||||
"@types/react": "^17.0.27",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-redux": "^7.1.21",
|
||||
"@types/remote-redux-devtools": "^0.5.5",
|
||||
"@types/styled-components": "^5.1.14",
|
||||
"chalk": "^4.1.2",
|
||||
"electron-acrylic-window": "^0.5.9",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
"electron-localshortcut": "^3.2.1",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"electron-store": "^8.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-redux": "^7.2.5",
|
||||
"react-select": "^5.1.0",
|
||||
"redux": "^4.1.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-persist-electron-storage": "^2.1.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"screenz": "^1.0.0",
|
||||
"serialport": "^9.2.4",
|
||||
"styled-components": "^5.3.1",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/preset-env": "^7.15.6",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@types/react-select": "^5.0.0",
|
||||
"@types/serialport": "^8.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"concurrently": "^6.3.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.3.0",
|
||||
"electron": "^13.5.1",
|
||||
"electron-builder": "^22.11.7",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.26.1",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.3.3",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"style-loader": "^3.3.0",
|
||||
"ts-loader": "^9.2.6",
|
||||
"wait-on": "^6.0.0",
|
||||
"webpack": "^5.57.1",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
},
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"bin": {
|
||||
"dashboard": "./bin/start.js"
|
||||
}
|
||||
}
|
3
scripts/installer.nsh
Normal file
|
@ -0,0 +1,3 @@
|
|||
!macro customInstall
|
||||
CreateShortcut "$SMSTARTUP\Heliox.lnk" "$INSTDIR\Heliox.exe"
|
||||
!macroend
|
35
src/App.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import React from "react";
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
import { registerIcons } from "@fluentui/react/lib/Styling";
|
||||
import { ChevronDownIcon, CancelIcon } from "@fluentui/react-icons-mdl2";
|
||||
import KnobSection from "./Components/KnobSection";
|
||||
import Settings from "./Components/Settings";
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
html {
|
||||
border-style: solid;
|
||||
border-color: #363636;
|
||||
border-width: 1px;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
border-bottom-style: hidden;
|
||||
border-right-style: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => {
|
||||
registerIcons({
|
||||
icons: {
|
||||
ChevronDown: <ChevronDownIcon />,
|
||||
Cancel: <CancelIcon />,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Settings />
|
||||
<GlobalStyle />
|
||||
<KnobSection />
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default App;
|
157
src/Components/Knob.tsx
Normal file
|
@ -0,0 +1,157 @@
|
|||
import React, { useRef, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { remote } from "electron";
|
||||
|
||||
const SvgHeight = 180;
|
||||
const steps = 5;
|
||||
const accentColor = remote.systemPreferences.getAccentColor();
|
||||
|
||||
const KnobSVG = styled.svg`
|
||||
.cls-1 {
|
||||
fill: #545353;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
font-size: 20px;
|
||||
fill: #fff;
|
||||
font-family: SegoeUI-Bold, Segoe UI;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
//TODO: change to styled theme
|
||||
fill: rgba(${parseInt(accentColor.substr(0, 2), 16)}, ${parseInt(accentColor.substr(2, 2), 16)}, ${parseInt(accentColor.substr(4, 2), 16)},${parseInt(accentColor.substr(6, 8), 16)});
|
||||
}
|
||||
#Status {
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Proptypes {
|
||||
increase: () => void;
|
||||
decrease: () => void;
|
||||
toggle: () => void;
|
||||
status: number;
|
||||
}
|
||||
|
||||
const Knob = ({
|
||||
increase, decrease, toggle, status,
|
||||
}: Proptypes) => {
|
||||
const overlayRef = useRef<SVGGElement>(null);
|
||||
const [isMouseDown, setisMouseDown] = useState(false);
|
||||
const [prevAngle, setPrevAngle] = useState(0);
|
||||
|
||||
const move = (e: React.MouseEvent<SVGElement, MouseEvent>) => {
|
||||
const relativePosX = e.nativeEvent.offsetX - SvgHeight / 2;
|
||||
const relativePosY = e.nativeEvent.offsetY - SvgHeight / 2;
|
||||
|
||||
const angleDegree = Math.atan2(relativePosY, relativePosX) * (180 / Math.PI);
|
||||
|
||||
let angle = 0;
|
||||
angle = angleDegree + 85;
|
||||
if (angle < 0) {
|
||||
angle = 360 + angle;
|
||||
} else if (angle > 360) {
|
||||
angle = 360 - angle;
|
||||
}
|
||||
|
||||
if (overlayRef.current !== null) {
|
||||
overlayRef.current.style.transformOrigin = "50% 50%";
|
||||
overlayRef.current.style.transform = `rotate(${angle}deg)`;
|
||||
}
|
||||
|
||||
if (angle % steps === Math.round(angle % steps)) {
|
||||
if (prevAngle < angle) {
|
||||
increase();
|
||||
} else if (prevAngle > angle) {
|
||||
decrease();
|
||||
}
|
||||
}
|
||||
setPrevAngle(angle);
|
||||
};
|
||||
|
||||
const mouseDownHandler: React.MouseEventHandler<SVGElement> = (e) => {
|
||||
if (e.button !== 2) {
|
||||
move(e);
|
||||
setisMouseDown(true);
|
||||
}
|
||||
};
|
||||
|
||||
const mouseUpHandler: React.MouseEventHandler<SVGElement> = () => {
|
||||
setisMouseDown(false);
|
||||
};
|
||||
|
||||
const mouseMoveHandler: React.MouseEventHandler<SVGElement> = (e) => {
|
||||
if (isMouseDown) {
|
||||
move(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<KnobSVG
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 141.73 141.73"
|
||||
height={`${SvgHeight}px`}
|
||||
onContextMenu={() => { toggle(); }}
|
||||
onMouseDown={mouseDownHandler}
|
||||
onMouseUp={mouseUpHandler}
|
||||
onMouseMove={mouseMoveHandler}
|
||||
>
|
||||
<g id="Base">
|
||||
<path
|
||||
className="cls-1"
|
||||
d="M352.12,273A70.87,70.87,0,1,0,423,343.84,70.86,70.86,0,0,0,352.12,273Zm0,120.48a49.61,49.61,0,1,1,49.61-49.61A49.61,49.61,0,0,1,352.12,393.45Z"
|
||||
transform="translate(-281.26 -272.97)"
|
||||
/>
|
||||
</g>
|
||||
<g id="Status">
|
||||
<text className="cls-2" transform="translate(70.41 77.41)" textAnchor="middle">
|
||||
{status}
|
||||
</text>
|
||||
</g>
|
||||
<g id="Overlay" ref={overlayRef}>
|
||||
<g>
|
||||
<path
|
||||
className="cls-3"
|
||||
d="M363.54,295.57"
|
||||
transform="translate(-281.26 -272.97)"
|
||||
/>
|
||||
<path
|
||||
className="cls-3"
|
||||
d="M363.54,295.57a49.44,49.44,0,0,0-11.42-1.34"
|
||||
transform="translate(-281.26 -272.97)"
|
||||
/>
|
||||
<path
|
||||
className="cls-3"
|
||||
d="M352.12,294.23"
|
||||
transform="translate(-281.26 -272.97)"
|
||||
/>
|
||||
<path
|
||||
className="cls-3"
|
||||
d="M368.76,275l-.33-.1v0Z"
|
||||
transform="translate(-281.26 -272.97)"
|
||||
/>
|
||||
<polygon
|
||||
className="cls-3"
|
||||
points="87.18 1.89 87.18 1.89 87.17 1.91 87.17 1.91 87.18 1.89"
|
||||
/>
|
||||
<polygon
|
||||
className="cls-3"
|
||||
points="82.28 22.58 87.17 1.91 87.17 1.91 82.28 22.58 82.28 22.58"
|
||||
/>
|
||||
<path
|
||||
className="cls-3"
|
||||
d="M368.76,275l-.33-.08A71.24,71.24,0,0,0,352.12,273a10.71,10.71,0,0,0-10.53,10.73,10.53,10.53,0,0,0,10.53,10.53,49.44,49.44,0,0,1,11.42,1.34v0a10.33,10.33,0,0,0,12.56-7.68A10.79,10.79,0,0,0,368.76,275Z"
|
||||
transform="translate(-281.26 -272.97)"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</KnobSVG>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Knob;
|
44
src/Components/KnobSection.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { useAppDispatch, useAppSelector } from "../redux/hooks";
|
||||
import { sendMessage } from "../redux/slices/serialConnectionSlice";
|
||||
import Knob from "./Knob";
|
||||
|
||||
const KnobContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 1rem;
|
||||
`;
|
||||
|
||||
const ids: number[] = [0, 1, 2, 3];
|
||||
|
||||
const KnobSection = () => {
|
||||
const serialConnection = useAppSelector((state) => state.serialConnection);
|
||||
const dispatch = useAppDispatch();
|
||||
const sendIncreaseHandler = (index: number) => {
|
||||
dispatch(sendMessage(`${index}i`));
|
||||
console.log(index);
|
||||
};
|
||||
const sendDecreaseHandler = (index: number) => {
|
||||
dispatch(sendMessage(`${index}d`));
|
||||
console.log(index);
|
||||
};
|
||||
const sendToggleHandler = (index: number) => {
|
||||
dispatch(sendMessage(`${index}t`));
|
||||
console.log(index);
|
||||
};
|
||||
return (
|
||||
<KnobContainer>
|
||||
{ids.map((id) => (
|
||||
<Knob
|
||||
key={id}
|
||||
increase={() => { sendIncreaseHandler(id); }}
|
||||
decrease={() => { sendDecreaseHandler(id); }}
|
||||
toggle={() => { sendToggleHandler(id); }}
|
||||
status={Math.floor((parseInt(serialConnection.message.split(",")[id], 10) / 255) * 100)}
|
||||
/>
|
||||
))}
|
||||
</KnobContainer>
|
||||
);
|
||||
};
|
||||
export default KnobSection;
|
104
src/Components/Settings.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import electron from "electron";
|
||||
import React, { useState } from "react";
|
||||
import { Dialog, DialogType, DialogFooter } from "@fluentui/react/lib/Dialog";
|
||||
import { DefaultButton } from "@fluentui/react/lib/Button";
|
||||
import { ThemeProvider, PartialTheme } from "@fluentui/react/lib/Theme";
|
||||
import { ComboBox, IComboBoxOption } from "@fluentui/react/lib/ComboBox";
|
||||
import SerialPort from "serialport";
|
||||
import { useAppDispatch, useAppSelector } from "../redux/hooks";
|
||||
import { connect, disconnect, setSerialPort } from "../redux/slices/serialConnectionSlice";
|
||||
|
||||
type Proptypes = {
|
||||
}
|
||||
const dialogStyles = { main: { maxWidth: 450 } };
|
||||
|
||||
const myTheme: PartialTheme = {
|
||||
palette: {
|
||||
themePrimary: "#0078d4",
|
||||
themeLighterAlt: "#eff6fc",
|
||||
themeLighter: "#deecf9",
|
||||
themeLight: "#c7e0f4",
|
||||
themeTertiary: "#71afe5",
|
||||
themeSecondary: "#2b88d8",
|
||||
themeDarkAlt: "#106ebe",
|
||||
themeDark: "#005a9e",
|
||||
themeDarker: "#004578",
|
||||
neutralLighterAlt: "#282828",
|
||||
neutralLighter: "#313131",
|
||||
neutralLight: "#3f3f3f",
|
||||
neutralQuaternaryAlt: "#484848",
|
||||
neutralQuaternary: "#4f4f4f",
|
||||
neutralTertiaryAlt: "#6d6d6d",
|
||||
neutralTertiary: "#c8c8c8",
|
||||
neutralSecondary: "#d0d0d0",
|
||||
neutralPrimaryAlt: "#dadada",
|
||||
neutralPrimary: "#ffffff",
|
||||
neutralDark: "#f4f4f4",
|
||||
black: "#f8f8f8",
|
||||
white: "#1f1f1f",
|
||||
},
|
||||
};
|
||||
|
||||
const modalProps = {
|
||||
titleAriaId: "labelId",
|
||||
subtitleAriaId: "subTextId",
|
||||
isBlocking: false,
|
||||
styles: dialogStyles,
|
||||
};
|
||||
const dialogContentProps = {
|
||||
type: DialogType.close,
|
||||
title: "Settings",
|
||||
closeButtonAriaLabel: "Close",
|
||||
};
|
||||
|
||||
const options: IComboBoxOption[] = [
|
||||
];
|
||||
|
||||
const fetchPorts = async () => {
|
||||
const data = await SerialPort.list();
|
||||
options.splice(0, options.length);
|
||||
data.forEach((comPort) => { options.push({ key: comPort.path, text: comPort.path }); });
|
||||
};
|
||||
|
||||
const Settings: React.FC<Proptypes> = () => {
|
||||
const [hidden, setHidden] = useState(true);
|
||||
const ipcHandler = () => {
|
||||
setHidden(false);
|
||||
};
|
||||
electron.ipcRenderer.on("show-settings", ipcHandler);
|
||||
const dispatch = useAppDispatch();
|
||||
const handlePortChange = async (option: IComboBoxOption | undefined) => {
|
||||
if (option !== null && option !== undefined) {
|
||||
dispatch(setSerialPort(option.key.toString()));
|
||||
}
|
||||
};
|
||||
fetchPorts();
|
||||
const selector = useAppSelector((state) => state);
|
||||
console.log(selector.serialConnection.port);
|
||||
return (
|
||||
<ThemeProvider theme={myTheme}>
|
||||
<Dialog
|
||||
hidden={hidden}
|
||||
onDismiss={() => { setHidden(true); }}
|
||||
dialogContentProps={dialogContentProps}
|
||||
modalProps={modalProps}
|
||||
>
|
||||
<ComboBox
|
||||
label="COM Port"
|
||||
options={options}
|
||||
onMenuOpen={() => { fetchPorts(); }}
|
||||
onChange={(_, option) => { handlePortChange(option); }}
|
||||
selectedKey={selector.serialConnection.port}
|
||||
disabled={selector.serialConnection.status.connected}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<DefaultButton primary disabled={selector.serialConnection.status.connected} onClick={() => { dispatch(connect()); }} text="Connect" />
|
||||
<DefaultButton disabled={!selector.serialConnection.status.connected} onClick={() => { dispatch(disconnect()); }} text="Disconnect" />
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
183
src/electron.js
Normal file
|
@ -0,0 +1,183 @@
|
|||
/* eslint global-require: off, no-console: off */
|
||||
const path = require("path");
|
||||
const { BrowserWindow } = require("electron-acrylic-window");
|
||||
|
||||
const {
|
||||
app, Tray, Menu, ipcMain,
|
||||
} = require("electron");
|
||||
const isDev = require("electron-is-dev");
|
||||
const screenz = require("screenz");
|
||||
const els = require("electron-localshortcut");
|
||||
const { default: installExtension, REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS } = require("electron-devtools-installer");
|
||||
|
||||
const windowWidth = 900;
|
||||
const windowHeight = 230;
|
||||
|
||||
let tray = null;
|
||||
let mainWindow = null;
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
const extensions = [REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS];
|
||||
|
||||
// Handle creating/removing shortcuts on Windows when installing/uninstalling
|
||||
if (require("electron-squirrel-startup")) {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
const RESOURCES_PATH = app.isPackaged
|
||||
? path.join(process.resourcesPath, "assets")
|
||||
: path.join(__dirname, "../assets");
|
||||
|
||||
const getAssetPath = (...paths) => path.join(RESOURCES_PATH, ...paths);
|
||||
|
||||
function createWindow() {
|
||||
// Create the browser window.
|
||||
const win = new BrowserWindow({
|
||||
width: windowWidth,
|
||||
height: windowHeight,
|
||||
x: screenz.width - windowWidth,
|
||||
y: screenz.height - windowHeight,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
contextIsolation: false,
|
||||
},
|
||||
transparent: true,
|
||||
movable: false,
|
||||
resizable: false,
|
||||
frame: false,
|
||||
show: false,
|
||||
skipTaskbar: true,
|
||||
alwaysOnTop: true,
|
||||
vibrancy: {
|
||||
effect: "acrylic",
|
||||
useCustomWindowRefreshMethod: true,
|
||||
disableOnBlur: false,
|
||||
},
|
||||
});
|
||||
|
||||
// and load the index.html of the app.
|
||||
win.loadURL(
|
||||
isDev
|
||||
? "http://localhost:8420"
|
||||
: `file://${path.join(__dirname, "../build/index.html")}`,
|
||||
);
|
||||
|
||||
if (isDev) {
|
||||
win.webContents.openDevTools({ mode: "detach" });
|
||||
}
|
||||
|
||||
// Register shortcut to open devtools
|
||||
els.register(win, "Ctrl+Shift+I", () => {
|
||||
if (win.webContents.isDevToolsOpened()) {
|
||||
win.webContents.closeDevTools();
|
||||
} else {
|
||||
win.webContents.openDevTools({ mode: "detach" });
|
||||
}
|
||||
});
|
||||
|
||||
return win;
|
||||
}
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// Hide window when out of focus
|
||||
app.on("browser-window-blur", () => {
|
||||
if (!isDev) {
|
||||
mainWindow.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
|
||||
function createTray() {
|
||||
const trayIcon = new Tray(getAssetPath("tray/tray-yellow-32.png"));
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Show",
|
||||
click: () => {
|
||||
mainWindow.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
click: () => {
|
||||
mainWindow.webContents.send("show-settings", true);
|
||||
mainWindow.show();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Exit",
|
||||
click: () => {
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
trayIcon.setToolTip("Heliox");
|
||||
trayIcon.setContextMenu(contextMenu);
|
||||
return trayIcon;
|
||||
}
|
||||
|
||||
// Force single app instance
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
app.on("second-instance", () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
app.on("ready", async () => {
|
||||
app.allowRendererProcessReuse = false;
|
||||
mainWindow = createWindow();
|
||||
|
||||
console.log("Installing extensions");
|
||||
installExtension(extensions)
|
||||
.then((name) => console.log(`Added Extension: ${name}`))
|
||||
.catch((err) => console.log("An error occurred: ", err));
|
||||
|
||||
const ElectronStore = require("electron-store");
|
||||
ElectronStore.initRenderer();
|
||||
|
||||
mainWindow.setMenu(null);
|
||||
|
||||
mainWindow.webContents.on("did-frame-finish-load", () => {
|
||||
// Create tray icon with context menu
|
||||
tray = createTray();
|
||||
mainWindow.setBounds({ y: (screenz.height - mainWindow.getBounds().height) - tray.getBounds().height });
|
||||
|
||||
// Listen to tray icon onclick event
|
||||
tray.on("click", () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
ipcMain.on("asynchronous-message", (event, arg) => {
|
||||
if (arg === "green") {
|
||||
tray.setImage(getAssetPath("tray/tray-green-32.png"));
|
||||
} else if (arg === "red") {
|
||||
tray.setImage(getAssetPath("tray/tray-red-32.png"));
|
||||
} else if (arg === "yellow") {
|
||||
tray.setImage(getAssetPath("tray/tray-yellow-32.png"));
|
||||
} else if (arg === "default") {
|
||||
tray.setImage(getAssetPath("tray/tray-default-32.png"));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
11
src/index.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Heliox</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="portal"></div>
|
||||
</body>
|
||||
</html>
|
16
src/index.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from "react";
|
||||
import { render } from "react-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import { PersistGate } from "redux-persist/integration/react";
|
||||
import store, { persistor } from "./redux/store";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<App />
|
||||
</PersistGate>
|
||||
</Provider>,
|
||||
document.getElementById("root"),
|
||||
);
|
10
src/interfaces/ISerialConnectionState.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default interface ISerialConnectionState {
|
||||
port: string;
|
||||
message: string;
|
||||
status: {
|
||||
connecting: boolean;
|
||||
connected: boolean;
|
||||
error: string;
|
||||
};
|
||||
// eslint-disable-next-line semi
|
||||
}
|
6
src/redux/hooks/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch, RootState } from "../store/types.d";
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
10
src/redux/middlewares/logger.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Middleware } from "@reduxjs/toolkit";
|
||||
import { RootState } from "../store/types.d";
|
||||
|
||||
const logger: Middleware<{}, RootState> = (store) => (next) => (action) => {
|
||||
console.log(action.type);
|
||||
console.log(store.getState());
|
||||
return next(action);
|
||||
};
|
||||
|
||||
export default logger;
|
74
src/redux/middlewares/serialConnection.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { Middleware, Action, createAction } from "@reduxjs/toolkit";
|
||||
import electron from "electron";
|
||||
import { REHYDRATE } from "redux-persist";
|
||||
import { RootState } from "../store/types.d";
|
||||
import {
|
||||
connect, disconnect, setSerialPort, setMessage, sendMessage, connectionStart, connectionFailure, connectionSuccess, connectionEnd,
|
||||
} from "../slices/serialConnectionSlice";
|
||||
import PortController from "../../serial/PortController";
|
||||
|
||||
let serialPort: PortController|null = null;
|
||||
|
||||
const serialConnection: Middleware<{}, RootState> = (state) => (next) => (action: Action) => {
|
||||
const errorCalbackHandler = (error: Error | null | undefined) => {
|
||||
if (error === null || error === undefined) {
|
||||
state.dispatch(connectionSuccess());
|
||||
} else {
|
||||
state.dispatch(connectionFailure(error.message));
|
||||
}
|
||||
};
|
||||
const closeCalbackHandler = (error: Error | null | undefined) => {
|
||||
if (error !== null && error !== undefined) {
|
||||
if (error.message === "Reading from COM port (ReadIOCompletion): Access denied") {
|
||||
state.dispatch(connectionFailure(`Device ${state.getState().serialConnection.port} disconnected`));
|
||||
state.dispatch(connectionEnd());
|
||||
}
|
||||
}
|
||||
};
|
||||
const serialDataListener = (msg: string) => {
|
||||
if (msg !== state.getState().serialConnection.message) {
|
||||
state.dispatch(setMessage(msg));
|
||||
}
|
||||
};
|
||||
|
||||
const connection = { serialConnection: state.getState().serialConnection };
|
||||
const rehydrate = createAction < typeof connection >(REHYDRATE);
|
||||
if (rehydrate.match(action)) {
|
||||
state.dispatch(setSerialPort(action.payload.serialConnection.port));
|
||||
state.dispatch(connect());
|
||||
}
|
||||
|
||||
if (connect.match(action)) {
|
||||
state.dispatch(connectionStart());
|
||||
if (state.getState().serialConnection.port === "" || serialPort === null) {
|
||||
state.dispatch(connectionFailure("No Serial Port set"));
|
||||
return next(action);
|
||||
}
|
||||
if (serialPort.isOpen) {
|
||||
state.dispatch(disconnect());
|
||||
}
|
||||
serialPort?.open(errorCalbackHandler, closeCalbackHandler);
|
||||
serialPort?.parser.on("data", serialDataListener);
|
||||
} else if (connectionSuccess.match(action)) {
|
||||
electron.ipcRenderer.send("asynchronous-message", "default");
|
||||
} else if (connectionFailure.match(action)) {
|
||||
electron.ipcRenderer.send("asynchronous-message", "red");
|
||||
const { Notification } = electron.remote;
|
||||
new Notification({ title: "Connection Failure", body: action.payload }).show();
|
||||
} else if (disconnect.match(action)) {
|
||||
serialPort?.close();
|
||||
state.dispatch(connectionEnd());
|
||||
} else if (connectionEnd.match(action)) {
|
||||
electron.ipcRenderer.send("asynchronous-message", "yellow");
|
||||
} else if (setSerialPort.match(action)) {
|
||||
if (serialPort?.isOpen) {
|
||||
serialPort.close();
|
||||
}
|
||||
serialPort = new PortController(action.payload);
|
||||
} else if (sendMessage.match(action)) {
|
||||
serialPort?.write(action.payload);
|
||||
}
|
||||
return next(action);
|
||||
};
|
||||
|
||||
export default serialConnection;
|
62
src/redux/slices/serialConnectionSlice.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import ISerialConnectionState from "../../interfaces/ISerialConnectionState";
|
||||
// import type { RootState } from "../store/types.d";
|
||||
|
||||
// Define the initial state using that type
|
||||
const initialState: ISerialConnectionState = {
|
||||
port: "",
|
||||
message: "0,0,0,0",
|
||||
status: {
|
||||
connecting: false,
|
||||
connected: false,
|
||||
error: "",
|
||||
},
|
||||
};
|
||||
|
||||
export const serialConnectionSlice = createSlice({
|
||||
name: "serialConnection",
|
||||
initialState,
|
||||
reducers: {
|
||||
setSerialPort: (state, action: PayloadAction<string>) => {
|
||||
state.port = action.payload;
|
||||
},
|
||||
connectionStart: (state) => {
|
||||
state.status.connecting = true;
|
||||
state.status.connected = false;
|
||||
state.status.error = "";
|
||||
},
|
||||
connectionSuccess: (state) => {
|
||||
state.status.connecting = false;
|
||||
state.status.connected = true;
|
||||
state.status.error = "";
|
||||
},
|
||||
connectionFailure: (state, action: PayloadAction<string>) => {
|
||||
state.status.connecting = false;
|
||||
state.status.connected = false;
|
||||
state.status.error = action.payload;
|
||||
},
|
||||
connectionEnd: (state) => {
|
||||
state.status.connecting = false;
|
||||
state.status.connected = false;
|
||||
},
|
||||
connect: () => {},
|
||||
disconnect: () => {},
|
||||
setMessage: (state, action: PayloadAction<string>) => {
|
||||
state.message = action.payload;
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
sendMessage: (_state, _action: PayloadAction<string>) => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setSerialPort, connectionStart, connectionSuccess, connectionFailure, connectionEnd,
|
||||
connect, disconnect, setMessage, sendMessage,
|
||||
} = serialConnectionSlice.actions;
|
||||
|
||||
// Other code such as selectors can use the imported `RootState` type
|
||||
// export const selectCount = (state: RootState) => state.counter.value;
|
||||
|
||||
export default serialConnectionSlice.reducer;
|
48
src/redux/store/index.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import {
|
||||
persistStore,
|
||||
persistReducer,
|
||||
FLUSH,
|
||||
REHYDRATE,
|
||||
PAUSE,
|
||||
PERSIST,
|
||||
PURGE,
|
||||
REGISTER,
|
||||
} from "redux-persist";
|
||||
import createElectronStorage from "redux-persist-electron-storage";
|
||||
import ElectronStore from "electron-store";
|
||||
import serialConnection from "../middlewares/serialConnection";
|
||||
import rootReducer from "./root-reducer";
|
||||
|
||||
export const electronStore = new ElectronStore();
|
||||
|
||||
createElectronStorage({
|
||||
electronStore,
|
||||
});
|
||||
|
||||
const persistConfig = {
|
||||
key: "serialConnection",
|
||||
storage: createElectronStorage({
|
||||
electronStore,
|
||||
}),
|
||||
};
|
||||
|
||||
export const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||
|
||||
const middlewares = [serialConnection];
|
||||
|
||||
const store = configureStore(
|
||||
{
|
||||
reducer: persistedReducer,
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
||||
},
|
||||
}).concat(middlewares),
|
||||
devTools: true,
|
||||
},
|
||||
);
|
||||
|
||||
export default store;
|
||||
|
||||
export const persistor = persistStore(store);
|
8
src/redux/store/root-reducer.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { combineReducers } from "redux";
|
||||
import SerialConnectionReducer from "../slices/serialConnectionSlice";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
serialConnection: SerialConnectionReducer,
|
||||
});
|
||||
|
||||
export default rootReducer;
|
5
src/redux/store/types.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
import store from ".";
|
||||
import rootReducer from "./root-reducer";
|
||||
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
55
src/serial/PortController.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import SerialPort, { parsers } from "serialport";
|
||||
|
||||
class PortController {
|
||||
path: string;
|
||||
|
||||
port: SerialPort;
|
||||
|
||||
parser: any;
|
||||
|
||||
onCloseCallback: (error:Error)=>void;
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
this.port = new SerialPort(path, {
|
||||
baudRate: 9600,
|
||||
autoOpen: false,
|
||||
});
|
||||
this.parser = this.port.pipe(new parsers.Readline({ delimiter: "\r\n" }));
|
||||
this.onCloseCallback = () => {};
|
||||
}
|
||||
|
||||
get isOpen(): boolean {
|
||||
if (this.port == null) {
|
||||
return false;
|
||||
}
|
||||
return this.port.isOpen;
|
||||
}
|
||||
|
||||
get getParser(): any {
|
||||
return this.parser;
|
||||
}
|
||||
|
||||
open(callback: (error:Error | null | undefined)=>void, onCloseCallback:(error:Error | null | undefined)=>void) {
|
||||
if (this.isOpen) {
|
||||
throw new Error("Port already open");
|
||||
}
|
||||
|
||||
this.port.open((error) => callback(error));
|
||||
this.port.on("close", onCloseCallback);
|
||||
this.onCloseCallback = onCloseCallback;
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.port) {
|
||||
this.port.close();
|
||||
this.port.removeListener("data", this.onCloseCallback);
|
||||
}
|
||||
}
|
||||
|
||||
write(message: string) {
|
||||
this.port.write(message);
|
||||
}
|
||||
}
|
||||
|
||||
export default PortController;
|
14
styled.d.ts
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
// import original module declarations
|
||||
import "styled-components";
|
||||
|
||||
// and extend them!
|
||||
declare module "styled-components" {
|
||||
export interface DefaultTheme {
|
||||
borderRadius: string;
|
||||
|
||||
colors: {
|
||||
main: string;
|
||||
secondary: string;
|
||||
};
|
||||
}
|
||||
}
|
24
tsconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "CommonJS",
|
||||
"lib": ["dom", "esnext"],
|
||||
"declaration": false,
|
||||
"noEmit": false,
|
||||
"jsx": "react",
|
||||
"strict": true,
|
||||
"pretty": true,
|
||||
"sourceMap": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["src", "assets/assets.d.ts"],
|
||||
"exclude": ["src/electron.js"]
|
||||
}
|
79
webpack.config.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const nodeModules = { };
|
||||
fs.readdirSync("node_modules")
|
||||
.filter((x) => [".bin"].indexOf(x) === -1)
|
||||
.forEach((mod) => {
|
||||
nodeModules[mod] = `commonjs ${mod}`;
|
||||
});
|
||||
|
||||
return {
|
||||
entry: "./src/index.tsx",
|
||||
output: {
|
||||
path: path.join(__dirname, "build"),
|
||||
filename: "index.bundle.js",
|
||||
},
|
||||
mode: process.env.NODE_ENV || "development",
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".js"],
|
||||
},
|
||||
devtool: "source-map",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: ["babel-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(css|scss)$/,
|
||||
use: ["style-loader", "css-loader"],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jp(e*)g|svg|gif)$/,
|
||||
use: [
|
||||
{
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
name: "assets/[hash]-[name].[ext]",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(__dirname, "src", "index.html"),
|
||||
}),
|
||||
new ForkTsCheckerWebpackPlugin(),
|
||||
{
|
||||
apply: (compiler) => {
|
||||
compiler.hooks.done.tap("DonePlugin", () => {
|
||||
if (argv.mode === "production") {
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
target: "electron-renderer",
|
||||
externals: nodeModules,
|
||||
};
|
||||
};
|