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

@@ -46,23 +46,23 @@ window.nanodevices.on_event('update', (evt, deviceid, data) => {
window.nanodevices.list_devices().then((devs) => store.init_devices(devs))
</script>
<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" />
<div class="flex-1 min-h-0 flex flex-row justify-center">
<div class="basis-1/3 min-w-60 flex-1 flex overflow-hidden">
<div class="flex min-h-0 flex-1 flex-row justify-center">
<div class="flex min-w-60 flex-1 basis-1/3 overflow-hidden">
<Transition name="slide-left">
<ProfileManager
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>
</div>
<DevicePreview class="basis-1/3 flex-col flex" />
<div class="basis-2/5 flex-1 flex overflow-hidden">
<DevicePreview class="flex basis-1/3 flex-col" />
<div class="flex flex-1 basis-2/5 overflow-hidden">
<Transition name="slide-right">
<ConfigPane
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>
</div>

View File

@@ -1,6 +1,6 @@
<template>
<div class="bg-wip w-full text-center p-8 text-zinc-600">
<span class="bg-black font-heading p-1">WORK IN PROGRESS</span>
<div class="bg-wip w-full p-8 text-center text-zinc-600">
<span class="font-heading bg-black p-1">WORK IN PROGRESS</span>
</div>
</template>
@@ -17,4 +17,4 @@
var(--stripe-color-b) calc(var(--stripe-width) * 2)
);
}
</style>
</style>

View File

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

View File

@@ -1,122 +1,179 @@
<template>
<div
class="mx-2 flex p-4 font-heading rounded-b-lg border-x border-b border-zinc-800"
:class="{'rounded-t-lg': roundedTop}"
:style="{backgroundColor: color.hex()}">
class="font-heading mx-2 flex rounded-b-lg border-x border-b border-zinc-800 p-4"
:class="{ 'rounded-t-lg': roundedTop }"
:style="{ backgroundColor: color.hex() }"
>
<div
ref="colorFieldText" class="w-full flex opacity-70"
: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">
ref="colorFieldText"
class="flex w-full opacity-70"
: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>
<form @submit.prevent="onSubmitHueInput">
<label for="hueInput">H: </label><input
id="hueInput"
v-model="hueInput"
onfocus="this.select()"
type="number" maxlength="3"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
<label for="hueInput">H: </label
><input
id="hueInput"
v-model="hueInput"
onfocus="this.select()"
type="number"
maxlength="3"
class="w-8 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form>
<form @submit.prevent="onSubmitSaturationInput">
<label for="saturationInput">S: </label><input
id="saturationInput"
v-model="saturationInput"
onfocus="this.select()"
type="number" maxlength="3"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
<label for="saturationInput">S: </label
><input
id="saturationInput"
v-model="saturationInput"
onfocus="this.select()"
type="number"
maxlength="3"
class="w-8 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form>
<form @submit.prevent="onSubmitValueInput">
<label for="valueInput">V: </label><input
id="valueInput"
v-model="valueInput"
onfocus="this.select()"
type="number" maxlength="3"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
<label for="valueInput">V: </label
><input
id="valueInput"
v-model="valueInput"
onfocus="this.select()"
type="number"
maxlength="3"
class="w-8 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form>
</div>
<div class="mx-auto">
<form @submit.prevent="onSubmitHexInput">
<label for="hexInput">#</label><input
id="hexInput"
v-model="hexInput" maxlength="6"
onfocus="this.select()"
class="w-16 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
<label for="hexInput">#</label
><input
id="hexInput"
v-model="hexInput"
maxlength="6"
onfocus="this.select()"
class="w-16 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form>
</div>
<div>
<form @submit.prevent="onSubmitRGBInput">
<label for="rInput">R: </label><input
id="rInput"
v-model="rInput"
onfocus="this.select()"
type="number" maxlength="3"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
<label for="rInput">R: </label
><input
id="rInput"
v-model="rInput"
onfocus="this.select()"
type="number"
maxlength="3"
class="w-8 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form>
<form @submit.prevent="onSubmitRGBInput">
<label for="gInput">G: </label><input
id="gInput"
v-model="gInput"
onfocus="this.select()"
type="number" maxlength="3"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
<label for="gInput">G: </label
><input
id="gInput"
v-model="gInput"
onfocus="this.select()"
type="number"
maxlength="3"
class="w-8 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form>
<form @submit.prevent="onSubmitRGBInput">
<label for="bInput">B: </label><input
id="bInput"
v-model="bInput"
onfocus="this.select()"
type="number" maxlength="3"
class="w-8 bg-transparent focus-visible:ring-0 focus-visible:outline-none"
@blur="updateInputs">
<label for="bInput">B: </label
><input
id="bInput"
v-model="bInput"
onfocus="this.select()"
type="number"
maxlength="3"
class="w-8 bg-transparent focus-visible:outline-none focus-visible:ring-0"
@blur="updateInputs"
/>
</form>
</div>
</div>
</div>
<div class="flex py-4 px-8">
<div class="flex px-8 py-4">
<SliderRoot
v-model="hueSliderModel" :max="359"
class="relative flex w-full touch-none select-none items-center h-10">
v-model="hueSliderModel"
:max="359"
class="relative flex h-10 w-full touch-none select-none items-center"
>
<SliderTrack
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
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"
style="box-shadow: -3px 0 15px 0 rgba(0,0,0,0.6)">
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)"
>
<MoreHorizontal class="h-full" />
</SliderThumb>
</SliderRoot>
</div>
<Separator />
<div class="flex py-4 px-8">
<div class="flex px-8 py-4">
<SliderRoot
v-model="saturationSliderModel" :max="100"
class="relative flex w-full touch-none select-none items-center h-10">
v-model="saturationSliderModel"
:max="100"
class="relative flex h-10 w-full touch-none select-none items-center"
>
<SliderTrack
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
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"
style="box-shadow: -3px 0 15px 0 rgba(0,0,0,0.6)">
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)"
>
<MoreHorizontal class="h-full" />
</SliderThumb>
</SliderRoot>
</div>
<Separator />
<div class="flex py-4 px-8">
<div class="flex px-8 py-4">
<SliderRoot
v-model="valueSliderModel" :max="100"
class="relative flex w-full touch-none select-none items-center h-10">
v-model="valueSliderModel"
:max="100"
class="relative flex h-10 w-full touch-none select-none items-center"
>
<SliderTrack
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
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"
style="box-shadow: -3px 0 15px 0 rgba(0,0,0,0.6)">
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)"
>
<MoreHorizontal class="h-full" />
</SliderThumb>
</SliderRoot>
@@ -133,8 +190,8 @@ import { Separator } from '@renderer/components/ui/separator'
defineProps({
roundedTop: {
type: Boolean,
default: false,
},
default: false
}
})
const hueSliderValue = ref(0)
@@ -148,7 +205,7 @@ const hueSliderModel = computed({
set(hue) {
hueSliderValue.value = hue[0]
color.value = color.value.hue(hue[0])
},
}
})
const saturationSliderModel = computed({
@@ -158,7 +215,7 @@ const saturationSliderModel = computed({
set(saturation) {
saturationSliderValue.value = saturation[0]
color.value = color.value.saturationv(saturation[0])
},
}
})
const valueSliderModel = computed({
@@ -168,12 +225,12 @@ const valueSliderModel = computed({
set(value) {
valueSliderValue.value = value[0]
color.value = color.value.value(value[0])
},
}
})
const color = defineModel({
type: Color,
default: () => Color.rgb(255, 0, 0),
default: () => Color.rgb(255, 0, 0)
})
const saturationSliderColor = computed(() => {
@@ -199,8 +256,7 @@ function onSubmitHexInput() {
}
if (input.match(/^#[0-9A-F]{6}$/i)) {
color.value = Color(input)
} else
shake()
} else shake()
}
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
return yiq < 128
}
</script>
<style scoped>
.shake {
@@ -324,4 +379,4 @@ function isDark(color) {
transform: translateX(0);
}
}
</style>
</style>

View File

@@ -1,22 +1,36 @@
<template>
<div
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
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
v-for="(option, key) in options" :key="key"
class="flex-1 py-2 items-center text-center rounded-t-lg min-w-0 transition-colors"
: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">
v-for="(option, key) in options"
:key="key"
class="min-w-0 flex-1 items-center rounded-t-lg py-2 text-center transition-colors"
: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) }}
</button>
</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
v-for="(option, key) in options" :key="key" class="flex-1 h-6"
:class="{ 'color-tab': currentOption === key}"
:style="{background: option.color.hex()}" @click="currentOption = key" />
v-for="(option, key) in options"
:key="key"
class="h-6 flex-1"
:class="{ 'color-tab': currentOption === key }"
:style="{ background: option.color.hex() }"
@click="currentOption = key"
/>
</div>
<HSVInput v-model="options[currentOption].color" />
</div>
@@ -35,26 +49,24 @@ const model = defineModel({
default: () => ({
one: {
titleKey: 'One',
color: Color('#ff0000'),
color: Color('#ff0000')
},
two: {
titleKey: 'Two',
color: Color('#00ff00'),
color: Color('#00ff00')
},
three: {
titleKey: 'Three',
color: Color('#0000ff'),
},
}),
color: Color('#0000ff')
}
})
})
const options = reactive(model.value)
onBeforeMount(() => {
if (currentOption.value === null)
currentOption.value = Object.keys(options)[0]
if (currentOption.value === null) currentOption.value = Object.keys(options)[0]
})
</script>
<style scoped>
.color-tab {
@@ -68,7 +80,7 @@ onBeforeMount(() => {
bottom: -1px;
width: var(--rounded);
height: var(--rounded);
content: " ";
content: ' ';
}
.color-tab:before {
@@ -87,7 +99,8 @@ onBeforeMount(() => {
z-index: 1;
}
.color-tab:after, .color-tab:before {
.color-tab:after,
.color-tab:before {
border: none;
}
</style>
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<template>
<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 />
</button>
</template>
</template>

View File

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

View File

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

View File

@@ -1,33 +1,46 @@
<template>
<ConfigSection :title="$t('config_options.profile_settings.profile_properties.title')" :icon-component="Type">
<div class="px-8 my-4">
<span class="text-sm text-muted-foreground font-mono">Title</span>
<ConfigSection
:title="$t('config_options.profile_settings.profile_properties.title')"
: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" />
</div>
<div class="px-8 my-4">
<span class="text-sm text-muted-foreground font-mono">Description</span>
<Textarea class="font-pixelsm mt-2 uppercase" default-value="Descriptive description describing the profile" />
<div class="my-4 px-8">
<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"
/>
</div>
</ConfigSection>
<ConfigSection
v-if="false" :title="$t('config_options.profile_settings.connection_type.title')"
:icon-component="Cable">
v-if="false"
:title="$t('config_options.profile_settings.connection_type.title')"
:icon-component="Cable"
>
<!-- TODO: Remove later if not needed -->
<TabSelect v-model="connectionType" :options="connectionTypeOptions" />
</ConfigSection>
<ConfigSection
:title="$t('config_options.profile_settings.internal_profile_toggle.title')"
:icon-component="Replace" :show-toggle="true">
<p class="flex flex-col p-8 py-4 text-muted-foreground text-xs">
:icon-component="Replace"
: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') }}
<Separator class="mt-4" />
<span class="py-4 space-y-4">{{ $t('config_options.profile_settings.internal_profile_toggle.operation')
}}:<br>
<Badge class="bg-orange-500">SHIFT</Badge> + <Badge
class="bg-zinc-500">Fn3</Badge> + <Badge>Rotation</Badge></span>
<span class="space-y-4 py-4"
>{{ $t('config_options.profile_settings.internal_profile_toggle.operation') }}:<br />
<Badge class="bg-orange-500">SHIFT</Badge> + <Badge class="bg-zinc-500">Fn3</Badge> +
<Badge>Rotation</Badge></span
>
<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>
</ConfigSection>
</template>
@@ -49,12 +62,11 @@ const connectionType = ref('usb') // TODO: replace with actual value
const connectionTypeOptions = {
usb: {
icon: UsbIcon,
titleKey: 'config_options.profile_settings.connection_type.usb',
titleKey: 'config_options.profile_settings.connection_type.usb'
},
midi: {
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
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
class="flex flex-1 items-center h-full min-w-0 font-heading"
@click="showProfileConfig=store.selectedProfile && !showProfileConfig">
<component :is="showProfileConfig ? ArrowLeft : List" class="w-5 h-full mr-1 shrink-0" />
class="font-heading flex h-full min-w-0 flex-1 items-center"
@click="showProfileConfig = store.selectedProfile && !showProfileConfig"
>
<component :is="showProfileConfig ? ArrowLeft : List" class="mr-1 h-full w-5 shrink-0" />
<ScrambleText
: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
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
:fill-interval="20"
:delay="500"
:text="`(${store.profiles.length}/${ maxProfiles})`" />
:text="`(${store.profiles.length}/${maxProfiles})`"
/>
</button>
<DropdownMenu>
<DropdownMenuTrigger>
<Transition name="fade">
<button
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"
@click="store.addProfile">
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"
>
<Plus class="h-4" />
</button>
</Transition>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
Profile
</DropdownMenuItem>
<DropdownMenuItem>
Category
</DropdownMenuItem>
<DropdownMenuItem> Profile </DropdownMenuItem>
<DropdownMenuItem> Category </DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Separator />
</div>
<div class="grow overflow-y-auto relative">
<div
v-if="renderProfileList"
class="absolute w-full">
<div class="relative grow overflow-y-auto">
<div v-if="renderProfileList" class="absolute w-full">
<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
scramble-on-mount :fill-interval="5" class="text-sm text-muted-foreground"
:text="$t('profiles.not_found')" />
scramble-on-mount
:fill-interval="5"
class="text-sm text-muted-foreground"
:text="$t('profiles.not_found')"
/>
</div>
</div>
<div v-else>
@@ -60,18 +63,21 @@
v-bind="dragOptions"
@start="drag = true"
@end="drag = false"
@change="onCategoryDrop">
@change="onCategoryDrop"
>
<template #item="dragCategory">
<Collapsible
v-model:open="collapse[dragCategory.element.name]"
:default-open="true">
<Collapsible v-model:open="collapse[dragCategory.element.name]" :default-open="true">
<!-- TODO: Make profile groups computed instead defining them of using v-for -->
<CollapsibleTrigger
class="w-full h-12 py-2 text-left text-muted-foreground text-sm bg-zinc-900 border-0 border-b">
<ChevronRight class="chevrot h-4 w-4 mb-0.5 ml-4 inline-block transition-transform" />
{{ dragCategory.element.name }}<span
class="font-heading text-sm text-zinc-600"> ({{ dragCategory.element.profiles?.length || 0
}})</span>
class="h-12 w-full border-0 border-b bg-zinc-900 py-2 text-left text-sm text-muted-foreground"
>
<ChevronRight
class="chevrot mb-0.5 ml-4 inline-block size-4 transition-transform"
/>
{{ dragCategory.element.name
}}<span class="font-heading text-sm text-zinc-600">
({{ dragCategory.element.profiles?.length || 0 }})</span
>
</CollapsibleTrigger>
<CollapsibleContent>
<TransitionGroup>
@@ -83,9 +89,10 @@
v-bind="dragOptions"
@start="drag = true"
@end="drag = false"
@change="(event)=>onProfileDrop(event, dragCategory.index)">
@change="(event) => onProfileDrop(event, dragCategory.index)"
>
<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" />
</div>
</template>
@@ -95,9 +102,15 @@
:profile="dragProfile.element"
:show-hover-buttons="!drag"
: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)"
@delete="store.removeProfile(dragProfile.element.id)" />
@delete="store.removeProfile(dragProfile.element.id)"
/>
</div>
</template>
</draggable>
@@ -109,7 +122,7 @@
</div>
</div>
<Transition name="slide">
<div v-if="showProfileConfig" class="absolute bg-[#101013] h-full">
<div v-if="showProfileConfig" class="absolute h-full bg-[#101013]">
<ProfileConfig />
</div>
</Transition>
@@ -120,25 +133,34 @@
import { Separator } from '@renderer/components/ui/separator'
import { ChevronRight, Plus, ArrowLeft, List, MoreHorizontal } from 'lucide-vue-next'
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 { useStore } from '@renderer/store'
import ProfileButton from '@renderer/components/profile/ProfileButton.vue'
import ProfileConfig from '@renderer/components/profile/ProfileConfig.vue'
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({
showFilter: {
type: Boolean,
default: false,
},
default: false
}
})
const dragOptions = ref({
ghostClass: 'ghost',
animation: 150,
direction: 'vertical',
direction: 'vertical'
})
const maxProfiles = 32
@@ -152,7 +174,7 @@ const renderProfileList = ref(!showProfileConfig.value)
const drag = ref(false)
watch(showProfileConfig, value => {
watch(showProfileConfig, (value) => {
if (value) {
renderProfileConfig.value = true
setTimeout(() => {
@@ -213,10 +235,9 @@ const onProfileDrop = (event, categoryIndex) => {
store.changeProfileCategory(profile.id, categoryIndex, newIndex)
}
}
</script>
<style scoped>
[data-state=open] > .chevrot {
[data-state='open'] > .chevrot {
transform: rotate(90deg);
}
@@ -255,4 +276,4 @@ const onProfileDrop = (event, categoryIndex) => {
.hideable-header:only-child {
display: flex;
}
</style>
</style>

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) => {
let 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
},
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?
}
};
};
handle_error_message: (error) => {
console.error('Device: ERROR: ', error.error)
// TODO show/handle error in UI?
}
}
}