UPD: Linting frenzy
This commit is contained in:
@@ -2,16 +2,19 @@
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'@electron-toolkit',
|
||||
'@electron-toolkit/eslint-config-ts/eslint-recommended',
|
||||
'@vue/eslint-config-typescript/recommended',
|
||||
'@vue/eslint-config-prettier'
|
||||
'@vue/eslint-config-prettier',
|
||||
'plugin:tailwindcss/recommended'
|
||||
],
|
||||
rules: {
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/multi-word-component-names': 'off'
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'tailwindcss/no-custom-classname': 'off'
|
||||
}
|
||||
}
|
||||
|
||||
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
@@ -1,3 +1,6 @@
|
||||
{
|
||||
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"Vue.vscode-typescript-vue-plugin"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-vite": "^2.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-tailwindcss": "^3.14.3",
|
||||
"eslint-plugin-vue": "^9.20.1",
|
||||
"postcss": "^8.4.35",
|
||||
"prettier": "^3.2.4",
|
||||
|
||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -103,6 +103,9 @@ devDependencies:
|
||||
eslint:
|
||||
specifier: ^8.56.0
|
||||
version: 8.57.0
|
||||
eslint-plugin-tailwindcss:
|
||||
specifier: ^3.14.3
|
||||
version: 3.14.3(tailwindcss@3.4.1)
|
||||
eslint-plugin-vue:
|
||||
specifier: ^9.20.1
|
||||
version: 9.22.0(eslint@8.57.0)
|
||||
@@ -2457,6 +2460,17 @@ packages:
|
||||
synckit: 0.8.8
|
||||
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):
|
||||
resolution: {integrity: sha512-7wCXv5zuVnBtZE/74z4yZ0CM8AjH6bk4MQGm7hZjUC2DBppKU5ioeOk5LGSg/s9a1ZJnIsdPLJpXnu1Rc+cVHg==}
|
||||
engines: {node: ^14.17.0 || >=16.0.0}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"> ({{ ""+store.numAttachedDevices }})</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
Device
|
||||
<template v-if="store.numAttachedDevices !== 1">
|
||||
Devices<span class="text-zinc-500"> ({{ '' + 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user