UPD: Linting frenzy

This commit is contained in:
Robert Kossessa
2024-03-01 23:03:09 +01:00
parent 746c339c16
commit bc3e4ac32f
30 changed files with 912 additions and 629 deletions

View File

@@ -2,16 +2,19 @@
require('@rushstack/eslint-patch/modern-module-resolution') require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = { module.exports = {
root: true,
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:vue/vue3-recommended', 'plugin:vue/vue3-recommended',
'@electron-toolkit', '@electron-toolkit',
'@electron-toolkit/eslint-config-ts/eslint-recommended', '@electron-toolkit/eslint-config-ts/eslint-recommended',
'@vue/eslint-config-typescript/recommended', '@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier' '@vue/eslint-config-prettier',
'plugin:tailwindcss/recommended'
], ],
rules: { rules: {
'vue/require-default-prop': 'off', 'vue/require-default-prop': 'off',
'vue/multi-word-component-names': 'off' 'vue/multi-word-component-names': 'off',
'tailwindcss/no-custom-classname': 'off'
} }
} }

View File

@@ -1,3 +1,6 @@
{ {
"recommendations": ["dbaeumer.vscode-eslint"] "recommendations": [
"dbaeumer.vscode-eslint",
"Vue.vscode-typescript-vue-plugin"
]
} }

View File

@@ -55,6 +55,7 @@
"electron-builder": "^24.9.1", "electron-builder": "^24.9.1",
"electron-vite": "^2.0.0", "electron-vite": "^2.0.0",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-plugin-tailwindcss": "^3.14.3",
"eslint-plugin-vue": "^9.20.1", "eslint-plugin-vue": "^9.20.1",
"postcss": "^8.4.35", "postcss": "^8.4.35",
"prettier": "^3.2.4", "prettier": "^3.2.4",

14
pnpm-lock.yaml generated
View File

@@ -103,6 +103,9 @@ devDependencies:
eslint: eslint:
specifier: ^8.56.0 specifier: ^8.56.0
version: 8.57.0 version: 8.57.0
eslint-plugin-tailwindcss:
specifier: ^3.14.3
version: 3.14.3(tailwindcss@3.4.1)
eslint-plugin-vue: eslint-plugin-vue:
specifier: ^9.20.1 specifier: ^9.20.1
version: 9.22.0(eslint@8.57.0) version: 9.22.0(eslint@8.57.0)
@@ -2457,6 +2460,17 @@ packages:
synckit: 0.8.8 synckit: 0.8.8
dev: true dev: true
/eslint-plugin-tailwindcss@3.14.3(tailwindcss@3.4.1):
resolution: {integrity: sha512-1MKT8CrVuqVJleHxb7ICHsF2QwO0G+VJ28athTtlcOkccp0qmwK7nCUa1C9paCZ+VVgQU4fonsjLz/wUxoMHJQ==}
engines: {node: '>=12.13.0'}
peerDependencies:
tailwindcss: ^3.4.0
dependencies:
fast-glob: 3.3.2
postcss: 8.4.35
tailwindcss: 3.4.1
dev: true
/eslint-plugin-vue@9.22.0(eslint@8.57.0): /eslint-plugin-vue@9.22.0(eslint@8.57.0):
resolution: {integrity: sha512-7wCXv5zuVnBtZE/74z4yZ0CM8AjH6bk4MQGm7hZjUC2DBppKU5ioeOk5LGSg/s9a1ZJnIsdPLJpXnu1Rc+cVHg==} resolution: {integrity: sha512-7wCXv5zuVnBtZE/74z4yZ0CM8AjH6bk4MQGm7hZjUC2DBppKU5ioeOk5LGSg/s9a1ZJnIsdPLJpXnu1Rc+cVHg==}
engines: {node: ^14.17.0 || >=16.0.0} engines: {node: ^14.17.0 || >=16.0.0}

View File

@@ -46,23 +46,23 @@ window.nanodevices.on_event('update', (evt, deviceid, data) => {
window.nanodevices.list_devices().then((devs) => store.init_devices(devs)) window.nanodevices.list_devices().then((devs) => store.init_devices(devs))
</script> </script>
<template> <template>
<main class="select-none w-screen h-screen flex flex-col"> <main class="flex h-screen w-screen select-none flex-col">
<Navbar class="flex-none" /> <Navbar class="flex-none" />
<div class="flex-1 min-h-0 flex flex-row justify-center"> <div class="flex min-h-0 flex-1 flex-row justify-center">
<div class="basis-1/3 min-w-60 flex-1 flex 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="store.connected"
class="flex-1 max-w-full flex flex-col border-solid border-0 border-r bg-zinc-900 bg-opacity-50" class="flex max-w-full flex-1 flex-col border-0 border-r border-solid bg-zinc-900/50"
/> />
</Transition> </Transition>
</div> </div>
<DevicePreview class="basis-1/3 flex-col flex" /> <DevicePreview class="flex basis-1/3 flex-col" />
<div class="basis-2/5 flex-1 flex 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="store.connected"
class="flex-1 max-w-full flex flex-col border-solid border-0 border-l bg-zinc-900 bg-opacity-50" class="flex max-w-full flex-1 flex-col border-0 border-l border-solid bg-zinc-900/50"
/> />
</Transition> </Transition>
</div> </div>

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="bg-wip w-full text-center p-8 text-zinc-600"> <div class="bg-wip w-full p-8 text-center text-zinc-600">
<span class="bg-black font-heading p-1">WORK IN PROGRESS</span> <span class="font-heading bg-black p-1">WORK IN PROGRESS</span>
</div> </div>
</template> </template>

View File

@@ -1,18 +1,25 @@
<template> <template>
<Collapsible v-model:open="collapse" :default-open="true"> <Collapsible v-model:open="collapse" :default-open="true">
<div class="w-full bg-zinc-900 h-12 flex"> <div class="flex h-12 w-full bg-zinc-900">
<div <div
class="flex-1 flex items-center px-4" class="flex flex-1 items-center px-4"
:class="{'cursor-pointer hover:bg-zinc-800': showToggle}" :class="{ 'cursor-pointer hover:bg-zinc-800': showToggle }"
@click="toggle = !toggle"> @click="toggle = !toggle"
<component :is="iconComponent" v-if="iconComponent" class="h-4 w-4 mr-2" /> >
<h2 class="text-sm py-4">{{ title }}<slot name="title"/></h2> <component :is="iconComponent" v-if="iconComponent" class="mr-2 size-4" />
<h2 class="py-4 text-sm">{{ title }}<slot name="title" /></h2>
<Switch <Switch
v-if="showToggle" :checked="toggle" v-if="showToggle"
class="ml-auto" @click.stop="toggle=!toggle" /> :checked="toggle"
class="ml-auto"
@click.stop="toggle = !toggle"
/>
</div> </div>
<CollapsibleTrigger v-if="foldable" class="flex items-center justify-center h-12 aspect-square hover:bg-zinc-800"> <CollapsibleTrigger
<ChevronLeft class="chevrot h-4 w-4 mt-0.5 transition-transform text-muted-foreground" /> v-if="foldable"
class="flex aspect-square h-12 items-center justify-center hover:bg-zinc-800"
>
<ChevronLeft class="chevrot mt-0.5 size-4 text-muted-foreground transition-transform" />
</CollapsibleTrigger> </CollapsibleTrigger>
</div> </div>
<CollapsibleContent> <CollapsibleContent>
@@ -22,7 +29,11 @@
</template> </template>
<script setup> <script setup>
import { ChevronLeft } from 'lucide-vue-next' import { ChevronLeft } from 'lucide-vue-next'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@renderer/components/ui/collapsible' import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from '@renderer/components/ui/collapsible'
import { ref } from 'vue' import { ref } from 'vue'
import { Switch } from '@renderer/components/ui/switch' import { Switch } from '@renderer/components/ui/switch'
@@ -30,31 +41,30 @@ const collapse = ref(true)
const toggle = defineModel({ const toggle = defineModel({
type: Boolean, type: Boolean,
default: true, default: true
}) })
defineProps({ defineProps({
title: { title: {
type: String, type: String,
default: 'MISSING_TITLE', default: 'MISSING_TITLE'
}, },
iconComponent: { iconComponent: {
type: [String, Object, Function], type: [String, Object, Function],
default: undefined, default: undefined
}, },
showToggle: { showToggle: {
type: Boolean, type: Boolean,
default: false, default: false
}, },
foldable: { foldable: {
type: Boolean, type: Boolean,
default: true, default: true
}, }
}) })
</script> </script>
<style scoped> <style scoped>
[data-state=open] > .chevrot { [data-state='open'] > .chevrot {
transform: rotate(-90deg); transform: rotate(-90deg);
} }
</style> </style>

View File

@@ -1,122 +1,179 @@
<template> <template>
<div <div
class="mx-2 flex p-4 font-heading rounded-b-lg border-x border-b border-zinc-800" class="font-heading mx-2 flex rounded-b-lg border-x border-b border-zinc-800 p-4"
:class="{'rounded-t-lg': roundedTop}" :class="{ 'rounded-t-lg': roundedTop }"
:style="{backgroundColor: color.hex()}"> :style="{ backgroundColor: color.hex() }"
>
<div <div
ref="colorFieldText" class="w-full flex opacity-70" ref="colorFieldText"
:class="!isDark(color) ? 'text-black selection:bg-black selection:text-white' : 'selection:bg-white selection:text-black'" class="flex w-full opacity-70"
style="transition: color 0.2s ease-in-out"> :class="
!isDark(color)
? 'text-black selection:bg-black selection:text-white'
: 'selection:bg-white selection:text-black'
"
style="transition: color 0.2s ease-in-out"
>
<div> <div>
<form @submit.prevent="onSubmitHueInput"> <form @submit.prevent="onSubmitHueInput">
<label for="hueInput">H: </label><input <label for="hueInput">H: </label
id="hueInput" ><input
v-model="hueInput" id="hueInput"
onfocus="this.select()" v-model="hueInput"
type="number" maxlength="3" onfocus="this.select()"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none" type="number"
@blur="updateInputs"> maxlength="3"
class="w-8 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form> </form>
<form @submit.prevent="onSubmitSaturationInput"> <form @submit.prevent="onSubmitSaturationInput">
<label for="saturationInput">S: </label><input <label for="saturationInput">S: </label
id="saturationInput" ><input
v-model="saturationInput" id="saturationInput"
onfocus="this.select()" v-model="saturationInput"
type="number" maxlength="3" onfocus="this.select()"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none" type="number"
@blur="updateInputs"> maxlength="3"
class="w-8 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form> </form>
<form @submit.prevent="onSubmitValueInput"> <form @submit.prevent="onSubmitValueInput">
<label for="valueInput">V: </label><input <label for="valueInput">V: </label
id="valueInput" ><input
v-model="valueInput" id="valueInput"
onfocus="this.select()" v-model="valueInput"
type="number" maxlength="3" onfocus="this.select()"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none" type="number"
@blur="updateInputs"> maxlength="3"
class="w-8 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form> </form>
</div> </div>
<div class="mx-auto"> <div class="mx-auto">
<form @submit.prevent="onSubmitHexInput"> <form @submit.prevent="onSubmitHexInput">
<label for="hexInput">#</label><input <label for="hexInput">#</label
id="hexInput" ><input
v-model="hexInput" maxlength="6" id="hexInput"
onfocus="this.select()" v-model="hexInput"
class="w-16 bg-transparent focus-visible:ring-0 focus-visible:outline-none" maxlength="6"
@blur="updateInputs"> onfocus="this.select()"
class="w-16 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form> </form>
</div> </div>
<div> <div>
<form @submit.prevent="onSubmitRGBInput"> <form @submit.prevent="onSubmitRGBInput">
<label for="rInput">R: </label><input <label for="rInput">R: </label
id="rInput" ><input
v-model="rInput" id="rInput"
onfocus="this.select()" v-model="rInput"
type="number" maxlength="3" onfocus="this.select()"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none" type="number"
@blur="updateInputs"> maxlength="3"
class="w-8 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form> </form>
<form @submit.prevent="onSubmitRGBInput"> <form @submit.prevent="onSubmitRGBInput">
<label for="gInput">G: </label><input <label for="gInput">G: </label
id="gInput" ><input
v-model="gInput" id="gInput"
onfocus="this.select()" v-model="gInput"
type="number" maxlength="3" onfocus="this.select()"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none" type="number"
@blur="updateInputs"> maxlength="3"
class="w-8 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form> </form>
<form @submit.prevent="onSubmitRGBInput"> <form @submit.prevent="onSubmitRGBInput">
<label for="bInput">B: </label><input <label for="bInput">B: </label
id="bInput" ><input
v-model="bInput" id="bInput"
onfocus="this.select()" v-model="bInput"
type="number" maxlength="3" onfocus="this.select()"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none" type="number"
@blur="updateInputs"> maxlength="3"
class="w-8 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<div class="flex py-4 px-8"> <div class="flex px-8 py-4">
<SliderRoot <SliderRoot
v-model="hueSliderModel" :max="359" v-model="hueSliderModel"
class="relative flex w-full touch-none select-none items-center h-10"> :max="359"
class="relative flex h-10 w-full touch-none select-none items-center"
>
<SliderTrack <SliderTrack
class="relative h-2.5 w-full grow overflow-hidden rounded-full border-2 border-zinc-900" class="relative h-2.5 w-full grow overflow-hidden rounded-full border-2 border-zinc-900"
style="background: linear-gradient(90deg, rgba(255, 0, 0, 1) 0%, rgba(255, 154, 0, 1) 10%, rgba(208, 222, 33, 1) 20%, rgba(79, 220, 74, 1) 30%, rgba(63, 218, 216, 1) 40%, rgba(47, 201, 226, 1) 50%, rgba(28, 127, 238, 1) 60%, rgba(95, 21, 242, 1) 70%, rgba(186, 12, 248, 1) 80%, rgba(251, 7, 217, 1) 90%, rgba(255, 0, 0, 1) 100%)" /> style="
background: linear-gradient(
90deg,
rgba(255, 0, 0, 1) 0%,
rgba(255, 154, 0, 1) 10%,
rgba(208, 222, 33, 1) 20%,
rgba(79, 220, 74, 1) 30%,
rgba(63, 218, 216, 1) 40%,
rgba(47, 201, 226, 1) 50%,
rgba(28, 127, 238, 1) 60%,
rgba(95, 21, 242, 1) 70%,
rgba(186, 12, 248, 1) 80%,
rgba(251, 7, 217, 1) 90%,
rgba(255, 0, 0, 1) 100%
);
"
/>
<SliderThumb <SliderThumb
class="flex h-6 w-8 rounded-[8px] hover:bg-zinc-200 border border-zinc-100 bg-zinc-300 focus-visible:outline-none focus-visible:ring-1 cursor-pointer focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 text-zinc-600 justify-center items-center" class="flex h-6 w-8 cursor-pointer items-center justify-center rounded-[8px] border border-zinc-100 bg-zinc-300 text-zinc-600 hover:bg-zinc-200 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
style="box-shadow: -3px 0 15px 0 rgba(0,0,0,0.6)"> style="box-shadow: -3px 0 15px 0 rgba(0, 0, 0, 0.6)"
>
<MoreHorizontal class="h-full" /> <MoreHorizontal class="h-full" />
</SliderThumb> </SliderThumb>
</SliderRoot> </SliderRoot>
</div> </div>
<Separator /> <Separator />
<div class="flex py-4 px-8"> <div class="flex px-8 py-4">
<SliderRoot <SliderRoot
v-model="saturationSliderModel" :max="100" v-model="saturationSliderModel"
class="relative flex w-full touch-none select-none items-center h-10"> :max="100"
class="relative flex h-10 w-full touch-none select-none items-center"
>
<SliderTrack <SliderTrack
class="relative h-2.5 w-full grow overflow-hidden rounded-full border-2 border-zinc-900" class="relative h-2.5 w-full grow overflow-hidden rounded-full border-2 border-zinc-900"
:style="{background: `linear-gradient(90deg, hsl(0, 0%, ${saturationSliderColor.lightness()}%) 0%, hsl(${saturationSliderColor.hue()}, 100%, ${saturationSliderColor.lightness()}%) 100%)`}" /> :style="{
background: `linear-gradient(90deg, hsl(0, 0%, ${saturationSliderColor.lightness()}%) 0%, hsl(${saturationSliderColor.hue()}, 100%, ${saturationSliderColor.lightness()}%) 100%)`
}"
/>
<SliderThumb <SliderThumb
class="flex h-6 w-8 rounded-[8px] hover:bg-zinc-200 border border-zinc-100 bg-zinc-300 focus-visible:outline-none focus-visible:ring-1 cursor-pointer focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 text-zinc-600 justify-center items-center" class="flex h-6 w-8 cursor-pointer items-center justify-center rounded-[8px] border border-zinc-100 bg-zinc-300 text-zinc-600 hover:bg-zinc-200 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
style="box-shadow: -3px 0 15px 0 rgba(0,0,0,0.6)"> style="box-shadow: -3px 0 15px 0 rgba(0, 0, 0, 0.6)"
>
<MoreHorizontal class="h-full" /> <MoreHorizontal class="h-full" />
</SliderThumb> </SliderThumb>
</SliderRoot> </SliderRoot>
</div> </div>
<Separator /> <Separator />
<div class="flex py-4 px-8"> <div class="flex px-8 py-4">
<SliderRoot <SliderRoot
v-model="valueSliderModel" :max="100" v-model="valueSliderModel"
class="relative flex w-full touch-none select-none items-center h-10"> :max="100"
class="relative flex h-10 w-full touch-none select-none items-center"
>
<SliderTrack <SliderTrack
class="relative h-2.5 w-full grow overflow-hidden rounded-full border-2 border-zinc-900" class="relative h-2.5 w-full grow overflow-hidden rounded-full border-2 border-zinc-900"
:style="{background: `linear-gradient(90deg, black, ${valueSliderColor.hex()} 100%`}" /> :style="{ background: `linear-gradient(90deg, black, ${valueSliderColor.hex()} 100%` }"
/>
<SliderThumb <SliderThumb
class="flex h-6 w-8 rounded-[8px] hover:bg-zinc-200 border border-zinc-100 bg-zinc-300 focus-visible:outline-none focus-visible:ring-1 cursor-pointer focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 text-zinc-600 justify-center items-center" class="flex h-6 w-8 cursor-pointer items-center justify-center rounded-[8px] border border-zinc-100 bg-zinc-300 text-zinc-600 hover:bg-zinc-200 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
style="box-shadow: -3px 0 15px 0 rgba(0,0,0,0.6)"> style="box-shadow: -3px 0 15px 0 rgba(0, 0, 0, 0.6)"
>
<MoreHorizontal class="h-full" /> <MoreHorizontal class="h-full" />
</SliderThumb> </SliderThumb>
</SliderRoot> </SliderRoot>
@@ -133,8 +190,8 @@ import { Separator } from '@renderer/components/ui/separator'
defineProps({ defineProps({
roundedTop: { roundedTop: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}) })
const hueSliderValue = ref(0) const hueSliderValue = ref(0)
@@ -148,7 +205,7 @@ const hueSliderModel = computed({
set(hue) { set(hue) {
hueSliderValue.value = hue[0] hueSliderValue.value = hue[0]
color.value = color.value.hue(hue[0]) color.value = color.value.hue(hue[0])
}, }
}) })
const saturationSliderModel = computed({ const saturationSliderModel = computed({
@@ -158,7 +215,7 @@ const saturationSliderModel = computed({
set(saturation) { set(saturation) {
saturationSliderValue.value = saturation[0] saturationSliderValue.value = saturation[0]
color.value = color.value.saturationv(saturation[0]) color.value = color.value.saturationv(saturation[0])
}, }
}) })
const valueSliderModel = computed({ const valueSliderModel = computed({
@@ -168,12 +225,12 @@ const valueSliderModel = computed({
set(value) { set(value) {
valueSliderValue.value = value[0] valueSliderValue.value = value[0]
color.value = color.value.value(value[0]) color.value = color.value.value(value[0])
}, }
}) })
const color = defineModel({ const color = defineModel({
type: Color, type: Color,
default: () => Color.rgb(255, 0, 0), default: () => Color.rgb(255, 0, 0)
}) })
const saturationSliderColor = computed(() => { const saturationSliderColor = computed(() => {
@@ -199,8 +256,7 @@ function onSubmitHexInput() {
} }
if (input.match(/^#[0-9A-F]{6}$/i)) { if (input.match(/^#[0-9A-F]{6}$/i)) {
color.value = Color(input) color.value = Color(input)
} else } else shake()
shake()
} }
function onSubmitHueInput() { function onSubmitHueInput() {
@@ -288,7 +344,6 @@ function isDark(color) {
const yiq = (rgb[0] * 6126 + rgb[1] * 7152 + rgb[2] * 722) / 10000 // Changed r factor from 2126 const yiq = (rgb[0] * 6126 + rgb[1] * 7152 + rgb[2] * 722) / 10000 // Changed r factor from 2126
return yiq < 128 return yiq < 128
} }
</script> </script>
<style scoped> <style scoped>
.shake { .shake {

View File

@@ -1,22 +1,36 @@
<template> <template>
<div <div
class="pt-2" class="pt-2"
:style="{background: `linear-gradient(180deg, ${options[currentOption].color.hex()+'11'}, ${options[currentOption].color.hex()+'30'} 25%, ${options[currentOption].color.hex()+'30'} 40%, transparent 60%`}"> :style="{
background: `linear-gradient(180deg, ${options[currentOption].color.hex() + '11'}, ${options[currentOption].color.hex() + '30'} 25%, ${options[currentOption].color.hex() + '30'} 40%, transparent 60%`
}"
>
<div <div
class="mx-2 flex font-heading rounded-t-lg overflow-hidden border-t border-x border-zinc-800 bg-zinc-900"> class="font-heading mx-2 flex overflow-hidden rounded-t-lg border-x border-t border-zinc-800 bg-zinc-900"
>
<button <button
v-for="(option, key) in options" :key="key" v-for="(option, key) in options"
class="flex-1 py-2 items-center text-center rounded-t-lg min-w-0 transition-colors" :key="key"
:class="currentOption!==key ? 'hover:bg-zinc-800 text-muted-foreground mx-[1px]' : 'text-black bg-zinc-300 hover:bg-zinc-200 border-x border-t border-zinc-100'" class="min-w-0 flex-1 items-center rounded-t-lg py-2 text-center transition-colors"
@click="currentOption = key"> :class="
currentOption !== key
? 'hover:bg-zinc-800 text-muted-foreground mx-[1px]'
: 'text-black bg-zinc-300 hover:bg-zinc-200 border-x border-t border-zinc-100'
"
@click="currentOption = key"
>
{{ $t(option.titleKey) }} {{ $t(option.titleKey) }}
</button> </button>
</div> </div>
<div class="mx-2 flex border-x border-zinc-800 overflow-hidden"> <div class="mx-2 flex overflow-hidden border-x border-zinc-800">
<button <button
v-for="(option, key) in options" :key="key" class="flex-1 h-6" v-for="(option, key) in options"
:class="{ 'color-tab': currentOption === key}" :key="key"
:style="{background: option.color.hex()}" @click="currentOption = key" /> class="h-6 flex-1"
:class="{ 'color-tab': currentOption === key }"
:style="{ background: option.color.hex() }"
@click="currentOption = key"
/>
</div> </div>
<HSVInput v-model="options[currentOption].color" /> <HSVInput v-model="options[currentOption].color" />
</div> </div>
@@ -35,26 +49,24 @@ const model = defineModel({
default: () => ({ default: () => ({
one: { one: {
titleKey: 'One', titleKey: 'One',
color: Color('#ff0000'), color: Color('#ff0000')
}, },
two: { two: {
titleKey: 'Two', titleKey: 'Two',
color: Color('#00ff00'), color: Color('#00ff00')
}, },
three: { three: {
titleKey: 'Three', titleKey: 'Three',
color: Color('#0000ff'), color: Color('#0000ff')
}, }
}), })
}) })
const options = reactive(model.value) const options = reactive(model.value)
onBeforeMount(() => { onBeforeMount(() => {
if (currentOption.value === null) if (currentOption.value === null) currentOption.value = Object.keys(options)[0]
currentOption.value = Object.keys(options)[0]
}) })
</script> </script>
<style scoped> <style scoped>
.color-tab { .color-tab {
@@ -68,7 +80,7 @@ onBeforeMount(() => {
bottom: -1px; bottom: -1px;
width: var(--rounded); width: var(--rounded);
height: var(--rounded); height: var(--rounded);
content: " "; content: ' ';
} }
.color-tab:before { .color-tab:before {
@@ -87,7 +99,8 @@ onBeforeMount(() => {
z-index: 1; z-index: 1;
} }
.color-tab:after, .color-tab:before { .color-tab:after,
.color-tab:before {
border: none; border: none;
} }
</style> </style>

View File

@@ -11,40 +11,40 @@ function playClick() {
const props = defineProps({ const props = defineProps({
text: { text: {
type: String, type: String,
default: '', default: ''
}, },
characterSet: { characterSet: {
type: String, type: String,
default: 'x01_-/', default: 'x01_-/'
}, },
scrambleOnHover: { scrambleOnHover: {
type: Boolean, type: Boolean,
default: false, default: false
}, },
fillInterval: { fillInterval: {
type: Number, type: Number,
default: 0, default: 0
}, },
scrambleAmount: { scrambleAmount: {
type: Number, type: Number,
default: 1, default: 1
}, },
replaceInterval: { replaceInterval: {
type: Number, type: Number,
default: 15, default: 15
}, },
scrambleOnMount: { scrambleOnMount: {
type: Boolean, type: Boolean,
default: false, default: false
}, },
resize: { resize: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
delay: { delay: {
type: Number, type: Number,
default: 0, default: 0
}, }
}) })
const content = ref('') const content = ref('')
@@ -67,27 +67,38 @@ function replaceContent(text = props.text, replaceInterval = props.replaceInterv
} }
if (indices.length > 0) { if (indices.length > 0) {
const index = indices[Math.floor(Math.random() * indices.length)] const index = indices[Math.floor(Math.random() * indices.length)]
content.value = content.value.substring(0, index) + text.charAt(index) + content.value.substring(index + 1) content.value =
content.value.substring(0, index) + text.charAt(index) + content.value.substring(index + 1)
} else if (content.value.length < text.length) { } else if (content.value.length < text.length) {
content.value += text.charAt(content.value.length) content.value += text.charAt(content.value.length)
} else { } else {
content.value = content.value.substring(0, content.value.length - 1) content.value = content.value.substring(0, content.value.length - 1)
} }
//playClick() //playClick()
setTimeout(() => { setTimeout(
replaceContent(text, replaceInterval, steps + 1) () => {
}, replaceInterval * (1 + Math.random())) replaceContent(text, replaceInterval, steps + 1)
},
replaceInterval * (1 + Math.random())
)
} else { } else {
emit('finish') emit('finish')
} }
} }
function scramble(scrambleAmount = props.scrambleAmount, replaceInterval = props.replaceInterval, fillInterval = props.fillInterval, characterSet = props.characterSet, text = props.text, fillText = props.text) { function scramble(
scrambleAmount = props.scrambleAmount,
replaceInterval = props.replaceInterval,
fillInterval = props.fillInterval,
characterSet = props.characterSet,
text = props.text,
fillText = props.text
) {
content.value = '' content.value = ''
const spec = props.resize && (Math.random() > 0.99) const spec = props.resize && Math.random() > 0.99
let i = 0 let i = 0
const specChars = atob('S09TUk8tRUFTVEVSRUdH') const specChars = atob('S09TUk8tRUFTVEVSRUdH')
const fillContent = function() { const fillContent = function () {
if (content.value.length < text.length) { if (content.value.length < text.length) {
const char = fillText.charAt(content.value.length) || '' const char = fillText.charAt(content.value.length) || ''
if (spec) { if (spec) {
@@ -127,16 +138,18 @@ onMounted(() => {
} }
}) })
watch(() => props.text, () => { watch(
if (content.value === '') { () => props.text,
scramble() () => {
} else { if (content.value === '') {
replaceContent() scramble()
} else {
replaceContent()
}
} }
}) )
</script> </script>
<template> <template>
<span @mouseenter="scrambleOnHover && scramble">{{ content }}</span> <span @mouseenter="scrambleOnHover && scramble">{{ content }}</span>
</template> </template>

View File

@@ -1,28 +1,37 @@
<template> <template>
<div class="flex flex-col px-8 my-4"> <div class="my-4 flex flex-col px-8">
<span class="text-sm text-muted-foreground font-mono">{{ label }}</span> <span class="font-mono text-sm text-muted-foreground">{{ label }}</span>
<Slider <Slider ref="steppedSlider" v-model="sliderModelValue" :max="max" :step="1" class="pt-4" />
ref="steppedSlider" v-model="sliderModelValue" :max="max" :step="1"
class="pt-4" />
<div class="flex justify-between py-2"> <div class="flex justify-between py-2">
<button <button
v-for="(position, index) in positions" :key="position" v-for="(position, index) in positions"
class="min-w-0 text-nowrap group" :key="position"
class="group min-w-0 text-nowrap"
:class="{ :class="{
'slider-start mr-4': index===0, 'slider-start mr-4': index === 0,
'slider-center': index > 0 && index < positions.length-1, 'slider-center': index > 0 && index < positions.length - 1,
'slider-end ml-4': index === positions.length-1}" 'slider-end ml-4': index === positions.length - 1
@click="value = position.value"> }"
@click="value = position.value"
>
<span <span
v-if="index===0" class="rounded-full w-2 h-1.5 inline-block mb-[1px] transition-colors" v-if="index === 0"
:class="value===position.value ? 'bg-zinc-100' : 'bg-zinc-600 group-hover:bg-zinc-500'" /> class="mb-[1px] inline-block h-1.5 w-2 rounded-full transition-colors"
:class="value === position.value ? 'bg-zinc-100' : 'bg-zinc-600 group-hover:bg-zinc-500'"
/>
<span <span
v-if="position.label" class="text-xs font-mono uppercase mx-1 transition-colors" v-if="position.label"
:class="value===position.value ? 'text-zinc-100' : 'text-zinc-600 group-hover:text-zinc-500'">{{ position.label }}</span> class="mx-1 font-mono text-xs uppercase transition-colors"
:class="
value === position.value ? 'text-zinc-100' : 'text-zinc-600 group-hover:text-zinc-500'
"
>{{ position.label }}</span
>
<span <span
v-if="!position.label || index === positions.length-1" v-if="!position.label || index === positions.length - 1"
class="rounded-full w-2 h-1.5 inline-block mb-[1px] transition-colors" class="mb-[1px] inline-block h-1.5 w-2 rounded-full transition-colors"
:class="value===position.value ? 'bg-zinc-100' : 'bg-zinc-600 group-hover:bg-zinc-500'" /> :class="value === position.value ? 'bg-zinc-100' : 'bg-zinc-600 group-hover:bg-zinc-500'"
/>
</button> </button>
</div> </div>
</div> </div>
@@ -33,45 +42,45 @@ import { computed } from 'vue'
const value = defineModel({ const value = defineModel({
type: Number, type: Number,
default: 0, default: 0
}) })
const sliderModelValue = computed({ const sliderModelValue = computed({
get: () => [value.value], get: () => [value.value],
set: (val) => { set: (val) => {
value.value = val[0] value.value = val[0]
}, }
}) })
const props = defineProps({ const props = defineProps({
label: { label: {
type: String, type: String,
default: null, default: null
}, },
max: { max: {
type: Number, type: Number,
default: 4, default: 4
}, },
namedPositions: { namedPositions: {
type: Array, type: Array,
default: () => [ default: () => [
{ {
label: 'Min', label: 'Min',
value: 0, value: 0
}, },
{ {
value: 2, value: 2
}, },
{ {
label: 'Max', label: 'Max',
value: 4, value: 4
}, }
], ]
}, },
autoMarkers: { autoMarkers: {
type: Boolean, type: Boolean,
default: true, default: true
}, }
}) })
const positions = computed(() => { const positions = computed(() => {

View File

@@ -1,14 +1,17 @@
<template> <template>
<div class="p-2 border-solid border-0 border-b"> <div class="border-0 border-b border-solid p-2">
<div class="flex rounded-lg overflow-hidden border border-zinc-800"> <div class="flex overflow-hidden rounded-lg border border-zinc-800">
<TransitionGroup name="flex"> <TransitionGroup name="flex">
<TabSelectButton <TabSelectButton
v-for="(option, key) in options" :key="key" v-for="(option, key) in options"
:ref="(el) => buttons[key] = el" :key="key"
:ref="(el) => (buttons[key] = el)"
:title="$t(option.titleKey)" :title="$t(option.titleKey)"
:icon="option.icon" :selected="model===key" :icon="option.icon"
:selected="model === key"
class="min-w-0 overflow-hidden" class="min-w-0 overflow-hidden"
@select="model=key"> @select="model = key"
>
<template v-if="$slots[key]" #replace> <template v-if="$slots[key]" #replace>
<slot :name="key" /> <slot :name="key" />
</template> </template>
@@ -24,7 +27,7 @@ import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
const model = defineModel({ const model = defineModel({
type: String, type: String,
default: 'a', default: 'a'
}) })
const buttons = ref({}) const buttons = ref({})
@@ -35,7 +38,7 @@ const backgroundStyle = ref({
top: '0', top: '0',
left: '0', left: '0',
width: '0', width: '0',
height: '0', height: '0'
}) })
const updateBackgroundStyle = () => { const updateBackgroundStyle = () => {
@@ -45,7 +48,7 @@ const updateBackgroundStyle = () => {
top: `${selected.$el.offsetTop}px`, top: `${selected.$el.offsetTop}px`,
left: `${selected.$el.offsetLeft}px`, left: `${selected.$el.offsetLeft}px`,
width: `${selected.$el.offsetWidth}px`, width: `${selected.$el.offsetWidth}px`,
height: `${selected.$el.offsetHeight}px`, height: `${selected.$el.offsetHeight}px`
} }
} }
} }
@@ -72,9 +75,9 @@ defineProps({
default: () => ({ default: () => ({
a: { titleKey: 'Option A', icon: CircleDot }, a: { titleKey: 'Option A', icon: CircleDot },
b: { titleKey: 'Option B', icon: CircleDot }, b: { titleKey: 'Option B', icon: CircleDot },
c: { titleKey: 'Option C', icon: CircleDot }, c: { titleKey: 'Option C', icon: CircleDot }
}), })
}, }
}) })
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,18 +1,33 @@
<template> <template>
<button <button
class="flex-1 flex flex-col items-center rounded-lg p-2 gap-2 font-heading transition-all border" class="font-heading flex flex-1 flex-col items-center gap-2 rounded-lg border p-2 transition-all"
:class="{'text-black bg-zinc-300 hover:bg-zinc-200 border-zinc-100': selected, :class="{
'hover:bg-zinc-800 text-muted-foreground border-transparent' : !selected}" 'border-zinc-100 bg-zinc-300 text-black hover:bg-zinc-200': selected,
@click="$emit('select'); $refs.title?.scramble()"> 'border-transparent text-muted-foreground hover:bg-zinc-800': !selected
}"
@click="
() => {
$emit('select')
$refs.title?.scramble()
}
"
>
<slot v-if="$slots['replace']" name="replace" /> <slot v-if="$slots['replace']" name="replace" />
<template v-else> <template v-else>
<img <img
v-if="icon" v-if="icon"
draggable="false" draggable="false"
:src="icon" :alt="title" :src="icon"
:alt="title"
class="h-16" class="h-16"
:class="{'invert': selected}"> :class="{ invert: selected }"
<ScrambleText ref="title" :resize="false" class="text-xs text-wrap line-clamp-2 text-ellipsis overflow-hidden" :text="title" /> />
<ScrambleText
ref="title"
:resize="false"
class="line-clamp-2 overflow-hidden text-ellipsis text-wrap text-xs"
:text="title"
/>
</template> </template>
</button> </button>
</template> </template>
@@ -24,15 +39,15 @@ defineEmits(['select'])
defineProps({ defineProps({
title: { title: {
type: String, type: String,
default: '', default: ''
}, },
icon: { icon: {
type: [String, Object, Function], type: [String, Object, Function],
default: null, default: null
}, },
selected: { selected: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}) })
</script> </script>

View File

@@ -5,7 +5,8 @@
v-if="showTabs" v-if="showTabs"
v-model="configPage" v-model="configPage"
:options="configPages" :options="configPages"
class="p-2 border solid border-b bg-zinc-900"> class="solid border bg-zinc-900 p-2"
>
<template v-for="(page, key) in configPages" #[key] :key="key"> <template v-for="(page, key) in configPages" #[key] :key="key">
<ScrambleText ref="title" :text="$t(page.titleKey)" /> <ScrambleText ref="title" :text="$t(page.titleKey)" />
</template> </template>
@@ -15,13 +16,14 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div class="flex grow justify-center items-center text-muted-foreground pb-16"> <div class="flex grow items-center justify-center pb-16 text-muted-foreground">
<ChevronLeft class="h-5 mb-0.5 inline-block" /> <ChevronLeft class="mb-0.5 inline-block h-5" />
<ScrambleText <ScrambleText
scramble-on-mount scramble-on-mount
:fill-interval="5" :fill-interval="5"
:replace-interval="5" :replace-interval="5"
text="Select a profile first" /> text="Select a profile first"
/>
</div> </div>
</template> </template>
</div> </div>
@@ -38,13 +40,13 @@ const store = useStore()
const configPages = computed(() => store.currentConfigPages) const configPages = computed(() => store.currentConfigPages)
const configPage = computed({ const configPage = computed({
get: () => store.currentConfigPage, get: () => store.currentConfigPage,
set: (value) => store.setCurrentConfigPage(value), set: (value) => store.setCurrentConfigPage(value)
}) })
defineProps({ defineProps({
showTabs: { showTabs: {
type: Boolean, type: Boolean,
default: true, default: true
}, }
}) })
</script> </script>

View File

@@ -1,41 +1,51 @@
<template> <template>
<ConfigSection <ConfigSection
:title="$t('config_options.feedback_designer.feedback_type.title')" :title="$t('config_options.feedback_designer.feedback_type.title')"
:icon-component="GaugeCircle"> :icon-component="GaugeCircle"
>
<TabSelect v-model="feedbackType" :options="feedbackTypeOptions" /> <TabSelect v-model="feedbackType" :options="feedbackTypeOptions" />
</ConfigSection> </ConfigSection>
<ConfigSection <ConfigSection
:title="$t('config_options.feedback_designer.haptic_response.title')" :title="$t('config_options.feedback_designer.haptic_response.title')"
:icon-component="AudioWaveform" :icon-component="AudioWaveform"
:show-toggle="true"> :show-toggle="true"
>
<SteppedSlider <SteppedSlider
v-model="feedbackStrength" v-model="feedbackStrength"
:label="$t('config_options.feedback_designer.haptic_response.feedback_strength')" /> :label="$t('config_options.feedback_designer.haptic_response.feedback_strength')"
/>
<Separator /> <Separator />
<SteppedSlider <SteppedSlider
v-model="bounceBackStrength" v-model="bounceBackStrength"
:label="$t('config_options.feedback_designer.haptic_response.bounce_back_strength')" /> :label="$t('config_options.feedback_designer.haptic_response.bounce_back_strength')"
/>
<Separator /> <Separator />
<SteppedSlider <SteppedSlider
v-model="outputRampDampening" v-model="outputRampDampening"
:label="$t('config_options.feedback_designer.haptic_response.output_ramp_dampening')" /> :label="$t('config_options.feedback_designer.haptic_response.output_ramp_dampening')"
/>
</ConfigSection> </ConfigSection>
<ConfigSection <ConfigSection
:title="$t('config_options.feedback_designer.auditory_response.title')" :title="$t('config_options.feedback_designer.auditory_response.title')"
:icon-component="AudioLines" :show-toggle="true"> :icon-component="AudioLines"
:show-toggle="true"
>
<SteppedSlider <SteppedSlider
v-model="auditoryHapticLevel" v-model="auditoryHapticLevel"
:label="$t('config_options.feedback_designer.auditory_response.haptic_level')" /> :label="$t('config_options.feedback_designer.auditory_response.haptic_level')"
/>
<Separator /> <Separator />
<SteppedSlider <SteppedSlider
v-model="auditoryMagnitude" v-model="auditoryMagnitude"
:label="$t('config_options.feedback_designer.auditory_response.magnitude')" :label="$t('config_options.feedback_designer.auditory_response.magnitude')"
:max="3" :max="3"
:named-positions="[ :named-positions="[
{value:0, label: 'Faint'}, { value: 0, label: 'Faint' },
{value:1, label: 'Soft'}, { value: 1, label: 'Soft' },
{value:2, label: 'Normal'}, { value: 2, label: 'Normal' },
{value:3, label: 'Loud'}]" /> { value: 3, label: 'Loud' }
]"
/>
</ConfigSection> </ConfigSection>
</template> </template>
<script setup> <script setup>
@@ -55,20 +65,20 @@ const feedbackType = ref('fineDetents') // TODO: replace with actual value
const feedbackTypeOptions = { const feedbackTypeOptions = {
fineDetents: { fineDetents: {
icon: FdIcon, icon: FdIcon,
titleKey: 'config_options.feedback_designer.feedback_type.fine_detents', titleKey: 'config_options.feedback_designer.feedback_type.fine_detents'
}, },
coarseDetents: { coarseDetents: {
icon: CdIcon, icon: CdIcon,
titleKey: 'config_options.feedback_designer.feedback_type.coarse_detents', titleKey: 'config_options.feedback_designer.feedback_type.coarse_detents'
}, },
viscousRotation: { viscousRotation: {
icon: VfIcon, icon: VfIcon,
titleKey: 'config_options.feedback_designer.feedback_type.viscous_rotation', titleKey: 'config_options.feedback_designer.feedback_type.viscous_rotation'
}, },
returnToCenter: { returnToCenter: {
icon: RcIcon, icon: RcIcon,
titleKey: 'config_options.feedback_designer.feedback_type.return_to_center', titleKey: 'config_options.feedback_designer.feedback_type.return_to_center'
}, }
} }
const feedbackStrength = ref(2) const feedbackStrength = ref(2)

View File

@@ -13,11 +13,11 @@ import { ref } from 'vue'
const keyColors = ref({ const keyColors = ref({
default: { default: {
titleKey: 'default', titleKey: 'default',
color: Color('#4f25ef'), color: Color('#4f25ef')
}, },
pressed: { pressed: {
titleKey: 'pressed', titleKey: 'pressed',
color: Color('#d0078f'), color: Color('#d0078f')
}, }
}) })
</script> </script>

View File

@@ -1,8 +1,10 @@
<template> <template>
<ConfigSection title="Key Mapping" :icon-component="PlusSquare"> <ConfigSection title="Key Mapping" :icon-component="PlusSquare">
<template #title><span class="text-zinc-500"> ({{ store.selectedKey}})</span></template> <template #title
<div class="px-8 my-4"> ><span class="text-zinc-500"> ({{ store.selectedKey }})</span></template
<span class="text-sm text-muted-foreground font-mono">Action:</span> >
<div class="my-4 px-8">
<span class="font-mono text-sm text-muted-foreground">Action:</span>
<Popover v-model:open="open"> <Popover v-model:open="open">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Button <Button
@@ -10,18 +12,17 @@
variant="outline" variant="outline"
role="combobox" role="combobox"
:aria-expanded="open" :aria-expanded="open"
class="my-2 w-full justify-between"> class="my-2 w-full justify-between"
>
<ScrambleText :text="value ? actionOptions[value] : 'Select an action...'" /> <ScrambleText :text="value ? actionOptions[value] : 'Select an action...'" />
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent class="p-0" :style="{width: $refs.comboboxButton?.$el.offsetWidth}"> <PopoverContent class="p-0" :style="{ width: $refs.comboboxButton?.$el.offsetWidth }">
<Command> <Command>
<CommandInput class="h-9" placeholder="Search actions..." /> <CommandInput class="h-9" placeholder="Search actions..." />
<CommandEmpty> <CommandEmpty>
<ScrambleText <ScrambleText scramble-on-mount text="No actions found." />
scramble-on-mount
text="No actions found." />
</CommandEmpty> </CommandEmpty>
<CommandList> <CommandList>
<CommandGroup> <CommandGroup>
@@ -29,13 +30,17 @@
v-for="(action, key) in actionOptions" v-for="(action, key) in actionOptions"
:key="key" :key="key"
:value="action" :value="action"
@select="() => { @select="
value = key () => {
open = false value = key
}"> open = false
}
"
>
{{ action }} {{ action }}
<Check <Check
:class="cn('ml-auto h-4 w-4',value === key ? 'opacity-100' : 'opacity-0')" /> :class="cn('ml-auto h-4 w-4', value === key ? 'opacity-100' : 'opacity-0')"
/>
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
@@ -52,7 +57,14 @@ import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import WIP from '@renderer/components/WIP.vue' import WIP from '@renderer/components/WIP.vue'
import { Popover, PopoverTrigger, PopoverContent } from '@renderer/components/ui/popover' import { Popover, PopoverTrigger, PopoverContent } from '@renderer/components/ui/popover'
import { Button } from '@renderer/components/ui/button' import { Button } from '@renderer/components/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@renderer/components/ui/command' import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from '@renderer/components/ui/command'
import { ref } from 'vue' import { ref } from 'vue'
import { cn } from '@renderer/lib/utils' import { cn } from '@renderer/lib/utils'
import ScrambleText from '@renderer/components/common/ScrambleText.vue' import ScrambleText from '@renderer/components/common/ScrambleText.vue'
@@ -70,12 +82,11 @@ const actionOptions = ref({
sendSerial: 'Send a Serial Message', sendSerial: 'Send a Serial Message',
controlMedia: 'Control Media Playback', controlMedia: 'Control Media Playback',
controlSystem: 'Control your OS', controlSystem: 'Control your OS',
runProgram: 'Start a Program', runProgram: 'Start a Program'
}) })
const comboboxButton = ref(null) const comboboxButton = ref(null)
const open = ref(false) const open = ref(false)
const value = ref('') const value = ref('')
</script> </script>

View File

@@ -1,41 +1,51 @@
<template> <template>
<ConfigSection <ConfigSection
:title="$t('config_options.feedback_designer.feedback_type.title')" :title="$t('config_options.feedback_designer.feedback_type.title')"
:icon-component="GaugeCircle"> :icon-component="GaugeCircle"
>
<TabSelect v-model="feedbackType" :options="feedbackTypeOptions" /> <TabSelect v-model="feedbackType" :options="feedbackTypeOptions" />
</ConfigSection> </ConfigSection>
<ConfigSection <ConfigSection
:title="$t('config_options.feedback_designer.haptic_response.title')" :title="$t('config_options.feedback_designer.haptic_response.title')"
:icon-component="AudioWaveform" :icon-component="AudioWaveform"
:show-toggle="true"> :show-toggle="true"
>
<SteppedSlider <SteppedSlider
v-model="feedbackStrength" v-model="feedbackStrength"
:label="$t('config_options.feedback_designer.haptic_response.feedback_strength')" /> :label="$t('config_options.feedback_designer.haptic_response.feedback_strength')"
/>
<Separator /> <Separator />
<SteppedSlider <SteppedSlider
v-model="bounceBackStrength" v-model="bounceBackStrength"
:label="$t('config_options.feedback_designer.haptic_response.bounce_back_strength')" /> :label="$t('config_options.feedback_designer.haptic_response.bounce_back_strength')"
/>
<Separator /> <Separator />
<SteppedSlider <SteppedSlider
v-model="outputRampDampening" v-model="outputRampDampening"
:label="$t('config_options.feedback_designer.haptic_response.output_ramp_dampening')" /> :label="$t('config_options.feedback_designer.haptic_response.output_ramp_dampening')"
/>
</ConfigSection> </ConfigSection>
<ConfigSection <ConfigSection
:title="$t('config_options.feedback_designer.auditory_response.title')" :title="$t('config_options.feedback_designer.auditory_response.title')"
:icon-component="AudioLines" :show-toggle="true"> :icon-component="AudioLines"
:show-toggle="true"
>
<SteppedSlider <SteppedSlider
v-model="auditoryHapticLevel" v-model="auditoryHapticLevel"
:label="$t('config_options.feedback_designer.auditory_response.haptic_level')" /> :label="$t('config_options.feedback_designer.auditory_response.haptic_level')"
/>
<Separator /> <Separator />
<SteppedSlider <SteppedSlider
v-model="auditoryMagnitude" v-model="auditoryMagnitude"
:label="$t('config_options.feedback_designer.auditory_response.magnitude')" :label="$t('config_options.feedback_designer.auditory_response.magnitude')"
:max="3" :max="3"
:named-positions="[ :named-positions="[
{value:0, label: 'Faint'}, { value: 0, label: 'Faint' },
{value:1, label: 'Soft'}, { value: 1, label: 'Soft' },
{value:2, label: 'Normal'}, { value: 2, label: 'Normal' },
{value:3, label: 'Loud'}]" /> { value: 3, label: 'Loud' }
]"
/>
</ConfigSection> </ConfigSection>
</template> </template>
<script setup> <script setup>
@@ -55,20 +65,20 @@ const feedbackType = ref('fineDetents') // TODO: replace with actual value
const feedbackTypeOptions = { const feedbackTypeOptions = {
fineDetents: { fineDetents: {
icon: FdIcon, icon: FdIcon,
titleKey: 'config_options.feedback_designer.feedback_type.fine_detents', titleKey: 'config_options.feedback_designer.feedback_type.fine_detents'
}, },
coarseDetents: { coarseDetents: {
icon: CdIcon, icon: CdIcon,
titleKey: 'config_options.feedback_designer.feedback_type.coarse_detents', titleKey: 'config_options.feedback_designer.feedback_type.coarse_detents'
}, },
viscousRotation: { viscousRotation: {
icon: VfIcon, icon: VfIcon,
titleKey: 'config_options.feedback_designer.feedback_type.viscous_rotation', titleKey: 'config_options.feedback_designer.feedback_type.viscous_rotation'
}, },
returnToCenter: { returnToCenter: {
icon: RcIcon, icon: RcIcon,
titleKey: 'config_options.feedback_designer.feedback_type.return_to_center', titleKey: 'config_options.feedback_designer.feedback_type.return_to_center'
}, }
} }
const feedbackStrength = ref(2) const feedbackStrength = ref(2)

View File

@@ -13,15 +13,15 @@ import { ref } from 'vue'
const ringColors = ref({ const ringColors = ref({
primary: { primary: {
titleKey: 'config_options.light_designer.primary_color', titleKey: 'config_options.light_designer.primary_color',
color: Color('#8f9af2'), color: Color('#8f9af2')
}, },
secondary: { secondary: {
titleKey: 'config_options.light_designer.secondary_color', titleKey: 'config_options.light_designer.secondary_color',
color: Color('#c06300'), color: Color('#c06300')
}, },
pointer: { pointer: {
titleKey: 'config_options.light_designer.pointer_color', titleKey: 'config_options.light_designer.pointer_color',
color: Color('#ffa346'), color: Color('#ffa346')
}, }
}) })
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<ConfigSection title="Knob Mapping" :icon-component="PlusCircle"> <ConfigSection title="Knob Mapping" :icon-component="PlusCircle">
<div class="px-8 my-4"> <div class="my-4 px-8">
<span class="text-sm text-muted-foreground font-mono">Control:</span> <span class="font-mono text-sm text-muted-foreground">Control:</span>
<Popover v-model:open="open"> <Popover v-model:open="open">
<PopoverTrigger as-child> <PopoverTrigger as-child>
<Button <Button
@@ -9,18 +9,17 @@
variant="outline" variant="outline"
role="combobox" role="combobox"
:aria-expanded="open" :aria-expanded="open"
class="my-2 w-full justify-between"> class="my-2 w-full justify-between"
>
<ScrambleText :text="value ? knobMappingOptions[value] : 'Select an action...'" /> <ScrambleText :text="value ? knobMappingOptions[value] : 'Select an action...'" />
<ChevronsUpDown class="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent class="p-0" :style="{width: $refs.comboboxButton?.$el.offsetWidth}"> <PopoverContent class="p-0" :style="{ width: $refs.comboboxButton?.$el.offsetWidth }">
<Command> <Command>
<CommandInput class="h-9" placeholder="Search actions..." /> <CommandInput class="h-9" placeholder="Search actions..." />
<CommandEmpty> <CommandEmpty>
<ScrambleText <ScrambleText scramble-on-mount text="No actions found." />
scramble-on-mount
text="No actions found." />
</CommandEmpty> </CommandEmpty>
<CommandList> <CommandList>
<CommandGroup> <CommandGroup>
@@ -28,13 +27,17 @@
v-for="(action, key) in knobMappingOptions" v-for="(action, key) in knobMappingOptions"
:key="key" :key="key"
:value="action" :value="action"
@select="() => { @select="
value = key () => {
open = false value = key
}"> open = false
}
"
>
{{ action }} {{ action }}
<Check <Check
:class="cn('ml-auto h-4 w-4',value === key ? 'opacity-100' : 'opacity-0')" /> :class="cn('ml-auto h-4 w-4', value === key ? 'opacity-100' : 'opacity-0')"
/>
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
@@ -51,7 +54,14 @@ import ConfigSection from '@renderer/components/common/ConfigSection.vue'
import WIP from '@renderer/components/WIP.vue' import WIP from '@renderer/components/WIP.vue'
import { Popover, PopoverTrigger, PopoverContent } from '@renderer/components/ui/popover' import { Popover, PopoverTrigger, PopoverContent } from '@renderer/components/ui/popover'
import { Button } from '@renderer/components/ui/button' import { Button } from '@renderer/components/ui/button'
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@renderer/components/ui/command' import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from '@renderer/components/ui/command'
import { ref } from 'vue' import { ref } from 'vue'
import { cn } from '@renderer/lib/utils' import { cn } from '@renderer/lib/utils'
import ScrambleText from '@renderer/components/common/ScrambleText.vue' import ScrambleText from '@renderer/components/common/ScrambleText.vue'
@@ -62,7 +72,7 @@ const knobMappingOptions = ref({
controlOsc: 'Control an OSC Value', controlOsc: 'Control an OSC Value',
controlVolume: 'Control your OS Volume', controlVolume: 'Control your OS Volume',
moveMouse: 'Move the Mouse', moveMouse: 'Move the Mouse',
scrollMouse: 'Scroll the Mouse', scrollMouse: 'Scroll the Mouse'
}) })
const comboboxButton = ref(null) const comboboxButton = ref(null)

View File

@@ -8,11 +8,11 @@ const bar = ref(null)
const props = defineProps({ const props = defineProps({
width: { type: Number, default: 160 }, width: { type: Number, default: 160 },
count: { type: Number, default: 40 }, count: { type: Number, default: 40 },
gapWidth: { type: Number, default: 2 }, gapWidth: { type: Number, default: 2 }
}) })
const rectWidth = computed(() => { const rectWidth = computed(() => {
return (props.width - ((props.count + 1) * props.gapWidth)) / props.count return (props.width - (props.count + 1) * props.gapWidth) / props.count
}) })
const currentPosition = computed(() => { const currentPosition = computed(() => {
return Math.round((model.value / 100) * (props.count - 1)) return Math.round((model.value / 100) * (props.count - 1))
@@ -25,7 +25,10 @@ function onMouseDown() {
function onMouseMove(e) { function onMouseMove(e) {
const rect = bar.value.getBoundingClientRect() const rect = bar.value.getBoundingClientRect()
model.value = Math.max(0, Math.min(Math.round((e.clientX - rect.left - 9) / (props.width - 6) * 100), 100)) model.value = Math.max(
0,
Math.min(Math.round(((e.clientX - rect.left - 9) / (props.width - 6)) * 100), 100)
)
} }
function onMouseUp() { function onMouseUp() {
@@ -35,48 +38,25 @@ function onMouseUp() {
</script> </script>
<template> <template>
<span @mousedown="onMouseDown"> <span @mousedown="onMouseDown">
<svg ref="bar" :width="width+12" height="32"> <svg ref="bar" :width="width + 12" height="32">
<g> <g>
<rect <rect
v-for="(_, i) in count" v-for="(_, i) in count"
:key="`key${i}`" :key="`key${i}`"
:style="`fill:${i < currentPosition ? '#fff' : '#4a4a4a'}`" :style="`fill:${i < currentPosition ? '#fff' : '#4a4a4a'}`"
:width="rectWidth" :width="rectWidth"
:height="i===0 || i===count-1 ? 8 : 5" :height="i === 0 || i === count - 1 ? 8 : 5"
:x="6+gapWidth+i*(rectWidth+gapWidth)" :x="6 + gapWidth + i * (rectWidth + gapWidth)"
y="10" /> y="10"
<g :transform="`translate(${6+(rectWidth+gapWidth)*currentPosition},0)`"> />
<rect <g :transform="`translate(${6 + (rectWidth + gapWidth) * currentPosition},0)`">
style="fill:#000" <rect style="fill: #000" :width="6" height="13" x="0" y="10" />
:width="6" <rect style="fill: #c66936" :width="2" height="11" :x="2" y="10" />
height="13" <rect style="fill: #c66936" :width="2" :height="2" x="0" y="21" />
x="0" <rect style="fill: #c66936" :width="2" :height="2" :x="4" y="21" />
y="10"
/>
<rect
style="fill:#c66936"
:width="2"
height="11"
:x="2"
y="10"
/>
<rect
style="fill:#c66936"
:width="2"
:height="2"
x="0"
y="21"
/>
<rect
style="fill:#c66936"
:width="2"
:height="2"
:x="4"
y="21"
/>
</g>
</g> </g>
</svg> </g>
</span> </svg>
</span>
</template> </template>

View File

@@ -2,14 +2,19 @@
<div class="flex"> <div class="flex">
<button <button
v-for="(color, key) in keys" v-for="(color, key) in keys"
:key="key" :class="{'outline outline-white ' : key === selected, :key="key"
'hover:outline outline-zinc-400' : key !== selected}" :class="{
class="aspect-square flex-1 rounded-[2px] flex items-center justify-center transition-all outline-2" 'outline outline-white ': key === selected,
'outline-zinc-400 hover:outline': key !== selected
}"
class="flex aspect-square flex-1 items-center justify-center rounded-[2px] outline-2 transition-all"
:style="`box-shadow: 0 3px 20px -2px ${color.hex()}`" :style="`box-shadow: 0 3px 20px -2px ${color.hex()}`"
@click="$emit('select', key)"> @click="$emit('select', key)"
>
<span <span
class="font-heading text-2xl transition-colors" class="font-heading text-2xl transition-colors"
:class="{'opacity-25 text-white': key!==selected}">{{ key }} :class="{ 'text-white opacity-25': key !== selected }"
>{{ key }}
</span> </span>
</button> </button>
</div> </div>
@@ -25,13 +30,13 @@ defineProps({
a: Color.hsl(265, 100, 50), a: Color.hsl(265, 100, 50),
b: Color.hsl(280, 100, 50), b: Color.hsl(280, 100, 50),
c: Color.hsl(300, 100, 50), c: Color.hsl(300, 100, 50),
d: Color.hsl(330, 100, 50), d: Color.hsl(330, 100, 50)
}), })
}, },
selected: { selected: {
type: String, type: String,
default: 'a', default: 'a'
}, }
}) })
defineEmits(['select']) defineEmits(['select'])

View File

@@ -2,19 +2,25 @@
<svg :viewBox="`0 0 ${size} ${size}`" filter="url(#blur)"> <svg :viewBox="`0 0 ${size} ${size}`" filter="url(#blur)">
<filter id="blur" color-interpolation-filters="sRGB"> <filter id="blur" color-interpolation-filters="sRGB">
<feGaussianBlur <feGaussianBlur
v-for="index in blurSteps" :key="index" in="SourceGraphic" :stdDeviation="blur*index" v-for="index in blurSteps"
:result="index" /> :key="index"
in="SourceGraphic"
:stdDeviation="blur * index"
:result="index"
/>
<feMerge result="blurMerge"> <feMerge result="blurMerge">
<feMergeNode v-for="index in blurSteps" :key="index" :in="index" /> <feMergeNode v-for="index in blurSteps" :key="index" :in="index" />
</feMerge> </feMerge>
</filter> </filter>
<circle <circle
v-for="index in ledCount" :key="index" v-for="index in ledCount"
:transform="`rotate(${index/ledCount*360} ${size/2} ${size/2})`" :key="index"
:transform="`rotate(${(index / ledCount) * 360} ${size / 2} ${size / 2})`"
:r="ledRadius" :r="ledRadius"
:cx="size/2" :cx="size / 2"
:cy="padding + ledRadius" :cy="padding + ledRadius"
:fill="leds[index-1]?.hex()" /> :fill="leds[index - 1]?.hex()"
/>
</svg> </svg>
</template> </template>
<script setup> <script setup>
@@ -24,8 +30,8 @@ import Color from 'color'
const props = defineProps({ const props = defineProps({
value: { value: {
type: Number, type: Number,
default: 0, default: 0
}, }
}) })
const leds = ref(Array(60).fill(Color())) const leds = ref(Array(60).fill(Color()))
@@ -43,7 +49,7 @@ let interval = null
onMounted(() => { onMounted(() => {
interval = setInterval(() => { interval = setInterval(() => {
const valueIndex = Math.floor(props.value / 100 * ledCount.value) const valueIndex = Math.floor((props.value / 100) * ledCount.value)
leds.value.forEach((color, index) => { leds.value.forEach((color, index) => {
const distance = Math.abs(index - valueIndex) % ledCount.value const distance = Math.abs(index - valueIndex) % ledCount.value
if (distance < 1) { if (distance < 1) {

View File

@@ -1,40 +1,59 @@
<template> <template>
<div class="aspect-[800/1100]"> <div class="aspect-[800/1100]">
<div <div
class="bg-contain bg-top bg-no-repeat h-full w-full relative" class="relative size-full bg-contain bg-top bg-no-repeat"
:style="{backgroundImage: `linear-gradient(to bottom, black, rgba(0,0,0,0.25) 12%, rgba(0,0,0,0.35) 95%, black), url(${previewDeviceImage})`, :style="{
backgroundBlendMode: 'multiply'}"> backgroundImage: `linear-gradient(to bottom, black, rgba(0,0,0,0.25) 12%, rgba(0,0,0,0.35) 95%, black), url(${previewDeviceImage})`,
backgroundBlendMode: 'multiply'
}"
>
<Transition name="fade"> <Transition name="fade">
<div v-if="store.connected" class="px-10 h-12 flex justify-between items-center"> <div v-if="store.connected" class="flex h-12 items-center justify-between px-10">
<h2> <h2>
<ScrambleText :delay="100" scramble-on-mount :fill-interval="50" :replace-interval="50" text="Nano_D++" /> <ScrambleText
:delay="100"
scramble-on-mount
:fill-interval="50"
:replace-interval="50"
text="Nano_D++"
/>
</h2> </h2>
<div class="font-mono text-sm"> <div class="font-mono text-sm">
<span class="text-muted-foreground">Firmware: </span> <span class="text-muted-foreground">Firmware: </span>
<ScrambleText :delay="100" scramble-on-mount :fill-interval="50" :replace-interval="50" text="v1.3.2a" /> <ScrambleText
:delay="100"
scramble-on-mount
:fill-interval="50"
:replace-interval="50"
text="v1.3.2a"
/>
</div> </div>
</div> </div>
</Transition> </Transition>
<Transition name="fade-delayed"> <Transition name="fade-delayed">
<DeviceLEDRing <DeviceLEDRing
v-if="store.connected" :value="barValue" v-if="store.connected"
class="absolute h-[66%] top-[12.5%] left-0 right-0 mx-auto" /> :value="barValue"
class="absolute inset-x-0 top-[12.5%] mx-auto h-[66%]"
/>
</Transition> </Transition>
<div <div
class="rounded-full aspect-square absolute h-[30%] top-[30.5%] left-0 right-0 mx-auto flex flex-col justify-center items-center overflow-hidden" class="absolute inset-x-0 top-[30.5%] mx-auto flex aspect-square h-[30%] flex-col items-center justify-center overflow-hidden rounded-full"
style="background: linear-gradient(45deg, black 30%, #252525 50%, #232323 60%, black)"> style="background: linear-gradient(45deg, black 30%, #252525 50%, #232323 60%, black)"
>
<TransitionGroup name="fade-display"> <TransitionGroup name="fade-display">
<div <div
v-if="store.connected" v-if="store.connected"
class="absolute flex flex-col items-center text-center pb-2 mix-blend-screen"> class="absolute flex flex-col items-center pb-2 text-center mix-blend-screen"
<img :src="LogoMidi" alt="midi-logo" class="opacity-50 h-4"> >
<img :src="LogoMidi" alt="midi-logo" class="h-4 opacity-50" />
<h2 class="font-pixellg text-5xl">{{ parseInt(value) }}</h2> <h2 class="font-pixellg text-5xl">{{ parseInt(value) }}</h2>
<div class="font-pixelsm text-md">HIGH PASS</div> <div class="font-pixelsm text-md">HIGH PASS</div>
<DeviceBar v-model="barValue" :count="30" :width="120" /> <DeviceBar v-model="barValue" :count="30" :width="120" />
<span class="w-36 font-pixelsm text-[7pt] text-muted-foreground uppercase"> <span class="font-pixelsm w-36 text-[7pt] uppercase text-muted-foreground">
KORG MINILOGUE HIGH PASS FILTER 0-127 KORG MINILOGUE HIGH PASS FILTER 0-127
</span> </span>
</div> </div>
<div v-else class="flex flex-col items-center text-center mix-blend-screen"> <div v-else class="flex flex-col items-center text-center mix-blend-screen">
<ScrambleText <ScrambleText
@@ -44,25 +63,30 @@
:delay="1000" :delay="1000"
:fill-interval="50" :fill-interval="50"
:replace-interval="50" :replace-interval="50"
class="uppercase font-pixelsm text-[7pt] text-muted-foreground" class="font-pixelsm text-[7pt] uppercase text-muted-foreground"
@finish="nextOfflineText" /> @finish="nextOfflineText"
/>
</div> </div>
</TransitionGroup> </TransitionGroup>
</div> </div>
<Transition name="fade-delayed"> <Transition name="fade-delayed">
<button <button
v-if="store.connected" v-if="store.connected"
class="rounded-full outline-2 absolute h-[41.5%] top-[24.5%] aspect-square left-0 right-0 mx-auto transition-all" 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', :class="{
'hover:outline outline-zinc-400': store.selectedFeature!=='knob'}" 'outline outline-white': store.selectedFeature === 'knob',
@click="store.selectConfigFeature('knob')" /> 'outline-zinc-400 hover:outline': store.selectedFeature !== 'knob'
}"
@click="store.selectConfigFeature('knob')"
/>
</Transition> </Transition>
<Transition name="fade-delayed"> <Transition name="fade-delayed">
<DeviceKeys <DeviceKeys
v-if="store.connected" v-if="store.connected"
class="absolute w-[72.7%] top-[77.5%] gap-[2.2%] left-0 right-0 mx-auto" class="absolute inset-x-0 top-[77.5%] mx-auto w-[72.7%] gap-[2.2%]"
:selected="store.selectedFeature === 'key' ? store.selectedKey : ''" :selected="store.selectedFeature === 'key' ? store.selectedKey : ''"
@select="store.selectKey" /> @select="store.selectKey"
/>
</Transition> </Transition>
</div> </div>
</div> </div>
@@ -81,16 +105,18 @@ import DeviceKeys from '@renderer/components/device/DeviceKeys.vue'
const value = ref(69) const value = ref(69)
const barValue = computed(() => value.value / 127 * 100) const barValue = computed(() => (value.value / 127) * 100)
const store = useStore() const store = useStore()
const previewDeviceImages = { const previewDeviceImages = {
nanoOne: RenderNanoOne, nanoOne: RenderNanoOne,
nanoZero: RenderNanoZero, nanoZero: RenderNanoZero
} }
const previewDeviceImage = computed(() => previewDeviceImages[store.previewDeviceModel || 'nanoOne']) const previewDeviceImage = computed(
() => previewDeviceImages[store.previewDeviceModel || 'nanoOne']
)
const targetValue = ref(69) const targetValue = ref(69)
const animateValue = () => { const animateValue = () => {
@@ -112,7 +138,7 @@ const offlineTexts = [
'AWAITING CONNECTION', 'AWAITING CONNECTION',
'DEVICE OFFLINE', 'DEVICE OFFLINE',
'NAP TIME', 'NAP TIME',
'NO DEVICE CONNECTED', 'NO DEVICE CONNECTED'
] ]
let offlineTextIndex = 0 let offlineTextIndex = 0

View File

@@ -1,6 +1,6 @@
<template> <template>
<button <button
class="app-titlebar-button text-muted-foreground flex items-center rounded-sm px-3 py-1 text-sm font-medium hover:bg-accent hover:text-accent-foreground" class="app-titlebar-button flex items-center rounded-sm px-3 py-1 text-sm font-medium text-muted-foreground hover:bg-accent hover:text-accent-foreground"
> >
<slot /> <slot />
</button> </button>

View File

@@ -1,23 +1,38 @@
<template> <template>
<div class="flex app-titlebar"> <div class="app-titlebar flex">
<Menubar class="w-full h-14 rounded-none bg-zinc-900 justify-between text-muted-foreground font-mono px-3"> <Menubar
<div v-if="isMacOS" :style="{width: 80 / zoomFactor + 'px'}" /> class="h-14 w-full justify-between rounded-none bg-zinc-900 px-3 font-mono text-muted-foreground"
>
<div v-if="isMacOS" :style="{ width: 80 / zoomFactor + 'px' }" />
<div class="flex items-center"> <div class="flex items-center">
<h1 <h1
class="text-2xl min-w-32 app-titlebar-button text-zinc-100 text-nowrap" class="app-titlebar-button min-w-32 text-nowrap text-2xl text-zinc-100"
@click="$refs.zerooneTitle.scramble(1,100,0); $refs.zerooneSubtitle.scramble(1,75,30)"> @click="
() => {
$refs.zerooneTitle.scramble(1, 100, 0)
$refs.zerooneSubtitle.scramble(1, 75, 30)
}
"
>
<ScrambleText <ScrambleText
ref="zerooneTitle" ref="zerooneTitle"
text=" ZERO/ONE" scramble-on-mount :scramble-amount="1" :fill-interval="100" text=" ZERO/ONE"
scramble-on-mount
:scramble-amount="1"
:fill-interval="100"
:replace-interval="100" :replace-interval="100"
/> />
</h1> </h1>
<h2 class="text-sm min-w-[188px] text-muted-foreground font-mono text-nowrap"> <h2 class="min-w-[188px] text-nowrap font-mono text-sm text-muted-foreground">
:: ::
<ScrambleText <ScrambleText
ref="zerooneSubtitle" ref="zerooneSubtitle"
text="Configuration Suite" scramble-on-mount :scramble-amount="1" :fill-interval="35" text="Configuration Suite"
:replace-interval="40" /> scramble-on-mount
:scramble-amount="1"
:fill-interval="35"
:replace-interval="40"
/>
</h2> </h2>
</div> </div>
<div class="h-8 px-2"> <div class="h-8 px-2">
@@ -26,12 +41,10 @@
<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="store.numAttachedDevices !== 1">
Devices<span class="text-zinc-500">&nbsp;({{ ""+store.numAttachedDevices }})</span> Devices<span class="text-zinc-500">&nbsp;({{ '' + store.numAttachedDevices }})</span>
</template>
<template v-else>
Device
</template> </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 -->
@@ -39,7 +52,8 @@
{{ store.connected ? $t('navbar.device.disconnect') : $t('navbar.device.connect') }} {{ store.connected ? $t('navbar.device.disconnect') : $t('navbar.device.connect') }}
<MenubarShortcut>D</MenubarShortcut> <MenubarShortcut>D</MenubarShortcut>
</MenubarItem> </MenubarItem>
<MenubarItem v-if="store.multipleDevicesConnected">Next Device <MenubarItem v-if="store.multipleDevicesConnected"
>Next Device
<MenubarShortcut>N</MenubarShortcut> <MenubarShortcut>N</MenubarShortcut>
</MenubarItem> </MenubarItem>
<MenubarSeparator /> <MenubarSeparator />
@@ -55,19 +69,25 @@
<MenubarShortcut>S</MenubarShortcut> <MenubarShortcut>S</MenubarShortcut>
</MenubarItem> </MenubarItem>
<MenubarSeparator /> <MenubarSeparator />
<MenubarItem>{{ $t('navbar.device.export') }} <MenubarItem
>{{ $t('navbar.device.export') }}
<MenubarShortcut>E</MenubarShortcut> <MenubarShortcut>E</MenubarShortcut>
</MenubarItem> </MenubarItem>
<MenubarItem>{{ $t('navbar.device.import') }} <MenubarItem
>{{ $t('navbar.device.import') }}
<MenubarShortcut>I</MenubarShortcut> <MenubarShortcut>I</MenubarShortcut>
</MenubarItem> </MenubarItem>
<MenubarSeparator /> <MenubarSeparator />
<MenubarItem>{{ $t('navbar.device.quit') }} <MenubarItem
>{{ $t('navbar.device.quit') }}
<MenubarShortcut>Q</MenubarShortcut> <MenubarShortcut>Q</MenubarShortcut>
</MenubarItem> </MenubarItem>
</MenubarContent> </MenubarContent>
</MenubarMenu> </MenubarMenu>
<MenubarButton class="app-titlebar-button" @click="electron?.openExternal('https://discord.gg/jgRd77YN5T')"> <MenubarButton
class="app-titlebar-button"
@click="electron?.openExternal('https://discord.gg/jgRd77YN5T')"
>
Community Community
</MenubarButton> </MenubarButton>
<MenubarMenu> <MenubarMenu>
@@ -96,27 +116,31 @@
<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="store.setConnected(!store.connected)"
>
{{ store.connected ? 'Disconnect' : 'Connect' }} {{ store.connected ? 'Disconnect' : 'Connect' }}
</MenubarButton> </MenubarButton>
<div v-if="!isMacOS" class="flex h-full"> <div v-if="!isMacOS" class="flex h-full">
<button <button
v-if="minimizable" v-if="minimizable"
class="grow flex justify-center items-center app-titlebar-button hover:text-white px-2" class="app-titlebar-button flex grow items-center justify-center px-2 hover:text-white"
@click="electron?.minimizeWindow"> @click="electron?.minimizeWindow"
<Minus class="h-5 w-5" /> >
<Minus class="size-5" />
</button> </button>
<button <button
v-if="maximizable" v-if="maximizable"
class="grow flex justify-center items-center app-titlebar-button hover:text-white px-2" class="app-titlebar-button flex grow items-center justify-center px-2 hover:text-white"
@click="electron?.toggleMaximizeWindow"> @click="electron?.toggleMaximizeWindow"
<Copy v-if="isMaximized" class="h-4 w-4" /> >
<Square v-else class="h-3.5 w-3.5 mr-0.5" /> <Copy v-if="isMaximized" class="size-4" />
<Square v-else class="mr-0.5 size-3.5" />
</button> </button>
<button <button
class="grow flex justify-center items-center app-titlebar-button hover:text-white px-2" class="app-titlebar-button flex grow items-center justify-center px-2 hover:text-white"
@click="electron?.closeWindow"> @click="electron?.closeWindow"
<X class="h-5 w-5 mr-0.5" /> >
<X class="mr-0.5 size-5" />
</button> </button>
</div> </div>
</Menubar> </Menubar>
@@ -130,7 +154,7 @@ import {
MenubarMenu, MenubarMenu,
MenubarSeparator, MenubarSeparator,
MenubarShortcut, MenubarShortcut,
MenubarTrigger, MenubarTrigger
} from '@renderer/components/ui/menubar' } from '@renderer/components/ui/menubar'
import ScrambleText from '@renderer/components/common/ScrambleText.vue' import ScrambleText from '@renderer/components/common/ScrambleText.vue'
import { Minus, Square, Copy, X } from 'lucide-vue-next' import { Minus, Square, Copy, X } from 'lucide-vue-next'
@@ -154,7 +178,7 @@ const zoomFactor = ref(1)
const previewDeviceNames = ref({ const previewDeviceNames = ref({
nanoOne: 'One', nanoOne: 'One',
nanoZero: 'Zero', nanoZero: 'Zero'
}) })
onMounted(() => { onMounted(() => {
@@ -170,11 +194,9 @@ onMounted(() => {
isMaximized.value = false isMaximized.value = false
}) })
}) })
</script> </script>
<style scoped> <style scoped>
.app-titlebar { .app-titlebar {
-webkit-user-select: none;
-webkit-app-region: drag; -webkit-app-region: drag;
} }

View File

@@ -1,97 +1,133 @@
<template> <template>
<div <div
class="h-12 flex overflow-hidden rounded-lg m-2 transition-all" class="m-2 flex h-12 overflow-hidden rounded-lg transition-all"
:class="{'border border-zinc-100 bg-zinc-300': selected, :class="{
'border border-transparent hover:border-zinc-900': !selected, 'border border-zinc-100 bg-zinc-300': selected,
'group': showHoverButtons}"> 'border border-transparent hover:border-zinc-900': !selected,
group: showHoverButtons
}"
>
<form <form
v-if="nameEditable && editing" v-if="nameEditable && editing"
class="flex-1 flex h-full text-left whitespace-nowrap overflow-hidden" class="flex h-full flex-1 overflow-hidden whitespace-nowrap text-left"
:class="{'bg-zinc-300' : selected}" :class="{ 'bg-zinc-300': selected }"
@submit.prevent="store.renameProfile(profile.id, nameInput); editing=false"> @submit.prevent="
() => {
store.renameProfile(profile.id, nameInput)
editing = false
}
"
>
<input <input
ref="profileNameInput" v-model="nameInput" ref="profileNameInput"
onfocus="this.select()" :placeholder="$t('profiles.name_placeholder')" v-model="nameInput"
class="flex-1 pl-8 h-full rounded-lg text-sm bg-transparent focus-visible:ring-0 focus-visible:outline-none min-w-0 transition-all" onfocus="this.select()"
:class="{'font-semibold bg-zinc-300 hover:bg-zinc-200 text-black' : selected, :placeholder="$t('profiles.name_placeholder')"
'hover:bg-zinc-900 bg-opacity-50 text-muted-foreground': !selected}" class="h-full min-w-0 flex-1 rounded-lg bg-transparent pl-8 text-sm transition-all focus-visible:outline-none focus-visible:ring-0"
@blur="onNameInputBlur"> :class="{
'bg-zinc-300 font-semibold text-black hover:bg-zinc-200': selected,
'text-muted-foreground hover:bg-zinc-900/50': !selected
}"
@blur="onNameInputBlur"
/>
<button <button
ref="nameSubmitButton" ref="nameSubmitButton"
type="submit" type="submit"
:class="{'bg-zinc-300 hover:bg-zinc-200 text-black' : selected, :class="{
'hover:bg-opacity-100 bg-zinc-900 text-zinc-100 bg-opacity-50': !selected}" 'bg-zinc-300 text-black hover:bg-zinc-200': selected,
class="flex h-full rounded-lg aspect-square justify-center items-center flex-shrink-0 transition-all"> 'bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900': !selected
<Check class="h-4 w-4" /> }"
class="flex aspect-square h-full shrink-0 items-center justify-center rounded-lg transition-all"
>
<Check class="size-4" />
</button> </button>
</form> </form>
<!-- TODO: Make hover buttons use Transition(Group) and v-if directive --> <!-- TODO: Make hover buttons use Transition(Group) and v-if directive -->
<button <button
v-else v-else
:class="{'font-semibold bg-zinc-300 hover:bg-zinc-200 text-black' : selected, :class="{
'hover:bg-zinc-900 bg-opacity-50 text-muted-foreground': !selected}" 'bg-zinc-300 font-semibold text-black hover:bg-zinc-200': selected,
class="flex-1 h-12 rounded-lg text-left text-sm whitespace-nowrap overflow-hidden text-ellipsis pr-4 transition-all" 'bg-zinc-900/50 text-muted-foreground hover:bg-zinc-900': !selected
@click="!editing && $emit('select') && $refs.profileTitle.scramble()"> }"
<span class="ml-2 w-4 mr-2 cursor-grab" :class="{'ml-2': !draggable}"> class="h-12 flex-1 truncate rounded-lg pr-4 text-left text-sm transition-all"
@click="!editing && $emit('select') && $refs.profileTitle.scramble()"
>
<span class="mx-2 w-4 cursor-grab" :class="{ 'ml-2': !draggable }">
<GripHorizontal <GripHorizontal
v-if="draggable" v-if="draggable"
:class="{'text-zinc-600': selected, :class="{ 'text-zinc-600': selected, 'text-muted-foreground': !selected }"
'text-muted-foreground': !selected}" class="mb-0.5 inline-block size-4 opacity-0 transition-all group-hover:opacity-100"
class="mb-0.5 h-4 w-4 opacity-0 group-hover:opacity-100 inline-block transition-all" /> />
</span> </span>
<ScrambleText <ScrambleText
ref="profileTitle" ref="profileTitle"
class="transition-colors" class="transition-colors"
:class="{'text-black': selected, 'text-muted-foreground': !selected}" :class="{ 'text-black': selected, 'text-muted-foreground': !selected }"
:text="profile.name" /> :text="profile.name"
<span />
v-if="showId" <span v-if="showId" class="text-xs text-zinc-600 group-hover:hidden">
class="text-xs text-zinc-600 group-hover:hidden"> UID:{{ profile.id }}</span> UID:{{ profile.id }}</span
>
</button> </button>
<template v-if="!confirmDelete"> <template v-if="!confirmDelete">
<button <button
v-if="nameEditable" v-if="nameEditable"
:class="{'bg-zinc-300 hover:bg-zinc-200 text-black' : selected, :class="{
'hover:bg-opacity-100 bg-zinc-900 text-zinc-100 bg-opacity-50': !selected, 'bg-zinc-300 text-black hover:bg-zinc-200': selected,
'group-hover:w-12' : !editing}" 'bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900': !selected,
class="flex w-0 h-12 rounded-lg justify-center items-center flex-shrink-0 transition-all" 'group-hover:w-12': !editing
@click="startEditing"> }"
<PenLine class="h-4 w-4" /> class="flex h-12 w-0 shrink-0 items-center justify-center rounded-lg transition-all"
@click="startEditing"
>
<PenLine class="size-4" />
</button> </button>
<button <button
:class="{'bg-zinc-300 hover:bg-zinc-200 text-black' : selected, :class="{
'hover:bg-opacity-100 bg-zinc-900 text-zinc-100 bg-opacity-50': !selected, 'bg-zinc-300 text-black hover:bg-zinc-200': selected,
'group-hover:w-12' : !editing, 'bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900': !selected,
'rounded-l-lg': !nameEditable}" 'group-hover:w-12': !editing,
class="flex w-0 h-12 rounded-lg justify-center items-center flex-shrink-0 transition-all" 'rounded-l-lg': !nameEditable
@click="$emit('duplicate')"> }"
<Copy class="h-4 w-4" /> class="flex h-12 w-0 shrink-0 items-center justify-center rounded-lg transition-all"
@click="$emit('duplicate')"
>
<Copy class="size-4" />
</button> </button>
<button <button
:class="{'bg-orange-700 hover:bg-orange-600 text-black' : selected, :class="{
'hover:bg-opacity-100 bg-orange-900 text-zinc-100 bg-opacity-50': !selected, 'bg-orange-700 text-black hover:bg-orange-600': selected,
'group-hover:w-12' : !editing}" 'bg-orange-900/50 text-zinc-100 hover:bg-orange-900': !selected,
class="flex w-0 h-12 rounded-lg justify-center items-center flex-shrink-0 transition-all" 'group-hover:w-12': !editing
@click="confirmDelete=true"> }"
<Trash2 class="h-4 w-4" /> class="flex h-12 w-0 shrink-0 items-center justify-center rounded-lg transition-all"
@click="confirmDelete = true"
>
<Trash2 class="size-4" />
</button> </button>
</template> </template>
<template v-else> <template v-else>
<button <button
:class="{'bg-orange-600 hover:bg-orange-500 text-black' : selected, :class="{
'hover:bg-opacity-100 bg-orange-900 text-zinc-100 bg-opacity-50': !selected, 'bg-orange-600 text-black hover:bg-orange-500': selected,
'group-hover:w-12' : !editing}" 'bg-orange-900/50 text-zinc-100 hover:bg-orange-900': !selected,
class="flex w-0 h-12 rounded-lg justify-center items-center flex-shrink-0 transition-all" 'group-hover:w-12': !editing
@click="$emit('delete', profile.id)"> }"
<Check class="h-4 w-4" /> class="flex h-12 w-0 shrink-0 items-center justify-center rounded-lg transition-all"
@click="$emit('delete', profile.id)"
>
<Check class="size-4" />
</button> </button>
<button <button
:class="{'bg-zinc-300 hover:bg-zinc-200 text-black' : selected, :class="{
'hover:bg-opacity-100 bg-zinc-900 text-zinc-100 bg-opacity-50': !selected, 'bg-zinc-300 text-black hover:bg-zinc-200': selected,
'group-hover:w-12' : !editing}" 'bg-zinc-900/50 text-zinc-100 hover:bg-zinc-900': !selected,
class="flex w-0 h-12 rounded-lg justify-center items-center flex-shrink-0 transition-all" 'group-hover:w-12': !editing
@click="confirmDelete=false"> }"
<X class="h-4 w-4" /> class="flex h-12 w-0 shrink-0 items-center justify-center rounded-lg transition-all"
@click="confirmDelete = false"
>
<X class="size-4" />
</button> </button>
</template> </template>
</div> </div>
@@ -113,35 +149,35 @@ const props = defineProps({
type: Object, type: Object,
default: () => ({ default: () => ({
id: '1234', id: '1234',
name: 'Profile Name', name: 'Profile Name'
}), }),
required: true, required: true
}, },
selected: { selected: {
type: Boolean, type: Boolean,
default: false, default: false
}, },
showId: { showId: {
type: Boolean, type: Boolean,
default: false, default: false
}, },
nameEditable: { nameEditable: {
type: Boolean, type: Boolean,
default: true, default: true
}, },
initEditing: { initEditing: {
type: Boolean, type: Boolean,
default: false, default: false
}, },
draggable: { draggable: {
// Not implemented yet // Not implemented yet
type: Boolean, type: Boolean,
default: true, default: true
}, },
showHoverButtons: { showHoverButtons: {
type: Boolean, type: Boolean,
default: true, default: true
}, }
}) })
async function startEditing() { async function startEditing() {
@@ -162,5 +198,4 @@ const nameInput = ref(props.profile.name)
const editing = ref(props.initEditing) const editing = ref(props.initEditing)
const confirmDelete = ref(false) const confirmDelete = ref(false)
</script> </script>

View File

@@ -1,33 +1,46 @@
<template> <template>
<ConfigSection :title="$t('config_options.profile_settings.profile_properties.title')" :icon-component="Type"> <ConfigSection
<div class="px-8 my-4"> :title="$t('config_options.profile_settings.profile_properties.title')"
<span class="text-sm text-muted-foreground font-mono">Title</span> :icon-component="Type"
>
<div class="my-4 px-8">
<span class="font-mono text-sm text-muted-foreground">Title</span>
<Input class="font-pixelsm mt-2 uppercase" default-value="Title text" /> <Input class="font-pixelsm mt-2 uppercase" default-value="Title text" />
</div> </div>
<div class="px-8 my-4"> <div class="my-4 px-8">
<span class="text-sm text-muted-foreground font-mono">Description</span> <span class="font-mono text-sm text-muted-foreground">Description</span>
<Textarea class="font-pixelsm mt-2 uppercase" default-value="Descriptive description describing the profile" /> <Textarea
class="font-pixelsm mt-2 uppercase"
default-value="Descriptive description describing the profile"
/>
</div> </div>
</ConfigSection> </ConfigSection>
<ConfigSection <ConfigSection
v-if="false" :title="$t('config_options.profile_settings.connection_type.title')" v-if="false"
:icon-component="Cable"> :title="$t('config_options.profile_settings.connection_type.title')"
:icon-component="Cable"
>
<!-- TODO: Remove later if not needed --> <!-- TODO: Remove later if not needed -->
<TabSelect v-model="connectionType" :options="connectionTypeOptions" /> <TabSelect v-model="connectionType" :options="connectionTypeOptions" />
</ConfigSection> </ConfigSection>
<ConfigSection <ConfigSection
:title="$t('config_options.profile_settings.internal_profile_toggle.title')" :title="$t('config_options.profile_settings.internal_profile_toggle.title')"
:icon-component="Replace" :show-toggle="true"> :icon-component="Replace"
<p class="flex flex-col p-8 py-4 text-muted-foreground text-xs"> :show-toggle="true"
>
<p class="flex flex-col p-8 py-4 text-xs text-muted-foreground">
{{ $t('config_options.profile_settings.internal_profile_toggle.subtitle') }} {{ $t('config_options.profile_settings.internal_profile_toggle.subtitle') }}
<Separator class="mt-4" /> <Separator class="mt-4" />
<span class="py-4 space-y-4">{{ $t('config_options.profile_settings.internal_profile_toggle.operation') <span class="space-y-4 py-4"
}}:<br> >{{ $t('config_options.profile_settings.internal_profile_toggle.operation') }}:<br />
<Badge class="bg-orange-500">SHIFT</Badge> + <Badge <Badge class="bg-orange-500">SHIFT</Badge> + <Badge class="bg-zinc-500">Fn3</Badge> +
class="bg-zinc-500">Fn3</Badge> + <Badge>Rotation</Badge></span> <Badge>Rotation</Badge></span
>
<Separator /> <Separator />
<span class="pt-4">{{ $t('config_options.profile_settings.internal_profile_toggle.warning') }}</span> <span class="pt-4">{{
$t('config_options.profile_settings.internal_profile_toggle.warning')
}}</span>
</p> </p>
</ConfigSection> </ConfigSection>
</template> </template>
@@ -49,12 +62,11 @@ const connectionType = ref('usb') // TODO: replace with actual value
const connectionTypeOptions = { const connectionTypeOptions = {
usb: { usb: {
icon: UsbIcon, icon: UsbIcon,
titleKey: 'config_options.profile_settings.connection_type.usb', titleKey: 'config_options.profile_settings.connection_type.usb'
}, },
midi: { midi: {
icon: MidiIcon, icon: MidiIcon,
titleKey: 'config_options.profile_settings.connection_type.midi', titleKey: 'config_options.profile_settings.connection_type.midi'
}, }
} }
</script> </script>

View File

@@ -2,53 +2,56 @@
<div> <div>
<div> <div>
<div <div
class="w-full h-12 px-4 flex items-center justify-between flex-nowrap text-nowrap bg-zinc-900"> class="flex h-12 w-full flex-nowrap items-center justify-between text-nowrap bg-zinc-900 px-4"
>
<button <button
class="flex flex-1 items-center h-full min-w-0 font-heading" class="font-heading flex h-full min-w-0 flex-1 items-center"
@click="showProfileConfig=store.selectedProfile && !showProfileConfig"> @click="showProfileConfig = store.selectedProfile && !showProfileConfig"
<component :is="showProfileConfig ? ArrowLeft : List" class="w-5 h-full mr-1 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 ? store.selectedProfile?.name : $t('profiles.title')"
class="text-ellipsis overflow-hidden min-w-0" /> class="min-w-0 overflow-hidden text-ellipsis"
/>
<ScrambleText <ScrambleText
v-if="!showProfileConfig" class="text-sm text-zinc-600 text-ellipsis overflow-hidden min-w-0" v-if="!showProfileConfig"
class="min-w-0 overflow-hidden text-ellipsis text-sm text-zinc-600"
scramble-on-mount scramble-on-mount
:fill-interval="20" :fill-interval="20"
:delay="500" :delay="500"
:text="`(${store.profiles.length}/${ maxProfiles})`" /> :text="`(${store.profiles.length}/${maxProfiles})`"
/>
</button> </button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger> <DropdownMenuTrigger>
<Transition name="fade"> <Transition name="fade">
<button <button
v-if="!showProfileConfig" v-if="!showProfileConfig"
class="bg-zinc-300 text-black hover:bg-zinc-200 border border-zinc-100 rounded-lg h-8 aspect-square flex justify-center items-center" 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="store.addProfile"
>
<Plus class="h-4" /> <Plus class="h-4" />
</button> </button>
</Transition> </Transition>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem> <DropdownMenuItem> Profile </DropdownMenuItem>
Profile <DropdownMenuItem> Category </DropdownMenuItem>
</DropdownMenuItem>
<DropdownMenuItem>
Category
</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
<Separator /> <Separator />
</div> </div>
<div class="grow overflow-y-auto relative"> <div class="relative grow overflow-y-auto">
<div <div v-if="renderProfileList" class="absolute w-full">
v-if="renderProfileList"
class="absolute w-full">
<div v-if="store.profiles.length === 0"> <div v-if="store.profiles.length === 0">
<div class="flex flex-col items-center justify-center h-32"> <div class="flex h-32 flex-col items-center justify-center">
<ScrambleText <ScrambleText
scramble-on-mount :fill-interval="5" class="text-sm text-muted-foreground" scramble-on-mount
:text="$t('profiles.not_found')" /> :fill-interval="5"
class="text-sm text-muted-foreground"
:text="$t('profiles.not_found')"
/>
</div> </div>
</div> </div>
<div v-else> <div v-else>
@@ -60,18 +63,21 @@
v-bind="dragOptions" v-bind="dragOptions"
@start="drag = true" @start="drag = true"
@end="drag = false" @end="drag = false"
@change="onCategoryDrop"> @change="onCategoryDrop"
>
<template #item="dragCategory"> <template #item="dragCategory">
<Collapsible <Collapsible v-model:open="collapse[dragCategory.element.name]" :default-open="true">
v-model:open="collapse[dragCategory.element.name]"
:default-open="true">
<!-- TODO: Make profile groups computed instead defining them of using v-for --> <!-- TODO: Make profile groups computed instead defining them of using v-for -->
<CollapsibleTrigger <CollapsibleTrigger
class="w-full h-12 py-2 text-left text-muted-foreground text-sm bg-zinc-900 border-0 border-b"> class="h-12 w-full border-0 border-b bg-zinc-900 py-2 text-left text-sm text-muted-foreground"
<ChevronRight class="chevrot h-4 w-4 mb-0.5 ml-4 inline-block transition-transform" /> >
{{ dragCategory.element.name }}<span <ChevronRight
class="font-heading text-sm text-zinc-600"> ({{ dragCategory.element.profiles?.length || 0 class="chevrot mb-0.5 ml-4 inline-block size-4 transition-transform"
}})</span> />
{{ dragCategory.element.name
}}<span class="font-heading text-sm text-zinc-600">
({{ dragCategory.element.profiles?.length || 0 }})</span
>
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<TransitionGroup> <TransitionGroup>
@@ -83,9 +89,10 @@
v-bind="dragOptions" v-bind="dragOptions"
@start="drag = true" @start="drag = true"
@end="drag = false" @end="drag = false"
@change="(event)=>onProfileDrop(event, dragCategory.index)"> @change="(event) => onProfileDrop(event, dragCategory.index)"
>
<template v-if="dragCategory.element.profiles.length === 0" #header> <template v-if="dragCategory.element.profiles.length === 0" #header>
<div class="flex h-12 justify-center items-center hideable-header"> <div class="hideable-header flex h-12 items-center justify-center">
<MoreHorizontal class="w-4 text-zinc-600" /> <MoreHorizontal class="w-4 text-zinc-600" />
</div> </div>
</template> </template>
@@ -95,9 +102,15 @@
:profile="dragProfile.element" :profile="dragProfile.element"
:show-hover-buttons="!drag" :show-hover-buttons="!drag"
:selected="store.selectedProfile?.id === dragProfile.element.id" :selected="store.selectedProfile?.id === dragProfile.element.id"
@select="store.selectProfile(dragProfile.element.id); showProfileConfig=true" @select="
() => {
store.selectProfile(dragProfile.element.id)
showProfileConfig = true
}
"
@duplicate="store.duplicateProfile(dragProfile.element.id)" @duplicate="store.duplicateProfile(dragProfile.element.id)"
@delete="store.removeProfile(dragProfile.element.id)" /> @delete="store.removeProfile(dragProfile.element.id)"
/>
</div> </div>
</template> </template>
</draggable> </draggable>
@@ -109,7 +122,7 @@
</div> </div>
</div> </div>
<Transition name="slide"> <Transition name="slide">
<div v-if="showProfileConfig" class="absolute bg-[#101013] h-full"> <div v-if="showProfileConfig" class="absolute h-full bg-[#101013]">
<ProfileConfig /> <ProfileConfig />
</div> </div>
</Transition> </Transition>
@@ -120,25 +133,34 @@
import { Separator } from '@renderer/components/ui/separator' import { Separator } from '@renderer/components/ui/separator'
import { ChevronRight, Plus, ArrowLeft, List, MoreHorizontal } from 'lucide-vue-next' import { ChevronRight, Plus, ArrowLeft, List, MoreHorizontal } from 'lucide-vue-next'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@renderer/components/ui/collapsible' import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} 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 { 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'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@renderer/components/ui/dropdown-menu' import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@renderer/components/ui/dropdown-menu'
defineProps({ defineProps({
showFilter: { showFilter: {
type: Boolean, type: Boolean,
default: false, default: false
}, }
}) })
const dragOptions = ref({ const dragOptions = ref({
ghostClass: 'ghost', ghostClass: 'ghost',
animation: 150, animation: 150,
direction: 'vertical', direction: 'vertical'
}) })
const maxProfiles = 32 const maxProfiles = 32
@@ -152,7 +174,7 @@ const renderProfileList = ref(!showProfileConfig.value)
const drag = ref(false) const drag = ref(false)
watch(showProfileConfig, value => { watch(showProfileConfig, (value) => {
if (value) { if (value) {
renderProfileConfig.value = true renderProfileConfig.value = true
setTimeout(() => { setTimeout(() => {
@@ -213,10 +235,9 @@ const onProfileDrop = (event, categoryIndex) => {
store.changeProfileCategory(profile.id, categoryIndex, newIndex) store.changeProfileCategory(profile.id, categoryIndex, newIndex)
} }
} }
</script> </script>
<style scoped> <style scoped>
[data-state=open] > .chevrot { [data-state='open'] > .chevrot {
transform: rotate(90deg); transform: rotate(90deg);
} }

View File

@@ -1,64 +1,58 @@
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)
}
},
handle_event_message: (event) => {
if (event.ks) {
store.update_keystates(event.ks)
}
if (event.hasOwnProperty('a')) {
store.update_knob_position(event.t, event.a, event.v)
}
},
handle_profile_message: (profile) => {
// TODO update profile
},
export const useMessageHandlers = function(store) { handle_profiles_message: (profiles) => {
// TODO update profiles
},
return { 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_message: (jsonstr) => { handle_error_message: (error) => {
let message = JSON.parse(jsonstr); console.error('Device: ERROR: ', error.error)
if (message.hasOwnProperty('event')) { // TODO show/handle error in UI?
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);
}
},
handle_event_message: (event) => {
if (event.ks){
store.update_keystates(event.ks);
}
if (event.hasOwnProperty('a')) {
store.update_knob_position(event.t, event.a, event.v);
}
},
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?
}
};
};