Improve repo directory structure

This commit is contained in:
GHOSCHT 2022-04-09 20:07:19 +02:00
commit 30fc7140eb
51 changed files with 25142 additions and 0 deletions

50
.eslintignore Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
{
"editor.formatOnSave": false
}

22
README.md Normal file
View 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
View file

@ -0,0 +1,4 @@
declare module "*.svg" {
const content: any;
export default content;
}

6630
assets/icons/Icon.ai Normal file

File diff suppressed because it is too large Load diff

BIN
assets/icons/Icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/icons/mac/icon.icns Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/icons/png/16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

BIN
assets/icons/png/24x24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
assets/icons/png/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

BIN
assets/icons/png/48x48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/icons/png/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/icons/win/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

6605
assets/knob/Knob.ai Normal file

File diff suppressed because one or more lines are too long

1
assets/knob/Knob.svg Normal file
View 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

File diff suppressed because one or more lines are too long

1879
assets/tray/Template.ai Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

BIN
assets/tray/tray-red-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

375
assets/tray/tray.ai Normal file

File diff suppressed because one or more lines are too long

4
babel.config.json Normal file
View file

@ -0,0 +1,4 @@
{
"presets": ["@babel/env", "@babel/react", "@babel/preset-typescript"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}

121
package.json Normal file
View 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
View file

@ -0,0 +1,3 @@
!macro customInstall
CreateShortcut "$SMSTARTUP\Heliox.lnk" "$INSTDIR\Heliox.exe"
!macroend

35
src/App.tsx Normal file
View 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
View 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;

View 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
View 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
View 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
View 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
View 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"),
);

View 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
View 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;

View 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;

View 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;

View 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
View 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);

View 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
View 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;

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

8044
yarn.lock Normal file

File diff suppressed because it is too large Load diff