Browse Source

Update project

Bootstrap to later Electron version.
master
TheoryOfNekomata 8 months ago
parent
commit
d181818ff7
63 changed files with 5652 additions and 13263 deletions
  1. +9
    -11
      .editorconfig
  2. +4
    -0
      .eslintignore
  3. +9
    -0
      .eslintrc.cjs
  4. +5
    -94
      .gitignore
  5. +1
    -0
      .npmrc
  6. +6
    -0
      .prettierignore
  7. +0
    -11
      .prettierrc
  8. +4
    -0
      .prettierrc.yaml
  9. +0
    -21
      LICENSE
  10. +18
    -25
      README.md
  11. +12
    -0
      build/entitlements.mac.plist
  12. BIN
      build/icon.icns
  13. BIN
      build/icon.ico
  14. BIN
      build/icon.png
  15. +3
    -0
      dev-app-update.yml
  16. BIN
      docs/screenshot.png
  17. +43
    -0
      electron-builder.yml
  18. +20
    -0
      electron.vite.config.ts
  19. +41
    -60
      package.json
  20. +4793
    -0
      pnpm-lock.yaml
  21. BIN
      public/favicon.png
  22. +0
    -109
      public/index.html
  23. BIN
      public/logo192.png
  24. BIN
      public/logo512.png
  25. +0
    -25
      public/manifest.json
  26. +0
    -3
      public/robots.txt
  27. +0
    -39
      public/style.css
  28. BIN
      resources/icon.png
  29. +0
    -9
      src/App.test.tsx
  30. +0
    -39
      src/App.tsx
  31. +0
    -56
      src/components/Keyboard/Keyboard.tsx
  32. +0
    -115
      src/components/PedalBoard/PedalBoard.tsx
  33. +0
    -261
      src/electron.ts
  34. +159
    -0
      src/main/index.ts
  35. +8
    -0
      src/preload/index.d.ts
  36. +22
    -0
      src/preload/index.ts
  37. +0
    -4
      src/react-app-env.d.ts
  38. +16
    -0
      src/renderer/index.html
  39. +322
    -0
      src/renderer/src/App.tsx
  40. +10
    -0
      src/renderer/src/assets/electron.svg
  41. +85
    -0
      src/renderer/src/assets/main.css
  42. +25
    -0
      src/renderer/src/assets/wavy-lines.svg
  43. +1
    -0
      src/renderer/src/env.d.ts
  44. +7
    -7
      src/renderer/src/main.tsx
  45. +0
    -149
      src/serviceWorker.ts
  46. +0
    -8
      src/services/Config.ts
  47. +0
    -7
      src/services/colors.json
  48. +0
    -15
      src/services/getKeyName.ts
  49. +0
    -7
      src/services/getKeyOctave.ts
  50. +0
    -15
      src/services/getNaturalKeyCount.ts
  51. +0
    -64
      src/services/isNaturalKey.test.ts
  52. +0
    -21
      src/services/isNaturalKey.ts
  53. +0
    -14
      src/services/keyNames.json
  54. +0
    -17
      src/services/messages.json
  55. +0
    -12
      src/services/messages.ts
  56. +0
    -4
      src/services/scaleFactors.json
  57. +0
    -34
      src/services/spans.json
  58. +0
    -3
      src/services/themes.json
  59. +0
    -5
      src/setupTests.ts
  60. +2
    -23
      tsconfig.json
  61. +8
    -0
      tsconfig.node.json
  62. +19
    -0
      tsconfig.web.json
  63. +0
    -11976
      yarn.lock

+ 9
- 11
.editorconfig View File

@@ -1,11 +1,9 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 120
tab_width = 8
trim_trailing_whitespace = true
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

+ 4
- 0
.eslintignore View File

@@ -0,0 +1,4 @@
node_modules
dist
out
.gitignore

+ 9
- 0
.eslintrc.cjs View File

@@ -0,0 +1,9 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'@electron-toolkit/eslint-config-ts/recommended',
'@electron-toolkit/eslint-config-prettier'
]
}

+ 5
- 94
.gitignore View File

@@ -1,96 +1,7 @@
.DS_Store
.AppleDouble
.LSOverride
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
.idea/
cmake-build-*/
*.iws
out/
.idea_modules/
atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.vscode/
*.code-workspace
.history/
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
*.stackdump
[Dd]esktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msix
*.msm
*.msp
*.lnk
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
pids
*.pid
*.seed
*.pid.lock
lib-cov
coverage
*.lcov
.nyc_output
.grunt
bower_components
.lock-wscript
build/Release
node_modules/
jspm_packages/
web_modules/
*.tsbuildinfo
.npm
.eslintcache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
.node_repl_history
*.tgz
.yarn-integrity
.env
.env.test
.cache
.parcel-cache
.next
.nuxt
node_modules
dist
.cache/
.vuepress/dist
.serverless/
.fusebox/
.dynamodb/
.tern-port
.vscode-test
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.pnp.*
build
public/electron.js
public/services
out
.DS_Store
*.log*
config.json
.idea/

+ 1
- 0
.npmrc View File

@@ -0,0 +1 @@
shamefully-hoist=true

+ 6
- 0
.prettierignore View File

@@ -0,0 +1,6 @@
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json

+ 0
- 11
.prettierrc View File

@@ -1,11 +0,0 @@
{
"printWidth": 120,
"semi": false,
"singleQuote": true,
"jsxSingleQuote": false,
"trailingComma": "all",
"arrowParens": "always",
"jsxBracketSameLine": false,
"quoteProps": "as-needed",
"endOfLine": "lf"
}

+ 4
- 0
.prettierrc.yaml View File

@@ -0,0 +1,4 @@
singleQuote: true
semi: false
printWidth: 100
trailingComma: none

+ 0
- 21
LICENSE View File

@@ -1,21 +0,0 @@
MIT License

Copyright (c) 2020 TheoryOfNekomata

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.

+ 18
- 25
README.md View File

@@ -1,37 +1,30 @@
# piano-midi-monitor
# Piano MIDI Monitor

Simple monitor for displaying MIDI status for digital pianos.
An Electron application with React and TypeScript

Supports a **full MIDI key range**, as well as
granular pedal status display for **soft pedal/una corda** (MIDI CC number 67),
**sostenuto** (MIDI CC number 66), and **sustain** (MIDI CC number 64, values from 0-127).
## Project Setup

![Screenshot](./docs/screenshot.png)
### Install

Tested with Node v.14.1.0.

## Instructions

### Building

Just run:

```shell script
yarn build
```bash
$ pnpm install
```

A directory `dist/` should be generated along with build output.

### Development

Just run:

```shell script
yarn start
```bash
$ pnpm dev
```

Create React App should run in watch mode, then Electron should spawn the application window shortly.
### Build

## License
```bash
# For windows
$ pnpm build:win

[MIT License](./LICENSE)
# For macOS
$ pnpm build:mac

# For Linux
$ pnpm build:linux
```

+ 12
- 0
build/entitlements.mac.plist View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
</dict>
</plist>

BIN
build/icon.icns View File


BIN
build/icon.ico View File

Before After

BIN
build/icon.png View File

Before After
Width: 512  |  Height: 512  |  Size: 35 KiB

+ 3
- 0
dev-app-update.yml View File

@@ -0,0 +1,3 @@
provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: piano-midi-monitor-electron-updater

BIN
docs/screenshot.png View File

Before After
Width: 2718  |  Height: 424  |  Size: 166 KiB

+ 43
- 0
electron-builder.yml View File

@@ -0,0 +1,43 @@
appId: com.electron.app
productName: piano-midi-monitor-electron
directories:
buildResources: build
files:
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
asarUnpack:
- resources/**
win:
executableName: piano-midi-monitor-electron
nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
entitlementsInherit: build/entitlements.mac.plist
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates

+ 20
- 0
electron.vite.config.ts View File

@@ -0,0 +1,20 @@
import { resolve } from 'path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()]
},
preload: {
plugins: [externalizeDepsPlugin()]
},
renderer: {
resolve: {
alias: {
'@renderer': resolve('src/renderer/src')
}
},
plugins: [react()]
}
})

+ 41
- 60
package.json View File

@@ -1,67 +1,48 @@
{
"name": "@theoryofnekomata/piano-midi-monitor",
"description": "Simple monitor for displaying MIDI status for digital pianos.",
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com> (https://modal.sh)",
"version": "0.1.0",
"main": "public/electron.js",
"private": true,
"license": "MIT",
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@theoryofnekomata/react-musical-keyboard": "1.0.7",
"@types/jest": "^24.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"electron-is-dev": "^1.2.0",
"midi": "^1.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
"typescript": "~3.7.2",
"yargs": "^15.4.1"
},
"name": "piano-midi-monitor",
"version": "1.0.0",
"description": "Display notes and pedaling from your MIDI inputs.",
"main": "./out/main/index.js",
"author": "TheoryOfNekomata <allan.crisostomo@outlook.com>",
"homepage": "https://modal.sh",
"scripts": {
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"start": "concurrently \"BROWSER=none react-scripts start\" \"wait-on http://localhost:3000 && electron .\"",
"test": "react-scripts test",
"compile": "tsc src/electron.ts --resolveJsonModule --esModuleInterop --outDir public/",
"rebuild": "electron-rebuild -f -w midi",
"prebuild": "react-scripts build",
"build": "electron-builder"
},
"eslintConfig": {
"extends": "react-app"
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@theoryofnekomata/react-musical-keyboard": "1.0.13",
"electron-updater": "^6.1.7"
},
"devDependencies": {
"@types/node": "12",
"concurrently": "^5.3.0",
"electron": "^9.2.0",
"electron-builder": "^22.8.0",
"fast-check": "^2.1.0",
"wait-on": "^5.1.0"
},
"build": {
"files": [
"./build/**/*",
"./node_modules/**/*"
],
"appId": "sh.modal.apps.pianomidimonitor",
"productName": "Piano MIDI Monitor",
"copyright": "Copyright © 2020 TheoryOfNekomata"
},
"homepage": "./"
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1",
"@types/node": "^18.19.9",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"electron": "^28.2.0",
"electron-builder": "^24.9.1",
"electron-vite": "^2.0.0",
"eslint": "^8.56.0",
"eslint-plugin-react": "^7.33.2",
"prettier": "^3.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "^5.3.3",
"vite": "^5.0.12"
}
}

+ 4793
- 0
pnpm-lock.yaml
File diff suppressed because it is too large
View File


BIN
public/favicon.png View File

Before After
Width: 160  |  Height: 160  |  Size: 7.8 KiB

+ 0
- 109
public/index.html View File

@@ -1,109 +0,0 @@
<!DOCTYPE html>
<html
lang="en-PH"
style="
--size-close-button: 2rem;
">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.

Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="stylesheet" href="%PUBLIC_URL%/style.css" />
<title>Piano MIDI Monitor</title>
</head>
<body>
<button
id="close"
class="theme button"
style="
display: block;
position: fixed;
line-height: 0;
top: 0;
right: 0;
padding: 0;
border: 0;
height: var(--size-close-button, 2rem);
width: var(--size-close-button, 2rem);
text-align: center;
outline: 0;
"
>
<span
style="
display: block;
background-color: currentColor;
width: 50%;
height: 0.125rem;
margin: 0 auto;
transform: rotate(45deg);
"
></span>
<span
style="
display: block;
background-color: currentColor;
width: 50%;
height: 0.125rem;
margin: 0 auto;
margin-top: -0.125rem;
transform: rotate(-45deg);
"
></span>
</button>
<noscript>You need to enable JavaScript to run this app.</noscript>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.

To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script>
((window, electron) => {
const { ipcRenderer, } = electron
const EVENTS = [
'note',
'pedal',
'spanchange',
]

EVENTS.forEach(event => {
ipcRenderer.on(event, (_, message) => {
window.postMessage({
event,
message
})
})
})

window.document.getElementById('close').addEventListener('click', () => {
ipcRenderer.send('quit')
})
})(window, require('electron'))
</script>
</body>
</html>

BIN
public/logo192.png View File

Before After
Width: 192  |  Height: 192  |  Size: 5.2 KiB

BIN
public/logo512.png View File

Before After
Width: 512  |  Height: 512  |  Size: 9.4 KiB

+ 0
- 25
public/manifest.json View File

@@ -1,25 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

+ 0
- 3
public/robots.txt View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

+ 0
- 39
public/style.css View File

@@ -1,39 +0,0 @@
html {
width: 100%;
height: 100%;
-webkit-app-region: drag;
}

body {
margin: 0;
width: 100%;
height: 100%;
}

main {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 1fr auto;
align-items: stretch;
}

.theme {
color: black;
background-color: white;
}

.button {
opacity: 0.5;
}

.button:hover {
opacity: 1;
}

@media (prefers-color-scheme: dark) {
.theme {
color: white;
background-color: black;
}
}

BIN
resources/icon.png View File

Before After
Width: 512  |  Height: 512  |  Size: 35 KiB

+ 0
- 9
src/App.test.tsx View File

@@ -1,9 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

+ 0
- 39
src/App.tsx View File

@@ -1,39 +0,0 @@
import * as React from 'react'
import Keyboard from './components/Keyboard/Keyboard'
import PedalBoard from './components/PedalBoard/PedalBoard'

const search = new URLSearchParams(window.location.search)

const App = () => {
const [startKey, setStartKey, ] = React.useState<number>(Number(search.get('startKey')))
const [endKey, setEndKey, ] = React.useState<number>(Number(search.get('endKey')))

React.useEffect(() => {
const onMessage = (e: MessageEvent) => {
if (e.data.event !== 'spanchange') {
return
}

const [startKey, endKey, ] = e.data.message.split(':')
setStartKey(Number(startKey))
setEndKey(Number(endKey))
}

window.addEventListener('message', onMessage)
return () => {
window.removeEventListener('message', onMessage)
}
}, [])

return (
<React.Fragment>
<Keyboard
startKey={startKey}
endKey={endKey}
/>
<PedalBoard />
</React.Fragment>
)
}

export default App;

+ 0
- 56
src/components/Keyboard/Keyboard.tsx View File

@@ -1,56 +0,0 @@
import * as React from 'react'
import KeyboardBase, { StyledAccidentalKey, StyledNaturalKey, } from '@theoryofnekomata/react-musical-keyboard'

const NaturalKey = React.memo(StyledNaturalKey)
const AccidentalKey = React.memo(StyledAccidentalKey)

const Keyboard = ({
startKey = 21,
endKey = 108,
}) => {
const [keyChannels, setKeyChannels, ] = React.useState<any[]>([])

React.useEffect(() => {
const onMessage = (e: MessageEvent) => {
if (e.data.event !== 'note') {
return
}

const [key, velocity, ] = e.data.message.split(':')
setKeyChannels(theKeyChannels => {
if (velocity > 0) {
return [
...theKeyChannels,
{
channel: 0,
key: Number(key),
velocity: Number(velocity),
},
]
}

return theKeyChannels.filter(k => k.key !== Number(key))
})
}

window.addEventListener('message', onMessage)
return () => {
window.removeEventListener('message', onMessage)
}
}, [])

return (
<KeyboardBase
height="100%"
startKey={startKey}
endKey={endKey}
keyChannels={keyChannels}
keyComponents={{
natural: NaturalKey,
accidental: AccidentalKey,
}}
/>
)
}

export default Keyboard

+ 0
- 115
src/components/PedalBoard/PedalBoard.tsx
File diff suppressed because it is too large
View File


+ 0
- 261
src/electron.ts View File

@@ -1,261 +0,0 @@
import { app, BrowserWindow, Menu, ipcMain, } from 'electron'
import path from 'path'
import fs from 'fs'

import SPANS from './services/spans.json'
import SCALE_FACTORS from './services/scaleFactors.json'
import THEMES from './services/themes.json'
import COLORS from './services/colors.json'
import { _ } from './services/messages'
import getKeyName from './services/getKeyName'
import getNaturalKeyCount from './services/getNaturalKeyCount'
import Config from './services/Config'
// @ts-ignore
import electronIsDev = require('electron-is-dev')
// @ts-ignore
import midi = require('midi')
// @ts-ignore

const WINDOW_HEIGHT = 100

let config: Config = {
startKey: 21,
endKey: 108,
scaleFactor: 1,
colorNaturalKey: undefined,
colorAccidentalKey: undefined,
colorHighlight: undefined,
}

const createWindow = () => {
const KEYS_WIDTH = getNaturalKeyCount(config.startKey, config.endKey) * 20
const WINDOW_WIDTH = KEYS_WIDTH + 207
const win = new BrowserWindow({
width: WINDOW_WIDTH,
height: WINDOW_HEIGHT,
useContentSize: true,
backgroundColor: '#000000',
resizable: false,
minimizable: false,
maximizable: false,
fullscreenable: false,
frame: false,
webPreferences: {
devTools: electronIsDev,
nodeIntegration: true,
},
})

const baseUrl: string = (
electronIsDev
? 'http://localhost:3000'
: `file:///${path.join(__dirname, '../build/index.html')}`
)

const search = new URLSearchParams(config as Record<string, string>)
const url = new URL(baseUrl)
url.search = search.toString()
win.loadURL(url.toString())

if (electronIsDev) {
win.webContents.openDevTools()
}

const input = new midi.Input()

if (input.getPortCount() > 0) {
input.openPort(0)
}

input.on('message', (deltaTime: number, message: unknown[]) => {
const [type, data1, data2] = message
switch (type) {
case 144:
win.webContents.send('note', data1 + ':' + data2)
break
case 176:
win.webContents.send('pedal', data1 + ':' + data2)
break
}
})
}

const reload = () => {
app.relaunch({
args: process.argv.slice(1).concat([
`--startKey=${config.startKey}`,
`--endKey=${config.endKey}`,
`--scaleFactor=${config.scaleFactor}`,
`--colorNaturalKey=${config.colorNaturalKey}`,
`--colorAccidentalKey=${config.colorAccidentalKey}`,
`--colorHighlight=${config.colorHighlight}`,
])
})
app.exit(0)
}

app
.whenReady()
.then(() => {
try {
const configJsonRaw = fs.readFileSync(path.join(app.getPath('userData'), 'config.json')).toString('utf-8')
config = JSON.parse(configJsonRaw)
} catch (e) {
config = {
startKey: 21,
endKey: 108,
scaleFactor: 1,
}
}

const platformMenu = Menu.buildFromTemplate([
...(
process.platform === 'darwin'
? [
{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'services' },
{ type: 'separator' },
{ role: 'hide' },
{ role: 'hideothers' },
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
],
}
]
: []
) as object[],
{
label: _('VIEW'),
submenu: [
{
label: _('SPAN'),
submenu: SPANS.map(s => ({
label: `${getKeyName(s.startKey)}–${getKeyName(s.endKey)}`,
sublabel: `${s.endKey - s.startKey + 1}-key`,
type: 'radio',
checked: config.startKey === s.startKey && config.endKey === s.endKey,
click: () => {
config.startKey = s.startKey
config.endKey = s.endKey
reload()
},
}))
},
{
label: _('DETAIL_SCALE_FACTOR'),
submenu: SCALE_FACTORS.map(s => ({
label: `${s}×`,
type: 'radio',
checked: config.scaleFactor === s,
click: () => {
config.scaleFactor = s
reload()
},
})),
},
{
label: _('THEME'),
submenu: [
{
label: _('BASE'),
submenu: [
{
label: _('DEFAULT'),
type: 'radio',
checked: (
typeof config.colorAccidentalKey === 'undefined'
&& typeof config.colorNaturalKey === 'undefined'
),
click: () => {
config.colorNaturalKey = undefined
config.colorAccidentalKey = undefined
reload()
}
},
...Object.entries(THEMES).map(([key, [colorNaturalKey, colorAccidentalKey]]) => ({
label: _(key),
type: 'radio',
checked: (
config.colorAccidentalKey === colorAccidentalKey
&& config.colorNaturalKey === colorNaturalKey
),
click: () => {
config.colorNaturalKey = colorNaturalKey
config.colorAccidentalKey = colorAccidentalKey
reload()
}
}))
]
},
{
label: _('HIGHLIGHT'),
submenu: [
{
label: _('NONE'),
type: 'radio',
checked: typeof config.colorHighlight === 'undefined',
click: () => {
config.colorHighlight = undefined
reload()
}
},
...Object.entries(COLORS).map(([key, colorHighlight]) => ({
label: _(key),
sublabel: colorHighlight,
type: 'radio',
checked: config.colorHighlight === colorHighlight,
click: () => {
config.colorHighlight = colorHighlight
reload()
}
}))
]
}
]
}
]
},
...(
electronIsDev
? [
{
label: _('DEBUG'),
submenu: [
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
]
}
]
: []
) as object[]
])

Menu.setApplicationMenu(platformMenu)
createWindow()

ipcMain.on('quit', () => {
app.quit()
})
})

app.on('quit', () => {
if (app.commandLine.hasSwitch('discardConfig')) {
return
}
fs.writeFileSync(path.join(app.getPath('userData'), 'config.json'), JSON.stringify(config))
})

app.on('window-all-closed', () => {
app.quit()
})

app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})

+ 159
- 0
src/main/index.ts View File

@@ -0,0 +1,159 @@
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { readFile, stat, writeFile } from "fs/promises";
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'

interface Config {
range: string
queryDeviceKey: string
scaleFactor: string
}

const defaultConfig: Config = {
range: '21|108',
queryDeviceKey: '',
scaleFactor: '1'
}

const configPath = './config.json'
const naturalKeyWidth = 20
const pedalBoardWidth = 239
const height = 200

const getNaturalKeys = (range: string): number => {
const [startKeyRaw, endKeyRaw] = range.split('|')
const startKey = Number(startKeyRaw)
const endKey = Number(endKeyRaw)
let naturalKeys = 0
for (let i = startKey; i <= endKey; i += 1) {
switch (i % 12) {
case 0:
case 2:
case 4:
case 5:
case 7:
case 9:
case 11:
naturalKeys += 1
break
default:
break
}
}
return naturalKeys
}

function createWindow(config: Config): void {
// Create the browser window.
const naturalKeys = getNaturalKeys(config.range)
const width = naturalKeys * naturalKeyWidth + pedalBoardWidth
const mainWindow = new BrowserWindow({
width,
height,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
devTools: is.dev
},
maximizable: is.dev,
minimizable: false,
maxHeight: is.dev ? undefined : height,
minHeight: is.dev ? undefined : height,
minWidth: is.dev ? undefined : width,
maxWidth: is.dev ? undefined : width,
fullscreenable: false,
resizable: is.dev
})

mainWindow.on('ready-to-show', () => {
mainWindow.show()
})

mainWindow.webContents.setWindowOpenHandler((details) => {
void shell.openExternal(details.url)
return { action: 'deny' }
})

let search: URLSearchParams
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
const url = new URL(process.env['ELECTRON_RENDERER_URL'])
search = new URLSearchParams(url.searchParams)
Object.entries(config).forEach(([key, value]) => {
search.set(key, value)
})
url.search = search.toString()
void mainWindow.loadURL(url.toString())
return
}

search = new URLSearchParams()
Object.entries(config).forEach(([key, value]) => {
search.set(key, value)
})
void mainWindow.loadFile(join(__dirname, '../renderer/index.html'), {
search: search.toString()
})
}

app.whenReady().then(async () => {
const effectiveConfig = { ...defaultConfig }
try {
const theStat = await stat(configPath)
if (theStat.isDirectory()) {
return
}
const jsonRaw = await readFile(configPath, 'utf-8')
const json = JSON.parse(jsonRaw)
Object.entries(json).forEach(([key, value]) => {
effectiveConfig[key] = value
})
} catch {
await writeFile(configPath, JSON.stringify(defaultConfig))
}

electronApp.setAppUserModelId('sh.modal.pianomidimonitor')

app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})

ipcMain.on('querydevicekeychange', async (_event, value) => {
effectiveConfig.queryDeviceKey = value
await writeFile(configPath, JSON.stringify(effectiveConfig))
})

ipcMain.on('scalefactorchange', async (_event, value) => {
effectiveConfig.scaleFactor = value
await writeFile(configPath, JSON.stringify(effectiveConfig))
})

ipcMain.on('rangechange', async (event, value) => {
effectiveConfig.range = value
await writeFile(configPath, JSON.stringify(effectiveConfig))
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
if (!win) {
return
}
win.close()
createWindow(effectiveConfig)
})

createWindow(effectiveConfig)

app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow(effectiveConfig)
}
})
})

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

+ 8
- 0
src/preload/index.d.ts View File

@@ -0,0 +1,8 @@
import { ElectronAPI } from '@electron-toolkit/preload'

declare global {
interface Window {
electron: ElectronAPI
api: unknown
}
}

+ 22
- 0
src/preload/index.ts View File

@@ -0,0 +1,22 @@
import { contextBridge } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'

// Custom APIs for renderer
const api = {}

// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
} catch (error) {
console.error(error)
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI
// @ts-ignore (define in dts)
window.api = api
}

+ 0
- 4
src/react-app-env.d.ts View File

@@ -1,4 +0,0 @@
/// <reference types="react-scripts" />
/// <reference types="node" />

declare module 'midi'

+ 16
- 0
src/renderer/index.html View File

@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en-PH">
<head>
<meta charset="UTF-8" />
<title>Piano MIDI Monitor</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

+ 322
- 0
src/renderer/src/App.tsx
File diff suppressed because it is too large
View File


+ 10
- 0
src/renderer/src/assets/electron.svg View File

@@ -0,0 +1,10 @@
<svg viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="64" cy="64" r="64" fill="#2F3242"/>
<ellipse cx="63.9835" cy="23.2036" rx="4.48794" ry="4.495" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path d="M51.3954 39.5028C52.3733 39.6812 53.3108 39.033 53.4892 38.055C53.6676 37.0771 53.0194 36.1396 52.0414 35.9612L51.3954 39.5028ZM28.6153 43.5751L30.1748 44.4741L30.1748 44.4741L28.6153 43.5751ZM28.9393 60.9358C29.4332 61.7985 30.5329 62.0976 31.3957 61.6037C32.2585 61.1098 32.5575 60.0101 32.0636 59.1473L28.9393 60.9358ZM37.6935 66.7457C37.025 66.01 35.8866 65.9554 35.1508 66.6239C34.415 67.2924 34.3605 68.4308 35.029 69.1666L37.6935 66.7457ZM53.7489 81.7014L52.8478 83.2597L53.7489 81.7014ZM96.9206 89.515C97.7416 88.9544 97.9526 87.8344 97.3919 87.0135C96.8313 86.1925 95.7113 85.9815 94.8904 86.5422L96.9206 89.515ZM52.0414 35.9612C46.4712 34.9451 41.2848 34.8966 36.9738 35.9376C32.6548 36.9806 29.0841 39.1576 27.0559 42.6762L30.1748 44.4741C31.5693 42.0549 34.1448 40.3243 37.8188 39.4371C41.5009 38.5479 46.1547 38.5468 51.3954 39.5028L52.0414 35.9612ZM27.0559 42.6762C24.043 47.9029 25.2781 54.5399 28.9393 60.9358L32.0636 59.1473C28.6579 53.1977 28.1088 48.0581 30.1748 44.4741L27.0559 42.6762ZM35.029 69.1666C39.6385 74.24 45.7158 79.1355 52.8478 83.2597L54.6499 80.1432C47.8081 76.1868 42.0298 71.5185 37.6935 66.7457L35.029 69.1666ZM52.8478 83.2597C61.344 88.1726 70.0465 91.2445 77.7351 92.3608C85.359 93.4677 92.2744 92.6881 96.9206 89.515L94.8904 86.5422C91.3255 88.9767 85.4902 89.849 78.2524 88.7982C71.0793 87.7567 62.809 84.8612 54.6499 80.1432L52.8478 83.2597ZM105.359 84.9077C105.359 81.4337 102.546 78.6127 99.071 78.6127V82.2127C100.553 82.2127 101.759 83.4166 101.759 84.9077H105.359ZM99.071 78.6127C95.5956 78.6127 92.7831 81.4337 92.7831 84.9077H96.3831C96.3831 83.4166 97.5892 82.2127 99.071 82.2127V78.6127ZM92.7831 84.9077C92.7831 88.3817 95.5956 91.2027 99.071 91.2027V87.6027C97.5892 87.6027 96.3831 86.3988 96.3831 84.9077H92.7831ZM99.071 91.2027C102.546 91.2027 105.359 88.3817 105.359 84.9077H101.759C101.759 86.3988 100.553 87.6027 99.071 87.6027V91.2027Z" fill="#A2ECFB"/>
<path d="M91.4873 65.382C90.8456 66.1412 90.9409 67.2769 91.7002 67.9186C92.4594 68.5603 93.5951 68.465 94.2368 67.7058L91.4873 65.382ZM99.3169 43.6354L97.7574 44.5344L99.3169 43.6354ZM84.507 35.2412C83.513 35.2282 82.6967 36.0236 82.6838 37.0176C82.6708 38.0116 83.4661 38.8279 84.4602 38.8409L84.507 35.2412ZM74.9407 39.8801C75.9127 39.6716 76.5315 38.7145 76.323 37.7425C76.1144 36.7706 75.1573 36.1517 74.1854 36.3603L74.9407 39.8801ZM53.7836 46.3728L54.6847 47.931L53.7836 46.3728ZM25.5491 80.9047C25.6932 81.8883 26.6074 82.5688 27.5911 82.4247C28.5747 82.2806 29.2552 81.3664 29.1111 80.3828L25.5491 80.9047ZM94.2368 67.7058C97.8838 63.3907 100.505 58.927 101.752 54.678C103.001 50.4213 102.9 46.2472 100.876 42.7365L97.7574 44.5344C99.1494 46.9491 99.3603 50.0419 98.2974 53.6644C97.2323 57.2945 94.9184 61.3223 91.4873 65.382L94.2368 67.7058ZM100.876 42.7365C97.9119 37.5938 91.7082 35.335 84.507 35.2412L84.4602 38.8409C91.1328 38.9278 95.7262 41.0106 97.7574 44.5344L100.876 42.7365ZM74.1854 36.3603C67.4362 37.8086 60.0878 40.648 52.8826 44.8146L54.6847 47.931C61.5972 43.9338 68.5948 41.2419 74.9407 39.8801L74.1854 36.3603ZM52.8826 44.8146C44.1366 49.872 36.9669 56.0954 32.1491 62.3927C27.3774 68.63 24.7148 75.2115 25.5491 80.9047L29.1111 80.3828C28.4839 76.1026 30.4747 70.5062 35.0084 64.5802C39.496 58.7143 46.2839 52.7889 54.6847 47.931L52.8826 44.8146Z" fill="#A2ECFB"/>
<path d="M49.0825 87.2295C48.7478 86.2934 47.7176 85.8059 46.7816 86.1406C45.8455 86.4753 45.358 87.5055 45.6927 88.4416L49.0825 87.2295ZM78.5635 96.4256C79.075 95.5732 78.7988 94.4675 77.9464 93.9559C77.0941 93.4443 75.9884 93.7205 75.4768 94.5729L78.5635 96.4256ZM79.5703 85.1795C79.2738 86.1284 79.8027 87.1379 80.7516 87.4344C81.7004 87.7308 82.71 87.2019 83.0064 86.2531L79.5703 85.1795ZM84.3832 64.0673H82.5832H84.3832ZM69.156 22.5301C68.2477 22.1261 67.1838 22.535 66.7799 23.4433C66.3759 24.3517 66.7848 25.4155 67.6931 25.8194L69.156 22.5301ZM45.6927 88.4416C47.5994 93.7741 50.1496 98.2905 53.2032 101.505C56.2623 104.724 59.9279 106.731 63.9835 106.731V103.131C61.1984 103.131 58.4165 101.765 55.8131 99.0249C53.2042 96.279 50.8768 92.2477 49.0825 87.2295L45.6927 88.4416ZM63.9835 106.731C69.8694 106.731 74.8921 102.542 78.5635 96.4256L75.4768 94.5729C72.0781 100.235 68.0122 103.131 63.9835 103.131V106.731ZM83.0064 86.2531C85.0269 79.7864 86.1832 72.1831 86.1832 64.0673H82.5832C82.5832 71.8536 81.4723 79.0919 79.5703 85.1795L83.0064 86.2531ZM86.1832 64.0673C86.1832 54.1144 84.4439 44.922 81.4961 37.6502C78.5748 30.4436 74.3436 24.8371 69.156 22.5301L67.6931 25.8194C71.6364 27.5731 75.3846 32.1564 78.1598 39.0026C80.9086 45.7836 82.5832 54.507 82.5832 64.0673H86.1832Z" fill="#A2ECFB"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M103.559 84.9077C103.559 82.4252 101.55 80.4127 99.071 80.4127C96.5924 80.4127 94.5831 82.4252 94.5831 84.9077C94.5831 87.3902 96.5924 89.4027 99.071 89.4027C101.55 89.4027 103.559 87.3902 103.559 84.9077V84.9077Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.8143 89.4027C31.2929 89.4027 33.3023 87.3902 33.3023 84.9077C33.3023 82.4252 31.2929 80.4127 28.8143 80.4127C26.3357 80.4127 24.3264 82.4252 24.3264 84.9077C24.3264 87.3902 26.3357 89.4027 28.8143 89.4027V89.4027V89.4027Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.8501 68.0857C62.6341 68.5652 60.451 67.1547 59.9713 64.9353C59.4934 62.7159 60.9007 60.5293 63.1167 60.0489C65.3326 59.5693 67.5157 60.9798 67.9954 63.1992C68.4742 65.4186 67.066 67.6052 64.8501 68.0857Z" fill="#A2ECFB"/>
</svg>

+ 85
- 0
src/renderer/src/assets/main.css View File

@@ -0,0 +1,85 @@
:root {
height: 100%;
width: 100%;
color: white;
background: black;
}

body {
height: 100%;
width: 100%;
margin: 0;
font-family: sans-serif;
}

#root {
display: contents;
}

.flex {
display: flex;
}

.flex-col {
flex-direction: column;
}

.items-stretch {
align-items: stretch;
}

.justify-center {
justify-content: center;
}

.h-full {
height: 100%;
}

.flex-shrink-0 {
flex-shrink: 0;
}

.items-center {
align-items: center;
}

.w-full {
width: 100%;
}

.gap-8 {
gap: 2rem;
}

.flex-auto {
flex: auto;
}

.bg-black {
background-color: black;
}

select {
color: inherit;
font: inherit;
border: 0;
}

.h-12 {
height: 3rem;
}

.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}

.px-8 {
padding-left: 2rem;
padding-right: 2rem;
}

.gap-4 {
gap: 1rem;
}

+ 25
- 0
src/renderer/src/assets/wavy-lines.svg View File

@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1422 800" opacity="0.3">
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="oooscillate-grad">
<stop stop-color="hsl(206, 75%, 49%)" stop-opacity="1" offset="0%"></stop>
<stop stop-color="hsl(331, 90%, 56%)" stop-opacity="1" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke-width="1" stroke="url(#oooscillate-grad)" fill="none" stroke-linecap="round">
<path d="M 0 448 Q 355.5 -100 711 400 Q 1066.5 900 1422 448" opacity="0.05"></path>
<path d="M 0 420 Q 355.5 -100 711 400 Q 1066.5 900 1422 420" opacity="0.11"></path>
<path d="M 0 392 Q 355.5 -100 711 400 Q 1066.5 900 1422 392" opacity="0.18"></path>
<path d="M 0 364 Q 355.5 -100 711 400 Q 1066.5 900 1422 364" opacity="0.24"></path>
<path d="M 0 336 Q 355.5 -100 711 400 Q 1066.5 900 1422 336" opacity="0.30"></path>
<path d="M 0 308 Q 355.5 -100 711 400 Q 1066.5 900 1422 308" opacity="0.37"></path>
<path d="M 0 280 Q 355.5 -100 711 400 Q 1066.5 900 1422 280" opacity="0.43"></path>
<path d="M 0 252 Q 355.5 -100 711 400 Q 1066.5 900 1422 252" opacity="0.49"></path>
<path d="M 0 224 Q 355.5 -100 711 400 Q 1066.5 900 1422 224" opacity="0.56"></path>
<path d="M 0 196 Q 355.5 -100 711 400 Q 1066.5 900 1422 196" opacity="0.62"></path>
<path d="M 0 168 Q 355.5 -100 711 400 Q 1066.5 900 1422 168" opacity="0.68"></path>
<path d="M 0 140 Q 355.5 -100 711 400 Q 1066.5 900 1422 140" opacity="0.75"></path>
<path d="M 0 112 Q 355.5 -100 711 400 Q 1066.5 900 1422 112" opacity="0.81"></path>
<path d="M 0 84 Q 355.5 -100 711 400 Q 1066.5 900 1422 84" opacity="0.87"></path>
<path d="M 0 56 Q 355.5 -100 711 400 Q 1066.5 900 1422 56" opacity="0.94"></path>
</g>
</svg>

+ 1
- 0
src/renderer/src/env.d.ts View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

src/index.tsx → src/renderer/src/main.tsx View File

@@ -1,18 +1,18 @@
import * as React from 'react'
import ReactDOM from 'react-dom'
import './assets/main.css'

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

const div = window.document.createElement('main')
const search = new URLSearchParams(window.location.search)
window.document.body.appendChild(div)
window.document.documentElement.style.setProperty('--size-scale-factor', search.get('scaleFactor'))
window.document.documentElement.style.setProperty('--color-natural-key', search.get('colorNaturalKey'))
window.document.documentElement.style.setProperty('--color-accidental-key', search.get('colorAccidentalKey'))
window.document.documentElement.style.setProperty('--color-channel-0', search.get('colorHighlight'))

ReactDOM.render(

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
div
</React.StrictMode>
)

+ 0
- 149
src/serviceWorker.ts View File

@@ -1,149 +0,0 @@
// This optional code is used to register a service worker.
// register() is not called by default.

// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.

// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA

const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);

type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};

export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL,
window.location.href
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}

window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;

if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);

// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}

function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);

// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');

// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}

function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}

export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}

+ 0
- 8
src/services/Config.ts View File

@@ -1,8 +0,0 @@
export default interface Config extends Record<string, string | number | undefined>{
startKey: number,
endKey: number,
scaleFactor: number,
colorNaturalKey?: string,
colorAccidentalKey?: string,
colorHighlight?: string,
}

+ 0
- 7
src/services/colors.json View File

@@ -1,7 +0,0 @@
{
"REDDISH": "rgb(255,134,113)",
"YELLOWISH": "rgb(248,190,74)",
"GREENISH": "rgb(224,255,113)",
"BLUISH": "rgb(113,153,255)",
"PURPLISH": "rgb(190,145,255)"
}

+ 0
- 15
src/services/getKeyName.ts View File

@@ -1,15 +0,0 @@
import getKeyOctave from './getKeyOctave'
import keyNames from './keyNames.json'

interface GetKeyName {
(key: number): string,
}

const getKeyName: GetKeyName = (key) => {
const octave = getKeyOctave(key)
const pitch = (Math.floor(key) % 12) as keyof typeof keyNames
const keyName: string = keyNames[pitch] as unknown as string
return `${keyName}${octave}`
}

export default getKeyName

+ 0
- 7
src/services/getKeyOctave.ts View File

@@ -1,7 +0,0 @@
interface GetKeyOctave {
(key: number): number,
}

const getOctave: GetKeyOctave = (key) => Math.floor(key / 12) - 1

export default getOctave

+ 0
- 15
src/services/getNaturalKeyCount.ts View File

@@ -1,15 +0,0 @@
import isNaturalKey from './isNaturalKey'

interface GetNaturalKeyCount {
(startKey: number, endKey: number): number,
}

const getNaturalKeyCount: GetNaturalKeyCount = (startKey, endKey) => (
Array(endKey - startKey + 1)
.fill(startKey)
.map((s, i) => s + i)
.filter(k => isNaturalKey(k))
.length
)

export default getNaturalKeyCount

+ 0
- 64
src/services/isNaturalKey.test.ts View File

@@ -1,64 +0,0 @@
import * as fc from 'fast-check'
import isNaturalKey from './isNaturalKey'

it('should exist', () => {
expect(isNaturalKey).toBeDefined()
})

it('should be a callable', () => {
expect(typeof isNaturalKey).toBe('function')
})

it('should accept 1 parameter', () => {
expect(isNaturalKey).toHaveLength(1)
})

it('should throw TypeError upon passing invalid types', () => {
fc.assert(
fc.property(
fc.anything().filter((anything) => typeof anything !== 'number'),
(anything) => {
expect(() => isNaturalKey(anything as number)).toThrowError(TypeError)
},
),
)
})

it('should throw RangeError upon passing NaN', () => {
expect(() => isNaturalKey(NaN)).toThrowError(RangeError)
})

it('should throw RangeError upon passing negative numbers', () => {
fc.assert(
fc.property(
fc.anything().filter((anything) => typeof anything! === 'number' && !isNaN(anything) && anything < 0),
(negativeValue) => {
expect(() => isNaturalKey(negativeValue as number)).toThrowError(RangeError)
},
),
)
})

describe('upon passing a positive number or zero', () => {
it('should not throw any error', () => {
fc.assert(
fc.property(
fc.anything().filter((anything) => typeof anything! === 'number' && !isNaN(anything) && anything >= 0),
(value) => {
expect(() => isNaturalKey(value as number)).not.toThrow()
},
),
)
})

it('should return a boolean', () => {
fc.assert(
fc.property(
fc.anything().filter((anything) => typeof anything! === 'number' && !isNaN(anything) && anything >= 0),
(value) => {
expect(typeof isNaturalKey(value as number)).toBe('boolean')
},
),
)
})
})

+ 0
- 21
src/services/isNaturalKey.ts View File

@@ -1,21 +0,0 @@
const NATURAL_KEYS = [0, 2, 4, 5, 7, 9, 11]

interface IsNaturalKey {
(k: number): boolean
}

const isNaturalKey: IsNaturalKey = (k: number): boolean => {
const type = typeof (k as unknown)
if ((type as string) !== 'number') {
throw TypeError(`Invalid value type passed to isNaturalKey, expected 'number', got ${type}.`)
}
if (isNaN(k)) {
throw RangeError('Value passed is NaN.')
}
if (k < 0) {
throw RangeError('Value must be positive.')
}
return NATURAL_KEYS.includes(Math.floor(k) % 12)
}

export default isNaturalKey

+ 0
- 14
src/services/keyNames.json View File

@@ -1,14 +0,0 @@
[
"C",
"C♯",
"D",
"D♯",
"E",
"F",
"F♯",
"G",
"G♯",
"A",
"A♯",
"B"
]

+ 0
- 17
src/services/messages.json View File

@@ -1,17 +0,0 @@
{
"SPAN": "Span",
"DETAIL_SCALE_FACTOR": "Detail Scale Factor",
"DEBUG": "Debug",
"VIEW": "View",
"THEME": "Theme",
"BASE": "Base",
"HIGHLIGHT": "Highlight",
"DEFAULT": "Default",
"NONE": "None",
"INVERSE": "Inverse",
"REDDISH": "Tomato",
"YELLOWISH": "Gold",
"GREENISH": "Lime",
"BLUISH": "Cerulean",
"PURPLISH": "Mauve"
}

+ 0
- 12
src/services/messages.ts View File

@@ -1,12 +0,0 @@
import messages from './messages.json'

type MsgId = keyof typeof messages

interface GetMessage {
(msgId: string): string,
}

export const _: GetMessage = (msgId) => {
const { [msgId as MsgId]: msgStr = msgId } = messages
return msgStr
}

+ 0
- 4
src/services/scaleFactors.json View File

@@ -1,4 +0,0 @@
[
1,
2
]

+ 0
- 34
src/services/spans.json View File

@@ -1,34 +0,0 @@
[
{
"startKey": 36,
"endKey": 84
},
{
"startKey": 36,
"endKey": 89
},
{
"startKey": 36,
"endKey": 96
},
{
"startKey": 28,
"endKey": 103
},
{
"startKey": 21,
"endKey": 108
},
{
"startKey": 12,
"endKey": 108
},
{
"startKey": 12,
"endKey": 119
},
{
"startKey": 0,
"endKey": 127
}
]

+ 0
- 3
src/services/themes.json View File

@@ -1,3 +0,0 @@
{
"INVERSE": ["rgb(53,54,58)", "rgb(145,147,155)"]
}

+ 0
- 5
src/setupTests.ts View File

@@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

+ 2
- 23
tsconfig.json View File

@@ -1,25 +1,4 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
},
"include": [
"src"
]
"files": [],
"references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }]
}

+ 8
- 0
tsconfig.node.json View File

@@ -0,0 +1,8 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"],
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"]
}
}

+ 19
- 0
tsconfig.web.json View File

@@ -0,0 +1,19 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
"include": [
"src/renderer/src/env.d.ts",
"src/renderer/src/**/*",
"src/renderer/src/**/*.tsx",
"src/preload/*.d.ts"
],
"compilerOptions": {
"composite": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@renderer/*": [
"src/renderer/src/*"
]
}
}
}

+ 0
- 11976
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save