ADD: Profile management functionality & store

This commit is contained in:
Robert Kossessa
2024-01-27 02:42:37 +01:00
parent 11c66766bf
commit b23df03df6
8 changed files with 273 additions and 85 deletions

12
package-lock.json generated
View File

@@ -27,7 +27,6 @@
"vee-validate": "^4.12.4", "vee-validate": "^4.12.4",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-i18n": "^9.9.0", "vue-i18n": "^9.9.0",
"vuex": "^4.1.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
@@ -13201,17 +13200,6 @@
"vue": "^3.0.0" "vue": "^3.0.0"
} }
}, },
"node_modules/vuex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz",
"integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==",
"dependencies": {
"@vue/devtools-api": "^6.0.0-beta.11"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "1.7.5", "version": "1.7.5",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz",

View File

@@ -10,7 +10,7 @@
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps", "postuninstall": "electron-builder install-app-deps",
"preview": "vite preview", "preview": "vite preview",
"serve": "concurrently \"json-server --watch src/data/nanoConfig.json --port=3001\" \"vite\" " "serve": "concurrently \"json-server src/data/nanoConfig.json --port=3001\" \"vite\" "
}, },
"main": "background.js", "main": "background.js",
"dependencies": { "dependencies": {
@@ -32,7 +32,6 @@
"vee-validate": "^4.12.4", "vee-validate": "^4.12.4",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-i18n": "^9.9.0", "vue-i18n": "^9.9.0",
"vuex": "^4.1.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -0,0 +1,114 @@
<template>
<div
class="h-12 flex profile-row"
@mouseenter="hover=true" @mouseleave="hover=false">
<button
:class="{'font-semibold bg-zinc-200 hover:bg-zinc-100 text-black' : selected,
'hover:bg-zinc-900 bg-opacity-50 text-white': !selected,
'text-ellipsis': !editing}"
class="flex-1 h-full text-left whitespace-nowrap overflow-hidden"
@click="!editing && $emit('select') && $refs.profileTitle.scramble()">
<FileDigit
:class="{'text-zinc-600': selected,
'text-muted-foreground': !selected,
'w-0': hover}"
class="h-4 ml-10 mb-1 inline-block" />
<input
v-if="editing" ref="profileNameInput" v-model="profile.name"
onfocus="this.select()" :placeholder="$t('profiles.name_placeholder')"
class="pl-10 w-full h-full bg-transparent focus-visible:ring-0 focus-visible:outline-none"
style="color: inherit; background: inherit">
<template v-else>
<ScrambleText
ref="profileTitle"
:class="{'text-black': selected, 'text-zinc-100': !selected}"
:text="profile.name" />
<span
class="text-xs text-zinc-600"
:class="{'hidden': hover}"> uID:{{ profile.id }}</span>
</template>
</button>
<template v-if="!confirmDelete">
<button
:class="{'bg-zinc-200 hover:bg-zinc-100 text-black' : selected,
'hover:bg-opacity-100 bg-zinc-900 text-zinc-100 bg-opacity-50': !selected,
'w-12': editing,
'w-0': !editing}"
class="flex h-12 justify-center items-center flex-shrink-0"
@click="editing=false">
<Check class="h-4 w-4" />
</button>
<button
:class="{'bg-zinc-200 hover:bg-zinc-100 text-black' : selected,
'hover:bg-opacity-100 bg-zinc-900 text-zinc-100 bg-opacity-50': !selected,
'w-12' : hover && !editing}"
class="flex w-0 h-12 justify-center items-center flex-shrink-0"
@click="editing=true; $nextTick(()=>{$refs.profileNameInput.focus()})">
<PenLine class="h-4 w-4" />
</button>
<button
:class="{'bg-zinc-200 hover:bg-zinc-100 text-black' : selected,
'hover:bg-opacity-100 bg-zinc-900 text-zinc-100 bg-opacity-50': !selected,
'w-12' : hover && !editing}"
class="flex w-0 h-12 justify-center items-center flex-shrink-0">
<Copy class="h-4 w-4" />
</button>
<button
:class="{'bg-orange-600 hover:bg-orange-500 text-black' : selected,
'hover:bg-opacity-100 bg-orange-900 text-zinc-100 bg-opacity-50': !selected,
'w-12' : hover && !editing}"
class="flex w-0 h-12 justify-center items-center flex-shrink-0"
@click="confirmDelete=true">
<Trash2 class="h-4 w-4" />
</button>
</template>
<template v-else>
<button
:class="{'bg-orange-600 hover:bg-orange-500 text-black' : selected,
'hover:bg-opacity-100 bg-orange-900 text-zinc-100 bg-opacity-50': !selected,
'w-12' : hover && !editing}"
class="flex w-0 h-12 justify-center items-center flex-shrink-0"
@click="$emit('delete', profile.id)">
<Check class="h-4 w-4" />
</button>
<button
:class="{'bg-zinc-200 hover:bg-zinc-100 text-black' : selected,
'hover:bg-opacity-100 bg-zinc-900 text-zinc-100 bg-opacity-50': !selected,
'w-12' : hover && !editing}"
class="flex w-0 h-12 justify-center items-center flex-shrink-0"
@click="confirmDelete=false">
<X class="h-4 w-4" />
</button>
</template>
</div>
</template>
<script setup>
import { Check, Copy, FileDigit, PenLine, Trash2, X } from 'lucide-vue-next'
import ScrambleText from '@/components/effects/ScrambleText.vue'
import { ref } from 'vue'
defineEmits(['select', 'duplicate', 'delete'])
const profile = defineModel({
type: Object,
required: true,
default: () => ({
id: '1234',
name: 'Profile Name',
}),
})
defineProps({
selected: {
type: Boolean,
default: false,
},
})
const editing = ref(false)
const confirmDelete = ref(false)
const hover = ref(false)
</script>

View File

@@ -23,7 +23,8 @@
v-model="filter" v-model="filter"
:placeholder="$t('profiles.filter_placeholder')" :placeholder="$t('profiles.filter_placeholder')"
class="grow h-full bg-transparent text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50"> class="grow h-full bg-transparent text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50">
<button class="h-full flex text-zinc-200 bg-zinc-900 justify-center items-center aspect-square border-solid border-0 border-l hover:bg-zinc-800"> <button
class="h-full flex text-zinc-200 bg-zinc-900 justify-center items-center aspect-square border-solid border-0 border-l hover:bg-zinc-800">
<Plus /> <Plus />
</button> </button>
</div> </div>
@@ -37,7 +38,8 @@
</div> </div>
<div> <div>
<Collapsible <Collapsible
v-for="[profileTag, tagProfiles] in filteredProfilesByTag" :key="profileTag" v-model:open="collapse[profileTag]" v-for="[profileTag, tagProfiles] in filteredProfilesByTag" :key="profileTag"
v-model:open="collapse[profileTag]"
:default-open="true"> :default-open="true">
<CollapsibleTrigger <CollapsibleTrigger
class="w-full h-12 py-2 text-left text-muted-foreground text-sm hover:bg-zinc-900"> class="w-full h-12 py-2 text-left text-muted-foreground text-sm hover:bg-zinc-900">
@@ -45,53 +47,33 @@
{{ profileTag }}<span class="font-heading text-sm text-zinc-600"> ({{ tagProfiles.length }})</span> {{ profileTag }}<span class="font-heading text-sm text-zinc-600"> ({{ tagProfiles.length }})</span>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div <ProfileButton
v-for="profile in tagProfiles" :key="profile.id" v-for="(profile, index) in tagProfiles" :key="profile.id" v-model="tagProfiles[index]"
class="h-12 flex profile-row"> :selected="currentProfile===profile.id"
<button @select="currentProfile=profile.id" />
:data-selected="currentProfile===profile.id"
class="flex-1 h-full text-left data-[selected=true]:font-semibold hover:bg-zinc-900 data-[selected=true]:bg-zinc-200 hover:data-[selected=true]:bg-zinc-100 data-[selected=true]:text-black flex-nowrap text-ellipsis overflow-hidden whitespace-nowrap"
@click="currentProfile=profile.id; profileTitles[profile.id].scramble()">
<FileDigit
:data-selected="currentProfile===profile.id"
class="h-4 w-4 mb-1 ml-10 mr-2 inline-block text-muted-foreground data-[selected=true]:text-zinc-600" />
<ScrambleText :ref="el => { profileTitles[profile.id] = el }" :text="profile.name" />
<span class="text-xs text-zinc-600"> uID:{{ profile.id }}</span>
</button>
<button
:data-selected="currentProfile===profile.id"
class="flex w-0 h-12 transition-all text-zinc-100 justify-center items-center profile-button hover:bg-opacity-100 bg-opacity-50 bg-zinc-900 data-[selected=true]:bg-zinc-200 hover:data-[selected=true]:bg-zinc-100 data-[selected=true]:text-black">
<Copy class="h-4 w-4" />
</button>
<button
:data-selected="currentProfile===profile.id"
class="flex w-0 h-12 transition-all bg-orange-900 text-zinc-100 justify-center items-center profile-button hover:bg-orange-700 data-[selected=true]:bg-orange-600 hover:data-[selected=true]:bg-orange-500 data-[selected=true]:text-black">
<Trash2 class="h-4 w-4" />
</button>
</div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
</div> </div>
</div> </div>
<SchemaTest />
</div> </div>
</template> </template>
<script setup> <script setup>
import SchemaTest from '@/components/SchemaTest.vue'
import { Separator } from '@/components/ui/separator/index.js' import { Separator } from '@/components/ui/separator/index.js'
import { FileDigit, ChevronRight, Search, Trash2, Copy, Plus } from 'lucide-vue-next' import { ChevronRight, Plus, Search } from 'lucide-vue-next'
import axios from 'axios' import { computed, ref } from 'vue'
import { computed, onMounted, ref } from 'vue' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import ScrambleText from '@/components/effects/ScrambleText.vue' import ScrambleText from '@/components/effects/ScrambleText.vue'
import { store } from '@/store.js'
import ProfileButton from '@/components/ProfileButton.vue'
const maxProfiles = 32 const maxProfiles = 32
const profiles = ref([]) const editingId = ref(null)
const profiles = computed({
get: () => store.device.profiles,
set: val => store.device.profiles = val,
})
const filter = ref('') const filter = ref('')
const collapse = ref({}) const collapse = ref({})
@@ -122,25 +104,10 @@ const filteredProfilesByTag = computed(() => {
}) })
return map return map
}) })
function fetchProfiles() {
axios.get('http://localhost:3001/profiles').then(res => {
profiles.value = res.data
}).catch(err => {
console.error(err)
})
}
onMounted(() => {
fetchProfiles()
})
</script> </script>
<style scoped> <style scoped>
[data-state=open] > .chevrot { [data-state=open] > .chevrot {
transform: rotate(90deg); transform: rotate(90deg);
} }
.profile-row:hover .profile-button {
width: 3rem /* 48px */;
}
</style> </style>

View File

@@ -51,6 +51,108 @@
"guiEnable": true "guiEnable": true
} }
}, },
{
"id": "1232",
"name": "Another",
"profileTag": "Binaris",
"profileConfig": {
"profileDesc": "KORG MINILOGUE OSCILLATOR 1",
"profileType": 1,
"showDesc": true
},
"feedbackConfig": {
"feedbackEn": true,
"feedbackType": "fd",
"multiRev": false,
"feedbackStrength": 1,
"endstopStrength": 1,
"outputRamp": 10000,
"pos": 140,
"secondaryHaptic": true,
"secondaryVol": 5
},
"mappingConfig": {
"internalMacro": false,
"knobMap": "arrL",
"switchA": "shift",
"switchB": "ctrl",
"switchC": "alt",
"switchD": "esc"
},
"ledConfig": {
"ledEnable": true,
"ledMode": 1,
"primary": {
"h": 255,
"s": 255,
"l": 100
},
"secondary": {
"h": 255,
"s": 255,
"l": 100
},
"pointer": {
"h": 255,
"s": 255,
"l": 10
}
},
"guiConfig": {
"guiEnable": true
}
},
{
"id": "1232",
"name": "DUPLICATE ID",
"profileTag": "Binaris",
"profileConfig": {
"profileDesc": "KORG MINILOGUE OSCILLATOR 1",
"profileType": 1,
"showDesc": true
},
"feedbackConfig": {
"feedbackEn": true,
"feedbackType": "fd",
"multiRev": false,
"feedbackStrength": 1,
"endstopStrength": 1,
"outputRamp": 10000,
"pos": 140,
"secondaryHaptic": true,
"secondaryVol": 5
},
"mappingConfig": {
"internalMacro": false,
"knobMap": "arrL",
"switchA": "shift",
"switchB": "ctrl",
"switchC": "alt",
"switchD": "esc"
},
"ledConfig": {
"ledEnable": true,
"ledMode": 1,
"primary": {
"h": 255,
"s": 255,
"l": 100
},
"secondary": {
"h": 255,
"s": 255,
"l": 100
},
"pointer": {
"h": 255,
"s": 255,
"l": 10
}
},
"guiConfig": {
"guiEnable": true
}
},
{ {
"id": "5238", "id": "5238",
"name": "sdfsdf", "name": "sdfsdf",

View File

@@ -100,6 +100,7 @@
"filter_placeholder": "Filter Profiles...", "filter_placeholder": "Filter Profiles...",
"not_found": "No profiles found ;-;", "not_found": "No profiles found ;-;",
"subtitle": "Customize to your heart's content", "subtitle": "Customize to your heart's content",
"title": "Device Profiles" "title": "Device Profiles",
"name_placeholder": "Profile name"
} }
} }

View File

@@ -1,26 +1,39 @@
import './assets/main.css' import './assets/main.css'
import Axios from 'axios'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createStore } from 'vuex'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import en from './lang/en.json' import en from '@/lang/en.json'
import App from './App.vue' import App from '@/App.vue'
import { store } from '@/store.js'
import Ajv from 'ajv' import Ajv from 'ajv'
import schema from '@/data/profileSchema.json'
// Create a new store instance const ajv = new Ajv()
const store = createStore({
state() { Axios.get('http://localhost:3001/profiles').then((res) => {
return { const profiles = res.data
device: { console.log(profiles)
connected: false, const ids = new Set()
profiles: [], const validate = ajv.compile(schema)
}, store.device.profiles = profiles.filter((profile) => {
if (!validate(profile)) {
console.error('Failed to validate profile: ' + profile.name, validate.errors)
return false
} }
}, if (ids.has(profile.id)) {
mutations: {}, console.error('Duplicate profile id: ' + profile.id + ' for profile: ' + profile.name)
return false
}
ids.add(profile.id)
return true
})
}).catch((err) => {
console.error(err)
}) })
// Create VueI18n instance with locales loaded from /lang directory // Create VueI18n instance with locales loaded from /lang directory
@@ -31,10 +44,5 @@ const i18n = createI18n({
}) })
const app = createApp(App) const app = createApp(App)
app.use(i18n) app.use(i18n)
app.use(store)
app.provide('ajv', new Ajv())
app.mount('#app') app.mount('#app')

9
src/store.js Normal file
View File

@@ -0,0 +1,9 @@
import { reactive } from 'vue'
export const store = reactive({
device: {
profiles: [],
activeProfile: null,
},
currentProfile: null,
})