734 lines
34 KiB
PHP
734 lines
34 KiB
PHP
<?php include 'header.php'; ?>
|
||
<div class="main-panel">
|
||
<div class="content-wrapper" style="padding:8px 15px;">
|
||
|
||
<!-- TOOLBAR -->
|
||
<div class="card mb-2" style="border-radius:10px;">
|
||
<div class="card-body p-2 d-flex align-items-center flex-wrap" style="gap:8px;">
|
||
<input type="text" id="projectName" class="form-control form-control-sm" style="width:150px;border-radius:6px;" value="Untitled">
|
||
<select id="savedDesigns" class="form-control form-control-sm" style="width:140px;border-radius:6px;"><option value="0">-- New --</option></select>
|
||
<div class="btn-group btn-group-sm">
|
||
<button class="btn btn-primary" onclick="TFT.save()" title="Save"><i class="ti-save"></i> Save</button>
|
||
<button class="btn btn-outline-primary" onclick="TFT.loadList()" title="Load"><i class="ti-folder"></i></button>
|
||
<button class="btn btn-outline-primary" onclick="TFT.exportCpp()" title="Export C++"><i class="ti-download"></i> C++</button>
|
||
<button class="btn btn-success" onclick="TFT.sync()" title="Sync to ESP32"><i class="ti-pulse"></i> Sync</button>
|
||
<button class="btn btn-info" onclick="TFT.pullESP()" title="Pull from ESP32"><i class="ti-import"></i> Pull</button>
|
||
</div>
|
||
<select id="orientationSelect" class="form-control form-control-sm" style="width:130px;border-radius:6px;" onchange="TFT.setOrientation(this.value)">
|
||
<option value="landscape">Landscape 480×320</option>
|
||
<option value="portrait">Portrait 320×480</option>
|
||
</select>
|
||
<div class="btn-group btn-group-sm ml-auto">
|
||
<button class="btn btn-outline-danger btn-sm" onclick="TFT.deleteSelected()" title="Delete"><i class="ti-trash"></i></button>
|
||
<button class="btn btn-outline-secondary btn-sm" onclick="TFT.undo()" title="Undo"><i class="ti-back-left"></i></button>
|
||
<button class="btn btn-outline-secondary btn-sm" onclick="TFT.redo()" title="Redo"><i class="ti-back-right"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MAIN EDITOR -->
|
||
<div class="d-flex" style="gap:8px; height:calc(100vh - 170px);">
|
||
|
||
<!-- LEFT: Component Library -->
|
||
<div class="card" style="width:190px;min-width:190px;border-radius:10px;overflow-y:auto;">
|
||
<div class="card-body p-2">
|
||
<p class="clib-title">LAYOUTS</p>
|
||
<div class="clib-item" draggable="true" data-comp="sidebar"><i class="ti-layout-sidebar-left"></i>Sidebar</div>
|
||
<div class="clib-item" draggable="true" data-comp="header"><i class="ti-layout-tab-window"></i>Header / Navbar</div>
|
||
<div class="clib-item" draggable="true" data-comp="card"><i class="ti-widget"></i>Card / Panel</div>
|
||
<p class="clib-title mt-2">CONTROLS</p>
|
||
<div class="clib-item" draggable="true" data-comp="button"><i class="ti-control-stop"></i>Button</div>
|
||
<div class="clib-item" draggable="true" data-comp="toggle"><i class="ti-exchange-vertical"></i>Toggle Switch</div>
|
||
<div class="clib-item" draggable="true" data-comp="progressbar"><i class="ti-stats-up"></i>Progress Bar</div>
|
||
<p class="clib-title mt-2">DATA & TYPOGRAPHY</p>
|
||
<div class="clib-item" draggable="true" data-comp="datavalue"><i class="ti-dashboard"></i>Data Value</div>
|
||
<div class="clib-item" draggable="true" data-comp="label"><i class="ti-text"></i>Text / Label</div>
|
||
<div class="clib-item" draggable="true" data-comp="icon"><i class="ti-star"></i>Icon</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CENTER: Canvas + Hierarchy -->
|
||
<div class="flex-grow-1 d-flex flex-column" style="gap:6px;min-width:0;">
|
||
<!-- Hierarchy Tree -->
|
||
<div class="card" style="border-radius:8px;max-height:90px;overflow-y:auto;">
|
||
<div class="card-body p-1 px-2">
|
||
<div id="hierarchyTree" style="font-size:11px;font-family:monospace;"></div>
|
||
</div>
|
||
</div>
|
||
<!-- Canvas -->
|
||
<div class="card flex-grow-1" style="border-radius:10px;overflow:hidden;display:flex;align-items:center;justify-content:center;background:#2a2a2e;">
|
||
<div id="canvasFrame" style="position:relative;width:480px;height:320px;background:#1e2223;box-shadow:0 4px 24px rgba(0,0,0,.4);overflow:hidden;border:1px solid #444;">
|
||
<div id="tftCanvas" style="width:100%;height:100%;position:relative;"></div>
|
||
</div>
|
||
</div>
|
||
<!-- Screen Manager -->
|
||
<div class="card" style="border-radius:8px;">
|
||
<div class="card-body p-2 d-flex align-items-center" style="gap:6px;overflow-x:auto;">
|
||
<span style="font-size:10px;color:#888;font-weight:700;letter-spacing:1px;white-space:nowrap;">SCREENS</span>
|
||
<div id="screenTabs" class="d-flex" style="gap:4px;"></div>
|
||
<button class="btn btn-sm btn-outline-primary" onclick="TFT.addScreen()" style="border-radius:6px;white-space:nowrap;"><i class="ti-plus"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RIGHT: Property Inspector -->
|
||
<div class="card" style="width:270px;min-width:270px;border-radius:10px;overflow-y:auto;" id="propsPanel">
|
||
<div class="card-body p-2">
|
||
<div id="propsContent">
|
||
<p class="text-muted text-center mt-4" style="font-size:12px;">Select a component<br>to edit properties</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /d-flex main -->
|
||
</div><!-- /content-wrapper -->
|
||
<?php include 'footer.php'; ?>
|
||
|
||
<style>
|
||
.clib-title{font-size:10px;font-weight:700;color:#999;letter-spacing:1px;margin-bottom:4px;}
|
||
.clib-item{padding:7px 10px;border:1px solid #eee;border-radius:8px;margin-bottom:4px;cursor:grab;font-size:12px;display:flex;align-items:center;gap:8px;transition:all .15s;background:#fff;}
|
||
.clib-item:hover{background:#f0eeff;border-color:#c5c3f0;transform:translateX(2px);}
|
||
.clib-item i{font-size:15px;color:#4B49AC;width:18px;text-align:center;}
|
||
.screen-tab{padding:5px 12px;border:1px solid #555;border-radius:6px;font-size:11px;cursor:pointer;background:#333;color:#ccc;white-space:nowrap;transition:.15s;}
|
||
.screen-tab.active{background:#4B49AC;color:#fff;border-color:#4B49AC;}
|
||
.screen-tab:hover{opacity:.85;}
|
||
|
||
/* Canvas Components */
|
||
.tft-comp{position:absolute;box-sizing:border-box;cursor:move;user-select:none;transition:box-shadow .1s;}
|
||
.tft-comp:hover{box-shadow:0 0 0 1px #7B7BFF;}
|
||
.tft-comp.selected{box-shadow:0 0 0 2px #4B49AC !important;z-index:100;}
|
||
.tft-comp .resize-handle{position:absolute;right:-3px;bottom:-3px;width:8px;height:8px;background:#4B49AC;border-radius:1px;cursor:nwse-resize;display:none;}
|
||
.tft-comp.selected .resize-handle{display:block;}
|
||
.tft-comp-label{position:absolute;top:-14px;left:0;font-size:8px;color:#888;white-space:nowrap;pointer-events:none;}
|
||
|
||
/* Property Inspector */
|
||
.pi-section{margin-bottom:10px;border-bottom:1px solid #f0f0f0;padding-bottom:8px;}
|
||
.pi-section:last-child{border-bottom:none;}
|
||
.pi-title{font-size:9px;font-weight:700;color:#999;letter-spacing:.5px;text-transform:uppercase;margin-bottom:4px;}
|
||
.pi-row{display:flex;gap:4px;margin-bottom:4px;align-items:center;}
|
||
.pi-row label{font-size:10px;color:#666;min-width:18px;flex-shrink:0;}
|
||
.pi-row input,.pi-row select{font-size:11px;padding:3px 5px;border:1px solid #e0e0e0;border-radius:4px;width:100%;background:#fff;}
|
||
.pi-row input[type="color"]{width:32px;height:26px;padding:1px;cursor:pointer;flex-shrink:0;}
|
||
.pi-row input[type="range"]{padding:0;}
|
||
.pi-tabs{display:flex;gap:0;margin-bottom:8px;border-bottom:1px solid #eee;}
|
||
.pi-tab{padding:5px 10px;font-size:10px;font-weight:600;cursor:pointer;border:none;background:none;color:#aaa;border-bottom:2px solid transparent;transition:.15s;}
|
||
.pi-tab.active{color:#4B49AC;border-bottom-color:#4B49AC;}
|
||
.pi-tab:hover{color:#4B49AC;}
|
||
.rgb565-badge{font-size:9px;color:#888;margin-top:1px;}
|
||
.pi-btn-group{display:flex;gap:2px;}
|
||
.pi-btn-group button{flex:1;font-size:10px;padding:3px;border:1px solid #ddd;background:#fff;border-radius:3px;cursor:pointer;}
|
||
.pi-btn-group button.active{background:#4B49AC;color:#fff;border-color:#4B49AC;}
|
||
|
||
/* Hierarchy */
|
||
.ht-node{padding:1px 4px;cursor:pointer;border-radius:3px;display:inline-flex;align-items:center;gap:3px;}
|
||
.ht-node:hover{background:#f0eeff;}
|
||
.ht-node.active{background:#4B49AC;color:#fff;}
|
||
.ht-children{padding-left:14px;border-left:1px solid #e0e0e0;margin-left:6px;}
|
||
</style>
|
||
|
||
<script>
|
||
// ===== TFT Component-Based UI Builder =====
|
||
const COMP_DEFAULTS = {
|
||
sidebar: {w:80,h:320,bg:'#1a1a2e',border:0,borderColor:'#333',radius:0,label:'Sidebar'},
|
||
header: {w:480,h:40,bg:'#16213e',border:0,borderColor:'#333',radius:0,label:'Header'},
|
||
card: {w:140,h:100,bg:'#1e1e30',border:1,borderColor:'#444',radius:8,label:'Card'},
|
||
button: {w:90,h:32,bg:'#4B49AC',border:0,borderColor:'#666',radius:6,label:'Button',text:'Button',textColor:'#FFFFFF',fontSize:12,textAlign:'center'},
|
||
toggle: {w:44,h:22,bg:'#34C759',border:0,borderColor:'transparent',radius:11,label:'Toggle',toggleState:true},
|
||
progressbar: {w:120,h:10,bg:'#333',border:0,borderColor:'transparent',radius:5,label:'Progress',value:65,barColor:'#4B49AC'},
|
||
datavalue: {w:80,h:36,bg:'transparent',border:0,borderColor:'transparent',radius:0,label:'DataValue',text:'24.5°C',textColor:'#00FF88',fontSize:18,textAlign:'center'},
|
||
label: {w:100,h:20,bg:'transparent',border:0,borderColor:'transparent',radius:0,label:'Label',text:'Label Text',textColor:'#CCCCCC',fontSize:12,textAlign:'left'},
|
||
icon: {w:28,h:28,bg:'transparent',border:0,borderColor:'transparent',radius:0,label:'Icon',icon:'★',textColor:'#FFD700',fontSize:18,textAlign:'center'}
|
||
};
|
||
|
||
const CONTAINERS = ['sidebar','header','card'];
|
||
|
||
class TFTBuilder {
|
||
constructor(){
|
||
this.screens=[{name:'Screen 1',components:[]}];
|
||
this.currentScreen=0;
|
||
this.selectedId=null;
|
||
this.idCounter=1;
|
||
this.history=[];
|
||
this.historyIdx=-1;
|
||
this.designId=0;
|
||
this.orientation='landscape';
|
||
this.init();
|
||
}
|
||
|
||
init(){
|
||
this.bindDragDrop();
|
||
this.renderScreenTabs();
|
||
this.renderHierarchy();
|
||
this.pushHistory();
|
||
this.loadList();
|
||
document.addEventListener('keydown',e=>{
|
||
if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.tagName==='SELECT')return;
|
||
if(e.key==='Delete'||e.key==='Backspace')this.deleteSelected();
|
||
if(e.ctrlKey&&e.key==='z'){e.preventDefault();this.undo();}
|
||
if(e.ctrlKey&&e.key==='y'){e.preventDefault();this.redo();}
|
||
});
|
||
}
|
||
|
||
// ===== ORIENTATION =====
|
||
setOrientation(o){
|
||
this.orientation=o;
|
||
const f=document.getElementById('canvasFrame');
|
||
if(o==='landscape'){f.style.width='480px';f.style.height='320px';}
|
||
else{f.style.width='320px';f.style.height='480px';}
|
||
this.renderCanvas();
|
||
}
|
||
getW(){return this.orientation==='landscape'?480:320;}
|
||
getH(){return this.orientation==='landscape'?320:480;}
|
||
|
||
// ===== DRAG & DROP FROM LIBRARY =====
|
||
bindDragDrop(){
|
||
document.querySelectorAll('.clib-item').forEach(el=>{
|
||
el.addEventListener('dragstart',e=>{
|
||
e.dataTransfer.setData('newComp',el.dataset.comp);
|
||
e.dataTransfer.effectAllowed='copy';
|
||
});
|
||
});
|
||
const canvas=document.getElementById('tftCanvas');
|
||
canvas.addEventListener('dragover',e=>{e.preventDefault();e.dataTransfer.dropEffect='copy';});
|
||
canvas.addEventListener('drop',e=>{
|
||
e.preventDefault();
|
||
const compType=e.dataTransfer.getData('newComp');
|
||
if(!compType)return;
|
||
const rect=canvas.getBoundingClientRect();
|
||
const x=Math.max(0,Math.round((e.clientX-rect.left)/5)*5);
|
||
const y=Math.max(0,Math.round((e.clientY-rect.top)/5)*5);
|
||
// Check if dropped on a container
|
||
let parentId=null;
|
||
const target=document.elementFromPoint(e.clientX,e.clientY);
|
||
if(target){
|
||
const parentEl=target.closest('.tft-comp[data-container="true"]');
|
||
if(parentEl)parentId=parentEl.dataset.id;
|
||
}
|
||
this.addComponent(compType,x,y,parentId);
|
||
});
|
||
canvas.addEventListener('click',e=>{
|
||
if(e.target===canvas||e.target.id==='tftCanvas'){this.select(null);}
|
||
});
|
||
}
|
||
|
||
// ===== COMPONENT MANAGEMENT =====
|
||
addComponent(type,x=10,y=10,parentId=null){
|
||
const d=JSON.parse(JSON.stringify(COMP_DEFAULTS[type]));
|
||
const id='c'+this.idCounter++;
|
||
const comp={id,type,x,y,...d,padding:4,children:[],interactions:{event:'none',action:'none',target:'',payload:''}};
|
||
if(parentId){
|
||
const parent=this.findComp(this.currentComps(),parentId);
|
||
if(parent){
|
||
// Position relative to parent
|
||
const pr=this.getAbsPos(parentId);
|
||
comp.x=Math.max(0,x-(pr?.x||0));
|
||
comp.y=Math.max(0,y-(pr?.y||0));
|
||
parent.children.push(comp);
|
||
}else{this.currentComps().push(comp);}
|
||
}else{this.currentComps().push(comp);}
|
||
this.pushHistory();
|
||
this.renderCanvas();
|
||
this.select(id);
|
||
return id;
|
||
}
|
||
|
||
currentComps(){return this.screens[this.currentScreen].components;}
|
||
|
||
findComp(arr,id){
|
||
for(const c of arr){
|
||
if(c.id===id)return c;
|
||
if(c.children){const f=this.findComp(c.children,id);if(f)return f;}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
findParent(arr,id,parent=null){
|
||
for(const c of arr){
|
||
if(c.id===id)return{parent,list:arr};
|
||
if(c.children){const f=this.findParent(c.children,id,c);if(f)return f;}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
removeComp(id){
|
||
const info=this.findParent(this.currentComps(),id);
|
||
if(!info)return;
|
||
const idx=info.list.findIndex(c=>c.id===id);
|
||
if(idx>=0)info.list.splice(idx,1);
|
||
}
|
||
|
||
getAbsPos(id){
|
||
// Walk up hierarchy to get absolute position
|
||
const walk=(arr,tx,ty)=>{
|
||
for(const c of arr){
|
||
const ax=tx+c.x, ay=ty+c.y;
|
||
if(c.id===id)return{x:ax,y:ay};
|
||
if(c.children){const r=walk(c.children,ax,ay);if(r)return r;}
|
||
}
|
||
return null;
|
||
};
|
||
return walk(this.currentComps(),0,0);
|
||
}
|
||
|
||
deleteSelected(){
|
||
if(!this.selectedId)return;
|
||
this.removeComp(this.selectedId);
|
||
this.selectedId=null;
|
||
this.pushHistory();
|
||
this.renderCanvas();
|
||
this.renderHierarchy();
|
||
this.clearProps();
|
||
}
|
||
|
||
// ===== SELECTION =====
|
||
select(id){
|
||
this.selectedId=id;
|
||
document.querySelectorAll('.tft-comp').forEach(el=>el.classList.remove('selected'));
|
||
if(id){
|
||
const el=document.querySelector(`.tft-comp[data-id="${id}"]`);
|
||
if(el)el.classList.add('selected');
|
||
this.renderProps();
|
||
}else{this.clearProps();}
|
||
this.renderHierarchy();
|
||
}
|
||
|
||
clearProps(){
|
||
document.getElementById('propsContent').innerHTML='<p class="text-muted text-center mt-4" style="font-size:12px;">Select a component<br>to edit properties</p>';
|
||
}
|
||
|
||
// ===== RENDER CANVAS =====
|
||
renderCanvas(){
|
||
const canvas=document.getElementById('tftCanvas');
|
||
canvas.innerHTML='';
|
||
this.renderComps(this.currentComps(),canvas,0,0);
|
||
}
|
||
|
||
renderComps(comps,parentEl,offX,offY){
|
||
comps.forEach(c=>{
|
||
const el=document.createElement('div');
|
||
el.className='tft-comp'+(c.id===this.selectedId?' selected':'');
|
||
el.dataset.id=c.id;
|
||
el.dataset.container=CONTAINERS.includes(c.type)?'true':'false';
|
||
el.style.left=c.x+'px';
|
||
el.style.top=c.y+'px';
|
||
el.style.width=c.w+'px';
|
||
el.style.height=c.h+'px';
|
||
el.style.backgroundColor=c.bg||'transparent';
|
||
el.style.borderRadius=(c.radius||0)+'px';
|
||
if(c.border>0)el.style.border=c.border+'px solid '+(c.borderColor||'#444');
|
||
el.style.padding=(c.padding||0)+'px';
|
||
el.style.overflow='hidden';
|
||
|
||
// Component-specific inner render
|
||
el.innerHTML=this.renderCompInner(c);
|
||
|
||
// Label
|
||
const lbl=document.createElement('div');
|
||
lbl.className='tft-comp-label';
|
||
lbl.textContent=c.type;
|
||
el.appendChild(lbl);
|
||
|
||
// Resize handle
|
||
const rh=document.createElement('div');
|
||
rh.className='resize-handle';
|
||
el.appendChild(rh);
|
||
|
||
// Events
|
||
el.addEventListener('mousedown',e=>{
|
||
if(e.target.classList.contains('resize-handle')){this.startResize(e,c);return;}
|
||
e.stopPropagation();
|
||
this.select(c.id);
|
||
this.startDrag(e,c);
|
||
});
|
||
|
||
parentEl.appendChild(el);
|
||
|
||
// Render children inside this component
|
||
if(c.children&&c.children.length){
|
||
this.renderComps(c.children,el,0,0);
|
||
}
|
||
});
|
||
}
|
||
|
||
renderCompInner(c){
|
||
switch(c.type){
|
||
case 'button':
|
||
return `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:${c.textAlign==='left'?'flex-start':c.textAlign==='right'?'flex-end':'center'};color:${c.textColor||'#fff'};font-size:${c.fontSize||12}px;pointer-events:none;font-family:Arial,sans-serif;">${c.text||'Button'}</div>`;
|
||
case 'label':
|
||
return `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:${c.textAlign==='left'?'flex-start':c.textAlign==='right'?'flex-end':'center'};color:${c.textColor||'#ccc'};font-size:${c.fontSize||12}px;pointer-events:none;font-family:Arial,sans-serif;overflow:hidden;white-space:nowrap;">${c.text||''}</div>`;
|
||
case 'datavalue':
|
||
return `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:${c.textAlign||'center'};color:${c.textColor||'#0f8'};font-size:${c.fontSize||18}px;font-weight:700;pointer-events:none;font-family:'Courier New',monospace;">${c.text||'0'}</div>`;
|
||
case 'icon':
|
||
return `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:${c.textColor||'#FFD700'};font-size:${c.fontSize||18}px;pointer-events:none;">${c.icon||'★'}</div>`;
|
||
case 'toggle':
|
||
const on=c.toggleState!==false;
|
||
return `<div style="width:100%;height:100%;border-radius:${c.radius||11}px;background:${on?'#34C759':'#555'};position:relative;pointer-events:none;"><div style="position:absolute;top:2px;${on?'right:2px':'left:2px'};width:${c.h-4}px;height:${c.h-4}px;border-radius:50%;background:#fff;"></div></div>`;
|
||
case 'progressbar':
|
||
const pct=c.value||0;
|
||
return `<div style="width:100%;height:100%;border-radius:${c.radius||5}px;background:${c.bg||'#333'};overflow:hidden;pointer-events:none;"><div style="width:${pct}%;height:100%;border-radius:${c.radius||5}px;background:${c.barColor||'#4B49AC'};"></div></div>`;
|
||
case 'sidebar':
|
||
case 'header':
|
||
case 'card':
|
||
return '';// containers render children
|
||
default:return '';
|
||
}
|
||
}
|
||
|
||
// ===== DRAG (MOVE) =====
|
||
startDrag(e,comp){
|
||
e.preventDefault();
|
||
const startX=e.clientX,startY=e.clientY;
|
||
const origX=comp.x,origY=comp.y;
|
||
const move=ev=>{
|
||
comp.x=Math.max(0,Math.round((origX+(ev.clientX-startX))/5)*5);
|
||
comp.y=Math.max(0,Math.round((origY+(ev.clientY-startY))/5)*5);
|
||
const el=document.querySelector(`.tft-comp[data-id="${comp.id}"]`);
|
||
if(el){el.style.left=comp.x+'px';el.style.top=comp.y+'px';}
|
||
if(this.selectedId===comp.id)this.updatePropXY(comp);
|
||
};
|
||
const up=()=>{
|
||
document.removeEventListener('mousemove',move);
|
||
document.removeEventListener('mouseup',up);
|
||
this.pushHistory();
|
||
};
|
||
document.addEventListener('mousemove',move);
|
||
document.addEventListener('mouseup',up);
|
||
}
|
||
|
||
// ===== RESIZE =====
|
||
startResize(e,comp){
|
||
e.preventDefault();e.stopPropagation();
|
||
const startX=e.clientX,startY=e.clientY;
|
||
const origW=comp.w,origH=comp.h;
|
||
const move=ev=>{
|
||
comp.w=Math.max(20,Math.round((origW+(ev.clientX-startX))/5)*5);
|
||
comp.h=Math.max(10,Math.round((origH+(ev.clientY-startY))/5)*5);
|
||
const el=document.querySelector(`.tft-comp[data-id="${comp.id}"]`);
|
||
if(el){el.style.width=comp.w+'px';el.style.height=comp.h+'px';el.innerHTML=this.renderCompInner(comp);
|
||
const lbl=document.createElement('div');lbl.className='tft-comp-label';lbl.textContent=comp.type;el.appendChild(lbl);
|
||
const rh=document.createElement('div');rh.className='resize-handle';el.appendChild(rh);}
|
||
if(this.selectedId===comp.id)this.updatePropWH(comp);
|
||
};
|
||
const up=()=>{
|
||
document.removeEventListener('mousemove',move);
|
||
document.removeEventListener('mouseup',up);
|
||
this.pushHistory();
|
||
this.renderCanvas();
|
||
};
|
||
document.addEventListener('mousemove',move);
|
||
document.addEventListener('mouseup',up);
|
||
}
|
||
|
||
updatePropXY(c){const ex=document.getElementById('pi_x'),ey=document.getElementById('pi_y');if(ex)ex.value=c.x;if(ey)ey.value=c.y;}
|
||
updatePropWH(c){const ew=document.getElementById('pi_w'),eh=document.getElementById('pi_h');if(ew)ew.value=c.w;if(eh)eh.value=c.h;}
|
||
|
||
// ===== PROPERTY INSPECTOR =====
|
||
renderProps(){
|
||
const c=this.findComp(this.currentComps(),this.selectedId);
|
||
if(!c){this.clearProps();return;}
|
||
const isText=['button','label','datavalue','icon'].includes(c.type);
|
||
const isContainer=CONTAINERS.includes(c.type);
|
||
const rgb565=hexToRGB565(c.bg||'#1e2223');
|
||
|
||
let h=`<div class="pi-tabs">
|
||
<button class="pi-tab active" onclick="TFT.piTab(this,0)">Layout</button>
|
||
<button class="pi-tab" onclick="TFT.piTab(this,1)">Style</button>
|
||
${isText?'<button class="pi-tab" onclick="TFT.piTab(this,2)">Text</button>':''}
|
||
<button class="pi-tab" onclick="TFT.piTab(this,3)">Actions</button>
|
||
</div>`;
|
||
|
||
// === TAB 0: Layout ===
|
||
h+=`<div class="pi-panel" id="piPanel0">`;
|
||
h+=`<div class="pi-section"><div class="pi-title">Component</div>
|
||
<div style="font-size:12px;font-weight:600;color:#4B49AC;text-transform:capitalize;margin-bottom:4px;">${c.type}</div>
|
||
<div class="pi-row"><label>ID</label><input type="text" value="${c.id}" disabled style="font-size:10px;color:#888;"></div>
|
||
<div class="pi-row"><label>Name</label><input type="text" value="${c.label||''}" onchange="TFT.setProp('label',this.value)"></div>
|
||
</div>`;
|
||
h+=`<div class="pi-section"><div class="pi-title">Position (px)</div>
|
||
<div class="pi-row"><label>X</label><input type="number" id="pi_x" value="${c.x}" onchange="TFT.setProp('x',+this.value)" step="5">
|
||
<label>Y</label><input type="number" id="pi_y" value="${c.y}" onchange="TFT.setProp('y',+this.value)" step="5"></div>
|
||
</div>`;
|
||
h+=`<div class="pi-section"><div class="pi-title">Size (px)</div>
|
||
<div class="pi-row"><label>W</label><input type="number" id="pi_w" value="${c.w}" onchange="TFT.setProp('w',+this.value)" step="5">
|
||
<label>H</label><input type="number" id="pi_h" value="${c.h}" onchange="TFT.setProp('h',+this.value)" step="5"></div>
|
||
</div>`;
|
||
if(isContainer){
|
||
h+=`<div class="pi-section"><div class="pi-title">Padding (px)</div>
|
||
<div class="pi-row"><label>All</label><input type="number" value="${c.padding||0}" onchange="TFT.setProp('padding',+this.value)" min="0" max="50"></div>
|
||
</div>`;
|
||
}
|
||
h+=`</div>`;
|
||
|
||
// === TAB 1: Style ===
|
||
h+=`<div class="pi-panel" id="piPanel1" style="display:none;">`;
|
||
h+=`<div class="pi-section"><div class="pi-title">Background</div>
|
||
<div class="pi-row"><label>Color</label><input type="color" value="${c.bg||'#1e2223'}" onchange="TFT.setProp('bg',this.value)"><input type="text" value="${c.bg||''}" style="width:80px;font-size:10px;" onchange="TFT.setProp('bg',this.value)"></div>
|
||
<div class="rgb565-badge">RGB565: 0x${rgb565.toString(16).toUpperCase().padStart(4,'0')}</div>
|
||
</div>`;
|
||
h+=`<div class="pi-section"><div class="pi-title">Border Radius</div>
|
||
<div class="pi-row"><input type="range" min="0" max="50" value="${c.radius||0}" oninput="TFT.setProp('radius',+this.value);this.nextElementSibling.value=this.value"><input type="number" value="${c.radius||0}" style="width:45px;" onchange="TFT.setProp('radius',+this.value)"></div>
|
||
</div>`;
|
||
h+=`<div class="pi-section"><div class="pi-title">Border</div>
|
||
<div class="pi-row"><label>Width</label><input type="number" value="${c.border||0}" onchange="TFT.setProp('border',+this.value)" min="0" max="10"></div>
|
||
<div class="pi-row"><label>Color</label><input type="color" value="${c.borderColor||'#444444'}" onchange="TFT.setProp('borderColor',this.value)"><input type="text" value="${c.borderColor||''}" style="width:80px;font-size:10px;" onchange="TFT.setProp('borderColor',this.value)"></div>
|
||
</div>`;
|
||
if(c.type==='progressbar'){
|
||
h+=`<div class="pi-section"><div class="pi-title">Progress Bar</div>
|
||
<div class="pi-row"><label>Value</label><input type="range" min="0" max="100" value="${c.value||0}" oninput="TFT.setProp('value',+this.value);this.nextElementSibling.value=this.value"><input type="number" value="${c.value||0}" style="width:45px;" onchange="TFT.setProp('value',+this.value)"></div>
|
||
<div class="pi-row"><label>Bar</label><input type="color" value="${c.barColor||'#4B49AC'}" onchange="TFT.setProp('barColor',this.value)"></div>
|
||
</div>`;
|
||
}
|
||
if(c.type==='toggle'){
|
||
h+=`<div class="pi-section"><div class="pi-title">Toggle</div>
|
||
<div class="pi-row"><label>State</label><select onchange="TFT.setProp('toggleState',this.value==='on')"><option value="on" ${c.toggleState!==false?'selected':''}>ON</option><option value="off" ${c.toggleState===false?'selected':''}>OFF</option></select></div>
|
||
</div>`;
|
||
}
|
||
h+=`</div>`;
|
||
|
||
// === TAB 2: Text ===
|
||
if(isText){
|
||
h+=`<div class="pi-panel" id="piPanel2" style="display:none;">`;
|
||
h+=`<div class="pi-section"><div class="pi-title">${c.type==='icon'?'Icon':'Text Content'}</div>
|
||
<div class="pi-row"><input type="text" value="${(c.text||c.icon||'').replace(/"/g,'"')}" onchange="TFT.setProp('${c.type==='icon'?'icon':'text'}',this.value)"></div>
|
||
</div>`;
|
||
h+=`<div class="pi-section"><div class="pi-title">Font</div>
|
||
<div class="pi-row"><label>Size</label><input type="number" value="${c.fontSize||12}" onchange="TFT.setProp('fontSize',+this.value)" min="8" max="48"></div>
|
||
<div class="pi-row"><label>Color</label><input type="color" value="${c.textColor||'#FFFFFF'}" onchange="TFT.setProp('textColor',this.value)"><input type="text" value="${c.textColor||''}" style="width:80px;font-size:10px;" onchange="TFT.setProp('textColor',this.value)"></div>
|
||
<div class="rgb565-badge">RGB565: 0x${hexToRGB565(c.textColor||'#FFFFFF').toString(16).toUpperCase().padStart(4,'0')}</div>
|
||
</div>`;
|
||
h+=`<div class="pi-section"><div class="pi-title">Alignment</div>
|
||
<div class="pi-btn-group">
|
||
<button class="${c.textAlign==='left'?'active':''}" onclick="TFT.setProp('textAlign','left')">Left</button>
|
||
<button class="${c.textAlign==='center'?'active':''}" onclick="TFT.setProp('textAlign','center')">Center</button>
|
||
<button class="${c.textAlign==='right'?'active':''}" onclick="TFT.setProp('textAlign','right')">Right</button>
|
||
</div>
|
||
</div>`;
|
||
h+=`</div>`;
|
||
}
|
||
|
||
// === TAB 3: Interactions ===
|
||
const inter=c.interactions||{event:'none',action:'none',target:'',payload:''};
|
||
h+=`<div class="pi-panel" id="piPanel3" style="display:none;">`;
|
||
h+=`<div class="pi-section"><div class="pi-title">Event</div>
|
||
<div class="pi-row"><select onchange="TFT.setInteraction('event',this.value)"><option value="none" ${inter.event==='none'?'selected':''}>None</option><option value="onTouch" ${inter.event==='onTouch'?'selected':''}>onTouch</option></select></div>
|
||
</div>`;
|
||
h+=`<div class="pi-section"><div class="pi-title">Action</div>
|
||
<div class="pi-row"><select onchange="TFT.setInteraction('action',this.value)"><option value="none" ${inter.action==='none'?'selected':''}>None</option><option value="navigate" ${inter.action==='navigate'?'selected':''}>Maps to Page...</option><option value="command" ${inter.action==='command'?'selected':''}>Send Command...</option></select></div>
|
||
</div>`;
|
||
h+=`<div class="pi-section"><div class="pi-title">Target / Page</div>
|
||
<div class="pi-row"><input type="text" value="${inter.target||''}" onchange="TFT.setInteraction('target',this.value)" placeholder="Screen name or page index"></div>
|
||
</div>`;
|
||
h+=`<div class="pi-section"><div class="pi-title">Payload / Command</div>
|
||
<div class="pi-row"><input type="text" value="${inter.payload||''}" onchange="TFT.setInteraction('payload',this.value)" placeholder='e.g. {"relay":1,"state":"ON"}'></div>
|
||
</div>`;
|
||
h+=`</div>`;
|
||
|
||
document.getElementById('propsContent').innerHTML=h;
|
||
}
|
||
|
||
piTab(btn,idx){
|
||
btn.parentElement.querySelectorAll('.pi-tab').forEach(b=>b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
document.querySelectorAll('.pi-panel').forEach((p,i)=>p.style.display=i===idx?'block':'none');
|
||
}
|
||
|
||
setProp(key,val){
|
||
const c=this.findComp(this.currentComps(),this.selectedId);
|
||
if(!c)return;
|
||
c[key]=val;
|
||
this.renderCanvas();
|
||
this.select(c.id);
|
||
}
|
||
|
||
setInteraction(key,val){
|
||
const c=this.findComp(this.currentComps(),this.selectedId);
|
||
if(!c)return;
|
||
if(!c.interactions)c.interactions={event:'none',action:'none',target:'',payload:''};
|
||
c.interactions[key]=val;
|
||
}
|
||
|
||
// ===== HIERARCHY TREE =====
|
||
renderHierarchy(){
|
||
const el=document.getElementById('hierarchyTree');
|
||
const screenName=this.screens[this.currentScreen].name;
|
||
let h=`<span class="ht-node" onclick="TFT.select(null)">📱 ${screenName}</span>`;
|
||
h+=this.buildTree(this.currentComps());
|
||
el.innerHTML=h;
|
||
}
|
||
|
||
buildTree(comps){
|
||
if(!comps.length)return'';
|
||
let h='<div class="ht-children">';
|
||
comps.forEach(c=>{
|
||
const icon=CONTAINERS.includes(c.type)?'📦':'⬡';
|
||
h+=`<div><span class="ht-node ${c.id===this.selectedId?'active':''}" onclick="event.stopPropagation();TFT.select('${c.id}')">${icon} ${c.label||c.type}</span>`;
|
||
if(c.children&&c.children.length)h+=this.buildTree(c.children);
|
||
h+=`</div>`;
|
||
});
|
||
h+='</div>';
|
||
return h;
|
||
}
|
||
|
||
// ===== SCREEN MANAGEMENT =====
|
||
renderScreenTabs(){
|
||
let h='';
|
||
this.screens.forEach((s,i)=>{
|
||
h+=`<div class="screen-tab ${i===this.currentScreen?'active':''}" onclick="TFT.switchScreen(${i})">
|
||
<span ondblclick="TFT.renameScreen(${i})">${s.name}</span>
|
||
${this.screens.length>1?`<span style="margin-left:4px;cursor:pointer;opacity:.5;font-size:10px;" onclick="event.stopPropagation();TFT.deleteScreen(${i})">✕</span>`:''}
|
||
</div>`;
|
||
});
|
||
document.getElementById('screenTabs').innerHTML=h;
|
||
}
|
||
|
||
switchScreen(idx){
|
||
this.currentScreen=idx;
|
||
this.selectedId=null;
|
||
this.renderCanvas();
|
||
this.renderScreenTabs();
|
||
this.renderHierarchy();
|
||
this.clearProps();
|
||
}
|
||
|
||
addScreen(){
|
||
this.screens.push({name:'Screen '+(this.screens.length+1),components:[]});
|
||
this.switchScreen(this.screens.length-1);
|
||
this.pushHistory();
|
||
}
|
||
|
||
deleteScreen(idx){
|
||
if(this.screens.length<=1)return;
|
||
this.screens.splice(idx,1);
|
||
if(this.currentScreen>=this.screens.length)this.currentScreen=this.screens.length-1;
|
||
this.switchScreen(this.currentScreen);
|
||
this.pushHistory();
|
||
}
|
||
|
||
renameScreen(idx){
|
||
const name=prompt('Screen name:',this.screens[idx].name);
|
||
if(name){this.screens[idx].name=name;this.renderScreenTabs();}
|
||
}
|
||
|
||
// ===== HISTORY =====
|
||
pushHistory(){
|
||
const state=JSON.stringify(this.screens);
|
||
this.history=this.history.slice(0,this.historyIdx+1);
|
||
this.history.push(state);
|
||
if(this.history.length>30)this.history.shift();
|
||
this.historyIdx=this.history.length-1;
|
||
}
|
||
|
||
undo(){if(this.historyIdx<=0)return;this.historyIdx--;this.restoreHistory();}
|
||
redo(){if(this.historyIdx>=this.history.length-1)return;this.historyIdx++;this.restoreHistory();}
|
||
|
||
restoreHistory(){
|
||
this.screens=JSON.parse(this.history[this.historyIdx]);
|
||
if(this.currentScreen>=this.screens.length)this.currentScreen=0;
|
||
this.selectedId=null;
|
||
this.renderCanvas();
|
||
this.renderScreenTabs();
|
||
this.renderHierarchy();
|
||
this.clearProps();
|
||
}
|
||
|
||
// ===== EXPORT JSON =====
|
||
getDesignJSON(){
|
||
return {projectName:document.getElementById('projectName').value,orientation:this.orientation,screens:this.screens};
|
||
}
|
||
|
||
// ===== API =====
|
||
save(){
|
||
const data=this.getDesignJSON();
|
||
const fd=new FormData();
|
||
fd.append('action','save');
|
||
fd.append('id',this.designId||0);
|
||
fd.append('name',data.projectName||'Untitled');
|
||
fd.append('design_json',JSON.stringify(data));
|
||
fetch('api_sync.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
|
||
if(res.status==='success'){this.designId=res.id;alert('✅ Saved!');this.loadList();}
|
||
else alert('❌ '+res.message);
|
||
});
|
||
}
|
||
|
||
loadList(){
|
||
fetch('api_sync.php?action=list').then(r=>r.json()).then(res=>{
|
||
if(res.status==='success'){
|
||
let opts='<option value="0">-- New --</option>';
|
||
res.data.forEach(d=>{opts+=`<option value="${d.id}">${d.name}</option>`;});
|
||
const sel=document.getElementById('savedDesigns');sel.innerHTML=opts;
|
||
if(this.designId)sel.value=this.designId;
|
||
sel.onchange=function(){if(this.value!=='0')TFT.loadDesign(this.value);};
|
||
}
|
||
});
|
||
}
|
||
|
||
loadDesign(id){
|
||
fetch('api_sync.php?action=load&id='+id).then(r=>r.json()).then(res=>{
|
||
if(res.status!=='success')return;
|
||
this.designId=res.data.id;
|
||
document.getElementById('projectName').value=res.data.name;
|
||
const d=JSON.parse(res.data.design_json);
|
||
this.screens=d.screens||[{name:'Screen 1',components:[]}];
|
||
this.orientation=d.orientation||'landscape';
|
||
document.getElementById('orientationSelect').value=this.orientation;
|
||
this.setOrientation(this.orientation);
|
||
this.currentScreen=0;
|
||
this.selectedId=null;
|
||
this.renderCanvas();
|
||
this.renderScreenTabs();
|
||
this.renderHierarchy();
|
||
this.clearProps();
|
||
this.pushHistory();
|
||
});
|
||
}
|
||
|
||
exportCpp(){
|
||
const data=this.getDesignJSON();
|
||
const fd=new FormData();
|
||
fd.append('action','export_cpp');
|
||
fd.append('design_json',JSON.stringify(data));
|
||
fetch('api_sync.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
|
||
if(res.status==='success'){
|
||
const blob=new Blob([res.code],{type:'text/plain'});
|
||
const a=document.createElement('a');a.href=URL.createObjectURL(blob);
|
||
a.download=(data.projectName||'tft_ui')+'.ino';a.click();
|
||
}else alert('❌ '+res.message);
|
||
});
|
||
}
|
||
|
||
sync(){this.save();alert('✅ Design synced! ESP32 will pick up changes.');}
|
||
|
||
pullESP(){
|
||
fetch('api_sync.php?action=latest').then(r=>r.json()).then(res=>{
|
||
if(res.status==='success'&&res.elements&&res.elements.length>0){
|
||
// Convert flat elements to component structure
|
||
this.screens[this.currentScreen].components=res.elements.map((el,i)=>({
|
||
id:'c'+(this.idCounter++),type:el.type||'card',x:el.x||0,y:el.y||0,w:el.w||100,h:el.h||50,
|
||
bg:el.fill||'#1e1e30',border:el.strokeWidth||0,borderColor:el.stroke||'#444',
|
||
radius:el.radius||0,padding:4,label:el.type||'Imported',text:el.text||'',
|
||
textColor:el.textColor||'#fff',fontSize:el.fontSize||12,textAlign:'center',
|
||
children:[],interactions:{event:'none',action:'none',target:'',payload:''}
|
||
}));
|
||
this.renderCanvas();this.renderHierarchy();this.pushHistory();
|
||
alert('✅ Pulled from ESP32!');
|
||
}else alert('⚠️ No elements found.');
|
||
});
|
||
}
|
||
}
|
||
|
||
// === RGB565 Helper ===
|
||
function hexToRGB565(hex){
|
||
if(!hex||hex==='transparent')return 0;
|
||
hex=hex.replace('#','');
|
||
if(hex.length===3)hex=hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
|
||
const r=parseInt(hex.substring(0,2),16);
|
||
const g=parseInt(hex.substring(2,4),16);
|
||
const b=parseInt(hex.substring(4,6),16);
|
||
return((r&0xF8)<<8)|((g&0xFC)<<3)|(b>>3);
|
||
}
|
||
|
||
// Init
|
||
let TFT;
|
||
document.addEventListener('DOMContentLoaded',()=>{TFT=new TFTBuilder();});
|
||
</script>
|