bep40 commited on
Commit
8da37d7
·
verified ·
1 Parent(s): 2384d88

Render topic news items as cards; share opens article/post with OG preview; add Playlist button to all shorts feeds

Browse files
Files changed (1) hide show
  1. static/app_v2.js +114 -7
static/app_v2.js CHANGED
@@ -1,6 +1,40 @@
1
  // === VNEWS Frontend v2 - Full Functions ===
2
  // Updated: Rewrite fixes — better gallery, voice selector always shown, no-reload wall update
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  // === LOAD HOME ===
5
  async function loadHome(){
6
  const[featured,sh,wall,hlLeagues,ai,wcData]=await Promise.all([
@@ -32,6 +66,42 @@ async function loadHome(){
32
  document.getElementById('view-home').innerHTML=h;
33
  loadLivescore('today');loadHotTopics();
34
  if(_wc2026Data)switchWCTab('news');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
  // === WALL POST HELPERS ===
@@ -235,7 +305,30 @@ async function doInteract(videoId,type){try{const r=await fetch('/api/v2/interac
235
  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};}}
236
  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[];}}
237
  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[];}}
238
- 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>`;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  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);}}
240
  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);}}
241
  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);}
@@ -255,10 +348,10 @@ async function openHighlightFeed(league,idx,directLink){showView('view-tiktok');
255
  const results=await Promise.all(orderedArticles.map(async(a,i)=>{try{const r=await fetch('/api/video_url?url='+encodeURIComponent(a.link));const v=await r.json();if(v&&v.src)return{...a,...v,_ord:i}}catch(e){}return null}));
256
  const vids=results.filter(Boolean).sort((a,b)=>a._ord-b._ord);
257
  if(!vids.length){el.innerHTML='<div class="loading">Không tìm thấy video</div>';return}
258
- const ordered=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="${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.id||v._ord);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();}
259
- 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();}
260
- 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();}
261
- 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="${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>`;}
262
  async function rewriteArticle(){const url=_currentArticle?.url;if(!url)return;toast('⏳ Đang rewrite...');try{const r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,context: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);goToWallPost(j.post.id);}}catch(e){toast('❌ '+e.message)}}
263
  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);goToWallPost(j.post.id);}}catch(e){toast('❌ '+e.message)}}
264
  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}}
@@ -300,6 +393,19 @@ async function readWallPost(i){
300
 
301
  const hasVideo = p.video && p.video.length > 0;
302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  // Fetch voices dynamically from backend (filtered: VN + gTTS + Multilingual)
304
  const vd = await _getVoices();
305
  const voicesObj = vd.voices || {};
@@ -352,12 +458,13 @@ async function readWallPost(i){
352
  <span class="badge badge-ai">AI</span>
353
  <h1 class="article-title">${esc(p.title)}</h1>
354
  ${imgGallery}
355
- <p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>
 
356
  ${hasVideo?`<video class="article-img" src="${esc(p.video)}" controls playsinline style="max-height:400px"></video>`:''}
357
  ${voiceSelector}
358
  <div class="article-actions">
359
  ${actionButtons}
360
- <button onclick="doShare('${esc(p.title)}','${SPACE}','${esc(p.img||'')}')">📤</button>
361
  </div>
362
  </div>`;
363
 
 
1
  // === VNEWS Frontend v2 - Full Functions ===
2
  // Updated: Rewrite fixes — better gallery, voice selector always shown, no-reload wall update
3
 
4
+ // Inject extra styles (topic news items + playlist) once.
5
+ (function(){
6
+ if(document.getElementById('vnews-extra-css'))return;
7
+ const s=document.createElement('style');s.id='vnews-extra-css';
8
+ s.textContent=`
9
+ .topic-items{display:flex;flex-direction:column;gap:10px;margin:12px 0}
10
+ .topic-item{display:flex;gap:10px;background:#181818;border:1px solid #2a2a2a;border-radius:10px;padding:10px;position:relative}
11
+ .topic-item:active{opacity:.85}
12
+ .topic-item-no{position:absolute;top:-8px;left:-8px;width:22px;height:22px;border-radius:50%;background:#2d8659;color:#fff;font-size:11px;font-weight:800;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.4)}
13
+ .topic-item-img{flex:0 0 110px;aspect-ratio:16/9;border-radius:8px;overflow:hidden;background:#222}
14
+ .topic-item-img img{width:100%;height:100%;object-fit:cover}
15
+ .topic-item-body{flex:1;min-width:0}
16
+ .topic-item-title{font-size:13px;font-weight:700;color:#eee;line-height:1.35;margin-bottom:4px}
17
+ .topic-item-desc{font-size:11.5px;color:#aaa;line-height:1.45}
18
+ .playlist-btn{position:absolute;top:8px;right:8px;z-index:6;background:rgba(0,0,0,.6);border:1px solid rgba(255,255,255,.25);color:#fff;border-radius:18px;padding:6px 12px;font-size:12px;font-weight:700;cursor:pointer;display:flex;align-items:center;gap:5px}
19
+ .playlist-overlay{position:fixed;inset:0;background:rgba(0,0,0,.82);z-index:10000;display:none;flex-direction:column}
20
+ .playlist-overlay.active{display:flex}
21
+ .playlist-head{display:flex;justify-content:space-between;align-items:center;padding:14px 14px 8px;color:#fff}
22
+ .playlist-head h3{font-size:15px;font-weight:800}
23
+ .playlist-head button{background:none;border:0;color:#fff;font-size:24px;cursor:pointer;line-height:1}
24
+ .playlist-list{flex:1;overflow-y:auto;padding:6px 10px 24px}
25
+ .playlist-row{display:flex;gap:10px;padding:8px;border-radius:10px;cursor:pointer;align-items:center}
26
+ .playlist-row:active{background:#222}
27
+ .playlist-row.current{background:#1d3a2a;border:1px solid #2d8659}
28
+ .playlist-thumb{flex:0 0 90px;aspect-ratio:9/16;max-height:80px;border-radius:8px;overflow:hidden;background:#222;position:relative}
29
+ .playlist-thumb img{width:100%;height:100%;object-fit:cover}
30
+ .playlist-thumb .pl-play{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#fff;font-size:16px;background:rgba(0,0,0,.25)}
31
+ .playlist-meta{flex:1;min-width:0}
32
+ .playlist-no{font-size:10px;color:#5cb87a;font-weight:700}
33
+ .playlist-title{font-size:12px;color:#eee;line-height:1.35;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
34
+ `;
35
+ document.head.appendChild(s);
36
+ })();
37
+
38
  // === LOAD HOME ===
39
  async function loadHome(){
40
  const[featured,sh,wall,hlLeagues,ai,wcData]=await Promise.all([
 
66
  document.getElementById('view-home').innerHTML=h;
67
  loadLivescore('today');loadHotTopics();
68
  if(_wc2026Data)switchWCTab('news');
69
+ handlePending();
70
+ }
71
+
72
+ // === SHARE HELPERS (override index.html doShare to add description + image) ===
73
+ // Share an external article URL (with its og image + description).
74
+ function doShare(title,url,img,desc){
75
+ const q='url='+encodeURIComponent(url||'')+'&title='+encodeURIComponent(title||'')
76
+ +'&img='+encodeURIComponent(img||'')+'&desc='+encodeURIComponent(desc||'');
77
+ const shareUrl=location.origin+'/s?'+q;
78
+ if(navigator.share)navigator.share({title:title||'VNEWS',url:shareUrl}).catch(()=>{});
79
+ else navigator.clipboard.writeText(shareUrl).then(()=>alert('Đã sao chép link chia sẻ!')).catch(()=>alert(shareUrl));
80
+ }
81
+ // Share a saved AI/wall post by id (opens that post in-app + correct preview image).
82
+ function sharePost(p){
83
+ const desc=(p.items&&p.items.length?p.items.map(it=>it.title).join(' · '):(p.text||'')).slice(0,180);
84
+ const q='post='+encodeURIComponent(p.id||'')+'&title='+encodeURIComponent(p.title||'')
85
+ +'&img='+encodeURIComponent(p.img||'')+'&desc='+encodeURIComponent(desc);
86
+ const shareUrl=location.origin+'/s?'+q;
87
+ if(navigator.share)navigator.share({title:p.title||'VNEWS',url:shareUrl}).catch(()=>{});
88
+ else navigator.clipboard.writeText(shareUrl).then(()=>alert('Đã sao chép link chia sẻ!')).catch(()=>alert(shareUrl));
89
+ }
90
+
91
+ // === OPEN SHARED CONTENT AFTER ARRIVING FROM A /s OR /v SHARE LINK ===
92
+ function handlePending(){
93
+ try{
94
+ const art=localStorage.getItem('pending_article');
95
+ if(art){localStorage.removeItem('pending_article');setTimeout(()=>readArticle(art),300);return;}
96
+ const pid=localStorage.getItem('pending_post');
97
+ if(pid){localStorage.removeItem('pending_post');
98
+ const idx=_wallPosts.findIndex(p=>String(p.id)===String(pid));
99
+ if(idx>=0)setTimeout(()=>readWallPost(idx),300);return;}
100
+ const vraw=localStorage.getItem('pending_video');
101
+ if(vraw){localStorage.removeItem('pending_video');
102
+ let v=null;try{v=JSON.parse(vraw);}catch(e){}
103
+ if(v&&v.url){setTimeout(()=>{showView('view-tiktok');readArticle(v.url);},300);}return;}
104
+ }catch(e){}
105
  }
106
 
107
  // === WALL POST HELPERS ===
 
305
  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};}}
306
  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[];}}
307
  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[];}}
308
+ 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><button class="tiktok-right-btn" onclick="event.stopPropagation();openPlaylist()"><div class="icon">📋</div><div class="count">List</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>`;}
309
+ // === PLAYLIST: pick which short to watch next ===
310
+ let _playlist=[]; // [{title,img}] for the CURRENT tiktok feed (ordered)
311
+ function setPlaylist(items){_playlist=(items||[]).map(it=>({title:it.title||'',img:it.img||it.poster||''}));}
312
+ function openPlaylist(){
313
+ let ov=document.getElementById('playlist-overlay');
314
+ if(!ov){ov=document.createElement('div');ov.id='playlist-overlay';ov.className='playlist-overlay';document.body.appendChild(ov);}
315
+ // which slide is currently centered?
316
+ const feed=document.getElementById('tiktok-feed');let cur=0;
317
+ if(feed){const slides=feed.querySelectorAll('.tiktok-slide');const ctr=feed.getBoundingClientRect().top+feed.getBoundingClientRect().height/2;let bd=1e9;slides.forEach((sl,i)=>{const d=Math.abs(sl.getBoundingClientRect().top+sl.getBoundingClientRect().height/2-ctr);if(d<bd){bd=d;cur=i;}});}
318
+ let h=`<div class="playlist-head"><h3>📋 Danh sách (${_playlist.length})</h3><button onclick="closePlaylist()">✕</button></div><div class="playlist-list">`;
319
+ _playlist.forEach((it,i)=>{
320
+ const thumb=it.img?`<img src="${esc(it.img)}" onerror="this.style.display='none'">`:'';
321
+ h+=`<div class="playlist-row ${i===cur?'current':''}" onclick="jumpToSlide(${i})"><div class="playlist-thumb">${thumb}<div class="pl-play">▶</div></div><div class="playlist-meta"><div class="playlist-no">${i+1}/${_playlist.length}${i===cur?' • Đang xem':''}</div><div class="playlist-title">${esc(it.title)}</div></div></div>`;
322
+ });
323
+ h+=`</div>`;ov.innerHTML=h;ov.classList.add('active');
324
+ }
325
+ function closePlaylist(){const ov=document.getElementById('playlist-overlay');if(ov)ov.classList.remove('active');}
326
+ function jumpToSlide(i){
327
+ closePlaylist();
328
+ const feed=document.getElementById('tiktok-feed');if(!feed)return;
329
+ const slides=feed.querySelectorAll('.tiktok-slide');
330
+ if(slides[i])slides[i].scrollIntoView({behavior:'smooth',block:'start'});
331
+ }
332
  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);}}
333
  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);}}
334
  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);}
 
348
  const results=await Promise.all(orderedArticles.map(async(a,i)=>{try{const r=await fetch('/api/video_url?url='+encodeURIComponent(a.link));const v=await r.json();if(v&&v.src)return{...a,...v,_ord:i}}catch(e){}return null}));
349
  const vids=results.filter(Boolean).sort((a,b)=>a._ord-b._ord);
350
  if(!vids.length){el.innerHTML='<div class="loading">Không tìm thấy video</div>';return}
351
+ const ordered=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="${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.id||v._ord);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;setPlaylist(ordered.map(v=>({title:v.title,img:v.poster||v.img})));initTikTokFeed();}
352
+ 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;setPlaylist(ordered.map(v=>({title:v.title,img:v.img||('https://i.ytimg.com/vi/'+(v.id||'')+'/hqdefault.jpg')})));initTikTokFeed();}
353
+ 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;setPlaylist(ordered.map(p=>({title:p.title,img:p.img})));initTikTokFeed();}
354
+ 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="${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||'')}','${esc((data.summary||'').slice(0,180))}')">📤</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>`;}
355
  async function rewriteArticle(){const url=_currentArticle?.url;if(!url)return;toast('⏳ Đang rewrite...');try{const r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url,context: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);goToWallPost(j.post.id);}}catch(e){toast('❌ '+e.message)}}
356
  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);goToWallPost(j.post.id);}}catch(e){toast('❌ '+e.message)}}
357
  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}}
 
393
 
394
  const hasVideo = p.video && p.video.length > 0;
395
 
396
+ // === NEWS ITEMS (topic digest): each main point = 1 news (title + image + short desc), no URLs ===
397
+ let itemsHtml = '';
398
+ if(p.items && p.items.length){
399
+ itemsHtml = '<div class="topic-items">';
400
+ p.items.forEach((it,ix)=>{
401
+ const im = it.img ? `<div class="topic-item-img"><img src="${esc(it.img)}" onerror="this.parentElement.style.display='none'" loading="lazy"></div>` : '';
402
+ const desc = it.desc ? `<div class="topic-item-desc">${esc(it.desc)}</div>` : '';
403
+ const open = it.link ? ` onclick="readArticle('${esc(it.link)}')" style="cursor:pointer"` : '';
404
+ itemsHtml += `<div class="topic-item"${open}><div class="topic-item-no">${ix+1}</div>${im}<div class="topic-item-body"><div class="topic-item-title">${esc(it.title)}</div>${desc}</div></div>`;
405
+ });
406
+ itemsHtml += '</div>';
407
+ }
408
+
409
  // Fetch voices dynamically from backend (filtered: VN + gTTS + Multilingual)
410
  const vd = await _getVoices();
411
  const voicesObj = vd.voices || {};
 
458
  <span class="badge badge-ai">AI</span>
459
  <h1 class="article-title">${esc(p.title)}</h1>
460
  ${imgGallery}
461
+ ${itemsHtml}
462
+ ${(p.items&&p.items.length)?'':`<p class="article-p" style="white-space:pre-wrap">${esc(p.text)}</p>`}
463
  ${hasVideo?`<video class="article-img" src="${esc(p.video)}" controls playsinline style="max-height:400px"></video>`:''}
464
  ${voiceSelector}
465
  <div class="article-actions">
466
  ${actionButtons}
467
+ <button onclick="sharePost(_wallPosts[${i}])">📤</button>
468
  </div>
469
  </div>`;
470