1/** 2 * VNEWS Modal Quote Design v1.0 3 * Based on 4 Facebook images from bep40/ml-intern-469bfdbd-datasets 4 * 5 * Style 1 - TrÃch dẫn (Quote Modal): 6 * First type: Based on quote content - person talking about person/event in image (2 images) 7 * Second type: Person speaking the quote content directly (1 image) 8 * 9 * Style 2 - Lưá»i ảnh (Grid Modal): 10 * Grid from SP image onwards, up to 4 images, each with a quote about its content 11 */ 12 13// ===== State ===== 14var _quoteState = { 15 images: [], // uploaded images 16 currentImgIdx: -1, // index of image being edited 17 quotes: [], // array of {image, quote, speaker, speakerRole, quoteType} 18 quoteType: 'about', // 'about' (person talking about person/event in image) or 'speaker' (person speaking quote content) 19}; 20 21// ===== CSS for Modal Quote Design ===== 22var MODAL_QUOTE_CSS = ` 23<style id="modal-quote-css"> 24/* Modal Quote Design - VNEWS Theme */ 25.modal-quote-overlay { 26 position: fixed !important; 27 inset: 0 !important; 28 background: rgba(0, 0, 0, 0.94) !important; 29 z-index: 99999 !important; 30 display: flex !important; 31 align-items: center !important; 32 justify-content: center !important; 33 padding: 12px !important; 34 overflow-y: auto !important; 35} 36.modal-quote-box { 37 background: #16161a !important; 38 border: 2px solid #5cb87a !important; 39 border-radius: 16px !important; 40 padding: 18px !important; 41 max-width: 760px !important; 42 width: 100% !important; 43 max-height: 94vh !important; 44 overflow-y: auto !important; 45} 46.modal-quote-title { 47 color: #5cb87a !important; 48 font-size: 16px !important; 49 font-weight: 700 !important; 50 margin: 0 0 14px !important; 51} 52.modal-quote-close { 53 background: none !important; 54 border: 0 !important; 55 color: #fff !important; 56 font-size: 24px !important; 57 cursor: pointer !important; 58} 59.mq-section { 60 margin-bottom: 12px !important; 61} 62.mq-section-label { 63 color: #888 !important; 64 font-size: 11px !important; 65 font-weight: 700 !important; 66 display: block !important; 67 margin-bottom: 6px !important; 68} 69.mq-input { 70 width: 100% !important; 71 background: #222 !important; 72 border: 1px solid #333 !important; 73 color: #eee !important; 74 border-radius: 8px !important; 75 padding: 8px 12px !important; 76 font-size: 12px !important; 77 resize: vertical !important; 78 min-height: 44px !important; 79} 80.mq-select { 81 width: 100% !important; 82 background: #222 !important; 83 border: 1px solid #333 !important; 84 color: #eee !important; 85 border-radius: 8px !important; 86 padding: 8px 12px !important; 87 font-size: 12px !important; 88} 89.mq-type-btn { 90 flex: 1 !important; 91 padding: 8px 12px !important; 92 border-radius: 8px !important; 93 font-size: 11px !important; 94 font-weight: 700 !important; 95 cursor: pointer !important; 96 border: 1px solid #333 !important; 97 color: #ccc !important; 98 background: #333 !important; 99 transition: all 0.2s !important; 100} 101.mq-type-btn.active { 102 background: #2d8659 !important; 103 border-color: #2d8659 !important; 104 color: #fff !important; 105} 106.mq-preview { 107 margin-top: 12px !important; 108 border-radius: 12px !important; 109 overflow: hidden !important; 110 border: 1px solid #333 !important; 111 max-width: 100% !important; 112} 113.mq-preview img { 114 width: 100% !important; 115 display: block !important; 116 border-radius: 10px !important; 117} 118.mq-actions { 119 display: flex !important; 120 gap: 8px !important; 121 margin-top: 14px !important; 122} 123.mq-actions button { 124 flex: 1 !important; 125 padding: 11px !important; 126 border-radius: 12px !important; 127 font-size: 12px !important; 128 font-weight: 700 !important; 129 cursor: pointer !important; 130 border: 1px solid #333 !important; 131 color: #ccc !important; 132 background: #333 !important; 133} 134.mq-actions button.primary { 135 background: #2d8659 !important; 136 border-color: #2d8659 !important; 137 color: #fff !important; 138} 139.mq-actions button.secondary { 140 background: #222 !important; 141} 142.mq-image-grid { 143 display: grid !important; 144 grid-template-columns: repeat(4, 1fr) !important; 145 gap: 8px !important; 146 margin-top: 8px !important; 147} 148.mq-image-grid img { 149 width: 100% !important; 150 aspect-ratio: 3/4 !important; 151 object-fit: cover !important; 152 border-radius: 8px !important; 153 cursor: pointer !important; 154 border: 2px solid transparent !important; 155 transition: border-color 0.2s !important; 156} 157.mq-image-grid img.selected { 158 border-color: #ffd700 !important; 159} 160.mq-status { 161 color: #888 !important; 162 font-size: 11px !important; 163 text-align: center !important; 164 margin-top: 8px !important; 165 min-height: 16px !important; 166} 167.mq-color-palette { 168 display: flex !important; 169 flex-wrap: wrap !important; 170 gap: 6px !important; 171 margin-top: 6px !important; 172} 173.mq-color-btn { 174 width: 32px !important; 175 height: 32px !important; 176 border-radius: 6px !important; 177 border: 2px solid #333 !important; 178 cursor: pointer !important; 179 padding: 0 !important; 180 flex-shrink: 0 !important; 181} 182.mq-color-btn.active { 183 border-color: #fff !important; 184 transform: scale(1.1) !important; 185} 186</style> 187`; 188 189// ===== CSS for Grid Modal ===== 190var MODAL_GRID_CSS = ` 191<style id="modal-grid-css"> 192.modal-grid-overlay { 193 position: fixed !important; 194 inset: 0 !important; 195 background: rgba(0, 0, 0, 0.94) !important; 196 z-index: 99999 !important; 197 display: flex !important; 198 align-items: center !important; 199 justify-content: center !important; 200 padding: 12px !important; 201 overflow-y: auto !important; 202} 203.modal-grid-box { 204 background: #16161a !important; 205 border: 2px solid #5cb87a !important; 206 border-radius: 16px !important; 207 padding: 18px !important; 208 max-width: 680px !important; 209 width: 100% !important; 210 max-height: 94vh !important; 211 overflow-y: auto !important; 212} 213.mg-image-item { 214 background: #1a1a1a !important; 215 border: 1px solid #2a2a2a !important; 216 border-radius: 10px !important; 217 padding: 10px !important; 218 margin-bottom: 10px !important; 219} 220.mg-image-preview { 221 width: 100% !important; 222 aspect-ratio: 16/9 !important; 223 object-fit: cover !important; 224 border-radius: 8px !important; 225 margin-bottom: 6px !important; 226} 227.mg-quote-input { 228 width: 100% !important; 229 background: #222 !important; 230 border: 1px solid #333 !important; 231 color: #eee !important; 232 border-radius: 8px !important; 233 padding: 8px 12px !important; 234 font-size: 12px !important; 235 min-height: 44px !important; 236 resize: vertical !important; 237} 238</style> 239`; 240 241// ===== Open Quote Modal ===== 242function openModalQuoteDesigner(postIdx) { 243 const p = _wallPosts[postIdx]; 244 if(!p) return; 245 246 // Inject CSS 247 if(!document.getElementById('modal-quote-css')) { 248 document.head.insertAdjacentHTML('beforeend', MODAL_QUOTE_CSS); 249 } 250 251 // Initialize state 252 _quoteState.images = []; 253 _quoteState.currentImgIdx = -1; 254 _quoteState.quotes = []; 255 256 // Get all images from post 257 if(p.img && p.img) _quoteState.images.push(p.img); 258 if(p.images && p.images.length) _quoteState.images.push(...p.images); 259 260 // Build modal HTML 261 let h = `<div class="modal-quote-overlay" id="modal-quote-overlay"> 262 <div class="modal-quote-box"> 263 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px"> 264 <h3 class="modal-quote-title">ð¨ Thiết kế ảnh trÃch dẫn</h3> 265 <button class="modal-quote-close" onclick="closeModalQuote()">×</button> 266 </div>`; 267
268 // Image selection section 269 if(_quoteState.images.length > 0) { 270 h += `<div class="mq-section"> 271 <label class="mq-section-label">ð¼ï¸ Chá»n ảnh ná»n (SP trá» lên):</label> 272 <div class="mq-image-grid" id="mq-image-grid">`; 273 _quoteState.images.forEach((img, idx) => { 274 h += `<img src="${_proxyImg(img)}" onclick="selectQuoteImage(${idx})" 275 ${idx === 0 ? 'class="selected"' : ''} 276 onerror="this.style.display='none'">`; 277 }); 278 h += `</div></div>`; 279 } 280 281 // Quote type selection 282 h += `<div class="mq-section"> 283 <label class="mq-section-label">ð Phong cách trÃch dẫn:</label> 284 <div style="display:flex;gap:8px"> 285 <button class="mq-type-btn active" id="mq-type-about" onclick="setQuoteType('about')"> 286 ð£ï¸ Ngưá»i nói vá» ngưá»i/sá»± kiá»n trong ảnh 287 </button> 288 <button class="mq-type-btn" id="mq-type-speaker" onclick="setQuoteType('speaker')"> 289 ð¤ Ngưá»i nói ná»i dung trÃch dẫn 290 </button> 291 </div> 292 </div>`; 293 294 // Quote text input 295 h += `<div class="mq-section"> 296 <label class="mq-section-label">âï¸ Ná»i dung trÃch dẫn:</label> 297 <textarea id="mq-quote-text" class="mq-input" placeholder="Nháºp ná»i dung trÃch dẫn..."></textarea> 298 </div>`; 299 300 // Speaker input 301 h += `<div class="mq-section"> 302 <label class="mq-section-label">ð¤ Ngưá»i nói:</label> 303 <input type="text" id="mq-speaker" class="mq-input" placeholder="Tên ngưá»i nói..."> 304 </div>`; 305 306 // Speaker role 307 h += `<div class="mq-section"> 308 <label class="mq-section-label">ð·ï¸ Vai trò/chức danh:</label> 309 <input type="text" id="mq-speaker-role" class="mq-input" placeholder="Và dụ: HLV, Cầu thá»§, Chuyên gia..."> 310 </div>`; 311 312 // Color palette 313 h += `<div class="mq-section"> 314 <label class="mq-section-label">ð¨ Mà u chữ:</label> 315 <div class="mq-color-palette" id="mq-color-palette">`; 316 const colors = ['#ffffff', '#ffd700', '#5cb87a', '#ff6b6b', '#4dabf7', '#9400d3', '#ff69b4', '#ff8c00']; 317 colors.forEach((c, idx) => { 318 h += `<button class="mq-color-btn" style="background:${c}" onclick="selectQuoteColor('${c}')" 319 ${idx === 0 ? 'class="mq-color-btn active"' : 'class="mq-color-btn"'}></button>`; 320 }); 321 h += `</div></div>`; 322 323 // Preview 324 h += `<div class="mq-section"> 325 <label class="mq-section-label">ðï¸ Xem trưá»c:</label> 326 <div class="mq-preview" id="mq-preview"> 327 <div style="text-align:center;color:#888;font-size:12px;padding:40px">Chá»n ảnh và nháºp ná»i dung Äá» xem trưá»c</div> 328 </div> 329 </div>`; 330 331 // Status 332 h += `<div class="mq-status" id="mq-status"></div>`; 333 334 // Actions 335 h += `<div class="mq-actions"> 336 <button class="secondary" onclick="previewQuoteModal()">ðï¸ Xem trưá»c Äầy Äá»§</button> 337 <button class="primary" onclick="generateQuoteModal()">ð¨ Tạo ảnh trÃch dẫn</button> 338 </div></div></div>`; 339 340 document.body.insertAdjacentHTML('beforeend', h); 341 342 // Set initial image 343 if(_quoteState.images.length > 0) { 344 _quoteState.currentImgIdx = 0; 345 } 346 347 _currentQuoteColor = '#ffd700'; 348 349 // Add input listeners for live preview 350 ['mq-quote-text', 'mq-speaker', 'mq-speaker-role'].forEach(id => { 351 const el = document.getElementById(id); 352 if(el) el.addEventListener('input', () => updateQuotePreview()); 353 }); 354} 355 356// ===== Close Quote Modal ===== 357function closeModalQuote() { 358 document.getElementById('modal-quote-overlay')?.remove(); 359} 360 361// ===== Select quote image ===== 362function selectQuoteImage(idx) { 363 _quoteState.currentImgIdx = idx; 364 document.querySelectorAll('#mq-image-grid img').forEach((img, i) => { 365 img.classList.toggle('selected', i === idx); 366 }); 367 updateQuotePreview(); 368} 369 370// ===== Set quote type ===== 371var _currentQuoteType = 'about'; 372function setQuoteType(type) { 373 _currentQuoteType = type; 374 document.getElementById('mq-type-about').classList.toggle('active', type === 'about'); 375 document.getElementById('mq-type-speaker').classList.toggle('active', type !== 'about'); 376 updateQuotePreview(); 377} 378 379// ===== Color selection ===== 380var _currentQuoteColor = '#ffd700'; 381function selectQuoteColor(color) { 382 _currentQuoteColor = color; 383 document.querySelectorAll('.mq-color-btn').forEach(btn => { 384 btn.classList.toggle('active', btn.style.background === color); 385 }); 386 updateQuotePreview(); 387} 388 389// ===== Update preview ===== 390function updateQuotePreview() { 391 const idx = _quoteState.currentImgIdx; 392 if(idx < 0 || !_quoteState.images[idx]) return; 393 394 const text = document.getElementById('mq-quote-text')?.value || ''; 395 const speaker = document.getElementById('mq-speaker')?.value || 'Ẩn danh'; 396 const role = document.getElementById('mq-speaker-role')?.value || ''; 397 398 const typeLabel = _currentQuoteType === 'about' 399 ? 'ð£ï¸ Ngưá»i nói vá» ngưá»i/sá»± kiá»n trong ảnh' 400 : 'ð¤ Ngưá»i nói ná»i dung trÃch dẫn'; 401 402 const previewEl = document.getElementById('mq-preview'); 403 if(previewEl) { 404 previewEl.innerHTML = ` 405 <img src="${_proxyImg(_quoteState.images[idx])}" style="height:200px"> 406 <div style="padding:10px;background:#1a1a1a;border-top:1px solid #2a2a2a"> 407 <div style="color:#5cb87a;font-size:10px;margin-bottom:4px">${typeLabel}</div> 408 <div style="color:${_currentQuoteColor};font-size:12px;line-height:1.4">${text || 'Chưa nháºp trÃch dẫn...'}</div> 409 <div style="color:#ccc;font-size:12px;margin-top:4px;text-align:right">â ${speaker}</div> 410 ${role ? `<div style="color:#888;font-size:10px;text-align:right">${role}</div>` : ''} 411 </div> 412 `; 413 } 414} 415 416// ===== Generate Quote Modal ===== 417async function generateQuoteModal() { 418 const idx = _quoteState.currentImgIdx; 419 if(idx < 0 || !_quoteState.images[idx]) { 420 alert('Vui lòng chá»n má»t ảnh!'); 421 return; 422 } 423 424 const text = document.getElementById('mq-quote-text')?.value || ''; 425 if(!text.trim()) { 426 alert('Vui lòng nháºp ná»i dung trÃch dẫn!'); 427 return; 428 } 429 430 const speaker = document.getElementById('mq-speaker')?.value || 'Ẩn danh'; 431 const role = document.getElementById('mq-speaker-role')?.value || ''; 432 const imgUrl = _quoteState.images[idx]; 433 434 // Upload to wall as a quote post 435 const post = { 436 id: 'quote-' + Date.now(), 437 title: `"${text}"`, 438 text: text, 439 img: imgUrl, 440 url: '', 441 slides: [{ 442 text: text, 443 image: imgUrl, 444 index: 1, 445 }], 446 kind: 'quote_modal', 447 voice: 'vi-VN-HoaiMyNeural', 448 emotion: detectEmotion(text), 449 language: 'vi', 450 quote_type: _currentQuoteType, 451 speaker: speaker, 452 speaker_role: role, 453 color: _currentQuoteColor, 454 ts: Math.floor(Date.now() / 1000), 455 }; 456 457 if(typeof prependWallPost === 'function') { 458 prependWallPost(post); 459 } else if(Array.isArray(_wallPosts)) { 460 _wallPosts.unshift(post); 461 } 462 463 toast('â Äã tạo ảnh trÃch dẫn và ÄÄng lên Tưá»ng AI!'); 464 closeModalQuote(); 465} 466 467// ===== Preview full size ===== 468function previewQuoteModal() { 469 const idx = _quoteState.currentImgIdx; 470 if(idx < 0 || !_quoteState.images[idx]) return; 471 472 const text = document.getElementById('mq-quote-text')?.value || ''; 473 const speaker = document.getElementById('mq-speaker')?.value || 'Ẩn danh'; 474 const role = document.getElementById('mq-speaker-role')?.value || ''; 475 476 // Show full preview in overlay 477 const previewOverlay = document.createElement('div'); 478 previewOverlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.9);z-index:99998;display:flex;align-items:center;justify-content:center;padding:12px'; 479 previewOverlay.innerHTML = ` 480 <div style="max-width:90%;max-height:80vh;text-align:center"> 481 <img src="${_proxyImg(_quoteState.images[idx])}" style="max-width:100%;max-height:60vh;border-radius:12px;border:2px solid #5cb87a"> 482 <div style="margin-top:12px;padding:12px;background:#1a1a1a;border-radius:10px;border:1px solid #2a2a2a"> 483 <div style="color:#5cb87a;font-size:13px;font-weight:700;margin-bottom:8px"> 484 ${_currentQuoteType === 'about' ? 'ð£ï¸ Ngưá»i nói vá» ngưá»i/sá»± kiá»n' : 'ð¤ Ngưá»i nói ná»i dung trÃch dẫn'} 485 </div> 486 <div style="color:#fff;font-size:15px;line-height:1.5;margin-bottom:8px">${text}</div> 487 <div style="color:${_currentQuoteColor};font-size:14px;font-weight:700">â ${speaker}</div> 488 ${role ? `<div style="color:#888;font-size:12px">${role}</div>` : ''} 489 </div> 490 <button onclick="this.closest('div').parentElement.remove()" 491 style="position:absolute;top:8px;right:8px;background:none;border:0;color:#fff;font-size:24px;
491cursor:pointer">×</button> 492 </div> 493 `; 494 document.body.appendChild(previewOverlay); 495} 496 497// ===== Open Grid Modal Designer ===== 498function openModalGridDesigner(postIdx) { 499 const p = _wallPosts[postIdx]; 500 if(!p) return; 501 502 // Inject CSS 503 if(!document.getElementById('modal-grid-css')) { 504 document.head.insertAdjacentHTML('beforeend', MODAL_GRID_CSS); 505 } 506 507 // Get images (SP onwards = current image + any additional images) 508 const images = []; 509 if(p.img && p.img) images.push(p.img); 510 if(p.images && p.images.length) images.push(...p.images); 511 512 // Take up to 4 images (SP onwards) 513 const gridImages = images.slice(0, 4); 514 515 let h = `<div class="modal-grid-overlay" id="modal-grid-overlay"> 516 <div class="modal-grid-box"> 517 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px"> 518 <h3 style="color:#5cb87a;font-size:16px;font-weight:700;margin:0">ð Thiết kế ảnh lưá»i (tá»i Äa 4 ảnh)</h3> 519 <button onclick="closeModalGrid()" style="background:none;border:0;color:#fff;font-size:24px;cursor:pointer">×</button> 520 </div>`; 521 522 // Image list 523 gridImages.forEach((img, i) => { 524 const quote = _gridQuotes[i] || ''; 525 h += `<div class="mg-image-item"> 526 <img src="${_proxyImg(img)}" class="mg-image-preview"> 527 <input type="hidden" class="mg-img-url" value="${img}"> 528 <textarea class="mg-quote-input" placeholder="Nháºp trÃch dẫn cho ảnh ${i+1}..." 529 onchange="_gridQuotes[${i}]='${escape(quote)}'" 530 oninput="_gridQuotes[${i}]=this.value">${escape(quote)}</textarea> 531 </div>`; 532 }); 533 534 // Status 535 h += `<div style="color:#888;font-size:11px;text-align:center;margin:10px 0" id="mg-status"></div>`; 536 537 // Actions 538 h += `<div class="mq-actions"> 539 <button class="secondary" onclick="closeModalGrid()">Há»§y</button> 540 <button class="primary" onclick="generateGridModal()">ð¨ Tạo ảnh lưá»i</button> 541 </div></div></div>`; 542 543 document.body.insertAdjacentHTML('beforeend', h); 544 _gridQuotes = {}; 545 gridImages.forEach((_, i) => { 546 _gridQuotes[i] = ''; 547 }); 548} 549 550// ===== Grid state ===== 551var _gridQuotes = {}; 552 553// ===== Close Grid Modal ===== 554function closeModalGrid() { 555 document.getElementById('modal-grid-overlay')?.remove(); 556} 557 558// ===== Generate Grid Modal ===== 559async function generateGridModal() { 560 const items = document.querySelectorAll('.mg-image-item'); 561 if(!items.length) return; 562 563 const gridData = []; 564 items.forEach((item, i) => { 565 const imgUrl = item.querySelector('.mg-img-url').value; 566 const quote = _gridQuotes[i] || item.querySelector('.mg-quote-input').value; 567 if(imgUrl) { 568 gridData.push({ img: imgUrl, quote: quote || `Ná»i dung ảnh ${i+1}`, index: i + 1 }); 569 } 570 }); 571 572 if(!gridData.length) return; 573 574 // Create a wall post with all grid items 575 const post = { 576 id: 'grid-' + Date.now(), 577 title: 'ð Tóm tắt ảnh nhanh', 578 text: gridData.map(g => `⢠${g.quote}`).join('\n'), 579 img: gridData[0].img, 580 url: '', 581 slides: gridData.map(g => ({ 582 text: g.quote, 583 image: g.img, 584 index: g.index, 585 })), 586 kind: 'grid_modal', 587 grid_items: gridData, 588 ts: Math.floor(Date.now() / 1000), 589 }; 590 591 if(typeof prependWallPost === 'function') { 592 prependWallPost(post); 593 } else if(Array.isArray(_wallPosts)) { 594 _wallPosts.unshift(post); 595 } 596 597 toast('â Äã tạo ảnh lưá»i và ÄÄng lên Tưá»ng AI!'); 598 closeModalGrid(); 599} 600 601// ===== Escape helper ===== 602function escape(s) { 603 return String(s || '').replace(/'/g, "\\'"); 604} 605 606// ===== Init ===== 607console.log('VNEWS Modal Quote Design v1.0 loaded - Style 1: TrÃch dẫn, Style 2: Lưá»i ảnh');
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.