VNEWS / app_v2.js
bep40's picture
Sync main to commit 74ca420b: per-league highlight slides from xemlaibongda, fix football video slide CSS + Xem them shorts load-more, fix livescore standings CSS + 2026-2027 season
8f8b7ec verified
Raw
History Blame Contribute Delete
134 kB
/**
* VNEWS Frontend v2 - Shorts Dantri/SKDS removed, VTV Digital CDN
* v2.8 - Changed short AI feed video share button icon (📥) to distinguish from article share (📤)
* v2.6 - Fixed share links: doShare now includes post_id for wall posts,
* readSlidePost has share button, /s + /s/{slug} render slides/video inline
* v2.5 - Added 'Tạo lại' button on wall cards with video
* v2.4 - Fixed: prependWallPost detached-element bug, makeShortVideo UI update, slide viewer for rewrite posts
*/
// ---- World Cup 2026 tab switcher (was missing -> loadHome() crashed) ----
function switchWCTab(tab){
try {
var el = document.getElementById('wc-content');
if(!el) return;
var d = (typeof _wc2026Data !== 'undefined') ? _wc2026Data : null;
if(!d){ el.innerHTML = '<div class="loading">Không có dữ liệu World Cup 2026</div>'; return; }
if(tab === 'fixtures'){
var m = (d.fixtures && d.fixtures.matches) || [];
var h = '<div class="wc-list">';
m.slice(0, 40).forEach(function(x){
h += '<div class="wc-row"><span class="wc-grp">'+esc(x.group||'')+'</span><span class="wc-match">'+esc(x.home||'?')+' '+esc(x.score||'VS')+' '+esc(x.away||'?')+'</span><span class="wc-date">'+esc(x.date_vn||'')+'</span></div>';
});
h += '</div>';
el.innerHTML = m.length ? h : '<div class="loading">Chưa có lịch thi đấu</div>';
} else if(tab === 'standings'){
el.innerHTML = '<div class="loading">'+esc((d.standings && (typeof d.standings==='string'?d.standings:JSON.stringify(d.standings)))||'Bảng xếp hạng đang cập nhật')+'</div>';
} else if(tab === 'highlights'){
el.innerHTML = '<div class="loading">'+esc((d.highlights && (typeof d.highlights==='string'?d.highlights:JSON.stringify(d.highlights)))||'Highlight đang cập nhật')+'</div>';
} else if(tab === 'stats'){
el.innerHTML = '<div class="loading">'+esc((d.stats && (typeof d.stats==='string'?d.stats:JSON.stringify(d.stats)))||'Thống kê đang cập nhật')+'</div>';
} else {
var news = (d.news) || [];
var nh = '<div class="wc-news">';
(Array.isArray(news)?news:[]).slice(0,15).forEach(function(n){
nh += '<div class="wc-news-item"><a href="'+esc(n.link||'#')+'" target="_blank" rel="noopener">'+esc(n.title||'')+'</a></div>';
});
nh += '</div>';
el.innerHTML = Array.isArray(news)&&news.length ? nh : '<div class="loading">Đang tải tin World Cup 2026...</div>';
}
// update active tab styling
try {
document.querySelectorAll('.wc-tab').forEach(function(t){
var want = (tab==='news') ? 'news' : tab;
t.classList.toggle('active', t.getAttribute('onclick') && t.getAttribute('onclick').indexOf("'"+want+"'")>=0);
});
} catch(e){}
} catch(e){
var el2 = document.getElementById('wc-content');
if(el2) el2.innerHTML = '<div class="loading">Lỗi tải World Cup 2026</div>';
}
}
function _proxyImg(url){
if(!url || typeof url !== 'string') return '';
if(url.startsWith('http') && !url.includes(location.host)){
return '/api/proxy/img?url='+encodeURIComponent(url);
}
return url;
}
var _ttsSelections = {};
function _fetchWithTimeout(url, ms){
return new Promise((resolve,reject)=>{
const ctrl=new AbortController();
const tid=setTimeout(()=>ctrl.abort(),ms);
fetch(url,{signal:ctrl.signal}).then(r=>{
clearTimeout(tid);
if(!r.ok) return reject(new Error('HTTP '+r.status));
return r.json();
}).then(d=>resolve(d)).catch(e=>{clearTimeout(tid);reject(e);});
});
}
// ===== rewriteUrl: tạo bài rewrite slide AI từ URL bài viết =====
// Gọi /api/url_wall (backend thực tế), lấy post rồi đẩy lên Tường AI và mở slide viewer.
async function rewriteUrl(){
const inp = document.getElementById('url-input');
const url = (inp && inp.value || '').trim();
if(!url){ alert('Vui lòng dán URL bài viết'); return; }
if(!/^https?:\/\//i.test(url)){ alert('URL cần bắt đầu bằng http:// hoặc https://'); return; }
let btn = null;
try { btn = inp && inp.parentElement ? inp.parentElement.querySelector('button') : null; } catch(e){}
const orig = btn ? btn.textContent : 'Rewrite';
if(btn){ btn.disabled = true; btn.textContent = '⏳ Đang tạo...'; }
toast('⏳ Đang tạo bài rewrite slide AI...');
try {
const r = await fetch('/api/url_wall', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({url: url})
});
const j = await r.json();
if(!r.ok || j.error) throw new Error(j.error || 'Lỗi tạo bài');
if(!j.post) throw new Error('Không tạo được bài');
if(typeof prependWallPost === 'function'){ prependWallPost(j.post); }
else if(Array.isArray(_wallPosts)){ _wallPosts.unshift(j.post); }
if(inp) inp.value = '';
toast('✅ Đã tạo bài rewrite slide AI!');
const idx = (_wallPosts || []).indexOf(j.post);
if(j.post.slides && j.post.slides.length){ readSlidePost(idx >= 0 ? idx : 0); }
else if(typeof readWallPost === 'function'){ readWallPost(idx >= 0 ? idx : 0); }
} catch(e){
toast('❌ ' + e.message);
} finally {
if(btn){ btn.disabled = false; btn.textContent = orig; }
}
}
// ===== shareVideoToApps: gửi file MP4 qua app (chia sẻ video) =====
async function shareVideoToApps(videoUrl, title){
if(!videoUrl){ toast('Không có video để chia sẻ'); return; }
const safeTitle = (title || 'VNEWS Short AI').toString().slice(0, 80);
if(navigator.canShare && navigator.canShare({files:[]}) !== undefined){
try {
const resp = await fetch(videoUrl, {mode:'cors'});
if(resp && resp.ok){
const blob = await resp.blob();
let fname = (videoUrl.split('?')[0].split('/').pop() || 'vnews-short.mp4');
if(!fname.toLowerCase().endsWith('.mp4')) fname = 'vnews-short.mp4';
const file = new File([blob], fname, {type: (blob.type && blob.type.indexOf('video')>=0) ? blob.type : 'video/mp4'});
if(navigator.canShare && navigator.canShare({files:[file]})){
await navigator.share({files:[file], title: safeTitle, text: safeTitle});
return;
}
}
} catch(e){ /* fall through to link share */ }
}
const _base = (typeof SPACE !== 'undefined' && SPACE) ? SPACE : location.origin;
const shareUrl = _base + '/s?url=' + encodeURIComponent(videoUrl) + '&title=' + encodeURIComponent(safeTitle);
if(navigator.share){
try { await navigator.share({title: safeTitle, text: safeTitle, url: shareUrl}); return; } catch(e){ if(e && e.name === 'AbortError') return; }
}
if(navigator.clipboard && navigator.clipboard.writeText){
try { await navigator.clipboard.writeText(shareUrl); toast('📋 Đã sao chép link video!'); return; } catch(e){}
}
try {
const ta = document.createElement('textarea');
ta.value = shareUrl; ta.style.position='fixed'; ta.style.left='-9999px'; ta.style.top='-9999px'; ta.style.opacity='0';
document.body.appendChild(ta); ta.select(); ta.setSelectionRange(0, 99999);
if(document.execCommand('copy')){ toast('📋 Đã sao chép link video!'); }
else { prompt('📋 Sao chép link video:', shareUrl); }
document.body.removeChild(ta);
} catch(e){ prompt('📋 Sao chép link video:', shareUrl); }
}
async function loadHome(){
const homeEl = document.getElementById('view-home');
if(!homeEl) return;
homeEl.innerHTML =
'<div id="home-featured-area"></div>'
+'<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><button class="hot-short-btn" onclick="openShortHotCreator()">🔥 Thêm Short HOT</button></div><div class="ai-compose-row" style="margin-top:8px"><textarea id="opinion-input" placeholder="💭 Nhập quan điểm cá nhân của bạn..." rows="2" style="width:100%;padding:8px;border-radius:8px;border:1px solid #333;background:#222;color:#eee;font-size:12px;resize:vertical"></textarea></div><div class="ai-compose-row"><button onclick="openPersonalPostPreview()">📝 Xem trước & Đăng bài quan điểm</button></div><div id="hot-topics" class="hot-topic-row"></div></div>'
+'<div id="ai-wall-under-compose"></div>'
+'<div id="hashtag-box"></div>'
+'<div id="home-hl-section"></div>'
+'<div class="ls-section"><div class="ls-header"><h3>⚽ Livescore</h3></div><div class="ls-tabs"><span class="ls-tab active" data-tab="recent" onclick="loadLivescore(\'recent\')">📋 Cập nhật</span><span class="ls-tab" data-tab="live" onclick="loadLivescore(\'live\')">🔴 Live</span><span class="ls-tab" data-tab="today" onclick="loadLivescore(\'today\')">📅 Hôm nay</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>'
+'<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>'
+'<div id="home-after-wc"></div>';
const afterEl = homeEl.querySelector('#home-after-wc');
loadLivescore('recent');
loadHotTopics();
const [featuredData, wallData, hlLeagues, wcData] = await Promise.allSettled([
_fetchWithTimeout('/api/livescore/featured', 5000),
_fetchWithTimeout('/api/wall', 5000),
_fetchWithTimeout('/api/highlights/leagues', 10000),
_fetchWithTimeout('/api/wc2026', 20000),
]).then(results => results.map(r => r.status === 'fulfilled' ? r.value : null));
if(featuredData && featuredData.home){
const sc=featuredData.status==='live'?'':'upcoming';
const st=featuredData.status==='live'?`🔴 ${featuredData.minute||'LIVE'}`:`⏰ ${featuredData.time}`;
const area=document.getElementById('home-featured-area');
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>`;
}
_wallPosts = (wallData && wallData.posts) || [];
_hlLeagueData = hlLeagues || {};
_wc2026Data = wcData;
if(wcData) switchWCTab('news');
_renderHLSection();
_renderWallIn(afterEl);
}
function _renderSlidesIn(key, label, emoji, vids, afterEl){
if(!vids||!vids.length||!afterEl) return;
const wrap=document.createElement('div');
wrap.className='slider-wrap';
let h=`<div class="slider-header"><span class="slider-label">${emoji} ${label}</span></div><div class="slider-track">`;
const isHL = key==='world-cup'||key==='premier-league'||key==='champions-league'||key==='la-liga'||key==='serie-a'||key==='bundesliga'||key==='friendly';
vids.slice(0,isHL?8:12).forEach((a,i)=>{
if(isHL){
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>`;
} else {
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>`;
}
});
h+='</div>';
wrap.innerHTML=h;
afterEl.parentNode.insertBefore(wrap, afterEl);
}
function _renderWallIn(afterEl){
if(!_wallPosts||!_wallPosts.length) return;
const posts=_wallPosts;
const wrap=document.createElement('div');
wrap.className='slider-wrap';wrap.id='ai-wall-wrap';
let h='<div class="slider-header"><span class="slider-label">🧱 Tường AI</span></div><div class="slider-track" id="ai-wall-track">';
posts.slice(0,20).forEach((p,i)=>{h+=makeWallItem(p,i);});
h+='</div>';wrap.innerHTML=h;
const target=document.getElementById('ai-wall-under-compose');
if(target) target.appendChild(wrap);
else if(afterEl) afterEl.parentNode.insertBefore(wrap,afterEl);
}
function _renderHLIn(afterEl){
if(!_hlLeagueData||Object.keys(_hlLeagueData).length===0||!afterEl) return;
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:"🤝"}};
for(const[key,cfg] of Object.entries(HL_CONFIG)){
const vids=_hlLeagueData[key];
if(!vids||!vids.length) continue;
_renderSlidesIn(key,cfg.name,cfg.emoji,vids,afterEl);
}
}
// === RENDER HIGHLIGHT (Video bóng đá) SECTION — above Livescore, with "Xem thêm" per league ===
var _hlPage = {}; // {leagueKey: page}
var _hlHasMore = {}; // {leagueKey: bool}
function _renderHLSection(){
const hlEl = document.getElementById('home-hl-section');
if(!hlEl) return;
if(!_hlLeagueData || Object.keys(_hlLeagueData).length===0){
return;
}
const HL_CONFIG={"premier-league":{name:"Ngoại Hạng Anh",emoji:"🏴"},"champions-league":{name:"Champions League",emoji:"⭐"},"la-liga":{name:"La Liga",emoji:"🇪🇸"},"serie-a":{name:"Serie A",emoji:"🇮🇹"},"bundesliga":{name:"Bundesliga",emoji:"🇩🇪"},"world-cup":{name:"World Cup 2026",emoji:"🌍"},"fa-cup":{name:"FA Cup",emoji:"🏆"},"friendly":{name:"Giao hữu",emoji:"🤝"}};
// Collect all leagues that have videos (premier-league first)
const orderedLeagues = [];
const plVids = _hlLeagueData['premier-league'];
if(plVids && plVids.length) orderedLeagues.push('premier-league');
for(const [key, cfg] of Object.entries(HL_CONFIG)){
if(key === 'premier-league') continue;
const vids = _hlLeagueData[key];
if(vids && vids.length) orderedLeagues.push(key);
}
if(orderedLeagues.length === 0){
return; // nothing to show
}
let html = '<div class="slider-wrap"><div class="slider-header"><span class="slider-label">🎬 Video bóng đá</span></div><div class="slider-track" id="hl-main-track">';
// Main track: premier-league (Ngoại Hạng Anh)
html += '<div id="hl-premier-league" class="hl-league-block"></div>';
html += '</div></div>';
hlEl.innerHTML = html;
_renderHLSlides('premier-league', HL_CONFIG['premier-league'].name, HL_CONFIG['premier-league'].emoji, plVids, 'hl-premier-league');
// Temporarily hide per-league blocks until slide formatting is verified
// (Remaining leagues rendering deferred to avoid broken slider layout)
}
function _renderHLSlides(key, label, emoji, vids, containerId){
_hlPage[key] = 0;
_hlHasMore[key] = vids && vids.length >= 8;
const container = document.getElementById(containerId);
if(!container) return;
if(!vids || !vids.length){
container.innerHTML = '<div class="loading" style="padding:10px;color:#888;font-size:11px;text-align:center">Chưa có video</div>';
return;
}
let h = '';
const limit = 8;
const batch = vids.slice(0, limit);
batch.forEach((a, i) => {
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>';
});
container.innerHTML = h;
}
async function _loadMoreHL(key){
const page = (_hlPage[key] || 0) + 1;
const btn = event && event.target;
if(btn){ btn.disabled = true; btn.textContent = '⏳ Đang tải...'; }
try{
const r = await fetch('/api/highlights/' + key + '/page?page=' + page + '&limit=15');
const j = await r.json();
if(!j || !j.videos || !j.videos.length){
if(btn) btn.style.display = 'none';
return;
}
// Append new videos to _hlLeagueData so openHighlightFeed picks them up
if(!_hlLeagueData[key]) _hlLeagueData[key] = [];
_hlLeagueData[key] = _hlLeagueData[key].concat(j.videos);
// Re-index existing items (their _idx was assigned at creation time in openHighlightFeed
// from articles order, so appending is sufficient — new items go to the end)
const container = document.getElementById('hl-track-' + key) || document.getElementById('hl-premier-league');
if(!container || !container.querySelector){ if(btn) btn.style.display = 'none'; return; }
let h = '';
const startIdx = (_hlLeagueData[key].length - j.videos.length);
const offset = startIdx;
j.videos.forEach((a, i) => {
h += '<div class="slider-item" onclick="openHighlightFeed(\'' + key + '\',' + (offset + 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>';
});
container.insertAdjacentHTML('beforeend', h);
_hlPage[key] = page;
_hlHasMore[key] = j.has_more;
if(!j.has_more && btn) btn.style.display = 'none';
}catch(e){
if(btn) btn.textContent = '❌ Lỗi';
}
}
// === WALL POST HELPERS ===
function makeWallItem(p,i){
var hasVideo = p.video && p.video.length > 0;
var isSlide = (p.slides && p.slides.length) || p.kind==='slide_summary';
// Slide/design posts: keep the image's natural aspect ratio (no 16:9 crop)
// so portrait-designed slides aren't cut — this fixes the "saved image ratio
// and background position look wrong vs the preview" bug on the wall thumb.
var thumbStyle = isSlide
? 'aspect-ratio:auto;max-height:340px;display:flex;align-items:flex-start;justify-content:center;background:#000'
: '';
var thumbContent = p.img
? (isSlide
? '<img src="'+_proxyImg(p.img)+'" style="width:100%;height:auto;max-width:100%;object-fit:contain" loading="lazy" onerror="this.style.display=\'none\'">'
: '<img src="'+_proxyImg(p.img)+'" loading="lazy" onerror="this.style.display=\'none\'">')
: (hasVideo ? '<video src="'+esc(p.video)+'" muted></video>' : '');
var videoBadge = hasVideo ? '<div class="wall-video-badge">🎬</div>' : '';
var vid = p.id||i;
var lang = p.language || detectLanguage(p.title + ' ' + (p.text||''));
var curVoice = p.voice || getAutoVoice(lang);
var curEmotion = p.emotion || detectEmotion(p.title + ' ' + (p.text||''));
var selKey = 'inline-' + vid;
if(!_ttsSelections[selKey]) _ttsSelections[selKey] = {voice: curVoice, emotion: curEmotion};
var voiceOpts = '';
VOICE_LIST.forEach(function(v){
voiceOpts += '<option value="'+v.id+'"'+(v.id===_ttsSelections[selKey].voice?' selected':'')+'>'+v.label+'</option>';
});
var emotOpts = '';
EMOTION_LIST.forEach(function(e){
emotOpts += '<option value="'+e.id+'"'+(e.id===_ttsSelections[selKey].emotion?' selected':'')+'>'+e.label+'</option>';
});
var spd = p.short_speed || '1.2';
var voiceBar = '<div class="wall-tts-bar" style="margin:6px 0 4px;display:grid;grid-template-columns:1fr auto auto;gap:3px">'
+'<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">'
+voiceOpts+'</select>'
+'<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">'
+emotOpts+'</select>'
+'<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)">'
+'<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>';
var makeBtn = hasVideo
? '<button class="wall-btn-video" onclick="event.stopPropagation();openShortAIFeed('+i+')">▶ Xem Short</button><button class="wall-btn-make" style="margin-left:4px" onclick="event.stopPropagation();openShortCreator(\''+esc(vid)+'\')">🎬 Tạo lại</button>'
: '<button class="wall-btn-make" onclick="event.stopPropagation();openShortCreator(\''+esc(vid)+'\')">🎬 Tạo Short</button>';
var designBtn = (p.slides && p.slides.length) ? '<button class="wall-btn-design" onclick="event.stopPropagation();openSlideDesigner('+i+')">🎨 Thiết kế ảnh</button>' : '';
return '<div class="wall-item" id="wall-item-'+esc(vid)+'"><div class="wall-thumb" style="'+thumbStyle+'">'+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>'+designBtn+makeBtn+'</div></div>';
}
/* =====================================================================
* Short AI Creator v2
* - TikTok background-music list (server-provided) or uploaded audio
* - uploaded image OR uploaded video as the short background
* - "Tạo lại" (recreate): use the DESIGNED slide images + the previous
* short's audio (NO text ever enters the video again)
* ===================================================================== */
var _shortMusicList = [];
async function loadShortMusicList(){
if(_shortMusicList.length) return _shortMusicList;
try{
const r = await fetch('/api/ai-short/music');
const j = await r.json();
_shortMusicList = (j.music || []);
}catch(e){ _shortMusicList = []; }
return _shortMusicList;
}
function openShortCreator(postId){
if(!postId) return;
const p = _wallPosts.find(x => String(x.id) === String(postId));
if(!p) return;
// if the post has designed slides, recreate defaults to slides mode
const hasSlides = p.slides && p.slides.length > 0;
_buildShortCreatorModal(p, hasSlides, false);
}
/* 🔥 Thêm Short HOT — homepage button. Opens the SAME short-creator modal with
* full scrap capability (image / uploaded video / video-from-link) but creates
* a BRAND-NEW wall post (no source article) when the short is generated. */
function openShortHotCreator(){
const p = {
id: 'hot-' + Date.now(),
title: '',
text: '',
img: null,
images: [],
slides: null,
video: null,
language: 'vi',
};
_buildShortCreatorModal(p, false, true);
}
function _buildShortCreatorModal(p, defaultUseSlides, hotMode){
loadShortMusicList().then(music => {
const hasPrev = !!(p.short_audio_url || p.video);
const hasSlides = p.slides && p.slides.length > 0;
const useSlideDef = defaultUseSlides !== undefined ? defaultUseSlides : (hasSlides && hasPrev);
const overlay = document.createElement('div');
overlay.id = 'short-creator-overlay';
overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.94);z-index:99999;display:flex;align-items:center;justify-content:center;padding:12px;overflow-y:auto';
// voice/emotion from stored selection
const selKey = 'inline-' + p.id;
if(!_ttsSelections[selKey]) _ttsSelections[selKey] = {voice: p.voice || getAutoVoice(p.language || detectLanguage(p.title+' '+(p.text||''))), emotion: p.emotion || detectEmotion(p.title+' '+(p.text||''))};
const curVoice = _ttsSelections[selKey].voice || 'vi-VN-HoaiMyNeural';
const curEmotion = _ttsSelections[selKey].emotion || 'neutral';
let h = '<div style="background:#16161a;border:2px solid #5cb87a;border-radius:16px;padding:18px;max-width:640px;width:100%;max-height:94vh;overflow-y:auto">';
h += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px"><h3 style="color:#5cb87a;margin:0;font-size:16px">'+(hotMode?'🔥 Thêm Short HOT':'🎬 Tạo Short AI')+(hasPrev?' (Tạo lại)':'')+'</h3><button onclick="document.getElementById(\'short-creator-overlay\').remove()" style="background:none;border:0;color:#fff;font-size:24px;cursor:pointer">✕</button></div>';
if(hotMode){
h += '<div style="margin-bottom:10px"><label style="color:#888;font-size:11px;display:block;margin-bottom:4px">📝 Tiêu đề bài đăng (hiển thị trên Tường AI, cũng là nội dung giọng đọc nếu không chọn video):</label>';
h += '<input id="sc-hot-title" type="text" placeholder="VD: Pha bóng đẹp mắt tại C1..." style="width:100%;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:12px"></div>';
}
/* 0. Recreate mode (designed slides only, no text) */
if(hasSlides){
h += '<div style="margin-bottom:10px;padding:10px;background:#1e2621;border:1px solid #2d8659;border-radius:10px">';
h += '<label style="color:#eee;font-size:12px;font-weight:700;display:flex;align-items:center;gap:6px;cursor:pointer"><input type="checkbox" id="sc-reuse-slides"'+(useSlideDef?' checked':'')+' style="accent-color:#5cb87a;width:15px;height:15px"> 🖼️ Tạo lại từ ảnh đã thiết kế (không đưa text vào short)</label>';
h += '<div style="color:#888;font-size:10px;margin-top:4px">Dùng ảnh slide đã thiết kế làm nền + giữ nguyên audio của Short trước đó. Chữ KHÔNG xuất hiện trong video.</div>';
h += '<div id="sc-slide-picker" style="margin-top:8px;display:'+(useSlideDef?'block':'none')+'">';
h += '<div style="color:#5cb87a;font-size:10px;margin-bottom:4px">Chọn slide muốn đưa vào Short (tích = dùng):</div><div style="display:flex;flex-wrap:wrap;gap:6px">';
p.slides.forEach((s,i)=>{
const simg = s.image || p.img || '';
h += '<label style="display:flex;align-items:center;gap:5px;background:#262b2e;border:1px solid #333;border-radius:8px;padding:5px 8px;cursor:pointer;font-size:10px;color:#ddd"><input type="checkbox" class="sc-slide-chk" data-idx="'+i+'"'+(s.image?' checked':'')+' style="accent-color:#5cb87a">'
+(simg?'<img src="'+_proxyImg(simg)+'" style="width:26px;height:34px;object-fit:cover;border-radius:3px" onerror="this.style.display=\'none\'">':'<span style="width:26px;height:34px;background:#333;border-radius:3px;display:inline-flex;align-items:center;justify-content:center;font-size:9px;color:#666">no</span>')
+' Slide '+(s.index||(i+1))+'</label>';
});
h += '</div></div></div>';
}
/* 1. Background: image (default) / uploaded video / scraped video link */
h += '<div style="margin-bottom:10px"><label style="color:#888;font-size:11px;display:block;margin-bottom:6px">🖼️ Nền video:</label>';
h += '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px">';
h += '<button id="sc-bg-mode-img" style="background:#2d8659;border:0;color:#fff;padding:7px 12px;border-radius:10px;font-size:11px;cursor:pointer">🖼️ Ảnh</button>';
h += '<button id="sc-bg-mode-video" style="background:#333;border:0;color:#ccc;padding:7px 12px;border-radius:10px;font-size:11px;cursor:pointer">🎞️ Video tải lên</button>';
h += '<button id="sc-bg-mode-link" style="background:#333;border:0;color:#ccc;padding:7px 12px;border-radius:10px;font-size:11px;cursor:pointer">🔗 Video từ link</button>';
h += '</div>';
h += '<div id="sc-bg-img-wrap"'+(hotMode?' style="display:none"':'')+'>';
h += '<input id="sc-img-url" type="text" placeholder="URL ảnh ngoài (để trống = dùng ảnh bài hoặc ảnh mặc định)" style="width:100%;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:12px;margin-bottom:6px">';
h += '<input type="file" id="sc-img-file" accept="image/*" style="font-size:11px;color:#888">';
/* danh sách ảnh của bài rewrite để chọn 1 ảnh cho nhiều slide */
var allImgs = [];
if(p.images && p.images.length) allImgs = allImgs.concat(p.images);
if(p.img && p.img && allImgs.indexOf(p.img) < 0) allImgs.push(p.img);
if(p.slides && p.slides.length) p.slides.forEach(s=>{ if(s.image && allImgs.indexOf(s.image)<0) allImgs.push(s.image); });
var _defaultPick = (p.short_images && p.short_images[0]) || p.img || "";
if(!allImgs.includes(_defaultPick)) _defaultPick = "";
h += '<div id="sc-rewrite-list-wrap" style="margin-top:6px">';
h += '<div style="color:#888;font-size:10px;margin-bottom:4px">📋 Ảnh trong bài (bấm để chọn 1 ảnh cố định cho nhiều slide):</div>';
h += '<div id="sc-rewrite-list" style="display:flex;flex-wrap:wrap;gap:6px">';
if(allImgs.length){
allImgs.forEach(function(u){
var sel = (u === _defaultPick);
h += '<button type="button" class="sc-rw-img" data-img="'+esc(u)+'" data-sel="'+(sel?'1':'0')+'"'
+ ' style="width:56px;height:84px;background:#222;border:2px solid '+(sel?'#ffd700':'#333')+';border-radius:8px;padding:0;cursor:pointer;'
+ 'background-image:url('+(_proxyImg ? _proxyImg(u) : u)+');background-size:cover;background-position:center"></button>';
});
} else {
h += '<span style="color:#555;font-size:10px">Chưa có ảnh trong bài. Tải lên hoặc nhập URL.</span>';
}
h += '</div>';
h += '<div style="color:#888;font-size:10px;margin-top:4px">✅ ảnh được chọn (vàng) áp dụng cho <b>tất cả slide</b> trong short. Để trống = dùng ảnh từng slide.</div>';
h += '</div>';
h += '</div>';
h += '<input type="hidden" id="sc-chosen-img" value="">';
h += '<div id="sc-bg-video-wrap" style="display:none">';
h += '<input type="file" id="sc-video-file" accept="video/*" style="font-size:11px;color:#888">';
h += '<label style="display:flex;align-items:center;gap:6px;color:#bbb;font-size:11px;margin-top:6px;cursor:pointer"><input type="checkbox" id="sc-video-mute" style="accent-color:#5cb87a;width:14px;height:14px"> 🔇 Tắt tiếng video này (chỉ dùng hình ảnh, âm thanh do giọng đọc đảm nhiệm)</label>';
h += '<div style="color:#888;font-size:10px;margin-top:4px">Tải lên video làm nền (mp4/webm, tối đa 60MB). Nếu chọn video, ảnh sẽ không được dùng.</div>';
h += '</div>';
h += '<div id="sc-bg-link-wrap"'+(hotMode?' style="display:block"':' style="display:none"')+'>';
h += '<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px">';
h += '<input id="sc-video-link" type="text" placeholder="Dán link video: YouTube, TikTok, VnExpress, Dân trí, Znews..." style="flex:1;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:12px">';
h += '<button id="sc-video-scrape" type="button" style="background:#1a3a5a;border:1px solid #3a5a7b;color:#fff;padding:8px 12px;border-radius:8px;font-size:11px;cursor:pointer;white-space:nowrap">📥 Lấy video</button>';
h += '</div>';
h += '<div id="sc-video-scrape-status" style="color:#888;font-size:10px;margin-bottom:6px">Dán link YouTube / TikTok / video tin tức, bấm "Lấy video" để xem trước.</div>';
h += '<div id="sc-video-preview" style="display:none;margin-bottom:6px"></div>';
h += '<div id="sc-video-meta" style="color:#aaa;font-size:11px;margin-bottom:6px"></div>';
h += '<label style="display:flex;align-items:center;gap:6px;color:#bbb;font-size:11px;margin-top:6px;cursor:pointer"><input type="checkbox" id="sc-link-mute" style="accent-color:#5cb87a;width:14px;height:14px"> 🔇 Tắt tiếng video này</label>';
h += '<div style="color:#888;font-size:10px;margin-top:4px">Short sẽ chạy video này. Bỏ trống nếu muốn giữ ảnh làm nền.</div>';
h += '<input type="hidden" id="sc-scrape-direct">';
h += '<input type="hidden" id="sc-scrape-duration">';
h += '</div></div>';
/* 2. Audio + 3. Voice — SKIPPED in HOT mode (video keeps its own audio) */
if(!hotMode){
/* 2. Audio: previous short audio / TikTok music / uploaded audio */
h += '<div style="margin-bottom:10px"><label style="color:#888;font-size:11px;display:block;margin-bottom:6px">🎵 Âm thanh:</label>';
h += '<select id="sc-audio-src" style="width:100%;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:12px;margin-bottom:6px">';
if(hasPrev && p.video){
h += '<option value="prev">🔁 Giữ nguyên audio Short trước</option>';
}
h += '<option value="tts">🎙️ Đọc lại bằng giọng AI (TTS)</option>';
h += '<option value="music">🎵 Nhạc nền TikTok có sẵn (thêm vào)</option>';
h += '<option value="upload">📤 Tải audio lên</option>';
h += '<option value="none">🔇 Không âm thanh</option>';
h += '</select>';
/* Music chooser — luôn hiển thị để người dùng NGHE THỬ trước khi chọn */
h += '<div id="sc-music-wrap">';
h += '<div style="display:flex;gap:6px;align-items:center;margin-bottom:4px"><select id="sc-music" style="flex:1;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:12px">';
(music||[]).forEach(m=>{ h += '<option value="'+m.id+'"'+(m.id===(p.short_music||'')?' selected':'')+' data-aurl="'+(m.url||'')+'">'+m.name+'</option>'; });
h += '</select>';
h += '<button id="sc-music-listen" type="button" style="background:#1a3a5a;border:1px solid #3a5a7b;color:#fff;padding:8px 12px;border-radius:8px;font-size:11px;cursor:pointer;white-space:nowrap">▶ Nghe thử</button></div>';
h += '<div id="sc-music-playerbox" style="display:none;margin-bottom:4px;text-align:center"></div>';
h += '<div style="color:#888;font-size:10px">▶ Nghe thử nhạc nền TikTok trước khi chọn. Nhạc sẽ được trộn vào video (nền) khi tạo.</div>';
h += '</div>';
h += '<div id="sc-upload-wrap" style="display:none"><input type="file" id="sc-audio-file" accept="audio/*" style="font-size:11px;color:#888"></div>';
h += '</div>';
/* 3. Voice + emotion + speed (TTS) */
h += '<div style="margin-bottom:10px"><label style="color:#888;font-size:11px;display:block;margin-bottom:6px">🎙️ Giọng đọc:</label>';
h += '<select id="sc-voice" style="width:100%;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:12px">';
VOICE_LIST.forEach(v=>{ h += '<option value="'+v.id+'"'+(v.id===curVoice?' selected':'')+'>'+v.label+'</option>'; });
h += '</select></div>';
h += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">';
h += '<div><label style="color:#888;font-size:11px;display:block;margin-bottom:4px">😊 Cảm xúc:</label><select id="sc-emotion" style="width:100%;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:12px">';
EMOTION_LIST.forEach(e=>{ h += '<option value="'+e.id+'"'+(e.id===curEmotion?' selected':'')+'>'+e.label+'</option>'; });
h += '</select></div>';
h += '<div><label style="color:#888;font-size:11px;display:block;margin-bottom:4px">⚡ Tốc độ:</label><select id="sc-speed" style="width:100%;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:12px">';
['0.85','1.0','1.2','1.35'].forEach(s=>{ h += '<option value="'+s+'"'+(s===(String(p.short_speed)||'1.2')?' selected':'')+'>'+s+'x</option>'; });
h += '</select></div></div>';
}
h += '<div style="display:flex;gap:8px;margin-top:14px">';
h += '<button id="sc-create-btn" style="flex:1;background:#2d8659;border:0;color:#fff;padding:11px;border-radius:12px;font-size:13px;font-weight:700;cursor:pointer">'+(hotMode?'🔥 Đăng Short HOT':'🎬 Tạo Short AI')+'</button>';
h += '<button onclick="document.getElementById(\'short-creator-overlay\').remove()" style="background:#333;border:0;color:#ccc;padding:11px 16px;border-radius:12px;font-size:12px;cursor:pointer">Hủy</button>';
h += '</div>';
h += '<div id="sc-status" style="color:#888;font-size:11px;margin-top:8px;text-align:center"></div>';
overlay.innerHTML=h; document.body.appendChild(overlay);
// Cleanup HLS preview instance whenever the modal is removed (✕ / Hủy / success)
(function(){
const obs = new MutationObserver(function(muts){
if(document.getElementById('sc-scrape-video')){
const p = document.getElementById('sc-scrape-video');
if(p._hls){ try{ p._hls.destroy(); }catch(e){} p._hls = null; }
}
});
obs.observe(document.body, {childList:true, subtree:false});
// stop after this overlay is gone
setTimeout(()=>obs.disconnect(), 60*1000);
})();
// ---- mode toggles ----
let bgMode = hotMode ? 'link' : 'img';
function setBgMode(m){
bgMode = m;
document.getElementById('sc-bg-mode-img').style.background = m==='img' ? '#2d8659' : '#333';
document.getElementById('sc-bg-mode-img').style.color = m==='img' ? '#fff' : '#ccc';
document.getElementById('sc-bg-mode-video').style.background = m==='video' ? '#2d8659' : '#333';
document.getElementById('sc-bg-mode-video').style.color = m==='video' ? '#fff' : '#ccc';
document.getElementById('sc-bg-mode-link').style.background = m==='link' ? '#2d8659' : '#333';
document.getElementById('sc-bg-mode-link').style.color = m==='link' ? '#fff' : '#ccc';
document.getElementById('sc-bg-img-wrap').style.display = (m==='img' || m==='link') ? 'block' : 'none';
document.getElementById('sc-bg-video-wrap').style.display = m==='video' ? 'block' : 'none';
document.getElementById('sc-bg-link-wrap').style.display = m==='link' ? 'block' : 'none';
}
if(hotMode){
// HOT mode: only video-from-link is offered; hide ảnh/upload-video buttons
document.getElementById('sc-bg-mode-img').style.display = 'none';
document.getElementById('sc-bg-mode-video').style.display = 'none';
document.getElementById('sc-bg-mode-link').style.background = '#2d8659';
document.getElementById('sc-bg-mode-link').style.color = '#fff';
} else {
document.getElementById('sc-bg-mode-img').addEventListener('click', ()=>setBgMode('img'));
document.getElementById('sc-bg-mode-video').addEventListener('click', ()=>setBgMode('video'));
document.getElementById('sc-bg-mode-link').addEventListener('click', ()=>setBgMode('link'));
}
// ---- scraped video link: fetch preview ----
const scLinkInput = document.getElementById('sc-video-link');
const scScrapeBtn = document.getElementById('sc-video-scrape');
if(scScrapeBtn && scLinkInput){
scScrapeBtn.addEventListener('click', async ()=>{
const url = scLinkInput.value.trim();
const st = document.getElementById('sc-video-scrape-status');
const preview = document.getElementById('sc-video-preview');
const meta = document.getElementById('sc-video-meta');
if(!url){
st.textContent = '⚠️ Dán link video trước.';
st.style.color = '#e0a030';
return;
}
st.textContent = '⏳ Đang lấy thông tin video...';
st.style.color = '#888';
try{
const r = await fetch('/api/ai-short/scrape?url=' + encodeURIComponent(url));
const j = await r.json();
if(!r.ok || j.error) throw new Error(j.error || 'Không lấy được video');
// metadata
let metaHtml = '';
if(j.title) metaHtml += '<div style="font-weight:600;color:#ddd;margin-bottom:2px">' + esc(j.title) + '</div>';
if(j.duration) metaHtml += '<div style="color:#8f8">⏱️ ' + Math.round(j.duration) + 's</div>';
if(j.extractor) metaHtml += '<div style="color:#777">Nguồn: ' + esc(j.extractor) + '</div>';
if(j.note) metaHtml += '<div style="color:#e0a030">⚠️ ' + esc(j.note) + '</div>';
meta.innerHTML = metaHtml;
meta.style.display = 'block';
// direct URL for the short
const scDirect = j.direct_url || '';
meta.setAttribute('data-title', j.title || '');
meta.setAttribute('data-direct', scDirect || '');
document.getElementById('sc-scrape-direct').value = scDirect;
document.getElementById('sc-scrape-duration').value = j.duration ? String(j.duration) : '';
if(j.previewable && scDirect){
const isHls = !!j.is_hls || /\.m3u8/i.test(scDirect);
const streamUrl = isHls
? '/api/ai-short/scrape/hls?url=' + encodeURIComponent(scDirect)
: '/api/ai-short/scrape/preview?url=' + encodeURIComponent(scDirect);
preview.style.display = 'block';
preview.setAttribute('data-ishls', isHls ? '1' : '0');
preview.innerHTML = '<video id="sc-scrape-video" controls playsinline preload="metadata" style="width:100%;max-height:260px;background:#000;border-radius:8px"'+(isHls?' data-hls="'+streamUrl+'"':' src="'+streamUrl+'"')+'></video>';
const v = preview.querySelector('#sc-scrape-video');
if(isHls && v && typeof Hls !== 'undefined' && Hls.isSupported()){
const hls = new Hls();
hls.loadSource(streamUrl);
hls.attachMedia(v);
hls.on(Hls.Events.MANIFEST_PARSED, ()=>{ v.play().catch(()=>{}); });
v._hls = hls;
} else if(isHls && v && v.canPlayType('application/vnd.apple.mpegurl')){
v.src = streamUrl; // native Safari HLS
}
st.textContent = '✅ Đã lấy video. Bấm play để xem trước.';
st.style.color = '#8f8';
} else {
preview.style.display = 'none';
if(j.note){
st.textContent = '⚠️ ' + j.note;
st.style.color = '#e0a030';
} else {
st.textContent = '❌ Không phát trước được (có thể do nguồn chặn). Bạn vẫn có thể thử tạo short.';
st.style.color = '#e05555';
}
}
}catch(e){
st.textContent = '❌ ' + e.message;
st.style.color = '#e05555';
preview.style.display = 'none';
document.getElementById('sc-scrape-direct').value = '';
document.getElementById('sc-scrape-duration').value = '';
}
});
}
// ---- rewrite image picker (chọn 1 ảnh cho nhiều slide) ----
let _scChosenImg = '';
document.getElementById('sc-rewrite-list').addEventListener('click', e=>{
const btn = e.target.closest('.sc-rw-img');
if(!btn) return;
document.querySelectorAll('.sc-rw-img').forEach(b=>{ b.style.borderColor='#333'; b.dataset.sel='0'; });
btn.style.borderColor='#ffd700'; btn.dataset.sel='1';
_scChosenImg = btn.dataset.img || '';
const hidden = document.getElementById('sc-chosen-img');
if(hidden) hidden.value = _scChosenImg;
});
// ---- audio source toggles (skipped in HOT mode) ----
if(!hotMode){
function setAudioSrc(v){
// nhạc nền TikTok luôn hiển thị để nghe thử; ẩn khi chọn "Không âm thanh"
document.getElementById('sc-music-wrap').style.display = v==='none' ? 'none' : 'block';
document.getElementById('sc-upload-wrap').style.display = v==='upload' ? 'block' : 'none';
}
document.getElementById('sc-audio-src').addEventListener('change', e=>setAudioSrc(e.target.value));
if(hasPrev && p.video) setAudioSrc('prev'); else setAudioSrc('tts');
}
// ---- recreate slides: show/hide slide picker ----
const scReuse = document.getElementById('sc-reuse-slides');
if(scReuse){
scReuse.addEventListener('change', function(){
const pk = document.getElementById('sc-slide-picker');
if(pk) pk.style.display = this.checked ? 'block' : 'none';
});
}
// ---- music preview player (skipped in HOT mode) ----
let _scAudio = null;
const listenBtn = hotMode ? null : document.getElementById('sc-music-listen');
const musicSel = hotMode ? null : document.getElementById('sc-music');
const playerBox = hotMode ? null : document.getElementById('sc-music-playerbox');
if(listenBtn && musicSel){
listenBtn.addEventListener('click', function(){
const opt = musicSel.options[musicSel.selectedIndex];
const aurl = opt ? (opt.dataset.aurl || '') : '';
if(!aurl){
alert('Không có link nhạc cho lựa chọn này.');
return;
}
// proxy để tránh CORS khi phát thử trong trình duyệt
const streamUrl = '/api/ai-short/music/stream?url=' + encodeURIComponent(aurl);
if(_scAudio && _scAudio.dataset.src === aurl && !_scAudio.paused){
_scAudio.pause(); listenBtn.textContent = '▶ Nghe thử';
return;
}
if(_scAudio) _scAudio.pause();
if(playerBox) playerBox.style.display = 'block';
if(playerBox) playerBox.innerHTML = '<div style="color:#aaa;font-size:11px;margin-bottom:2px">🎧 '+esc(opt.text)+'</div><audio controls autoplay style="width:100%;height:34px" src="'+streamUrl+'"></audio>';
_scAudio = playerBox ? playerBox.querySelector('audio') : null;
if(_scAudio){
_scAudio.dataset.src = aurl;
_scAudio.addEventListener('ended', function(){ listenBtn.textContent = '▶ Nghe thử'; });
_scAudio.addEventListener('error', function(){
listenBtn.textContent = '▶ Nghe thử';
toast('❌ Không phát được nhạc thử (mạng/vùng chặn). Bạn vẫn có thể chọn để trộn vào video.');
});
listenBtn.textContent = '⏹ Dừng';
}
});
}
// ---- create ----
document.getElementById('sc-create-btn').addEventListener('click', async ()=>{
const btn = document.getElementById('sc-create-btn');
const st = document.getElementById('sc-status');
btn.disabled = true; btn.textContent = '⏳ Đang tạo...';
st.textContent = 'Đang chuẩn bị...';
// upload files first
try{
// image file (not in HOT mode)
let imgUrl = '';
const imgUrlInput = document.getElementById('sc-img-url');
if(imgUrlInput) imgUrl = imgUrlInput.value.trim() || '';
const imgFile = hotMode ? null : document.getElementById('sc-img-file').files[0];
if(imgFile){
st.textContent = '⏳ Đang tải ảnh lên...';
const fd = new FormData(); fd.append('file', imgFile, imgFile.name);
const r = await fetch('/api/ai-short/upload', {method:'POST', body:fd});
const j = await r.json();
if(!r.ok || j.error) throw new Error(j.error || 'Lỗi upload ảnh');
imgUrl = j.url;
}
// video file (not in HOT mode)
let videoUrl = '';
const videoFile = hotMode ? null : document.getElementById('sc-video-file').files[0];
if(videoFile){
st.textContent = '⏳ Đang tải video lên...';
const fd = new FormData(); fd.append('file', videoFile, videoFile.name);
const r = await fetch('/api/ai-short/upload', {method:'POST', body:fd});
const j = await r.json();
if(!r.ok || j.error) throw new Error(j.error || 'Lỗi upload video');
videoUrl = j.url;
}
// audio file (not in HOT mode)
let uploadAudioUrl = '';
const audioFile = hotMode ? null : document.getElementById('sc-audio-file').files[0];
if(audioFile){
st.textContent = '⏳ Đang tải audio lên...';
const fd = new FormData(); fd.append('file', audioFile, audioFile.name);
const r = await fetch('/api/ai-short/upload', {method:'POST', body:fd});
const j = await r.json();
if(!r.ok || j.error) throw new Error(j.error || 'Lỗi upload audio');
uploadAudioUrl = j.url;
}
const audioSrc = hotMode ? 'video' : (document.getElementById('sc-audio-src').value);
const useSlides = hotMode ? false : (document.getElementById('sc-reuse-slides') ? document.getElementById('sc-reuse-slides').checked : false);
// nhạc nền TikTok: giá trị != none được trộn vào video (kể cả tạo lại)
let musicId = '';
const musicSel = hotMode ? null : document.getElementById('sc-music');
if(musicSel && musicSel.value && musicSel.value !== 'none' && audioSrc !== 'none'){
musicId = musicSel.value;
}
if(audioSrc === 'music') musicId = musicSel ? musicSel.value : '';
let audioUrl = '';
if(hotMode){ audioUrl = ''; }
else if(audioSrc==='upload') audioUrl = uploadAudioUrl;
else if(audioSrc==='prev' && p.video){ audioUrl = p.short_audio_url || ''; }
// reuse_audio explicitly: only when user chose "giữ nguyên audio short trước"
const reuseAudio = hotMode ? false : (audioSrc === 'prev');
// collect selected slide indices (recreate mode)
let slideIndices = [];
if(useSlides){
document.querySelectorAll('#sc-slide-picker .sc-slide-chk:checked').forEach(chk=>{
slideIndices.push(parseInt(chk.dataset.idx));
});
if(!slideIndices.length){
st.textContent = '⚠️ Chọn ít nhất 1 slide để tạo lại.';
throw new Error('Chọn ít nhất 1 slide');
}
}
// reuse_audio is sent explicitly above; music (audio_music) is overlaid by the backend
const body = {
post_id: p.id,
use_slides: useSlides,
slide_indices: slideIndices,
reuse_audio: reuseAudio,
voice: hotMode ? 'vi-VN-HoaiMyNeural' : document.getElementById('sc-voice').value,
emotion: hotMode ? 'neutral' : document.getElementById('sc-emotion').value,
speed: hotMode ? 1.0 : (parseFloat(document.getElementById('sc-speed').value) || 1.2),
audio_music: musicId,
audio_url: audioUrl,
};
if(hotMode){
// create a brand-new wall post on the AI wall
body.create_new = true;
const hotTitleEl = document.getElementById('sc-hot-title');
body.title = hotTitleEl ? hotTitleEl.value.trim() : '';
// scraped video title as fallback wall title
if(!body.title){
const meta = document.getElementById('sc-video-meta');
body.title = (meta && meta.dataset.title) ? meta.dataset.title : '🔥 Short HOT';
}
body.text = '';
body.language = 'vi';
const srcEl = document.getElementById('sc-video-link');
if(srcEl && srcEl.value.trim()) body.url = srcEl.value.trim();
}
// if uploaded video selected -> use it as background via images? No:
if(bgMode==='video' && videoUrl){
body.video_url = videoUrl; // backend handles
const vmute = document.getElementById('sc-video-mute');
if(vmute && vmute.checked) body.video_muted = true;
} else if(bgMode==='link'){
// scraped video from link: video-first + image tail completion
const scDirect = document.getElementById('sc-scrape-direct') ? document.getElementById('sc-scrape-direct').value : '';
if(scDirect){
body.video_url = scDirect;
body.scrape_mode = true; // video-first assembly
const vdur = document.getElementById('sc-scrape-duration') ? document.getElementById('sc-scrape-duration').value : '';
if(vdur) body.video_dur = parseFloat(vdur) || undefined;
const lmute = document.getElementById('sc-link-mute');
if(lmute && lmute.checked) body.video_muted = true;
// still send the chosen images (they complete the short if the
// video is shorter than the voice)
const chosen = document.getElementById('sc-chosen-img') ? document.getElementById('sc-chosen-img').value : '';
const imgs = [];
if(chosen) imgs.push(chosen);
if(imgUrl) imgs.push(imgUrl);
if(imgs.length) body.images = imgs;
if(chosen) body.fixed_image = chosen;
} else {
st.textContent = '⚠️ Bấm "📥 Lấy video" để lấy video từ link trước khi tạo.';
throw new Error('Chưa lấy được video từ link');
}
} else {
// 1. ảnh chọn từ danh sách rewrite (áp dụng cho nhiều slide, fix nền cho cả short)
// 2. ảnh URL nhập tay / tải lên
const chosen = document.getElementById('sc-chosen-img') ? document.getElementById('sc-chosen-img').value : '';
const imgs = [];
if(chosen) imgs.push(chosen);
if(imgUrl) imgs.push(imgUrl);
if(imgs.length) body.images = imgs; // backend: first = fixed bg (normal mode)
if(chosen) body.fixed_image = chosen; // applies to slides mode too (1 ảnh cho nhiều slide)
}
st.textContent = '⏳ Đang tạo video short (có thể mất 1-2 phút)...';
const r2 = await fetch('/api/ai-short', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)});
const j2 = await r2.json();
if(!r2.ok || j2.error) throw new Error(j2.error || 'Lỗi tạo video');
toast('✅ Đã tạo Short AI!');
const pp = _wallPosts.find(x => String(x.id) === String(p.id));
if(pp){
pp.video = j2.video;
pp.voice = j2.voice; pp.emotion = j2.emotion; pp.short_speed = j2.speed;
pp.short_music = j2.music; pp.short_audio_url = audioUrl; pp.short_use_slides = j2.use_slides;
pp.short_images = j2.images;
}
if(hotMode && j2.post){
// 🆕 brand-new wall post created by the backend -> prepend on the AI wall
prependWallPost(j2.post);
} else {
const itemId = 'wall-item-'+p.id;
const el = document.getElementById(itemId);
if(el){
const idx = _wallPosts.indexOf(pp);
el.insertAdjacentHTML('afterend', makeWallItem(pp, idx));
el.remove();
}
}
document.getElementById('short-creator-overlay').remove();
}catch(e){
st.textContent = '❌ ' + e.message;
btn.disabled = false; btn.textContent = hotMode ? '🔥 Thêm Short HOT' : '🎬 Tạo Short AI';
toast('❌ ' + e.message);
}
});
});
}
/* legacy fast path — used by any remaining tts-create-btn handlers */
async function makeShortVideo(postId, btn, voice, speed, emotion){
openShortCreator(postId);
return;
}
var VOICE_LIST = [
{id:'vi-VN-HoaiMyNeural', label:'🎙️ Hoài My (VI)', lang:'vi'},
{id:'vi-VN-NamMinhNeural', label:'🎙️ Nam Minh (VI)', lang:'vi'},
{id:'en-US-AndrewMultilingualNeural', label:'🎙️ Andrew (EN)', lang:'en'},
{id:'en-AU-WilliamMultilingualNeural', label:'🎙️ William (EN)', lang:'en'},
{id:'pt-BR-ThalitaMultilingualNeural', label:'🎙️ Thalita (PT)', lang:'pt'},
{id:'fr-FR-VivienneMultilingualNeural', label:'🎙️ Vivienne (FR)', lang:'fr'},
{id:'fr-FR-RemyMultilingualNeural', label:'🎙️ Rémy (FR)', lang:'fr'},
{id:'de-DE-SeraphinaMultilingualNeural', label:'🎙️ Seraphina (DE)', lang:'de'},
{id:'de-DE-FlorianMultilingualNeural', label:'🎙️ Florian (DE)', lang:'de'},
{id:'ko-KR-HyunsuMultilingualNeural', label:'🎙️ Hyunsu (KO)', lang:'ko'},
{id:'it-IT-GiuseppeMultilingualNeural', label:'🎙️ Giuseppe (IT)', lang:'it'},
];
var EMOTION_LIST = [
{id:'neutral', label:'😐 Trung tính'},
{id:'happy', label:'😊 Vui vẻ'},
{id:'excited', label:'🔥 Hào hứng'},
{id:'sad', label:'😢 Buồn'},
{id:'humorous', label:'😂 Hài hước'},
{id:'serious', label:'⚠️ Nghiêm túc'},
{id:'urgent', label:'🚨 Khẩn cấp'},
{id:'warm', label:'💖 Ấm áp'},
];
document.addEventListener('click',function(e){
var btn = e.target.closest('.tts-voice-btn');
if(btn){
var container = btn.closest('.tts-selector');
if(container){
var selKey = 'inline-'+container.dataset.postId;
if(!_ttsSelections[selKey]) _ttsSelections[selKey]={voice:btn.dataset.voice,emotion:'neutral'};
var allBtns = container.querySelectorAll('.tts-voice-btn');
for(var i=0;i<allBtns.length;i++){allBtns[i].style.borderColor='#333';allBtns[i].style.background='#222';allBtns[i].classList.remove('selected');}
btn.style.borderColor='#5cb87a';btn.style.background='#1a2a1f';btn.classList.add('selected');
_ttsSelections[selKey].voice = btn.dataset.voice;
}
return;
}
var ebtn = e.target.closest('.tts-emotion-btn');
if(ebtn){
var container = ebtn.closest('.tts-selector');
if(container){
var selKey = 'inline-'+container.dataset.postId;
if(!_ttsSelections[selKey]) _ttsSelections[selKey]={voice:'vi-VN-HoaiMyNeural',emotion:ebtn.dataset.emotion};
var allBtns = container.querySelectorAll('.tts-emotion-btn');
for(var i=0;i<allBtns.length;i++){allBtns[i].style.borderColor='#333';allBtns[i].style.background='#222';allBtns[i].classList.remove('selected');}
ebtn.style.borderColor='#5cb87a';ebtn.style.background='#1a2a1f';ebtn.classList.add('selected');
_ttsSelections[selKey].emotion = ebtn.dataset.emotion;
}
return;
}
var cbtn = e.target.closest('.tts-create-btn');
if(cbtn){
var container = cbtn.closest('.tts-selector');
if(container){
var selKey = 'inline-'+container.dataset.postId;
var selVoice = _ttsSelections[selKey] ? _ttsSelections[selKey].voice : 'vi-VN-HoaiMyNeural';
var selEmotion = _ttsSelections[selKey] ? _ttsSelections[selKey].emotion : 'neutral';
var speedSel = container.querySelector('.tts-speed');
var speed = speedSel ? parseFloat(speedSel.value)||1.2 : 1.2;
window.makeShortVideo(container.dataset.postId, cbtn, selVoice, speed, selEmotion);
}
return;
}
});
function detectLanguage(text){
if(!text) return 'vi';
var t=text.toLowerCase(), chars=new Set(t);
var vnChars='đăâêôơưàảãạáằẳẵặắầẩẫậấèẻẽẹéềễểệếìỉĩịíòỏõọóồổỗộốờởỡợớùủũụúừửữựứỳỷỹỵý';
var vnCount=0; for(var c of vnChars){if(chars.has(c)) vnCount++;}
if(vnCount>=2) return 'vi';
if(chars.has('ñ')||chars.has('¿')||chars.has('¡')) return 'es';
if(chars.has('ã')||chars.has('õ')) return 'pt';
var words=t.split(/\s+/);
var enWords=['the','is','at','which','on','and','or','but','this','that','with','from','have','been'];
var enCount=words.filter(function(w){return enWords.indexOf(w)>=0;}).length;
if(enCount>=2) return 'en';
return 'vi';
}
function detectEmotion(text){
if(!text) return 'neutral';
var t=text.toLowerCase();
var kws={
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'],
excited:['hào hứng','phấn khích','đột phá','kỷ lục','đỉnh cao','emocionante','increíble','excited','thrilling','unbelievable','awesome','breakthrough'],
sad:['buồn','đau','mất','thảm họa','khủng hoảng','triste','terrible','sad','unhappy','tragic','painful','death'],
humorous:['hài hước','buồn cười','haha','đùa','engraçado','gracioso','funny','hilarious','joke','lol'],
serious:['nghiêm trọng','khẩn cấp','quan trọng','lo ngại','sério','crítico','serious','critical','urgent','severe','crisis'],
urgent:['khẩn cấp','báo động','ngay lập tức','urgent','breaking','alert','emergency'],
warm:['ấm áp','tình cảm','yêu thương','warm','love','heart','touching']
};
var bestScore=0, bestEmotion='neutral';
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;}}
return bestEmotion;
}
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';}
function buildVoiceEmotionSelector(post){
var lang=post.language||detectLanguage(post.title+' '+(post.text||''));
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'};
var _postVoice = post.voice ? (_oldVoiceMap[post.voice] || post.voice) : '';
var autoVoice= _postVoice || getAutoVoice(lang);
var autoEmotion=post.emotion||detectEmotion(post.title+' '+(post.text||''));
var selKey = 'inline-'+post.id;
if(!_ttsSelections[selKey]){_ttsSelections[selKey] = {voice: autoVoice, emotion: autoEmotion};}
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">';
h+='<div style="font-size:11px;color:#888;margin-bottom:6px">🎙️ Giọng đọc (ngôn ngữ: '+lang.toUpperCase()+'):</div><div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">';
VOICE_LIST.forEach(function(v){var sel=v.id===_ttsSelections[selKey].voice?'border-color:#5cb87a;background:#1a2a1f':'border-color:#333;background:#222';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>';});
h+='</div><div style="font-size:11px;color:#888;margin-bottom:6px">😊 Cảm xúc:</div><div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px">';
EMOTION_LIST.forEach(function(e){var sel=e.id===_ttsSelections[selKey].emotion?'border-color:#5cb87a;background:#1a2a1f':'border-color:#333;background:#222';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>';});
h+='</div><div style="display:flex;align-items:center;gap:6px;margin-bottom:8px"><span style="font-size:11px;color:#888">⚡ Tốc độ:</span>';
h+='<select class="tts-speed" style="background:#222;border:1px solid #333;color:#ccc;padding:3px 8px;border-radius:8px;font-size:10px"><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>';
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></div>';
return h;
}
window.showVoiceEmotionSelector=function(postId,title,text){
var overlay=document.createElement('div');
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';
var box=document.createElement('div');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';
var lang=detectLanguage(title+' '+text);var autoEmotion=detectEmotion(title+' '+text);
var h='<h3 style="color:#5cb87a;margin-bottom:12px;font-size:16px">🎬 Tạo Short AI (ngôn ngữ: '+lang.toUpperCase()+')</h3>';
h+='<div style="margin-bottom:12px"><div style="color:#aaa;font-size:11px;margin-bottom:6px">🎙️ Chọn giọng đọc:</div>';
VOICE_LIST.forEach(function(v){var sel=v.id===getAutoVoice(lang)?'border-color:#5cb87a;background:#1a2a1f':'border-color:#333;background:#222';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>';});
h+='</div><div style="margin-bottom:12px"><div style="color:#aaa;font-size:11px;margin-bottom:6px">😊 Chọn cảm xúc:</div>';
EMOTION_LIST.forEach(function(e){var sel=e.id===autoEmotion?'border-color:#5cb87a;background:#1a2a1f':'border-color:#333;background:#222';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>';});
h+='</div><div style="margin-bottom:12px"><div style="color:#aaa;font-size:11px;margin-bottom:6px">⚡ Tốc độ:</div>';
h+='<select id="ve-speed" style="background:#222;border:1px solid #333;color:#ccc;padding:6px 12px;border-radius:10px;font-size:11px"><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>';
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>';
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>';
h+='<div id="ve-status" style="color:#888;font-size:10px;margin-top:8px;display:none"></div>';
box.innerHTML=h;overlay.appendChild(box);document.body.appendChild(overlay);
var selectedVoice=getAutoVoice(lang),selectedEmotion=autoEmotion;
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;});});
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;});});
box.querySelector('#ve-cancel-btn').addEventListener('click',function(){overlay.remove();});
box.querySelector('#ve-create-btn').addEventListener('click',async function(){
this.disabled=true;this.textContent='⏳ Đang tạo...';
box.querySelector('#ve-status').style.display='block';box.querySelector('#ve-status').textContent='Đang tạo video shorts...';
try{
var speed=parseFloat(box.querySelector('#ve-speed').value)||1.2;
if(!_ttsSelections["inline-"+postId]) _ttsSelections["inline-"+postId]={voice:"vi-VN-HoaiMyNeural",emotion:"neutral"};
_ttsSelections["inline-"+postId].voice=selectedVoice;_ttsSelections["inline-"+postId].emotion=selectedEmotion;
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})});
var j=await r.json();
if(!r.ok||j.error) throw new Error(j.error||'Lỗi tạo video');
toast('✅ Đã tạo Short AI!');overlay.remove();
var p=_wallPosts.find(function(x){return String(x.id)===String(postId);});
if(p){p.video=j.video;p.voice=j.voice;p.emotion=j.emotion;}
}catch(e){this.disabled=false;this.textContent='🎬 Tạo Short';box.querySelector('#ve-status').textContent='❌ '+e.message;}
});
};
function prependWallPost(post){
_wallPosts.unshift(post);
const track=document.getElementById('ai-wall-track');
const wrap=document.getElementById('ai-wall-wrap');
const target=document.getElementById('ai-wall-under-compose');
if(target && (!track||!wrap)){
const newWrap=document.createElement('div');
newWrap.className='slider-wrap';newWrap.id='ai-wall-wrap';
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>`;
target.appendChild(newWrap);
const firstItem=newWrap.querySelector('.wall-item');
if(firstItem)firstItem.className='wall-item wall-item-new';
return;
}
if(track){
track.insertAdjacentHTML('afterbegin', makeWallItem(post, 0));
track.scrollTo({left:0,behavior:'smooth'});
}
}
let _wallPosts=[];
let _currentView='home';
let _currentEventId=null;
let _currentMatchUrl=null;
let _htPage=0,_htTopic='';
async function loadHotTopics(){let j=null;try{j=await _fetchWithTimeout('/api/hot_topics',8000);}catch(e){j={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('');}
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);}
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;const ctrl=new AbortController();setTimeout(()=>ctrl.abort(),4000);fetch('/api/article?url='+encodeURIComponent(s.url),{signal:ctrl.signal}).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>`;}}
function loadMoreHashtag(){_htPage++;const btn=document.getElementById('ht-more');if(btn){btn.disabled=true;btn.textContent='Đang tải...';}showHashtagSources(_htTopic,_htPage);}
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';}}}
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 d=await _fetchWithTimeout(ep,12000);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 tải hoặc hết thời gian</div>';}}
function bindMatchClicks(el){el.querySelectorAll('.match-detail').forEach(md=>{md.style.cursor='pointer';md.addEventListener('click',function(e){const statusA=this.querySelector('.status a');const teamA=this.querySelector('.teams a[href*="/tran-dau/"]');const a = statusA || teamA;if(a){e.preventDefault();e.stopPropagation();const href=a.getAttribute('href')||'';const m=href.match(/\/tran-dau\/(\d+)\//);if(m){const fullUrl=href.startsWith('http')?href:'https://bongda.com.vn'+href;openMatch(m[1],fullUrl);}}});});el.querySelectorAll('a').forEach(a=>{a.addEventListener('click',e=>{e.preventDefault();e.stopPropagation()});});}
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')}
function closeMatch(){document.getElementById('match-overlay').classList.remove('active');document.body.style.overflow=''}
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>'}}
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]))}
function showView(id){document.querySelectorAll('.view').forEach(v=>v.classList.remove('active'));document.getElementById(id)?.classList.add('active')}
function switchCat(id){document.querySelectorAll('.cat').forEach(c=>c.classList.remove('active'));document.querySelector(`[data-cat="${id}"]`)?.classList.add('active');document.querySelectorAll('.view').forEach(v=>v.classList.remove('active'));document.querySelectorAll('video').forEach(v=>{v.pause();if(v._hls){v._hls.destroy();v._hls=null}});document.querySelectorAll('iframe[data-yt-src]').forEach(f=>{f.src=''});if(id==='home')document.getElementById('view-home').classList.add('active');else if(id==='news-all'){document.getElementById('view-cat').classList.add('active');loadNewsTab()}else{document.getElementById('view-cat').classList.add('active');loadCat(id)}}
function toast(msg){let t=document.getElementById('progress-toast');if(t){t.textContent=msg;t.style.display='block';setTimeout(()=>{t.style.display='none'},3500)}}
// ===== doShare: COPY link to clipboard, then try native share as bonus =====
function doShare(title,url,img,postId){
var shareUrl;
var _base = (typeof SPACE!=='undefined' && SPACE) ? SPACE : location.origin;
if(postId){
shareUrl = _base+'/s?post_id='+encodeURIComponent(postId)+'&title='+encodeURIComponent(title);
} else {
shareUrl = _base+'/s?url='+encodeURIComponent(url)+'&title='+encodeURIComponent(title)+'&img='+encodeURIComponent(img||'');
}
// Try clipboard API first (modern, works on HTTPS)
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(shareUrl).then(function(){
toast('📋 Đã sao chép link!');
try{if(navigator.share)navigator.share({title:title||'',url:shareUrl}).catch(function(){});}catch(e){}
}).catch(function(){
// Fallback: execCommand (deprecated but works on some browsers)
try{
var ta=document.createElement('textarea');
ta.value=shareUrl;ta.style.position='fixed';ta.style.left='-9999px';ta.style.top='-9999px';ta.style.opacity='0';
document.body.appendChild(ta);ta.select();ta.setSelectionRange(0,99999);
if(document.execCommand('copy')){toast('📋 Đã sao chép link!');}
else{prompt('📋 Sao chép link:', shareUrl);}
document.body.removeChild(ta);
}catch(e){prompt('📋 Sao chép link:', shareUrl);}
try{if(navigator.share)navigator.share({title:title||'',url:shareUrl}).catch(function(){});}catch(e){}
});
} else {
// execCommand approach
try{
var ta=document.createElement('textarea');
ta.value=shareUrl;ta.style.position='fixed';ta.style.left='-9999px';ta.style.top='-9999px';ta.style.opacity='0';
document.body.appendChild(ta);ta.select();ta.setSelectionRange(0,99999);
if(document.execCommand('copy')){toast('📋 Đã sao chép link!');}
else{prompt('📋 Sao chép link:', shareUrl);}
document.body.removeChild(ta);
}catch(e){prompt('📋 Sao chép link:', shareUrl);}
try{if(navigator.share)navigator.share({title:title||'',url:shareUrl}).catch(function(){});}catch(e){}
}
}
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;}}
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};}}
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[];}}
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[];}}
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||'')}','','${esc(opts.postId||'')}')"><div class="icon">📤</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();toggle169View('${esc(opts.videoId)}','169-toggle-${opts.idx}')"><div class="icon" id="169-toggle-${opts.idx}">🖥️</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>`;}
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);}}
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);}}
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);}
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);}}
async function toggleComments(videoId,idx){const panel=document.getElementById('cmt-inline-'+idx);if(!panel)return;if(panel.style.display!=='none'){panel.style.display='none';/* Restore snap alignment */var sl=panel.closest('.tiktok-slide');if(sl)sl.style.scrollSnapAlign='start';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);/* Remove snap so feed can scroll to show comment input */var sl=panel.closest('.tiktok-slide');if(sl)sl.style.scrollSnapAlign='';setTimeout(function(){var feed=document.getElementById('tiktok-feed');if(feed){var slidePos=sl?sl.offsetTop:0;feed.scrollTop=slidePos+200;}/* Focus input */var inp=document.getElementById('cmt-input-'+idx);if(inp)inp.focus();},300);}
function renderInlineComments(panel,videoId,idx,cmts){let h='<div class="inline-cmt-header"><span>💬 Bình luận</span><button onclick="toggleComments(\''+esc(videoId)+'\','+idx+')">✕</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;}
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);/* Keep focus on input after submit */var inp2=document.getElementById('cmt-input-'+idx);if(inp2)inp2.focus();}
function toggle169View(videoId,iconId){const slide=videoId?document.querySelector(`.tiktok-slide[data-vid="${videoId}"]`):null;if(slide){slide.classList.toggle('ratio-wide');const iconEl=iconId?document.getElementById(iconId):null;if(iconEl)iconEl.textContent=slide.classList.contains('ratio-wide')?'📺':'🖥️';else{const btn=slide.querySelector('.tiktok-right-btn .icon');if(btn)btn.textContent=slide.classList.contains('ratio-wide')?'📺':'🖥️';}return}document.querySelectorAll('.tiktok-slide.ratio-wide').forEach(s=>s.classList.remove('ratio-wide'));document.querySelectorAll('.tiktok-slide').forEach(s=>s.classList.add('ratio-wide'));document.querySelectorAll('.tiktok-right-btn .icon').forEach(b=>{if(b.textContent==='🖥️')b.textContent='📺';})}
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)}
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||'',postId:'',extraBtn:`<button class="tiktok-right-btn" onclick="event.stopPropagation();openHighlightFeed('${league}',${(i+1)%ordered.length})"><div class="icon">⏭️</div></button>`})});h+='</div></div>';el.innerHTML=h;setTimeout(()=>initTikTokFeed(),200);}
async function openYTShortsFeed(idx){showView('view-tiktok');const el=document.getElementById('view-tiktok');el.innerHTML='<div class="loading">Đã xóa Shorts Dân trí/SKĐS</div>';}
async function openShortAIFeed(idx){showView('view-tiktok');const el=document.getElementById('view-tiktok');if(!_wallPosts||!_wallPosts.length){el.innerHTML='<div class="loading">Không có Short AI</div>';return}const aiPosts=_wallPosts.filter(p=>p.video);if(!aiPosts.length||idx>=aiPosts.length){el.innerHTML='<div class="loading">Không có Short AI</div>';return}const ordered=aiPosts.slice(idx).concat(aiPosts.slice(0,idx));let h=`<button class="back-btn" onclick="switchCat('home')">← Tường AI</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">`;ordered.forEach((p,i)=>{const baseIdx=_wallPosts.indexOf(p);const vtag=p.video?`<video playsinline preload="none" src="${esc(p.video)}" loop controls></video>`:'';h+=buildTikTokSlide({vtag,title:p.title,badge:'Short AI',badgeClass:'badge-ai',videoId:p.id||'ai-'+i,idx:i,total:ordered.length,shareUrl:p.video||'',postId:p.id||'',extraBtn:`<button class="tiktok-right-btn" onclick="event.stopPropagation();shareVideoToApps('${esc(p.video)}','${esc(p.title)}')"><div class="icon">📥</div></button>`})});h+='</div></div>';el.innerHTML=h;setTimeout(()=>initTikTokFeed(),200);}
function readWallPost(idx){const p=_wallPosts&&_wallPosts[idx];if(!p)return;if(p.slides&&p.slides.length){readSlidePost(idx);return}readArticle(p.url||'','','',p.title,p.text);}
/** Show rewrite slide viewer - vertical slides with text+image */
function readSlidePost(idx){const p=_wallPosts[idx];if(!p||!p.slides)return;showView('view-article');const el=document.getElementById('view-article');let h=`<button class="back-btn" onclick="switchCat('home')">← Tường AI</button><div class="slide-viewer" style="padding:12px;max-width:600px;margin:0 auto">`;p.slides.forEach((s,i)=>{h+=`<div class="slide-card" style="background:#1a1a1a;border:1px solid #2a2a2a;border-radius:12px;padding:16px;margin-bottom:12px"><div class="slide-num" style="color:#5cb87a;font-size:12px;font-weight:700;margin-bottom:6px">Slide ${s.index||i+1}/${p.slides.length}</div>${s.image?`<img src="${_proxyImg(s.image)}" style="display:block;width:100%;height:auto;max-width:100%;border-radius:8px;margin:0 auto 8px" loading="lazy" onerror="this.style.display=\'none\'">`:''}<p style="color:#ddd;font-size:14px;line-height:1.6;margin:0">${esc(s.text)}</p></div>`;});h+=`</div><div class="article-actions" style="padding:12px;text-align:center;flex-wrap:wrap;gap:8px;display:flex;justify-content:center"><button onclick="doShare('${esc(p.title)}','${esc(p.url||'')}','${esc(p.img||'')}','${esc(p.id||'')}')" style="background:#2d8659;border:0;color:#fff;padding:8px 16px;border-radius:10px;cursor:pointer;font-size:12px">📤 Chia sẻ bài viết này</button><button onclick="openSlideDesigner(${idx})" style="background:#1a3a5a;border:1px solid #3a5a7a;color:#fff;padding:8px 16px;border-radius:10px;cursor:pointer;font-size:12px">🎨 Thiết kế ảnh</button></div>`;el.innerHTML=h;}
/** Slide Designer Modal - design a slide image with high-contrast text on background */
function designerGetRatio(){const v=document.getElementById('designer-ratio')?.value||'3:4';const m=v.split(':');return m.length===2?{w:parseInt(m[0]),h:parseInt(m[1])}:1;}
function designerGetLayout(){return document.getElementById('designer-layout')?.value||'solid';}
function designerDebounce(fn,d){clearTimeout(fn._t);fn._t=setTimeout(fn,d);}
function openSlideDesigner(idx){
designerSlideIdx=idx;
const p=_wallPosts[idx];if(!p||!p.slides)return;
const overlay=document.createElement('div');overlay.id='slide-designer-overlay';overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.92);z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px;overflow-y:auto';
let h='<div style="background:#1a1a1a;border:2px solid #5cb87a;border-radius:16px;padding:20px;max-width:720px;width:100%;max-height:90vh;overflow-y:auto">';
h+='<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px"><h3 style="color:#5cb87a;margin:0;font-size:16px">🎨 Thiết kế ảnh slide</h3><button onclick="document.getElementById(\'slide-designer-overlay\').remove()" style="background:none;border:0;color:#fff;font-size:24px;cursor:pointer">✕</button></div>';
h+='<div style="margin-bottom:12px"><label style="color:#888;font-size:11px;display:block;margin-bottom:4px">Chọn slide:</label><select id="designer-slide-select" style="width:100%;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:12px">';
p.slides.forEach((s,i)=>{h+=`<option value="${i}">Slide ${s.index||i+1}: ${(s.text||'').slice(0,60)}...</option>`;});
h+='</select></div>';
h+='<div style="margin-bottom:12px"><label style="color:#888;font-size:11px;display:block;margin-bottom:4px">Nội dung trên ảnh:</label><textarea id="designer-text" style="width:100%;min-height:80px;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:13px;resize:vertical">'+esc(p.slides[0].text)+'</textarea></div>';
h+='<div style="margin-bottom:12px"><label style="color:#888;font-size:11px;display:block;margin-bottom:4px">Tỉ lệ cắt ảnh nền:</label><select id="designer-ratio" style="width:100%;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:12px">';
h+='<option value="3:4">📐 3:4 (dọc)</option><option value="1:1">📐 1:1 (vuông)</option><option value="4:3">📐 4:3 (ngang)</option>';
h+='</select></div>';
h+='<div style="margin-bottom:12px"><label style="color:#888;font-size:11px;display:block;margin-bottom:4px">Bố cục nền:</label><select id="designer-layout" style="width:100%;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:12px">';
h+='<option value="solid">🎨 Nền đẳng (phủ màu)</option>';
h+='<option value="vignette">🌙 Vignette (viền tối dần)</option>';
h+='<option value="split">✂️ Chia đôi (trên mờ, dưới rõ)</option>';
h+='<option value="spotlight">🎯 Spotlight (ánh sáng giữa)</option>';
h+='<option value="diagonal">↘️ Đường chéo (góc trái-trên→phải-dưới)</option>';
h+='</select></div>';
h+='<div style="margin-bottom:12px"><label style="color:#888;font-size:11px;display:block;margin-bottom:4px">Màu chữ:</label><div style="display:flex;gap:6px;flex-wrap:wrap">';
['#ffffff','#000000','#ff4444','#44ff44','#4444ff','#ffff00','#ff00ff','#00ffff','#ff8800','#88ff00'].forEach(c=>{h+=`<button class="designer-color-btn" data-color="${c}" style="width:28px;height:28px;border-radius:50%;border:2px solid ${c==='#ffffff'?'#888':'#333'};background:${c};cursor:pointer" onclick="document.getElementById('designer-text').style.color=this.dataset.color;document.querySelectorAll('.designer-color-btn').forEach(b=>b.style.borderColor='#333');this.style.borderColor='#fff'"></button>`;});
h+='</div></div>';
h+='<div style="margin-bottom:12px"><label style="color:#888;font-size:11px;display:block;margin-bottom:4px">Ảnh nền (URL hoặc upload):</label><input id="designer-bg-url" type="text" value="'+(esc(p.slides[0].image||p.img||''))+'" placeholder="https://..." style="width:100%;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:12px;margin-bottom:6px"><input type="file" id="designer-bg-file" accept="image/*" style="font-size:11px;color:#888" onchange="handleDesignerBgFile(this);designerDebounce(previewDesignerSlide,150)"></div>';
h+='<div style="margin-top:16px;text-align:center"><button id="designer-preview-btn" style="background:#2d8659;border:0;color:#fff;padding:10px 20px;border-radius:10px;font-size:12px;font-weight:700;cursor:pointer;margin-right:8px">👁 Xem trưởng</button><button id="designer-save-btn" style="background:#1a3a5a;border:1px solid #3a5a7b;color:#fff;padding:10px 20px;border-radius:10px;font-size:12px;font-weight:700;cursor:pointer">💾 Lưu ảnh đăng tường</button></div>';
h+='<div id="designer-preview-area" style="margin-top:12px;text-align:center;display:none"></div>';
h+='<div id="designer-status" style="color:#888;font-size:11px;margin-top:8px;text-align:center"></div>';
overlay.innerHTML=h;document.body.appendChild(overlay);
document.getElementById('designer-slide-select').addEventListener('change',function(){updateDesignerText(this.value);previewDesignerSlide();});
document.getElementById('designer-preview-btn').addEventListener('click',function(){previewDesignerSlide();});
document.getElementById('designer-save-btn').addEventListener('click',function(){saveDesignerSlide(p.title,(p.id||''),idx);});
['change','input'].forEach(function(evt){
const txt=document.getElementById('designer-text');if(txt)txt.addEventListener(evt,function(){designerDebounce(previewDesignerSlide,150);});
const bg=document.getElementById('designer-bg-url');if(bg)bg.addEventListener(evt,function(){designerDebounce(previewDesignerSlide,150);});
const ratio=document.getElementById('designer-ratio');if(ratio)ratio.addEventListener(evt,function(){previewDesignerSlide();});
const layout=document.getElementById('designer-layout');if(layout)layout.addEventListener(evt,function(){previewDesignerSlide();});
});
previewDesignerSlide();
}
function updateDesignerText(slideIdx){const p=_wallPosts[designerSlideIdx];if(!p||!p.slides)return;const s=p.slides[parseInt(slideIdx)];if(s)document.getElementById('designer-text').value=s.text||'';document.getElementById('designer-bg-url').value=s.image||p.img||'';designerBgDataUrl=null;}
function handleDesignerBgFile(input){
const file=input.files&&input.files[0];if(!file)return;
const reader=new FileReader();
reader.onload=function(e){designerBgDataUrl=e.target.result;document.getElementById('designer-bg-url').value='[uploaded]';};
reader.readAsDataURL(file);
}
function previewDesignerSlide(){
const slideIdx=parseInt(document.getElementById('designer-slide-select')?.value||'0');
const p=_wallPosts[designerSlideIdx];if(!p||!p.slides)return;
const s=p.slides[slideIdx];if(!s)return;
const text=document.getElementById('designer-text')?.value||s.text||'';
const bgUrlInput=document.getElementById('designer-bg-url')?.value||'';
const bgUrl=designerBgDataUrl||bgUrlInput||s.image||p.img||'';
const textColor=document.getElementById('designer-text')?.style.color||'#ffffff';
const ratio=designerGetRatio();
const layout=designerGetLayout();
const previewArea=document.getElementById('designer-preview-area');if(!previewArea)return;
previewArea.style.display='block';previewArea.innerHTML='<div style="color:#888;font-size:12px">⏳ Đang tạo xem trưởng...</div>';
const canvas=document.createElement('canvas');
const vw=Math.max(document.documentElement.clientWidth||540, document.body.clientWidth||540);
const maxW=Math.min(540, Math.floor(vw*0.9));
canvas.width=maxW;canvas.height=Math.round(maxW*ratio.h/ratio.w);
canvas.style.maxWidth='100%';canvas.style.width='100%';canvas.style.height='auto';
canvas.style.borderRadius='12px';canvas.style.boxShadow='0 8px 24px rgba(0,0,0,.5)';
canvas.style.display='block';canvas.style.margin='0 auto';
const ctx=canvas.getContext('2d');const img=new Image();img.crossOrigin='anonymous';
function drawLayout(){
if(layout==='vignette'){const g=ctx.createRadialGradient(canvas.width/2,canvas.height/2,0,canvas.width/2,canvas.height/2,Math.max(canvas.width,canvas.height)*0.6);g.addColorStop(0,'rgba(0,0,0,0)');g.addColorStop(1,'rgba(0,0,0,0.7)');ctx.fillStyle=g;ctx.fillRect(0,0,canvas.width,canvas.height);}
else if(layout==='split'){ctx.fillStyle='rgba(0,0,0,0)';ctx.fillRect(0,0,canvas.width,canvas.height/2);ctx.fillStyle='rgba(0,0,0,0.7)';ctx.fillRect(0,canvas.height/2,canvas.width,canvas.height/2);}
else if(layout==='spotlight'){const cx=canvas.width/2,cy=canvas.height/2,r=Math.max(canvas.width,canvas.height)*0.35;ctx.save();ctx.fillStyle='rgba(0,0,0,0.7)';ctx.fillRect(0,0,canvas.width,canvas.height);ctx.globalCompositeOperation='destination-out';ctx.fillStyle='white';ctx.beginPath();ctx.arc(cx,cy,r,0,Math.PI*2);ctx.fill();ctx.globalCompositeOperation='source-over';}
else if(layout==='diagonal'){ctx.fillStyle='rgba(0,0,0,0.55)';ctx.fillRect(0,0,canvas.width,canvas.height);ctx.fillStyle='rgba(0,0,0,0.75)';ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(canvas.width,0);ctx.lineTo(0,canvas.height);ctx.closePath();ctx.fill();}
else {ctx.fillStyle='rgba(0,0,0,0.55)';ctx.fillRect(0,0,canvas.width,canvas.height);}
}
function drawText(){
ctx.fillStyle=textColor;ctx.font='bold '+Math.round(canvas.width*28/540)+'px sans-serif';
ctx.textAlign='center';ctx.textBaseline='middle';
const maxW=Math.round(canvas.width*0.82);
const words=text.split(' ');let line='';let y=Math.round(canvas.height/2);
const fontSize=Math.round(canvas.width*28/540);const lineHeight=Math.round(fontSize*1.3);
for(let i=0;i<words.length;i++){const test=line+' '+words[i];if(ctx.measureText(test).width>maxW&&i>0){ctx.fillText(line.trim(),canvas.width/2,y);y+=lineHeight;line=words[i];}else{line=test;}}
ctx.fillText(line.trim(),canvas.width/2,y);
}
function renderAll(){
ctx.fillStyle='#1a1a1a';ctx.fillRect(0,0,canvas.width,canvas.height);
drawLayout();
drawText();
previewArea.innerHTML='';previewArea.appendChild(canvas);
}
img.onload=function(){
const iw=img.naturalWidth||1,ih=img.naturalHeight||1;
const scale=Math.max(canvas.width/iw,canvas.height/ih);
const dw=iw*scale,dh=ih*scale;
const dx=(canvas.width-dw)/2,dy=(canvas.height-dh)/2;
ctx.drawImage(img,0,0,iw,ih,dx,dy,dw,dh);
drawLayout();drawText();
previewArea.innerHTML='';previewArea.appendChild(canvas);
};
img.onerror=function(){renderAll();};
if(bgUrl){img.src=bgUrl.startsWith('/api/')?bgUrl:bgUrl.startsWith('data:')?bgUrl:'/api/proxy/img?url='+encodeURIComponent(bgUrl);}else{renderAll();}
}
async function saveDesignerSlide(title,postId,idx){
const slideIdx=parseInt(document.getElementById('designer-slide-select')?.value||'0');
const p=_wallPosts[idx];if(!p||!p.slides)return;
const s=p.slides[slideIdx];if(!s)return;
const text=document.getElementById('designer-text')?.value||s.text||'';
const bgUrlInput=document.getElementById('designer-bg-url')?.value||'';
const bgUrl=designerBgDataUrl||bgUrlInput||s.image||p.img||'';
const textColor=document.getElementById('designer-text')?.style.color||'#ffffff';
const ratio=designerGetRatio();
const layout=designerGetLayout();
const statusEl=document.getElementById('designer-status');if(statusEl)statusEl.textContent='⏳ Đang tạo ảnh...';
const FINAL_W=ratio.w>=ratio.h?1800:1350;
const FINAL_H=Math.round(FINAL_W*ratio.h/ratio.w);
const canvas=document.createElement('canvas');canvas.width=FINAL_W;canvas.height=FINAL_H;
const ctx=canvas.getContext('2d');
const img=new Image();img.crossOrigin='anonymous';
function drawLayoutF(){
if(layout==='vignette'){const g=ctx.createRadialGradient(FINAL_W/2,FINAL_H/2,0,FINAL_W/2,FINAL_H/2,Math.max(FINAL_W,FINAL_H)*0.6);g.addColorStop(0,'rgba(0,0,0,0)');g.addColorStop(1,'rgba(0,0,0,0.7)');ctx.fillStyle=g;ctx.fillRect(0,0,FINAL_W,FINAL_H);}
else if(layout==='split'){ctx.fillStyle='rgba(0,0,0,0)';ctx.fillRect(0,0,FINAL_W,FINAL_H/2);ctx.fillStyle='rgba(0,0,0,0.7)';ctx.fillRect(0,FINAL_H/2,FINAL_W,FINAL_H/2);}
else if(layout==='spotlight'){const cx=FINAL_W/2,cy=FINAL_H/2,r=Math.max(FINAL_W,FINAL_H)*0.35;ctx.save();ctx.fillStyle='rgba(0,0,0,0.7)';ctx.fillRect(0,0,FINAL_W,FINAL_H);ctx.globalCompositeOperation='destination-out';ctx.fillStyle='white';ctx.beginPath();ctx.arc(cx,cy,r,0,Math.PI*2);ctx.fill();ctx.globalCompositeOperation='source-over';}
else if(layout==='diagonal'){ctx.fillStyle='rgba(0,0,0,0.55)';ctx.fillRect(0,0,FINAL_W,FINAL_H);ctx.fillStyle='rgba(0,0,0,0.75)';ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(FINAL_W,0);ctx.lineTo(0,FINAL_H);ctx.closePath();ctx.fill();}
else {ctx.fillStyle='rgba(0,0,0,0.55)';ctx.fillRect(0,0,FINAL_W,FINAL_H);}
}
function drawTextF(){
ctx.fillStyle=textColor;ctx.font='bold '+Math.round(FINAL_W*48/1080)+'px sans-serif';
ctx.textAlign='center';ctx.textBaseline='middle';
const maxW=Math.round(FINAL_W*0.82);const words=text.split(' ');let line='';let y=Math.round(FINAL_H/2);
const fontSize=Math.round(FINAL_W*48/1080);const lineHeight=Math.round(fontSize*1.3);
for(let i=0;i<words.length;i++){const test=line+' '+words[i];if(ctx.measureText(test).width>maxW&&i>0){ctx.fillText(line.trim(),FINAL_W/2,y);y+=lineHeight;line=words[i];}else{line=test;}}ctx.fillText(line.trim(),FINAL_W/2,y);
}
function render(){
ctx.fillStyle='#1a1a1a';ctx.fillRect(0,0,FINAL_W,FINAL_H);
drawLayoutF();drawTextF();
canvas.toBlob(async function(blob){
const formData=new FormData();formData.append('file',blob,'slide_'+slideIdx+'.png');formData.append('post_id',postId||p.id||'');
try{const r=await fetch('/api/wall/img',{method:'POST',body:formData});const j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi upload');
s.image=j.url;
const post={id:p.id,title:title||p.title,text:p.text,img:j.url,url:p.url,slides:p.slides,images:p.images,voice:p.voice,emotion:p.emotion,language:p.language,ts:p.ts,kind:p.kind||'slide_summary'};
const wr=await fetch('/api/wall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(post)});
const wj=await wr.json();if(wr.ok&&wj.post){prependWallPost(wj.post);toast('✅ Đã lưu ảnh đăng lên Tường AI!');}else{toast('✅ Đã lưu ảnh slide!');}
document.getElementById('slide-designer-overlay').remove();
}catch(e){if(statusEl)statusEl.textContent=''+e.message;toast(''+e.message);}
},'image/png');
}
img.onload=function(){
const iw=img.naturalWidth||1,ih=img.naturalHeight||1;
const scale=Math.max(FINAL_W/iw,FINAL_H/ih);
const dw=iw*scale,dh=ih*scale;
const dx=(FINAL_W-dw)/2,dy=(FINAL_H-dh)/2;
ctx.drawImage(img,0,0,iw,ih,dx,dy,dw,dh);
drawLayoutF();drawTextF();
canvas.toBlob(async function(blob){
const formData=new FormData();formData.append('file',blob,'slide_'+slideIdx+'.png');formData.append('post_id',postId||p.id||'');
try{const r=await fetch('/api/wall/img',{method:'POST',body:formData});const j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi upload');
s.image=j.url;
const post={id:p.id,title:title||p.title,text:p.text,img:j.url,url:p.url,slides:p.slides,images:p.images,voice:p.voice,emotion:p.emotion,language:p.language,ts:p.ts,kind:p.kind||'slide_summary'};
const wr=await fetch('/api/wall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(post)});
const wj=await wr.json();if(wr.ok&&wj.post){prependWallPost(wj.post);toast('✅ Đã lưu ảnh đăng lên Tường AI!');}else{toast('✅ Đã lưu ảnh slide!');}
document.getElementById('slide-designer-overlay').remove();
}catch(e){if(statusEl)statusEl.textContent=''+e.message;toast(''+e.message);}
},'image/png');
};
img.onerror=function(){if(statusEl)statusEl.textContent='⚠️ Không tải được ảnh nền, dùng nền mặc định';ctx.fillStyle='#1a1a1a';ctx.fillRect(0,0,FINAL_W,FINAL_H);drawLayoutF();drawTextF();canvas.toBlob(function(blob){
const formData=new FormData();formData.append('file',blob,'slide_'+slideIdx+'.png');formData.append('post_id',postId||p.id||'');
fetch('/api/wall/img',{method:'POST',body:formData}).then(r=>r.json()).then(j=>{
if(j.ok){s.image=j.url;const post={id:p.id,title:title||p.title,text:p.text,img:j.url,url:p.url,slides:p.slides,images:p.images,voice:p.voice,emotion:p.emotion,language:p.language,ts:p.ts,kind:p.kind||'slide_summary'};
fetch('/api/wall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(post)}).then(r=>r.json()).then(wj=>{if(wj.post)prependWallPost(wj.post);toast('✅ Đã lưu ảnh đăng lên Tường AI!');document.getElementById('slide-designer-overlay').remove();});}
}).catch(e=>{if(statusEl)statusEl.textContent=''+e.message;toast(''+e.message);});
},'image/png');};
if(bgUrl){img.src=bgUrl.startsWith('/api/')?bgUrl:bgUrl.startsWith('data:')?bgUrl:'/api/proxy/img?url='+encodeURIComponent(bgUrl);}else{render();}
}
function readNewsTab(tab){loadNewsTab();}
function loadNewsTab(){const el=document.getElementById('view-cat');if(!el)return;el.innerHTML='<div class="loading">Đang tải tin tức...</div>';fetch('/api/homepage').then(r=>r.json()).then(articles=>{if(!articles||!articles.length){el.innerHTML='<div class="loading">Không có tin</div>';return}let h='<div class="grid">';articles.forEach(a=>{const src=a.source||'vne';const badge=a.group||a.source||'';h+=`<div class="card" onclick="readArticle('${esc(a.link)}')"><div class="card-img">${a.img?`<img src="${_proxyImg(a.img)}" loading="lazy" onerror="this.style.display=\'none\'">`:''}</div><div class="card-body"><span class="badge badge-${src}">${esc(badge)}</span><div class="card-title">${esc(a.title)}</div></div></div>`;});h+='</div>';el.innerHTML=h;}).catch(()=>{el.innerHTML='<div class="loading">Lỗi tải</div>';});}
function readArticle(url,title,img,presetTitle,presetText){showView('view-article');const el=document.getElementById('view-article');el.innerHTML='<div class="loading">Đang tải...</div><button class="back-btn" onclick="switchCat(\'home\')">← Quay lại</button>';if(presetTitle){el.innerHTML=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="article-view"><h1 class="article-title">${esc(presetTitle)}</h1>${presetText?`<div class="article-summary" style="white-space:pre-wrap">${esc(presetText)}</div>`:''}</div><div class="article-actions"><button onclick="doShare('${esc(presetTitle)}','${esc(url||'')}','')">📤 Chia sẻ</button></div>`;return;}if(!url)return;fetch('/api/article?url='+encodeURIComponent(url)).then(r=>r.json()).then(d=>{let h=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="article-view">`;if(d.title)h+=`<h1 class="article-title">${esc(d.title)}</h1>`;if(d.summary)h+=`<div class="article-summary">${esc(d.summary)}</div>`;if(d.body)d.body.forEach(b=>{if(b.type==='p')h+=`<p class="article-p">${esc(b.text)}</p>`;else if(b.type==='heading')h+=`<h2 class="article-h2">${esc(b.text)}</h2>`;else if(b.type==='img'&&b.src)h+=`<img class="article-img" src="${_proxyImg(b.src)}" loading="lazy" onerror="this.style.display=\'none\'">`;});h+=`</div><div class="article-actions"><button onclick="doShare('${esc(d.title||'')}','${esc(url)}','${esc(d.og_image||'')}')">📤 Chia sẻ</button><button class="primary" onclick="rewriteSlide('${esc(url)}')">🤖 Slide Rewrite AI</button></div>`;el.innerHTML=h;}).catch(()=>{el.innerHTML=`<button class="back-btn" onclick="switchCat('home')">← Quay lại</button><div class="article-view"><p>Không thể tải bài viết</p></div>`;});}
async function rewriteSlide(url){if(!url)return;const btn=document.querySelector('.article-actions .primary')||event?.target;if(btn){btn.disabled=true;btn.textContent='⏳ Đang tạo slides...';}toast('⏳ Đang tạo slide rewrite...');try{const r=await fetch('/api/rewrite_share',{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||'Lỗi');toast('✅ Đã tạo slide! Đang tạo Short AI...');if(btn)btn.textContent='✅ Đang tạo Short AI...';if(j.post){j.post.slides=j.slides||[];prependWallPost(j.post);}// Show slides immediately if available
if(j.slides && j.slides.length){showView('view-article');const el=document.getElementById('view-article');let h=`<button class="back-btn" onclick="switchCat('home')">← Tường AI</button><div class="slide-viewer" style="padding:12px;max-width:600px;margin:0 auto">`;j.slides.forEach(s=>{h+=`<div class="slide-card" style="background:#1a1a1a;border:1px solid #2a2a2a;border-radius:12px;padding:16px;margin-bottom:12px"><div class="slide-num" style="color:#5cb87a;font-size:12px;font-weight:700;margin-bottom:6px">Slide ${s.index}/${j.slides.length}</div>${s.image?`<img src="${s.image.startsWith('/api/')?s.image:'/api/proxy/img?url='+encodeURIComponent(s.image)}" style="display:block;width:100%;height:auto;max-width:100%;border-radius:8px;margin:0 auto 8px" loading="lazy" onerror="this.style.display='none'">`:''}<p style="color:#ddd;font-size:14px;line-height:1.6;margin:0">${esc(s.text)}</p></div>`;});h+=`</div><div style="text-align:center;padding:12px"><button onclick="openSlideDesigner(typeof _wallPosts!=='undefined'?_wallPosts.indexOf(j.post):0)" style="background:#1a3a5a;border:1px solid #3a5a7a;color:#fff;padding:8px 16px;border-radius:10px;cursor:pointer;font-size:12px;margin-bottom:8px">🎨 Thiết kế ảnh</button><div id="short-ai-status" style="color:#888;font-size:12px">⏳ Đang tạo video Short AI...</div></div>`;el.innerHTML=h;}
const postId=j.post&&j.post.id;if(postId){setTimeout(async()=>{const sr=await fetch('/api/ai/short/'+encodeURIComponent(postId),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({voice:'vi-VN-HoaiMyNeural',emotion:'neutral',speed:1.2})});const sj=await sr.json();if(sr.ok&&sj.video&&typeof _wallPosts!=='undefined'){const p=_wallPosts.find(x=>String(x.id)===String(postId));if(p){p.video=sj.video;const itemId='wall-item-'+postId;const el2=document.getElementById(itemId);if(el2){const idx=_wallPosts.indexOf(p);el2.insertAdjacentHTML('afterend',makeWallItem(p,idx));el2.remove();}}toast('✅ Short AI đã sẵn sàng!');const statusEl=document.getElementById('short-ai-status');if(statusEl)statusEl.innerHTML='✅ Short AI đẵn sàng! <button class="primary" onclick="openShortAIFeed(0)" style="background:#2d8659;border:0;color:#fff;padding:6px 12px;border-radius:10px;font-size:11px;cursor:pointer;margin-left:8px">▶ Xem ngay</button>';}},500);}if(btn)btn.textContent='✅ Hoàn tất';}catch(e){toast(''+e.message);if(btn){btn.disabled=false;btn.textContent='🤖 Slide Rewrite AI';}}}
function downloadVideo(url, title){
var a = document.createElement('a');
a.href = url;
a.download = (title||'video').toString().replace(/[^a-zA-Z0-9_\-\p{L}]/gu,'_').substring(0,60)+'.mp4';
a.target = '_blank';
a.rel = 'noopener';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
toast('📥 Đang tải video xuống...');
}
// ===== PERSONAL OPINION POST: Viết bài dựa trên quan điểm cá nhân + tin HOT =====
async function openPersonalPostPreview() {
const opinion = document.getElementById('opinion-input')?.value.trim();
if (!opinion || opinion.length < 10) {
alert('Vui lòng nhập quan điểm cá nhân (ít nhất 10 ký tự)');
return;
}
// Get selected hot topics from UI (if any)
const selectedTopics = [];
if (window._htTopic) {
selectedTopics.push(window._htTopic);
}
// Get selected sources from hashtag view
const selectedSources = [];
document.querySelectorAll('.hashtag-src-item.selected').forEach(el => {
const idx = parseInt(el.dataset.idx || '0');
// Would need source tracking
});
const btn = event?.target;
const origText = btn ? btn.textContent : '';
if (btn) { btn.disabled = true; btn.textContent = '⏳ Đang tạo preview...'; }
try {
const resp = await fetch('/api/personal_post/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
opinion: opinion,
selected_topics: selectedTopics,
selected_sources: selectedSources
})
});
const data = await resp.json();
if (!resp.ok || data.error) throw new Error(data.error || 'Lỗi tạo preview');
showPersonalPostModal(data.preview, opinion, selectedTopics, selectedSources);
} catch (e) {
toast('' + e.message);
} finally {
if (btn) { btn.disabled = false; btn.textContent = origText; }
}
}
function showPersonalPostModal(preview, originalOpinion, selectedTopics, selectedSources) {
// Remove existing modal
const existing = document.getElementById('personal-post-modal');
if (existing) existing.remove();
const modal = document.createElement('div');
modal.id = 'personal-post-modal';
modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.9);z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px;overflow-y:auto';
// Each slide renders EXACTLY like the designer's final PNG (same ratio,
// same bg draw, same text draw) — preview = pixel-identical to wall post image.
const slidesHtml = (preview.slides || []).map((s, i) => {
const ratio = {w:3, h:4}; // preview always 3:4 (same as designer final)
const slideStyle = 'width:100%;max-width:280px;margin:0 auto;display:block;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.5)';
return '<div class="preview-slide" style="background:#1a1a1a;border:1px solid #2a2a2a;border-radius:12px;padding:16px;margin-bottom:12px;max-width:600px;margin-left:auto;margin-right:auto">'
+ '<div style="margin-bottom:6px;color:#5cb87a;font-size:12px;font-weight:700">Slide ' + (i+1) + '</div>'
+ '<canvas class="preview-slide-canvas" data-idx="' + i + '" style="' + slideStyle + '"></canvas>'
+ '<textarea class="preview-slide-text" data-index="' + i + '" data-idx="' + i + '" style="width:100%;min-height:80px;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px;font-size:13px;margin-top:8px;resize:vertical;font-family:inherit">' + (s.text || '').replace(/"/g, '"') + '</textarea>'
+ '</div>';
}).join('');
// Background image source: uploaded data-URL (set later) or slide.image / first source image.
// We keep a small state object so text edits re-render live.
window._pvState = { slides: (preview.slides||[]).map(function(s,i){
var bg = s.image || (preview.images && preview.images[0]) || '';
return { text: s.text||'', bg: bg };
}), ratio: 3/4, layout: 'solid', filter: 'none', glow: 'white', pos: 'bottom', color: '#ffffff' };
const sourcesHtml = (preview.sources || []).map((s, i) => {
const imgSrc = (preview.images && preview.images[i+1]) ? preview.images[i+1] : '';
return '<div style="display:flex;gap:8px;padding:8px;background:#202020;border-radius:8px;margin:4px 0;cursor:pointer" onclick="toggleSourceSelection(this, ' + i + ')" data-source-idx="' + i + '" data-url="' + (s.url || '') + '">'
+ '<div style="flex:0 0 60px;aspect-ratio:16/9;background:#333;border-radius:4px;overflow:hidden">'
+ (imgSrc ? '<img src="' + imgSrc + '" style="width:100%;height:100%;object-fit:cover" onerror="this.style.display=\'none\'">' : '')
+ '</div>'
+ '<div style="flex:1;min-width:0;font-size:11px;color:#ddd">' + (s.title || '') + '</div>'
+ '<input type="checkbox" class="source-checkbox" style="margin-top:4px" ' + (i < 3 ? 'checked' : '') + '>'
+ '</div>';
}).join('');
modal.innerHTML = '<div style="background:#1a1a1a;border:2px solid #2d8659;border-radius:16px;padding:20px;max-width:700px;width:100%;max-height:90vh;overflow-y:auto">'
+ '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">'
+ '<h3 style="color:#5cb87a;margin:0;font-size:16px">📝 Xem trước bài quan điểm cá nhân</h3>'
+ '<button onclick="this.closest(\'#personal-post-modal\').remove()" style="background:none;border:0;color:#fff;font-size:24px;cursor:pointer">✕</button>'
+ '</div>'
+ '<div style="margin-bottom:12px">'
+ '<label style="display:block;color:#888;font-size:11px;margin-bottom:4px">Tiêu đề:</label>'
+ '<input id="preview-title" value="' + (preview.title || '').replace(/"/g, '"') + '" style="width:100%;background:#222;border:1px solid #333;color:#eee;border-radius:8px;padding:8px 12px;font-size:14px;font-weight:700">'
+ '</div>'
+ '<div style="margin-bottom:12px">'
+ '<label style="display:block;color:#888;font-size:11px;margin-bottom:4px">Quan điểm gốc:</label>'
+ '<textarea id="preview-opinion" readonly style="width:100%;min-height:60px;background:#141414;border:1px solid #2a2a2a;color:#aaa;border-radius:8px;padding:8px 12px;font-size:12px;resize:vertical">' + originalOpinion.replace(/"/g, '"') + '</textarea>'
+ '</div>'
+ '<div style="margin-bottom:4px">'
+ '<label style="display:block;color:#888;font-size:11px;margin-bottom:6px">🖼️ Các slide — bản xem trước hiển thị ĐÚNG 100% như ảnh thiết kế (chữ + nền):</label>'
+ '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px">'
+ '<button onclick="openPreviewDesigner()" style="background:#1a3a5a;border:1px solid #3a5a7a;color:#fff;padding:8px 12px;border-radius:10px;font-size:12px;font-weight:700;cursor:pointer">🎨 Thiết kế ảnh (canvas)</button>'
+ '<button onclick="previewRedrawAllSlides()" style="background:#333;border:0;color:#ddd;padding:8px 12px;border-radius:10px;font-size:11px;cursor:pointer">🔄 Vẽ lại</button>'
+ '</div>'
+ '<div style="max-height:440px;overflow-y:auto">' + slidesHtml + '</div>'
+ '</div>'
+ '<div style="margin-bottom:12px">'
+ '<label style="display:block;color:#888;font-size:11px;margin-bottom:8px">Nguồn tin tham khảo (bỏ chọn để loại):</label>'
+ '<div style="max-height:200px;overflow-y:auto">'
+ (sourcesHtml || '<div style="color:#777;font-size:12px;padding:8px">Không có nguồn tin</div>')
+ '</div></div>'
+ '<div style="display:flex;gap:8px">'
+ '<button onclick="publishPersonalPostFromModal()" style="flex:1;background:#2d8659;border:0;color:#fff;padding:12px;border-radius:12px;font-size:13px;font-weight:700;cursor:pointer">✅ Đăng lên Tường AI</button>'
+ '<button onclick="this.closest(\'#personal-post-modal\').remove()" style="background:#333;border:0;color:#ccc;padding:12px 16px;border-radius:12px;font-size:12px;cursor:pointer">Hủy</button>'
+ '</div></div>';
document.body.appendChild(modal);
// draw every slide preview canvas
previewRedrawAllSlides();
// text edits re-render live
modal.querySelectorAll('.preview-slide-text').forEach(ta => {
ta.addEventListener('input', function(){
const idx = parseInt(this.dataset.idx || '0', 10);
if(window._pvState && window._pvState.slides[idx]) window._pvState.slides[idx].text = this.value;
previewDrawSlide(idx);
});
});
// Store preview data for publishing
window._personalPostPreview = preview;
window._personalPostOpinion = originalOpinion;
}
/* Live-edit slide preview canvas (exact same rendering style as designer output) */
function previewDrawSlide(idx){
const canvas = document.querySelector('.preview-slide-canvas[data-idx="'+idx+'"]');
if(!canvas || !window._pvState) return;
const st = window._pvState.slides[idx];
if(!st) return;
const W = 600, H = Math.round(W * window._pvState.ratio); // 3:4 -> 800
canvas.width = W; canvas.height = H;
const ctx = canvas.getContext('2d');
const opts = { text: st.text, pos: window._pvState.pos, glow: window._pvState.glow,
color: window._pvState.color, layout: window._pvState.layout,
filter: window._pvState.filter, bgUrl: st.bg };
function draw(img){
// base dark
ctx.fillStyle='#141414'; ctx.fillRect(0,0,W,H);
if(img){
ctx.save();
let f='';
if(opts.filter==='grayscale') f='grayscale(1)';
else if(opts.filter==='sepia') f='sepia(0.85)';
else if(opts.filter==='saturate') f='saturate(2.4)';
else if(opts.filter==='warm') f='sepia(0.45) saturate(1.5) hue-rotate(-15deg)';
else if(opts.filter==='cool') f='saturate(1.2) hue-rotate(15deg) brightness(1.05)';
else if(opts.filter==='invert') f='invert(1)';
else if(opts.filter==='noir') f='grayscale(1) contrast(1.6) brightness(0.9)';
const iw=img.naturalWidth||1, ih=img.naturalHeight||1;
const scale=Math.max(W/iw, H/ih);
const dw=iw*scale, dh=ih*scale;
ctx.drawImage(img,0,0,iw,ih,(W-dw)/2,(H-dh)/2,dw,dh);
ctx.filter='none';
ctx.restore();
}
// solid overlay like designer
ctx.fillStyle='rgba(0,0,0,0.6)'; ctx.fillRect(0,0,W,H);
drawPreviewText(ctx,W,H,opts);
}
const bgSrc = (typeof _proxyImg==='function') ? _proxyImg(st.bg||'') : (st.bg || '');
if(bgSrc){
const im = new Image(); im.crossOrigin='anonymous';
im.onload = function(){ draw(im); };
im.onerror = function(){ draw(null); };
im.src = bgSrc;
} else draw(null);
}
function drawPreviewText(ctx,W,H,opts){
const text = opts.text || '';
if(!text.trim()) return;
const fs = Math.min(52, Math.round(W*0.09));
ctx.font = 'bold '+fs+'px "Segoe UI","Arial","Noto Sans",sans-serif';
ctx.textAlign='center';
const lines=[]; let line='';
const maxW=Math.round(W*0.82);
text.split(' ').forEach(w=>{
const test = line? line+' '+w : w;
if(ctx.measureText(test).width>maxW && line){ lines.push(line.trim()); line=w; } else { line=test; }
});
if(line) lines.push(line.trim());
const lh=Math.round(fs*1.25);
const blockH=lines.length*lh;
let y;
if(opts.pos==='top') y=Math.round(H*0.14)+lh/2;
else if(opts.pos==='bottom') y=Math.round(H*0.88)-blockH+lh/2;
else y=Math.round(H/2)-blockH/2+lh/2;
if(opts.glow==='white'){ ctx.shadowColor='rgba(255,255,255,0.95)'; ctx.shadowBlur=Math.max(8,fs*0.5); }
else if(opts.glow==='color'){ ctx.shadowColor=opts.color; ctx.shadowBlur=Math.max(10,fs*0.6); }
else if(opts.glow==='neon'){ ctx.shadowColor='#00ffff'; ctx.shadowBlur=Math.max(16,fs*0.9); }
ctx.fillStyle=opts.color;
lines.forEach((l,li)=>{ ctx.fillText(l, W/2, y+li*lh); });
ctx.shadowBlur=0;
}
function previewRedrawAllSlides(){
if(!window._pvState) return;
window._pvState.slides.forEach(function(_, i){ previewDrawSlide(i); });
}
function openPreviewDesigner(){
// opens the real slide designer on the FIRST preview slide as a background.
// The designer modal already renders canvas + uploads PNGs; we reuse it by
// temporarily creating a wall-post-like object from the preview slides.
if(!window._personalPostPreview || !window._pvState) return;
const slides = window._pvState.slides.map(function(s,i){
return { text: s.text, image: s.bg, index: i+1 };
});
const fakePost = {
id: 'preview-fake', title: document.getElementById('preview-title')?.value || 'Bài quan điểm',
slides: slides, kind: 'personal_opinion', img: slides[0]?.image||'', url: '', video: ''
};
if(typeof openSlideDesigner === 'function'){
// designer expects index into _wallPosts; temporarily append so designer works
const existed = _wallPosts && _wallPosts.some(p => p.id === 'preview-fake');
if(!existed && Array.isArray(_wallPosts)) _wallPosts.unshift(fakePost);
const idx = _wallPosts.findIndex(p => p.id === 'preview-fake');
openSlideDesigner(idx >= 0 ? idx : 0);
// the fake post is only a designer context; the designer's own save posts
// the designed slides directly to the wall. Remove the fake entry on close.
setTimeout(function(){
const ov = document.getElementById('slide-designer-overlay');
if(!ov && Array.isArray(_wallPosts)){
_wallPosts = _wallPosts.filter(p => p.id !== 'preview-fake');
}
}, 30000);
} else {
toast('Không có công cụ thiết kế!');
}
}
function toggleSourceSelection(el, idx) {
const checkbox = el.querySelector('.source-checkbox');
checkbox.checked = !checkbox.checked;
el.style.border = checkbox.checked ? '1px solid #2d8659' : '1px solid transparent';
el.style.background = checkbox.checked ? '#1a2a1f' : '#202020';
}
async function publishPersonalPostFromModal() {
const title = document.getElementById('preview-title')?.value.trim();
if (!title) {
alert('Vui lòng nhập tiêu đề');
return;
}
// Collect edited slides (text + designed/bg image from _pvState)
const slides = [];
if (window._pvState && window._pvState.slides) {
window._pvState.slides.forEach((s, i) => {
const text = (s.text || '').trim();
if (text) slides.push({ text, image: s.bg || '', index: slides.length + 1 });
});
} else {
document.querySelectorAll('.preview-slide-text').forEach(ta => {
const text = ta.value.trim();
if (text) slides.push({ text, image: '', index: slides.length + 1 });
});
}
// Collect selected sources
const sources = [];
document.querySelectorAll('[data-source-idx]').forEach((el, i) => {
const checkbox = el.querySelector('.source-checkbox');
if (checkbox.checked && window._personalPostPreview?.sources?.[i]) {
sources.push(window._personalPostPreview.sources[i]);
}
});
const btn = event.target;
const origText = btn.textContent;
btn.disabled = true;
btn.textContent = '⏳ Đang đăng...';
try {
const resp = await fetch('/api/personal_post', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
opinion: window._personalPostOpinion,
selected_topics: [],
selected_sources: sources,
custom_title: title,
custom_slides: slides
})
});
const data = await resp.json();
if (!resp.ok || data.error) throw new Error(data.error || 'Lỗi đăng bài');
toast('✅ Đã đăng bài quan điểm lên Tường AI!');
// Prepend to wall
if (data.post && typeof prependWallPost === 'function') {
prependWallPost(data.post);
}
// Close modal
document.getElementById('personal-post-modal')?.remove();
// Clear opinion input
document.getElementById('opinion-input').value = '';
} catch (e) {
toast('❌ ' + e.message);
} finally {
btn.disabled = false;
btn.textContent = origText;
}
}