Spaces:
Running
Running
feat: full frontend with category tabs, shorts on homepage, video playlist below feed
Browse files- static/index.html +152 -249
static/index.html
CHANGED
|
@@ -10,325 +10,228 @@
|
|
| 10 |
<style>
|
| 11 |
*{box-sizing:border-box;margin:0;padding:0}
|
| 12 |
body{background:#111;color:#eee;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;overflow-x:hidden}
|
| 13 |
-
.header{background:linear-gradient(135deg,#0d1117,#1a3a2a 50%,#8b7500);padding:
|
| 14 |
-
.header h1{color:#fff;font-size:
|
| 15 |
-
.header p{color:rgba(255,255,255,.6);font-size:
|
| 16 |
-
|
| 17 |
-
.
|
| 18 |
-
.
|
| 19 |
-
.
|
| 20 |
-
/* Views */
|
| 21 |
.view{display:none}.view.active{display:block}
|
| 22 |
-
|
| 23 |
-
.grid{
|
| 24 |
-
@media(min-width:
|
| 25 |
-
.card{background:#1a1a1a;border-radius:
|
| 26 |
.card:active{transform:scale(.97)}
|
| 27 |
.card-img{position:relative;width:100%;aspect-ratio:16/9;background:#222;overflow:hidden}
|
| 28 |
.card-img img{width:100%;height:100%;object-fit:cover}
|
| 29 |
-
.card-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:
|
| 30 |
-
.card-body{padding:8px}
|
| 31 |
-
.card-title{font-size:
|
| 32 |
-
.
|
|
|
|
| 33 |
.badge-vne{background:#c0392b;color:#fff}.badge-bdp{background:#1a5c35;color:#fff}.badge-24h{background:#e67e22;color:#fff}
|
| 34 |
-
|
| 35 |
-
.slider-
|
| 36 |
-
.slider-
|
| 37 |
-
.slider-
|
| 38 |
-
.slider-track{display:flex;overflow-x:auto;gap:8px;padding:4px 12px 12px;scrollbar-width:none;scroll-snap-type:x mandatory}
|
| 39 |
.slider-track::-webkit-scrollbar{display:none}
|
| 40 |
-
.slider-item{flex:0 0
|
| 41 |
-
.slider-thumb{position:relative;width:100%;aspect-ratio:16/9;border-radius:
|
| 42 |
.slider-thumb img{width:100%;height:100%;object-fit:cover}
|
| 43 |
-
.slider-title{color:#ccc;font-size:
|
| 44 |
-
|
| 45 |
-
.shorts-
|
| 46 |
-
.
|
| 47 |
-
/* TikTok Feed */
|
| 48 |
-
.tiktok-container{width:100%;height:80vh;max-height:680px;min-height:400px;background:#000;position:relative}
|
| 49 |
.tiktok-feed{height:100%;overflow-y:scroll;scroll-snap-type:y mandatory;scrollbar-width:none}
|
| 50 |
.tiktok-feed::-webkit-scrollbar{display:none}
|
| 51 |
.tiktok-slide{height:80vh;max-height:680px;min-height:400px;scroll-snap-align:start;scroll-snap-stop:always;position:relative;display:flex;align-items:center;justify-content:center;background:#000}
|
| 52 |
.tiktok-slide video{width:100%;height:100%;object-fit:cover;display:block}
|
| 53 |
.tiktok-slide.contain-mode video{object-fit:contain}
|
| 54 |
-
.tiktok-bottom{position:absolute;bottom:0;left:0;right:0;padding:
|
| 55 |
-
.tiktok-title{color:#fff;font-size:
|
| 56 |
-
.tiktok-actions{display:flex;gap:
|
| 57 |
-
.tiktok-btn{background:rgba(255,255,255,.
|
| 58 |
-
.tiktok-counter{position:absolute;top:
|
| 59 |
-
.tiktok-unmute{position:absolute;top:
|
| 60 |
-
.tiktok-
|
| 61 |
-
|
| 62 |
-
.
|
| 63 |
-
.
|
| 64 |
-
.
|
| 65 |
-
.
|
| 66 |
</style>
|
| 67 |
</head>
|
| 68 |
<body>
|
| 69 |
-
<div class="header"><h1>📰 VNEWS</h1><p>VnExpress · BongDaPlus · 24h</p></div>
|
| 70 |
-
|
| 71 |
-
<div class="
|
| 72 |
-
<div
|
| 73 |
-
<div
|
| 74 |
-
</div>
|
| 75 |
-
|
| 76 |
-
<div id="view-home" class="view active">
|
| 77 |
-
<div class="loading">Đang tải...</div>
|
| 78 |
-
</div>
|
| 79 |
-
|
| 80 |
-
<div id="view-video" class="view">
|
| 81 |
-
<div class="loading">Đang tải video...</div>
|
| 82 |
-
</div>
|
| 83 |
-
|
| 84 |
-
<div id="view-tiktok" class="view">
|
| 85 |
-
</div>
|
| 86 |
|
| 87 |
<script>
|
| 88 |
-
const SPACE=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
-
|
| 91 |
-
document.querySelectorAll('.
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
});
|
| 100 |
|
| 101 |
-
// ═══
|
| 102 |
async function loadHome(){
|
| 103 |
-
const [
|
| 104 |
fetch('/api/homepage').then(r=>r.json()),
|
| 105 |
fetch('/api/highlights').then(r=>r.json()),
|
| 106 |
fetch('/api/shorts').then(r=>r.json())
|
| 107 |
]);
|
| 108 |
-
let
|
| 109 |
-
// Shorts
|
| 110 |
-
if(
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
}
|
| 115 |
-
// Highlights
|
| 116 |
-
if(
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
}
|
| 121 |
-
// News
|
| 122 |
-
const groups={};
|
| 123 |
-
newsR.forEach(a=>{if(!groups[a.group])groups[a.group]=[];groups[a.group].push(a);});
|
| 124 |
for(const[g,arts] of Object.entries(groups)){
|
| 125 |
-
|
| 126 |
arts.slice(0,6).forEach(a=>{
|
| 127 |
-
|
|
|
|
|
|
|
| 128 |
});
|
| 129 |
-
|
| 130 |
}
|
| 131 |
-
document.getElementById('view-home').innerHTML=
|
| 132 |
}
|
| 133 |
|
| 134 |
-
// ═══
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
async function loadVideos(){
|
| 136 |
const el=document.getElementById('view-video');
|
| 137 |
if(el.dataset.loaded)return;
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
| 148 |
}
|
| 149 |
|
| 150 |
// ═══ TikTok Feed ═══
|
| 151 |
-
let
|
| 152 |
-
|
| 153 |
-
async function openTikTok(type, startIdx){
|
| 154 |
-
// Show tiktok view
|
| 155 |
document.querySelectorAll('.view').forEach(x=>x.classList.remove('active'));
|
| 156 |
document.getElementById('view-tiktok').classList.add('active');
|
| 157 |
-
document.querySelectorAll('.
|
| 158 |
-
|
| 159 |
const el=document.getElementById('view-tiktok');
|
| 160 |
el.innerHTML='<div class="loading">Đang tải video...</div>';
|
| 161 |
|
| 162 |
-
// Fetch video list
|
| 163 |
let articles;
|
| 164 |
-
if(type==='shorts')
|
| 165 |
-
else if(type==='highlights')
|
| 166 |
else articles=await fetch('/api/bdp_videos').then(r=>r.json());
|
| 167 |
|
| 168 |
-
// Fetch video URLs in parallel
|
| 169 |
const vids=[];
|
| 170 |
-
|
| 171 |
-
try{
|
| 172 |
-
|
| 173 |
-
const v=await r.json();
|
| 174 |
-
if(v&&v.src)vids.push({...a,...v,_idx:i});
|
| 175 |
-
}catch(e){}
|
| 176 |
-
});
|
| 177 |
-
await Promise.all(promises);
|
| 178 |
vids.sort((a,b)=>a._idx-b._idx);
|
| 179 |
-
|
| 180 |
if(!vids.length){el.innerHTML='<div class="loading">Không tìm thấy video</div>';return;}
|
| 181 |
|
| 182 |
-
// Reorder: startIdx first
|
| 183 |
const target=vids.find(v=>v._idx===startIdx);
|
| 184 |
const ordered=target?[target,...vids.filter(v=>v!==target)]:vids;
|
| 185 |
-
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
html+='<div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';
|
| 190 |
ordered.forEach((v,i)=>{
|
| 191 |
const isHLS=v.src.includes('.m3u8');
|
| 192 |
const poster=v.poster?` poster="${v.poster}"`:'';
|
| 193 |
-
const
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
const label=v.source==='24h-shorts'?'24h':v.source==='24h'?'24h':'BDP';
|
| 198 |
-
html+=`<div class="tiktok-slide" id="tslide-${i}" data-idx="${i}">
|
| 199 |
-
${videoTag}
|
| 200 |
-
<div class="tiktok-bottom"><span class="badge ${badge}">${label}</span>
|
| 201 |
-
<p class="tiktok-title">${v.title}</p>
|
| 202 |
-
<div class="tiktok-actions">
|
| 203 |
-
<button class="tiktok-btn" onclick="shareVideo(${i})">📤 Chia sẻ</button>
|
| 204 |
-
<button class="tiktok-btn" onclick="seekV(${i},-10)">⏪10s</button>
|
| 205 |
-
<button class="tiktok-btn" onclick="seekV(${i},10)">10s⏩</button>
|
| 206 |
-
</div></div>
|
| 207 |
-
<button class="tiktok-contain-btn" onclick="toggleContain(this)">⬇ Contain</button>
|
| 208 |
-
<div class="tiktok-unmute" onclick="toggleMute()">🔇 Bật tiếng</div>
|
| 209 |
-
<span class="tiktok-counter">${i+1}/${ordered.length}</span>
|
| 210 |
-
</div>`;
|
| 211 |
});
|
| 212 |
-
|
| 213 |
|
| 214 |
-
//
|
| 215 |
-
|
| 216 |
ordered.forEach((v,i)=>{
|
| 217 |
-
const
|
| 218 |
-
const
|
| 219 |
-
|
| 220 |
});
|
| 221 |
-
|
| 222 |
-
el.innerHTML=
|
| 223 |
-
|
| 224 |
-
// Init HLS + autoplay
|
| 225 |
-
initTikTokFeed();
|
| 226 |
}
|
| 227 |
|
| 228 |
-
function
|
| 229 |
-
const feed=document.getElementById('tiktok-feed');
|
| 230 |
-
if(!feed)return;
|
| 231 |
const slides=feed.querySelectorAll('.tiktok-slide');
|
| 232 |
-
|
| 233 |
-
// Init HLS
|
| 234 |
slides.forEach(sl=>{
|
| 235 |
const v=sl.querySelector('video[data-hls]');
|
| 236 |
-
if(v&&!v.
|
| 237 |
-
v._hlsInit=true;
|
| 238 |
-
const src=v.dataset.hls;
|
| 239 |
-
if(v.canPlayType('application/vnd.apple.mpegURL'))v.src=src;
|
| 240 |
-
else if(Hls.isSupported()){const h=new Hls();h.loadSource(src);h.attachMedia(v);}
|
| 241 |
-
else v.src=src;
|
| 242 |
-
}
|
| 243 |
});
|
| 244 |
-
|
| 245 |
-
// Scroll-based activation
|
| 246 |
let cur=-1;
|
| 247 |
-
function
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
if(i===idx){v.currentTime=0;v.play().catch(()=>{setTimeout(()=>v.play().catch(()=>{}),500);});}
|
| 252 |
-
else v.pause();
|
| 253 |
-
});
|
| 254 |
-
cur=idx;_tikTokCurrent=idx;
|
| 255 |
-
}
|
| 256 |
-
let sT;
|
| 257 |
-
feed.addEventListener('scroll',()=>{
|
| 258 |
-
clearTimeout(sT);
|
| 259 |
-
sT=setTimeout(()=>{
|
| 260 |
-
const rect=feed.getBoundingClientRect();
|
| 261 |
-
const center=rect.top+rect.height/2;
|
| 262 |
-
let best=-1,bestD=99999;
|
| 263 |
-
slides.forEach((sl,i)=>{
|
| 264 |
-
const r=sl.getBoundingClientRect();
|
| 265 |
-
const d=Math.abs(r.top+r.height/2-center);
|
| 266 |
-
if(d<bestD){bestD=d;best=i;}
|
| 267 |
-
});
|
| 268 |
-
if(best>=0)activate(best);
|
| 269 |
-
},150);
|
| 270 |
-
});
|
| 271 |
-
setTimeout(()=>activate(0),500);
|
| 272 |
-
|
| 273 |
-
// Tap to pause/play
|
| 274 |
-
slides.forEach(sl=>{
|
| 275 |
-
const v=sl.querySelector('video');
|
| 276 |
-
v.addEventListener('click',e=>{
|
| 277 |
-
e.preventDefault();
|
| 278 |
-
if(v.paused)v.play().catch(()=>{});
|
| 279 |
-
else v.pause();
|
| 280 |
-
});
|
| 281 |
-
});
|
| 282 |
-
}
|
| 283 |
-
|
| 284 |
-
function goHome(){
|
| 285 |
-
document.querySelectorAll('.view').forEach(x=>x.classList.remove('active'));
|
| 286 |
-
document.getElementById('view-home').classList.add('active');
|
| 287 |
-
document.querySelectorAll('.tab').forEach(x=>x.classList.remove('active'));
|
| 288 |
-
document.querySelector('[data-tab="home"]').classList.add('active');
|
| 289 |
-
// Pause all videos
|
| 290 |
-
document.querySelectorAll('#view-tiktok video').forEach(v=>v.pause());
|
| 291 |
-
}
|
| 292 |
-
|
| 293 |
-
function toggleMute(){
|
| 294 |
-
const feed=document.getElementById('tiktok-feed');
|
| 295 |
-
if(!feed)return;
|
| 296 |
-
const vids=feed.querySelectorAll('video');
|
| 297 |
-
const first=vids[0];if(!first)return;
|
| 298 |
-
const mute=!first.muted;
|
| 299 |
-
vids.forEach(v=>v.muted=mute);
|
| 300 |
-
feed.querySelectorAll('.tiktok-unmute').forEach(b=>b.textContent=mute?'🔇 Bật tiếng':'🔊 Đang phát');
|
| 301 |
}
|
| 302 |
|
| 303 |
-
function
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
}
|
| 308 |
-
|
| 309 |
-
function seekV(idx,sec){
|
| 310 |
-
const sl=document.getElementById('tslide-'+idx);
|
| 311 |
-
if(!sl)return;
|
| 312 |
-
const v=sl.querySelector('video');
|
| 313 |
-
if(v)v.currentTime=Math.max(0,Math.min(v.duration||9999,v.currentTime+sec));
|
| 314 |
-
}
|
| 315 |
|
| 316 |
-
|
| 317 |
-
const v=_tikTokData[idx];
|
| 318 |
-
if(!v)return;
|
| 319 |
-
const img=encodeURIComponent(v.img||v.poster||'');
|
| 320 |
-
const title=encodeURIComponent(v.title);
|
| 321 |
-
const shareUrl=SPACE+'/share/'+encodeURIComponent(v.title.substring(0,50))+'?img='+img+'&title='+title;
|
| 322 |
-
if(navigator.share){navigator.share({title:v.title,url:v.link||shareUrl}).catch(()=>{});}
|
| 323 |
-
else{navigator.clipboard.writeText(v.link||shareUrl).then(()=>alert('Đã sao chép link!')).catch(()=>{});}
|
| 324 |
-
}
|
| 325 |
-
|
| 326 |
-
function openArticle(url){
|
| 327 |
-
window.open(url,'_blank');
|
| 328 |
-
}
|
| 329 |
-
|
| 330 |
-
// ═══ Init ═══
|
| 331 |
-
loadHome();
|
| 332 |
</script>
|
| 333 |
</body>
|
| 334 |
</html>
|
|
|
|
| 10 |
<style>
|
| 11 |
*{box-sizing:border-box;margin:0;padding:0}
|
| 12 |
body{background:#111;color:#eee;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;overflow-x:hidden}
|
| 13 |
+
.header{background:linear-gradient(135deg,#0d1117,#1a3a2a 50%,#8b7500);padding:12px;text-align:center}
|
| 14 |
+
.header h1{color:#fff;font-size:18px;font-weight:800}
|
| 15 |
+
.header p{color:rgba(255,255,255,.6);font-size:10px;margin-top:2px}
|
| 16 |
+
.cats{display:flex;overflow-x:auto;background:#1a1a1a;border-bottom:1px solid #333;padding:0 4px;scrollbar-width:none}
|
| 17 |
+
.cats::-webkit-scrollbar{display:none}
|
| 18 |
+
.cat{padding:9px 12px;color:#888;font-size:11px;cursor:pointer;white-space:nowrap;border-bottom:2px solid transparent;flex-shrink:0}
|
| 19 |
+
.cat.active{color:#5cb87a;border-bottom-color:#5cb87a;font-weight:600}
|
|
|
|
| 20 |
.view{display:none}.view.active{display:block}
|
| 21 |
+
.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:6px;padding:6px 4px}
|
| 22 |
+
@media(min-width:600px){.grid{grid-template-columns:repeat(3,1fr)}}
|
| 23 |
+
@media(min-width:900px){.grid{grid-template-columns:repeat(4,1fr)}}
|
| 24 |
+
.card{background:#1a1a1a;border-radius:8px;overflow:hidden;cursor:pointer;border:1px solid #222;transition:transform .1s}
|
| 25 |
.card:active{transform:scale(.97)}
|
| 26 |
.card-img{position:relative;width:100%;aspect-ratio:16/9;background:#222;overflow:hidden}
|
| 27 |
.card-img img{width:100%;height:100%;object-fit:cover}
|
| 28 |
+
.card-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:30px;height:30px;background:rgba(0,0,0,.5);border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px}
|
| 29 |
+
.card-body{padding:6px 8px}
|
| 30 |
+
.card-title{font-size:11px;font-weight:600;color:#eee;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
| 31 |
+
.card-summ{font-size:10px;color:#777;margin-top:3px;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}
|
| 32 |
+
.badge{font-size:8px;padding:1px 5px;border-radius:3px;font-weight:700;display:inline-block;margin-bottom:2px}
|
| 33 |
.badge-vne{background:#c0392b;color:#fff}.badge-bdp{background:#1a5c35;color:#fff}.badge-24h{background:#e67e22;color:#fff}
|
| 34 |
+
.slider-wrap{margin:6px 4px;background:#1a1a1a;border-radius:8px;overflow:hidden;border:1px solid #2a2a2a}
|
| 35 |
+
.slider-header{display:flex;justify-content:space-between;align-items:center;padding:7px 10px}
|
| 36 |
+
.slider-label{color:#f0c040;font-size:13px;font-weight:700}
|
| 37 |
+
.slider-track{display:flex;overflow-x:auto;gap:8px;padding:4px 10px 10px;scrollbar-width:none;scroll-snap-type:x mandatory}
|
|
|
|
| 38 |
.slider-track::-webkit-scrollbar{display:none}
|
| 39 |
+
.slider-item{flex:0 0 160px;scroll-snap-align:start;cursor:pointer}
|
| 40 |
+
.slider-thumb{position:relative;width:100%;aspect-ratio:16/9;border-radius:6px;overflow:hidden;background:#222}
|
| 41 |
.slider-thumb img{width:100%;height:100%;object-fit:cover}
|
| 42 |
+
.slider-title{color:#ccc;font-size:10px;margin-top:3px;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
| 43 |
+
.shorts-item{flex:0 0 110px}
|
| 44 |
+
.shorts-thumb{aspect-ratio:3/4!important;border-radius:8px!important}
|
| 45 |
+
.tiktok-container{width:100%;height:80vh;max-height:680px;min-height:400px;background:#000}
|
|
|
|
|
|
|
| 46 |
.tiktok-feed{height:100%;overflow-y:scroll;scroll-snap-type:y mandatory;scrollbar-width:none}
|
| 47 |
.tiktok-feed::-webkit-scrollbar{display:none}
|
| 48 |
.tiktok-slide{height:80vh;max-height:680px;min-height:400px;scroll-snap-align:start;scroll-snap-stop:always;position:relative;display:flex;align-items:center;justify-content:center;background:#000}
|
| 49 |
.tiktok-slide video{width:100%;height:100%;object-fit:cover;display:block}
|
| 50 |
.tiktok-slide.contain-mode video{object-fit:contain}
|
| 51 |
+
.tiktok-bottom{position:absolute;bottom:0;left:0;right:0;padding:12px 10px 18px;background:linear-gradient(transparent,rgba(0,0,0,.85));z-index:3}
|
| 52 |
+
.tiktok-title{color:#fff;font-size:12px;font-weight:600;margin:3px 0 5px;line-height:1.3;text-shadow:0 1px 3px rgba(0,0,0,.8)}
|
| 53 |
+
.tiktok-actions{display:flex;gap:6px;flex-wrap:wrap}
|
| 54 |
+
.tiktok-btn{background:rgba(255,255,255,.12);color:#fff;border:none;padding:4px 10px;border-radius:12px;font-size:10px;cursor:pointer}
|
| 55 |
+
.tiktok-counter{position:absolute;top:8px;left:8px;background:rgba(0,0,0,.5);color:#fff;font-size:9px;padding:2px 7px;border-radius:8px;z-index:4}
|
| 56 |
+
.tiktok-unmute{position:absolute;top:8px;right:8px;background:rgba(0,0,0,.5);color:#fff;font-size:10px;padding:4px 9px;border-radius:12px;cursor:pointer;z-index:4}
|
| 57 |
+
.tiktok-cbtn{position:absolute;bottom:60px;right:8px;background:rgba(255,255,255,.12);color:#fff;border:none;padding:4px 9px;border-radius:10px;font-size:9px;cursor:pointer;z-index:4}
|
| 58 |
+
.loading{text-align:center;padding:40px;color:#666;font-size:13px}
|
| 59 |
+
.topbar{display:flex;justify-content:space-between;padding:6px 8px;color:#555;font-size:10px}
|
| 60 |
+
.section-title{font-size:14px;font-weight:700;color:#5cb87a;margin:8px 0 4px;padding-left:8px;border-left:3px solid #5cb87a}
|
| 61 |
+
.back-btn{position:sticky;top:0;background:rgba(17,17,17,.9);color:#fff;border:none;padding:8px 16px;font-size:12px;cursor:pointer;z-index:100;width:100%;text-align:center;backdrop-filter:blur(4px)}
|
| 62 |
+
a.card-link{text-decoration:none;color:inherit;display:block}
|
| 63 |
</style>
|
| 64 |
</head>
|
| 65 |
<body>
|
| 66 |
+
<div class="header"><h1>📰 VNEWS</h1><p>VnExpress · BongDaPlus · 24h · Cập nhật tự động</p></div>
|
| 67 |
+
<div class="cats" id="cat-bar"></div>
|
| 68 |
+
<div id="view-home" class="view active"><div class="loading">Đang tải...</div></div>
|
| 69 |
+
<div id="view-cat" class="view"></div>
|
| 70 |
+
<div id="view-video" class="view"></div>
|
| 71 |
+
<div id="view-tiktok" class="view"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
<script>
|
| 74 |
+
const SPACE=location.origin;
|
| 75 |
+
let _cats=[];
|
| 76 |
+
|
| 77 |
+
// ═══ Init ═══
|
| 78 |
+
async function init(){
|
| 79 |
+
_cats=await fetch('/api/categories').then(r=>r.json());
|
| 80 |
+
let bar='<div class="cat active" data-cat="home">🏠 Trang Chủ</div><div class="cat" data-cat="video">🎬 Video</div>';
|
| 81 |
+
_cats.forEach(c=>{bar+=`<div class="cat" data-cat="${c.id}">${c.name}</div>`;});
|
| 82 |
+
document.getElementById('cat-bar').innerHTML=bar;
|
| 83 |
+
document.querySelectorAll('.cat').forEach(t=>{t.onclick=()=>switchCat(t.dataset.cat);});
|
| 84 |
+
loadHome();
|
| 85 |
+
}
|
| 86 |
|
| 87 |
+
function switchCat(id){
|
| 88 |
+
document.querySelectorAll('.cat').forEach(x=>x.classList.remove('active'));
|
| 89 |
+
document.querySelector(`[data-cat="${id}"]`).classList.add('active');
|
| 90 |
+
document.querySelectorAll('.view').forEach(x=>x.classList.remove('active'));
|
| 91 |
+
document.querySelectorAll('#view-tiktok video').forEach(v=>v.pause());
|
| 92 |
+
if(id==='home'){document.getElementById('view-home').classList.add('active');}
|
| 93 |
+
else if(id==='video'){document.getElementById('view-video').classList.add('active');loadVideos();}
|
| 94 |
+
else{document.getElementById('view-cat').classList.add('active');loadCategory(id);}
|
| 95 |
+
}
|
|
|
|
| 96 |
|
| 97 |
+
// ═══ Homepage ═══
|
| 98 |
async function loadHome(){
|
| 99 |
+
const [news,hl,sh]=await Promise.all([
|
| 100 |
fetch('/api/homepage').then(r=>r.json()),
|
| 101 |
fetch('/api/highlights').then(r=>r.json()),
|
| 102 |
fetch('/api/shorts').then(r=>r.json())
|
| 103 |
]);
|
| 104 |
+
let h='';
|
| 105 |
+
// Shorts
|
| 106 |
+
if(sh.length){
|
| 107 |
+
h+='<div class="slider-wrap"><div class="slider-header"><span class="slider-label">📱 Shorts 24h</span></div><div class="slider-track">';
|
| 108 |
+
sh.forEach((a,i)=>{if(a.img)h+=`<div class="slider-item shorts-item" onclick="openTikTok('shorts',${i})"><div class="slider-thumb shorts-thumb"><img src="${a.img}"><div class="card-play">▶</div></div><div class="slider-title">${a.title}</div></div>`;});
|
| 109 |
+
h+='</div></div>';
|
| 110 |
}
|
| 111 |
+
// Highlights
|
| 112 |
+
if(hl.length){
|
| 113 |
+
h+='<div class="slider-wrap"><div class="slider-header"><span class="slider-label">🎬 Highlight</span></div><div class="slider-track">';
|
| 114 |
+
hl.forEach((a,i)=>{if(a.img)h+=`<div class="slider-item" onclick="openTikTok('highlights',${i})"><div class="slider-thumb"><img src="${a.img}"><div class="card-play">▶</div></div><div class="slider-title">${a.title}</div></div>`;});
|
| 115 |
+
h+='</div></div>';
|
| 116 |
}
|
| 117 |
+
// News groups
|
| 118 |
+
const groups={};news.forEach(a=>{if(!groups[a.group])groups[a.group]=[];groups[a.group].push(a);});
|
|
|
|
| 119 |
for(const[g,arts] of Object.entries(groups)){
|
| 120 |
+
h+=`<div class="section-title">${g}</div><div class="grid">`;
|
| 121 |
arts.slice(0,6).forEach(a=>{
|
| 122 |
+
const bg=a.source==='bdp'?'badge-bdp':'badge-vne';
|
| 123 |
+
const lb=a.source==='bdp'?'BDP':'VnExpress';
|
| 124 |
+
h+=`<div class="card" onclick="openArticle('${a.link.replace(/'/g,"\\'")}')"><div class="card-img">${a.img?`<img src="${a.img}">`:''}</div><div class="card-body"><span class="badge ${bg}">${lb}</span><div class="card-title">${a.title}</div>${a.summary?`<div class="card-summ">${a.summary}</div>`:''}</div></div>`;
|
| 125 |
});
|
| 126 |
+
h+='</div>';
|
| 127 |
}
|
| 128 |
+
document.getElementById('view-home').innerHTML=h;
|
| 129 |
}
|
| 130 |
|
| 131 |
+
// ═══ Category ═══
|
| 132 |
+
async function loadCategory(id){
|
| 133 |
+
const el=document.getElementById('view-cat');
|
| 134 |
+
el.innerHTML='<div class="loading">Đang tải...</div>';
|
| 135 |
+
const arts=await fetch('/api/category/'+id).then(r=>r.json());
|
| 136 |
+
if(!arts.length){el.innerHTML='<div class="loading">Không có tin</div>';return;}
|
| 137 |
+
const cat=_cats.find(c=>c.id===id);
|
| 138 |
+
let h=`<div class="topbar"><span>${cat?cat.name:id}</span><span>${arts.length} tin</span></div><div class="grid">`;
|
| 139 |
+
arts.forEach(a=>{
|
| 140 |
+
const bg=a.source==='bdp'?'badge-bdp':'badge-vne';
|
| 141 |
+
const lb=a.source==='bdp'?'BDP':'VnExpress';
|
| 142 |
+
h+=`<div class="card" onclick="openArticle('${a.link.replace(/'/g,"\\'")}')"><div class="card-img">${a.img?`<img src="${a.img}">`:''}</div><div class="card-body"><span class="badge ${bg}">${lb}</span><div class="card-title">${a.title}</div></div></div>`;
|
| 143 |
+
});
|
| 144 |
+
h+='</div>';el.innerHTML=h;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// ═══ Video tab ═══
|
| 148 |
async function loadVideos(){
|
| 149 |
const el=document.getElementById('view-video');
|
| 150 |
if(el.dataset.loaded)return;
|
| 151 |
+
el.innerHTML='<div class="loading">Đang tải...</div>';
|
| 152 |
+
const[hl,bdp]=await Promise.all([fetch('/api/highlights').then(r=>r.json()),fetch('/api/bdp_videos').then(r=>r.json())]);
|
| 153 |
+
let h='<div class="section-title">🎬 Highlight 24h</div><div class="grid">';
|
| 154 |
+
hl.forEach((a,i)=>{h+=`<div class="card" onclick="openTikTok('highlights',${i})"><div class="card-img">${a.img?`<img src="${a.img}">`:''}<div class="card-play">▶</div></div><div class="card-body"><span class="badge badge-24h">24h</span><div class="card-title">${a.title}</div></div></div>`;});
|
| 155 |
+
h+='</div>';
|
| 156 |
+
if(bdp.length){
|
| 157 |
+
h+='<div class="section-title">⚽ BDP Video</div><div class="grid">';
|
| 158 |
+
bdp.forEach((a,i)=>{h+=`<div class="card" onclick="openTikTok('bdp',${i})"><div class="card-img">${a.img?`<img src="${a.img}">`:''}<div class="card-play">▶</div></div><div class="card-body"><span class="badge badge-bdp">BDP</span><div class="card-title">${a.title}</div></div></div>`;});
|
| 159 |
+
h+='</div>';
|
| 160 |
+
}
|
| 161 |
+
el.innerHTML=h;el.dataset.loaded='1';
|
| 162 |
}
|
| 163 |
|
| 164 |
// ═══ TikTok Feed ═══
|
| 165 |
+
let _tikData=[];
|
| 166 |
+
async function openTikTok(type,startIdx){
|
|
|
|
|
|
|
| 167 |
document.querySelectorAll('.view').forEach(x=>x.classList.remove('active'));
|
| 168 |
document.getElementById('view-tiktok').classList.add('active');
|
| 169 |
+
document.querySelectorAll('.cat').forEach(x=>x.classList.remove('active'));
|
|
|
|
| 170 |
const el=document.getElementById('view-tiktok');
|
| 171 |
el.innerHTML='<div class="loading">Đang tải video...</div>';
|
| 172 |
|
|
|
|
| 173 |
let articles;
|
| 174 |
+
if(type==='shorts')articles=await fetch('/api/shorts').then(r=>r.json());
|
| 175 |
+
else if(type==='highlights')articles=await fetch('/api/highlights').then(r=>r.json());
|
| 176 |
else articles=await fetch('/api/bdp_videos').then(r=>r.json());
|
| 177 |
|
|
|
|
| 178 |
const vids=[];
|
| 179 |
+
await Promise.all(articles.map(async(a,i)=>{
|
| 180 |
+
try{const r=await fetch('/api/video_url?url='+encodeURIComponent(a.link));const v=await r.json();if(v&&v.src)vids.push({...a,...v,_idx:i});}catch(e){}
|
| 181 |
+
}));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
vids.sort((a,b)=>a._idx-b._idx);
|
|
|
|
| 183 |
if(!vids.length){el.innerHTML='<div class="loading">Không tìm thấy video</div>';return;}
|
| 184 |
|
|
|
|
| 185 |
const target=vids.find(v=>v._idx===startIdx);
|
| 186 |
const ordered=target?[target,...vids.filter(v=>v!==target)]:vids;
|
| 187 |
+
_tikData=ordered;
|
| 188 |
|
| 189 |
+
let h='<button class="back-btn" onclick="goHome()">← Quay lại</button>';
|
| 190 |
+
h+='<div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';
|
|
|
|
| 191 |
ordered.forEach((v,i)=>{
|
| 192 |
const isHLS=v.src.includes('.m3u8');
|
| 193 |
const poster=v.poster?` poster="${v.poster}"`:'';
|
| 194 |
+
const vtag=isHLS?`<video playsinline preload="metadata"${poster} data-hls="${v.src}" muted loop></video>`:`<video playsinline preload="metadata"${poster} muted loop><source src="${v.src}" type="video/mp4"></video>`;
|
| 195 |
+
const bg=v.source==='bdp'?'badge-bdp':'badge-24h';
|
| 196 |
+
const lb=v.source==='bdp'?'BDP':'24h';
|
| 197 |
+
h+=`<div class="tiktok-slide" id="tslide-${i}" data-idx="${i}">${vtag}<div class="tiktok-bottom"><span class="badge ${bg}">${lb}</span><p class="tiktok-title">${v.title}</p><div class="tiktok-actions"><button class="tiktok-btn" onclick="shareV(${i})">📤 Chia sẻ</button><button class="tiktok-btn" onclick="seekV(${i},-10)">⏪10s</button><button class="tiktok-btn" onclick="seekV(${i},10)">10s⏩</button></div></div><button class="tiktok-cbtn" onclick="toggleContain(this)">⬇ Contain</button><div class="tiktok-unmute" onclick="toggleMute()">🔇 Bật tiếng</div><span class="tiktok-counter">${i+1}/${ordered.length}</span></div>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
});
|
| 199 |
+
h+='</div></div>';
|
| 200 |
|
| 201 |
+
// Playlist below
|
| 202 |
+
h+='<div class="topbar"><span>📋 Danh sách phát ('+ordered.length+' video)</span></div><div class="grid">';
|
| 203 |
ordered.forEach((v,i)=>{
|
| 204 |
+
const bg=v.source==='bdp'?'badge-bdp':'badge-24h';
|
| 205 |
+
const lb=v.source==='bdp'?'BDP':'24h';
|
| 206 |
+
h+=`<a href="#tslide-${i}" class="card-link"><div class="card"><div class="card-img">${(v.img||v.poster)?`<img src="${v.img||v.poster}">`:''}<div class="card-play">▶</div></div><div class="card-body"><span class="badge ${bg}">${lb}</span><div class="card-title">${v.title}</div></div></div></a>`;
|
| 207 |
});
|
| 208 |
+
h+='</div>';
|
| 209 |
+
el.innerHTML=h;
|
| 210 |
+
initFeed();
|
|
|
|
|
|
|
| 211 |
}
|
| 212 |
|
| 213 |
+
function initFeed(){
|
| 214 |
+
const feed=document.getElementById('tiktok-feed');if(!feed)return;
|
|
|
|
| 215 |
const slides=feed.querySelectorAll('.tiktok-slide');
|
|
|
|
|
|
|
| 216 |
slides.forEach(sl=>{
|
| 217 |
const v=sl.querySelector('video[data-hls]');
|
| 218 |
+
if(v&&!v._hlsI){v._hlsI=true;const s=v.dataset.hls;if(v.canPlayType('application/vnd.apple.mpegURL'))v.src=s;else if(Hls.isSupported()){const h=new Hls();h.loadSource(s);h.attachMedia(v);}else v.src=s;}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
});
|
|
|
|
|
|
|
| 220 |
let cur=-1;
|
| 221 |
+
function act(idx){if(idx===cur)return;slides.forEach((sl,i)=>{const v=sl.querySelector('video');if(i===idx){v.currentTime=0;v.play().catch(()=>setTimeout(()=>v.play().catch(()=>{}),500));}else v.pause();});cur=idx;}
|
| 222 |
+
let sT;feed.addEventListener('scroll',()=>{clearTimeout(sT);sT=setTimeout(()=>{const rect=feed.getBoundingClientRect();const 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);});
|
| 223 |
+
setTimeout(()=>act(0),500);
|
| 224 |
+
slides.forEach(sl=>{const v=sl.querySelector('video');v.addEventListener('click',e=>{e.preventDefault();v.paused?v.play().catch(()=>{}):v.pause();});});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
}
|
| 226 |
|
| 227 |
+
function goHome(){switchCat('home');}
|
| 228 |
+
function toggleMute(){const f=document.getElementById('tiktok-feed');if(!f)return;const vs=f.querySelectorAll('video');const m=!vs[0]?.muted;vs.forEach(v=>v.muted=m);f.querySelectorAll('.tiktok-unmute').forEach(b=>b.textContent=m?'🔇 Bật tiếng':'🔊 Đang phát');}
|
| 229 |
+
function toggleContain(btn){const sl=btn.closest('.tiktok-slide');sl.classList.toggle('contain-mode');btn.textContent=sl.classList.contains('contain-mode')?'⤢ 1:1':'⬇ Contain';}
|
| 230 |
+
function seekV(i,s){const sl=document.getElementById('tslide-'+i);if(sl){const v=sl.querySelector('video');if(v)v.currentTime=Math.max(0,Math.min(v.duration||9999,v.currentTime+s));}}
|
| 231 |
+
function shareV(i){const v=_tikData[i];if(!v)return;const u=v.link||'';if(navigator.share)navigator.share({title:v.title,url:u}).catch(()=>{});else{navigator.clipboard.writeText(u).then(()=>alert('Đã sao chép!')).catch(()=>{});}}
|
| 232 |
+
function openArticle(url){window.open(url,'_blank');}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
+
init();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
</script>
|
| 236 |
</body>
|
| 237 |
</html>
|