bep40 commited on
Commit
4a48d3e
·
verified ·
1 Parent(s): ad95efe

Upload static/app_v2.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. static/app_v2.js +175 -566
static/app_v2.js CHANGED
@@ -1,4 +1,3 @@
1
-
2
  function _proxyImg(url){
3
  if(!url || typeof url !== 'string') return '';
4
  if(url.startsWith('http') && !url.includes(location.host)){
@@ -7,12 +6,35 @@ function _proxyImg(url){
7
  return url;
8
  }
9
 
10
- // === VNEWS Frontend v2 - Fixed for Short AI voice/emotion selection ===
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- // Store selection state globally for each post
13
  var _ttsSelections = {};
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- // === LOAD HOME - Fast: immediate shell + parallel fetch ===
16
  function _fetchWithTimeout(url, ms){
17
  return new Promise((resolve,reject)=>{
18
  const ctrl=new AbortController();
@@ -25,668 +47,255 @@ function _fetchWithTimeout(url, ms){
25
  });
26
  }
27
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  async function loadHome(){
29
  const homeEl = document.getElementById('view-home');
30
  if(!homeEl) return;
31
 
32
- // Build shell IMMEDIATELY — no skeleton, no delay
33
  homeEl.innerHTML =
34
  '<div id="home-featured-area"></div>'
35
  +'<div class="ai-compose"><div class="ai-compose-title">🤖 AI viết bài</div><div class="ai-compose-row"><input id="topic-input" placeholder="Nhập chủ đề..."><button onclick="searchTopic()">Tìm nguồn</button></div><div class="ai-compose-row"><input id="url-input" placeholder="Dán URL bài viết..."><button class="secondary" onclick="rewriteUrl()">Rewrite</button></div><div id="hot-topics" class="hot-topic-row"></div></div>'
36
  +'<div id="hashtag-box"></div>'
37
  +'<div class="ls-section"><div class="ls-header"><h3>⚽ Livescore</h3></div><div class="ls-tabs"><span class="ls-tab active" data-tab="today" onclick="loadLivescore(\'today\')">📅 Hôm nay</span><span class="ls-tab" data-tab="live" onclick="loadLivescore(\'live\')">🔴 Live</span><span class="ls-tab" data-tab="incoming" onclick="loadLivescore(\'incoming\')">⏰ Sắp tới</span><span class="ls-tab" data-tab="results" onclick="loadLivescore(\'results\')">✅ Kết quả</span><span class="ls-tab" data-tab="bxh_nha" onclick="loadLivescore(\'bxh_nha\')">🏆 NHA</span><span class="ls-tab" data-tab="bxh_laliga" onclick="loadLivescore(\'bxh_laliga\')">🏆 La Liga</span></div><div class="ls-content" id="ls-content"><div class="loading">Đang tải...</div></div></div>'
38
- +'<div id="wc2026-live-section" class="wc2026-section"><div class="wc-header"><h2>🏆 World Cup 2026</h2><span class="wc-live-badge">● LIVE</span></div><div class="wc-tabs"><span class="wc-tab active" onclick="switchWCTab(\'news\')">📰 Tin tức</span><span class="wc-tab" onclick="switchWCTab(\'fixtures\')">📅 Lịch thi đấu</span><span class="wc-tab" onclick="switchWCTab(\'standings\')">🏆 BXH</span><span class="wc-tab" onclick="switchWCTab(\'highlights\')">🎬 Highlight</span><span class="wc-tab" onclick="switchWCTab(\'stats\')">📊 Thống kê</span></div><div class="wc-content" id="wc-content"><div class="loading">Đang tải World Cup 2026...</div></div></div>'
39
  +'<div id="home-after-wc"></div>';
40
 
41
  const afterEl = homeEl.querySelector('#home-after-wc');
42
-
43
- // Start critical loads immediately
44
  loadLivescore('today');
45
  loadHotTopics();
46
 
47
- // Fetch all data in parallel with shorter timeouts
48
  const [featuredData, shortsData, wallData, hlLeagues, aiData, wcData] = await Promise.allSettled([
49
  _fetchWithTimeout('/api/livescore/featured', 5000),
50
- _fetchWithTimeout('/api/shorts', 8000),
51
  _fetchWithTimeout('/api/wall', 5000),
52
  _fetchWithTimeout('/api/highlights/leagues', 10000),
53
  _fetchWithTimeout('/api/genk_ai', 8000),
54
  _fetchWithTimeout('/api/wc2026', 8000),
55
  ]).then(results => results.map(r => r.status === 'fulfilled' ? r.value : null));
56
 
57
- // Render featured match
58
  if(featuredData && featuredData.home){
59
  const sc=featuredData.status==='live'?'':'upcoming';
60
- const st=featuredData.status==='live'?`🔴 ${featuredData.minute||'LIVE'}`:`${featuredData.time}`;
61
  const area=document.getElementById('home-featured-area');
62
- if(area) area.innerHTML=`<div class="featured-match" onclick="openMatch('${featuredData.event_id}')"><div class="fm-league">${featuredData.league}</div><div class="fm-teams"><div class="fm-team"><img src="${_proxyImg(featuredData.home_logo)}" onerror="this.style.display='none'"><span>${featuredData.home}</span></div><div class="fm-score">${featuredData.score||'VS'}</div><div class="fm-team"><img src="${_proxyImg(featuredData.away_logo)}" onerror="this.style.display='none'"><span>${featuredData.away}</span></div></div><div class="fm-status ${sc}">${st}</div></div>`;
63
  }
64
 
65
- // Store globally
66
  _shortsData = shortsData || [];
67
  _wallPosts = (wallData && wallData.posts) || [];
68
  _hlLeagueData = hlLeagues || {};
69
  _wc2026Data = wcData;
70
 
71
- // Render WC if data arrived
72
  if(wcData) switchWCTab('news');
73
 
74
- // Render sections into the after-wc area
75
  _renderShortsIn(afterEl);
76
  _renderWallIn(afterEl);
77
  _renderHLIn(afterEl);
78
  if(aiData && aiData.length) _renderSlidesIn('ai-articles','Ứng dụng AI','🤖',aiData,afterEl);
79
  }
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  function _renderSlidesIn(key, label, emoji, vids, afterEl){
82
  if(!vids||!vids.length||!afterEl) return;
83
- const wrap=document.createElement('div');
84
  wrap.className='slider-wrap';
85
- let h=`<div class="slider-header"><span class="slider-label">${emoji} ${label}</span></div><div class="slider-track">`;
86
  const isHL = key==='world-cup'||key==='premier-league'||key==='champions-league'||key==='la-liga'||key==='serie-a'||key==='bundesliga'||key==='friendly';
87
  vids.slice(0,isHL?8:12).forEach((a,i)=>{
88
  if(isHL){
89
- h+=`<div class="slider-item" onclick="openHighlightFeed('${key}',${i})"><div class="slider-thumb">${a.img?`<img src="${_proxyImg(a.img)}" loading="lazy">`:''}<div class="card-play">▶</div></div><div class="slider-title">${esc(a.title)}</div></div>`;
90
  } else {
91
- h+=`<div class="slider-item" onclick="readArticle('${esc(a.link)}')"><div class="slider-thumb">${a.img?`<img src="${_proxyImg(a.img)}" loading="lazy">`:''}</div><div class="slider-title">${esc(a.title)}</div></div>`;
92
  }
93
  });
94
- h+='</div>';
95
- wrap.innerHTML=h;
96
- afterEl.parentNode.insertBefore(wrap, afterEl);
97
- }
98
-
99
- function _renderShortsIn(afterEl){
100
- if(!_shortsData||!_shortsData.length||!afterEl) return;
101
- const mixed=interleaveShorts(_shortsData);
102
- if(!mixed.length) return;
103
- const wrap=document.createElement('div');
104
- wrap.className='slider-wrap';
105
- let h=`<div class="slider-header"><span class="slider-label">📱 Shorts Dân trí & SKĐS</span><span class="slider-note">Mới nhất · xen kẽ</span></div><div class="slider-track">`;
106
- mixed.slice(0,30).forEach((a,i)=>{
107
- const badge=a.channel==='baosuckhoedoisongboyte'?'SKĐS':'Dân trí';
108
- h+=`<div class="slider-item shorts-item" onclick="openYTShortsFeed(${i})"><div class="slider-thumb shorts-thumb">${a.img?`<img src="${_proxyImg(a.img)}" loading="lazy">`:''}<div class="card-play">▶</div></div><div class="slider-title"><span style="color:#f0c040;font-size:8px">${badge}</span> ${esc(a.title)}</div></div>`;
109
- });
110
- h+='</div>';wrap.innerHTML=h;
111
- afterEl.parentNode.insertBefore(wrap,afterEl);
112
  }
113
 
114
  function _renderWallIn(afterEl){
115
  if(!_wallPosts||!_wallPosts.length||!afterEl) return;
116
- const posts=_wallPosts;
117
- const aiShorts=posts.filter(p=>p.video);
118
  if(aiShorts.length){
119
- const wrap=document.createElement('div');
120
- wrap.className='slider-wrap';wrap.id='short-ai-section';
121
- let h='<div class="slider-header"><span class="slider-label">🎬 Short AI</span></div><div class="slider-track">';
122
- aiShorts.slice(0,20).forEach((p,i)=>{h+=`<div class="slider-item shorts-item" onclick="openShortAIFeed(${i})"><div class="slider-thumb shorts-thumb"><video src="${p.video}" muted preload="metadata"></video><div class="card-play">▶</div></div><div class="slider-title">${esc(p.title)}</div></div>`;});
123
- h+='</div>';wrap.innerHTML=h;
124
- afterEl.parentNode.insertBefore(wrap,afterEl);
125
  }
126
- const wrap=document.createElement('div');
127
- wrap.className='slider-wrap';wrap.id='ai-wall-wrap';
128
- let h='<div class="slider-header"><span class="slider-label">🧱 Tường AI</span></div><div class="slider-track" id="ai-wall-track">';
129
  posts.slice(0,20).forEach((p,i)=>{h+=makeWallItem(p,i);});
130
- h+='</div>';wrap.innerHTML=h;
131
- afterEl.parentNode.insertBefore(wrap,afterEl);
132
  }
133
 
134
  function _renderHLIn(afterEl){
135
  if(!_hlLeagueData||Object.keys(_hlLeagueData).length===0||!afterEl) return;
136
- const HL_CONFIG={"world-cup":{name:"World Cup 2026",emoji:"🌍"},"premier-league":{name:"Premier League",emoji:"🏴"},"champions-league":{name:"Champions League",emoji:"⭐"},"la-liga":{name:"La Liga",emoji:"🇪🇸"},"serie-a":{name:"Serie A",emoji:"🇮🇹"},"bundesliga":{name:"Bundesliga",emoji:"🇩🇪"},"friendly":{name:"Giao hữu",emoji:"🤝"}};
137
- for(const[key,cfg] of Object.entries(HL_CONFIG)){
138
- const vids=_hlLeagueData[key];
139
  if(!vids||!vids.length) continue;
140
  _renderSlidesIn(key,cfg.name,cfg.emoji,vids,afterEl);
141
  }
142
  }
143
 
144
- // === WALL POST HELPERS ===
145
  function makeWallItem(p,i){
146
  var hasVideo = p.video && p.video.length > 0;
147
- var thumbContent = p.img
148
- ? '<img src="/api/proxy/img?url='+encodeURIComponent(p.img)+'" loading="lazy" onerror="this.style.display=\'none\'">'
149
- : (hasVideo ? '<video src="'+esc(p.video)+'" muted></video>' : '');
150
  var videoBadge = hasVideo ? '<div class="wall-video-badge">🎬</div>' : '';
151
  var vid = p.id||i;
152
- var lang = p.language || detectLanguage(p.title + ' ' + (p.text||''));
153
- var curVoice = p.voice || getAutoVoice(lang);
154
- var curEmotion = p.emotion || detectEmotion(p.title + ' ' + (p.text||''));
155
  var selKey = 'inline-' + vid;
156
- if(!_ttsSelections[selKey]) _ttsSelections[selKey] = {voice: curVoice, emotion: curEmotion};
157
- var voiceOpts = '';
158
- VOICE_LIST.forEach(function(v){
159
- voiceOpts += '<option value="'+v.id+'"'+(v.id===_ttsSelections[selKey].voice?' selected':'')+'>'+v.label+'</option>';
160
  });
161
- var emotOpts = '';
162
- EMOTION_LIST.forEach(function(e){
163
- emotOpts += '<option value="'+e.id+'"'+(e.id===_ttsSelections[selKey].emotion?' selected':'')+'>'+e.label+'</option>';
164
  });
165
  var spd = p.short_speed || '1.2';
166
- var voiceBar = '<div class="wall-tts-bar" style="margin:6px 0 4px;display:grid;grid-template-columns:1fr auto auto;gap:3px">'
167
- +'<select class="wvs" data-selkey="'+selKey+'" style="background:#1a1a1a;border:1px solid #333;color:#ccc;padding:3px;border-radius:6px;font-size:9px;min-width:0" onchange="_ttsSelections[this.dataset.selkey].voice=this.value">'
168
- +voiceOpts+'</select>'
169
- +'<select class="wes" data-selkey="'+selKey+'" style="background:#1a1a1a;border:1px solid #333;color:#ccc;padding:3px;border-radius:6px;font-size:9px;min-width:0" onchange="_ttsSelections[this.dataset.selkey].emotion=this.value">'
170
- +emotOpts+'</select>'
171
- +'<select class="wss" data-selkey="'+selKey+'" style="background:#1a1a1a;border:1px solid #333;color:#ccc;padding:3px;border-radius:6px;font-size:9px;min-width:0" onchange="_ttsSelections[this.dataset.selkey].speed=parseFloat(this.value)">'
172
- +'<option value="0.85"'+(spd==='0.85'?' selected':'')+'>0.85x</option>'
173
- +'<option value="1.0"'+(spd==='1.0'?' selected':'')+'>1.0x</option>'
174
- +'<option value="1.2"'+(spd==='1.2'?' selected':'')+'>1.2x</option>'
175
- +'<option value="1.35"'+(spd==='1.35'?' selected':'')+'>1.35x</option>'
176
- +'</select>'
177
- +'</div>';
178
- var makeBtn = hasVideo
179
- ? '<button class="wall-btn-video" onclick="event.stopPropagation();openShortAIFeed('+i+')">▶ Xem Short</button>'
180
- : '<button class="wall-btn-make" onclick="event.stopPropagation();makeShortVideo(\''+esc(vid)+'\',this,_ttsSelections[\'inline-'+esc(vid)+'\'].voice,parseFloat(document.querySelector(\'.wss[data-selkey=inline-'+esc(vid)+']\').value)||1.2,_ttsSelections[\'inline-'+esc(vid)+'\'].emotion)">🎬 Tạo Short</button>';
181
  return '<div class="wall-item" id="wall-item-'+esc(vid)+'"><div class="wall-thumb">'+thumbContent+videoBadge+'</div><div class="wall-title">'+esc(p.title)+'</div><div class="wall-text">'+esc((p.text||'').slice(0,180))+'</div>'+voiceBar+'<div class="wall-actions"><button class="primary" onclick="readWallPost('+i+')">Xem</button>'+makeBtn+'</div></div>';
182
  }
183
 
184
- async function makeShortVideo(postId, btn, voice, speed, emotion){
185
  if(!postId)return;
186
- const origText = btn ? btn.textContent : '🎬 Tạo Video';
187
  if(btn){btn.disabled=true;btn.textContent='⏳ Đang tạo...';}
188
  toast('⏳ Đang tạo video shorts...');
189
-
190
- // FIX: If no voice/emotion passed, read from _ttsSelections or from the tts-selector UI
191
- if(!voice || !emotion){
192
- const selKey = 'inline-'+postId;
193
- const container = document.querySelector('.tts-selector[data-post-id="'+postId+'"]');
194
- if(container){
195
- // Try to read from _ttsSelections first
196
- if(_ttsSelections[selKey]){
197
- voice = voice || _ttsSelections[selKey].voice;
198
- emotion = emotion || _ttsSelections[selKey].emotion;
199
- }
200
- // Fallback to style-based reading
201
- if(!voice){
202
- const selectedVoiceBtn = container.querySelector('.tts-voice-btn.selected') ||
203
- container.querySelector('.tts-voice-btn[style*="5cb87a"]') ||
204
- container.querySelector('.tts-voice-btn');
205
- voice = selectedVoiceBtn ? selectedVoiceBtn.dataset.voice : 'vi-VN-HoaiMyNeural';
206
- }
207
- if(!emotion){
208
- const selectedEmotionBtn = container.querySelector('.tts-emotion-btn.selected') ||
209
- container.querySelector('.tts-emotion-btn[style*="5cb87a"]') ||
210
- container.querySelector('.tts-emotion-btn');
211
- emotion = selectedEmotionBtn ? selectedEmotionBtn.dataset.emotion : 'neutral';
212
- }
213
- const speedSelect = container.querySelector('.tts-speed');
214
- speed = speedSelect ? parseFloat(speedSelect.value) || 1.2 : (speed || 1.2);
215
- } else {
216
- voice = voice || 'vi-VN-HoaiMyNeural';
217
- emotion = emotion || 'neutral';
218
- speed = speed || 1.2;
219
- }
220
- }
221
-
222
  try{
223
- const r = await fetch('/api/ai/short/' + encodeURIComponent(postId), {
224
- method:'POST',
225
- headers:{'Content-Type':'application/json'},
226
- body: JSON.stringify({voice:voice, emotion:emotion, speed:speed})
227
- });
228
- const j = await r.json();
229
- if(!r.ok || j.error) throw new Error(j.error||'Lỗi tạo video');
 
 
230
  toast('✅ Đã tạo video shorts!');
231
- const p = _wallPosts.find(x => String(x.id) === String(postId));
232
- if(p){
233
- p.video = j.video;
234
- p.voice = j.voice;
235
- p.emotion = j.emotion;
236
- const itemId = 'wall-item-'+postId;
237
- const el = document.getElementById(itemId);
238
- if(el){
239
- const idx = _wallPosts.indexOf(p);
240
- el.outerHTML = makeWallItem(p, idx);
241
- const newEl = document.getElementById(itemId);
242
- if(newEl) newEl.className = 'wall-item wall-item-new';
243
- }
244
- }
245
  refreshShortAISlider();
246
- }catch(e){
247
- toast('❌ '+e.message);
248
- if(btn){btn.disabled=false;btn.textContent=origText;}
249
- }
250
  }
251
 
252
  function refreshShortAISlider(){
253
- const aiShorts = _wallPosts.filter(p=>p.video);
254
- let shortAISection = document.getElementById('short-ai-section');
255
- if(aiShorts.length === 0){
256
- if(shortAISection) shortAISection.remove();
257
- return;
258
- }
259
- if(!shortAISection){
260
- // Create the section if it doesn't exist (first short created)
261
- const homeEl=document.getElementById('view-home');
262
- if(!homeEl) return;
263
- shortAISection=document.createElement('div');
264
- shortAISection.className='slider-wrap';shortAISection.id='short-ai-section';
265
- shortAISection.innerHTML='<div class="slider-header"><span class="slider-label">🎬 Short AI</span></div><div class="slider-track"></div>';
266
- const insertBefore=homeEl.querySelector('.slider-wrap');
267
- if(insertBefore) homeEl.insertBefore(shortAISection,insertBefore);
268
- else homeEl.appendChild(shortAISection);
269
- }
270
- const track = shortAISection.querySelector('.slider-track');
271
- if(track){
272
- let h = '';
273
- aiShorts.slice(0,20).forEach((p,i)=>{
274
- h+=`<div class="slider-item shorts-item" onclick="openShortAIFeed(${i})"><div class="slider-thumb shorts-thumb"><video src="${esc(p.video)}" muted preload="metadata"></video><div class="card-play">▶</div></div><div class="slider-title">${esc(p.title)}</div></div>`;
275
- });
276
- track.innerHTML = h;
277
  }
 
 
278
  }
279
 
280
- // === VOICE + EMOTION SYSTEM ===
281
- var VOICE_LIST = [
282
- {id:'vi-VN-HoaiMyNeural', label:'🎙️ Hoài My (VI)', lang:'vi'},
283
- {id:'vi-VN-NamMinhNeural', label:'🎙️ Nam Minh (VI)', lang:'vi'},
284
- {id:'en-US-AndrewMultilingualNeural', label:'🎙️ Andrew (EN)', lang:'en'},
285
- {id:'en-AU-WilliamMultilingualNeural', label:'🎙️ William (EN)', lang:'en'},
286
- {id:'pt-BR-ThalitaMultilingualNeural', label:'🎙️ Thalita (PT)', lang:'pt'},
287
- {id:'fr-FR-VivienneMultilingualNeural', label:'🎙️ Vivienne (FR)', lang:'fr'},
288
- {id:'fr-FR-RemyMultilingualNeural', label:'🎙️ Rémy (FR)', lang:'fr'},
289
- {id:'de-DE-SeraphinaMultilingualNeural', label:'🎙️ Seraphina (DE)', lang:'de'},
290
- {id:'de-DE-FlorianMultilingualNeural', label:'🎙️ Florian (DE)', lang:'de'},
291
- {id:'ko-KR-HyunsuMultilingualNeural', label:'🎙️ Hyunsu (KO)', lang:'ko'},
292
- {id:'it-IT-GiuseppeMultilingualNeural', label:'🎙️ Giuseppe (IT)', lang:'it'},
293
- ];
294
- var EMOTION_LIST = [
295
- {id:'neutral', label:'😐 Trung tính'},
296
- {id:'happy', label:'😊 Vui vẻ'},
297
- {id:'excited', label:'🔥 Hào hứng'},
298
- {id:'sad', label:'😢 Buồn'},
299
- {id:'humorous', label:'😂 Hài hước'},
300
- {id:'serious', label:'⚠️ Nghiêm túc'},
301
- {id:'urgent', label:'🚨 Khẩn cấp'},
302
- {id:'warm', label:'💖 Ấm áp'},
303
- ];
304
 
305
- // Event delegation for voice/emotion buttons (backup for dynamically rendered content)
306
- // Event delegation for TTS voice/emotion/speed - single source of truth
307
- document.addEventListener('click',function(e){
308
- // Voice button
309
- var btn = e.target.closest('.tts-voice-btn');
310
- if(btn){
311
- var container = btn.closest('.tts-selector');
312
- if(container){
313
- var selKey = 'inline-'+container.dataset.postId;
314
- if(!_ttsSelections[selKey]) _ttsSelections[selKey]={voice:btn.dataset.voice,emotion:'neutral'};
315
- // Reset all voice buttons in this container
316
- var allBtns = container.querySelectorAll('.tts-voice-btn');
317
- for(var i=0;i<allBtns.length;i++){
318
- allBtns[i].style.borderColor='#333';
319
- allBtns[i].style.background='#222';
320
- allBtns[i].classList.remove('selected');
321
- }
322
- // Highlight clicked button
323
- btn.style.borderColor='#5cb87a';
324
- btn.style.background='#1a2a1f';
325
- btn.classList.add('selected');
326
- _ttsSelections[selKey].voice = btn.dataset.voice;
327
- }
328
- return;
329
- }
330
- // Emotion button
331
- var ebtn = e.target.closest('.tts-emotion-btn');
332
- if(ebtn){
333
- var container = ebtn.closest('.tts-selector');
334
- if(container){
335
- var selKey = 'inline-'+container.dataset.postId;
336
- if(!_ttsSelections[selKey]) _ttsSelections[selKey]={voice:'vi-VN-HoaiMyNeural',emotion:ebtn.dataset.emotion};
337
- var allBtns = container.querySelectorAll('.tts-emotion-btn');
338
- for(var i=0;i<allBtns.length;i++){
339
- allBtns[i].style.borderColor='#333';
340
- allBtns[i].style.background='#222';
341
- allBtns[i].classList.remove('selected');
342
- }
343
- ebtn.style.borderColor='#5cb87a';
344
- ebtn.style.background='#1a2a1f';
345
- ebtn.classList.add('selected');
346
- _ttsSelections[selKey].emotion = ebtn.dataset.emotion;
347
- }
348
- return;
349
  }
350
- // Create button
351
- var cbtn = e.target.closest('.tts-create-btn');
352
- if(cbtn){
353
- var container = cbtn.closest('.tts-selector');
354
- if(container){
355
- var selKey = 'inline-'+container.dataset.postId;
356
- var selVoice = _ttsSelections[selKey] ? _ttsSelections[selKey].voice : 'vi-VN-HoaiMyNeural';
357
- var selEmotion = _ttsSelections[selKey] ? _ttsSelections[selKey].emotion : 'neutral';
358
- var speedSel = container.querySelector('.tts-speed');
359
- var speed = speedSel ? parseFloat(speedSel.value)||1.2 : 1.2;
360
- window.makeShortVideo(container.dataset.postId, cbtn, selVoice, speed, selEmotion);
361
- }
362
- return;
363
- }
364
- });
365
- function detectLanguage(text){
366
- if(!text) return 'vi';
367
- var t=text.toLowerCase(), chars=new Set(t);
368
- var vnChars='đăâêôơưàảãạáằẳẵặắầẩẫậấèẻẽẹéềễểệếìỉĩịíòỏõọóồổỗộốờởỡợớùủũụúừửữựứỳỷỹỵý';
369
- var vnCount=0; for(var c of vnChars){if(chars.has(c)) vnCount++;}
370
- if(vnCount>=2) return 'vi';
371
- if(chars.has('ñ')||chars.has('¿')||chars.has('¡')) return 'es';
372
- if(chars.has('ã')||chars.has('õ')) return 'pt';
373
- var words=t.split(/\s+/);
374
- var enWords=['the','is','at','which','on','and','or','but','this','that','with','from','have','been'];
375
- var enCount=words.filter(function(w){return enWords.indexOf(w)>=0;}).length;
376
- if(enCount>=2) return 'en';
377
- return 'vi';
378
  }
379
- function detectEmotion(text){
380
- if(!text) return 'neutral';
381
- var t=text.toLowerCase();
382
- var kws={
383
- happy:['vui','hạnh phúc','tuyệt','thành công','chiến thắng','feliz','maravilloso','happy','joy','wonderful','great','amazing','love','excellent'],
384
- excited:['hào hứng','phấn khích','đột phá','kỷ lục','đỉnh cao','emocionante','increíble','excited','thrilling','unbelievable','awesome','breakthrough'],
385
- sad:['buồn','đau','mất','thảm họa','khủng hoảng','triste','terrible','sad','unhappy','tragic','painful','death'],
386
- humorous:['hài hước','buồn cười','haha','đùa','engraçado','gracioso','funny','hilarious','joke','lol'],
387
- serious:['nghiêm trọng','khẩn cấp','quan trọng','lo ngại','sério','crítico','serious','critical','urgent','severe','crisis'],
388
- urgent:['khẩn cấp','báo động','ngay lập tức','urgent','breaking','alert','emergency'],
389
- warm:['ấm áp','tình cảm','yêu thương','warm','love','heart','touching']
390
- };
391
- var bestScore=0, bestEmotion='neutral';
392
- for(var em in kws){var score=0; for(var kw of kws[em]){if(t.indexOf(kw)>=0) score++;} if(score>bestScore){bestScore=score;bestEmotion=em;}}
393
- return bestEmotion;
394
- }
395
- function getAutoVoice(lang){var map={vi:'vi-VN-HoaiMyNeural',pt:'pt-BR-ThalitaMultilingualNeural',en:'en-US-AndrewMultilingualNeural',fr:'fr-FR-VivienneMultilingualNeural',de:'de-DE-SeraphinaMultilingualNeural',ko:'ko-KR-HyunsuMultilingualNeural',it:'it-IT-GiuseppeMultilingualNeural'};return map[lang]||'vi-VN-HoaiMyNeural';}
396
- function buildVoiceEmotionSelector(post){
397
- var lang=post.language||detectLanguage(post.title+' '+(post.text||''));
398
- // Normalize old voice IDs to new MultilingualNeural format
399
- var _oldVoiceMap = {'hoaimy':'vi-VN-HoaiMyNeural','namminh':'vi-VN-NamMinhNeural','andrew':'en-US-AndrewMultilingualNeural','jenny':'en-US-AndrewMultilingualNeural','thalita':'pt-BR-ThalitaMultilingualNeural','pt_thalita':'pt-BR-ThalitaMultilingualNeural','vivienne':'fr-FR-VivienneMultilingualNeural','remy':'fr-FR-RemyMultilingualNeural','seraphina':'de-DE-SeraphinaMultilingualNeural','florian':'de-DE-FlorianMultilingualNeural','sunhee':'ko-KR-HyunsuMultilingualNeural','hyunsu':'ko-KR-HyunsuMultilingualNeural','giuseppe':'it-IT-GiuseppeMultilingualNeural','ela':'en-US-AndrewMultilingualNeural','denise':'fr-FR-VivienneMultilingualNeural','katja':'de-DE-SeraphinaMultilingualNeural','nanami':'en-US-AndrewMultilingualNeural','xiaochen':'en-US-AndrewMultilingualNeural','es_carlos':'en-US-AndrewMultilingualNeural','pt_francisco':'pt-BR-ThalitaMultilingualNeural'};
400
- var _postVoice = post.voice ? (_oldVoiceMap[post.voice] || post.voice) : '';
401
- var autoVoice= _postVoice || getAutoVoice(lang);
402
- var autoEmotion=post.emotion||detectEmotion(post.title+' '+(post.text||''));
403
- var selKey = 'inline-'+post.id;
404
-
405
- if(!_ttsSelections[selKey]){
406
- _ttsSelections[selKey] = {voice: autoVoice, emotion: autoEmotion};
407
- }
408
-
409
- var h='<div class="tts-selector" data-post-id="'+post.id+'" style="margin-top:10px;padding:10px;background:#1a1a1a;border:1px solid #2a2a2a;border-radius:10px">';
410
- h+='<div style="font-size:11px;color:#888;margin-bottom:6px">🎙️ Giọng đọc (ngôn ngữ: '+lang.toUpperCase()+'):</div>';
411
- h+='<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">';
412
- VOICE_LIST.forEach(function(v){
413
- var sel=v.id===_ttsSelections[selKey].voice?'border-color:#5cb87a;background:#1a2a1f':'border-color:#333;background:#222';
414
- h+='<button class="tts-voice-btn '+(v.id===_ttsSelections[selKey].voice?'selected':'')+'" data-voice="'+v.id+'" data-post-id="'+post.id+'" style="'+sel+';border:1px solid;color:#ccc;padding:4px 8px;border-radius:10px;font-size:10px;cursor:pointer">'+v.label+'</button>';
415
- });
416
- h+='</div>';
417
- h+='<div style="font-size:11px;color:#888;margin-bottom:6px">😊 Cảm xúc:</div>';
418
- h+='<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">';
419
- EMOTION_LIST.forEach(function(e){
420
- var sel=e.id===_ttsSelections[selKey].emotion?'border-color:#5cb87a;background:#1a2a1f':'border-color:#333;background:#222';
421
- h+='<button class="tts-emotion-btn '+(e.id===_ttsSelections[selKey].emotion?'selected':'')+'" data-emotion="'+e.id+'" data-post-id="'+post.id+'" style="'+sel+';border:1px solid;color:#ccc;padding:4px 8px;border-radius:10px;font-size:10px;cursor:pointer">'+e.label+'</button>';
422
  });
423
- h+='</div>';
424
- h+='<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px"><span style="font-size:11px;color:#888">⚡ Tốc độ:</span>';
425
- h+='<select class="tts-speed" style="background:#222;border:1px solid #333;color:#ccc;padding:3px 8px;border-radius:8px;font-size:10px">';
426
- h+='<option value="0.85">0.85x Chậm</option><option value="1.0">1.0x Bình thường</option><option value="1.2" selected>1.2x Nhanh</option><option value="1.35">1.35x Rất nhanh</option>';
427
- h+='</select></div>';
428
- h+='<button class="tts-create-btn" style="width:100%;background:#2d8659;border:0;color:#fff;padding:8px;border-radius:10px;font-size:11px;font-weight:700;cursor:pointer">🎬 Tạo Short AI</button>';
429
- h+='</div>';
430
- return h;
431
  }
432
- // FIXED: bindVoiceEmotionSelector now uses proper class-based selection tracking
433
- window.showVoiceEmotionSelector=function(postId,title,text){
434
- var overlay=document.createElement('div');
435
- overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px';
436
- var box=document.createElement('div');
437
- box.style.cssText='background:#1a1a1a;border:2px solid #2d8659;border-radius:16px;padding:20px;max-width:400px;width:100%;max-height:80vh;overflow-y:auto';
438
- var lang=detectLanguage(title+' '+text);
439
- var autoEmotion=detectEmotion(title+' '+text);
440
- var h='<h3 style="color:#5cb87a;margin-bottom:12px;font-size:16px">🎬 Tạo Short AI (ngôn ngữ: '+lang.toUpperCase()+')</h3>';
441
- h+='<div style="margin-bottom:12px"><div style="color:#aaa;font-size:11px;margin-bottom:6px">🎙️ Chọn giọng đọc:</div>';
442
- VOICE_LIST.forEach(function(v){
443
- var sel=v.id===getAutoVoice(lang)?'border-color:#5cb87a;background:#1a2a1f':'border-color:#333;background:#222';
444
- h+='<button class="ve-voice-btn" data-voice="'+v.id+'" style="display:inline-block;'+sel+';border:1px solid;color:#ccc;padding:5px 10px;border-radius:12px;font-size:10px;margin:2px;cursor:pointer">'+v.label+'</button>';
445
- });
446
- h+='</div>';
447
- h+='<div style="margin-bottom:12px"><div style="color:#aaa;font-size:11px;margin-bottom:6px">😊 Chọn cảm xúc:</div>';
448
- EMOTION_LIST.forEach(function(e){
449
- var sel=e.id===autoEmotion?'border-color:#5cb87a;background:#1a2a1f':'border-color:#333;background:#222';
450
- h+='<button class="ve-emotion-btn" data-emotion="'+e.id+'" style="display:inline-block;'+sel+';border:1px solid;color:#ccc;padding:5px 10px;border-radius:12px;font-size:10px;margin:2px;cursor:pointer">'+e.label+'</button>';
451
- });
452
- h+='</div>';
453
- h+='<div style="margin-bottom:12px"><div style="color:#aaa;font-size:11px;margin-bottom:6px">⚡ Tốc độ:</div>';
454
- h+='<select id="ve-speed" style="background:#222;border:1px solid #333;color:#ccc;padding:6px 12px;border-radius:10px;font-size:11px">';
455
- h+='<option value="0.85">0.85x Chậm</option><option value="1.0">1.0x Bình thường</option><option value="1.2" selected>1.2x Nhanh</option><option value="1.35">1.35x Rất nhanh</option></select></div>';
456
- h+='<div style="display:flex;gap:8px"><button id="ve-create-btn" style="flex:1;background:#2d8659;border:0;color:#fff;padding:10px;border-radius:12px;font-size:12px;font-weight:700;cursor:pointer">🎬 Tạo Short</button>';
457
- h+='<button id="ve-cancel-btn" style="background:#333;border:0;color:#ccc;padding:10px 16px;border-radius:12px;font-size:11px;cursor:pointer">✕</button></div>';
458
- h+='<div id="ve-status" style="color:#888;font-size:10px;margin-top:8px;display:none"></div>';
459
- box.innerHTML=h;overlay.appendChild(box);document.body.appendChild(overlay);
460
- var selectedVoice=getAutoVoice(lang),selectedEmotion=autoEmotion;
461
- box.querySelectorAll('.ve-voice-btn').forEach(function(btn){btn.addEventListener('click',function(){box.querySelectorAll('.ve-voice-btn').forEach(function(b){b.style.borderColor='#333';b.style.background='#222';});this.style.borderColor='#5cb87a';this.style.background='#1a2a1f';selectedVoice=this.dataset.voice;});});
462
- box.querySelectorAll('.ve-emotion-btn').forEach(function(btn){btn.addEventListener('click',function(){box.querySelectorAll('.ve-emotion-btn').forEach(function(b){b.style.borderColor='#333';b.style.background='#222';});this.style.borderColor='#5cb87a';this.style.background='#1a2a1f';selectedEmotion=this.dataset.emotion;});});
463
- box.querySelector('#ve-cancel-btn').addEventListener('click',function(){overlay.remove();});
464
- box.querySelector('#ve-create-btn').addEventListener('click',async function(){
465
- this.disabled=true;this.textContent='⏳ Đang tạo...';
466
- box.querySelector('#ve-status').style.display='block';box.querySelector('#ve-status').textContent='Đang tạo video shorts...';
467
- try{
468
- var speed=parseFloat(box.querySelector('#ve-speed').value)||1.2;
469
- // FIX: Store selections in _ttsSelections for consistency
470
- if(!_ttsSelections["inline-"+postId]) _ttsSelections["inline-"+postId]={voice:"vi-VN-HoaiMyNeural",emotion:"neutral"};
471
- _ttsSelections["inline-"+postId].voice=selectedVoice;
472
- _ttsSelections["inline-"+postId].emotion=selectedEmotion;
473
- var r=await fetch('/api/ai/short/'+encodeURIComponent(postId),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({voice:selectedVoice,emotion:selectedEmotion,speed:speed})});
474
- var j=await r.json();
475
- if(!r.ok||j.error) throw new Error(j.error||'Lỗi tạo video');
476
- toast('✅ Đã tạo Short AI!');overlay.remove();
477
- var p=_wallPosts.find(function(x){return String(x.id)===String(postId);});
478
- if(p){p.video=j.video;p.voice=j.voice;p.emotion=j.emotion;}
479
- refreshShortAISlider();
480
- }catch(e){this.disabled=false;this.textContent='🎬 Tạo Short';box.querySelector('#ve-status').textContent='❌ '+e.message;}
481
- });
482
- };
483
- window.showSlidePreview=function(slides,title){
484
- if(!slides||!slides.length) return;
485
- var overlay=document.createElement('div');overlay.id='slide-preview';
486
- overlay.style.cssText='position:fixed;inset:0;background:#000;z-index:99999;display:flex;flex-direction:column;overflow:hidden';
487
- var currentSlide=0;
488
- function renderSlide(idx){
489
- var s=slides[idx];
490
- overlay.innerHTML='<div style="position:absolute;top:10px;left:10px;right:10px;display:flex;justify-content:space-between;align-items:center;z-index:2">'+
491
- '<button onclick="document.getElementById(\'slide-preview\').remove()" style="background:rgba(0,0,0,.6);border:0;color:#fff;padding:8px 14px;border-radius:20px;font-size:12px;cursor:pointer">✕ Đóng</button>'+
492
- '<span style="color:#fff;font-size:11px;background:rgba(0,0,0,.6);padding:4px 10px;border-radius:10px">'+(idx+1)+'/'+slides.length+'</span>'+
493
- '</div>'+
494
- '<div style="flex:1;display:flex;align-items:center;justify-content:center;padding:20px">'+
495
- (s.image?'<img src="'+esc(s.image)+'" style="max-width:100%;max-height:60vh;border-radius:10px;object-fit:contain" onerror="this.style.display=\'none\'">':'')+
496
- '</div>'+
497
- '<div style="padding:16px 20px;background:linear-gradient(transparent,rgba(0,0,0,.9));min-height:100px">'+
498
- '<p style="color:#fff;font-size:14px;line-height:1.6;text-align:center">'+esc(s.text)+'</p>'+
499
- '</div>'+
500
- '<div style="display:flex;gap:10px;padding:10px 20px 20px;justify-content:center">'+
501
- '<button onclick="window.prevSlide()" style="background:#333;border:0;color:#fff;padding:10px 20px;border-radius:20px;font-size:12px;cursor:pointer"'+(idx===0?' disabled style="opacity:.3"':'')+'>← Trước</button>'+
502
- '<button onclick="window.nextSlideRip('+idx+')" style="background:#2d8659;border:0;color:#fff;padding:10px 20px;border-radius:20px;font-size:12px;cursor:pointer"'+(idx===slides.length-1?' disabled style="opacity:.3"':'')+'>Tiếp →</button>'+
503
- '</div>';
504
- }
505
- window.nextSlideRip=function(currentIdx){if(currentIdx<slides.length-1){currentSlide=currentIdx+1;renderSlide(currentSlide);}};
506
- window.prevSlide=function(){if(currentSlide>0){currentSlide--;renderSlide(currentSlide);}};
507
- renderSlide(0);document.body.appendChild(overlay);
508
- var startX=0;
509
- overlay.addEventListener('touchstart',function(e){startX=e.touches[0].clientX;});
510
- overlay.addEventListener('touchend',function(e){var diff=e.changedTouches[0].clientX-startX;if(diff<-50)window.nextSlideRip(currentSlide);else if(diff>50)window.prevSlide();});
511
- };
512
 
513
- function prependWallPost(post){
514
- _wallPosts.unshift(post);
515
- const track=document.getElementById('ai-wall-track');
516
- const wrap=document.getElementById('ai-wall-wrap');
517
- const homeEl=document.getElementById('view-home');
518
- if(!track||!wrap){
519
- if(homeEl){
520
- let insertBefore=homeEl.querySelector('.slider-wrap');
521
- const newWrap=document.createElement('div');
522
- newWrap.className='slider-wrap';
523
- newWrap.id='ai-wall-wrap';
524
- newWrap.innerHTML=`<div class="slider-header"><span class="slider-label">🧱 Tường AI</span></div><div class="slider-track" id="ai-wall-track">${makeWallItem(post,0)}</div>`;
525
- if(insertBefore) homeEl.insertBefore(newWrap,insertBefore);
526
- else homeEl.appendChild(newWrap);
527
- const firstItem=newWrap.querySelector('.wall-item');
528
- if(firstItem)firstItem.className='wall-item wall-item-new';
529
- }
530
- return;
531
  }
532
- const div=document.createElement('div');
533
- div.className='wall-item wall-item-new';
534
- div.id='wall-item-'+(post.id||'new-'+Date.now());
535
- const hasVideo = post.video && post.video.length > 0;
536
- const thumbContent = post.img
537
- ? `<img src="${_proxyImg(esc(post.img))}" loading="lazy" onerror="this.style.display='none'">`
538
- : (hasVideo ? `<video src="${esc(post.video)}" muted></video>` : '');
539
- const videoBadge = hasVideo ? `<div class="wall-video-badge">🎬</div>` : '';
540
- const videoBtn = hasVideo
541
- ? `<button class="wall-btn-video" onclick="event.stopPropagation();openShortAIFeed(0)">▶ Xem Short</button>`
542
- : `<button class="wall-btn-make" onclick="event.stopPropagation();makeShortVideo('${esc(post.id)}',this)">🎬 Tạo Video</button>`;
543
- div.innerHTML=`<div class="wall-thumb">${thumbContent}${videoBadge}</div><div class="wall-title">${esc(post.title)}</div><div class="wall-text">${esc((post.text||'').slice(0,180))}</div><div class="wall-actions"><button class="primary" onclick="readWallPost(0)">Xem</button>${videoBtn}</div>`;
544
- track.prepend(div);
545
- track.scrollTo({left:0,behavior:'smooth'});
546
- if(hasVideo) refreshShortAISlider();
547
  }
548
 
549
- // === REST OF FUNCTIONS ===
550
- let _shortsData=[];
551
- let _wallPosts=[];
552
- let _currentView='home';
553
- let _currentEventId=null;
554
- let _currentMatchUrl=null;
555
- function interleaveShorts(shorts){const dt=shorts.filter(s=>s.channel==='baodantri7941');const sk=shorts.filter(s=>s.channel==='baosuckhoedoisongboyte');const result=[];let i=0,j=0;while(i<dt.length||j<sk.length){if(i<dt.length)result.push(dt[i++]);if(j<sk.length)result.push(sk[j++]);}return result;}
556
- let _htPage=0,_htTopic='';
557
- async function loadHotTopics(){const j=await fetch('/api/hot_topics').then(r=>r.json()).catch(()=>({topics:[]}));const el=document.getElementById('hot-topics');if(!el)return;el.innerHTML=(j.topics||[]).slice(0,18).map(t=>{const topicText=t.topic||t.label.replace(/^#/,'');return`<button class="hot-chip" onclick="searchTopic('${topicText.replace(/'/g,"\\'")}')">${esc(t.label)}</button>`;}).join('');if(j.topics&&j.topics[0]){const firstTopic=j.topics[0].topic||j.topics[0].label.replace(/^#/,'');setTimeout(()=>searchTopic(firstTopic),800);}}
558
- function searchTopic(topic){if(!topic){topic=document.getElementById('topic-input')?.value.trim();if(!topic){alert('Nhập chủ đề');return;}}document.getElementById('topic-input').value='';_htTopic=topic;_htPage=0;showHashtagSources(topic,0);}
559
- async function showHashtagSources(topic,page){const box=document.getElementById('hashtag-box');if(!box)return;if(page===0)box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div class="hashtag-loading"><div class="hashtag-spinner"></div>Đang tìm...</div></div>`;try{const r=await fetch(`/api/hashtag/sources?topic=${encodeURIComponent(topic)}&page=${page}`);const j=await r.json();const sources=j.sources||[];if(!sources.length&&page===0){box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div style="color:#888;padding:8px">Không tìm được bài viết liên quan</div></div>`;return;}let h='';if(page===0)h=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)} <span style="font-size:10px;color:#888">(${j.total} bài từ 8 nguồn)</span></h3><div id="ht-list">`;sources.forEach((s,i)=>{const idx=page*8+i;h+=`<div class="hashtag-src-item" onclick="readArticle('${esc(s.url)}')"><div class="hashtag-src-img" id="ht-img-${idx}"></div><div class="hashtag-src-text"><div class="hashtag-src-title">${esc(s.title)}</div><div class="hashtag-src-via">${esc(s.via||'')}</div></div></div>`;});if(page===0){h+=`</div><button class="hashtag-rewrite-btn" onclick="rewriteHashtag('${esc(topic).replace(/'/g,"\\'")}')">🤖 Rewrite AI tổng hợp & đăng tường</button>`;if(j.has_more)h+=`<button class="hashtag-load-more" id="ht-more" onclick="loadMoreHashtag()">Tải thêm ▼</button>`;h+=`</div>`;box.innerHTML=h;}else{document.getElementById('ht-list')?.insertAdjacentHTML('beforeend',h);const btn=document.getElementById('ht-more');if(btn){if(!j.has_more)btn.remove();else{btn.disabled=false;btn.textContent='Tải thêm ▼';}}}sources.forEach((s,i)=>{const idx=page*8+i;if(!s.url)return;fetch('/api/article?url='+encodeURIComponent(s.url)).then(r=>r.json()).then(d=>{if(d&&(d.og_image||d.img)){const el=document.getElementById('ht-img-'+idx);if(el)el.innerHTML=`<img src="${_proxyImg(esc(d.og_image||d.img))}" onerror="this.style.display='none'">`;}}).catch(()=>{});});}catch(e){box.innerHTML=`<div class="hashtag-sources"><h3>🔍 ${esc(topic)}</h3><div style="color:#e74c3c;padding:8px">Lỗi: ${esc(e.message)}</div></div>`;}}
560
- function loadMoreHashtag(){_htPage++;const btn=document.getElementById('ht-more');if(btn){btn.disabled=true;btn.textContent='Đang tải...';}showHashtagSources(_htTopic,_htPage);}
561
- async function rewriteHashtag(topic){const btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tổng hợp...';}try{const r=await fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic})});const j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');toast('✅ Đã đăng Tường AI!');if(btn)btn.textContent='✅ Đăng thành công!';if(j.post)prependWallPost(j.post);}catch(e){toast('❌ '+e.message);if(btn){btn.disabled=false;btn.textContent='🤖 Rewrite AI';}}}
562
- async function loadLivescore(tab){document.querySelectorAll('.ls-tab').forEach(t=>t.classList.remove('active'));document.querySelector(`.ls-tab[data-tab="${tab}"]`)?.classList.add('active');const el=document.getElementById('ls-content');if(!el)return;el.innerHTML='<div class="loading">Đang tải...</div>';let ep='/api/livescore/'+tab;if(tab.startsWith('bxh_'))ep='/api/livescore/standings/'+tab.replace('bxh_','');try{const r=await fetch(ep);const d=await r.json();el.innerHTML=d.html&&d.html.length>50?d.html:'<div class="loading">Không có dữ liệu</div>';bindMatchClicks(el);}catch(e){el.innerHTML='<div class="loading">Lỗi</div>';}}
563
- function bindMatchClicks(el){
564
- el.querySelectorAll('.match-detail').forEach(md=>{
565
- md.style.cursor='pointer';
566
- md.addEventListener('click',function(e){
567
- const statusA=this.querySelector('.status a');
568
- const teamA=this.querySelector('.teams a[href*="/tran-dau/"]');
569
- const a = statusA || teamA;
570
- if(a){
571
- e.preventDefault();
572
- e.stopPropagation();
573
- const href=a.getAttribute('href')||'';
574
- const m=href.match(/\/tran-dau\/(\d+)\//);
575
- if(m){
576
- const fullUrl=href.startsWith('http')?href:'https://bongda.com.vn'+href;
577
- openMatch(m[1],fullUrl);
578
- }
579
- }
580
- });
581
- });
582
- el.querySelectorAll('a').forEach(a=>{
583
- a.addEventListener('click',e=>{e.preventDefault();e.stopPropagation()});
584
- });
585
  }
586
- function openMatch(id,url){if(!id)return;_currentEventId=id;if(url)_currentMatchUrl=url;document.getElementById('match-overlay').classList.add('active');document.body.style.overflow='hidden';loadMatchTab('detail')}
587
- function closeMatch(){document.getElementById('match-overlay').classList.remove('active');document.body.style.overflow=''}
588
- async function loadMatchTab(tab){document.querySelectorAll('.mo-tab').forEach(t=>t.classList.remove('active'));document.querySelectorAll('.mo-tab').forEach(t=>{if((tab==='comm'&&t.textContent==='Diễn biến')||(tab==='stats'&&t.textContent==='Thống kê')||(tab==='detail'&&t.textContent.includes('Chi tiết')))t.classList.add('active')});const el=document.getElementById('mo-body');if(!el)return;el.innerHTML='<div class="loading">Đang tải...</div>';try{let apiUrl;if(tab==='stats')apiUrl=`/api/match/${_currentEventId}/stats`;else if(tab==='comm')apiUrl=`/api/match/${_currentEventId}/commentaries`;else{apiUrl=`/api/match/${_currentEventId}/detail`;if(_currentMatchUrl)apiUrl+='?url='+encodeURIComponent(_currentMatchUrl)}const r=await fetch(apiUrl);if(!r.ok){el.innerHTML='<div class="loading">Lỗi máy chủ ('+r.status+')</div>';return}const d=await r.json();if(d.error){el.innerHTML='<div class="loading">'+esc(d.error)+'</div>';return}if(tab==='detail'&&typeof renderMatchDetail==='function'){renderMatchDetail(el,d);return}el.innerHTML=d.html||'<div class="loading">Không có dữ liệu</div>'}catch(e){el.innerHTML='<div class="loading">Lỗi</div>'}}
589
- async function doInteract(videoId,type){try{const r=await fetch('/api/v2/interact',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:videoId,type})});return await r.json();}catch(e){return null;}}
590
- async function getInteractions(videoId){try{return await fetch('/api/v2/interactions?id='+encodeURIComponent(videoId)).then(r=>r.json());}catch(e){return{views:0,likes:0,comments:0};}}
591
- async function getComments(videoId){try{const j=await fetch('/api/v2/comments?id='+encodeURIComponent(videoId)).then(r=>r.json());return j.comments||[];}catch(e){return[];}}
592
- async function postComment(videoId,text){try{const j=await fetch('/api/v2/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:videoId,text})}).then(r=>r.json());return j.comments||[];}catch(e){return[];}}
593
- function buildTikTokSlide(opts){return`<div class="tiktok-slide" data-vid="${esc(opts.videoId)}">${opts.vtag}<div class="tiktok-bottom"><span class="badge ${opts.badgeClass||'badge-fpt'}">${opts.badge||''}</span><p class="tiktok-title">${esc(opts.title)}</p></div><div class="tiktok-right"><button class="tiktok-right-btn" onclick="event.stopPropagation();doView('${esc(opts.videoId)}',this)"><div class="icon">👁</div><div class="count" id="vc-${opts.idx}">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();doLike('${esc(opts.videoId)}',this)"><div class="icon">❤️</div><div class="count" id="lc-${opts.idx}">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();toggleComments('${esc(opts.videoId)}',${opts.idx})"><div class="icon">💬</div><div class="count" id="cc-${opts.idx}">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();doShare('${esc(opts.title)}','${esc(opts.shareUrl||'')}','')"><div class="icon">📤</div></button>${opts.extraBtn||''}</div><span class="tiktok-counter">${opts.idx+1}/${opts.total}</span><div class="inline-comments" id="cmt-inline-${opts.idx}" style="display:none"></div></div>`;}
594
- async function doView(videoId,btn){const j=await doInteract(videoId,'view');if(j){const c=btn.querySelector('.count');if(c)c.textContent=fmtNum(j.views);}}
595
- async function doLike(videoId,btn){const j=await doInteract(videoId,'like');if(j){const c=btn.querySelector('.count');if(c)c.textContent=fmtNum(j.likes);}}
596
- function fmtNum(n){if(!n)return'0';if(n>=1000000)return(n/1000000).toFixed(1)+'M';if(n>=1000)return(n/1000).toFixed(1)+'K';return String(n);}
597
- async function loadCounters(videoIds){for(let i=0;i<videoIds.length;i++){const id=videoIds[i];if(!id)continue;const j=await getInteractions(id);const vc=document.getElementById('vc-'+i);if(vc)vc.textContent=fmtNum(j.views);const lc=document.getElementById('lc-'+i);if(lc)lc.textContent=fmtNum(j.likes);const cc=document.getElementById('cc-'+i);if(cc)cc.textContent=fmtNum(j.comments);}}
598
- async function toggleComments(videoId,idx){const panel=document.getElementById('cmt-inline-'+idx);if(!panel)return;if(panel.style.display!=='none'){panel.style.display='none';return;}panel.style.display='block';panel.innerHTML='<div style="padding:8px;color:#888;font-size:11px">Đang tải...</div>';const cmts=await getComments(videoId);renderInlineComments(panel,videoId,idx,cmts);}
599
- function renderInlineComments(panel,videoId,idx,cmts){let h='<div class="inline-cmt-header"><span>💬 Bình luận</span><button onclick="document.getElementById(\'cmt-inline-'+idx+'\').style.display=\'none\'">✕</button></div><div class="inline-cmt-list">';if(cmts.length){cmts.slice(-30).forEach(c=>{h+=`<div class="inline-cmt-item"><span class="inline-cmt-time">${c.time||''}</span>${esc(c.text)}</div>`;});}else{h+='<div style="color:#777;font-size:11px;padding:4px">Chưa có bình luận</div>';}h+=`</div><div class="inline-cmt-input"><input id="cmt-input-${idx}" placeholder="Viết bình luận..." onkeydown="if(event.key==='Enter')submitInlineCmt('${esc(videoId)}',${idx})"><button onclick="submitInlineCmt('${esc(videoId)}',${idx})">Gửi</button></div>`;panel.innerHTML=h;const list=panel.querySelector('.inline-cmt-list');if(list)list.scrollTop=list.scrollHeight;}
600
- async function submitInlineCmt(videoId,idx){const inp=document.getElementById('cmt-input-'+idx);if(!inp)return;const text=inp.value.trim();if(!text)return;inp.value='';inp.disabled=true;const cmts=await postComment(videoId,text);inp.disabled=false;const panel=document.getElementById('cmt-inline-'+idx);if(panel)renderInlineComments(panel,videoId,idx,cmts);const cc=document.getElementById('cc-'+idx);if(cc)cc.textContent=fmtNum(cmts.length);}
601
- function initTikTokFeed(){const feed=document.getElementById('tiktok-feed');if(!feed)return;const slides=feed.querySelectorAll('.tiktok-slide');let cur=-1;function act(i){if(i===cur)return;slides.forEach((sl,idx)=>{const v=sl.querySelector('video');const fr=sl.querySelector('iframe');if(idx===i){if(v&&v.dataset.hls&&!v._hls&&typeof Hls!=='undefined'&&Hls.isSupported()){const hls=new Hls();hls.loadSource(v.dataset.hls);hls.attachMedia(v);hls.on(Hls.Events.MANIFEST_PARSED,()=>v.play().catch(()=>{}));v._hls=hls}else if(v)v.play().catch(()=>{});if(fr&&!fr.src&&fr.dataset.ytSrc)fr.src=fr.dataset.ytSrc;const vid=sl.dataset.vid;if(vid&&!sl._viewed){sl._viewed=true;doInteract(vid,'view').then(j=>{if(j){const vc=document.getElementById('vc-'+idx);if(vc)vc.textContent=fmtNum(j.views);}});}}else{if(v){v.pause();if(v._hls){v._hls.destroy();v._hls=null}}if(fr&&fr.src)fr.src=''}});cur=i}let sT;feed.addEventListener('scroll',()=>{clearTimeout(sT);sT=setTimeout(()=>{const rect=feed.getBoundingClientRect(),ctr=rect.top+rect.height/2;let best=-1,bestD=1e9;slides.forEach((sl,i)=>{const d=Math.abs(sl.getBoundingClientRect().top+sl.getBoundingClientRect().height/2-ctr);if(d<bestD){bestD=d;best=i}});if(best>=0)act(best)},150)});setTimeout(()=>act(0),400);slides.forEach(sl=>{const v=sl.querySelector('video');if(v)v.addEventListener('click',e=>{e.preventDefault();v.paused?v.play().catch(()=>{}):v.pause()})});const ids=[...slides].map(sl=>sl.dataset.vid||'');loadCounters(ids)}
602
- async function openHighlightFeed(league,idx,forceUrl){showView('view-tiktok');const el=document.getElementById('view-tiktok');el.innerHTML='<div class="loading">Đang tải...</div>';let articles=(_hlLeagueData||{})[league]||[];if(!articles.length){try{articles=await fetch('/api/highlights/'+league).then(r=>r.json())}catch(e){articles=[]}}if(!articles.length){el.innerHTML='<div class="loading">Không có video</div>';return}const vids=[];const results=await Promise.all(articles.map(async(a,i)=>{try{const r=await fetch('/api/video_url?url='+encodeURIComponent(a.link)+'&img='+encodeURIComponent(a.img||''));const v=await r.json();if(v&&v.src){return{_idx:i,title:a.title||v.title||'',link:a.link||'',img:a.img||v.poster||'',src:v.src,type:v.type||'',poster:v.poster||a.img||''}}return null}catch(e){}return null}));results.forEach(r=>{if(r)vids.push(r)});vids.sort((a,b)=>a._idx-b._idx);if(!vids.length){el.innerHTML='<div class="loading">Không tìm thấy video</div>';return}let ti=vids.findIndex(v=>v._idx===idx);if(ti<0)ti=0;const ordered=ti>0?[...vids.slice(ti),...vids.slice(0,ti)]:vids;let h=`<button class="back-btn" onclick="switchCat('home')">← Highlight</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">`;ordered.forEach((v,i)=>{const isYT=v.type==='youtube',isHLS=!isYT&&v.src?.includes('.m3u8'),poster=v.poster?` poster="${esc(v.poster)}"`:'';const vtag=isYT?`<iframe data-yt-src="${v.src}" allowfullscreen allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture"></iframe>`:isHLS?`<video playsinline preload="none"${poster} data-hls="${v.src}" loop controls></video>`:`<video playsinline preload="none"${poster} loop controls><source src="${v.src}" type="video/mp4"></video>`;const videoId='hl-'+league+'-'+v._idx;h+=buildTikTokSlide({vtag,title:v.title,badge:'HL',badgeClass:'badge-fpt',videoId,idx:i,total:ordered.length,shareUrl:v.link||'',extraBtn:`<button class="tiktok-right-btn" onclick="event.stopPropagation();this.closest('.tiktok-slide').classList.toggle('ratio-wide')"><div class="icon">⬜</div><div class="count">16:9</div></button>`});});h+='</div></div>';el.innerHTML=h;initTikTokFeed();}
603
- async function openYTShortsFeed(startIdx){showView('view-tiktok');const el=document.getElementById('view-tiktok');el.innerHTML='<div class="loading">Đang tải...</div>';const arts=_shortsData.length?_shortsData:await fetch('/api/shorts').then(r=>r.json()).catch(()=>[]);if(!arts.length){el.innerHTML='<div class="loading">Không có shorts</div>';return}const ordered=startIdx>0?[...arts.slice(startIdx),...arts.slice(0,startIdx)]:arts;let h=`<button class="back-btn" onclick="switchCat('home')">← Shorts</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">`;ordered.forEach((v,i)=>{const id=v.id||'';const src=`https://www.youtube.com/embed/${id}?autoplay=1&rel=0&playsinline=1`;const vtag=`<iframe data-yt-src="${src}" allowfullscreen allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture"></iframe>`;const badge=v.channel==='baosuckhoedoisongboyte'?'SKĐS':'Dân trí';const videoId='yt-'+id;h+=buildTikTokSlide({vtag,title:v.title,badge,badgeClass:'badge-fpt',videoId,idx:i,total:ordered.length,shareUrl:'https://youtube.com/watch?v='+id});});h+='</div></div>';el.innerHTML=h;initTikTokFeed();}
604
- async function openShortAIFeed(startIdx){showView('view-tiktok');const el=document.getElementById('view-tiktok');el.innerHTML='<div class="loading">Đang tải...</div>';const wall=(await fetch('/api/wall').then(r=>r.json()).catch(()=>({posts:[]}))).posts||[];const vids=wall.filter(p=>p.video);if(!vids.length){el.innerHTML='<div class="loading">Chưa có Short AI</div>';return}const ordered=startIdx>0?[...vids.slice(startIdx),...vids.slice(0,startIdx)]:vids;let h=`<button class="back-btn" onclick="switchCat('home')">← Short AI</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">`;ordered.forEach((p,i)=>{const vtag=`<video src="${p.video}" playsinline loop controls></video>`;const videoId='ai-'+(p.id||i);h+=buildTikTokSlide({vtag,title:p.title,badge:'AI',badgeClass:'badge-ai',videoId,idx:i,total:ordered.length,shareUrl:SPACE});});h+='</div></div>';el.innerHTML=h;initTikTokFeed();}
605
- async function readArticle(url){showView('view-article');const el=document.getElementById('view-article');el.innerHTML='<div class="loading">Đang tải...</div>';try{const r=await fetch('/api/article?url='+encodeURIComponent(url));const data=await r.json();if(data&&!data.error&&data.body&&data.body.length){_currentArticle={url,data};let h=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="article-view"><h1 class="article-title">${esc(data.title)}</h1>`;if(data.summary)h+=`<div class="article-summary">${esc(data.summary)}</div>`;const seen={};data.body.forEach(b=>{if(b.type==='p')h+=`<p class="article-p">${b.text}</p>`;else if(b.type==='img'&&b.src&&!seen[b.src]){seen[b.src]=1;h+=`<img class="article-img" src="${_proxyImg(esc(b.src))}" onerror="this.style.display='none'">`}else if(b.type==='heading')h+=`<h2 class="article-h2">${esc(b.text)}</h2>`});h+=`<div class="article-actions"><button class="primary" onclick="rewriteArticle()">🤖 Rewrite AI đăng tường</button><button onclick="doShare('${esc(data.title)}','${esc(url)}','${esc(data.og_image||'')}')">📤</button><button onclick="window.open('${esc(url)}','_blank')">🔗 Gốc</button></div><div class="article-ai-ask"><h3 style="font-size:14px;color:#5cb87a">🤖 Hỏi AI</h3><textarea id="ask-q" placeholder="Hỏi về bài viết..."></textarea><button onclick="askAI()">Hỏi</button><div id="ask-a" class="article-ai-answer"></div></div></div>`;el.innerHTML=h;window.scrollTo(0,0);return;}}catch(e){}el.innerHTML=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="loading"><p>Không đọc được.</p><a href="${esc(url)}" target="_blank" style="color:#5cb87a">Mở gốc →</a></div>`;}
606
- async function rewriteArticle(){const url=_currentArticle?.url;if(!url)return;toast('⏳ Đang tạo bài đăng Tường AI...');try{const r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:url,context:document.querySelector('.article-view')?document.querySelector('.article-view').innerText.slice(0,14000):''})});const j=await r.json();if(!r.ok||j.error)throw new Error(j.error);toast('✅ Đã đăng Tường AI!');if(j.post)prependWallPost(j.post);if(j.post&&typeof goToWallPost==='function')goToWallPost(j.post.id);if(j.slides&&j.slides.length){setTimeout(function(){showSlidePreview(j.slides,j.post?j.post.title:'');},500);}if(j.post&&!j.post.video){setTimeout(function(){showVoiceEmotionSelector(j.post.id,j.post.title,j.post.text);},1000);}}catch(e){toast('❌ '+e.message);}}
607
- async function rewriteUrl(){const url=document.getElementById('url-input')?.value.trim();if(!url)return alert('Dán URL');toast('⏳ Đang rewrite...');try{const r=await fetch('/api/url_wall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})});const j=await r.json();if(!r.ok||j.error)throw new Error(j.error);toast('✅ Đã đăng!');document.getElementById('url-input').value='';if(j.post)prependWallPost(j.post);}catch(e){toast('❌ '+e.message)}}
608
- async function askAI(){const q=document.getElementById('ask-q')?.value.trim();if(!q)return alert('Nhập câu hỏi');const a=document.getElementById('ask-a');a.textContent='Đang hỏi...';try{const r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:_currentArticle?.url||'',question:q,context:document.querySelector('.article-view')?.innerText?.slice(0,12000)||''})});const j=await r.json();a.textContent=j.answer||'Không trả lời được';}catch(e){a.textContent='Lỗi: '+e.message}}
609
- async function readWallPost(i){
610
- var p=_wallPosts[i];
611
- if(!p)return;
612
- showView('view-article');
613
- var images = p.images || [];
614
- var imgGallery = '';
615
- if(images.length > 0){
616
- imgGallery = '<div class="article-image-gallery">';
617
- images.forEach(function(imgUrl, imgIdx) {
618
- if(imgIdx === 0){
619
- imgGallery += '<img class="article-img article-hero-img" src="'+esc(imgUrl)+'" onerror="this.style.display=\'none\'" loading="eager">';
620
- } else {
621
- if(imgIdx === 1) imgGallery += '<div class="gallery-thumbs">';
622
- imgGallery += '<div class="gallery-thumb"><img src="'+esc(imgUrl)+'" onerror="this.parentElement.style.display=\'none\'" loading="lazy"></div>';
623
- }
624
- });
625
- if(images.length > 1) imgGallery += '</div>';
626
- imgGallery += '</div>';
627
- }
628
- var hasVideo = p.video && p.video.length > 0;
629
- var voiceEmotionHtml = '';
630
- // Always show voice selector so user can change voice and regenerate video
631
- voiceEmotionHtml = buildVoiceEmotionSelector(p);
632
- document.getElementById('view-article').innerHTML =
633
- '<button class="back-btn" onclick="switchCat(\'home\')">← Quay lại</button>' +
634
- '<div class="article-view"><span class="badge badge-ai">AI</span><h1 class="article-title">'+esc(p.title)+'</h1>' +
635
- imgGallery +
636
- '<p class="article-p" style="white-space:pre-wrap">'+esc(p.text)+'</p>' +
637
- (hasVideo ? '<video class="article-img" src="'+esc(p.video)+'" controls playsinline style="max-height:400px"></video>' : '') +
638
- '<div class="article-actions">' +
639
- (hasVideo ? '<button onclick="openShortAIFeed('+i+')">🎬 Xem Short</button>' : '') +
640
- '</div>' +
641
- voiceEmotionHtml +
642
- '</div>';
643
- window.scrollTo(0,0);
644
- // Always bind voice selector events
645
-
646
  }
647
- async function loadNewsTab(){const el=document.getElementById('view-cat');el.innerHTML='<div class="loading">Đang tải...</div>';try{const r=await fetch('/api/homepage');const news=await r.json();if(!news.length){el.innerHTML='<div class="loading">Không có tin</div>';return}const groups={};news.forEach(a=>{if(!groups[a.group])groups[a.group]=[];groups[a.group].push(a)});let h='';for(const[g,arts]of Object.entries(groups)){h+=`<div class="section-title">${g}</div><div class="grid">`;arts.slice(0,6).forEach(a=>{h+=`<div class="card" onclick="readArticle('${esc(a.link)}')"><div class="card-img">${a.img?`<img src="${_proxyImg(a.img)}" loading="lazy">`:''}</div><div class="card-body"><span class="badge badge-vne">${esc(a.source||'VnE')}</span><div class="card-title">${esc(a.title)}</div></div></div>`});h+='</div>'}el.innerHTML=h}catch(e){el.innerHTML='<div class="loading">Lỗi</div>'}}
648
- async function loadCat(id){const el=document.getElementById('view-cat');el.innerHTML='<div class="loading">Đang tải...</div>';const arts=await fetch('/api/category/'+id).then(r=>r.json()).catch(()=>[]);if(!arts.length){el.innerHTML='<div class="loading">Không có tin</div>';return}let h='<div class="grid">';arts.forEach(a=>{h+=`<div class="card" onclick="readArticle('${esc(a.link)}')"><div class="card-img">${a.img?`<img src="${_proxyImg(a.img)}" loading="lazy">`:''}</div><div class="card-body"><span class="badge badge-vne">${esc(a.source||'')}</span><div class="card-title">${esc(a.title)}</div></div></div>`});h+='</div>';el.innerHTML=h}
649
- fetch('/api/storage_status').then(r=>r.json()).then(j=>{if(!j.persistent){const home=document.getElementById('view-home');if(home){const w=document.createElement('div');w.className='storage-warn';w.innerHTML='⚠️ Persistent Storage chưa bật.';home.prepend(w)}}}).catch(()=>{});
650
 
651
- (function(){
652
- try{
653
- var hash = window.location.hash;
654
- if(hash && hash.length > 1){
655
- var articleUrl = decodeURIComponent(hash.substring(1));
656
- if(articleUrl.startsWith('http')){
657
- history.replaceState(null, '', window.location.pathname);
658
- setTimeout(function(){
659
- if(typeof readArticle==='function') readArticle(articleUrl);
660
- }, 1500);
661
- }
662
- }
663
- }catch(e){}
664
- })();
665
 
666
- (function(){
667
- try{
668
- const pa=localStorage.getItem('pending_article');
669
- const pv=localStorage.getItem('pending_video');
670
- if(pa){
671
- localStorage.removeItem('pending_article');
672
- setTimeout(()=>{
673
- if(typeof readArticle==='function') readArticle(pa);
674
- },1500);
675
- }
676
- if(pv){
677
- localStorage.removeItem('pending_video');
678
- try{
679
- const v=JSON.parse(pv);
680
- if(v&&v.url) setTimeout(()=>{window.open(v.url,'_blank')},1500);
681
- }catch(e){}
682
- }
683
- }catch(e){}
684
- })();
685
 
686
- if (document.readyState === 'loading') {
687
- document.addEventListener('DOMContentLoaded', function() {
688
- if (typeof loadHome === 'function') loadHome();
689
- });
690
- } else {
691
- if (typeof loadHome === 'function') loadHome();
692
- }
 
 
1
  function _proxyImg(url){
2
  if(!url || typeof url !== 'string') return '';
3
  if(url.startsWith('http') && !url.includes(location.host)){
 
6
  return url;
7
  }
8
 
9
+ // Static fallback for Dantri + SKĐS shorts (always available to prevent empty homepage)
10
+ var SHORTS_FALLBACK_STATIC = [
11
+ {"id":"Lu_iCQ5YwNM","title":"Công an lập hồ sơ xử lý người phụ nữ chửi bới tát nam tài xế ô tô ở Hà Nội","channel":"baodantri7941"},
12
+ {"id":"CwWvijF8BOA","title":"Chú rể Ninh Bình bật khóc nhận món quà bí mật người cha quá cố gửi 26 năm trước","channel":"baodantri7941"},
13
+ {"id":"tvPewsc2ph4","title":"Tính năng ẩn trên iPhone giúp giảm mỏi mắt","channel":"baodantri7941"},
14
+ {"id":"b1Nxzv9ixlU","title":"Y án 3 năm tù với nữ tài xế uống 8 lon bia lái xe tông chủ tịch xã tử vong","channel":"baodantri7941"},
15
+ {"id":"Xp5eTwAZAis","title":"Người đánh hàng xóm tại chung cư ở Hà Nội bị tuyên hơn 4 tháng tù","channel":"baodantri7941"},
16
+ {"id":"Htzvwg6iOBM","title":"Xe điện Audi S6 Sportback e-tron có gì đặc biệt?","channel":"baodantri7941"},
17
+ {"id":"7Pd6vZ2Lz1M","title":"Hành động ấm lòng của người đàn ông tìm kiếm 5 học sinh tử vong ở sông Lô","channel":"baosuckhoedoisongboyte"},
18
+ {"id":"SlHLt_ZyPiE","title":"Xử phạt người đàn ông xóa số điện thoại cứu hộ trên cao tốc Bắc Nam","channel":"baosuckhoedoisongboyte"},
19
+ {"id":"IUOprcJyYr4","title":"Phụ nữ táo bón có phải do lười ăn rau?","channel":"baosuckhoedoisongboyte"},
20
+ {"id":"YY8ojFNE-AU","title":"Quái xế tự quay clip nẹt pô đánh võng đăng TikTok bị xử lý","channel":"baosuckhoedoisongboyte"},
21
+ ];
22
 
 
23
  var _ttsSelections = {};
24
+ var _shortsData=[];
25
+ var _wallPosts=[];
26
+ var _currentView='home';
27
+ var _hlLeagueData={};
28
+ var _wc2026Data=null;
29
+ var _currentArticle=null;
30
+
31
+ function esc(s){
32
+ if(!s) return '';
33
+ return String(s).replace(/[&<>"']/g, function(c){
34
+ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];
35
+ });
36
+ }
37
 
 
38
  function _fetchWithTimeout(url, ms){
39
  return new Promise((resolve,reject)=>{
40
  const ctrl=new AbortController();
 
47
  });
48
  }
49
 
50
+ function interleaveShorts(shorts){
51
+ const dt=shorts.filter(s=>s.channel==='baodantri7941');
52
+ const sk=shorts.filter(s=>s.channel==='baosuckhoedoisongboyte');
53
+ const result=[];
54
+ let i=0,j=0;
55
+ while(i<dt.length||j<sk.length){
56
+ if(i<dt.length)result.push(dt[i++]);
57
+ if(j<sk.length)result.push(sk[j++]);
58
+ }
59
+ return result;
60
+ }
61
+
62
  async function loadHome(){
63
  const homeEl = document.getElementById('view-home');
64
  if(!homeEl) return;
65
 
 
66
  homeEl.innerHTML =
67
  '<div id="home-featured-area"></div>'
68
  +'<div class="ai-compose"><div class="ai-compose-title">🤖 AI viết bài</div><div class="ai-compose-row"><input id="topic-input" placeholder="Nhập chủ đề..."><button onclick="searchTopic()">Tìm nguồn</button></div><div class="ai-compose-row"><input id="url-input" placeholder="Dán URL bài viết..."><button class="secondary" onclick="rewriteUrl()">Rewrite</button></div><div id="hot-topics" class="hot-topic-row"></div></div>'
69
  +'<div id="hashtag-box"></div>'
70
  +'<div class="ls-section"><div class="ls-header"><h3>⚽ Livescore</h3></div><div class="ls-tabs"><span class="ls-tab active" data-tab="today" onclick="loadLivescore(\'today\')">📅 Hôm nay</span><span class="ls-tab" data-tab="live" onclick="loadLivescore(\'live\')">🔴 Live</span><span class="ls-tab" data-tab="incoming" onclick="loadLivescore(\'incoming\')">⏰ Sắp tới</span><span class="ls-tab" data-tab="results" onclick="loadLivescore(\'results\')">✅ Kết quả</span><span class="ls-tab" data-tab="bxh_nha" onclick="loadLivescore(\'bxh_nha\')">🏆 NHA</span><span class="ls-tab" data-tab="bxh_laliga" onclick="loadLivescore(\'bxh_laliga\')">🏆 La Liga</span></div><div class="ls-content" id="ls-content"><div class="loading">Đang tải...</div></div></div>'
71
+ +'<div id="wc2026-live-section" class="wc2026-section"><div class="wc-header"><h2>🏆 World Cup 2026</h2><span class="wc-live-badge">● LIVE</span></div><div class="wc-tabs"><span class="wc-tab active" onclick="switchWCTab(\'news\')">📰 Tin tức</span><span class="wc-tab" onclick="switchWCTab(\'fixtures\')">📅 Lịch thi đấu</span><span class="wc-tab" onclick="switchWCTab(\'standings\')">🏆 BXH</span><span class="wc-tab" onclick="switchWCTab(\'highlights\')">🎬 Highlight</span><span class="wc-tab" onclick="switchWCTab(\'stats\')">📊 Thống kê</span></div><div class="wc-content" id="wc-content"><div class="loading">Đang tải...</div></div></div>'
72
  +'<div id="home-after-wc"></div>';
73
 
74
  const afterEl = homeEl.querySelector('#home-after-wc');
 
 
75
  loadLivescore('today');
76
  loadHotTopics();
77
 
 
78
  const [featuredData, shortsData, wallData, hlLeagues, aiData, wcData] = await Promise.allSettled([
79
  _fetchWithTimeout('/api/livescore/featured', 5000),
80
+ _fetchWithTimeout('/api/shorts', 6000),
81
  _fetchWithTimeout('/api/wall', 5000),
82
  _fetchWithTimeout('/api/highlights/leagues', 10000),
83
  _fetchWithTimeout('/api/genk_ai', 8000),
84
  _fetchWithTimeout('/api/wc2026', 8000),
85
  ]).then(results => results.map(r => r.status === 'fulfilled' ? r.value : null));
86
 
 
87
  if(featuredData && featuredData.home){
88
  const sc=featuredData.status==='live'?'':'upcoming';
89
+ const st=featuredData.status==='live'? '🔴 '+featuredData.minute : ''+featuredData.time;
90
  const area=document.getElementById('home-featured-area');
91
+ if(area) area.innerHTML='<div class="featured-match" onclick="openMatch(\''+featuredData.event_id+'\')"><div class="fm-league">'+featuredData.league+'</div><div class="fm-teams"><div class="fm-team"><img src="'+_proxyImg(featuredData.home_logo)+'" onerror="this.style.display=\'none\'"><span>'+featuredData.home+'</span></div><div class="fm-score">'+(featuredData.score||'VS')+'</div><div class="fm-team"><img src="'+_proxyImg(featuredData.away_logo)+'" onerror="this.style.display=\'none\'"><span>'+featuredData.away+'</span></div></div><div class="fm-status '+sc+'">'+st+'</div></div>';
92
  }
93
 
 
94
  _shortsData = shortsData || [];
95
  _wallPosts = (wallData && wallData.posts) || [];
96
  _hlLeagueData = hlLeagues || {};
97
  _wc2026Data = wcData;
98
 
 
99
  if(wcData) switchWCTab('news');
100
 
 
101
  _renderShortsIn(afterEl);
102
  _renderWallIn(afterEl);
103
  _renderHLIn(afterEl);
104
  if(aiData && aiData.length) _renderSlidesIn('ai-articles','Ứng dụng AI','🤖',aiData,afterEl);
105
  }
106
 
107
+ function _renderShortsIn(afterEl){
108
+ if(!afterEl) return;
109
+ var data = _shortsData && _shortsData.length ? _shortsData : SHORTS_FALLBACK_STATIC;
110
+ var mixed=interleaveShorts(data);
111
+ if(!mixed.length) return;
112
+ var wrap=document.createElement('div');
113
+ wrap.className='slider-wrap';
114
+ var h='<div class="slider-header"><span class="slider-label">📱 Shorts Dân trí & SKĐS</span><span class="slider-note">Mới nhất · xen kẽ</span></div><div class="slider-track">';
115
+ mixed.slice(0,30).forEach((a,i)=>{
116
+ var badge=a.channel==='baosuckhoedoisongboyte'?'SKĐS':'Dân trí';
117
+ h+='<div class="slider-item shorts-item" onclick="openYTShortsFeed('+i+')"><div class="slider-thumb shorts-thumb">'+(a.img?'<img src="'+_proxyImg(a.img)+'" loading="lazy">':'')+'<div class="card-play">▶</div></div><div class="slider-title"><span style="color:#f0c040;font-size:8px">'+badge+'</span> '+esc(a.title)+'</div></div>';
118
+ });
119
+ h+='</div>';wrap.innerHTML=h;
120
+ afterEl.parentNode.insertBefore(wrap,afterEl);
121
+ }
122
+
123
  function _renderSlidesIn(key, label, emoji, vids, afterEl){
124
  if(!vids||!vids.length||!afterEl) return;
125
+ var wrap=document.createElement('div');
126
  wrap.className='slider-wrap';
127
+ var h='<div class="slider-header"><span class="slider-label">'+emoji+' '+label+'</span></div><div class="slider-track">';
128
  const isHL = key==='world-cup'||key==='premier-league'||key==='champions-league'||key==='la-liga'||key==='serie-a'||key==='bundesliga'||key==='friendly';
129
  vids.slice(0,isHL?8:12).forEach((a,i)=>{
130
  if(isHL){
131
+ h+='<div class="slider-item" onclick="openHighlightFeed(\''+key+'\','+i+')"><div class="slider-thumb">'+(a.img?'<img src="'+_proxyImg(a.img)+'" loading="lazy">':'')+'<div class="card-play">▶</div></div><div class="slider-title">'+esc(a.title)+'</div></div>';
132
  } else {
133
+ h+='<div class="slider-item" onclick="readArticle(\''+esc(a.link)+'\')"><div class="slider-thumb">'+(a.img?'<img src="'+_proxyImg(a.img)+'" loading="lazy">':'')+'</div><div class="slider-title">'+esc(a.title)+'</div></div>';
134
  }
135
  });
136
+ h+='</div>';wrap.innerHTML=h;afterEl.parentNode.insertBefore(wrap,afterEl);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  }
138
 
139
  function _renderWallIn(afterEl){
140
  if(!_wallPosts||!_wallPosts.length||!afterEl) return;
141
+ var posts=_wallPosts;
142
+ var aiShorts=posts.filter(p=>p.video);
143
  if(aiShorts.length){
144
+ var wrap=document.createElement('div');wrap.className='slider-wrap';wrap.id='short-ai-section';
145
+ var h='<div class="slider-header"><span class="slider-label">🎬 Short AI</span></div><div class="slider-track">';
146
+ aiShorts.slice(0,20).forEach((p,i)=>{h+='<div class="slider-item shorts-item" onclick="openShortAIFeed('+i+')"><div class="slider-thumb shorts-thumb"><video src="'+p.video+'" muted preload="metadata"></video><div class="card-play">▶</div></div><div class="slider-title">'+esc(p.title)+'</div></div>';});
147
+ h+='</div>';wrap.innerHTML=h;afterEl.parentNode.insertBefore(wrap,afterEl);
 
 
148
  }
149
+ var wrap=document.createElement('div');wrap.className='slider-wrap';wrap.id='ai-wall-wrap';
150
+ var h='<div class="slider-header"><span class="slider-label">🧱 Tường AI</span></div><div class="slider-track" id="ai-wall-track">';
 
151
  posts.slice(0,20).forEach((p,i)=>{h+=makeWallItem(p,i);});
152
+ h+='</div>';wrap.innerHTML=h;afterEl.parentNode.insertBefore(wrap,afterEl);
 
153
  }
154
 
155
  function _renderHLIn(afterEl){
156
  if(!_hlLeagueData||Object.keys(_hlLeagueData).length===0||!afterEl) return;
157
+ var HL_CONFIG={"world-cup":{name:"World Cup 2026",emoji:"🌍"},"premier-league":{name:"Premier League",emoji:"����"},"champions-league":{name:"Champions League",emoji:"⭐"},"la-liga":{name:"La Liga",emoji:"🇪🇸"},"serie-a":{name:"Serie A",emoji:"🇮🇹"},"bundesliga":{name:"Bundesliga",emoji:"🇩🇪"},"friendly":{name:"Giao hữu",emoji:"🤝"}};
158
+ for(var[key,cfg] of Object.entries(HL_CONFIG)){
159
+ var vids=_hlLeagueData[key];
160
  if(!vids||!vids.length) continue;
161
  _renderSlidesIn(key,cfg.name,cfg.emoji,vids,afterEl);
162
  }
163
  }
164
 
 
165
  function makeWallItem(p,i){
166
  var hasVideo = p.video && p.video.length > 0;
167
+ var thumbContent = p.img ? '<img src="/api/proxy/img?url='+encodeURIComponent(p.img)+'" loading="lazy" onerror="this.style.display=\'none\'">' : (hasVideo ? '<video src="'+esc(p.video)+'" muted></video>' : '');
 
 
168
  var videoBadge = hasVideo ? '<div class="wall-video-badge">🎬</div>' : '';
169
  var vid = p.id||i;
 
 
 
170
  var selKey = 'inline-' + vid;
171
+ if(!_ttsSelections[selKey]) _ttsSelections[selKey] = {voice: 'vi-VN-HoaiMyNeural', emotion: 'neutral'};
172
+ var voiceOpts='', emotOpts='';
173
+ ['vi-VN-HoaiMyNeural','vi-VN-NamMinhNeural'].forEach(function(v){
174
+ voiceOpts += '<option value="'+v+'"'+(v===_ttsSelections[selKey].voice?' selected':'')+'</option>';
175
  });
176
+ ['neutral','happy','excited','sad','humorous','serious','urgent','warm'].forEach(function(e){
177
+ emotOpts += '<option value="'+e+'"'+(e===_ttsSelections[selKey].emotion?' selected':'')+'</option>';
 
178
  });
179
  var spd = p.short_speed || '1.2';
180
+ var voiceBar = '<div class="wall-tts-bar" style="margin:6px 0 4px"><select style="background:#1a1a1a" onchange="_ttsSelections[\''+selKey+'\'].voice=this.value">'+voiceOpts+'</select><select style="background:#1a1a1a" onchange="_ttsSelections[\''+selKey+'\'].emotion=this.value">'+emotOpts+'</select><select style="background:#1a1a1a" onchange="_ttsSelections[\''+selKey+'\'].speed=parseFloat(this.value)"><option value="0.85"'+(spd==='0.85'?' selected':'')+'>0.85x</option><option value="1.0"'+(spd==='1.0'?' selected':'')+'>1.0x</option><option value="1.2"'+(spd==='1.2'?' selected':'')+'>1.2x</option><option value="1.35"'+(spd==='1.35'?' selected':'')+'>1.35x</option></select></div>';
181
+ var makeBtn = hasVideo ? '<button class="wall-btn-video" onclick="event.stopPropagation();openShortAIFeed('+i+')">▶ Xem Short</button>' : '<button class="wall-btn-make" onclick="event.stopPropagation();makeShortVideo(\''+esc(vid)+'\',this)">🎬 Tạo Short</button>';
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  return '<div class="wall-item" id="wall-item-'+esc(vid)+'"><div class="wall-thumb">'+thumbContent+videoBadge+'</div><div class="wall-title">'+esc(p.title)+'</div><div class="wall-text">'+esc((p.text||'').slice(0,180))+'</div>'+voiceBar+'<div class="wall-actions"><button class="primary" onclick="readWallPost('+i+')">Xem</button>'+makeBtn+'</div></div>';
183
  }
184
 
185
+ async function makeShortVideo(postId, btn){
186
  if(!postId)return;
 
187
  if(btn){btn.disabled=true;btn.textContent='⏳ Đang tạo...';}
188
  toast('⏳ Đang tạo video shorts...');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  try{
190
+ var selKey='inline-'+postId;
191
+ var voice=_ttsSelections[selKey]?_ttsSelections[selKey].voice:'vi-VN-HoaiMyNeural';
192
+ var emotion=_ttsSelections[selKey]?_ttsSelections[selKey].emotion:'neutral';
193
+ var speed=1.2;
194
+ var speedInp=document.querySelector('.wss[data-selkey="'+selKey+'"]');
195
+ if(speedInp) speed=parseFloat(speedInp.value)||1.2;
196
+ var r=await fetch('/api/ai/short/'+encodeURIComponent(postId),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({voice,voice,emotion,speed})});
197
+ var j=await r.json();
198
+ if(!r.ok||j.error) throw new Error(j.error||'Lỗi tạo video');
199
  toast('✅ Đã tạo video shorts!');
200
+ var p=_wallPosts.find(function(x){return String(x.id)===String(postId);});
201
+ if(p){p.video=j.video;p.voice=j.voice;p.emotion=j.emotion;var el=document.getElementById('wall-item-'+postId);if(el)el.outerHTML=makeWallItem(p,_wallPosts.indexOf(p));}
 
 
 
 
 
 
 
 
 
 
 
 
202
  refreshShortAISlider();
203
+ }catch(e){toast('❌ '+e.message);if(btn){btn.disabled=false;btn.textContent='🎬 Tạo Short';}}
 
 
 
204
  }
205
 
206
  function refreshShortAISlider(){
207
+ var aiShorts=_wallPosts.filter(p=>p.video);
208
+ var sec=document.getElementById('short-ai-section');
209
+ if(aiShorts.length===0){if(sec)sec.remove();return;}
210
+ if(!sec){
211
+ var homeEl=document.getElementById('view-home');if(!homeEl)return;
212
+ sec=document.createElement('div');sec.className='slider-wrap';sec.id='short-ai-section';
213
+ sec.innerHTML='<div class="slider-header"><span class="slider-label">🎬 Short AI</span></div><div class="slider-track"></div>';
214
+ var ins=homeEl.querySelector('.slider-wrap');if(ins)homeEl.insertBefore(sec,ins);else homeEl.appendChild(sec);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  }
216
+ var track=sec.querySelector('.slider-track');
217
+ if(track){var h='';aiShorts.slice(0,20).forEach((p,i)=>{h+='<div class="slider-item shorts-item" onclick="openShortAIFeed('+i+')"><div class="slider-thumb shorts-thumb"><video src="'+esc(p.video)+'" muted preload="metadata"></video><div class="card-play">▶</div></div><div class="slider-title">'+esc(p.title)+'</div></div>';});track.innerHTML=h;}
218
  }
219
 
220
+ function showView(viewId){
221
+ document.querySelectorAll('[id^="view-"]').forEach(el=>el.style.display='none');
222
+ var el=document.getElementById(viewId);if(el)el.style.display='block';_currentView=viewId;
223
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
+ function switchWCTab(tab){
226
+ if(!_wc2026Data)return;
227
+ var el=document.getElementById('wc-content');if(!el)return;
228
+ if(tab==='news'){
229
+ el.innerHTML='<div class="section-title">World Cup 2026 News</div><div class="grid">'+(Array.isArray(_wc2026Data.news)?_wc2026Data.news.map(a=>'<div class="card" onclick="readArticle(\''+esc(a.link)+'\')"><div class="card-img">'+(a.img?'<img src="'+_proxyImg(a.img)+'" loading="lazy">':'')+'</div><div class="card-body"><div class="card-title">'+esc(a.title)+'</div></div></div>').join(''):'')+'</div>';
230
+ }else if(tab==='highlights'){
231
+ el.innerHTML='<div class="section-title">World Cup Highlights</div><div class="grid">'+(Object.entries(_wc2026Data).filter(([k])=>k!=='news'&&k!=='standings'&&k!=='stats').map(([k,vids])=>vids? vids.slice(0,6).map(v=>'<div class="card" onclick="openHighlightFeed(\''+k+'\',0)"><div class="card-img">'+(v.img?'<img src="'+_proxyImg(v.img)+'" loading="lazy">':'')+'<div class="card-play">▶</div></div><div class="card-body"><div class="card-title">'+esc(v.title)+'</div></div></div>').join(''):'').join('')+'</div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  }
233
+ document.querySelectorAll('.wc-tab').forEach(t=>t.classList.remove('active'));
234
+ var act=document.querySelector('.wc-tab[onclick*="'+tab+'"]');if(act)act.classList.add('active');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  }
236
+
237
+ async function openYTShortsFeed(startIdx){
238
+ showView('view-tiktok');
239
+ var el=document.getElementById('view-tiktok');el.innerHTML='<div class="loading">Đang tải...</div>';
240
+ var arts=_shortsData.length?_shortsData:SHORTS_FALLBACK_STATIC;
241
+ if(!arts||!arts.length){el.innerHTML='<div class="loading">Không shorts</div>';return;}
242
+ var ordered=startIdx>0?[...arts.slice(startIdx),...arts.slice(0,startIdx)]:arts;
243
+ var h='<button class="back-btn" onclick="switchCat(\'home\')">← Shorts</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';
244
+ ordered.forEach((v,i)=>{
245
+ var badge=v.channel==='baosuckhoedoisongboyte'?'SKĐS':'Dân trí';
246
+ h+='<div class="tiktok-slide" data-vid="yt-'+v.id+'"><iframe data-yt-src="https://www.youtube.com/embed/'+v.id+'?autoplay=1&rel=0" allowfullscreen allow="accelerometer;autoplay"></iframe><div class="tiktok-bottom"><span class="badge badge-fpt">'+badge+'</span><p class="tiktok-title">'+esc(v.title)+'</p></div><span class="tiktok-counter">'+(i+1)+'/'+ordered.length+'</span></div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  });
248
+ h+='</div></div>';
249
+ el.innerHTML=h;initTikTokFeed();
 
 
 
 
 
 
250
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
+ function initTikTokFeed(){
253
+ var feed=document.getElementById('tiktok-feed');if(!feed)return;
254
+ var slides=feed.querySelectorAll('.tiktok-slide');
255
+ var cur=-1;
256
+ function act(i){
257
+ if(i===cur)return;
258
+ slides.forEach(function(sl,idx){
259
+ var fr=sl.querySelector('iframe');
260
+ if(idx===i&&fr&&!fr.src&&fr.dataset.ytSrc){fr.src=fr.dataset.ytSrc;}
261
+ else if(fr&&fr.src){fr.src='';}
262
+ });
263
+ cur=i;
 
 
 
 
 
 
264
  }
265
+ setTimeout(function(){act(0);},400);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  }
267
 
268
+ function switchCat(cat){showView('view-'+cat);if(cat==='home')loadHome();}
269
+
270
+ function toast(msg){
271
+ var t=document.createElement('div');t.textContent=msg;t.style.cssText='position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#333;color:#fff;padding:10px 20px;border-radius:8px;z-index:9999';document.body.appendChild(t);setTimeout(function(){t.remove();},3000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  }
273
+
274
+ function loadLivescore(tab){
275
+ document.querySelectorAll('.ls-tab').forEach(t=>t.classList.remove('active'));
276
+ document.querySelector('.ls-tab[data-tab="'+tab+'"]').classList.add('active');
277
+ var el=document.getElementById('ls-content');if(!el)return;
278
+ el.innerHTML='<div class="loading">Đang tải...</div>';
279
+ fetch('/api/livescore/'+tab).then(r=>r.json()).then(d=>{el.innerHTML=d.html&&d.html.length>50?d.html:'<div class="loading">Không có dữ liệu</div>';}).catch(()=>el.innerHTML='<div class="loading">Lỗi</div>');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  }
 
 
 
281
 
282
+ function loadHotTopics(){
283
+ fetch('/api/hot_topics').then(r=>r.json()).then(j=>{
284
+ var el=document.getElementById('hot-topics');if(!el)return;
285
+ el.innerHTML=(j.topics||[]).slice(0,18).map(t=>'<button class="hot-chip" onclick="searchTopic(\''+(t.topic||t.label.replace(/^#/,'')).replace(/'/g,"\\'")+'\')">'+esc(t.label)+'</button>').join('');
286
+ }).catch(()=>{});
287
+ }
 
 
 
 
 
 
 
 
288
 
289
+ document.addEventListener('click',function(e){
290
+ var cbtn=e.target.closest('.wall-btn-make');
291
+ if(cbtn){
292
+ var postId=cbtn.closest('.wall-item').id.replace('wall-item-','');
293
+ makeShortVideo(postId,cbtn);
294
+ return;
295
+ }
296
+ });
 
 
 
 
 
 
 
 
 
 
 
297
 
298
+ // Auto-load when ready
299
+ if(document.readyState==='loading'){
300
+ document.addEventListener('DOMContentLoaded',function(){loadHome();});
301
+ }else{loadHome();}