KEXEL/git
0
1<!DOCTYPE html>2<html lang="pt-BR">3<head>4<meta charset="UTF-8">5<meta name="viewport" content="width=device-width, initial-scale=1.0">6<title>SlideForge - Editor PPTX</title>7<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">8<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">9<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>10<style>11:root {12 --bg: #0c0e14;13 --surface: #13161f;14 --card: #1a1e2b;15 --card-hover: #222738;16 --border: #2a2f42;17 --border-light: #353b52;18 --text: #e4e7f0;19 --text-dim: #a0a6c0;20 --muted: #6b7394;21 --accent: #10b981;22 --accent-hover: #34d399;23 --accent-dim: rgba(16,185,129,0.15);24 --danger: #ef4444;25 --danger-dim: rgba(239,68,68,0.15);26 --warning: #f59e0b;27 --selection: rgba(16,185,129,0.4);28}29*{margin:0;padding:0;box-sizing:border-box;}30body{font-family:'Space Grotesk',sans-serif;background:var(--bg);color:var(--text);overflow:hidden;height:100vh;display:flex;flex-direction:column;}31::-webkit-scrollbar{width:6px;}32::-webkit-scrollbar-track{background:var(--surface);}33::-webkit-scrollbar-thumb{background:var(--border-light);border-radius:3px;}34::-webkit-scrollbar-thumb:hover{background:var(--muted);}35 36/* TOOLBAR */37#toolbar{display:flex;align-items:center;gap:4px;padding:6px 12px;background:var(--surface);border-bottom:1px solid var(--border);min-height:48px;flex-shrink:0;overflow-x:auto;}38.tool-group{display:flex;align-items:center;gap:3px;}39.tool-sep{width:1px;height:28px;background:var(--border);margin:0 6px;flex-shrink:0;}40#toolbar button{background:transparent;border:1px solid transparent;color:var(--text-dim);width:34px;height:34px;border-radius:6px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:13px;transition:all .15s;}41#toolbar button:hover{background:var(--card);color:var(--text);border-color:var(--border);}42#toolbar button.active{background:var(--accent-dim);color:var(--accent);border-color:var(--accent);}43#toolbar select,#toolbar input[type="number"]{background:var(--card);border:1px solid var(--border);color:var(--text);padding:4px 6px;border-radius:5px;font-size:12px;font-family:inherit;height:30px;outline:none;}44#toolbar select:focus,#toolbar input[type="number"]:focus{border-color:var(--accent);}45#toolbar input[type="color"]{width:30px;height:30px;border:1px solid var(--border);border-radius:5px;background:var(--card);cursor:pointer;padding:2px;}46#toolbar input[type="color"]::-webkit-color-swatch-wrapper{padding:0;}47#toolbar input[type="color"]::-webkit-color-swatch{border:none;border-radius:3px;}48#toolbar input[type="number"]{width:52px;text-align:center;}49 50/* MAIN LAYOUT */51#main{display:flex;flex:1;overflow:hidden;}52 53/* SLIDES PANEL */54#slides-panel{width:170px;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;}55#slides-panel h4{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);padding:10px 12px 6px;font-weight:500;}56#slides-list{flex:1;overflow-y:auto;padding:0 8px 8px;display:flex;flex-direction:column;gap:6px;}57.slide-thumb{position:relative;cursor:pointer;border-radius:6px;border:2px solid transparent;overflow:hidden;aspect-ratio:4/3;background:#fff;transition:all .15s;flex-shrink:0;}58.slide-thumb:hover{border-color:var(--border-light);}59.slide-thumb.active{border-color:var(--accent);box-shadow:0 0 12px var(--accent-dim);}60.slide-thumb-num{position:absolute;bottom:2px;right:4px;font-size:9px;color:#999;background:rgba(255,255,255,0.85);padding:0 4px;border-radius:3px;font-weight:600;}61.slide-thumb-content{width:100%;height:100%;position:relative;overflow:hidden;transform-origin:top left;}62.thumb-el{position:absolute;overflow:hidden;display:flex;align-items:center;justify-content:center;font-size:3px;line-height:1.1;color:#333;word-break:break-word;padding:1px;}63#btn-add-slide-panel{margin:4px 8px 8px;padding:8px;border-radius:6px;border:1px dashed var(--border);background:transparent;color:var(--muted);cursor:pointer;font-size:12px;font-family:inherit;transition:all .15s;display:flex;align-items:center;justify-content:center;gap:6px;}64#btn-add-slide-panel:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-dim);}65 66/* EDITOR AREA */67#editor-area{flex:1;display:flex;align-items:center;justify-content:center;background:var(--bg);position:relative;overflow:hidden;}68#editor-area::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at center,rgba(16,185,129,0.03) 0%,transparent 70%);pointer-events:none;}69#slide-wrapper{position:relative;box-shadow:0 8px 40px rgba(0,0,0,0.5),0 0 0 1px rgba(255,255,255,0.05);}70#slide-canvas{position:relative;overflow:hidden;background:#fff;}71 72/* SLIDE ELEMENTS */73.slide-element{position:absolute;cursor:move;user-select:none;overflow:hidden;}74.slide-element.editing{cursor:text;user-select:text;outline:2px solid var(--accent);outline-offset:-1px;}75.slide-element.selected{outline:2px solid var(--accent);outline-offset:-1px;}76.slide-element.selected::after{content:'';position:absolute;inset:-4px;border:1px dashed var(--accent);pointer-events:none;}77.resize-handle{position:absolute;width:8px;height:8px;background:var(--accent);border:1px solid #fff;border-radius:2px;z-index:10;display:none;}78.slide-element.selected .resize-handle{display:block;}79.resize-handle.nw{top:-4px;left:-4px;cursor:nw-resize;}80.resize-handle.ne{top:-4px;right:-4px;cursor:ne-resize;}81.resize-handle.sw{bottom:-4px;left:-4px;cursor:sw-resize;}82.resize-handle.se{bottom:-4px;right:-4px;cursor:se-resize;}83.resize-handle.n{top:-4px;left:50%;transform:translateX(-50%);cursor:n-resize;}84.resize-handle.s{bottom:-4px;left:50%;transform:translateX(-50%);cursor:s-resize;}85.resize-handle.w{top:50%;left:-4px;transform:translateY(-50%);cursor:w-resize;}86.resize-handle.e{top:50%;right:-4px;transform:translateY(-50%);cursor:e-resize;}87 88/* PROPERTIES PANEL */89#props-panel{width:220px;background:var(--surface);border-left:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;overflow-y:auto;}90#props-panel h4{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);padding:10px 12px 6px;font-weight:500;}91.prop-section{padding:6px 12px 12px;}92.prop-row{display:flex;align-items:center;gap:6px;margin-bottom:6px;}93.prop-label{font-size:11px;color:var(--muted);width:24px;text-align:right;flex-shrink:0;}94.prop-input{flex:1;background:var(--card);border:1px solid var(--border);color:var(--text);padding:4px 6px;border-radius:4px;font-size:11px;font-family:inherit;outline:none;height:28px;}95.prop-input:focus{border-color:var(--accent);}96.prop-color{width:28px;height:28px;border:1px solid var(--border);border-radius:4px;background:var(--card);cursor:pointer;padding:1px;}97.prop-color::-webkit-color-swatch-wrapper{padding:0;}98.prop-color::-webkit-color-swatch{border:none;border-radius:2px;}99.prop-btn{padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--card);color:var(--text-dim);cursor:pointer;font-size:11px;font-family:inherit;transition:all .12s;}100.prop-btn:hover{background:var(--card-hover);color:var(--text);}101.prop-btn.active{background:var(--accent-dim);color:var(--accent);border-color:var(--accent);}102.prop-btn.danger:hover{background:var(--danger-dim);color:var(--danger);border-color:var(--danger);}103.no-selection{color:var(--muted);font-size:12px;text-align:center;padding:20px 12px;line-height:1.6;}104 105/* EMPTY STATE */106#empty-state{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:20;background:var(--bg);}107#empty-state.hidden{display:none;}108.empty-icon{font-size:64px;color:var(--border-light);margin-bottom:20px;}109.empty-title{font-size:28px;font-weight:700;margin-bottom:8px;background:linear-gradient(135deg,var(--accent),#06b6d4);-webkit-background-clip:text;-webkit-text-fill-color:transparent;}110.empty-sub{color:var(--muted);font-size:14px;margin-bottom:28px;}111.empty-actions{display:flex;gap:12px;}112.empty-btn{padding:12px 28px;border-radius:8px;font-size:14px;font-weight:600;font-family:inherit;cursor:pointer;transition:all .2s;border:none;display:flex;align-items:center;gap:8px;}113.empty-btn.primary{background:var(--accent);color:#fff;}114.empty-btn.primary:hover{background:var(--accent-hover);transform:translateY(-1px);box-shadow:0 4px 20px rgba(16,185,129,0.3);}115.empty-btn.secondary{background:var(--card);color:var(--text);border:1px solid var(--border);}116.empty-btn.secondary:hover{background:var(--card-hover);border-color:var(--border-light);}117 118/* TOAST */119#toast-container{position:fixed;top:60px;right:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;}120.toast{padding:10px 16px;border-radius:8px;font-size:13px;font-family:inherit;color:#fff;animation:toastIn .3s ease;box-shadow:0 4px 20px rgba(0,0,0,0.4);display:flex;align-items:center;gap:8px;max-width:320px;}121.toast.success{background:#065f46;border:1px solid var(--accent);}122.toast.error{background:#7f1d1d;border:1px solid var(--danger);}123.toast.info{background:#1e3a5f;border:1px solid #3b82f6;}124.toast.fadeout{animation:toastOut .3s ease forwards;}125@keyframes toastIn{from{opacity:0;transform:translateX(40px);}to{opacity:1;transform:translateX(0);}}126@keyframes toastOut{from{opacity:1;transform:translateX(0);}to{opacity:0;transform:translateX(40px);}}127 128/* FILE INPUT HIDDEN */129#file-input{display:none;}130 131/* STATUS BAR */132#status-bar{height:26px;background:var(--surface);border-top:1px solid var(--border);display:flex;align-items:center;padding:0 12px;font-size:11px;color:var(--muted);gap:16px;flex-shrink:0;}133#status-bar span{display:flex;align-items:center;gap:4px;}134</style>135</head>136<body>137 138<!-- BARRA DE FERRAMENTAS -->139<div id="toolbar">140 <div class="tool-group">141 <button id="btn-new" title="Nova Apresentação (Ctrl+N)"><i class="fas fa-file"></i></button>142 <button id="btn-open" title="Abrir Arquivo (Ctrl+O)"><i class="fas fa-folder-open"></i></button>143 <button id="btn-save" title="Salvar PPTX (Ctrl+S)"><i class="fas fa-download"></i></button>144 </div>145 <div class="tool-sep"></div>146 <div class="tool-group">147 <button id="btn-undo" title="Desfazer (Ctrl+Z)"><i class="fas fa-undo"></i></button>148 <button id="btn-redo" title="Refazer (Ctrl+Y)"><i class="fas fa-redo"></i></button>149 </div>150 <div class="tool-sep"></div>151 <div class="tool-group" id="text-tools">152 <select id="font-select" title="Fonte">153 <option value="Calibri">Calibri</option>154 <option value="Arial">Arial</option>155 <option value="Times New Roman">Times New Roman</option>156 <option value="Courier New">Courier New</option>157 <option value="Georgia">Georgia</option>158 <option value="Verdana">Verdana</option>159 <option value="Tahoma">Tahoma</option>160 <option value="Impact">Impact</option>161 <option value="Comic Sans MS">Comic Sans MS</option>162 </select>163 <input type="number" id="font-size" min="6" max="200" value="18" title="Tamanho (pt)">164 <button id="btn-bold" title="Negrito (Ctrl+B)"><i class="fas fa-bold"></i></button>165 <button id="btn-italic" title="Itálico (Ctrl+I)"><i class="fas fa-italic"></i></button>166 <button id="btn-underline" title="Sublinhado (Ctrl+U)"><i class="fas fa-underline"></i></button>167 <input type="color" id="font-color" value="#000000" title="Cor do Texto">168 <button id="btn-align-l" title="Alinhar à Esquerda"><i class="fas fa-align-left"></i></button>169 <button id="btn-align-c" title="Centralizar"><i class="fas fa-align-center"></i></button>170 <button id="btn-align-r" title="Alinhar à Direita"><i class="fas fa-align-right"></i></button>171 </div>172 <div class="tool-sep"></div>173 <div class="tool-group">174 <button id="btn-add-text" title="Caixa de Texto"><i class="fas fa-font"></i></button>175 <button id="btn-add-rect" title="Retângulo"><i class="far fa-square"></i></button>176 <button id="btn-add-oval" title="Oval"><i class="far fa-circle"></i></button>177 </div>178 <div class="tool-sep"></div>179 <div class="tool-group">180 <button id="btn-add-slide" title="Novo Slide"><i class="fas fa-plus"></i></button>181 <button id="btn-dup-slide" title="Duplicar Slide"><i class="fas fa-copy"></i></button>182 <button id="btn-del-slide" title="Excluir Slide"><i class="fas fa-trash"></i></button>183 </div>184</div>185 186<!-- CONTEÚDO PRINCIPAL -->187<div id="main">188 <!-- PAINEL DE SLIDES -->189 <div id="slides-panel">190 <h4>Slides</h4>191 <div id="slides-list"></div>192 <button id="btn-add-slide-panel"><i class="fas fa-plus"></i> Novo Slide</button>193 </div>194 195 <!-- ÁREA DO EDITOR -->196 <div id="editor-area">197 <div id="empty-state">198 <div class="empty-icon"><i class="fas fa-file-powerpoint"></i></div>199 <div class="empty-title">SlideForge</div>200 <div class="empty-sub">Editor de apresentações PPTX no navegador</div>201 <div class="empty-actions">202 <button class="empty-btn primary" id="btn-new-empty"><i class="fas fa-plus"></i> Nova Apresentação</button>203 <button class="empty-btn secondary" id="btn-open-empty"><i class="fas fa-folder-open"></i> Abrir Arquivo</button>204 </div>205 </div>206 <div id="slide-wrapper" style="display:none;">207 <div id="slide-canvas"></div>208 </div>209 </div>210 211 <!-- PAINEL DE PROPRIEDADES -->212 <div id="props-panel">213 <h4>Propriedades</h4>214 <div id="props-content">215 <p class="no-selection">Selecione um elemento no slide para ver suas propriedades</p>216 </div>217 </div>218</div>219 220<!-- BARRA DE STATUS -->221<div id="status-bar">222 <span id="status-file"><i class="fas fa-file"></i> Nenhum arquivo</span>223 <span id="status-slide"><i class="fas fa-layer-group"></i> Slide -</span>224 <span id="status-elems"><i class="fas fa-shapes"></i> 0 elementos</span>225 <span style="margin-left:auto;" id="status-zoom"><i class="fas fa-search"></i> 100%</span>226</div>227 228<!-- TOAST CONTAINER -->229<div id="toast-container"></div>230 231<!-- INPUT DE ARQUIVO OCULTO -->232<input type="file" id="file-input" accept=".pptx">233 234<script>235// ============================================================236// ESTADO GLOBAL237// ============================================================238const NS_P = 'http://schemas.openxmlformats.org/presentationml/2006/main';239const NS_A = 'http://schemas.openxmlformats.org/drawingml/2006/main';240const NS_R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';241 242const state = {243 zip: null,244 fileName: 'Apresentacao',245 slides: [],246 currentSlide: 0,247 selectedId: null,248 editing: false,249 dragging: false,250 resizing: false,251 resizeDir: '',252 dragStart: { x: 0, y: 0 },253 elemStart: { x: 0, y: 0, w: 0, h: 0 },254 scale: 1,255 nextId: 200,256 history: [],257 historyIdx: -1258};259 260// ============================================================261// UTILITÁRIOS262// ============================================================263function escXml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }264function escHtml(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }265 266function toast(msg, type='info') {267 const c = document.getElementById('toast-container');268 const t = document.createElement('div');269 t.className = 'toast ' + type;270 const icons = { success:'fa-check-circle', error:'fa-exclamation-circle', info:'fa-info-circle' };271 t.innerHTML = `<i class="fas ${icons[type]||icons.info}"></i> ${escHtml(msg)}`;272 c.appendChild(t);273 setTimeout(() => { t.classList.add('fadeout'); setTimeout(() => t.remove(), 300); }, 3000);274}275 276function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }277 278function pushHistory() {279 state.history = state.history.slice(0, state.historyIdx + 1);280 state.history.push(deepClone(state.slides));281 state.historyIdx = state.history.length - 1;282 if (state.history.length > 50) { state.history.shift(); state.historyIdx--; }283}284 285function undo() {286 if (state.historyIdx > 0) {287 state.historyIdx--;288 state.slides = deepClone(state.history[state.historyIdx]);289 state.selectedId = null;290 renderAll();291 toast('Desfeito', 'info');292 }293}294 295function redo() {296 if (state.historyIdx < state.history.length - 1) {297 state.historyIdx++;298 state.slides = deepClone(state.history[state.historyIdx]);299 state.selectedId = null;300 renderAll();301 toast('Refeito', 'info');302 }303}304 305// ============================================================306// PARSER PPTX307// ============================================================308function qns(parent, local, ns) {309 return parent.getElementsByTagNameNS(ns, local);310}311 312function getAttr(node, local, ns, attr) {313 const els = node.getElementsByTagNameNS(ns, local);314 return els.length > 0 ? els[0].getAttribute(attr) : null;315}316 317async function parsePPTX(zip) {318 const slides = [];319 320 // Encontrar todos os arquivos de slide321 const slideFiles = [];322 zip.forEach((path) => {323 const m = path.match(/^ppt\/slides\/slide(\d+)\.xml$/);324 if (m) slideFiles.push({ path, num: parseInt(m[1]) });325 });326 slideFiles.sort((a, b) => a.num - b.num);327 328 // Ler relações do presentation.xml.rels para mapear rIds329 let presRels = {};330 try {331 const relsXml = await zip.file('ppt/_rels/presentation.xml.rels').async('string');332 const relsDoc = new DOMParser().parseFromString(relsXml, 'application/xml');333 const rels = relsDoc.getElementsByTagName('Relationship');334 for (const r of rels) {335 presRels[r.getAttribute('Id')] = r.getAttribute('Target');336 }337 } catch(e) {}338 339 // Ler relações de cada slide340 for (const sf of slideFiles) {341 const slideXml = await zip.file(sf.path).async('string');342 const doc = new DOMParser().parseFromString(slideXml, 'application/xml');343 const slide = {344 xmlPath: sf.path,345 slideNum: sf.num,346 originalXml: slideXml,347 modified: false,348 width: 9144000,349 height: 6858000,350 background: '#FFFFFF',351 elements: []352 };353 354 // Dimensões do slide (do presentation.xml)355 // Usaremos padrão por enquanto356 357 // Relações deste slide358 let slideRels = {};359 try {360 const relPath = `ppt/slides/_rels/slide${sf.num}.xml.rels`;361 if (zip.file(relPath)) {362 const srXml = await zip.file(relPath).async('string');363 const srDoc = new DOMParser().parseFromString(srXml, 'application/xml');364 for (const r of srDoc.getElementsByTagName('Relationship')) {365 slideRels[r.getAttribute('Id')] = r.getAttribute('Target');366 }367 }368 } catch(e) {}369 370 // Background371 const bgEls = qns(doc, 'bg', NS_P);372 if (bgEls.length > 0) {373 const fills = qns(bgEls[0], 'solidFill', NS_A);374 if (fills.length > 0) {375 const clr = fills[0].firstElementChild;376 if (clr) {377 if (clr.localName === 'srgbClr') slide.background = '#' + clr.getAttribute('val');378 else if (clr.localName === 'schemeClr') {379 const map = { 'bg1':'#FFFFFF', 'tx1':'#000000', 'bg2':'#E7E6E6', 'tx2':'#44546A' };380 slide.background = map[clr.getAttribute('val')] || '#FFFFFF';381 }382 }383 }384 }385 386 // Elementos (shapes, pictures)387 const spTree = qns(doc, 'spTree', NS_P);388 if (spTree.length > 0) {389 // Shapes com texto (p:sp)390 const shapes = qns(spTree[0], 'sp', NS_P);391 for (const sp of shapes) {392 const elem = parseShape(sp);393 if (elem) slide.elements.push(elem);394 }395 396 // Imagens (p:pic)397 const pics = qns(spTree[0], 'pic', NS_P);398 for (const pic of pics) {399 const elem = await parsePicture(pic, slideRels, zip);400 if (elem) slide.elements.push(elem);401 }402 }403 404 slides.push(slide);405 }406 407 return slides;408}409 410function parseShape(sp) {411 // ID e nome412 const cNvPr = qns(sp, 'cNvPr', NS_P);413 if (cNvPr.length === 0) return null;414 const id = parseInt(cNvPr[0].getAttribute('id')) || state.nextId++;415 const name = cNvPr[0].getAttribute('name') || 'Shape';416 417 // É caixa de texto?418 const cNvSpPr = qns(sp, 'cNvSpPr', NS_P);419 const isTextBox = cNvSpPr.length > 0 && cNvSpPr[0].getAttribute('txBox') === '1';420 421 // Posição e tamanho422 const xfrm = qns(sp, 'xfrm', NS_A);423 let x = 457200, y = 457200, w = 914400, h = 457200;424 if (xfrm.length > 0) {425 const off = qns(xfrm[0], 'off', NS_A);426 const ext = qns(xfrm[0], 'ext', NS_A);427 if (off.length > 0) { x = parseInt(off[0].getAttribute('x')) || 0; y = parseInt(off[0].getAttribute('y')) || 0; }428 if (ext.length > 0) { w = parseInt(ext[0].getAttribute('cx')) || 914400; h = parseInt(ext[0].getAttribute('cy')) || 457200; }429 }430 431 // Tipo de forma geométrica432 const prstGeom = qns(sp, 'prstGeom', NS_A);433 let shapeType = 'rect';434 if (prstGeom.length > 0) {435 const prst = prstGeom[0].getAttribute('prst');436 if (prst && prst.includes('ellip') || prst === 'oval') shapeType = 'oval';437 }438 439 // Fill440 let fill = null;441 const solidFill = qns(sp, 'solidFill', NS_A);442 if (solidFill.length > 0) {443 const clr = solidFill[solidFill.length - 1].firstElementChild;444 if (clr && clr.localName === 'srgbClr') fill = clr.getAttribute('val');445 }446 447 // Texto448 const txBody = qns(sp, 'txBody', NS_A);449 let paragraphs = [];450 if (txBody.length > 0) {451 paragraphs = parseParagraphs(txBody[0]);452 }453 454 // Se não tem texto e não é caixa de texto, pode ser uma forma pura455 if (paragraphs.length === 0 && !isTextBox) {456 return {457 id, name, type: 'shape', shapeType,458 x, y, width: w, height: h,459 fill: fill || '4472C4', line: '2F528F'460 };461 }462 463 // Se tem texto (mesmo que vazio), é um elemento de texto464 if (paragraphs.length === 0) {465 paragraphs = [{ align: 'left', runs: [{ text: '', bold: false, italic: false, underline: false, size: 18, color: '000000', font: 'Calibri' }] }];466 }467 468 return {469 id, name, type: 'text', isTextBox,470 x, y, width: w, height: h,471 paragraphs,472 fill: fill || null,473 shapeType: shapeType !== 'rect' ? shapeType : null474 };475}476 477function parseParagraphs(txBody) {478 const paras = qns(txBody, 'p', NS_A);479 const result = [];480 for (const p of paras) {481 const para = { align: 'left', runs: [] };482 const pPr = qns(p, 'pPr', NS_A);483 if (pPr.length > 0) {484 const algn = pPr[0].getAttribute('algn');485 if (algn === 'ctr') para.align = 'center';486 else if (algn === 'r') para.align = 'right';487 else if (algn === 'just') para.align = 'justify';488 }489 const runs = qns(p, 'r', NS_A);490 for (const r of runs) {491 const run = { text: '', bold: false, italic: false, underline: false, size: 18, color: '000000', font: 'Calibri' };492 const t = qns(r, 't', NS_A);493 if (t.length > 0) run.text = t[0].textContent || '';494 const rPr = qns(r, 'rPr', NS_A);495 if (rPr.length > 0) {496 const pr = rPr[0];497 run.bold = pr.getAttribute('b') === '1';498 run.italic = pr.getAttribute('i') === '1';499 run.underline = pr.getAttribute('u') === 'sng' || pr.getAttribute('u') === '1';500 const sz = pr.getAttribute('sz');501 if (sz) run.size = parseInt(sz) / 100;502 // Cor503 const srgbClr = qns(pr, 'srgbClr', NS_A);504 if (srgbClr.length > 0) run.color = srgbClr[0].getAttribute('val') || '000000';505 else {506 const schemeClr = qns(pr, 'schemeClr', NS_A);507 if (schemeClr.length > 0) {508 const v = schemeClr[0].getAttribute('val');509 const map = { 'tx1':'#000000', 'tx2':'#44546A', 'bg1':'#FFFFFF' };510 run.color = (map[v] || '#000000').replace('#', '');511 }512 }513 // Fonte514 const latin = pr.getAttribute('latin');515 if (latin) run.font = latin;516 }517 para.runs.push(run);518 }519 // Se não tem runs, adicionar um vazio520 if (para.runs.length === 0) {521 para.runs.push({ text: '', bold: false, italic: false, underline: false, size: 18, color: '000000', font: 'Calibri' });522 }523 result.push(para);524 }525 return result.length > 0 ? result : [{ align: 'left', runs: [{ text: '', bold: false, italic: false, underline: false, size: 18, color: '000000', font: 'Calibri' }] }];526}527 528async function parsePicture(pic, slideRels, zip) {529 const cNvPr = qns(pic, 'cNvPr', NS_P);530 if (cNvPr.length === 0) return null;531 const id = parseInt(cNvPr[0].getAttribute('id')) || state.nextId++;532 const name = cNvPr[0].getAttribute('name') || 'Imagem';533 534 const xfrm = qns(pic, 'xfrm', NS_A);535 let x = 0, y = 0, w = 914400, h = 685800;536 if (xfrm.length > 0) {537 const off = qns(xfrm[0], 'off', NS_A);538 const ext = qns(xfrm[0], 'ext', NS_A);539 if (off.length > 0) { x = parseInt(off[0].getAttribute('x')) || 0; y = parseInt(off[0].getAttribute('y')) || 0; }540 if (ext.length > 0) { w = parseInt(ext[0].getAttribute('cx')) || 914400; h = parseInt(ext[0].getAttribute('cy')) || 685800; }541 }542 543 // Imagem embutida544 const blip = qns(pic, 'blip', NS_A);545 let dataUrl = '';546 if (blip.length > 0) {547 const rEmbed = blip[0].getAttributeNS(NS_R, 'embed');548 if (rEmbed && slideRels[rEmbed]) {549 const imgPath = 'ppt/slides/' + slideRels[rEmbed].replace('../', '');550 const imgFile = zip.file(imgPath);551 if (imgFile) {552 const blob = await imgFile.async('blob');553 dataUrl = await new Promise(res => {554 const fr = new FileReader();555 fr.onload = () => res(fr.result);556 fr.readAsDataURL(blob);557 });558 }559 }560 }561 562 return { id, name, type: 'image', x, y, width: w, height: h, dataUrl, imgPath: slideRels[blip[0]?.getAttributeNS(NS_R, 'embed')] || '' };563}564 565// ============================================================566// ESCRITOR PPTX567// ============================================================568function buildSlideXml(slide) {569 let shapesXml = '';570 for (const el of slide.elements) {571 if (el.type === 'text') shapesXml += buildTextShapeXml(el);572 else if (el.type === 'shape') shapesXml += buildPureShapeXml(el);573 else if (el.type === 'image') shapesXml += buildPicXml(el);574 }575 return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>576<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">577<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr>${shapesXml}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sld>`;578}579 580function buildTextShapeXml(el) {581 const id = el.id;582 const name = escXml(el.name);583 const txBox = el.isTextBox ? '1' : '0';584 let parasXml = '';585 for (const p of el.paragraphs) {586 const algnMap = { left:'l', center:'ctr', right:'r', justify:'just' };587 const pPr = `<a:pPr algn="${algnMap[p.align] || 'l'}"/>`;588 let runsXml = '';589 for (const r of p.runs) {590 let rPrAttrs = `lang="pt-BR" sz="${Math.round(r.size * 100)}" dirty="0"`;591 if (r.bold) rPrAttrs += ' b="1"';592 if (r.italic) rPrAttrs += ' i="1"';593 if (r.underline) rPrAttrs += ' u="sng"';594 if (r.font && r.font !== 'Calibri') rPrAttrs += ` latin="${escXml(r.font)}" ea="${escXml(r.font)}"`;595 let fillXml = '';596 if (r.color && r.color !== '000000') {597 fillXml = `<a:solidFill><a:srgbClr val="${r.color}"/></a:solidFill>`;598 }599 runsXml += `<a:r><a:rPr ${rPrAttrs}>${fillXml}</a:rPr><a:t>${escXml(r.text)}</a:t></a:r>`;600 }601 parasXml += `<a:p>${pPr}${runsXml}</a:p>`;602 }603 const prst = el.shapeType === 'oval' ? 'ellipse' : 'rect';604 let fillXml = '';605 if (el.fill) fillXml = `<a:solidFill><a:srgbClr val="${el.fill}"/></a:solidFill>`;606 return `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="${name}"/><p:cNvSpPr txBox="${txBox}"/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${Math.round(el.x)}" y="${Math.round(el.y)}"/><a:ext cx="${Math.round(el.width)}" cy="${Math.round(el.height)}"/></a:xfrm><a:prstGeom prst="${prst}"><a:avLst/></a:prstGeom>${fillXml}</p:spPr><p:txBody><a:bodyPr wrap="square" lIns="91440" tIns="45720" rIns="91440" bIns="45720" anchor="t"/><a:lstStyle/>${parasXml}</p:txBody></p:sp>`;607}608 609function buildPureShapeXml(el) {610 const id = el.id;611 const name = escXml(el.name);612 const prst = el.shapeType === 'oval' ? 'ellipse' : 'rect';613 let fillXml = '';614 if (el.fill) fillXml = `<a:solidFill><a:srgbClr val="${el.fill}"/></a:solidFill>`;615 let lineXml = '';616 if (el.line) lineXml = `<a:ln><a:solidFill><a:srgbClr val="${el.line}"/></a:solidFill></a:ln>`;617 return `<p:sp><p:nvSpPr><p:cNvPr id="${id}" name="${name}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${Math.round(el.x)}" y="${Math.round(el.y)}"/><a:ext cx="${Math.round(el.width)}" cy="${Math.round(el.height)}"/></a:xfrm><a:prstGeom prst="${prst}"><a:avLst/></a:prstGeom>${fillXml}${lineXml}</p:spPr></p:sp>`;618}619 620function buildPicXml(el) {621 const id = el.id;622 const name = escXml(el.name);623 // Para imagens, usamos o mesmo rId2 (precisaria ser dinâmico, mas simplificamos)624 return `<p:pic><p:nvPicPr><p:cNvPr id="${id}" name="${name}"/><p:cNvPicPr/><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="rId2"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="${Math.round(el.x)}" y="${Math.round(el.y)}"/><a:ext cx="${Math.round(el.width)}" cy="${Math.round(el.height)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>`;625}626 627async function savePPTX() {628 let zip;629 if (state.zip) {630 zip = await state.zip.generateAsync({ type: 'blob' }).then(b => JSZip.loadAsync(b));631 } else {632 zip = createNewZip();633 }634 635 const slideCount = state.slides.length;636 637 // Atualizar/criar XML de cada slide638 for (let i = 0; i < slideCount; i++) {639 const slide = state.slides[i];640 const path = `ppt/slides/slide${i + 1}.xml`;641 if (slide.modified) {642 zip.file(path, buildSlideXml(slide));643 } else {644 zip.file(path, slide.originalXml);645 }646 // Criar rels do slide647 let rels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>648<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">649<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>`;650 // Adicionar rels de imagem se necessário651 let imgRId = 2;652 for (const el of slide.elements) {653 if (el.type === 'image' && el.imgPath) {654 rels += `\n<Relationship Id="rId${imgRId}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="${escXml(el.imgPath)}"/>`;655 imgRId++;656 }657 }658 rels += `\n</Relationships>`;659 zip.file(`ppt/slides/_rels/slide${i + 1}.xml.rels`, rels);660 }661 662 // Remover slides antigos que não existem mais663 const toRemove = [];664 zip.forEach((path) => {665 const m = path.match(/^ppt\/slides\/slide(\d+)\.xml$/);666 if (m && parseInt(m[1]) > slideCount) toRemove.push(path);667 const m2 = path.match(/^ppt\/slides\/_rels\/slide(\d+)\.xml\.rels$/);668 if (m2 && parseInt(m2[1]) > slideCount) toRemove.push(path);669 });670 toRemove.forEach(p => zip.remove(p));671 672 // Atualizar presentation.xml673 const presXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>674<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">675<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId2"/></p:sldMasterIdLst>676<p:sldIdLst>${state.slides.map((_, i) => `<p:sldId id="${256 + i}" r:id="rId${i + 3}"/>`).join('\n')}</p:sldIdLst>677<p:sldSz cx="9144000" cy="6858000" type="screen4x3"/>678<p:notesSz cx="6858000" cy="9144000"/>679</p:presentation>`;680 zip.file('ppt/presentation.xml', presXml);681 682 // Atualizar presentation.xml.rels683 let presRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>684<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">685<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>686<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>`;687 for (let i = 0; i < slideCount; i++) {688 presRels += `\n<Relationship Id="rId${i + 3}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide${i + 1}.xml"/>`;689 }690 presRels += `\n</Relationships>`;691 zip.file('ppt/_rels/presentation.xml.rels', presRels);692 693 // Atualizar [Content_Types].xml694 let contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>695<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">696<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>697<Default Extension="xml" ContentType="application/xml"/>698<Default Extension="png" ContentType="image/png"/>699<Default Extension="jpeg" ContentType="image/jpeg"/>700<Default Extension="jpg" ContentType="image/jpeg"/>701<Default Extension="gif" ContentType="image/gif"/>702<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>703<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>704<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>705<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>`;706 for (let i = 0; i < slideCount; i++) {707 contentTypes += `\n<Override PartName="/ppt/slides/slide${i + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>`;708 }709 contentTypes += `\n</Types>`;710 zip.file('[Content_Types].xml', contentTypes);711 712 // Gerar e baixar713 const blob = await zip.generateAsync({ type: 'blob', mimeType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' });714 const url = URL.createObjectURL(blob);715 const a = document.createElement('a');716 a.href = url;717 a.download = state.fileName + '.pptx';718 a.click();719 URL.revokeObjectURL(url);720 toast('Arquivo salvo com sucesso!', 'success');721}722 723function createNewZip() {724 const zip = new JSZip();725 zip.file('[Content_Types].xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>726<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">727<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>728<Default Extension="xml" ContentType="application/xml"/>729<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>730<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>731<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>732<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>733<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>734</Types>`);735 zip.file('_rels/.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>736<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">737<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>738</Relationships>`);739 // Slide master mínimo740 zip.file('ppt/slideMasters/slideMaster1.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>741<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">742<p:cSld><p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef></p:bg><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld>743<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>744<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>745</p:sldMaster>`);746 zip.file('ppt/slideMasters/_rels/slideMaster1.xml.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>747<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">748<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>749<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>750</Relationships>`);751 // Slide layout mínimo752 zip.file('ppt/slideLayouts/slideLayout1.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>753<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="blank" preserve="1">754<p:cSld name="Em Branco"><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld>755<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>756</p:sldLayout>`);757 zip.file('ppt/slideLayouts/_rels/slideLayout1.xml.rels', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>758<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">759<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>760</Relationships>`);761 // Tema mínimo762 zip.file('ppt/theme/theme1.xml', `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>763<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="Tema">764<a:themeElements><a:clrScheme name="Escritório"><a:dk1><a:sysClr val="windowText" lastClr="000000"/></a:dk1><a:lt1><a:sysClr val="window" lastClr="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="44546A"/></a:dk2><a:lt2><a:srgbClr val="E7E6E6"/></a:lt2><a:accent1><a:srgbClr val="4472C4"/></a:accent1><a:accent2><a:srgbClr val="ED7D31"/></a:accent2><a:accent3><a:srgbClr val="A5A5A5"/></a:accent3><a:accent4><a:srgbClr val="FFC000"/></a:accent4><a:accent5><a:srgbClr val="5B9BD5"/></a:accent5><a:accent6><a:srgbClr val="70AD47"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink></a:clrScheme><a:fontScheme name="Padrão"><a:majorFont><a:latin typeface="Calibri Light"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont><a:minorFont><a:latin typeface="Calibri"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont></a:fontScheme><a:fmtScheme name="Padrão"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst><a:lnStyleLst><a:ln w="6350" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="12700" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln><a:ln w="19050" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements>765<a:objectDefaults/><a:extraClrSchemeLst/>766</a:theme>`);767 return zip;768}769 770// ============================================================771// RENDERIZAÇÃO772// ============================================================773function renderAll() {774 renderSlideList();775 renderSlide();776 renderProps();777 updateStatus();778}779 780function renderSlideList() {781 const list = document.getElementById('slides-list');782 list.innerHTML = '';783 state.slides.forEach((slide, i) => {784 const thumb = document.createElement('div');785 thumb.className = 'slide-thumb' + (i === state.currentSlide ? ' active' : '');786 thumb.style.background = slide.background || '#fff';787 788 const content = document.createElement('div');789 content.className = 'slide-thumb-content';790 const thumbScale = 160 / slide.width;791 792 slide.elements.forEach(el => {793 const d = document.createElement('div');794 d.className = 'thumb-el';795 d.style.left = (el.x * thumbScale) + 'px';796 d.style.top = (el.y * thumbScale) + 'px';797 d.style.width = (el.width * thumbScale) + 'px';798 d.style.height = (el.height * thumbScale) + 'px';799 800 if (el.type === 'text') {801 const txt = el.paragraphs.map(p => p.runs.map(r => r.text).join('')).join(' ');802 d.textContent = txt || ' ';803 if (el.fill) d.style.background = '#' + el.fill;804 } else if (el.type === 'shape') {805 d.style.background = el.fill ? '#' + el.fill : '#ccc';806 if (el.shapeType === 'oval') d.style.borderRadius = '50%';807 } else if (el.type === 'image' && el.dataUrl) {808 d.innerHTML = `<img src="${el.dataUrl}" style="width:100%;height:100%;object-fit:contain;">`;809 }810 content.appendChild(d);811 });812 813 // Escalar o conteúdo para caber no thumb814 const thumbH = thumb.offsetWidth ? thumb.offsetWidth * 0.75 : 120;815 content.style.transform = `scale(${thumbH / (slide.height * thumbScale)})`;816 content.style.width = (slide.width * thumbScale) + 'px';817 content.style.height = (slide.height * thumbScale) + 'px';818 819 thumb.appendChild(content);820 821 const num = document.createElement('div');822 num.className = 'slide-thumb-num';823 num.textContent = i + 1;824 thumb.appendChild(num);825 826 thumb.addEventListener('click', () => {827 if (state.editing) exitEditing();828 state.currentSlide = i;829 state.selectedId = null;830 renderAll();831 });832 833 list.appendChild(thumb);834 });835}836 837function renderSlide() {838 if (state.slides.length === 0) return;839 840 const slide = state.slides[state.currentSlide];841 const area = document.getElementById('editor-area');842 const wrapper = document.getElementById('slide-wrapper');843 const canvas = document.getElementById('slide-canvas');844 845 const availW = area.clientWidth - 60;846 const availH = area.clientHeight - 60;847 const aspect = slide.width / slide.height;848 849 let cw, ch;850 if (availW / availH > aspect) {851 ch = availH;852 cw = ch * aspect;853 } else {854 cw = availW;855 ch = cw / aspect;856 }857 858 state.scale = cw / slide.width;859 const fontScale = 12700 * state.scale; // pontos para pixels860 861 canvas.style.width = cw + 'px';862 canvas.style.height = ch + 'px';863 canvas.style.background = slide.background || '#fff';864 wrapper.style.display = 'block';865 866 canvas.innerHTML = '';867 868 slide.elements.forEach(el => {869 const div = document.createElement('div');870 div.className = 'slide-element';871 if (el.id === state.selectedId) div.classList.add('selected');872 div.dataset.id = el.id;873 div.style.left = (el.x * state.scale) + 'px';874 div.style.top = (el.y * state.scale) + 'px';875 div.style.width = (el.width * state.scale) + 'px';876 div.style.height = (el.height * state.scale) + 'px';877 878 if (el.type === 'text') {879 if (el.fill) div.style.background = '#' + el.fill;880 if (el.shapeType === 'oval') div.style.borderRadius = '50%';881 div.innerHTML = el.paragraphs.map(p => {882 const align = p.align || 'left';883 return `<p style="margin:0;text-align:${align};line-height:1.2;">${p.runs.map(r => {884 let s = `font-size:${Math.max(1, r.size * fontScale)}px;`;885 if (r.bold) s += 'font-weight:700;';886 if (r.italic) s += 'font-style:italic;';887 if (r.underline) s += 'text-decoration:underline;';888 if (r.color) s += `color:#${r.color};`;889 if (r.font) s += `font-family:'${r.font}',sans-serif;`;890 return `<span style="${s}">${escHtml(r.text)}</span>`;891 }).join('')}</p>`;892 }).join('');893 } else if (el.type === 'shape') {894 div.style.background = el.fill ? '#' + el.fill : 'transparent';895 if (el.line) div.style.border = `2px solid #${el.line}`;896 if (el.shapeType === 'oval') div.style.borderRadius = '50%';897 } else if (el.type === 'image' && el.dataUrl) {898 div.innerHTML = `<img src="${el.dataUrl}" style="width:100%;height:100%;object-fit:contain;pointer-events:none;">`;899 }900 901 // Handles de redimensionamento902 ['nw','n','ne','e','se','s','sw','w'].forEach(dir => {903 const h = document.createElement('div');904 h.className = 'resize-handle ' + dir;905 h.dataset.dir = dir;906 div.appendChild(h);907 });908 909 canvas.appendChild(div);910 });911}912 913function renderProps() {914 const cont = document.getElementById('props-content');915 const slide = state.slides[state.currentSlide];916 if (!slide) { cont.innerHTML = '<p class="no-selection">Nenhum slide</p>'; return; }917 918 const el = slide.elements.find(e => e.id === state.selectedId);919 if (!el) {920 cont.innerHTML = `921 <div class="prop-section">922 <div class="prop-row"><span class="prop-label" style="width:auto;color:var(--text-dim);font-size:11px;">Slide ${state.currentSlide + 1}</span></div>923 <div class="prop-row"><span class="prop-label">Cor</span><input type="color" class="prop-color" id="prop-bg-color" value="${(slide.background || '#ffffff').replace('#','')}"></div>924 </div>925 <p class="no-selection">Selecione um elemento para editar</p>`;926 const bgInput = document.getElementById('prop-bg-color');927 if (bgInput) bgInput.addEventListener('input', (e) => {928 slide.background = '#' + e.target.value;929 slide.modified = true;930 renderSlide();931 renderSlideList();932 });933 return;934 }935 936 let html = `<div class="prop-section">937 <div style="font-size:12px;color:var(--text-dim);margin-bottom:8px;font-weight:500;">${escHtml(el.name || 'Elemento')}</div>938 <div class="prop-row"><span class="prop-label">X</span><input type="number" class="prop-input" id="prop-x" value="${Math.round(el.x / 9144)}" step="1"></div>939 <div class="prop-row"><span class="prop-label">Y</span><input type="number" class="prop-input" id="prop-y" value="${Math.round(el.y / 9144)}" step="1"></div>940 <div class="prop-row"><span class="prop-label">L</span><input type="number" class="prop-input" id="prop-w" value="${Math.round(el.width / 9144)}" step="1" min="1"></div>941 <div class="prop-row"><span class="prop-label">A</span><input type="number" class="prop-input" id="prop-h" value="${Math.round(el.height / 9144)}" step="1" min="1"></div>942 </div>`;943 944 if (el.type === 'text') {945 const firstRun = el.paragraphs[0]?.runs[0] || {};946 html += `<div class="prop-section">947 <div style="font-size:11px;color:var(--muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px;">Texto</div>948 <div class="prop-row"><span class="prop-label"><i class="fas fa-font" style="font-size:10px;"></i></span><select class="prop-input" id="prop-font">${['Calibri','Arial','Times New Roman','Courier New','Georgia','Verdana','Tahoma','Impact','Comic Sans MS'].map(f => `<option value="${f}" ${firstRun.font === f ? 'selected' : ''}>${f}</option>`).join('')}</select></div>949 <div class="prop-row"><span class="prop-label" style="font-size:10px;">pt</span><input type="number" class="prop-input" id="prop-fsize" value="${firstRun.size || 18}" min="6" max="200"></div>950 <div class="prop-row"><span class="prop-label">Cor</span><input type="color" class="prop-color" id="prop-fcolor" value="#${firstRun.color || '000000'}"></div>951 <div class="prop-row" style="gap:4px;">952 <button class="prop-btn ${firstRun.bold ? 'active' : ''}" id="prop-bold" title="Negrito"><i class="fas fa-bold"></i></button>953 <button class="prop-btn ${firstRun.italic ? 'active' : ''}" id="prop-italic" title="Itálico"><i class="fas fa-italic"></i></button>954 <button class="prop-btn ${firstRun.underline ? 'active' : ''}" id="prop-underline" title="Sublinhado"><i class="fas fa-underline"></i></button>955 </div>956 <div class="prop-row" style="gap:4px;">957 <button class="prop-btn ${el.paragraphs[0]?.align === 'left' ? 'active' : ''}" id="prop-al"><i class="fas fa-align-left"></i></button>958 <button class="prop-btn ${el.paragraphs[0]?.align === 'center' ? 'active' : ''}" id="prop-ac"><i class="fas fa-align-center"></i></button>959 <button class="prop-btn ${el.paragraphs[0]?.align === 'right' ? 'active' : ''}" id="prop-ar"><i class="fas fa-align-right"></i></button>960 </div>961 </div>`;962 }963 964 if (el.type === 'shape' || (el.type === 'text' && el.fill)) {965 html += `<div class="prop-section">966 <div style="font-size:11px;color:var(--muted);margin-bottom:6px;text-transform:uppercase;letter-spacing:0.5px;">Preenchimento</div>967 <div class="prop-row"><span class="prop-label">Cor</span><input type="color" class="prop-color" id="prop-fill" value="#${el.fill || '4472C4'}"></div>968 </div>`;969 }970 971 html += `<div class="prop-section">972 <button class="prop-btn danger" id="prop-delete" style="width:100%;"><i class="fas fa-trash"></i> Excluir Elemento</button>973 </div>`;974 975 cont.innerHTML = html;976 977 // Bind eventos das propriedades978 const bind = (id, fn) => { const e = document.getElementById(id); if (e) e.addEventListener('input', fn); };979 const bindClick = (id, fn) => { const e = document.getElementById(id); if (e) e.addEventListener('click', fn); };980 981 bind('prop-x', e => { el.x = parseFloat(e.target.value) * 9144; slide.modified = true; renderSlide(); });982 bind('prop-y', e => { el.y = parseFloat(e.target.value) * 9144; slide.modified = true; renderSlide(); });983 bind('prop-w', e => { el.width = Math.max(9144, parseFloat(e.target.value) * 9144); slide.modified = true; renderSlide(); });984 bind('prop-h', e => { el.height = Math.max(9144, parseFloat(e.target.value) * 9144); slide.modified = true; renderSlide(); });985 986 if (el.type === 'text') {987 bind('prop-font', e => { el.paragraphs.forEach(p => p.runs.forEach(r => r.font = e.target.value)); slide.modified = true; renderSlide(); syncToolbar(); });988 bind('prop-fsize', e => { const sz = parseFloat(e.target.value) || 18; el.paragraphs.forEach(p => p.runs.forEach(r => r.size = sz)); slide.modified = true; renderSlide(); syncToolbar(); });989 bind('prop-fcolor', e => { const c = e.target.value.replace('#',''); el.paragraphs.forEach(p => p.runs.forEach(r => r.color = c)); slide.modified = true; renderSlide(); syncToolbar(); });990 bindClick('prop-bold', () => { const b = !el.paragraphs[0].runs[0].bold; el.paragraphs.forEach(p => p.runs.forEach(r => r.bold = b)); slide.modified = true; renderSlide(); renderProps(); syncToolbar(); });991 bindClick('prop-italic', () => { const v = !el.paragraphs[0].runs[0].italic; el.paragraphs.forEach(p => p.runs.forEach(r => r.italic = v)); slide.modified = true; renderSlide(); renderProps(); syncToolbar(); });992 bindClick('prop-underline', () => { const v = !el.paragraphs[0].runs[0].underline; el.paragraphs.forEach(p => p.runs.forEach(r => r.underline = v)); slide.modified = true; renderSlide(); renderProps(); syncToolbar(); });993 bindClick('prop-al', () => { el.paragraphs.forEach(p => p.align = 'left'); slide.modified = true; renderSlide(); renderProps(); syncToolbar(); });994 bindClick('prop-ac', () => { el.paragraphs.forEach(p => p.align = 'center'); slide.modified = true; renderSlide(); renderProps(); syncToolbar(); });995 bindClick('prop-ar', () => { el.paragraphs.forEach(p => p.align = 'right'); slide.modified = true; renderSlide(); renderProps(); syncToolbar(); });996 }997 998 if (el.type === 'shape' || (el.type === 'text' && el.fill !== undefined)) {999 bind('prop-fill', e => { el.fill = e.target.value.replace('#',''); slide.modified = true; renderSlide(); });1000 }1001 1002 bindClick('prop-delete', () => {1003 pushHistory();1004 const si = state.slides[state.currentSlide];1005 si.elements = si.elements.filter(e => e.id !== el.id);1006 si.modified = true;1007 state.selectedId = null;1008 renderAll();1009 });1010}1011 1012function syncToolbar() {1013 const slide = state.slides[state.currentSlide];1014 if (!slide) return;1015 const el = slide.elements.find(e => e.id === state.selectedId);1016 if (!el || el.type !== 'text') return;1017 1018 const run = el.paragraphs[0]?.runs[0] || {};1019 document.getElementById('font-select').value = run.font || 'Calibri';1020 document.getElementById('font-size').value = run.size || 18;1021 document.getElementById('font-color').value = '#' + (run.color || '000000');1022 document.getElementById('btn-bold').classList.toggle('active', !!run.bold);1023 document.getElementById('btn-italic').classList.toggle('active', !!run.italic);1024 document.getElementById('btn-underline').classList.toggle('active', !!run.underline);1025 document.getElementById('btn-align-l').classList.toggle('active', el.paragraphs[0]?.align === 'left');1026 document.getElementById('btn-align-c').classList.toggle('active', el.paragraphs[0]?.align === 'center');1027 document.getElementById('btn-align-r').classList.toggle('active', el.paragraphs[0]?.align === 'right');1028}1029 1030function updateStatus() {1031 document.getElementById('status-file').innerHTML = `<i class="fas fa-file"></i> ${escHtml(state.fileName)}`;1032 document.getElementById('status-slide').innerHTML = `<i class="fas fa-layer-group"></i> Slide ${state.slides.length > 0 ? state.currentSlide + 1 : '-'}/${state.slides.length}`;1033 const slide = state.slides[state.currentSlide];1034 document.getElementById('status-elems').innerHTML = `<i class="fas fa-shapes"></i> ${slide ? slide.elements.length : 0} elementos`;1035 document.getElementById('status-zoom').innerHTML = `<i class="fas fa-search"></i> ${Math.round(state.scale * 914400 / 96)}%`;1036}1037 1038// ============================================================1039// INTERAÇÃO (SELEÇÃO, DRAG, RESIZE, EDIÇÃO)1040// ============================================================1041function exitEditing() {1042 if (!state.editing) return;1043 state.editing = false;1044 const slide = state.slides[state.currentSlide];1045 const el = slide?.elements.find(e => e.id === state.selectedId);1046 if (el && el.type === 'text') {1047 const domEl = document.querySelector(`.slide-element[data-id="${el.id}"]`);1048 if (domEl) {1049 // Extrair texto editado1050 const paragraphs = [];1051 domEl.querySelectorAll('p').forEach(p => {1052 const para = { align: p.style.textAlign || 'left', runs: [] };1053 const firstRun = el.paragraphs[0]?.runs[0] || { bold:false, italic:false, underline:false, size:18, color:'000000', font:'Calibri' };1054 1055 if (p.children.length === 0) {1056 para.runs.push({ ...firstRun, text: p.textContent || '' });1057 } else {1058 p.childNodes.forEach(node => {1059 if (node.nodeType === Node.TEXT_NODE) {1060 if (node.textContent) para.runs.push({ ...firstRun, text: node.textContent });1061 } else if (node.nodeType === Node.ELEMENT_NODE) {1062 const s = node.style;1063 const run = {1064 text: node.textContent || '',1065 bold: s.fontWeight === '700' || s.fontWeight === 'bold',1066 italic: s.fontStyle === 'italic',1067 underline: s.textDecoration === 'underline',1068 size: parseFloat(s.fontSize) / (12700 * state.scale) || firstRun.size,1069 color: (s.color || '').replace('#','') || firstRun.color,1070 font: (s.fontFamily || '').replace(/['"]/g,'') || firstRun.font1071 };1072 para.runs.push(run);1073 }1074 });1075 }1076 if (para.runs.length === 0) para.runs.push({ ...firstRun, text: '' });1077 paragraphs.push(para);1078 });1079 if (paragraphs.length > 0) {1080 el.paragraphs = paragraphs;1081 slide.modified = true;1082 }1083 }1084 }1085 renderSlide();1086}1087 1088function initEditorEvents() {1089 const canvas = document.getElementById('slide-canvas');1090 1091 canvas.addEventListener('mousedown', (e) => {1092 const handle = e.target.closest('.resize-handle');1093 const elemDiv = e.target.closest('.slide-element');1094 1095 if (handle && elemDiv) {1096 // Iniciar resize1097 e.preventDefault();1098 e.stopPropagation();1099 state.resizing = true;1100 state.resizeDir = handle.dataset.dir;1101 state.selectedId = parseInt(elemDiv.dataset.id);1102 const slide = state.slides[state.currentSlide];1103 const el = slide.elements.find(e => e.id === state.selectedId);1104 state.dragStart = { x: e.clientX, y: e.clientY };1105 state.elemStart = { x: el.x, y: el.y, w: el.width, h: el.height };1106 pushHistory();1107 return;1108 }1109 1110 if (elemDiv) {1111 const id = parseInt(elemDiv.dataset.id);1112 if (state.editing && state.selectedId === id) return; // Já está editando1113 1114 if (state.editing) exitEditing();1115 1116 state.selectedId = id;1117 state.dragging = true;1118 state.dragStart = { x: e.clientX, y: e.clientY };1119 const slide = state.slides[state.currentSlide];1120 const el = slide.elements.find(e => e.id === id);1121 state.elemStart = { x: el.x, y: el.y, w: el.width, h: el.height };1122 pushHistory();1123 renderSlide();1124 renderProps();1125 syncToolbar();1126 return;1127 }1128 1129 // Clicou no vazio1130 if (state.editing) exitEditing();1131 state.selectedId = null;1132 renderSlide();1133 renderProps();1134 });1135 1136 canvas.addEventListener('dblclick', (e) => {1137 const elemDiv = e.target.closest('.slide-element');1138 if (!elemDiv) return;1139 const id = parseInt(elemDiv.dataset.id);1140 const slide = state.slides[state.currentSlide];1141 const el = slide.elements.find(e => e.id === id);1142 if (!el || el.type !== 'text') return;1143 1144 state.editing = true;1145 state.selectedId = id;1146 elemDiv.classList.add('editing');1147 elemDiv.contentEditable = 'true';1148 elemDiv.focus();1149 1150 // Selecionar todo o texto1151 const range = document.createRange();1152 range.selectNodeContents(elemDiv);1153 const sel = window.getSelection();1154 sel.removeAllRanges();1155 sel.addRange(range);1156 });1157 1158 document.addEventListener('mousemove', (e) => {1159 if (state.dragging && state.selectedId !== null) {1160 const dx = (e.clientX - state.dragStart.x) / state.scale;1161 const dy = (e.clientY - state.dragStart.y) / state.scale;1162 const slide = state.slides[state.currentSlide];1163 const el = slide.elements.find(e => e.id === state.selectedId);1164 if (el) {1165 el.x = Math.max(0, state.elemStart.x + dx);1166 el.y = Math.max(0, state.elemStart.y + dy);1167 slide.modified = true;1168 // Atualizar posição diretamente no DOM (sem re-render completo)1169 const domEl = document.querySelector(`.slide-element[data-id="${el.id}"]`);1170 if (domEl) {1171 domEl.style.left = (el.x * state.scale) + 'px';1172 domEl.style.top = (el.y * state.scale) + 'px';1173 }1174 }1175 }1176 1177 if (state.resizing && state.selectedId !== null) {1178 const dx = (e.clientX - state.dragStart.x) / state.scale;1179 const dy = (e.clientY - state.dragStart.y) / state.scale;1180 const slide = state.slides[state.currentSlide];1181 const el = slide.elements.find(e => e.id === state.selectedId);1182 if (!el) return;1183 1184 const minS = 91440; // tamanho mínimo1185 const dir = state.resizeDir;1186 let nx = state.elemStart.x, ny = state.elemStart.y;1187 let nw = state.elemStart.w, nh = state.elemStart.h;1188 1189 if (dir.includes('e')) nw = Math.max(minS, state.elemStart.w + dx);1190 if (dir.includes('w')) { nw = Math.max(minS, state.elemStart.w - dx); nx = state.elemStart.x + state.elemStart.w - nw; }1191 if (dir.includes('s')) nh = Math.max(minS, state.elemStart.h + dy);1192 if (dir.includes('n')) { nh = Math.max(minS, state.elemStart.h - dy); ny = state.elemStart.y + state.elemStart.h - nh; }1193 1194 el.x = nx; el.y = ny; el.width = nw; el.height = nh;1195 slide.modified = true;1196 1197 const domEl = document.querySelector(`.slide-element[data-id="${el.id}"]`);1198 if (domEl) {1199 domEl.style.left = (el.x * state.scale) + 'px';1200 domEl.style.top = (el.y * state.scale) + 'px';