TKK_E32230273/design.php

734 lines
34 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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,'&quot;')}" 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>