UPD: Split store into app and device

This commit is contained in:
Robert Kossessa
2024-03-12 10:36:08 +01:00
parent 0c117e2c76
commit 974beee118
14 changed files with 478 additions and 472 deletions

View File

@@ -2,7 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron'
// expose an API to choose available devices // expose an API to choose available devices
contextBridge.exposeInMainWorld('nanoSerialApi', { contextBridge.exposeInMainWorld('nanoSerialApi', {
list_devices() { listConnectedDevices() {
return ipcRenderer.invoke('nanoSerialApi:list_devices') return ipcRenderer.invoke('nanoSerialApi:list_devices')
}, },
connect(deviceid) { connect(deviceid) {
@@ -11,20 +11,13 @@ contextBridge.exposeInMainWorld('nanoSerialApi', {
disconnect(deviceid) { disconnect(deviceid) {
return ipcRenderer.invoke('nanoSerialApi:disconnect', deviceid) return ipcRenderer.invoke('nanoSerialApi:disconnect', deviceid)
}, },
on_event(eventid_filter, callback) { on(callback) {
//console.log('attaching filter for ', eventid_filter)
ipcRenderer.on('nanoSerialApi:event', (_event, eventid, deviceid, ...data) => { ipcRenderer.on('nanoSerialApi:event', (_event, eventid, deviceid, ...data) => {
//console.log('Event in ipcRenderer ', eventid, deviceid, data) callback(eventid, deviceid, ...data)
if (eventid_filter == '*' || eventid_filter == eventid) {
callback(eventid, deviceid, ...data)
}
}) })
}, },
send(deviceid, obj) { send(deviceid, obj) {
return ipcRenderer.invoke('nanoSerialApi:send', deviceid, JSON.stringify(obj)) return ipcRenderer.invoke('nanoSerialApi:send', deviceid, JSON.stringify(obj))
},
save(deviceid) {
return ipcRenderer.invoke('nanoSerialApi:send', deviceid, JSON.stringify({ save: true }))
} }
}) })

View File

@@ -3,41 +3,39 @@ import ProfileManager from '@renderer/components/profile/ProfileManager.vue'
import DevicePreview from '@renderer/components/device/DevicePreview.vue' import DevicePreview from '@renderer/components/device/DevicePreview.vue'
import ConfigPane from '@renderer/components/config/ConfigPane.vue' import ConfigPane from '@renderer/components/config/ConfigPane.vue'
import Navbar from '@renderer/components/navbar/Navbar.vue' import Navbar from '@renderer/components/navbar/Navbar.vue'
import { useStore } from '@renderer/store' import { useDeviceStore } from '@renderer/deviceStore'
import { useMessageHandlers } from '@renderer/device'
const { electronApi, nanoSerialApi } = window const deviceStore = useDeviceStore()
const store = useStore()
const menuActions = { // const menuActions = {
connect: () => store.setConnected(!store.connected), // connect: () => store.setConnected(!store.connected),
orientation: () => store.cycleScreenOrientation(), // orientation: () => store.cycleScreenOrientation(),
skin: () => store.switchPreviewDeviceModel() // skin: () => store.switchPreviewDeviceModel()
} // }
electronApi.onMenu((key) => { // electronApi.onMenu((key) => {
console.log('menu', key) // console.log('menu', key)
if (menuActions[key]) { // if (menuActions[key]) {
menuActions[key]() // menuActions[key]()
} // }
}) // })
store.fetchProfiles() // TODO remove me! // store.fetchProfiles() // TODO remove me!
// handle device events // handle device events
const handlers = useMessageHandlers(store) // const handlers = useMessageHandlers(store)
nanoSerialApi.on_event('device-attached', (evt, deviceid, data) => store.device_attached(deviceid)) // nanoSerialApi.on_event('device-attached', (evt, deviceid, data) => store.device_attached(deviceid))
nanoSerialApi.on_event('device-detached', (evt, deviceid, data) => store.device_detached(deviceid)) // nanoSerialApi.on_event('device-detached', (evt, deviceid, data) => store.device_detached(deviceid))
nanoSerialApi.on_event('device-error', (evt, deviceid, data) => { // nanoSerialApi.on_event('device-error', (evt, deviceid, data) => {
/* TODO handle connection errors */ // /* TODO handle connection errors */
}) // })
nanoSerialApi.on_event('connected', (evt, deviceid, data) => store.device_connected(deviceid)) // nanoSerialApi.on_event('connected', (evt, deviceid, data) => store.device_connected(deviceid))
nanoSerialApi.on_event('disconnected', (evt, deviceid, data) => store.device_disconnected(deviceid)) // nanoSerialApi.on_event('disconnected', (evt, deviceid, data) => store.device_disconnected(deviceid))
nanoSerialApi.on_event('update', (evt, deviceid, data) => { // nanoSerialApi.on_event('update', (evt, deviceid, data) => {
handlers.handle_message(data) // handlers.handle_message(data)
}) // })
// get list of the currently attached devices // // get list of the currently attached devices
nanoSerialApi.list_devices().then((devs) => store.init_devices(devs)) // nanoSerialApi.list_devices().then((devs) => store.init_devices(devs))
</script> </script>
<template> <template>
<main class="flex h-screen w-screen select-none flex-col"> <main class="flex h-screen w-screen select-none flex-col">
@@ -46,7 +44,7 @@ nanoSerialApi.list_devices().then((devs) => store.init_devices(devs))
<div class="flex min-w-60 flex-1 basis-1/3 overflow-hidden"> <div class="flex min-w-60 flex-1 basis-1/3 overflow-hidden">
<Transition name="slide-left"> <Transition name="slide-left">
<ProfileManager <ProfileManager
v-if="store.connected" v-if="deviceStore.connected"
class="flex max-w-full flex-1 flex-col border-0 border-r border-solid bg-zinc-900/50" class="flex max-w-full flex-1 flex-col border-0 border-r border-solid bg-zinc-900/50"
/> />
</Transition> </Transition>
@@ -55,7 +53,7 @@ nanoSerialApi.list_devices().then((devs) => store.init_devices(devs))
<div class="flex flex-1 basis-2/5 overflow-hidden"> <div class="flex flex-1 basis-2/5 overflow-hidden">
<Transition name="slide-right"> <Transition name="slide-right">
<ConfigPane <ConfigPane
v-if="store.connected" v-if="deviceStore.connected"
class="flex max-w-full flex-1 flex-col border-0 border-l border-solid bg-zinc-900/50" class="flex max-w-full flex-1 flex-col border-0 border-l border-solid bg-zinc-900/50"
/> />
</Transition> </Transition>

View File

@@ -0,0 +1,271 @@
import { defineStore } from 'pinia'
import WIP from '@renderer/components/WIP.vue'
import KnobFeedbackConfig from '@renderer/components/config/knob/KnobFeedbackConfig.vue'
import KnobLightConfig from '@renderer/components/config/knob/KnobLightConfig.vue'
import KeyLightConfig from '@renderer/components/config/keys/KeyLightConfig.vue'
import KnobMappingConfig from '@renderer/components/config/knob/KnobMappingConfig.vue'
import KeyMappingConfig from '@renderer/components/config/keys/KeyMappingConfig.vue'
import { shallowRef } from 'vue'
export const useAppStore = defineStore('app', {
state: () => {
return {
selectedFeature: 'knob',
selectedKey: 'a',
currentConfigPage: 'mapping',
configPages: {
knob: {
mapping: {
titleKey: 'config_options.mapping_configuration.title',
component: shallowRef(KnobMappingConfig)
},
feedback: {
titleKey: 'config_options.feedback_designer.title',
component: shallowRef(KnobFeedbackConfig)
},
lighting: {
titleKey: 'config_options.light_designer.title',
component: shallowRef(KnobLightConfig)
}
},
key: {
mapping: {
titleKey: 'config_options.mapping_configuration.title',
component: shallowRef(KeyMappingConfig)
},
lighting: {
titleKey: 'config_options.light_designer.title',
component: shallowRef(KeyLightConfig)
}
}
},
previewDeviceModel: localStorage.getItem('previewDeviceModel') || 'nanoOne'
}
},
getters: {
// profiles: (state) => state.profileCategories.flatMap((c) => c.profiles),
// profileIds: (state) => state.profiles.map((p) => p.id),
// selectedProfileCategory: (state) =>
// state.profileCategories.find((c) => c.profiles.find((p) => p.id === state.selectedProfileId)),
// selectedProfile: (state) => state.profiles.find((p) => p.id === state.selectedProfileId),
currentConfigComponent: (state) =>
state.configPages[state.selectedFeature][state.currentConfigPage]?.component || WIP,
currentConfigPages: (state) => state.configPages[state.selectedFeature] || {}
// multipleDevicesConnected: (state) => state.connectedDevices.length > 1,
// numAttachedDevices: (state) => Object.keys(state.devices).length
// connected: (state) => state.connectedId !== null,
},
actions: {
// selectProfile(id) {
// if (!this.profileIds.includes(id)) return false
// this.selectedProfileId = id
// return true
// },
// addProfile(profile, categoryIndex, newIndex) {
// const category = this.profileCategories[categoryIndex]
// category.profiles.splice(newIndex, 0, profile)
// },
// removeProfile(profileId) {
// const category = this.profileCategories.find((c) =>
// c.profiles.find((p) => p.id === profileId)
// )
// const index = category.profiles.findIndex((p) => p.id === profileId)
// category.profiles.splice(index, 1)
// },
// duplicateProfile(profileId) {
// const profile = this.profiles.find((p) => p.id === profileId)
// const newProfile = JSON.parse(JSON.stringify(profile))
// newProfile.id = this.newProfileId(profile.id)
// newProfile.name = this.newProfileName(profile.name)
// const category = this.profileCategories.find((c) =>
// c.profiles.find((p) => p.id === profileId)
// )
// const index = category.profiles.findIndex((p) => p.id === profileId)
// category.profiles.splice(index + 1, 0, newProfile)
// this.selectProfile(newProfile.id)
// },
// moveProfile(profileId, oldIndex, newIndex) {
// // Find the profile category, then swap the profiles at the old and new indices
// const category = this.profileCategories.find((c) =>
// c.profiles.find((p) => p.id === profileId)
// )
// const tmpProfile = category.profiles[newIndex]
// category.profiles[newIndex] = category.profiles[oldIndex]
// category.profiles[newIndex] = tmpProfile
// },
// moveProfileCategory(categoryName, oldIndex, newIndex) {
// const tmpCategory = this.profileCategories[newIndex]
// this.profileCategories[newIndex] = this.profileCategories[oldIndex]
// this.profileCategories[newIndex] = tmpCategory
// },
// changeProfileCategory(profileId, newCategoryIndex, newIndex) {
// const profile = this.profiles.find((p) => p.id === profileId)
// const oldCategory = this.profileCategories.find((c) =>
// c.profiles.find((p) => p.id === profileId)
// )
// const newCategory = this.profileCategories[newCategoryIndex]
// oldCategory.profiles = oldCategory.profiles.filter((p) => p.id !== profileId)
// newCategory.profiles.splice(newIndex, 0, profile)
// },
// renameProfile(profileId, newName) {
// const profile = this.profiles.find((p) => p.id === profileId)
// profile.name = newName
// },
// fetchProfiles() {
// const categories = mockData.categories
// console.log(categories)
// const ids = new Set()
// // const validate = ajv.compile(schema) // see below
// this.$patch({
// profileCategories: categories.map((category) => ({
// name: category.name,
// profiles: category.profiles.filter((profile) => {
// // Ajv validation requires unsafe-eval CSP, let's not do that
// // TODO: Remove ajv validation completely or compile schema at build time
// // if (!validate(profile)) {
// // console.error('Failed to validate profile: ' + profile.name, validate.errors)
// // return false
// // }
// if (ids.has(profile.id)) {
// console.error('Duplicate profile id: ' + profile.id + ' for profile: ' + profile.name)
// return false
// }
// ids.add(profile.id)
// return true
// })
// })),
// selectedProfileId: categories[0]?.profiles[0]?.id || null
// })
// },
// newProfileName(originalName = '') {
// let name = originalName
// let i = 1
// while (this.profiles.find((p) => p.name === name)) {
// name = `${originalName} (${i++})`
// }
// return name
// },
// newProfileId(originalId = '') {
// let id = originalId
// if (originalId) {
// do {
// id = Math.floor((parseInt(id) + 1) % 9999)
// .toString()
// .padStart(4, '0')
// } while (this.profileIds.includes(id))
// } else {
// do {
// id = Math.floor(Math.random() * 9999)
// .toString()
// .padStart(4, '0')
// } while (this.profileIds.includes(id))
// }
// return id
// },
selectConfigFeature(feature) {
this.selectedFeature = feature
if (!this.currentConfigPages[this.currentConfigPage]) this.setCurrentConfigPage('mapping')
},
selectKey(key) {
this.selectedKey = key
this.selectConfigFeature('key')
},
setCurrentConfigPage(page) {
this.currentConfigPage = page
},
// setConnected(connected) {
// this.connected = connected
// },
switchPreviewDeviceModel() {
this.previewDeviceModel = this.previewDeviceModel === 'nanoOne' ? 'nanoZero' : 'nanoOne'
localStorage.setItem('previewDeviceModel', this.previewDeviceModel)
}
// cycleScreenOrientation() {
// this.screenOrientation = (this.screenOrientation + 90) % 360
// },
// setKeyDefaultColor(color) {
// // this.selectedProfile.keys[this.selectedKey].default = color
// const props = {}
// props[`button${this.selectedKey.toUpperCase()}Idle`] = color.rgbNumber()
// nanoSerialApi.send(this.connectedId, { p: { name: 'Default Profile', ...props } })
// },
// setKeyPressedColor(color) {
// // this.selectedProfile.keys[this.selectedKey].pressed = color
// const props = {}
// props[`button${this.selectedKey.toUpperCase()}Press`] = color.rgbNumber()
// nanoSerialApi.send(this.connectedId, { p: { name: 'Default Profile', ...props } })
// },
// // devices, device attachment, connection, and disconnection
// init_devices(ids) {
// console.log('Initializing devices: ', ids)
// for (const id of ids) this.update_devices(id, true)
// if (Object.keys(this.devices).length == 1) {
// // TODO auto-connect to the device
// const deviceid = Object.keys(this.devices)[0]
// console.log('Auto-connecting to device ', deviceid)
// window.nanoSerialApi.connect(deviceid)
// }
// },
// update_devices(deviceid, attached) {
// if (attached) {
// if (!this.devices.hasOwnProperty(deviceid))
// this.devices[deviceid] = { serialNumber: deviceid, connected: false }
// } else {
// if (this.devices.hasOwnProperty(deviceid)) delete this.devices[deviceid] // TODO maybe mark as detached instead of deleting? then we can remember its name, etc...
// }
// },
// device_attached(deviceid) {
// this.update_devices(deviceid, true)
// if (Object.keys(this.devices).length == 1) {
// // TODO auto-connect to the device
// console.log('Auto-connecting to device ', deviceid)
// window.nanoSerialApi.connect(deviceid)
// }
// },
// device_detached(deviceid) {
// if (this.devices[deviceid].connected) {
// // detached event arrived before disconnected event?
// this.devices[deviceid].connected = false
// this.connected = false
// }
// this.update_devices(deviceid, false)
// },
// device_connected(deviceid) {
// this.devices[deviceid].connected = true
// this.connected = true
// this.connectedId = deviceid
// // TODO load profiles from device
// // nanoSerialApi.send(deviceid, { profiles: "#all" }) // request profiles
// // "Default Profile", for now, is the only profile after the device
// // starts up, so it is also the current (eg. 'selected') profile
// // nanoSerialApi.send(deviceid, { p: "Default Profile" }) // request Default Profile
// // TODO maybe you want to request all the profiles right now?
// // or only on demand?
// },
// device_disconnected(deviceid) {
// this.devices[deviceid].connected = false
// this.connected = false
// this.connectedId = null
// // TODO switch UI to disconnected state
// },
// // device events
// update_knob_position(turns, angle, velocity) {
// this.turns = turns
// this.angle = angle
// this.velocity = velocity
// this.last_event = Date.now()
// },
// update_keystate(keystate) {
// this.keyState = keystate
// this.last_event = Date.now()
// },
// // settings changes
// update_device_name(name) {
// this.devices[this.connectedId].name = name
// }
}
})

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<template v-if="store.selectedProfile"> <template v-if="deviceStore.currentProfile">
<TabSelect <TabSelect
v-if="showTabs" v-if="showTabs"
v-model="configPage" v-model="configPage"
@@ -12,7 +12,7 @@
</template> </template>
</TabSelect> </TabSelect>
<div class="grow overflow-y-auto"> <div class="grow overflow-y-auto">
<component :is="store.currentConfigComponent" /> <component :is="appStore.currentConfigComponent" />
</div> </div>
</template> </template>
<template v-else> <template v-else>
@@ -29,18 +29,20 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { useStore } from '@renderer/store' import { useAppStore } from '@renderer/appStore'
import { useDeviceStore } from '@renderer/deviceStore'
import TabSelect from '@renderer/components/common/TabSelect.vue' import TabSelect from '@renderer/components/common/TabSelect.vue'
import { computed } from 'vue' import { computed } from 'vue'
import ScrambleText from '@renderer/components/common/ScrambleText.vue' import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { ChevronLeft } from 'lucide-vue-next' import { ChevronLeft } from 'lucide-vue-next'
const store = useStore() const appStore = useAppStore()
const deviceStore = useDeviceStore()
const configPages = computed(() => store.currentConfigPages) const configPages = computed(() => appStore.currentConfigPages)
const configPage = computed({ const configPage = computed({
get: () => store.currentConfigPage, get: () => appStore.currentConfigPage,
set: (value) => store.setCurrentConfigPage(value) set: (value) => appStore.setCurrentConfigPage(value)
}) })
defineProps({ defineProps({

View File

@@ -9,9 +9,9 @@ import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import PaletteInput from '@renderer/components/common/PaletteInput.vue' import PaletteInput from '@renderer/components/common/PaletteInput.vue'
import Color from 'color' import Color from 'color'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { useStore } from '@renderer/store' import { useDeviceStore } from '@renderer/deviceStore'
const store = useStore() const deviceStore = useDeviceStore()
const keyColors = ref({ const keyColors = ref({
default: { default: {
@@ -27,8 +27,8 @@ const keyColors = ref({
watch( watch(
keyColors, keyColors,
(newVal) => { (newVal) => {
store.setKeyDefaultColor(newVal.default.color) // store.setKeyDefaultColor(newVal.default.color)
store.setKeyPressedColor(newVal.pressed.color) // store.setKeyPressedColor(newVal.pressed.color)
}, },
{ deep: true } { deep: true }
) )

View File

@@ -1,17 +1,17 @@
<template> <template>
<ConfigSection :title="`${store.selectedKey} Pressed`" :icon-component="PanelBottomClose"> <ConfigSection :title="`${appStore.selectedKey} Pressed`" :icon-component="PanelBottomClose">
<template #title> <template #title>
<span class="text-zinc-500">&nbsp;({{ actionsPressed.length }})</span></template <span class="text-zinc-500">&nbsp;({{ actionsPressed.length }})</span></template
> >
<ActionGroup :actions="actionsPressed" class="p-2" /> <ActionGroup :actions="actionsPressed" class="p-2" />
</ConfigSection> </ConfigSection>
<ConfigSection :title="`${store.selectedKey} Released`" :icon-component="PanelBottomOpen"> <ConfigSection :title="`${appStore.selectedKey} Released`" :icon-component="PanelBottomOpen">
<template #title> <template #title>
<span class="text-zinc-500">&nbsp;({{ actionsReleased.length }})</span></template <span class="text-zinc-500">&nbsp;({{ actionsReleased.length }})</span></template
> >
<ActionGroup :actions="actionsReleased" class="p-2" /> <ActionGroup :actions="actionsReleased" class="p-2" />
</ConfigSection> </ConfigSection>
<ConfigSection :title="`${store.selectedKey} Held`" :icon-component="Clock2"> <ConfigSection :title="`${appStore.selectedKey} Held`" :icon-component="Clock2">
<template #title> <template #title>
<span class="text-zinc-500">&nbsp;({{ actionsHeld.length }})</span></template <span class="text-zinc-500">&nbsp;({{ actionsHeld.length }})</span></template
> >
@@ -21,11 +21,11 @@
<script setup> <script setup>
import { PanelBottomClose, PanelBottomOpen, Clock2 } from 'lucide-vue-next' import { PanelBottomClose, PanelBottomOpen, Clock2 } from 'lucide-vue-next'
import ConfigSection from '@renderer/components/common/ConfigSection.vue' import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import { useStore } from '@renderer/store' import { useAppStore } from '@renderer/appStore'
import { ref } from 'vue' import { ref } from 'vue'
import ActionGroup from '@renderer/components/config/actions/ActionGroup.vue' import ActionGroup from '@renderer/components/config/actions/ActionGroup.vue'
const store = useStore() const appStore = useAppStore()
const actionsPressed = ref([ const actionsPressed = ref([
{ {
id: '1' id: '1'

View File

@@ -8,7 +8,7 @@
}" }"
> >
<Transition name="fade"> <Transition name="fade">
<div v-if="store.connected" class="flex h-12 items-center justify-between px-10"> <div v-if="deviceStore.connected" class="flex h-12 items-center justify-between px-10">
<h2> <h2>
<ScrambleText <ScrambleText
:delay="100" :delay="100"
@@ -32,7 +32,7 @@
</Transition> </Transition>
<Transition name="fade-delayed"> <Transition name="fade-delayed">
<DeviceLEDRing <DeviceLEDRing
v-if="store.connected" v-if="deviceStore.connected"
:value="barValue" :value="barValue"
class="absolute inset-x-0 top-[12.5%] mx-auto h-[66%]" class="absolute inset-x-0 top-[12.5%] mx-auto h-[66%]"
/> />
@@ -44,11 +44,13 @@
> >
<TransitionGroup name="fade-display"> <TransitionGroup name="fade-display">
<div <div
v-if="store.connected" v-if="deviceStore.connected"
class="absolute flex flex-col items-center pb-2 text-center mix-blend-screen" class="absolute flex flex-col items-center pb-2 text-center mix-blend-screen"
> >
<img :src="LogoMidi" alt="midi-logo" class="h-4 opacity-50" /> <img :src="LogoMidi" alt="midi-logo" class="h-4 opacity-50" />
<h2 class="font-pixellg text-5xl">{{ parseInt(barValue - store.turns * 100) }}</h2> <h2 class="font-pixellg text-5xl">
{{ parseInt(barValue - deviceStore.turns * 100) }}
</h2>
<div class="font-pixelsm text-md">HIGH PASS</div> <div class="font-pixelsm text-md">HIGH PASS</div>
<DeviceBar :value="barValue" :count="30" :width="120" /> <DeviceBar :value="barValue" :count="30" :width="120" />
<span class="font-pixelsm w-36 text-[7pt] uppercase text-muted-foreground"> <span class="font-pixelsm w-36 text-[7pt] uppercase text-muted-foreground">
@@ -71,21 +73,21 @@
</div> </div>
<Transition name="fade-delayed"> <Transition name="fade-delayed">
<button <button
v-if="store.connected" v-if="deviceStore.connected"
class="absolute inset-x-0 top-[24.5%] mx-auto aspect-square h-[41.5%] rounded-full outline-2 transition-all" class="absolute inset-x-0 top-[24.5%] mx-auto aspect-square h-[41.5%] rounded-full outline-2 transition-all"
:class="{ :class="{
'outline outline-white': store.selectedFeature === 'knob', 'outline outline-white': appStore.selectedFeature === 'knob',
'outline-zinc-400 hover:outline': store.selectedFeature !== 'knob' 'outline-zinc-400 hover:outline': appStore.selectedFeature !== 'knob'
}" }"
@click="store.selectConfigFeature('knob')" @click="appStore.selectConfigFeature('knob')"
/> />
</Transition> </Transition>
<Transition name="fade-delayed"> <Transition name="fade-delayed">
<DeviceKeys <DeviceKeys
v-if="store.connected" v-if="deviceStore.connected"
class="absolute inset-x-0 top-[77.5%] mx-auto w-[72.7%] gap-[2.2%]" class="absolute inset-x-0 top-[77.5%] mx-auto w-[72.7%] gap-[2.2%]"
:selected="store.selectedFeature === 'key' ? store.selectedKey : ''" :selected="appStore.selectedFeature === 'key' ? appStore.selectedKey : ''"
@select="store.selectKey" @select="appStore.selectKey"
/> />
</Transition> </Transition>
</div> </div>
@@ -96,15 +98,17 @@ import RenderNanoOne from '@renderer/assets/images/renderNanoOneTransparent.png'
import RenderNanoZero from '@renderer/assets/images/renderNanoZeroTransparent.png' import RenderNanoZero from '@renderer/assets/images/renderNanoZeroTransparent.png'
import LogoMidi from '@renderer/assets/logos/logoMidi.svg' import LogoMidi from '@renderer/assets/logos/logoMidi.svg'
import DeviceBar from '@renderer/components/device/DeviceBar.vue' import DeviceBar from '@renderer/components/device/DeviceBar.vue'
import { useStore } from '@renderer/store' import { useAppStore } from '@renderer/appStore'
import { useDeviceStore } from '@renderer/deviceStore'
import ScrambleText from '@renderer/components/common/ScrambleText.vue' import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import DeviceLEDRing from '@renderer/components/device/DeviceLEDRing.vue' import DeviceLEDRing from '@renderer/components/device/DeviceLEDRing.vue'
import DeviceKeys from '@renderer/components/device/DeviceKeys.vue' import DeviceKeys from '@renderer/components/device/DeviceKeys.vue'
const store = useStore() const appStore = useAppStore()
const deviceStore = useDeviceStore()
const barValue = computed(() => 100 - (store.angle / Math.PI / 2) * 100) const barValue = computed(() => 100 - (deviceStore.angle / Math.PI / 2) * 100)
const previewDeviceImages = { const previewDeviceImages = {
nanoOne: RenderNanoOne, nanoOne: RenderNanoOne,
@@ -112,7 +116,7 @@ const previewDeviceImages = {
} }
const previewDeviceImage = computed( const previewDeviceImage = computed(
() => previewDeviceImages[store.previewDeviceModel || 'nanoOne'] () => previewDeviceImages[appStore.previewDeviceModel || 'nanoOne']
) )
const offlineText = ref('NO DEVICE CONNECTED') const offlineText = ref('NO DEVICE CONNECTED')

View File

@@ -41,31 +41,35 @@
<div class="flex gap-2"> <div class="flex gap-2">
<MenubarMenu> <MenubarMenu>
<MenubarTrigger class="app-titlebar-button"> <MenubarTrigger class="app-titlebar-button">
<template v-if="store.numAttachedDevices !== 1"> <template v-if="deviceStore.attachedDeviceIds.length !== 1">
Devices<span class="text-zinc-500">&nbsp;({{ '' + store.numAttachedDevices }})</span> Devices<span class="text-zinc-500"
>&nbsp;({{ '' + deviceStore.attachedDeviceIds.length }})</span
>
</template> </template>
<template v-else> Device </template> <template v-else> Device </template>
</MenubarTrigger> </MenubarTrigger>
<MenubarContent> <MenubarContent>
<!-- TODO: Switch keyboard shortcut icons based on platform --> <!-- TODO: Switch keyboard shortcut icons based on platform -->
<MenubarItem @click="store.setConnected(!store.connected)"> <MenubarItem @click="deviceStore.setConnected(!deviceStore.connected)">
{{ store.connected ? $t('navbar.device.disconnect') : $t('navbar.device.connect') }} {{
deviceStore.connected ? $t('navbar.device.disconnect') : $t('navbar.device.connect')
}}
<MenubarShortcut>D</MenubarShortcut> <MenubarShortcut>D</MenubarShortcut>
</MenubarItem> </MenubarItem>
<MenubarItem v-if="store.multipleDevicesConnected" <MenubarItem v-if="deviceStore.attachedDeviceIds.length > 1"
>Next Device >Next Device
<MenubarShortcut>N</MenubarShortcut> <MenubarShortcut>N</MenubarShortcut>
</MenubarItem> </MenubarItem>
<MenubarSeparator /> <MenubarSeparator />
<MenubarItem class="flex justify-between" @click="store.cycleScreenOrientation"> <MenubarItem class="flex justify-between" @click="deviceStore.cycleOrientation">
<p>Orientation:&nbsp;</p> <p>Orientation:&nbsp;</p>
<p>{{ store.screenOrientation }}°</p> <p>{{ deviceStore.orientation }}°</p>
<MenubarShortcut>R</MenubarShortcut> <MenubarShortcut>R</MenubarShortcut>
</MenubarItem> </MenubarItem>
<MenubarSeparator /> <MenubarSeparator />
<MenubarItem class="flex justify-between" @click="store.switchPreviewDeviceModel"> <MenubarItem class="flex justify-between" @click="appStore.switchPreviewDeviceModel">
<p>Skin:&nbsp;</p> <p>Skin:&nbsp;</p>
<p>{{ previewDeviceNames[store.previewDeviceModel || 'nanoOne'] }}</p> <p>{{ previewDeviceNames[appStore.previewDeviceModel || 'nanoOne'] }}</p>
<MenubarShortcut>S</MenubarShortcut> <MenubarShortcut>S</MenubarShortcut>
</MenubarItem> </MenubarItem>
<MenubarSeparator /> <MenubarSeparator />
@@ -133,7 +137,7 @@
</div> </div>
<div class="grow" /> <div class="grow" />
<Transition name="fade"> <Transition name="fade">
<div v-if="store.connected" class="flex items-center gap-2 px-2"> <div v-if="deviceStore.connected" class="flex items-center gap-2 px-2">
<div v-if="numberOfChanges" class="text-sm"> <div v-if="numberOfChanges" class="text-sm">
<PenLine class="inline-block h-4" />{{ numberOfChanges }} Changes <PenLine class="inline-block h-4" />{{ numberOfChanges }} Changes
</div> </div>
@@ -145,7 +149,7 @@
: 'border-2' : 'border-2'
" "
class="app-titlebar-button" class="app-titlebar-button"
@click="nanoSerialApi.save(store.connectedId)" @click="console.log('Save not implemented!')"
> >
Save Save
</MenubarButton> </MenubarButton>
@@ -154,9 +158,9 @@
<MenubarButton <MenubarButton
v-if="showDisconnectButton" v-if="showDisconnectButton"
class="app-titlebar-button border-2" class="app-titlebar-button border-2"
@click="store.setConnected(!store.connected)" @click="deviceStore.setConnected(!deviceStore.connected)"
> >
{{ store.connected ? 'Disconnect' : 'Connect' }} {{ deviceStore.connected ? 'Disconnect' : 'Connect' }}
</MenubarButton> </MenubarButton>
<div v-if="!isMacOS" class="flex h-full"> <div v-if="!isMacOS" class="flex h-full">
<button <button
@@ -198,10 +202,12 @@ import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { Minus, Square, Copy, X, PenLine } from 'lucide-vue-next' import { Minus, Square, Copy, X, PenLine } from 'lucide-vue-next'
import { onMounted, ref } from 'vue' import { onMounted, ref } from 'vue'
import { Separator } from '@renderer/components/ui/separator' import { Separator } from '@renderer/components/ui/separator'
import { useStore } from '@renderer/store'
import MenubarButton from '@renderer/components/navbar/MenubarButton.vue' import MenubarButton from '@renderer/components/navbar/MenubarButton.vue'
import { useAppStore } from '@renderer/appStore'
import { useDeviceStore } from '@renderer/deviceStore'
const store = useStore() const appStore = useAppStore()
const deviceStore = useDeviceStore()
const minimizable = ref(true) const minimizable = ref(true)
const maximizable = ref(true) const maximizable = ref(true)
@@ -209,7 +215,7 @@ const showDisconnectButton = ref(false)
const isMaximized = ref(false) const isMaximized = ref(false)
const { electronApi, nanoSerialApi } = window const { electronApi } = window
const isMacOS = electronApi.platform === 'darwin' const isMacOS = electronApi.platform === 'darwin'
const zoomFactor = ref(1) const zoomFactor = ref(1)

View File

@@ -13,7 +13,10 @@
:class="{ 'bg-zinc-300': selected }" :class="{ 'bg-zinc-300': selected }"
@submit.prevent=" @submit.prevent="
() => { () => {
store.renameProfile(profile.id, nameInput) // store.renameProfile(profile.id, nameInput)
console.log('Renaming profile to:', nameInput)
console.log('NOT IMPLEMENTED YET!')
// TODO: Implement deviceStore.renameProfile
editing = false editing = false
} }
" "
@@ -136,9 +139,6 @@
import { Check, Copy, PenLine, Trash2, X, GripHorizontal } from 'lucide-vue-next' import { Check, Copy, PenLine, Trash2, X, GripHorizontal } from 'lucide-vue-next'
import ScrambleText from '@renderer/components/common/ScrambleText.vue' import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { nextTick, ref } from 'vue' import { nextTick, ref } from 'vue'
import { useStore } from '@renderer/store'
const store = useStore()
defineEmits(['select', 'duplicate', 'delete']) defineEmits(['select', 'duplicate', 'delete'])

View File

@@ -6,11 +6,11 @@
> >
<button <button
class="font-heading flex h-full min-w-0 flex-1 items-center" class="font-heading flex h-full min-w-0 flex-1 items-center"
@click="showProfileConfig = store.selectedProfile && !showProfileConfig" @click="showProfileConfig = deviceStore.currentProfileName && !showProfileConfig"
> >
<component :is="showProfileConfig ? ArrowLeft : List" class="mr-1 h-full w-5 shrink-0" /> <component :is="showProfileConfig ? ArrowLeft : List" class="mr-1 h-full w-5 shrink-0" />
<ScrambleText <ScrambleText
:text="showProfileConfig ? store.selectedProfile?.name : $t('profiles.title')" :text="showProfileConfig ? deviceStore.currentProfileName : $t('profiles.title')"
class="min-w-0 overflow-hidden text-ellipsis" class="min-w-0 overflow-hidden text-ellipsis"
/> />
<ScrambleText <ScrambleText
@@ -19,7 +19,7 @@
scramble-on-mount scramble-on-mount
:fill-interval="20" :fill-interval="20"
:delay="500" :delay="500"
:text="`(${store.profiles.length}/${maxProfiles})`" :text="`(${deviceStore.profileNames.length}/${maxProfiles})`"
/> />
</button> </button>
<DropdownMenu> <DropdownMenu>
@@ -28,7 +28,7 @@
<button <button
v-if="!showProfileConfig" v-if="!showProfileConfig"
class="flex aspect-square h-8 items-center justify-center rounded-lg border border-zinc-100 bg-zinc-300 text-black hover:bg-zinc-200" class="flex aspect-square h-8 items-center justify-center rounded-lg border border-zinc-100 bg-zinc-300 text-black hover:bg-zinc-200"
@click="store.addProfile" @click="console.log('Add profile not implemented!')"
> >
<Plus class="h-4" /> <Plus class="h-4" />
</button> </button>
@@ -44,7 +44,7 @@
</div> </div>
<div class="relative grow overflow-y-auto"> <div class="relative grow overflow-y-auto">
<div v-if="renderProfileList" class="absolute w-full"> <div v-if="renderProfileList" class="absolute w-full">
<div v-if="store.profiles.length === 0"> <div v-if="deviceStore.profileNames.length === 0">
<div class="flex h-32 flex-col items-center justify-center"> <div class="flex h-32 flex-col items-center justify-center">
<ScrambleText <ScrambleText
scramble-on-mount scramble-on-mount
@@ -59,7 +59,7 @@
key="categoriesDraggable" key="categoriesDraggable"
group="profileCategories" group="profileCategories"
item-key="name" item-key="name"
:list="store.profileCategories" :list="deviceStore.profileCategories"
v-bind="dragOptions" v-bind="dragOptions"
handle=".category-handle" handle=".category-handle"
@start="drag = true" @start="drag = true"
@@ -105,15 +105,15 @@
<ProfileButton <ProfileButton
:profile="dragProfile.element" :profile="dragProfile.element"
:show-hover-buttons="!drag" :show-hover-buttons="!drag"
:selected="store.selectedProfile?.id === dragProfile.element.id" :selected="deviceStore.currentProfileName === dragProfile.element.name"
@select=" @select="
() => { () => {
store.selectProfile(dragProfile.element.id) console.log('Select profile not implemented!')
showProfileConfig = true showProfileConfig = true
} }
" "
@duplicate="store.duplicateProfile(dragProfile.element.id)" @duplicate="console.log('Duplicate profile not implemented!')"
@delete="store.removeProfile(dragProfile.element.id)" @delete="console.log('Delete profile not implemented!')"
/> />
</div> </div>
</template> </template>
@@ -149,7 +149,6 @@ import {
CollapsibleTrigger CollapsibleTrigger
} from '@renderer/components/ui/collapsible' } from '@renderer/components/ui/collapsible'
import ScrambleText from '@renderer/components/common/ScrambleText.vue' import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { useStore } from '@renderer/store'
import ProfileButton from '@renderer/components/profile/ProfileButton.vue' import ProfileButton from '@renderer/components/profile/ProfileButton.vue'
import ProfileConfig from '@renderer/components/profile/ProfileConfig.vue' import ProfileConfig from '@renderer/components/profile/ProfileConfig.vue'
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
@@ -159,6 +158,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from '@renderer/components/ui/dropdown-menu' } from '@renderer/components/ui/dropdown-menu'
import { useDeviceStore } from '@renderer/deviceStore'
defineProps({ defineProps({
showFilter: { showFilter: {
@@ -167,6 +167,8 @@ defineProps({
} }
}) })
const deviceStore = useDeviceStore()
const dragOptions = ref({ const dragOptions = ref({
ghostClass: 'ghost', ghostClass: 'ghost',
animation: 150, animation: 150,
@@ -175,7 +177,6 @@ const dragOptions = ref({
const maxProfiles = 32 const maxProfiles = 32
const store = useStore()
const collapse = ref({}) const collapse = ref({})
const showProfileConfig = ref(false) const showProfileConfig = ref(false)
@@ -228,7 +229,8 @@ const onCategoryDrop = (event) => {
const category = event.moved.element const category = event.moved.element
const oldIndex = event.moved.oldIndex const oldIndex = event.moved.oldIndex
const newIndex = event.moved.newIndex const newIndex = event.moved.newIndex
store.moveProfileCategory(category.name, oldIndex, newIndex) // store.moveProfileCategory(category.name, oldIndex, newIndex)
console.log('Move category not implemented!')
} }
} }
@@ -237,12 +239,14 @@ const onProfileDrop = (event, categoryIndex) => {
const profile = event.moved.element const profile = event.moved.element
const oldIndex = event.moved.oldIndex const oldIndex = event.moved.oldIndex
const newIndex = event.moved.newIndex const newIndex = event.moved.newIndex
store.moveProfile(profile.id, oldIndex, newIndex) // store.moveProfile(profile.id, oldIndex, newIndex)
console.log('Move profile not implemented!')
} }
if (event.added) { if (event.added) {
const profile = event.added.element const profile = event.added.element
const newIndex = event.added.newIndex const newIndex = event.added.newIndex
store.changeProfileCategory(profile.id, categoryIndex, newIndex) // store.changeProfileCategory(profile.id, categoryIndex, newIndex)
console.log('Change profile category not implemented!')
} }
} }
</script> </script>

View File

@@ -1,59 +0,0 @@
export const useMessageHandlers = function (store) {
return {
handle_message: (jsonstr) => {
const message = JSON.parse(jsonstr)
if (message.hasOwnProperty('event')) {
this.handle_event_message(message)
}
if (message.hasOwnProperty('p')) {
this.handle_profile_message(message)
}
if (message.hasOwnProperty('profiles')) {
this.handle_profiles_message(message)
}
if (message.hasOwnProperty('settings')) {
this.handle_settings_message(message)
}
if (message.hasOwnProperty('error')) {
this.handle_error_message(message)
}
if (message.hasOwnProperty('debug')) {
console.log('Device: DEBUG: ', message.debug)
}
if (message.hasOwnProperty('idle')) {
// TODO remove
console.log('Device present, idle since: ', message.idle)
}
// Moved these two up from handle_event_message
// Event messages don't include the event key atm, so handle_event_message was never called
if (message.hasOwnProperty('ks')) {
store.update_keystates(message.ks)
}
if (message.hasOwnProperty('a')) {
store.update_knob_position(message.t, message.a, message.v)
}
},
handle_event_message: (event) => {},
handle_profile_message: (profile) => {
// TODO update profile
},
handle_profiles_message: (profiles) => {
// TODO update profiles
},
handle_settings_message: (settings) => {
if (settings.hasOwnProperty('deviceName')) {
store.update_device_name(settings.deviceName)
}
// TODO update other settings - maybe this should be in a separate store? Or in the main store, but move these ifs to a action method in the store?
},
handle_error_message: (error) => {
console.error('Device: ERROR: ', error.error)
// TODO show/handle error in UI?
}
}
}

View File

@@ -0,0 +1,87 @@
import { defineStore } from 'pinia'
interface Profile {
version: number
name: string
desc: string
profileTag: string
profileType: number
profile_type: number
position_num: number
attract_distance: number
feedback_strength: number
bounce_strength: number
haptic_click_strength: number
output_ramp: number
ledEnable: boolean
ledBrightness: number
ledMode: number
pointer: number
primary: number
secondary: number
buttonAIdle: number
buttonBIdle: number
buttonCIdle: number
buttonDIdle: number
buttonAPress: number
buttonBPress: number
buttonCPress: number
buttonDPress: number
internalMacro: boolean
knobMap: string
switchA: string
switchB: string
switchC: string
switchD: string
guiEnable: boolean
}
const { nanoSerialApi } = window
export const useDeviceStore = defineStore('device', {
state: () => ({
attachedDeviceIds: [] as string[], // list of attached device ids
currentDeviceId: null as string | null, // id of the current device
profileNames: [] as string[], // list of profile names
profiles: {} as Record<string, Profile>, // map of profiles by name
currentProfileName: null as string | null, // name of the current profile
orientation: 0 as number, // orientation of the device
dirtyState: false as boolean, // whether the device state has changed
angle: 0 as number, // angle of the knob
turns: 0 as number, // number of turns of the knob
velocity: 0 as number, // velocity of the knob
keyStates: {} as Record<string, boolean> // state of the keys (true if pressed)
}),
getters: {
connected: (state) => state.currentDeviceId !== null,
currentProfile: (state) =>
state.currentProfileName ? state.profiles[state.currentProfileName] : null
},
actions: {
setAttachedDeviceIds(deviceIds: string[]) {
this.attachedDeviceIds = deviceIds
},
setConnected(connected: boolean) {
// TODO: This is here for compatibility, but it should be removed
// Real connect calls would need to know the last device id
// Maybe that should be stored here
// Then connect connects to the last device id of falls back to the first
},
setCurrentProfile(profileName: string, updateDevice: boolean = true) {
this.currentProfileName = profileName
if (updateDevice) {
nanoSerialApi.send(this.currentDeviceId!, JSON.stringify({ current: profileName }))
}
},
setOrientation(orientation: number, updateDevice: boolean = true) {
this.orientation = orientation
if (updateDevice) {
// TODO: send orientation to device
console.log('No orientation API message yet! Orientation:', orientation)
}
},
cycleOrientation() {
this.setOrientation((this.orientation + 90) % 360)
}
}
})

View File

@@ -4,7 +4,7 @@ import { createApp } from 'vue'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import App from './App.vue' import App from './App.vue'
import { pinia } from '@renderer/store' import { createPinia } from 'pinia'
import en from '@renderer/lang/en.json' import en from '@renderer/lang/en.json'
@@ -15,14 +15,16 @@ const i18n = createI18n({
messages: { en: en } messages: { en: en }
}) })
const pinia = createPinia()
const app = createApp(App) const app = createApp(App)
app.use(pinia) app.use(pinia)
app.use(i18n) app.use(i18n)
// TODO remove this // TODO remove this
window.nanoSerialApi.on_event('*', (eventid, deviceid, ...data) => { // window.nanoSerialApi.on_event('*', (eventid, deviceid, ...data) => {
console.log('Event on window ', eventid, deviceid, data) // console.log('Event on window ', eventid, deviceid, data)
}) // })
app.mount('#app') app.mount('#app')

View File

@@ -1,302 +0,0 @@
import { createPinia, defineStore } from 'pinia'
// import schema from '@renderer/data/profileSchema.json' // see below
// import Ajv from 'ajv' // see below
import WIP from '@renderer/components/WIP.vue'
import KnobFeedbackConfig from '@renderer/components/config/knob/KnobFeedbackConfig.vue'
import KnobLightConfig from '@renderer/components/config/knob/KnobLightConfig.vue'
import KeyLightConfig from '@renderer/components/config/keys/KeyLightConfig.vue'
import KnobMappingConfig from '@renderer/components/config/knob/KnobMappingConfig.vue'
import KeyMappingConfig from '@renderer/components/config/keys/KeyMappingConfig.vue'
import { shallowRef } from 'vue'
import mockData from '@renderer/data/nanoConfig.json'
// const ajv = new Ajv() // see below
const { nanoSerialApi } = window
// TODO: Define Profile type
// TODO: Define Device type
// TODO: Define Key type
// TODO: Define ProfileCategory type
export const useStore = defineStore('main', {
state: () => {
return {
devices: {},
connected: false, // TODO make into getter
connectedId: null,
profileCategories: [],
selectedProfileId: null,
connectedDevices: ['test1', 'test2'],
selectedFeature: 'knob',
selectedKey: 'a',
currentConfigPage: 'mapping',
configPages: {
knob: {
mapping: {
titleKey: 'config_options.mapping_configuration.title',
component: shallowRef(KnobMappingConfig)
},
feedback: {
titleKey: 'config_options.feedback_designer.title',
component: shallowRef(KnobFeedbackConfig)
},
lighting: {
titleKey: 'config_options.light_designer.title',
component: shallowRef(KnobLightConfig)
}
},
key: {
mapping: {
titleKey: 'config_options.mapping_configuration.title',
component: shallowRef(KeyMappingConfig)
},
lighting: {
titleKey: 'config_options.light_designer.title',
component: shallowRef(KeyLightConfig)
}
}
},
previewDeviceModel: localStorage.getItem('previewDeviceModel') || 'nanoOne',
screenOrientation: 90,
// device state as received from the device
keyState: 'abcd',
turns: 0,
angle: 0,
velocity: 0,
last_event: 0
}
},
getters: {
profiles: (state) => state.profileCategories.flatMap((c) => c.profiles),
profileIds: (state) => state.profiles.map((p) => p.id),
selectedProfileCategory: (state) =>
state.profileCategories.find((c) => c.profiles.find((p) => p.id === state.selectedProfileId)),
selectedProfile: (state) => state.profiles.find((p) => p.id === state.selectedProfileId),
currentConfigComponent: (state) =>
state.configPages[state.selectedFeature][state.currentConfigPage]?.component || WIP,
currentConfigPages: (state) => state.configPages[state.selectedFeature] || {},
multipleDevicesConnected: (state) => state.connectedDevices.length > 1,
numAttachedDevices: (state) => Object.keys(state.devices).length
// connected: (state) => state.connectedId !== null,
},
actions: {
selectProfile(id) {
if (!this.profileIds.includes(id)) return false
this.selectedProfileId = id
return true
},
addProfile(profile, categoryIndex, newIndex) {
const category = this.profileCategories[categoryIndex]
category.profiles.splice(newIndex, 0, profile)
},
removeProfile(profileId) {
const category = this.profileCategories.find((c) =>
c.profiles.find((p) => p.id === profileId)
)
const index = category.profiles.findIndex((p) => p.id === profileId)
category.profiles.splice(index, 1)
},
duplicateProfile(profileId) {
const profile = this.profiles.find((p) => p.id === profileId)
const newProfile = JSON.parse(JSON.stringify(profile))
newProfile.id = this.newProfileId(profile.id)
newProfile.name = this.newProfileName(profile.name)
const category = this.profileCategories.find((c) =>
c.profiles.find((p) => p.id === profileId)
)
const index = category.profiles.findIndex((p) => p.id === profileId)
category.profiles.splice(index + 1, 0, newProfile)
this.selectProfile(newProfile.id)
},
moveProfile(profileId, oldIndex, newIndex) {
// Find the profile category, then swap the profiles at the old and new indices
const category = this.profileCategories.find((c) =>
c.profiles.find((p) => p.id === profileId)
)
const tmpProfile = category.profiles[newIndex]
category.profiles[newIndex] = category.profiles[oldIndex]
category.profiles[newIndex] = tmpProfile
},
moveProfileCategory(categoryName, oldIndex, newIndex) {
const tmpCategory = this.profileCategories[newIndex]
this.profileCategories[newIndex] = this.profileCategories[oldIndex]
this.profileCategories[newIndex] = tmpCategory
},
changeProfileCategory(profileId, newCategoryIndex, newIndex) {
const profile = this.profiles.find((p) => p.id === profileId)
const oldCategory = this.profileCategories.find((c) =>
c.profiles.find((p) => p.id === profileId)
)
const newCategory = this.profileCategories[newCategoryIndex]
oldCategory.profiles = oldCategory.profiles.filter((p) => p.id !== profileId)
newCategory.profiles.splice(newIndex, 0, profile)
},
renameProfile(profileId, newName) {
const profile = this.profiles.find((p) => p.id === profileId)
profile.name = newName
},
fetchProfiles() {
const categories = mockData.categories
console.log(categories)
const ids = new Set()
// const validate = ajv.compile(schema) // see below
this.$patch({
profileCategories: categories.map((category) => ({
name: category.name,
profiles: category.profiles.filter((profile) => {
// Ajv validation requires unsafe-eval CSP, let's not do that
// TODO: Remove ajv validation completely or compile schema at build time
// if (!validate(profile)) {
// console.error('Failed to validate profile: ' + profile.name, validate.errors)
// return false
// }
if (ids.has(profile.id)) {
console.error('Duplicate profile id: ' + profile.id + ' for profile: ' + profile.name)
return false
}
ids.add(profile.id)
return true
})
})),
selectedProfileId: categories[0]?.profiles[0]?.id || null
})
},
newProfileName(originalName = '') {
let name = originalName
let i = 1
while (this.profiles.find((p) => p.name === name)) {
name = `${originalName} (${i++})`
}
return name
},
newProfileId(originalId = '') {
let id = originalId
if (originalId) {
do {
id = Math.floor((parseInt(id) + 1) % 9999)
.toString()
.padStart(4, '0')
} while (this.profileIds.includes(id))
} else {
do {
id = Math.floor(Math.random() * 9999)
.toString()
.padStart(4, '0')
} while (this.profileIds.includes(id))
}
return id
},
selectConfigFeature(feature) {
this.selectedFeature = feature
if (!this.currentConfigPages[this.currentConfigPage]) this.setCurrentConfigPage('mapping')
},
selectKey(key) {
this.selectedKey = key
this.selectConfigFeature('key')
},
setCurrentConfigPage(page) {
this.currentConfigPage = page
},
setConnected(connected) {
this.connected = connected
},
switchPreviewDeviceModel() {
this.previewDeviceModel = this.previewDeviceModel === 'nanoOne' ? 'nanoZero' : 'nanoOne'
localStorage.setItem('previewDeviceModel', this.previewDeviceModel)
},
cycleScreenOrientation() {
this.screenOrientation = (this.screenOrientation + 90) % 360
},
setKeyDefaultColor(color) {
// this.selectedProfile.keys[this.selectedKey].default = color
const props = {}
props[`button${this.selectedKey.toUpperCase()}Idle`] = color.rgbNumber()
nanoSerialApi.send(this.connectedId, { p: { name: 'Default Profile', ...props } })
},
setKeyPressedColor(color) {
// this.selectedProfile.keys[this.selectedKey].pressed = color
const props = {}
props[`button${this.selectedKey.toUpperCase()}Press`] = color.rgbNumber()
nanoSerialApi.send(this.connectedId, { p: { name: 'Default Profile', ...props } })
},
// devices, device attachment, connection, and disconnection
init_devices(ids) {
console.log('Initializing devices: ', ids)
for (const id of ids) this.update_devices(id, true)
if (Object.keys(this.devices).length == 1) {
// TODO auto-connect to the device
const deviceid = Object.keys(this.devices)[0]
console.log('Auto-connecting to device ', deviceid)
window.nanoSerialApi.connect(deviceid)
}
},
update_devices(deviceid, attached) {
if (attached) {
if (!this.devices.hasOwnProperty(deviceid))
this.devices[deviceid] = { serialNumber: deviceid, connected: false }
} else {
if (this.devices.hasOwnProperty(deviceid)) delete this.devices[deviceid] // TODO maybe mark as detached instead of deleting? then we can remember its name, etc...
}
},
device_attached(deviceid) {
this.update_devices(deviceid, true)
if (Object.keys(this.devices).length == 1) {
// TODO auto-connect to the device
console.log('Auto-connecting to device ', deviceid)
window.nanoSerialApi.connect(deviceid)
}
},
device_detached(deviceid) {
if (this.devices[deviceid].connected) {
// detached event arrived before disconnected event?
this.devices[deviceid].connected = false
this.connected = false
}
this.update_devices(deviceid, false)
},
device_connected(deviceid) {
this.devices[deviceid].connected = true
this.connected = true
this.connectedId = deviceid
// TODO load profiles from device
// nanoSerialApi.send(deviceid, { profiles: "#all" }) // request profiles
// "Default Profile", for now, is the only profile after the device
// starts up, so it is also the current (eg. 'selected') profile
// nanoSerialApi.send(deviceid, { p: "Default Profile" }) // request Default Profile
// TODO maybe you want to request all the profiles right now?
// or only on demand?
},
device_disconnected(deviceid) {
this.devices[deviceid].connected = false
this.connected = false
this.connectedId = null
// TODO switch UI to disconnected state
},
// device events
update_knob_position(turns, angle, velocity) {
this.turns = turns
this.angle = angle
this.velocity = velocity
this.last_event = Date.now()
},
update_keystate(keystate) {
this.keyState = keystate
this.last_event = Date.now()
},
// settings changes
update_device_name(name) {
this.devices[this.connectedId].name = name
}
}
})
export const pinia = createPinia()