diff --git a/src/common/config.ts b/src/common/config.ts index 1ffa8ce..511572c 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -1,13 +1,13 @@ export const RANGES = { - 'C3-C5': '48|72', - 'C3-C6': '48|84', - 'C2-C6': '36|84', - 'C2-C7': '36|96', - 'E1-E7': '28|100', - 'E1-G7': '28|103', - 'A0-C8': '21|108', - 'C0-B9': '12|119', - 'Full MIDI': '0|127' + 'C3-C5 (2 octaves)': '48|72', + 'C3-C6 (3 octaves)': '48|84', + 'C2-C6 (4 octaves)': '36|84', + 'C2-C7 (5 octaves)': '36|96', + 'E1-E7 (73 keys)': '28|100', + 'E1-G7 (76 keys)': '28|103', + 'A0-C8 (88 keys)': '21|108', + 'C0-B9 (108 keys)': '12|119', + 'Full MIDI (128 keys)': '0|127' } export const SCALE_FACTORS = { @@ -15,11 +15,7 @@ export const SCALE_FACTORS = { '2x': '2' } -export const NATURAL_KEY_WIDTHS = [ - 16, - 18, - 20 -] +export const NATURAL_KEY_WIDTHS = [16, 18, 20] export interface Config { range: string diff --git a/src/main/index.ts b/src/main/index.ts index c088567..97b9034 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,38 +1,71 @@ -import { app, shell, BrowserWindow, ipcMain } from 'electron' +import { app, 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' import { Config, defaultConfig } from '../common/config' +import { getNaturalKeys } from './utils/display' +import { Stats } from 'fs' const configPath = './config.json' const height = 160 -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 +const ALLOWED_PERMISSIONS = ['midi'] + +const defineWindowCommonSecurity = (browserWindow: BrowserWindow): void => { + browserWindow.webContents.setWindowOpenHandler(() => { + return { action: 'deny' } + }) +} + +const addWindowEvents = (browserWindow: BrowserWindow): void => { + browserWindow.on('ready-to-show', () => { + browserWindow.show() + }) +} + +const addMidiPermissions = (browserWindow: BrowserWindow): void => { + browserWindow.webContents.session.setPermissionRequestHandler( + (_webContents, permission, callback) => { + callback(ALLOWED_PERMISSIONS.includes(permission)) } + ) + + browserWindow.webContents.session.setPermissionCheckHandler((_webContents, permission) => { + return ALLOWED_PERMISSIONS.includes(permission) + }) +} + +const assignWindowView = async ( + browserWindow: BrowserWindow, + routeName: string, + config: Config +): Promise => { + let search = new URLSearchParams() + let url: URL | undefined + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + url = new URL(process.env['ELECTRON_RENDERER_URL']) + search = new URLSearchParams(url.searchParams) + } + + Object.entries(config).forEach(([key, value]) => { + search.set(key, value) + }) + search.set('window', routeName) + + if (typeof url !== 'undefined') { + url.search = search.toString() + await browserWindow.loadURL(url.toString()) + return browserWindow } - return naturalKeys + + await browserWindow.loadFile(join(__dirname, '../renderer/index.html'), { + search: search.toString() + }) + return browserWindow } const createMainWindow = async (config: Config): Promise => { - // Create the browser window. const naturalKeys = getNaturalKeys(config.range) const width = naturalKeys * Number(config.naturalKeyWidth || 20) + Number(config.scaleFactor) const mainWindow = new BrowserWindow({ @@ -46,6 +79,7 @@ const createMainWindow = async (config: Config): Promise => { sandbox: false, devTools: is.dev }, + title: 'Piano MIDI Monitor', maximizable: is.dev, minimizable: false, maxHeight: is.dev ? undefined : height, @@ -56,58 +90,57 @@ const createMainWindow = async (config: Config): Promise => { resizable: is.dev, useContentSize: true }) + defineWindowCommonSecurity(mainWindow) + addMidiPermissions(mainWindow) + addWindowEvents(mainWindow) + return assignWindowView(mainWindow, 'main', config) +} - mainWindow.on('ready-to-show', () => { - mainWindow.show() - }) - - mainWindow.webContents.setWindowOpenHandler((details) => { - void shell.openExternal(details.url) - return { action: 'deny' } +const createSettingsWindow = async ( + parent: BrowserWindow, + config: Config +): Promise => { + const settingsWindow = new BrowserWindow({ + width: 360, + height: 640, + show: false, + autoHideMenuBar: true, + modal: true, + ...(process.platform === 'linux' ? { icon } : {}), + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + sandbox: false, + devTools: is.dev + }, + title: 'Settings', + maximizable: false, + minimizable: false, + fullscreenable: false, + resizable: false, + useContentSize: true, + parent }) + defineWindowCommonSecurity(settingsWindow) + addMidiPermissions(settingsWindow) + addWindowEvents(settingsWindow) + return assignWindowView(settingsWindow, 'settings', config) +} - mainWindow.webContents.session.setPermissionRequestHandler( - (_webContents, permission, callback) => { - if (permission === 'midi') { - callback(true) - return - } - - callback(false) - } - ) - - 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) - }) - search.set('window', 'main') - url.search = search.toString() - await mainWindow.loadURL(url.toString()) - return mainWindow +const ensureConfig = async (defaultConfig: Config): Promise => { + const effectiveConfig = { ...defaultConfig } + let theStat: Stats + try { + theStat = await stat(configPath) + } catch { + await writeFile(configPath, JSON.stringify(defaultConfig)) + return effectiveConfig } - search = new URLSearchParams() - Object.entries(config).forEach(([key, value]) => { - search.set(key, value) - }) - search.set('window', 'main') - await mainWindow.loadFile(join(__dirname, '../renderer/index.html'), { - search: search.toString() - }) - return mainWindow -} + if (theStat.isDirectory()) { + throw new Error('Config path is a directory.') + } -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]) => { @@ -117,79 +150,24 @@ app.whenReady().then(async () => { await writeFile(configPath, JSON.stringify(defaultConfig)) } - electronApp.setAppUserModelId('sh.modal.pianomidimonitor') - - app.on('browser-window-created', (_, window) => { - optimizer.watchWindowShortcuts(window) - }) - - const createSettingsWindow = async ( - parent: BrowserWindow, - config: Config - ): Promise => { - const settingsWindow = new BrowserWindow({ - width: 360, - height: 640, - show: false, - autoHideMenuBar: true, - ...(process.platform === 'linux' ? { icon } : {}), - webPreferences: { - preload: join(__dirname, '../preload/index.js'), - sandbox: false, - devTools: is.dev - }, - maximizable: false, - minimizable: false, - fullscreenable: false, - resizable: false, - useContentSize: true, - parent - }) - - settingsWindow.on('ready-to-show', () => { - settingsWindow.show() - }) - - settingsWindow.webContents.setWindowOpenHandler((details) => { - void shell.openExternal(details.url) - return { action: 'deny' } - }) + return effectiveConfig +} - settingsWindow.webContents.session.setPermissionRequestHandler( - (_webContents, permission, callback) => { - if (permission === 'midi') { - callback(true) - return - } +const main = async (): Promise => { + const effectiveConfig = await ensureConfig(defaultConfig) - callback(false) - } - ) - - 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) - }) - search.set('window', 'settings') - url.search = search.toString() - await settingsWindow.loadURL(url.toString()) - return settingsWindow + app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() } + }) - search = new URLSearchParams() - Object.entries(config).forEach(([key, value]) => { - search.set(key, value) - }) - search.set('window', 'settings') - await settingsWindow.loadFile(join(__dirname, '../renderer/index.html'), { - search: search.toString() - }) + await app.whenReady() + electronApp.setAppUserModelId('sh.modal.pianomidimonitor') - return settingsWindow - } + app.on('browser-window-created', (_, window) => { + optimizer.watchWindowShortcuts(window) + }) ipcMain.on('action', async (event, value, formData) => { const webContents = event.sender @@ -236,10 +214,6 @@ app.whenReady().then(async () => { await createMainWindow(effectiveConfig) } }) -}) +} -app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { - app.quit() - } -}) +void main() diff --git a/src/main/utils/display.ts b/src/main/utils/display.ts new file mode 100644 index 0000000..d99551b --- /dev/null +++ b/src/main/utils/display.ts @@ -0,0 +1,22 @@ +export 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 +} diff --git a/src/renderer/index.html b/src/renderer/index.html index b98ab08..e4ddf7b 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -2,12 +2,15 @@ - Piano MIDI Monitor - + +
diff --git a/src/renderer/src/components/Activity/index.tsx b/src/renderer/src/components/Activity/index.tsx index e52a0d3..e78ac0d 100644 --- a/src/renderer/src/components/Activity/index.tsx +++ b/src/renderer/src/components/Activity/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import {DeviceChannelActive} from '../../hooks/midi'; +import { DeviceChannelActive } from '../../hooks/midi' export interface ActivityProps { device?: MIDIInput diff --git a/src/renderer/src/components/DropdownInput/index.tsx b/src/renderer/src/components/DropdownInput/index.tsx index fffbcfc..53e7043 100644 --- a/src/renderer/src/components/DropdownInput/index.tsx +++ b/src/renderer/src/components/DropdownInput/index.tsx @@ -8,10 +8,14 @@ export const DropdownInput = React.forwardRef { return ( ) } diff --git a/src/renderer/src/components/MidiDeviceProvider/index.tsx b/src/renderer/src/components/MidiDeviceProvider/index.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/renderer/src/components/SettingsForm/index.tsx b/src/renderer/src/components/SettingsForm/index.tsx index eb21da6..66d4b73 100644 --- a/src/renderer/src/components/SettingsForm/index.tsx +++ b/src/renderer/src/components/SettingsForm/index.tsx @@ -22,6 +22,8 @@ export const SettingsForm: React.FC = ({ ...etcProps }) => { const inputs = typeof midiAccess !== 'undefined' ? Array.from(midiAccess.inputs) : undefined + const naturalKeyColorId = React.useId() + const accidentalKeyColorId = React.useId() return (
@@ -89,9 +91,10 @@ export const SettingsForm: React.FC = ({
-
Natural key color
+
= ({
-
Accidental key color
+
= ({
diff --git a/src/renderer/src/pages/settings.tsx b/src/renderer/src/pages/settings.tsx index bcc49fc..a09e69f 100644 --- a/src/renderer/src/pages/settings.tsx +++ b/src/renderer/src/pages/settings.tsx @@ -1,6 +1,12 @@ import * as React from 'react' import { SettingsForm } from '../components/SettingsForm' -import {Config, defaultConfig, NATURAL_KEY_WIDTHS, RANGES, SCALE_FACTORS} from '../../../common/config' +import { + Config, + defaultConfig, + NATURAL_KEY_WIDTHS, + RANGES, + SCALE_FACTORS +} from '../../../common/config' import { useMidi } from '../hooks/midi' import { useForm } from '../hooks/form' @@ -23,9 +29,7 @@ const SettingsPage: React.FC = ({ params }) => { return (
-

- Settings -

+

Settings