Panels V1: basic panel overview, basic binding of macros to buttons, basic panel-view.

This commit is contained in:
Jesse Malotaux 2025-04-11 13:50:24 +02:00
parent 45d3135aa9
commit 887080efa6
9 changed files with 538 additions and 111 deletions

View file

@ -1,59 +0,0 @@
<template>
<div></div>
</template>
<script setup>
import { appUrl } from '@/services/ApiService'
import axios from 'axios'
import { nextTick, onMounted, reactive, ref } from 'vue'
const panel = reactive({
style: '',
styleEl: null,
html: '',
})
const testPanel = ref()
onMounted(() => {
axios.post(appUrl() + '/panel/get').then(async (data) => {
// console.log(data.data.html)
if (data.data) {
setPanelStyle(data.data.css)
panel.html = data.data.html
await nextTick()
addButtonEventListeners()
}
})
})
const setPanelStyle = (styleStr) => {
const styleEl = document.createElement('style')
styleEl.setAttribute('custom_panel_style', true)
styleEl.innerHTML = styleStr
document.head.appendChild(styleEl)
panel.styleEl = styleEl
}
const addButtonEventListeners = () => {
testPanel.value.querySelectorAll('[mcrm__button]').forEach((button) => {
button.addEventListener('click', () => {
console.log(button.id)
if (button.id == 'button_1') {
axios.post(appUrl() + '/macro/play', { macro: 'task_manager' })
}
})
})
}
</script>
<style>
@reference "@/assets/main.css";
[mcrm__button] {
@apply cursor-pointer;
}
</style>

View file

@ -0,0 +1,236 @@
<template>
<div id="panel-edit" class="mcrm-block block__dark !p-0 !gap-0" v-if="editPanel">
<div class="panel-preview">
<div class="panel-preview__content" ref="panelPreview" v-html="editPanel.html"></div>
</div>
<div class="panel-settings">
<AccordionComp title="Panel info" ref="infoAccordion">
<div class="grid grid-cols-[auto_1fr] gap-2 p-4">
<span>Name:</span><strong class="text-right">{{ editPanel.name }}</strong>
<span>Aspect ratio:</span><strong class="text-right">{{ editPanel.aspectRatio }}</strong>
<template v-if="editPanel.macros">
<span>Linked Macros:</span>
<strong class="text-right">{{ Object.keys(editPanel.macros).length }}</strong>
</template>
</div>
</AccordionComp>
<div>
<AccordionComp
v-if="editButton.id"
title="Button"
ref="buttonAccordion"
:open="editButton.id != ''"
>
<div class="grid gap-4 p-4">
<div class="grid grid-cols-[auto_1fr] gap-2">
<span>Button ID:</span>
<strong class="text-right">{{ editButton.id }}</strong>
</div>
<div class="grid">
<FormSelect
name="button_macro"
label="Button macro"
:search="true"
:options="macroList"
:value="editButton.macro"
@change="checkNewMacro(editButton.id, $event)"
/>
<div class="grid grid-cols-2 mt-4">
<ButtonComp
v-if="editButton.macro != ''"
class="col-start-1 w-fit"
size="sm"
variant="danger"
@click="unlinkMacro(editButton.id)"
ref="unlinkButton"
>
<IconTrash /> Unlink
</ButtonComp>
<ButtonComp
v-if="editButton.changed"
class="col-start-2 w-fit justify-self-end"
size="sm"
variant="primary"
@click="linkMacro(editButton.id)"
ref="linkButton"
>
<IconLink /> Link
</ButtonComp>
</div>
</div>
</div>
</AccordionComp>
</div>
<footer class="flex items-end justify-end h-full p-4">
<ButtonComp v-if="panelMacros.changed" variant="success" @click="savePanelChanges()">
<IconDeviceFloppy /> Save changes
</ButtonComp>
</footer>
</div>
</div>
</template>
<script setup>
import { CheckMacroListChange, GetMacroList } from '@/services/MacroService'
import {
PanelButtonListeners,
RemovePanelStyle,
SetPanelStyle,
StripPanelHTML,
} from '@/services/PanelService'
import { usePanelStore } from '@/stores/panel'
import { onMounted, onUnmounted, onUpdated, reactive, ref } from 'vue'
import AccordionComp from '../base/AccordionComp.vue'
import FormSelect from '../form/FormSelect.vue'
import ButtonComp from '../base/ButtonComp.vue'
import { IconDeviceFloppy, IconLink, IconTrash } from '@tabler/icons-vue'
import axios from 'axios'
import { appUrl } from '@/services/ApiService'
const props = defineProps({
dirname: String,
})
const panel = usePanelStore()
const panelPreview = ref(false)
const editPanel = ref({})
const panelMacros = reactive({
old: {},
changed: false,
})
const macroList = ref({})
const infoAccordion = ref(false)
const buttonAccordion = ref(false)
const unlinkButton = ref(null)
const linkButton = ref(null)
const editButton = reactive({
id: '',
macro: '',
newMacro: '',
changed: false,
})
onMounted(async () => {
const currentPanel = await panel.get(props.dirname)
editPanel.value = currentPanel
editPanel.value.dir = props.dirname
editPanel.value.html = StripPanelHTML(editPanel.value.html, editPanel.value.aspectRatio)
panelMacros.old = JSON.stringify(currentPanel.macros)
infoAccordion.value.toggleAccordion(true)
const macros = await GetMacroList()
macroList.value = Object.assign(
{},
...Object.keys(macros).map((key) => ({
[key]: { value: macros[key].macroname, label: macros[key].name },
})),
)
SetPanelStyle(editPanel.value.style)
EditButtonListeners()
})
onUpdated(() => {
console.log('updated')
})
onUnmounted(() => {
RemovePanelStyle()
})
function EditButtonListeners() {
const callback = (button) => {
infoAccordion.value.toggleAccordion(false)
setEditButton(button.id)
}
PanelButtonListeners(panelPreview.value, callback)
}
function setEditButton(id) {
editButton.id = id
editButton.macro = editPanel.value.macros[id] ? editPanel.value.macros[id] : ''
}
function checkNewMacro(id, macro) {
editButton.changed = editPanel.value.macros[id] != macro
editButton.newMacro = macro
}
function linkMacro(id) {
editPanel.value.macros[id] = editButton.newMacro
editButton.macro = editButton.newMacro
editButton.newMacro = ''
panelMacros.changed = CheckMacroListChange(panelMacros.old, editPanel.value.macros)
}
function unlinkMacro(id) {
delete editPanel.value.macros[id]
buttonAccordion.value.toggleAccordion(false)
panelMacros.changed = CheckMacroListChange(panelMacros.old, editPanel.value.macros)
}
function savePanelChanges() {
const panelData = {
dir: editPanel.value.dir,
name: editPanel.value.name,
description: editPanel.value.description,
aspectRatio: editPanel.value.aspectRatio,
macros: editPanel.value.macros,
}
axios.post(appUrl() + '/panel/save/json', panelData).then((data) => {
console.log(data)
})
}
</script>
<style>
@reference "@/assets/main.css";
[mcrm__button] {
@apply cursor-pointer;
}
#panel-edit {
@apply grid
grid-cols-[1fr_30ch]
size-full
overflow-hidden;
.panel-preview {
@apply border-r
border-slate-700;
.panel-preview__content {
@apply relative
grid
justify-center
size-full
p-8;
#panel-html__body {
@apply size-full
max-w-full max-h-full;
}
}
}
.panel-settings {
@apply grid
grid-rows-[auto_auto_1fr]
bg-black/30;
}
}
</style>

View file

@ -0,0 +1,74 @@
<template>
<div id="panel-view">
<div class="panel-preview__content" ref="panelView" v-html="viewPanel.html"></div>
</div>
</template>
<script setup>
import { RunMacro } from '@/services/MacroService'
import {
PanelButtonListeners,
RemovePanelStyle,
SetPanelStyle,
StripPanelHTML,
} from '@/services/PanelService'
import { usePanelStore } from '@/stores/panel'
import { onMounted, onUnmounted, ref } from 'vue'
const panel = usePanelStore()
const props = defineProps({
dirname: String,
})
const panelView = ref(null)
const viewPanel = ref({})
onMounted(async () => {
const currentPanel = await panel.get(props.dirname)
viewPanel.value = currentPanel
viewPanel.value.html = StripPanelHTML(viewPanel.value.html, viewPanel.value.aspectRatio)
SetPanelStyle(viewPanel.value.style)
setTimeout(() => {
viewPanelButtonListeners()
}, 50)
})
onUnmounted(() => {
RemovePanelStyle()
})
const viewPanelButtonListeners = () => {
const callback = (button) => {
RunMacro(viewPanel.value.macros[button.id])
}
PanelButtonListeners(panelView.value, callback)
}
</script>
<style scoped>
@reference "@/assets/main.css";
#panel-view {
@apply fixed
inset-0
size-full
bg-black;
.panel-preview__content {
@apply relative
grid
justify-center
size-full;
#panel-html__body {
@apply size-full
max-w-full max-h-full;
}
}
}
</style>

View file

@ -1,15 +1,35 @@
<template>
<div id="panels-overview">
<!-- <AlertComp v-if="Object.keys(panels.list).length == 0" type="info">No panels found</AlertComp> -->
<AlertComp v-if="Object.keys(panels.list).length == 0" variant="info">
No panels found
</AlertComp>
<div class="panel-list">
<div class="panel-item" v-for="(panel, i) in panels.list" :key="i">
<!-- <router-link :to="'/panel/' + panel.id"> -->
<div class="panel-item__content">
<img :src="panel.thumb" alt="" />
<h4>{{ panel.name }}</h4>
<p>{{ panel.description }}</p>
<div class="panel-item mcrm-block block__dark" v-for="(panel, i) in panels.list" :key="i">
<div class="panel-item__content" @click="panelItemClick(panel.dir)">
<div class="thumb">
<img v-if="panel.thumb" :src="`data:image/jpeg;base64,${panel.thumb}`" alt="" />
<IconLayoutGrid v-else />
</div>
<!-- </router-link> -->
<h4>{{ panel.name }}</h4>
<div class="description" v-if="isLocal()">
<div class="content">
<strong class="block mb-1 text-slate-400">{{ panel.name }}</strong>
<hr class="mb-2 border-slate-600" />
<p v-if="panel.description != 'null'" class="text-slate-200">
{{ panel.description }}
</p>
</div>
<footer>
<ButtonComp variant="subtle" size="sm" :href="`/panel/view/${panel.dir}`">
<IconEye /> Preview
</ButtonComp>
<ButtonComp variant="primary" size="sm" :href="`/panel/edit/${panel.dir}`">
<IconPencil /> Edit
</ButtonComp>
</footer>
</div>
</div>
<template v-if="!isLocal()"> </template>
</div>
</div>
</div>
@ -19,6 +39,10 @@
import { usePanelStore } from '@/stores/panel'
import { onMounted, reactive } from 'vue'
import AlertComp from '../base/AlertComp.vue'
import { IconEye, IconLayoutGrid, IconPencil } from '@tabler/icons-vue'
import ButtonComp from '../base/ButtonComp.vue'
import { isLocal } from '@/services/ApiService'
import { useRouter } from 'vue-router'
const panel = usePanelStore()
@ -26,12 +50,20 @@ const panels = reactive({
list: {},
})
const router = useRouter()
onMounted(async () => {
const panelList = await panel.getPanels()
console.log(panelList)
const panelList = await panel.getList()
// console.log(panelList)
panels.list = panelList
})
function panelItemClick(dir) {
if (isLocal()) return
router.push(`/panel/view/${dir}`)
}
</script>
<style scoped>
@ -43,6 +75,84 @@ onMounted(async () => {
md:grid-cols-4
lg:grid-cols-6
gap-4
size-full;
w-full h-fit;
}
.panel-item {
@apply p-px
overflow-hidden;
.thumb {
@apply flex
justify-center
items-center
w-full
aspect-[4/3];
&:not(:has(img)) {
@apply bg-sky-950;
}
svg {
@apply size-12;
}
}
h4 {
@apply px-4 py-2
h-12
truncate;
}
&:hover .description {
@apply opacity-100;
}
.description {
@apply absolute
inset-0
size-full
pt-2
pr-1
pb-13
bg-slate-900/60
backdrop-blur-md
text-slate-100
opacity-0
transition-opacity
cursor-default
z-10;
.content {
@apply h-full
p-4
pt-2
overflow-y-auto;
}
footer {
@apply absolute
bottom-0 left-0
w-full
h-12
grid
grid-cols-2
bg-slate-900
border-t
border-slate-600;
.btn {
@apply size-full
rounded-none
justify-center
border-0;
&:last-child {
@apply border-l
border-slate-600;
}
}
}
}
}
</style>

View file

@ -0,0 +1,36 @@
export const SetPanelStyle = (styleStr) => {
const styleEl = document.createElement('style')
styleEl.setAttribute('custom_panel_style', true)
styleEl.innerHTML = styleStr
document.head.appendChild(styleEl)
}
export const RemovePanelStyle = () => {
const styleEl = document.querySelector('style[custom_panel_style]')
if (styleEl) {
styleEl.remove()
}
}
export const StripPanelHTML = (html, aspectRatio) => {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
const body = doc.body
const bodyContents = body.innerHTML
const panelBody = document.createElement('div')
panelBody.id = 'panel-html__body'
panelBody.style = `aspect-ratio: ${aspectRatio}`
panelBody.innerHTML = bodyContents
return panelBody.outerHTML
}
export const PanelButtonListeners = (panelEl, callback) => {
panelEl.querySelectorAll('[mcrm__button]').forEach((button) => {
button.addEventListener('click', () => {
callback(button)
})
})
}

View file

@ -1,19 +1,62 @@
<template>
<div id="macros" class="panel">
<h1 class="panel__title">
Panels <span class="text-sm">{{ isLocal() ? 'remote' : 'servers' }}</span>
</h1>
<div class="panel__content !p-0">
<div class="macro-panel__content">
<PanelsOverview />
<div id="panels" class="panel">
<h1 class="flex items-end justify-between !w-full panel__title">
<div>
Panels
<span class="text-sm">{{ isLocal() ? 'remote' : 'servers' }}</span>
</div>
<ButtonComp
v-if="panel.function != 'overview'"
variant="subtle"
size="sm"
@click="router.push('/panels')"
>
<IconArrowLeft /> Overview
</ButtonComp>
</h1>
<div :class="`panel__content !p-0 !pt-4 ${panel.function == 'overview' ?? '!pr-4'}`">
<PanelsOverview v-if="panel.function == 'overview'" />
<PanelEdit v-if="panel.function == 'edit'" :dirname="panel.dirname" />
<PanelView v-if="panel.function == 'preview'" :dirname="panel.dirname" />
</div>
</div>
</template>
<script setup>
import ButtonComp from '@/components/base/ButtonComp.vue'
import PanelEdit from '@/components/panels/PanelEdit.vue'
import PanelView from '@/components/panels/PanelView.vue'
import PanelsOverview from '@/components/panels/PanelsOverview.vue'
import { isLocal } from '@/services/ApiService'
import { IconArrowLeft } from '@tabler/icons-vue'
import { onMounted, onUpdated, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const panel = reactive({
function: '',
dirname: '',
})
onMounted(() => {
setVarsByRoute()
})
onUpdated(() => {
setVarsByRoute()
})
const setVarsByRoute = () => {
if (route.name.includes('panel-')) {
panel.function = route.name == 'panel-edit' ? 'edit' : 'preview'
} else {
panel.function = 'overview'
}
panel.dirname = route.params.dirname
}
</script>
<style scoped>

View file

@ -1,6 +0,0 @@
{
"name": "Almost empty panel",
"description": "This is the third panel to be created. It is also a test panel.",
"aspectRatio": "4/3",
"macros": {}
}

View file

@ -6,116 +6,116 @@
<title>Document</title>
<link rel="stylesheet" href="./output.css" />
</head>
<body class="bg-slate-400 w-screen h-screen m-0">
<div class="h-full aspect-[9/20] bg-slate-500 border border-red-500">
<div class="size-full grid grid-cols-2 grid-rows-8 gap-2">
<body class="bg-slate-400 w-screen h-screen m-0 aspect-[9/20]">
<div class="h-full bg-slate-500">
<div class="grid grid-cols-2 gap-2 size-full grid-rows-8">
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_1"
mcrm__button
>
button1
Task manager
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_2"
mcrm__button
>
button2
Close window
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_3"
mcrm__button
>
button3
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_4"
mcrm__button
>
button4
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_5"
mcrm__button
>
button5
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_6"
mcrm__button
>
button6
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_7"
mcrm__button
>
button7
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_8"
mcrm__button
>
button8
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_9"
mcrm__button
>
button9
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_10"
mcrm__button
>
button10
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_11"
mcrm__button
>
button11
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_12"
mcrm__button
>
button12
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_13"
mcrm__button
>
button13
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_14"
mcrm__button
>
button14
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_15"
mcrm__button
>
button15
</div>
<div
class="bg-sky-400 flex justify-center items-center"
class="flex items-center justify-center bg-sky-400"
id="button_16"
mcrm__button
>

View file

@ -1,8 +1 @@
{
"name": "Test Panel 1",
"description": "This is the very first panel to be created. It is a test panel.",
"aspectRatio": "9/20",
"macros": {
"button_1": "task_manager"
}
}
{"dir":"","name":"Test Panel 1","description":"This is the very first panel to be created. It is a test panel. I'm gonna add more words to this description, not because it's an unclear description. But more so to test long descriptions in the UI. Very Nice.","aspectRatio":"10/20","macros":{"button_1":"Task_manager","button_16":"Task_manager","button_2":"ALT+F4"}}