ADD: Pinia

This commit is contained in:
Robert Kossessa
2024-01-31 00:11:55 +01:00
parent 98058e9a4a
commit 0f5e0df58b
7 changed files with 209 additions and 113 deletions

68
package-lock.json generated
View File

@@ -21,6 +21,7 @@
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"electron-squirrel-startup": "^1.0.0", "electron-squirrel-startup": "^1.0.0",
"lucide-vue-next": "^0.309.0", "lucide-vue-next": "^0.309.0",
"pinia": "^2.1.7",
"radix-vue": "^1.3.0", "radix-vue": "^1.3.0",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
@@ -8304,6 +8305,56 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/pinia": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz",
"integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==",
"dependencies": {
"@vue/devtools-api": "^6.5.0",
"vue-demi": ">=0.14.5"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4",
"vue": "^2.6.14 || ^3.3.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/pirates": { "node_modules/pirates": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
@@ -16738,6 +16789,23 @@
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="
}, },
"pinia": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz",
"integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==",
"requires": {
"@vue/devtools-api": "^6.5.0",
"vue-demi": ">=0.14.5"
},
"dependencies": {
"vue-demi": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz",
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
"requires": {}
}
}
},
"pirates": { "pirates": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",

View File

@@ -28,6 +28,7 @@
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"electron-squirrel-startup": "^1.0.0", "electron-squirrel-startup": "^1.0.0",
"lucide-vue-next": "^0.309.0", "lucide-vue-next": "^0.309.0",
"pinia": "^2.1.7",
"radix-vue": "^1.3.0", "radix-vue": "^1.3.0",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

View File

@@ -7,15 +7,21 @@ import ConfigSelect from '@/components/config/ConfigSelect.vue'
import { ref } from 'vue' import { ref } from 'vue'
import ConfigSection from '@/components/config/ConfigSection.vue' import ConfigSection from '@/components/config/ConfigSection.vue'
import { Bolt } from 'lucide-vue-next' import { Bolt } from 'lucide-vue-next'
import { useStore } from '@/store'
const currentConfigPage = ref('profile_settings') const currentConfigPage = ref('profile_settings')
const store = useStore()
store.fetchProfiles()
</script> </script>
<template> <template>
<main class="select-none w-screen h-screen flex flex-col"> <main class="select-none w-screen h-screen flex flex-col">
<Navbar class="flex-none" /> <Navbar class="flex-none" />
<div class="flex-1 min-h-0 flex flex-row"> <div class="flex-1 min-h-0 flex flex-row">
<ProfileManager class="basis-1/3 min-w-80 flex-1 flex flex-col border-solid border-0 border-r bg-zinc-900 bg-opacity-30" /> <ProfileManager
class="basis-1/3 min-w-80 flex-1 flex flex-col border-solid border-0 border-r bg-zinc-900 bg-opacity-30" />
<div class="basis-1/3 flex-1 flex flex-col border-solid border-0 border-r"> <div class="basis-1/3 flex-1 flex flex-col border-solid border-0 border-r">
<DevicePreview class="flex-col flex flex-none" /> <DevicePreview class="flex-col flex flex-none" />
<ConfigSection class="flex-none" :title="$t('config_options.title')" :foldable="false" :icon-component="Bolt" /> <ConfigSection class="flex-none" :title="$t('config_options.title')" :foldable="false" :icon-component="Bolt" />

View File

@@ -17,10 +17,12 @@ import ConfigSection from '@/components/config/ConfigSection.vue'
import PaletteInput from '@/components/config/PaletteInput.vue' import PaletteInput from '@/components/config/PaletteInput.vue'
import Color from 'color' import Color from 'color'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { store } from '@/store.js' import { useStore } from '@/store.js'
import { Slider } from '@/components/ui/slider/index.js' import { Slider } from '@/components/ui/slider/index.js'
const ledConfig = computed(() => store.currentProfile.ledConfig) const store = useStore()
const ledConfig = computed(() => store.selectedProfile.ledConfig)
const brightnessSliderModel = computed({ const brightnessSliderModel = computed({
get: () => [ledConfig.value.ledBrightness], get: () => [ledConfig.value.ledBrightness],

View File

@@ -5,7 +5,7 @@
class="w-full px-4 h-20 flex items-center"> class="w-full px-4 h-20 flex items-center">
<div> <div>
<h1 class="text-lg"> <h1 class="text-lg">
{{ $t(`profiles.title`) }}<span class="text-sm text-zinc-600"> ({{ profiles.length }}/{{ maxProfiles {{ $t(`profiles.title`) }}<span class="text-sm text-zinc-600"> ({{ store.profiles.length }}/{{ maxProfiles
}})</span> }})</span>
</h1> </h1>
<p class="text-xs text-muted-foreground"> <p class="text-xs text-muted-foreground">
@@ -53,10 +53,10 @@
<CollapsibleContent> <CollapsibleContent>
<ProfileButton <ProfileButton
v-for="(profile, index) in tagProfiles" :key="profile.id" v-model="tagProfiles[index]" v-for="(profile, index) in tagProfiles" :key="profile.id" v-model="tagProfiles[index]"
:selected="currentProfileId===profile.id" :selected="store.selectedProfile.id === profile.id"
@select="currentProfileId=profile.id" @select="store.selectProfile(profile.id)"
@duplicate="duplicateProfile(profile.id)" @duplicate="store.duplicateProfile(profile.id)"
@delete="deleteProfile(profile.id)" /> @delete="store.deleteProfile(profile.id)" />
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
</div> </div>
@@ -69,28 +69,21 @@ import { ChevronRight, Plus, Search } from 'lucide-vue-next'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } 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 { useStore } from '@/store.js'
import ProfileButton from '@/components/profile/ProfileButton.vue' import ProfileButton from '@/components/profile/ProfileButton.vue'
const maxProfiles = 32 const maxProfiles = 32
const profiles = computed({ const store = useStore()
get: () => store.device.profiles,
set: val => store.device.profiles = val,
})
const currentProfileId = computed({
get: () => store.currentProfileId,
set: val => store.currentProfileId = val,
})
const filter = ref('') const filter = ref('')
const collapse = ref({}) const collapse = ref({})
const filteredProfiles = computed(() => { const filteredProfiles = computed(() => {
if (!filter.value) { if (!filter.value) {
return profiles.value return store.profiles
} }
const filterLower = filter.value.toLowerCase() const filterLower = filter.value.toLowerCase()
return profiles.value.filter(profile => { return store.profiles.filter(profile => {
const nameLower = profile.name.toLowerCase() const nameLower = profile.name.toLowerCase()
const idLower = profile.id.toLowerCase() const idLower = profile.id.toLowerCase()
const tagLower = profile.profileTag.toLowerCase() const tagLower = profile.profileTag.toLowerCase()
@@ -98,47 +91,6 @@ const filteredProfiles = computed(() => {
}) })
}) })
function newProfileId(originalId = '') {
let id = originalId
if (originalId) {
do {
id = Math.floor((parseInt(id) + 1) % 9999).toString().padStart(4, '0')
} while (store.profileIds.includes(id))
} else {
do {
id = Math.floor(Math.random() * 9999).toString().padStart(4, '0')
} while (store.profileIds.includes(id))
}
return id
}
function newProfileName(originalName) {
let name = originalName
let i = 1
while (profiles.value.find(p => p.name === name)) {
name = `${originalName} (${i++})`
}
return name
}
const duplicateProfile = (id) => {
const profile = profiles.value.find(p => p.id === id)
const profileIndex = profiles.value.indexOf(profile)
const newProfile = JSON.parse(JSON.stringify(profile))
newProfile.id = newProfileId(id)
newProfile.name = newProfileName(profile.name)
profiles.value.splice(profileIndex + 1, 0, newProfile)
}
const deleteProfile = (id) => {
const profile = profiles.value.find(p => p.id === id)
const profileIndex = profiles.value.indexOf(profile)
profiles.value.splice(profileIndex, 1)
if (currentProfileId.value === id && profiles.value.length > 0) {
currentProfileId.value = profiles.value[0].id
}
}
const filteredProfilesByTag = computed(() => { const filteredProfilesByTag = computed(() => {
const map = new Map() const map = new Map()
filteredProfiles.value.forEach(profile => { filteredProfiles.value.forEach(profile => {
@@ -151,11 +103,6 @@ const filteredProfilesByTag = computed(() => {
return map return map
}) })
watch(profiles, () => {
if (!currentProfileId.value && profiles.value.length > 0)
currentProfileId.value = profiles.value[0].id
})
</script> </script>
<style scoped> <style scoped>
[data-state=open] > .chevrot { [data-state=open] > .chevrot {

View File

@@ -27,7 +27,7 @@
*/ */
import { createApp } from 'vue'; 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 en from '@/lang/en.json' import en from '@/lang/en.json'
@@ -35,41 +35,16 @@ import en from '@/lang/en.json'
import './assets/main.css' import './assets/main.css'
import './assets/index.css' import './assets/index.css'
import { store } from '@/store.js' import { pinia } from '@/store.js'
import Ajv from 'ajv'
import schema from '@/data/profileSchema.json'
import Axios from 'axios'
const ajv = new Ajv()
Axios.get('http://localhost:3001/profiles').then((res) => {
const profiles = res.data
console.log(profiles)
const ids = new Set()
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)) {
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
const i18n = createI18n({ const i18n = createI18n({
locale: 'en', locale: 'en',
fallbackLocale: 'en', fallbackLocale: 'en',
messages: { en: en }, messages: { en: en },
}); })
const app = createApp(App); const app = createApp(App)
app.use(pinia)
app.use(i18n) app.use(i18n)
app.mount('#app'); app.mount('#app')

View File

@@ -1,16 +1,113 @@
import { computed, reactive } from 'vue' // import { computed, reactive } from 'vue'
//
// export const store = reactive({
// device: {
// profiles: [],
// activeProfileId: null,
// activeProfile: computed(() => {
// return store.device.profiles.find(p => p.id === store.device.activeProfileId)
// }),
// },
// currentProfileId: null,
// currentProfile: computed(() => {
// return store.device.profiles.find(p => p.id === store.currentProfileId)
// }),
// profileIds: computed(() => store.device.profiles.map(p => p.id)),
// })
export const store = reactive({ import { createPinia, defineStore } from 'pinia'
device: { import Axios from 'axios'
profiles: [], import schema from '@/data/profileSchema.json'
activeProfileId: null, import Ajv from 'ajv'
activeProfile: computed(() => {
return store.device.profiles.find(p => p.id === store.device.activeProfileId) const ajv = new Ajv()
}),
export const useStore = defineStore('main', {
// arrow function recommended for full type inference
state: () => {
return {
profiles: [],
selectedProfileId: null,
connected: false,
}
}, },
currentProfileId: null, getters: {
currentProfile: computed(() => { profileIds: (state) => state.profiles.map(p => p.id),
return store.device.profiles.find(p => p.id === store.currentProfileId) selectedProfile: (state) => state.profiles.find(p => p.id === state.selectedProfileId),
}), },
profileIds: computed(() => store.device.profiles.map(p => p.id)), actions: {
}) selectProfile(id) {
if (!this.profileIds.includes(id)) return false
this.selectedProfileId = id
return true
},
duplicateProfile(id) {
const originalProfile = this.profiles.find(p => p.id === id)
const newProfile = JSON.parse(JSON.stringify(originalProfile))
newProfile.id = this.newProfileId(originalProfile.id)
newProfile.name = this.newProfileName(originalProfile.name)
this.profiles.push(newProfile)
this.selectedProfileId = newProfile.id
return newProfile.id
},
deleteProfile(id) {
const index = this.profiles.findIndex(p => p.id === id)
if (index >= 0) {
this.profiles.splice(index, 1)
if (this.selectedProfileId === id) {
this.selectedProfileId = this.profiles[0]?.id || null
}
return true
}
return false
},
fetchProfiles() {
Axios.get('http://localhost:3001/profiles').then((res) => {
const profiles = res.data
console.log(profiles)
const ids = new Set()
const validate = ajv.compile(schema)
this.$patch({
profiles: profiles.filter((profile) => {
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: profiles[0]?.id || null,
})
}).catch((err) => {
console.error(err)
})
},
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
},
},
})
export const pinia = createPinia()