Spaces:
Running
Running
Render topic news items as cards; share opens article/post with OG preview; add Playlist button to all shorts feeds
Browse files- 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 |
-
|
|
|
|
| 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="
|
| 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 |
|