PageSourceSearch

https://bep40-vnews.hf.space/static/modal_quote_design.js

js bep40-vnews.hf.space collected 2026-10-03 16:43:23 UTC 19,723 bytes, 607 lines download raw bytes

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()">&times;</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">&times;</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">&times;</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.