Simple monitor for displaying MIDI status for digital pianos.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

262 lines
7.1 KiB

  1. import { app, BrowserWindow, Menu, ipcMain, } from 'electron'
  2. import path from 'path'
  3. import fs from 'fs'
  4. import SPANS from './services/spans.json'
  5. import SCALE_FACTORS from './services/scaleFactors.json'
  6. import THEMES from './services/themes.json'
  7. import COLORS from './services/colors.json'
  8. import { _ } from './services/messages'
  9. import getKeyName from './services/getKeyName'
  10. import getNaturalKeyCount from './services/getNaturalKeyCount'
  11. import Config from './services/Config'
  12. // @ts-ignore
  13. import electronIsDev = require('electron-is-dev')
  14. // @ts-ignore
  15. import midi = require('midi')
  16. // @ts-ignore
  17. const WINDOW_HEIGHT = 100
  18. let config: Config = {
  19. startKey: 21,
  20. endKey: 108,
  21. scaleFactor: 1,
  22. colorNaturalKey: undefined,
  23. colorAccidentalKey: undefined,
  24. colorHighlight: undefined,
  25. }
  26. const createWindow = () => {
  27. const KEYS_WIDTH = getNaturalKeyCount(config.startKey, config.endKey) * 20
  28. const WINDOW_WIDTH = KEYS_WIDTH + 207
  29. const win = new BrowserWindow({
  30. width: WINDOW_WIDTH,
  31. height: WINDOW_HEIGHT,
  32. useContentSize: true,
  33. backgroundColor: '#000000',
  34. resizable: false,
  35. minimizable: false,
  36. maximizable: false,
  37. fullscreenable: false,
  38. frame: false,
  39. webPreferences: {
  40. devTools: electronIsDev,
  41. nodeIntegration: true,
  42. },
  43. })
  44. const baseUrl: string = (
  45. electronIsDev
  46. ? 'http://localhost:3000'
  47. : `file:///${path.join(__dirname, '../build/index.html')}`
  48. )
  49. const search = new URLSearchParams(config as Record<string, string>)
  50. const url = new URL(baseUrl)
  51. url.search = search.toString()
  52. win.loadURL(url.toString())
  53. if (electronIsDev) {
  54. win.webContents.openDevTools()
  55. }
  56. const input = new midi.Input()
  57. if (input.getPortCount() > 0) {
  58. input.openPort(0)
  59. }
  60. input.on('message', (deltaTime: number, message: unknown[]) => {
  61. const [type, data1, data2] = message
  62. switch (type) {
  63. case 144:
  64. win.webContents.send('note', data1 + ':' + data2)
  65. break
  66. case 176:
  67. win.webContents.send('pedal', data1 + ':' + data2)
  68. break
  69. }
  70. })
  71. }
  72. const reload = () => {
  73. app.relaunch({
  74. args: process.argv.slice(1).concat([
  75. `--startKey=${config.startKey}`,
  76. `--endKey=${config.endKey}`,
  77. `--scaleFactor=${config.scaleFactor}`,
  78. `--colorNaturalKey=${config.colorNaturalKey}`,
  79. `--colorAccidentalKey=${config.colorAccidentalKey}`,
  80. `--colorHighlight=${config.colorHighlight}`,
  81. ])
  82. })
  83. app.exit(0)
  84. }
  85. app
  86. .whenReady()
  87. .then(() => {
  88. try {
  89. const configJsonRaw = fs.readFileSync(path.join(app.getPath('userData'), 'config.json')).toString('utf-8')
  90. config = JSON.parse(configJsonRaw)
  91. } catch (e) {
  92. config = {
  93. startKey: 21,
  94. endKey: 108,
  95. scaleFactor: 1,
  96. }
  97. }
  98. const platformMenu = Menu.buildFromTemplate([
  99. ...(
  100. process.platform === 'darwin'
  101. ? [
  102. {
  103. label: app.name,
  104. submenu: [
  105. { role: 'about' },
  106. { type: 'separator' },
  107. { role: 'services' },
  108. { type: 'separator' },
  109. { role: 'hide' },
  110. { role: 'hideothers' },
  111. { role: 'unhide' },
  112. { type: 'separator' },
  113. { role: 'quit' }
  114. ],
  115. }
  116. ]
  117. : []
  118. ) as object[],
  119. {
  120. label: _('VIEW'),
  121. submenu: [
  122. {
  123. label: _('SPAN'),
  124. submenu: SPANS.map(s => ({
  125. label: `${getKeyName(s.startKey)}–${getKeyName(s.endKey)}`,
  126. sublabel: `${s.endKey - s.startKey + 1}-key`,
  127. type: 'radio',
  128. checked: config.startKey === s.startKey && config.endKey === s.endKey,
  129. click: () => {
  130. config.startKey = s.startKey
  131. config.endKey = s.endKey
  132. reload()
  133. },
  134. }))
  135. },
  136. {
  137. label: _('DETAIL_SCALE_FACTOR'),
  138. submenu: SCALE_FACTORS.map(s => ({
  139. label: `${s}×`,
  140. type: 'radio',
  141. checked: config.scaleFactor === s,
  142. click: () => {
  143. config.scaleFactor = s
  144. reload()
  145. },
  146. })),
  147. },
  148. {
  149. label: _('THEME'),
  150. submenu: [
  151. {
  152. label: _('BASE'),
  153. submenu: [
  154. {
  155. label: _('DEFAULT'),
  156. type: 'radio',
  157. checked: (
  158. typeof config.colorAccidentalKey === 'undefined'
  159. && typeof config.colorNaturalKey === 'undefined'
  160. ),
  161. click: () => {
  162. config.colorNaturalKey = undefined
  163. config.colorAccidentalKey = undefined
  164. reload()
  165. }
  166. },
  167. ...Object.entries(THEMES).map(([key, [colorNaturalKey, colorAccidentalKey]]) => ({
  168. label: _(key),
  169. type: 'radio',
  170. checked: (
  171. config.colorAccidentalKey === colorAccidentalKey
  172. && config.colorNaturalKey === colorNaturalKey
  173. ),
  174. click: () => {
  175. config.colorNaturalKey = colorNaturalKey
  176. config.colorAccidentalKey = colorAccidentalKey
  177. reload()
  178. }
  179. }))
  180. ]
  181. },
  182. {
  183. label: _('HIGHLIGHT'),
  184. submenu: [
  185. {
  186. label: _('NONE'),
  187. type: 'radio',
  188. checked: typeof config.colorHighlight === 'undefined',
  189. click: () => {
  190. config.colorHighlight = undefined
  191. reload()
  192. }
  193. },
  194. ...Object.entries(COLORS).map(([key, colorHighlight]) => ({
  195. label: _(key),
  196. sublabel: colorHighlight,
  197. type: 'radio',
  198. checked: config.colorHighlight === colorHighlight,
  199. click: () => {
  200. config.colorHighlight = colorHighlight
  201. reload()
  202. }
  203. }))
  204. ]
  205. }
  206. ]
  207. }
  208. ]
  209. },
  210. ...(
  211. electronIsDev
  212. ? [
  213. {
  214. label: _('DEBUG'),
  215. submenu: [
  216. { role: 'forceReload' },
  217. { role: 'toggleDevTools' },
  218. ]
  219. }
  220. ]
  221. : []
  222. ) as object[]
  223. ])
  224. Menu.setApplicationMenu(platformMenu)
  225. createWindow()
  226. ipcMain.on('quit', () => {
  227. app.quit()
  228. })
  229. })
  230. app.on('quit', () => {
  231. if (app.commandLine.hasSwitch('discardConfig')) {
  232. return
  233. }
  234. fs.writeFileSync(path.join(app.getPath('userData'), 'config.json'), JSON.stringify(config))
  235. })
  236. app.on('window-all-closed', () => {
  237. app.quit()
  238. })
  239. app.on('activate', () => {
  240. if (BrowserWindow.getAllWindows().length === 0) {
  241. createWindow()
  242. }
  243. })