280 lines
9.8 KiB
Vue
280 lines
9.8 KiB
Vue
<template>
|
|
<main class="p-5 overflow-auto flex flex-col items-center gap-4 min-h-0">
|
|
<div class="w-full">
|
|
<div class="bg-gradient-to-b from-white to-[#f6f9fc] border border-border rounded-[14px] shadow-[0_10px_24px_rgba(15,23,42,0.12)] w-full pt-3 px-3.5" :class="screensOpen ? 'pb-3' : 'pb-2'">
|
|
<div class="flex flex-wrap items-center justify-between gap-2.5" :class="screensOpen ? 'mb-3' : 'mb-0'">
|
|
<div class="flex items-center gap-2">
|
|
<h3 class="text-[12px] uppercase tracking-[0.08em] text-[#3a5f88]">Bildschirme</h3>
|
|
<span class="text-[11px] text-muted">{{ store.config.screens.length }}</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<button class="border border-border bg-panel-2 text-text px-2.5 py-1.5 rounded-[10px] text-[12px] font-semibold transition hover:-translate-y-0.5 hover:bg-[#e4ebf2] active:translate-y-0.5" @click="emit('open-screen-settings')">Einstellungen</button>
|
|
<button class="w-7 h-7 rounded-lg border border-border bg-panel-2 text-text grid place-items-center cursor-pointer hover:bg-[#e4ebf2] hover:border-[#b8c4d2]" @click="store.addScreen">+</button>
|
|
<button class="w-7 h-7 rounded-lg border border-border bg-panel-2 text-text grid place-items-center cursor-pointer hover:bg-[#e4ebf2] hover:border-[#b8c4d2]" @click="screensOpen = !screensOpen">
|
|
<span class="material-symbols-outlined text-[18px]">{{ screensOpen ? 'expand_less' : 'expand_more' }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div v-if="screensOpen" class="flex flex-wrap gap-2">
|
|
<button
|
|
v-for="screen in store.config.screens"
|
|
:key="screen.id"
|
|
class="border rounded-full px-3 py-1.5 text-left text-[12px] inline-flex items-center gap-2 cursor-pointer transition hover:-translate-y-0.5"
|
|
:class="screen.id === store.activeScreenId ? 'bg-[#2f6db8] text-white border-[#2b62a5] shadow-[0_0_0_2px_rgba(47,109,184,0.2)]' : 'bg-[#eef3f7] text-[#2d3c4a] border-[#c8d2dc] hover:bg-white'"
|
|
@click="selectScreen(screen.id)"
|
|
>
|
|
<span class="max-w-[160px] overflow-hidden text-ellipsis whitespace-nowrap">{{ screen.name }}</span>
|
|
<span
|
|
class="text-[10px] px-1.5 py-0.5 rounded-full uppercase tracking-[0.06em]"
|
|
:class="screen.id === store.activeScreenId ? 'bg-white/20 text-white' : 'bg-[#dfe9f3] text-[#3a5f88]'"
|
|
>
|
|
{{ screen.mode === 1 ? 'Modal' : 'Fullscreen' }}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="p-[18px] rounded-[18px] border border-border bg-white shadow-[0_10px_24px_rgba(15,23,42,0.12)]">
|
|
<div
|
|
class="relative border border-border overflow-hidden"
|
|
id="canvas"
|
|
:style="canvasStyle"
|
|
@click.self="deselect"
|
|
>
|
|
<div
|
|
class="pointer-events-none absolute inset-0 z-0 transition-opacity"
|
|
:class="store.showGrid ? 'opacity-50' : 'opacity-0'"
|
|
:style="gridStyle"
|
|
></div>
|
|
<WidgetElement
|
|
v-for="widget in rootWidgets"
|
|
:key="widget.id"
|
|
:widget="widget"
|
|
:scale="store.canvasScale"
|
|
:selected="store.selectedWidgetId === widget.id"
|
|
@select="store.selectedWidgetId = widget.id"
|
|
@drag-start="startDrag($event)"
|
|
@resize-start="startResize($event)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue';
|
|
import { useEditorStore } from '../stores/editor';
|
|
import WidgetElement from './WidgetElement.vue';
|
|
import { DISPLAY_W, DISPLAY_H, WIDGET_TYPES } from '../constants';
|
|
import { clamp, minSizeFor } from '../utils';
|
|
|
|
const emit = defineEmits(['open-screen-settings']);
|
|
const store = useEditorStore();
|
|
const screensOpen = ref(true);
|
|
|
|
const rootWidgets = computed(() => {
|
|
return store.activeScreen?.widgets.filter(w => w.parentId === -1) || [];
|
|
});
|
|
|
|
const canvasW = computed(() => {
|
|
if (store.activeScreen?.mode === 1) { // Modal
|
|
return store.activeScreen.modal.w || DISPLAY_W;
|
|
}
|
|
return DISPLAY_W;
|
|
});
|
|
|
|
const canvasH = computed(() => {
|
|
if (store.activeScreen?.mode === 1) { // Modal
|
|
return store.activeScreen.modal.h || DISPLAY_H;
|
|
}
|
|
return DISPLAY_H;
|
|
});
|
|
|
|
const canvasStyle = computed(() => ({
|
|
width: `${canvasW.value * store.canvasScale}px`,
|
|
height: `${canvasH.value * store.canvasScale}px`,
|
|
backgroundColor: store.activeScreen?.bgColor || '#1A1A2E'
|
|
}));
|
|
|
|
const gridStyle = computed(() => ({
|
|
backgroundImage: 'linear-gradient(to right, rgba(255, 255, 255, 0.1) 1px, transparent 1px), linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 1px, transparent 1px)',
|
|
backgroundSize: `${store.gridSize * store.canvasScale}px ${store.gridSize * store.canvasScale}px`
|
|
}));
|
|
|
|
function deselect() {
|
|
store.selectedWidgetId = null;
|
|
}
|
|
|
|
function selectScreen(id) {
|
|
store.activeScreenId = id;
|
|
store.selectedWidgetId = null;
|
|
}
|
|
|
|
function snap(val) {
|
|
if (!store.snapToGrid) return val;
|
|
const s = store.gridSize;
|
|
return Math.round(val / s) * s;
|
|
}
|
|
|
|
let dragState = null;
|
|
let resizeState = null;
|
|
|
|
function startDrag(payload) {
|
|
const { id, event: e } = payload;
|
|
if (e.target.closest('[data-resize-handle]')) return;
|
|
e.preventDefault();
|
|
store.selectedWidgetId = id;
|
|
|
|
const cx = e.touches ? e.touches[0].clientX : e.clientX;
|
|
const cy = e.touches ? e.touches[0].clientY : e.clientY;
|
|
|
|
const widget = store.activeScreen.widgets.find(w => w.id === id);
|
|
if (!widget) return;
|
|
|
|
dragState = {
|
|
id,
|
|
startX: cx,
|
|
startY: cy,
|
|
origX: widget.x,
|
|
origY: widget.y
|
|
};
|
|
resizeState = null;
|
|
|
|
document.addEventListener('mousemove', drag);
|
|
document.addEventListener('mouseup', endDrag);
|
|
document.addEventListener('touchmove', drag, { passive: false });
|
|
document.addEventListener('touchend', endDrag);
|
|
}
|
|
|
|
function drag(e) {
|
|
if (!dragState) return;
|
|
e.preventDefault();
|
|
|
|
const cx = e.touches ? e.touches[0].clientX : e.clientX;
|
|
const cy = e.touches ? e.touches[0].clientY : e.clientY;
|
|
|
|
const dx = (cx - dragState.startX) / store.canvasScale;
|
|
const dy = (cy - dragState.startY) / store.canvasScale;
|
|
|
|
const w = store.activeScreen.widgets.find(x => x.id === dragState.id);
|
|
if (w) {
|
|
let nx = dragState.origX + dx;
|
|
let ny = dragState.origY + dy;
|
|
|
|
if (store.snapToGrid) {
|
|
nx = snap(nx);
|
|
ny = snap(ny);
|
|
}
|
|
|
|
// Determine bounds
|
|
let maxW = canvasW.value;
|
|
let maxH = canvasH.value;
|
|
|
|
if (w.parentId !== -1) {
|
|
const parent = store.activeScreen.widgets.find(p => p.id === w.parentId);
|
|
if (parent) {
|
|
if (parent.type === WIDGET_TYPES.TABPAGE) {
|
|
// Parent is a TabPage (w=0, h=0), so look at grandparent (TabView)
|
|
const grandParent = store.activeScreen.widgets.find(gp => gp.id === parent.parentId);
|
|
if (grandParent) {
|
|
maxW = grandParent.w;
|
|
maxH = grandParent.h;
|
|
// Adjust for tab bar? For simplicity use full size for now
|
|
}
|
|
} else {
|
|
maxW = parent.w;
|
|
maxH = parent.h;
|
|
}
|
|
}
|
|
}
|
|
|
|
w.x = Math.max(0, Math.min(maxW - w.w, Math.round(nx)));
|
|
w.y = Math.max(0, Math.min(maxH - w.h, Math.round(ny)));
|
|
}
|
|
}
|
|
|
|
function endDrag() {
|
|
dragState = null;
|
|
document.removeEventListener('mousemove', drag);
|
|
document.removeEventListener('mouseup', endDrag);
|
|
document.removeEventListener('touchmove', drag);
|
|
document.removeEventListener('touchend', endDrag);
|
|
}
|
|
|
|
function startResize(payload) {
|
|
const { id, event: e } = payload;
|
|
e.preventDefault();
|
|
store.selectedWidgetId = id;
|
|
|
|
const cx = e.touches ? e.touches[0].clientX : e.clientX;
|
|
const cy = e.touches ? e.touches[0].clientY : e.clientY;
|
|
|
|
const widget = store.activeScreen.widgets.find(w => w.id === id);
|
|
if (!widget) return;
|
|
|
|
resizeState = {
|
|
id,
|
|
startX: cx,
|
|
startY: cy,
|
|
origW: widget.w,
|
|
origH: widget.h
|
|
};
|
|
dragState = null;
|
|
|
|
document.addEventListener('mousemove', resizeDrag);
|
|
document.addEventListener('mouseup', endResize);
|
|
document.addEventListener('touchmove', resizeDrag, { passive: false });
|
|
document.addEventListener('touchend', endResize);
|
|
}
|
|
|
|
function resizeDrag(e) {
|
|
if (!resizeState) return;
|
|
e.preventDefault();
|
|
|
|
const cx = e.touches ? e.touches[0].clientX : e.clientX;
|
|
const cy = e.touches ? e.touches[0].clientY : e.clientY;
|
|
|
|
const dx = (cx - resizeState.startX) / store.canvasScale;
|
|
const dy = (cy - resizeState.startY) / store.canvasScale;
|
|
|
|
const w = store.activeScreen.widgets.find(x => x.id === resizeState.id);
|
|
if (!w) return;
|
|
|
|
const minSize = minSizeFor(w);
|
|
const maxW = canvasW.value - w.x;
|
|
const maxH = canvasH.value - w.y;
|
|
|
|
let rawW = resizeState.origW + dx;
|
|
let rawH = resizeState.origH + dy;
|
|
|
|
if (store.snapToGrid) {
|
|
rawW = snap(rawW);
|
|
rawH = snap(rawH);
|
|
}
|
|
|
|
let newW = Math.round(rawW);
|
|
let newH = Math.round(rawH);
|
|
|
|
if (w.type === WIDGET_TYPES.LED) {
|
|
const maxSize = Math.min(maxW, maxH);
|
|
const size = clamp(Math.max(newW, newH), minSize.w, maxSize);
|
|
newW = size;
|
|
newH = size;
|
|
} else {
|
|
newW = clamp(newW, minSize.w, maxW);
|
|
newH = clamp(newH, minSize.h, maxH);
|
|
}
|
|
|
|
w.w = newW;
|
|
w.h = newH;
|
|
}
|
|
|
|
function endResize() {
|
|
resizeState = null;
|
|
document.removeEventListener('mousemove', resizeDrag);
|
|
document.removeEventListener('mouseup', endResize);
|
|
document.removeEventListener('touchmove', resizeDrag);
|
|
document.removeEventListener('touchend', endResize);
|
|
}
|
|
</script>
|