Skip to content

Commit

Permalink
feat(effects): add icons & improved dialog options
Browse files Browse the repository at this point in the history
  • Loading branch information
Yoronex committed Feb 2, 2025
1 parent 1ce3db8 commit 48d31d9
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 28 deletions.
60 changes: 60 additions & 0 deletions src/components/IconSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<template>
<Select
id="icon-selector"
class="w-full"
filter
filter-match-mode="contains"
:model-value="modelValue"
option-label="name"
option-value="value"
:options="options"
placeholder="Icon (optional)"
show-clear
:virtual-scroller-options="{ itemSize: 40 }"
@update:model-value="(v) => $emit('update:modelValue', v)"
>
<template #value="slotProps">
<div v-if="slotProps.value" class="flex items-center gap-2">
<i :class="slotProps.value" />
{{ getNameFromValue(slotProps.value) }}
</div>
<div v-else>{{ slotProps.placeholder }}</div>
</template>
<template #option="slotProps">
<div v-if="slotProps.option.value" class="flex items-center gap-2">
<i :class="slotProps.option.value" />
{{ slotProps.option.name }}
</div>
</template>
</Select>
</template>

<script setup lang="ts">
import { PrimeIcons } from '@primevue/core/api';
import { computed } from 'vue';
defineProps<{
modelValue?: string;
}>();
defineEmits<{
'update:modelValue': [icon: string];
}>();
const getNameFromValue = (value: string) => {
const nameParts: string[] = value.split(' ')[1].split('-');
nameParts.splice(0, 1);
let name = nameParts.join(' ');
name = name.charAt(0).toUpperCase() + name.slice(1);
return name;
};
const options = computed(() => {
return Object.values(PrimeIcons).map((value: string) => {
const name = getNameFromValue(value);
return { value, name };
});
});
</script>

<style scoped></style>
35 changes: 35 additions & 0 deletions src/components/lights/effects/EffectControllerButtonContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<template>
<div class="flex flex-row gap-2 justify-center items-center">
<div v-if="editing">
<span class="pi pi-pencil text-3xl" />
</div>
<div class="flex flex-col gap-1">
<div>
<span v-if="button.icon" :class="['pi', button.icon]" />
{{ button.name }}
</div>
<div v-if="button.properties.type === 'LightsButtonColors'" class="flex flex-row gap-1">
<ColorBox
v-for="color in (button.properties as LightsButtonColors).colors"
:key="color"
:color="colorStore.getHexColor(color as RgbColor)"
/>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { type LightsButtonColors, type LightsPredefinedEffectResponse, RgbColor } from '@/api';
import ColorBox from '@/components/lights/effects/ColorBox.vue';
import { useColorStore } from '@/stores/color.store';
const colorStore = useColorStore();
defineProps<{
button: LightsPredefinedEffectResponse;
editing?: boolean;
}>();
</script>

<style scoped></style>
48 changes: 45 additions & 3 deletions src/components/lights/effects/EffectControllerButtonDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<InputText id="lights-controller-button-name" v-model="name" autocomplete="off" class="w-full" />
<label for="lights-controller-button-name">Name (optional)</label>
</FloatLabel>
<IconSelector v-model="icon" />
<Divider />
<ButtonDialogColors
v-if="type === ButtonTypes.LightsButtonColors"
Expand Down Expand Up @@ -73,22 +74,38 @@
</template>
<template #footer>
<div class="flex flex-row gap-2">
<Button :disabled="!button || button.id < 0" icon="pi pi-trash" label="Delete" severity="danger" />
<Button :disabled="!propertiesValid" icon="pi pi-save" label="Save" severity="success" @click="handleSave" />
<Button
:disabled="!button || button.id < 0 || saveLoading"
icon="pi pi-trash"
label="Delete"
severity="danger"
@click="openDeleteDialog()"
/>
<Button
:disabled="!propertiesValid || saveLoading"
icon="pi pi-save"
label="Save"
:loading="saveLoading"
severity="success"
@click="handleSave"
/>
<ConfirmDialog />
</div>
</template>
</Dialog>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue';
import { useConfirm } from 'primevue/useconfirm';
import type { LightsPredefinedEffectProperties, LightsPredefinedEffectResponse } from '@/api';
import ButtonDialogColors from '@/components/lights/effects/button/ButtonDialogColors.vue';
import ButtonDialogEffectColor from '@/components/lights/effects/button/ButtonDialogEffectColor.vue';
import ButtonDialogEffectMovement from '@/components/lights/effects/button/ButtonDialogEffectMovement.vue';
import ButtonDialogStrobe from '@/components/lights/effects/button/ButtonDialogStrobe.vue';
import ButtonDialogSwitch from '@/components/lights/effects/button/ButtonDialogSwitch.vue';
import { useEffectsControllerStore } from '@/stores/effects-controller.store';
import IconSelector from '@/components/IconSelector.vue';
enum ButtonTypes {
LightsButtonColors = 'LightsButtonColors',
Expand All @@ -111,9 +128,11 @@ const emit = defineEmits<{
const store = useEffectsControllerStore();
const name = ref<string | undefined>();
const icon = ref<string | undefined>('');
const type = ref<ButtonTypes>(ButtonTypes.LightsButtonNull);
const properties = ref<LightsPredefinedEffectProperties | undefined>();
const propertiesValid = ref<boolean>(false);
const saveLoading = ref<boolean>(false);
const typeOptions = [
{ name: 'Colors', value: ButtonTypes.LightsButtonColors },
Expand All @@ -125,12 +144,35 @@ const typeOptions = [
const handleSave = async () => {
if (!propertiesValid.value || !properties.value) return;
saveLoading.value = true;
if (!!props.button && props.button.id < 0) {
await store.createButtonEffect({ properties: properties.value, buttonId: props.button.buttonId, name: name.value });
} else if (!!props.button && props.button?.id >= 0) {
await store.updateButtonEffectProperties(props.button.id, properties.value);
await store.updateButtonEffect(props.button.id, {
name: name.value,
icon: icon.value,
properties: properties.value,
});
}
emit('close');
saveLoading.value = false;
};
const confirm = useConfirm();
const openDeleteDialog = () => {
confirm.require({
message: 'Are you sure you want to delete this predefined effect?',
header: 'Are you sure?',
rejectLabel: 'Cancel',
acceptLabel: 'Yes',
accept() {
if (props.button && props.button?.id >= 0) {
store.deleteButtonEffect(props.button.id).then(() => emit('close'));
}
},
});
};
watch([props], () => {
Expand Down
49 changes: 29 additions & 20 deletions src/stores/effects-controller.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
type LightsEffectsMovementCreateParams,
type LightsGroupResponse,
type LightsPredefinedEffectCreateParams,
type LightsPredefinedEffectProperties,
type LightsPredefinedEffectResponse,
type LightsPredefinedEffectUpdateParams,
type LightsSwitchResponse,
RgbColor,
turnOffLightsSwitch,
Expand Down Expand Up @@ -173,6 +173,17 @@ export const useEffectsControllerStore = defineStore('effectsController', {
if (index >= 0) this.lightsSwitches[index].enabled = true;
});
},
createNullButtonEffect(buttonId: number): LightsPredefinedEffectResponse {
return {
id: -1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
buttonId,
properties: {
type: 'LightsButtonNull',
},
};
},
async getButtonEffects() {
const response = await getAllPredefinedLightsEffects();
if (response.response.ok && response.data) {
Expand All @@ -181,15 +192,7 @@ export const useEffectsControllerStore = defineStore('effectsController', {
this.buttonEffects.sort((a, b) => a.buttonId - b.buttonId);
for (let i = 1; i <= 64; i++) {
if (this.buttonEffects[i - 1]?.buttonId !== i) {
this.buttonEffects.splice(i, 0, {
id: -1,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
buttonId: i,
properties: {
type: 'LightsButtonNull',
},
});
this.buttonEffects.splice(i, 0, this.createNullButtonEffect(i));
}
}
},
Expand All @@ -205,15 +208,8 @@ export const useEffectsControllerStore = defineStore('effectsController', {
}
}
},
async updateButtonEffectProperties(id: number, properties: LightsPredefinedEffectProperties) {
const response = await updatePredefinedLightsEffect({ path: { id }, body: { properties } });
if (response.response.ok && response.data) {
const index = this.buttonEffects.findIndex((g) => g.id === id);
this.buttonEffects.splice(index, 1, response.data);
}
},
async updateButtonEffectPosition(id: number, newButtonId: number) {
const response = await updatePredefinedLightsEffect({ path: { id }, body: { buttonId: newButtonId } });
async updateButtonEffect(id: number, body: LightsPredefinedEffectUpdateParams) {
const response = await updatePredefinedLightsEffect({ path: { id }, body });
if (response.response.ok && response.data) {
const index = this.buttonEffects.findIndex((g) => g.id === id);
this.buttonEffects.splice(index, 1, response.data);
Expand All @@ -223,7 +219,8 @@ export const useEffectsControllerStore = defineStore('effectsController', {
const response = await deletePredefinedLightsEffect({ path: { id } });
if (response.response.ok) {
const index = this.buttonEffects.findIndex((g) => g.id === id);
this.buttonEffects.splice(index, 1);
const oldEffect = this.buttonEffects[index];
this.buttonEffects.splice(index, 1, this.createNullButtonEffect(oldEffect.buttonId));
}
},
async onEffectButtonPress(button: LightsPredefinedEffectResponse) {
Expand Down Expand Up @@ -272,6 +269,18 @@ export const useEffectsControllerStore = defineStore('effectsController', {
}
}
},
areSelectedColors(colors: RgbColor[]): boolean {
if (this.currentColors.length !== colors.length) {
return false;
}

for (let i = 0; i < colors.length; i++) {
// Colors should be in the same order
if (this.currentColors[i] !== colors[i]) return false;
}

return true;
},
},
});

Expand Down
26 changes: 21 additions & 5 deletions src/views/Lights/EffectsControllerSimple.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,24 @@
:key="button.buttonId"
class="h-14"
:disabled="button.properties.type === 'LightsButtonNull'"
severity="secondary"
raised
:severity="buttonActive(button) ? 'primary' : 'secondary'"
@blur="store.onEffectButtonRelease(button)"
@mousedown="store.onEffectButtonPress(button)"
@mouseleave="store.onEffectButtonRelease(button)"
@mouseup="store.onEffectButtonRelease(button)"
@touchend="store.onEffectButtonRelease(button)"
@touchstart="store.onEffectButtonPress(button)"
>
<span v-if="button.icon" :class="['pi', button.icon]" />
{{ button.name }}
<EffectControllerButtonContent :button="button" :editing="editing" />
</Button>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-8 gap-5">
<Button
v-for="button in store.buttonEffects"
:key="button.buttonId"
class="h-14"
raised
severity="secondary"
variant="outlined"
@click="
Expand All @@ -40,7 +41,7 @@
}
"
>
<span class="pi pi-pencil" />
<EffectControllerButtonContent :button="button" :editing="editing" />
</Button>

<EffectControllerButtonDialog
Expand All @@ -58,13 +59,28 @@ import BeatVisualizer from '@/components/audio/BeatVisualizer.vue';
import AppContainer from '@/layout/AppContainer.vue';
import { useEffectsControllerStore } from '@/stores/effects-controller.store';
import EffectControllerButtonDialog from '@/components/lights/effects/EffectControllerButtonDialog.vue';
import type { LightsPredefinedEffectResponse } from '@/api';
import { type LightsButtonSwitch, type LightsPredefinedEffectResponse } from '@/api';
import EffectControllerButtonContent from '@/components/lights/effects/EffectControllerButtonContent.vue';
const store = useEffectsControllerStore();
const editing = ref(false);
const editingButton = ref<LightsPredefinedEffectResponse>();
const editingDialogOpen = ref(false);
const buttonActive = (button: LightsPredefinedEffectResponse) => {
if (button.properties.type === 'LightsButtonColors') {
return store.areSelectedColors(button.properties.colors);
}
if (button.properties.type === 'LightsButtonSwitch') {
const storedSwitches = store.lightsSwitches.filter((s) =>
(button.properties as LightsButtonSwitch).switchIds.includes(s.id),
);
const enabledSwitches = storedSwitches.filter((s) => s.enabled);
return enabledSwitches.length > 0;
}
return false;
};
</script>

<style scoped></style>

0 comments on commit 48d31d9

Please sign in to comment.