fix scene load and add first version UpdateValueDialog

This commit is contained in:
Adrian Zuercher
2025-06-27 11:02:27 +02:00
parent 707d88fb7f
commit 38610471f3
14 changed files with 592 additions and 388 deletions

View File

@@ -47,10 +47,23 @@ func (sh *ScenesHandler) SaveScene(c *gin.Context) {
} }
if _, err := os.Stat(path.Join(sh.dir)); err != nil { if _, err := os.Stat(path.Join(sh.dir)); err != nil {
os.MkdirAll(sh.dir, 666) err := os.MkdirAll(sh.dir, 0755)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
} }
f, err := os.OpenFile(path.Join(sh.dir, scene.Name+".scene"), os.O_CREATE|os.O_TRUNC|os.O_RDWR, 666) f, err := os.OpenFile(path.Join(sh.dir, scene.Name+".scene"), os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0644)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
defer f.Close()
_, err = f.Write(body) _, err = f.Write(body)
if err != nil { if err != nil {
@@ -59,7 +72,6 @@ func (sh *ScenesHandler) SaveScene(c *gin.Context) {
}) })
return return
} }
defer f.Close()
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Scene '%s' saved", scene.Name), "message": fmt.Sprintf("Scene '%s' saved", scene.Name),
@@ -155,6 +167,7 @@ func (sh *ScenesHandler) LoadScene(c *gin.Context) {
} }
var scene models.Scene var scene models.Scene
err = json.Unmarshal(body, &scene) err = json.Unmarshal(body, &scene)
if err != nil { if err != nil {
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{
@@ -190,6 +203,13 @@ func (sh *ScenesHandler) LoadScene(c *gin.Context) {
}) })
return return
} }
}
c.JSON(http.StatusOK, scene) c.JSON(http.StatusOK, scene)
break
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Errorf("scene '%s' not found", scene.Name),
})
return
}
} }

View File

@@ -3,6 +3,7 @@ package utils
import ( import (
"fmt" "fmt"
"io/fs" "io/fs"
"os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"runtime" "runtime"
@@ -27,6 +28,10 @@ func OpenBrowser(url string, logger *logging.Logger) error {
{"open", url}, // fallback {"open", url}, // fallback
} }
default: // Linux default: // Linux
if os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" && os.Getenv("XDG_SESSION_TYPE") != "wayland" {
return fmt.Errorf("os is running i headless mode do not start browser")
}
commands = [][]string{ commands = [][]string{
{"chromium-browser", "--kiosk", url}, {"chromium-browser", "--kiosk", url},
{"google-chrome", "--kiosk", url}, {"google-chrome", "--kiosk", url},
@@ -47,11 +52,11 @@ func OpenBrowser(url string, logger *logging.Logger) error {
return fmt.Errorf("could not open browser") return fmt.Errorf("could not open browser")
} }
func FindAllFiles(rootDir, fileExtention string) (files []string, err error){ func FindAllFiles(rootDir, fileExtention string) (files []string, err error) {
err = filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error { err = filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error {
if d.IsDir() { if d.IsDir() {
return nil return nil
} else if filepath.Ext(d.Name()) == fileExtention{ } else if filepath.Ext(d.Name()) == fileExtention {
files = append(files, path) files = append(files, path)
} }
return err return err

12
package-lock.json generated
View File

@@ -1,16 +1,16 @@
{ {
"name": "lightcontrol", "name": "lightcontrol",
"version": "0.0.5", "version": "0.0.14",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "lightcontrol", "name": "lightcontrol",
"version": "0.0.5", "version": "0.0.14",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@quasar/extras": "^1.16.4", "@quasar/extras": "^1.16.4",
"axios": "^1.9.0", "axios": "^1.10.0",
"quasar": "^2.16.0", "quasar": "^2.16.0",
"vue": "^3.4.18", "vue": "^3.4.18",
"vue-router": "^4.0.12" "vue-router": "^4.0.12"
@@ -2312,9 +2312,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.9.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",

View File

@@ -1,6 +1,6 @@
{ {
"name": "lightcontrol", "name": "lightcontrol",
"version": "0.0.14", "version": "0.0.15",
"description": "A Tecamino App", "description": "A Tecamino App",
"productName": "Light Control", "productName": "Light Control",
"author": "A. Zuercher", "author": "A. Zuercher",
@@ -16,7 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@quasar/extras": "^1.16.4", "@quasar/extras": "^1.16.4",
"axios": "^1.9.0", "axios": "^1.10.0",
"quasar": "^2.16.0", "quasar": "^2.16.0",
"vue": "^3.4.18", "vue": "^3.4.18",
"vue-router": "^4.0.12" "vue-router": "^4.0.12"

View File

@@ -0,0 +1,13 @@
<template>
<QCard v-if="props.display"> Test </QCard>
</template>
<script setup lang="ts">
const props = defineProps({
display: {
type: Boolean,
default: true,
required: true,
},
});
</script>

View File

@@ -11,14 +11,24 @@
row-key="path" row-key="path"
virtual-scroll virtual-scroll
:rows-per-page-options="[0]" :rows-per-page-options="[0]"
/> >
<template v-slot:body-cell-value="props">
<q-td :props="props" @click="openDialog(props.row)">
<span :class="['cursor-pointer', open ? 'text-green' : '']"> {{ props.row.value }}</span>
</q-td>
</template>
</q-table>
<Dialog dialogLabel="Update Value" :show-dialog="open" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Dialog from 'src/components/dialog/UpdateValueDialog.vue';
import type { QTableProps } from 'quasar'; import type { QTableProps } from 'quasar';
import type { Subs } from 'src/models/Subscribe'; import type { Subs, Subscribe } from 'src/models/Subscribe';
import { computed } from 'vue'; import { computed, ref } from 'vue';
const open = ref(false);
// we generate lots of rows here // we generate lots of rows here
const props = defineProps<{ const props = defineProps<{
@@ -35,5 +45,16 @@ const columns = [
field: 'value', field: 'value',
align: 'left', align: 'left',
}, },
{
name: 'test',
label: '',
field: 'test',
align: 'left',
},
] as QTableProps['columns']; ] as QTableProps['columns'];
function openDialog(item: Subscribe) {
console.log(77, item);
open.value = true;
}
</script> </script>

View File

@@ -0,0 +1,101 @@
<template>
<q-dialog v-model="internalShowDialog">
<q-card :style="'width:' + props.width">
<q-card-section
v-if="props.dialogLabel"
class="text-h6 text-center"
:class="'text-' + props.labelColor"
>{{ props.dialogLabel }}</q-card-section
>
<q-card-section>
<q-input v-model="inputValue"></q-input>
</q-card-section>
<q-card-section v-if="props.text" class="text-center" style="white-space: pre-line">{{
props.text
}}</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn v-if="props.buttonCancelLabel" flat :label="props.buttonCancelLabel" v-close-popup>
</q-btn>
<q-btn
v-if="props.buttonOkLabel"
flat
:label="props.buttonOkLabel"
v-close-popup
@click="closeDialog"
>
</q-btn>
</q-card-actions>
</q-card>
</q-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
const props = defineProps({
showDialog: {
type: Boolean,
required: true,
},
buttonOkLabel: {
type: String,
default: 'OK',
},
labelColor: {
type: String,
default: 'primary',
},
dialogLabel: {
type: String,
default: '',
},
text: {
type: String,
default: '',
},
buttonCancelLabel: {
type: String,
default: '',
},
width: {
type: String,
default: '300px',
},
value: {
type: [String, Number],
},
});
const inputValue = ref(props.value);
const emit = defineEmits(['update:showDialog', 'update:value', 'confirmed', 'cancel']);
const internalShowDialog = ref(props.showDialog);
watch(inputValue, (val) => {
emit('update:value', val);
});
watch(
() => props.showDialog,
(newValue) => {
console.log('watch showDialog', newValue);
internalShowDialog.value = newValue;
},
);
watch(internalShowDialog, (newValue) => {
console.log('watch internalShowDialog', newValue);
emit('update:showDialog', newValue);
if (!newValue) {
console.log('emit cancel');
emit('cancel');
} else {
console.log('emit confirmed');
emit('confirmed');
}
});
function closeDialog() {
internalShowDialog.value = false;
emit('update:showDialog', false);
}
</script>

View File

@@ -104,17 +104,17 @@ select
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import LightSlider from './LightSlider.vue'; import LightSlider from './LightSlider.vue';
import { NotifyResponse } from 'src/composables/notify'; import { NotifyResponse } from 'src/composables/notify';
import { computed, onMounted, onUnmounted, ref } from 'vue'; import { onBeforeUpdate, computed, onMounted, onUnmounted, ref } from 'vue';
import { subscribe, unsubscribe, setValues } from 'src/services/websocket'; import { subscribeToPath, unsubscribe, setValues } from 'src/services/websocket';
import { LocalStorage } from 'quasar'; import { LocalStorage } from 'quasar';
import { getSubscriptionsByPath, buildTree, dbmData } from 'src/composables/dbm/dbmTree'; import { getSubscriptionsByPath, updateValue } from 'src/composables/dbm/dbmTree';
import DragPad from 'src/components/lights/DragPad.vue'; import DragPad from 'src/components/lights/DragPad.vue';
import SettingDialog from './SettingMovingHead.vue'; import SettingDialog from './SettingMovingHead.vue';
import type { Settings } from 'src/models/MovingHead'; import type { Settings } from 'src/models/MovingHead';
const $q = useQuasar(); const $q = useQuasar();
const brightness = updateBrightnessValue('MovingHead:Brightness'); const brightness = updateBrightnessValue('MovingHead:Brightness');
const state = updateValue('MovingHead:State'); const state = updateValue('MovingHead:State', $q);
const settings = ref<Settings>({ const settings = ref<Settings>({
show: false, show: false,
reversePan: false, reversePan: false,
@@ -125,24 +125,11 @@ const settings = ref<Settings>({
onMounted(() => { onMounted(() => {
settings.value.reversePan = LocalStorage.getItem('reversePan') ?? false; settings.value.reversePan = LocalStorage.getItem('reversePan') ?? false;
settings.value.reverseTilt = LocalStorage.getItem('reverseTilt') ?? false; settings.value.reverseTilt = LocalStorage.getItem('reverseTilt') ?? false;
subscribeToPath($q, 'MovingHead:.*');
});
subscribe([ onBeforeUpdate(() => {
{ subscribeToPath($q, 'MovingHead:.*');
path: 'MovingHead:.*',
depth: 0,
},
])
.then((response) => {
console.log(response);
if (response?.subscribe) {
dbmData.splice(0, dbmData.length, ...buildTree(response.subscribe));
} else {
NotifyResponse($q, response);
}
})
.catch((err) => {
NotifyResponse($q, err, 'error');
});
}); });
onUnmounted(() => { onUnmounted(() => {
@@ -157,45 +144,28 @@ onUnmounted(() => {
}); });
function changeState() { function changeState() {
console.log(55, brightness.value);
console.log(56, state.value);
if (brightness.value === 0) { if (brightness.value === 0) {
if (state.value === 0) { if (state.value === 0) {
brightness.value = 255; brightness.value = 255;
return; return;
} }
brightness.value = state.value; brightness.value = state.value;
console.log(57, brightness.value);
return; return;
} }
state.value = brightness.value; state.value = brightness.value;
console.log(58, state.value);
brightness.value = 0; brightness.value = 0;
} }
function updateValue(path: string, isDouble = false) {
return computed({
get() {
const sub = getSubscriptionsByPath(path);
const value = sub ? Number(sub.value ?? 0) : 0;
return isDouble ? value : value;
},
set(val) {
const setPaths = [{ path, value: val }];
if (isDouble) {
setPaths.push({ path: `${path}Fine`, value: val });
}
setValues(setPaths)
.then((response) => NotifyResponse($q, response))
.catch((err) => console.error(`Failed to update ${path.split(':')[1]}:`, err));
},
});
}
function updateBrightnessValue(path: string) { function updateBrightnessValue(path: string) {
return computed({ return computed({
get() { get() {
const sub = getSubscriptionsByPath(path); const sub = getSubscriptionsByPath(path);
const value = sub ? Number(sub.value ?? 0) : 0; if (!sub.value) return 0;
return value; return Number(sub.value.value);
}, },
set(val) { set(val) {
const setPaths = [{ path, value: val }]; const setPaths = [{ path, value: val }];

View File

@@ -0,0 +1,356 @@
<template>
<!-- new edit scene dialog-->
<q-dialog v-model="showDialog" persistent>
<q-card style="min-width: 350px">
<q-card-section>
<div class="text-primary text-h6">{{ dialogLabel }}</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-input
class="q-mb-md"
dense
v-model="newScene.name"
placeholder="Name"
autofocus
:rules="[(val) => !!val || 'Field is required']"
@keyup.enter="saveScene"
/>
<q-input
dense
v-model="newScene.description"
placeholder="Description"
autofocus
@keyup.enter="saveScene"
/>
<div class="q-py-md">
<div class="q-gutter-sm">
<q-checkbox v-model="newScene.movingHead" label="Moving Head" />
<q-checkbox v-model="newScene.lightBar" label="Light Bar" />
</div>
</div>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat :label="dialogLabel" @click="saveScene()" />
</q-card-actions>
</q-card>
</q-dialog>
<Dialog
dialogLabel="Duplicate Scene"
:text="`Scene '${newScene.name}' exists already`"
:show-dialog="existsAlready"
v-on:update:show-dialog="existsAlready = $event"
/>
<q-list
bordered
v-if="scenes?.length > 0"
class="q-mx-auto"
style="max-width: 100%; max-width: 500px"
>
<q-btn
rounded
color="primary"
:class="['q-ma-md', 'text-bold', 'text-white']"
@click="openDialog('add')"
>Add New Scene</q-btn
>
<q-item class="row">
<q-item-section :class="['text-black', 'text-bold', 'col-5']">Name</q-item-section>
<q-item-section :class="['text-black', 'text-left', 'text-bold', 'text-left']"
>Description</q-item-section
>
</q-item>
<q-item
v-for="(item, index) in scenes"
:key="item.name"
bordered
style="border: 0.1px solid lightgray; border-radius: 5px; margin-bottom: 1px"
>
<q-item-section
@click="openDialog('load', item)"
:class="['text-black', 'text-left', 'cursor-pointer']"
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
>{{ item.name }}</q-item-section
>
<q-item-section
@click="openDialog('load', item)"
:class="['text-black', 'text-left', 'cursor-pointer']"
left
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
>{{ item.description }}</q-item-section
>
<q-item-section top side>
<div class="text-grey-8 q-gutter-xs">
<q-btn size="12px" flat dense round icon="delete" @click="removeScene(item.name)" />
<q-btn
size="12px"
flat
dense
round
icon="more_vert"
@click="openDialog('edit', item, index)"
/>
</div>
</q-item-section>
</q-item>
</q-list>
<!-- Fallback if list is empty -->
<div v-else class="q-pa-md text-grey text-center">
<div>No scenes available</div>
<q-btn
rounded
color="primary"
:class="['q-ma-md', 'text-bold', 'text-white']"
@click="openDialog('add')"
>Add First Scene</q-btn
>
</div>
</template>
<script setup lang="ts">
import { NotifyDialog } from 'src/composables/notify';
import { onMounted, reactive, ref } from 'vue';
import { useQuasar } from 'quasar';
import type { Scene } from 'src/models/Scene';
import type { Set } from 'src/models/Set';
import axios from 'axios';
import { api } from 'boot/axios';
import { NotifyResponse } from 'src/composables/notify';
import Dialog from 'src/components/dialog/OkDialog.vue';
import { setValues } from 'src/services/websocket';
const $q = useQuasar();
const showDialog = ref(false);
const dialog = ref('');
const existsAlready = ref(false);
const editIndex = ref(-1);
const dialogLabel = ref('');
const newScene = reactive<Scene>({
name: '',
movingHead: false,
lightBar: false,
});
const scenes = ref<Scene[]>([]);
const quasarApi = axios.create({
baseURL: `http://localhost:9500`,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
onMounted(() => {
quasarApi
.get('/api/loadScenes')
.then((resp) => {
if (resp.data) {
scenes.value = resp.data;
}
})
.catch((err) => NotifyResponse($q, err.response.data.error, 'error'));
});
function removeScene(name: string) {
dialog.value = '';
NotifyDialog($q, 'Delete', 'Do you want to delete scene: ' + name, 'YES', 'NO')
.then((res) => {
if (res) {
scenes.value = scenes.value.filter((s) => s.name !== name);
quasarApi
.delete('/api/deleteScene', {
data: { name },
})
.then((res) => {
if (res.data) {
NotifyResponse($q, res.data, 'warning');
}
})
.catch((err) => {
NotifyResponse($q, err.response.data.error, 'error');
});
}
})
.catch((err) => NotifyResponse($q, err.resp, 'warning'));
}
function openDialog(dialogType: string, scene?: Scene, index?: number) {
switch (dialogType) {
case 'add':
dialog.value = 'add';
dialogLabel.value = 'Add Scene';
newScene.name = '';
newScene.movingHead = true;
newScene.lightBar = true;
showDialog.value = true;
break;
case 'edit':
if (!scene) return;
if (index === undefined) return;
dialog.value = 'edit';
dialogLabel.value = 'Update Scene';
editIndex.value = index;
Object.assign(newScene, JSON.parse(JSON.stringify(scene)));
showDialog.value = true;
break;
case 'load':
if (!scene) return;
dialog.value = 'load';
dialogLabel.value = 'Load Scene';
quasarApi
.post('/api/loadScene', scene)
.then((res) => {
if (res.data) {
Object.assign(newScene, JSON.parse(JSON.stringify(res.data)));
showDialog.value = true;
}
})
.catch((err) => NotifyResponse($q, err.response.data.error, 'error'));
break;
default:
showDialog.value = false;
break;
}
}
const saveScene = async () => {
const sendValues = [];
if (!newScene.name) {
return;
}
const exists = scenes.value.some(
(item, index) => item.name === newScene.name && index !== editIndex.value,
);
switch (dialog.value) {
case 'add':
if (exists) {
existsAlready.value = true;
return;
}
if (newScene.movingHead) {
sendValues.push({
path: 'MovingHead',
query: { depth: 0 },
});
}
if (newScene.lightBar) {
sendValues.push({
path: 'LightBar',
query: { depth: 0 },
});
}
if (sendValues.length > 0) {
try {
const res = await api.post('/json_data', { get: sendValues });
newScene.values = res.data.get;
} catch (err) {
NotifyResponse($q, err as Error, 'error');
}
} else {
newScene.values = [];
}
scenes.value = [...scenes.value, JSON.parse(JSON.stringify(newScene))];
// Sort alphabetically by scene name
scenes.value.sort((a, b) => a.name.localeCompare(b.name));
quasarApi
.post('/api/saveScene', JSON.stringify(newScene))
.then((res) => {
if (res.data) {
NotifyResponse($q, res.data);
}
})
.catch((err) => {
NotifyResponse($q, err.response.data.error, 'error');
});
scenes.value = [...scenes.value];
break;
case 'edit':
if (exists) {
existsAlready.value = true;
return;
}
if (newScene.movingHead) {
sendValues.push({
path: 'MovingHead',
query: { depth: 0 },
});
}
if (newScene.lightBar) {
sendValues.push({
path: 'LightBar',
query: { depth: 0 },
});
}
if (sendValues.length > 0) {
try {
const res = await api.post('/json_data', { get: sendValues });
newScene.values = res.data.get;
} catch (err) {
NotifyResponse($q, err as Error, 'error');
}
} else {
newScene.values = [];
}
scenes.value.splice(editIndex.value, 1, JSON.parse(JSON.stringify(newScene)));
scenes.value.sort((a, b) => a.name.localeCompare(b.name));
scenes.value = [...scenes.value];
quasarApi
.post('/api/saveScene', JSON.stringify(newScene))
.then((res) => {
if (res.data) {
NotifyResponse($q, res.data);
}
})
.catch((err) => {
NotifyResponse($q, err.response.data.error, 'error');
});
scenes.value = [...scenes.value];
break;
case 'load':
{
const setPaths = <Set[]>[];
if (newScene.movingHead) {
newScene.values?.forEach((element) => {
if (element.path && element.path.includes('MovingHead')) {
setPaths.push({ path: element.path, value: element.value });
}
});
}
if (newScene.lightBar) {
newScene.values?.forEach((element) => {
if (element.path && element.path.includes('LightBar')) {
setPaths.push({ path: element.path, value: element.value });
}
});
}
setValues(setPaths)
.then((response) => {
NotifyResponse($q, response);
})
.catch((err) => console.error(`Failed to load scene ${newScene.name}`, err));
}
break;
}
dialog.value = '';
showDialog.value = false;
};
</script>

View File

@@ -1,292 +0,0 @@
<template>
<!-- new edit scene dialog-->
<q-dialog v-model="showDialog" persistent>
<q-card style="min-width: 350px">
<q-card-section>
<div class="text-primary text-h6">{{ dialogLabel }}</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-input
class="q-mb-md"
dense
v-model="newScene.name"
placeholder="Name"
autofocus
:rules="[(val) => !!val || 'Field is required']"
@keyup.enter="saveScene"
/>
<q-input
dense
v-model="newScene.description"
placeholder="Description"
autofocus
@keyup.enter="saveScene"
/>
<div class="q-py-md">
<div class="q-gutter-sm">
<q-checkbox v-model="newScene.movingHead" label="Moving Head" />
<q-checkbox v-model="newScene.lightBar" label="Light Bar" />
</div>
</div>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn flat label="Cancel" v-close-popup />
<q-btn flat :label="dialogLabel" @click="saveScene" />
</q-card-actions>
</q-card>
</q-dialog>
<Dialog
dialogLabel="Duplicate Scene"
:text="`Scene '${newScene.name}' exists already`"
:show-dialog="existsAlready"
v-on:update:show-dialog="existsAlready = $event"
/>
<q-list
bordered
v-if="items.length > 0"
class="q-mx-auto"
style="max-width: 100%; max-width: 500px"
>
<q-btn
rounded
color="primary"
:class="['q-ma-md', 'text-bold', 'text-white']"
@click="openAddDialog"
>Add New Scene</q-btn
>
<q-item class="row">
<q-item-section :class="['text-black', 'text-bold', 'col-5']">Name</q-item-section>
<q-item-section :class="['text-black', 'text-left', 'text-bold', 'text-left']"
>Description</q-item-section
>
</q-item>
<q-item
v-for="(item, index) in items"
:key="item.name"
bordered
style="border: 0.1px solid lightgray; border-radius: 5px; margin-bottom: 1px"
>
<q-item-section
@click="openLoadDialog(item.name)"
:class="['text-black', 'text-left', 'cursor-pointer']"
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
>{{ item.name }}</q-item-section
>
<q-item-section
@click="openLoadDialog(item.name)"
:class="['text-black', 'text-left', 'cursor-pointer']"
left
style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis"
>{{ item.description }}</q-item-section
>
<q-item-section top side>
<div class="text-grey-8 q-gutter-xs">
<q-btn size="12px" flat dense round icon="delete" @click="removeScene(item.name)" />
<q-btn
size="12px"
flat
dense
round
icon="more_vert"
@click="openEditDialog(item, index)"
/>
</div>
</q-item-section>
</q-item>
</q-list>
<!-- Fallback if list is empty -->
<div v-else class="q-pa-md text-grey text-center">
<div>No scenes available</div>
<q-btn
rounded
color="primary"
:class="['q-ma-md', 'text-bold', 'text-white']"
@click="openAddDialog"
>Add First Scene</q-btn
>
</div>
</template>
<script setup lang="ts">
import { NotifyDialog } from 'src/composables/notify';
import { onMounted, ref } from 'vue';
import { useQuasar } from 'quasar';
import type { Scene } from 'src/models/Scene';
import type { Set } from 'src/models/Set';
import axios from 'axios';
import { api } from 'boot/axios';
import { NotifyResponse } from 'src/composables/notify';
import Dialog from 'src/components/dialog/okDialog.vue';
import { setValues } from 'src/services/websocket';
const $q = useQuasar();
const showDialog = ref(false);
const isEdit = ref(false);
const isLoad = ref(false);
const existsAlready = ref(false);
const editIndex = ref(-1);
const dialogLabel = ref('');
const newScene = ref<Scene>({
name: '',
movingHead: false,
lightBar: false,
});
const items = ref<Scene[]>([]);
const quasarApi = axios.create({
baseURL: `http://localhost:9500`,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
onMounted(() => {
quasarApi
.get('/api/loadScenes')
.then((resp) => {
if (resp.data) {
items.value = resp.data as Scene[];
}
})
.catch((err) => NotifyResponse($q, err.response.data.error, 'error'));
});
function removeScene(name: string) {
NotifyDialog($q, 'Delete', 'Do you want to delete scene: ' + name, 'YES', 'NO')
.then((res) => {
if (res) {
items.value = items.value.filter((s) => s.name !== name);
quasarApi
.delete('/api/deleteScene', {
data: { name },
})
.then((res) => {
if (res.data) {
NotifyResponse($q, res.data, 'warning');
}
})
.catch((err) => {
NotifyResponse($q, err.response.data.error, 'error');
});
}
})
.catch((err) => NotifyResponse($q, err.resp, 'warning'));
}
function openAddDialog() {
isEdit.value = false;
dialogLabel.value = 'Add Scene';
newScene.value = {
name: '',
movingHead: true,
lightBar: true,
};
showDialog.value = true;
}
function openEditDialog(scene: Scene, index: number) {
isEdit.value = true;
dialogLabel.value = 'Update Scene';
console.log(76, scene);
newScene.value = { ...scene };
editIndex.value = index;
showDialog.value = true;
}
const saveScene = async () => {
if (!newScene.value.name) {
return;
}
const exists = items.value.some(
(item, index) => item.name === newScene.value.name && index !== editIndex.value,
);
if (exists) {
if (isLoad.value) {
console.log(44, 'load');
const setPaths = <Set[]>[];
newScene.value.values?.forEach((element) => {
setPaths.push({ path: element.path, value: element.value });
});
setValues(setPaths)
.then((response) => NotifyResponse($q, response))
.catch((err) => console.error(`Failed to load scene ${newScene.value.name}`, err));
isLoad.value = false;
showDialog.value = false;
return;
}
existsAlready.value = true;
return;
}
if (isEdit.value && editIndex.value !== -1) {
items.value[editIndex.value] = { ...newScene.value };
} else {
items.value.push(newScene.value);
}
// Sort alphabetically by scene name
items.value.sort((a, b) => a.name.localeCompare(b.name));
const sendValues = [];
if (newScene.value.movingHead) {
sendValues.push({
path: 'MovingHead',
query: { depth: 0 },
});
}
if (newScene.value.lightBar) {
sendValues.push({
path: 'LightBar',
query: { depth: 0 },
});
}
console.log(33, sendValues);
if (sendValues.length > 0) {
try {
const res = await api.post('/json_data', { get: sendValues });
newScene.value.values = res.data.get;
} catch (err) {
NotifyResponse($q, err as Error, 'error');
}
} else {
newScene.value.values = [];
}
quasarApi
.post('/api/saveScene', JSON.stringify(newScene.value))
.then((res) => {
if (res.data) {
NotifyResponse($q, res.data);
}
})
.catch((err) => {
NotifyResponse($q, err.response.data.error, 'error');
});
showDialog.value = false;
};
function openLoadDialog(name: string) {
isLoad.value = true;
quasarApi
.post('/api/loadScene', { name })
.then((res) => {
if (res.data) {
const scene = res.data as Scene;
newScene.value = scene;
newScene.value.name = name;
showDialog.value = true;
isEdit.value = true;
dialogLabel.value = 'Load Scene';
}
})
.catch((err) => NotifyResponse($q, err.response.data.error, 'error'));
}
</script>

View File

@@ -31,7 +31,7 @@ export function buildTree(subs: Subs): TreeNode[] {
for (const item of subs) { for (const item of subs) {
if (item.path) { if (item.path) {
Subscriptions[item.path] = item; addNewSubscription(item);
} }
const pathParts = item.path?.split(':') ?? []; const pathParts = item.path?.split(':') ?? [];
let current = root; let current = root;
@@ -130,6 +130,11 @@ export function getSubscriptionsByPath(path: string) {
return ref(Subscriptions[path]); return ref(Subscriptions[path]);
} }
export function addNewSubscription(sub: Subscribe) {
if (!sub.path) return;
Subscriptions[sub.path] = sub;
}
export function getAllSubscriptions() { export function getAllSubscriptions() {
return Object.values(Subscriptions); return Object.values(Subscriptions);
} }
@@ -145,8 +150,7 @@ export function updateValue(
return computed({ return computed({
get() { get() {
const sub = getSubscriptionsByPath(toggle?.value && path2 ? path2 : path1); const sub = getSubscriptionsByPath(toggle?.value && path2 ? path2 : path1);
const value = sub?.value ? Number(sub.value.value ?? 0) : 0; return sub?.value ? Number(sub.value.value ?? 0) : 0;
return value;
}, },
set(val) { set(val) {
const baseValue = val; const baseValue = val;

View File

@@ -1,8 +1,8 @@
export type Publish = { export type Publish = {
event: string; event: string;
uuid?: string; uuid: string;
path?: string; path: string;
type?: string; type: string;
value?: undefined; value: undefined;
}; };
export type Pubs = Publish[]; export type Pubs = Publish[];

View File

@@ -7,7 +7,7 @@ const routes: RouteRecordRaw[] = [
children: [ children: [
{ path: '', component: () => import('pages/MainPage.vue') }, { path: '', component: () => import('pages/MainPage.vue') },
{ path: '/data', component: () => import('pages/DataPage.vue') }, { path: '/data', component: () => import('pages/DataPage.vue') },
{ path: '/scenes', component: () => import('components/scenes/ScenesTab.vue') }, { path: '/scenes', component: () => import('components/scenes/ScenesPage.vue') },
], ],
}, },

View File

@@ -3,18 +3,19 @@ import type { Publish } from 'src/models/Publish';
import type { Request } from 'src/models/Request'; import type { Request } from 'src/models/Request';
import type { QVueGlobals } from 'quasar'; import type { QVueGlobals } from 'quasar';
import { import {
getAllSubscriptions,
buildTree, buildTree,
dbmData, dbmData,
getSubscriptionsByUuid, getSubscriptionsByPath,
getAllSubscriptions,
} from 'src/composables/dbm/dbmTree'; } from 'src/composables/dbm/dbmTree';
import { ref, reactive } from 'vue'; import { ref } from 'vue';
import type { Subs } from 'src/models/Subscribe'; import type { Subs } from 'src/models/Subscribe';
import type { Sets } from 'src/models/Set'; import type { Sets } from 'src/models/Set';
import type { PongMessage } from 'src/models/Pong'; import type { PongMessage } from 'src/models/Pong';
import { NotifyResponse } from 'src/composables/notify';
const pendingResponses = new Map<string, (data: Response | undefined) => void>(); const pendingResponses = new Map<string, (data: Response | undefined) => void>();
const lastKnownValues: Record<string, string> = reactive({}); //const lastKnownValues: Record<string, string> = reactive({});
export let socket: WebSocket | null = null; export let socket: WebSocket | null = null;
const isConnected = ref(false); const isConnected = ref(false);
@@ -88,29 +89,14 @@ export function initWebSocket(url: string, $q?: QVueGlobals) {
pendingResponses.delete(id); pendingResponses.delete(id);
return; return;
} }
if (message.publish) { if (message.publish) {
(message.publish as Publish[]).forEach((pub) => { (message.publish as Publish[]).forEach((pub) => {
const uuid = pub.uuid; const sub = getSubscriptionsByPath(pub.path);
const value = pub.value ?? ''; if (sub.value && pub.value) {
sub.value.value = pub.value;
if (uuid === undefined) {
return;
}
const oldValue = lastKnownValues[uuid];
if (oldValue !== value) {
lastKnownValues[uuid] = value; // this is now reactive
if (pub.uuid) {
const existing = getSubscriptionsByUuid(pub.uuid);
if (existing.value) {
existing.value.value = value;
}
} else {
getAllSubscriptions().push({ value, uuid: uuid });
} }
dbmData.splice(0, dbmData.length, ...buildTree(getAllSubscriptions())); // rebuild reactive tree dbmData.splice(0, dbmData.length, ...buildTree(getAllSubscriptions())); // rebuild reactive tree
}
}); });
} }
} }
@@ -157,6 +143,26 @@ export function subscribe(data: Subs): Promise<Response | undefined> {
return send({ subscribe: data }); return send({ subscribe: data });
} }
export function subscribeToPath(q: QVueGlobals, path: string) {
subscribe([
{
path: path,
depth: 0,
},
])
.then((response) => {
console.log(response);
if (response?.subscribe) {
dbmData.splice(0, dbmData.length, ...buildTree(response.subscribe));
} else {
NotifyResponse(q, response);
}
})
.catch((err) => {
NotifyResponse(q, err, 'error');
});
}
export function unsubscribe(data: Subs): Promise<Response | undefined> { export function unsubscribe(data: Subs): Promise<Response | undefined> {
return send({ unsubscribe: data }); return send({ unsubscribe: data });
} }