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.
 
 
 
 

220 lines
6.0 KiB

  1. import { app, BrowserWindow, ipcMain } from 'electron'
  2. import { join } from 'path'
  3. import { readFile, stat, writeFile } from 'fs/promises'
  4. import { electronApp, optimizer, is } from '@electron-toolkit/utils'
  5. import icon from '../../resources/icon.png?asset'
  6. import { Config, defaultConfig } from '../common/config'
  7. import { getNaturalKeys } from './utils/display'
  8. import { Stats } from 'fs'
  9. const configPath = './config.json'
  10. const height = 160
  11. const ALLOWED_PERMISSIONS = ['midi']
  12. const defineWindowCommonSecurity = (browserWindow: BrowserWindow): void => {
  13. browserWindow.webContents.setWindowOpenHandler(() => {
  14. return { action: 'deny' }
  15. })
  16. }
  17. const addWindowEvents = (browserWindow: BrowserWindow): void => {
  18. browserWindow.on('ready-to-show', () => {
  19. browserWindow.show()
  20. })
  21. }
  22. const addMidiPermissions = (browserWindow: BrowserWindow): void => {
  23. browserWindow.webContents.session.setPermissionRequestHandler(
  24. (_webContents, permission, callback) => {
  25. callback(ALLOWED_PERMISSIONS.includes(permission))
  26. }
  27. )
  28. browserWindow.webContents.session.setPermissionCheckHandler((_webContents, permission) => {
  29. return ALLOWED_PERMISSIONS.includes(permission)
  30. })
  31. }
  32. const assignWindowView = async (
  33. browserWindow: BrowserWindow,
  34. routeName: string,
  35. config: Config
  36. ): Promise<BrowserWindow> => {
  37. let search = new URLSearchParams()
  38. let url: URL | undefined
  39. if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
  40. url = new URL(process.env['ELECTRON_RENDERER_URL'])
  41. search = new URLSearchParams(url.searchParams)
  42. }
  43. Object.entries(config).forEach(([key, value]) => {
  44. search.set(key, value)
  45. })
  46. search.set('window', routeName)
  47. if (typeof url !== 'undefined') {
  48. url.search = search.toString()
  49. await browserWindow.loadURL(url.toString())
  50. return browserWindow
  51. }
  52. await browserWindow.loadFile(join(__dirname, '../renderer/index.html'), {
  53. search: search.toString()
  54. })
  55. return browserWindow
  56. }
  57. const createMainWindow = async (config: Config): Promise<BrowserWindow> => {
  58. const naturalKeys = getNaturalKeys(config.range)
  59. const width = naturalKeys * Number(config.naturalKeyWidth || 20) + Number(config.scaleFactor)
  60. const mainWindow = new BrowserWindow({
  61. width,
  62. height,
  63. show: false,
  64. autoHideMenuBar: true,
  65. ...(process.platform === 'linux' ? { icon } : {}),
  66. webPreferences: {
  67. preload: join(__dirname, '../preload/index.js'),
  68. sandbox: false,
  69. devTools: is.dev
  70. },
  71. title: 'Piano MIDI Monitor',
  72. maximizable: is.dev,
  73. minimizable: false,
  74. maxHeight: is.dev ? undefined : height,
  75. minHeight: is.dev ? undefined : height,
  76. minWidth: is.dev ? undefined : width,
  77. maxWidth: is.dev ? undefined : width,
  78. fullscreenable: false,
  79. resizable: is.dev,
  80. useContentSize: true
  81. })
  82. defineWindowCommonSecurity(mainWindow)
  83. addMidiPermissions(mainWindow)
  84. addWindowEvents(mainWindow)
  85. return assignWindowView(mainWindow, 'main', config)
  86. }
  87. const createSettingsWindow = async (
  88. parent: BrowserWindow,
  89. config: Config
  90. ): Promise<BrowserWindow> => {
  91. const settingsWindow = new BrowserWindow({
  92. width: 360,
  93. height: 640,
  94. show: false,
  95. autoHideMenuBar: true,
  96. modal: true,
  97. ...(process.platform === 'linux' ? { icon } : {}),
  98. webPreferences: {
  99. preload: join(__dirname, '../preload/index.js'),
  100. sandbox: false,
  101. devTools: is.dev
  102. },
  103. title: 'Settings',
  104. maximizable: false,
  105. minimizable: false,
  106. fullscreenable: false,
  107. resizable: false,
  108. useContentSize: true,
  109. parent
  110. })
  111. defineWindowCommonSecurity(settingsWindow)
  112. addMidiPermissions(settingsWindow)
  113. addWindowEvents(settingsWindow)
  114. return assignWindowView(settingsWindow, 'settings', config)
  115. }
  116. const ensureConfig = async (defaultConfig: Config): Promise<Config> => {
  117. const effectiveConfig = { ...defaultConfig }
  118. let theStat: Stats
  119. try {
  120. theStat = await stat(configPath)
  121. } catch {
  122. await writeFile(configPath, JSON.stringify(defaultConfig))
  123. return effectiveConfig
  124. }
  125. if (theStat.isDirectory()) {
  126. throw new Error('Config path is a directory.')
  127. }
  128. try {
  129. const jsonRaw = await readFile(configPath, 'utf-8')
  130. const json = JSON.parse(jsonRaw)
  131. Object.entries(json).forEach(([key, value]) => {
  132. effectiveConfig[key] = value
  133. })
  134. } catch {
  135. await writeFile(configPath, JSON.stringify(defaultConfig))
  136. }
  137. return effectiveConfig
  138. }
  139. const main = async (): Promise<void> => {
  140. const effectiveConfig = await ensureConfig(defaultConfig)
  141. app.on('window-all-closed', () => {
  142. if (process.platform !== 'darwin') {
  143. app.quit()
  144. }
  145. })
  146. await app.whenReady()
  147. electronApp.setAppUserModelId('sh.modal.pianomidimonitor')
  148. app.on('browser-window-created', (_, window) => {
  149. optimizer.watchWindowShortcuts(window)
  150. })
  151. ipcMain.on('action', async (event, value, formData) => {
  152. const webContents = event.sender
  153. const win = BrowserWindow.fromWebContents(webContents)
  154. if (!win) {
  155. return
  156. }
  157. switch (value) {
  158. case 'showSettings': {
  159. await createSettingsWindow(win, effectiveConfig)
  160. return
  161. }
  162. case 'cancelSaveConfig': {
  163. win.close()
  164. return
  165. }
  166. case 'resetConfig': {
  167. Object.entries(defaultConfig).forEach(([key, value]) => {
  168. effectiveConfig[key] = value
  169. })
  170. await writeFile(configPath, JSON.stringify(effectiveConfig))
  171. win.close()
  172. win.getParentWindow()?.close()
  173. await createMainWindow(effectiveConfig)
  174. return
  175. }
  176. case 'saveConfig': {
  177. Object.entries(formData).forEach(([key, value]) => {
  178. effectiveConfig[key] = value
  179. })
  180. await writeFile(configPath, JSON.stringify(effectiveConfig))
  181. win.close()
  182. win.getParentWindow()?.close()
  183. await createMainWindow(effectiveConfig)
  184. return
  185. }
  186. }
  187. })
  188. await createMainWindow(effectiveConfig)
  189. app.on('activate', async () => {
  190. if (BrowserWindow.getAllWindows().length === 0) {
  191. await createMainWindow(effectiveConfig)
  192. }
  193. })
  194. }
  195. void main()