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
contextBridge.exposeInMainWorld('nanoSerialApi', {
list_devices() {
listConnectedDevices() {
return ipcRenderer.invoke('nanoSerialApi:list_devices')
},
connect(deviceid) {
@@ -11,20 +11,13 @@ contextBridge.exposeInMainWorld('nanoSerialApi', {
disconnect(deviceid) {
return ipcRenderer.invoke('nanoSerialApi:disconnect', deviceid)
},
on_event(eventid_filter, callback) {
//console.log('attaching filter for ', eventid_filter)
on(callback) {
ipcRenderer.on('nanoSerialApi:event', (_event, eventid, deviceid, ...data) => {
//console.log('Event in ipcRenderer ', eventid, deviceid, data)
if (eventid_filter == '*' || eventid_filter == eventid) {
callback(eventid, deviceid, ...data)
}
callback(eventid, deviceid, ...data)
})
},
send(deviceid, 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 ConfigPane from '@renderer/components/config/ConfigPane.vue'
import Navbar from '@renderer/components/navbar/Navbar.vue'
import { useStore } from '@renderer/store'
import { useMessageHandlers } from '@renderer/device'
import { useDeviceStore } from '@renderer/deviceStore'
const { electronApi, nanoSerialApi } = window
const store = useStore()
const deviceStore = useDeviceStore()
const menuActions = {
connect: () => store.setConnected(!store.connected),
orientation: () => store.cycleScreenOrientation(),
skin: () => store.switchPreviewDeviceModel()
}
// const menuActions = {
// connect: () => store.setConnected(!store.connected),
// orientation: () => store.cycleScreenOrientation(),
// skin: () => store.switchPreviewDeviceModel()
// }
electronApi.onMenu((key) => {
console.log('menu', key)
if (menuActions[key]) {
menuActions[key]()
}
})
// electronApi.onMenu((key) => {
// console.log('menu', key)
// if (menuActions[key]) {
// menuActions[key]()
// }
// })
store.fetchProfiles() // TODO remove me!
// store.fetchProfiles() // TODO remove me!
// handle device events
const handlers = useMessageHandlers(store)
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-error', (evt, deviceid, data) => {
/* TODO handle connection errors */
})
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('update', (evt, deviceid, data) => {
handlers.handle_message(data)
})
// get list of the currently attached devices
nanoSerialApi.list_devices().then((devs) => store.init_devices(devs))
// const handlers = useMessageHandlers(store)
// 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-error', (evt, deviceid, data) => {
// /* TODO handle connection errors */
// })
// 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('update', (evt, deviceid, data) => {
// handlers.handle_message(data)
// })
// // get list of the currently attached devices
// nanoSerialApi.list_devices().then((devs) => store.init_devices(devs))
</script>
<template>
<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">
<Transition name="slide-left">
<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"
/>
</Transition>
@@ -55,7 +53,7 @@ nanoSerialApi.list_devices().then((devs) => store.init_devices(devs))
<div class="flex flex-1 basis-2/5 overflow-hidden">
<Transition name="slide-right">
<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"
/>
</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>
<div>
<template v-if="store.selectedProfile">
<template v-if="deviceStore.currentProfile">
<TabSelect
v-if="showTabs"
v-model="configPage"
@@ -12,7 +12,7 @@
</template>
</TabSelect>
<div class="grow overflow-y-auto">
<component :is="store.currentConfigComponent" />
<component :is="appStore.currentConfigComponent" />
</div>
</template>
<template v-else>
@@ -29,18 +29,20 @@
</div>
</template>
<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 { computed } from 'vue'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
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({
get: () => store.currentConfigPage,
set: (value) => store.setCurrentConfigPage(value)
get: () => appStore.currentConfigPage,
set: (value) => appStore.setCurrentConfigPage(value)
})
defineProps({

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
}"
>
<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>
<ScrambleText
:delay="100"
@@ -32,7 +32,7 @@
</Transition>
<Transition name="fade-delayed">
<DeviceLEDRing
v-if="store.connected"
v-if="deviceStore.connected"
:value="barValue"
class="absolute inset-x-0 top-[12.5%] mx-auto h-[66%]"
/>
@@ -44,11 +44,13 @@
>
<TransitionGroup name="fade-display">
<div
v-if="store.connected"
v-if="deviceStore.connected"
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" />
<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>
<DeviceBar :value="barValue" :count="30" :width="120" />
<span class="font-pixelsm w-36 text-[7pt] uppercase text-muted-foreground">
@@ -71,21 +73,21 @@
</div>
<Transition name="fade-delayed">
<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="{
'outline outline-white': store.selectedFeature === 'knob',
'outline-zinc-400 hover:outline': store.selectedFeature !== 'knob'
'outline outline-white': appStore.selectedFeature === 'knob',
'outline-zinc-400 hover:outline': appStore.selectedFeature !== 'knob'
}"
@click="store.selectConfigFeature('knob')"
@click="appStore.selectConfigFeature('knob')"
/>
</Transition>
<Transition name="fade-delayed">
<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%]"
:selected="store.selectedFeature === 'key' ? store.selectedKey : ''"
@select="store.selectKey"
:selected="appStore.selectedFeature === 'key' ? appStore.selectedKey : ''"
@select="appStore.selectKey"
/>
</Transition>
</div>
@@ -96,15 +98,17 @@ import RenderNanoOne from '@renderer/assets/images/renderNanoOneTransparent.png'
import RenderNanoZero from '@renderer/assets/images/renderNanoZeroTransparent.png'
import LogoMidi from '@renderer/assets/logos/logoMidi.svg'
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 { computed, ref } from 'vue'
import DeviceLEDRing from '@renderer/components/device/DeviceLEDRing.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 = {
nanoOne: RenderNanoOne,
@@ -112,7 +116,7 @@ const previewDeviceImages = {
}
const previewDeviceImage = computed(
() => previewDeviceImages[store.previewDeviceModel || 'nanoOne']
() => previewDeviceImages[appStore.previewDeviceModel || 'nanoOne']
)
const offlineText = ref('NO DEVICE CONNECTED')

View File

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

View File

@@ -13,7 +13,10 @@
:class="{ 'bg-zinc-300': selected }"
@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
}
"
@@ -136,9 +139,6 @@
import { Check, Copy, PenLine, Trash2, X, GripHorizontal } from 'lucide-vue-next'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { nextTick, ref } from 'vue'
import { useStore } from '@renderer/store'
const store = useStore()
defineEmits(['select', 'duplicate', 'delete'])

View File

@@ -6,11 +6,11 @@
>
<button
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" />
<ScrambleText
:text="showProfileConfig ? store.selectedProfile?.name : $t('profiles.title')"
:text="showProfileConfig ? deviceStore.currentProfileName : $t('profiles.title')"
class="min-w-0 overflow-hidden text-ellipsis"
/>
<ScrambleText
@@ -19,7 +19,7 @@
scramble-on-mount
:fill-interval="20"
:delay="500"
:text="`(${store.profiles.length}/${maxProfiles})`"
:text="`(${deviceStore.profileNames.length}/${maxProfiles})`"
/>
</button>
<DropdownMenu>
@@ -28,7 +28,7 @@
<button
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"
@click="store.addProfile"
@click="console.log('Add profile not implemented!')"
>
<Plus class="h-4" />
</button>
@@ -44,7 +44,7 @@
</div>
<div class="relative grow overflow-y-auto">
<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">
<ScrambleText
scramble-on-mount
@@ -59,7 +59,7 @@
key="categoriesDraggable"
group="profileCategories"
item-key="name"
:list="store.profileCategories"
:list="deviceStore.profileCategories"
v-bind="dragOptions"
handle=".category-handle"
@start="drag = true"
@@ -105,15 +105,15 @@
<ProfileButton
:profile="dragProfile.element"
:show-hover-buttons="!drag"
:selected="store.selectedProfile?.id === dragProfile.element.id"
:selected="deviceStore.currentProfileName === dragProfile.element.name"
@select="
() => {
store.selectProfile(dragProfile.element.id)
console.log('Select profile not implemented!')
showProfileConfig = true
}
"
@duplicate="store.duplicateProfile(dragProfile.element.id)"
@delete="store.removeProfile(dragProfile.element.id)"
@duplicate="console.log('Duplicate profile not implemented!')"
@delete="console.log('Delete profile not implemented!')"
/>
</div>
</template>
@@ -149,7 +149,6 @@ import {
CollapsibleTrigger
} from '@renderer/components/ui/collapsible'
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { useStore } from '@renderer/store'
import ProfileButton from '@renderer/components/profile/ProfileButton.vue'
import ProfileConfig from '@renderer/components/profile/ProfileConfig.vue'
import draggable from 'vuedraggable'
@@ -159,6 +158,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger
} from '@renderer/components/ui/dropdown-menu'
import { useDeviceStore } from '@renderer/deviceStore'
defineProps({
showFilter: {
@@ -167,6 +167,8 @@ defineProps({
}
})
const deviceStore = useDeviceStore()
const dragOptions = ref({
ghostClass: 'ghost',
animation: 150,
@@ -175,7 +177,6 @@ const dragOptions = ref({
const maxProfiles = 32
const store = useStore()
const collapse = ref({})
const showProfileConfig = ref(false)
@@ -228,7 +229,8 @@ const onCategoryDrop = (event) => {
const category = event.moved.element
const oldIndex = event.moved.oldIndex
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 oldIndex = event.moved.oldIndex
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) {
const profile = event.added.element
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>

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