Browse Source

Update settings

Define a new window for settings.
master
TheoryOfNekomata 3 weeks ago
parent
commit
15763b360e
17 changed files with 860 additions and 440 deletions
  1. BIN
      docs/screenshots/screenshot-00.png
  2. +40
    -0
      src/common/config.ts
  3. +121
    -43
      src/main/index.ts
  4. +0
    -390
      src/renderer/src/App.tsx
  5. +68
    -0
      src/renderer/src/components/Activity/index.tsx
  6. +20
    -0
      src/renderer/src/components/DropdownInput/index.tsx
  7. +0
    -0
      src/renderer/src/components/MidiDeviceProvider/index.tsx
  8. +72
    -0
      src/renderer/src/components/PedalBoard/index.tsx
  9. +149
    -0
      src/renderer/src/components/SettingsForm/index.tsx
  10. +18
    -0
      src/renderer/src/hooks/form.ts
  11. +199
    -0
      src/renderer/src/hooks/midi.ts
  12. +24
    -6
      src/renderer/src/main.tsx
  13. +83
    -0
      src/renderer/src/pages/index.tsx
  14. +43
    -0
      src/renderer/src/pages/settings.tsx
  15. +21
    -0
      src/renderer/src/utils/midi.ts
  16. +1
    -1
      tsconfig.node.json
  17. +1
    -0
      tsconfig.web.json

BIN
docs/screenshots/screenshot-00.png View File

Before After
Width: 1044  |  Height: 202  |  Size: 17 KiB Width: 940  |  Height: 187  |  Size: 17 KiB

+ 40
- 0
src/common/config.ts View File

@@ -0,0 +1,40 @@
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'
}

export const SCALE_FACTORS = {
'1x': '1',
'2x': '2'
}

export const NATURAL_KEY_WIDTHS = [
16,
18,
20
]

export interface Config {
range: string
queryDeviceKey: string
scaleFactor: string
naturalKeyColor: string
accidentalKeyColor: string
naturalKeyWidth: string
}

export const defaultConfig: Config = {
range: '21|108',
queryDeviceKey: '',
scaleFactor: '2',
naturalKeyColor: '#e3e3e5',
accidentalKeyColor: '#35313b',
naturalKeyWidth: '18'
}

+ 121
- 43
src/main/index.ts View File

@@ -3,22 +3,10 @@ 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'
}
import { Config, defaultConfig } from '../common/config'

const configPath = './config.json'
const naturalKeyWidth = 20
const height = 175
const height = 160

const getNaturalKeys = (range: string): number => {
const [startKeyRaw, endKeyRaw] = range.split('|')
@@ -43,10 +31,10 @@ const getNaturalKeys = (range: string): number => {
return naturalKeys
}

function createWindow(config: Config): void {
const createMainWindow = async (config: Config): Promise<BrowserWindow> => {
// Create the browser window.
const naturalKeys = getNaturalKeys(config.range)
const width = naturalKeys * naturalKeyWidth + Number(config.scaleFactor)
const width = naturalKeys * Number(config.naturalKeyWidth || 20) + Number(config.scaleFactor)
const mainWindow = new BrowserWindow({
width,
height,
@@ -66,7 +54,7 @@ function createWindow(config: Config): void {
maxWidth: is.dev ? undefined : width,
fullscreenable: false,
resizable: is.dev,
useContentSize: true,
useContentSize: true
})

mainWindow.on('ready-to-show', () => {
@@ -78,6 +66,17 @@ function createWindow(config: Config): void {
return { action: 'deny' }
})

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'])
@@ -85,18 +84,21 @@ function createWindow(config: Config): void {
Object.entries(config).forEach(([key, value]) => {
search.set(key, value)
})
search.set('window', 'main')
url.search = search.toString()
void mainWindow.loadURL(url.toString())
return
await mainWindow.loadURL(url.toString())
return mainWindow
}

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

app.whenReady().then(async () => {
@@ -121,41 +123,117 @@ app.whenReady().then(async () => {
optimizer.watchWindowShortcuts(window)
})

ipcMain.on('querydevicekeychange', async (_event, value) => {
effectiveConfig.queryDeviceKey = value
await writeFile(configPath, JSON.stringify(effectiveConfig))
})
const createSettingsWindow = async (
parent: BrowserWindow,
config: Config
): Promise<BrowserWindow> => {
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
})

ipcMain.on('scalefactorchange', async (event, value) => {
effectiveConfig.scaleFactor = value
await writeFile(configPath, JSON.stringify(effectiveConfig))
await writeFile(configPath, JSON.stringify(effectiveConfig))
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
if (!win) {
return
settingsWindow.on('ready-to-show', () => {
settingsWindow.show()
})

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

settingsWindow.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', 'settings')
url.search = search.toString()
await settingsWindow.loadURL(url.toString())
return settingsWindow
}
win.close()
createWindow(effectiveConfig)
})

ipcMain.on('rangechange', async (event, value) => {
effectiveConfig.range = value
await writeFile(configPath, JSON.stringify(effectiveConfig))
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()
})

return settingsWindow
}

ipcMain.on('action', async (event, value, formData) => {
const webContents = event.sender
const win = BrowserWindow.fromWebContents(webContents)
if (!win) {
return
}
win.close()
createWindow(effectiveConfig)
switch (value) {
case 'showSettings': {
await createSettingsWindow(win, effectiveConfig)
return
}
case 'cancelSaveConfig': {
win.close()
return
}
case 'resetConfig': {
Object.entries(defaultConfig).forEach(([key, value]) => {
effectiveConfig[key] = value
})
await writeFile(configPath, JSON.stringify(effectiveConfig))
win.close()
win.getParentWindow()?.close()
await createMainWindow(effectiveConfig)
return
}
case 'saveConfig': {
Object.entries(formData).forEach(([key, value]) => {
effectiveConfig[key] = value
})
await writeFile(configPath, JSON.stringify(effectiveConfig))
win.close()
win.getParentWindow()?.close()
await createMainWindow(effectiveConfig)
return
}
}
})

createWindow(effectiveConfig)
await createMainWindow(effectiveConfig)

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


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


+ 68
- 0
src/renderer/src/components/Activity/index.tsx View File

@@ -0,0 +1,68 @@
import * as React from 'react'
import {DeviceChannelActive} from '../../hooks/midi';

export interface ActivityProps {
device?: MIDIInput
currentDeviceActive?: DeviceChannelActive
}

const getDeviceDisplayName = (device?: MIDIInput): string | undefined => {
if (typeof device === 'undefined') {
return undefined
}

return [device.name, device.version ? `v${device.version}` : null]
.filter((s) => Boolean(s))
.join(' ')
}

export const Activity: React.FC<ActivityProps> = ({
device,
currentDeviceActive = [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
]
}) => {
const deviceDisplayName = getDeviceDisplayName(device)

return (
<div>
<div className="text-xs">
Input device:{' '}
{typeof deviceDisplayName === 'undefined' && <span className="opacity-50">(None)</span>}
{typeof deviceDisplayName !== 'undefined' && deviceDisplayName}
</div>
<div className="h-1.5 flex gap-1 items-center">
<div
className="rounded-full w-1.5 h-1.5 bg-yellow-400"
style={{
opacity: device?.state === 'connected' ? 1 : 0.25
}}
/>
{currentDeviceActive.map((a, i) => (
<div
className="rounded-full w-1.5 h-1.5 bg-green-400"
key={i}
style={{
opacity: a ? 1 : 0.25
}}
/>
))}
</div>
</div>
)
}

+ 20
- 0
src/renderer/src/components/DropdownInput/index.tsx View File

@@ -0,0 +1,20 @@
import * as React from 'react'

export interface DropdownInputProps extends React.HTMLProps<HTMLElementTagNameMap['select']> {
label?: string
}

export const DropdownInput = React.forwardRef<HTMLElementTagNameMap['select'], DropdownInputProps>(
({ label, ...etcProps }, forwardedRef) => {
return (
<label className="block border rounded overflow-hidden box-border">
<span className="flex items-center h-3 text-xs -mb-3 relative pointer-events-none pt-1 px-1">
{label}
</span>
<select {...etcProps} ref={forwardedRef} className="w-full bg-black h-10 appearance-none pt-2 px-1" />
</label>
)
}
)

DropdownInput.displayName = 'DropdownInput'

+ 0
- 0
src/renderer/src/components/MidiDeviceProvider/index.tsx View File


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


+ 149
- 0
src/renderer/src/components/SettingsForm/index.tsx View File

@@ -0,0 +1,149 @@
import * as React from 'react'
import { DropdownInput } from '../DropdownInput'
import { Config } from '../../../../common/config'

export interface SettingsFormProps extends React.HTMLProps<HTMLElementTagNameMap['form']> {
defaultValues?: Config
ranges: Record<string, string>
scaleFactors: Record<string, string>
midiAccess?: MIDIAccess
lastStateChangeTimestamp?: number
naturalKeyWidths: number[]
}

export const SettingsForm: React.FC<SettingsFormProps> = ({
disabled,
midiAccess,
ranges,
defaultValues = {},
scaleFactors,
lastStateChangeTimestamp,
naturalKeyWidths,
...etcProps
}) => {
const inputs = typeof midiAccess !== 'undefined' ? Array.from(midiAccess.inputs) : undefined

return (
<form {...etcProps}>
<fieldset disabled={disabled} className="contents">
<legend className="sr-only">Settings</legend>
<div className="flex flex-col gap-4">
<div>
<DropdownInput
label="Input device"
name="queryDeviceKey"
defaultValue={defaultValues?.['queryDeviceKey']}
key={lastStateChangeTimestamp}
>
{inputs?.map(([name, device]) => (
<option key={name} value={name}>
{device.name}
</option>
))}
</DropdownInput>
</div>
<div className="flex-auto">
<DropdownInput
name="range"
label="Keyboard range"
defaultValue={defaultValues?.['range']}
>
{Object.entries(ranges).map(([name, range]) => (
<option key={name} value={range}>
{name}
</option>
))}
</DropdownInput>
</div>
<div className="flex justify-between items-center gap-8 h-10">
<div>Scale factor</div>
<div className="flex items-center gap-8">
{Object.entries(scaleFactors).map(([label, value]) => (
<div key={value}>
<label>
<input
name="scaleFactor"
type="radio"
value={value}
defaultChecked={
defaultValues?.['scaleFactor']?.toString() === value.toString()
}
/>{' '}
{label}
</label>
</div>
))}
</div>
</div>
<div className="flex-auto">
<DropdownInput
name="naturalKeyWidth"
label="Natural key width"
defaultValue={defaultValues?.['naturalKeyWidth']}
>
{naturalKeyWidths.map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</DropdownInput>
</div>
<div className="flex justify-between items-center gap-8">
<div>Natural key color</div>
<div>
<input
name="naturalKeyColor"
type="color"
className="h-10"
defaultValue={defaultValues?.['naturalKeyColor']}
/>
</div>
</div>
<div className="flex justify-between items-center gap-8">
<div>Accidental key color</div>
<div>
<input
name="accidentalKeyColor"
type="color"
className="h-10"
defaultValue={defaultValues?.['accidentalKeyColor']}
/>
</div>
</div>
<div className="mt-12 flex gap-4">
<div className="w-0 flex-auto">
<button
className="w-full h-10 rounded border overflow-hidden"
type="submit"
name="action"
value="cancelSaveConfig"
>
Cancel
</button>
</div>
<div className="w-0 flex-auto">
<button
className="w-full h-10 rounded border overflow-hidden"
type="submit"
name="action"
value="resetConfig"
>
Reset to Defaults
</button>
</div>
</div>
<div>
<button
className="w-full h-10 rounded border bg-white text-black overflow-hidden"
type="submit"
name="action"
value="saveConfig"
>
Save
</button>
</div>
</div>
</fieldset>
</form>
)
}

+ 18
- 0
src/renderer/src/hooks/form.ts View File

@@ -0,0 +1,18 @@
import * as React from 'react'

export const useForm = () => {
const handleAction: React.FormEventHandler<HTMLElementTagNameMap['form']> = (e) => {
e.preventDefault()
const { submitter } = e.nativeEvent as unknown as { submitter: HTMLElementTagNameMap['button'] }
if (submitter.name !== 'action') {
return
}
const formValue = new FormData(e.currentTarget)
const values = Object.fromEntries(formValue.entries())
window.electron.ipcRenderer.send('action', submitter.value, values)
}

return {
handleAction
}
}

+ 199
- 0
src/renderer/src/hooks/midi.ts View File

@@ -0,0 +1,199 @@
import * as React from 'react'
import { messages } from '../utils/midi'

export type DeviceChannelActive = [
boolean,
boolean,
boolean,
boolean,
boolean,
boolean,
boolean,
boolean,
boolean,
boolean,
boolean,
boolean,
boolean,
boolean,
boolean,
boolean
]

interface MidiMessageEvent extends Event {
data: [number, number, number]
}

interface UseMidiReturn {
midiAccess?: MIDIAccess
lastStateChangeTimestamp?: number
}
interface ChannelData {
channel: number
key: number
velocity: number
}

export const useMidi = (): UseMidiReturn => {
const [lastStateChangeTimestamp, setLastStateChangeTimestamp] = React.useState<number>()
const [midiAccess, setMidiAccess] = React.useState<MIDIAccess>()

React.useEffect(() => {
const stateChangeListener = (e: Event): void => {
setLastStateChangeTimestamp(e.timeStamp)
}

window.navigator.requestMIDIAccess().then((midiAccess) => {
setMidiAccess(midiAccess)
setLastStateChangeTimestamp(Date.now())
midiAccess.addEventListener('statechange', stateChangeListener)
})

return (): void => {
midiAccess?.removeEventListener('statechange', stateChangeListener)
}
}, [])

return {
midiAccess,
lastStateChangeTimestamp
}
}

interface UseMidiActivityReturn {
isChannelActive: DeviceChannelActive
unaCorda: number
sostenuto: number
sustain: number
keyChannels: ChannelData[]
}

export const useMidiActivity = (currentDevice?: MIDIInput): UseMidiActivityReturn => {
const [isChannelActive, setIsChannelActive] = React.useState<DeviceChannelActive>([
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
])
const currentDeviceActiveTimeoutRef = React.useRef<number>()
const [unaCorda, setUnaCorda] = React.useState(0)
const [sostenuto, setSostenuto] = React.useState(0)
const [sustain, setSustain] = React.useState(0)

const [keyChannels, setKeyChannels] = React.useState<ChannelData[]>([])

React.useEffect(() => {
if (typeof currentDevice === 'undefined') {
return
}

const addActivity = (channel: number): void => {
setIsChannelActive(
(oldCurrentDeviceActive) =>
oldCurrentDeviceActive.map((state, i) =>
i === channel ? true : state
) as DeviceChannelActive
)
window.clearTimeout(currentDeviceActiveTimeoutRef.current)
currentDeviceActiveTimeoutRef.current = window.setTimeout(() => {
setIsChannelActive(
(oldCurrentDeviceActive) =>
oldCurrentDeviceActive.map((state, i) =>
i === channel ? false : state
) as DeviceChannelActive
)
}, 100)
}

const listener = (e: Event): void => {
if (e.type !== 'midimessage') {
return
}

const midiEvent = e as MidiMessageEvent
const [messageType, param1, param2] = midiEvent.data
const channel = messageType & messages.CHANNEL_BITMASK
addActivity(channel)

if (channel === messages.CHANNEL_INDEX_PERCUSSION) {
return
}

switch (messageType & messages.TYPE_BITMASK) {
case messages.types.CONTINUOUS_CONTROL: {
const controlNumber = param1
const value = param2

switch (controlNumber) {
case messages.params.continuousControl.sustain:
setSustain(value)
return
case messages.params.continuousControl.sostenuto:
setSostenuto(value)
return
case messages.params.continuousControl.unaCorda:
setUnaCorda(value)
return
default:
break
}
return
}
case messages.types.NOTE_ON: {
const keyNumber = param1
const velocity = param2

setKeyChannels((oldKeyChannels) => {
if (velocity > 0) {
return [
...oldKeyChannels,
{
channel,
key: keyNumber,
velocity
}
]
}

return oldKeyChannels.filter((c) => !(c.channel === channel && c.key === keyNumber))
})
return
}
case messages.types.NOTE_OFF: {
const keyNumber = param1

setKeyChannels((oldKeyChannels) => {
return oldKeyChannels.filter((c) => !(c.channel === channel && c.key === keyNumber))
})
return
}
default:
break
}
}
currentDevice.addEventListener('midimessage', listener)
return () => {
currentDevice.removeEventListener('midimessage', listener)
}
}, [currentDevice])

return {
isChannelActive,
unaCorda,
sostenuto,
sustain,
keyChannels
}
}

+ 24
- 6
src/renderer/src/main.tsx View File

@@ -2,17 +2,35 @@ import './assets/main.css'

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import IndexPage from './pages'
import SettingsPage from './pages/settings'
import { defaultConfig } from '../../common/config'

const search = new URLSearchParams(window.location.search)
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'))

const scaleFactorRaw = search.get('scaleFactor') || defaultConfig.scaleFactor
const scaleFactorTryNumber = Number(scaleFactorRaw)
const scaleFactor = Number.isFinite(scaleFactorTryNumber) ? scaleFactorTryNumber : 1
window.document.documentElement.style.setProperty('--size-scale-factor', scaleFactor.toString())

const naturalKeyColor = search.get('naturalKeyColor') || defaultConfig.naturalKeyColor
window.document.documentElement.style.setProperty('--color-natural-key', naturalKeyColor)

const accidentalKeyColor = search.get('accidentalKeyColor') || defaultConfig.accidentalKeyColor
window.document.documentElement.style.setProperty('--color-accidental-key', accidentalKeyColor)

window.document.documentElement.style.setProperty('--color-channel-0', search.get('highlightColor'))

const windows = {
main: IndexPage,
settings: SettingsPage
}

const currentWindowKey = search.get('window') ?? 'main'
const Page = windows[currentWindowKey]

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
<Page params={search} />
</React.StrictMode>
)

+ 83
- 0
src/renderer/src/pages/index.tsx View File

@@ -0,0 +1,83 @@
import Keyboard, {
StyledNaturalKey,
StyledAccidentalKey
} from '@theoryofnekomata/react-musical-keyboard'
import * as React from 'react'
import { PedalBoard } from '../components/PedalBoard'
import { Activity } from '../components/Activity'
import { useMidi, useMidiActivity } from '../hooks/midi'
import { RANGES } from '../../../common/config'
import { useForm } from '../hooks/form'

interface IndexPageProps {
params: URLSearchParams
}

const IndexPage: React.FC<IndexPageProps> = ({ params: search }) => {
const { handleAction } = useForm()

const { midiAccess, lastStateChangeTimestamp: devicesLoadedTimestamp } = useMidi()
const inputDevices = Array.from(midiAccess?.inputs.entries() ?? [])
const queryDeviceKey = search.get('queryDeviceKey') || inputDevices?.[0]?.[0] || ''
const currentDevice =
typeof midiAccess?.inputs !== 'undefined' ? midiAccess.inputs.get(queryDeviceKey) : undefined

const { keyChannels, unaCorda, isChannelActive, sustain, sostenuto } =
useMidiActivity(currentDevice)

const range = search.get('range') || RANGES['A0-C8']
const [startKeyRaw, endKeyRaw] = range.split('|')
const startKey = Number(startKeyRaw)
const endKey = Number(endKeyRaw)

return (
<div
className="flex flex-col items-center h-full w-full group relative select-none"
key={devicesLoadedTimestamp}
>
<div className="absolute bottom-1 left-1">
<Activity device={currentDevice} currentDeviceActive={isChannelActive} />
</div>
<div className="flex-auto relative w-full">
<div
className="absolute w-full h-full"
style={{
paddingRight: 'calc(var(--size-scale-factor) * 1px)'
}}
>
<Keyboard
key={range}
height="100%"
startKey={startKey}
endKey={endKey}
keyChannels={keyChannels}
keyComponents={{
natural: StyledNaturalKey,
accidental: StyledAccidentalKey
}}
/>
</div>
</div>
<div className="flex sm:gap-16 w-full justify-between sm:justify-center items-center px-4 gap-4">
<div className="w=0 flex-auto hidden sm:block" />
<div className="flex-shrink-0 pt-2 pb-8">
<PedalBoard unaCorda={unaCorda} sostenuto={sostenuto} sustain={sustain} />
</div>
<div className="w-0 flex-auto flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity">
<form className="contents" onSubmit={handleAction}>
<button
type="submit"
name="action"
value="showSettings"
className="h-10 border rounded overflow-hidden px-4"
>
Settings
</button>
</form>
</div>
</div>
</div>
)
}

export default IndexPage

+ 43
- 0
src/renderer/src/pages/settings.tsx View File

@@ -0,0 +1,43 @@
import * as React from 'react'
import { SettingsForm } from '../components/SettingsForm'
import {Config, defaultConfig, NATURAL_KEY_WIDTHS, RANGES, SCALE_FACTORS} from '../../../common/config'
import { useMidi } from '../hooks/midi'
import { useForm } from '../hooks/form'

interface SettingsPageProps {
params: URLSearchParams
}

const SettingsPage: React.FC<SettingsPageProps> = ({ params }) => {
const { handleAction } = useForm()
const { midiAccess, lastStateChangeTimestamp } = useMidi()

const defaultValues = Object.entries(defaultConfig).reduce(
(theConfig, [key, value]) => ({
...theConfig,
[key]: params.get(key) || value
}),
{} as Config
)

return (
<main className="py-16 select-none">
<div className="px-4 mx-auto max-w-screen-sm">
<h1 className="text-5xl mb-8">
Settings
</h1>
<SettingsForm
lastStateChangeTimestamp={lastStateChangeTimestamp}
midiAccess={midiAccess}
defaultValues={defaultValues}
ranges={RANGES}
scaleFactors={SCALE_FACTORS}
naturalKeyWidths={NATURAL_KEY_WIDTHS}
onSubmit={handleAction}
/>
</div>
</main>
)
}

export default SettingsPage

+ 21
- 0
src/renderer/src/utils/midi.ts View File

@@ -0,0 +1,21 @@
export namespace messages {
export namespace types {
export const NOTE_ON = 0b10010000
export const NOTE_OFF = 0b10000000
export const CONTINUOUS_CONTROL = 0b10110000
}

export namespace params {
export namespace continuousControl {
export const sustain = 64
export const sostenuto = 66
export const unaCorda = 67
}
}

export const TYPE_BITMASK = 0b11110000

export const CHANNEL_BITMASK = 0b00001111

export const CHANNEL_INDEX_PERCUSSION = 9
}

+ 1
- 1
tsconfig.node.json View File

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


+ 1
- 0
tsconfig.web.json View File

@@ -1,6 +1,7 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
"include": [
"src/common/**/*",
"src/renderer/src/env.d.ts",
"src/renderer/src/**/*",
"src/renderer/src/**/*.tsx",


Loading…
Cancel
Save