bep40 commited on
Commit
8a0dccc
·
verified ·
1 Parent(s): f2df10e

feat: icon category grid + auto-hide seek buttons + video slide→TikTok fix

Browse files
Files changed (1) hide show
  1. app.py +150 -48
app.py CHANGED
@@ -37,6 +37,30 @@ CATEGORIES = {
37
  "🇻🇳 Bóng Đá VN": "bdp::https://bongdaplus.vn/bong-da-viet-nam::Bóng Đá",
38
  "🔄 Chuyển Nhượng": "bdp::https://bongdaplus.vn/tin-chuyen-nhuong::Bóng Đá",
39
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  HOMEPAGE_SOURCES = [
41
  ("vne","https://vnexpress.net/thoi-su","Thời Sự"),
42
  ("vne","https://vnexpress.net/the-gioi","Thế Giới"),
@@ -480,7 +504,7 @@ def render_video_carousel_html(videos):
480
  link = v.get("link","#")
481
  title = v.get("title","")
482
  aid = make_id(link); sl = slug(title)
483
- click_js = f"window.bdpOpen('{esc(link)}','{aid}','{sl}')"
484
  items.append(f'''<div class="vslide-item" onclick="{click_js}">
485
  <div class="vslide-thumb"><img src="{img}" alt="" class="bdp-lazy-img">
486
  <div class="vslide-play">▶</div><span class="vslide-badge vslide-badge-24h">24h</span></div>
@@ -722,7 +746,7 @@ footer,.built-with{display:none!important}
722
  .bdp-header h1{color:#fff;font-size:20px;margin:0;font-weight:800;text-shadow:0 2px 6px rgba(0,0,0,.4)}
723
  .bdp-header p{color:rgba(255,255,255,.6);font-size:11px;margin:2px 0 0}
724
  @media(min-width:768px){.bdp-header h1{font-size:26px}.bdp-header{padding:20px}}
725
- .controls-row{padding:0 8px!important;background:#111!important}
726
  .controls-row>div{background:#111!important;border:none!important}
727
  .controls-row label{color:#aaa!important;font-size:12px!important}
728
  .controls-row .wrap{border-color:#333!important;background:#1a1a1a!important}
@@ -830,15 +854,27 @@ footer,.built-with{display:none!important}
830
  .tiktok-unmute-hint{position:absolute;top:12px;right:12px;background:rgba(0,0,0,.6);color:#fff;font-size:12px;padding:6px 12px;border-radius:18px;cursor:pointer;z-index:4;backdrop-filter:blur(4px);transition:opacity .3s}
831
  .tiktok-pause-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:70px;height:70px;background:rgba(0,0,0,.5);border-radius:50%;color:#fff;font-size:28px;z-index:5;pointer-events:none;display:none;align-items:center;justify-content:center;line-height:70px;text-align:center}
832
  .tiktok-slide.paused .tiktok-pause-icon{display:block}
833
- .tiktok-seek-controls{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;gap:40px;z-index:6;pointer-events:auto}
834
- .tiktok-seek-btn{background:rgba(0,0,0,.4);color:#fff;border:none;padding:8px 14px;border-radius:20px;font-size:12px;cursor:pointer;backdrop-filter:blur(4px);font-weight:600;opacity:.7;transition:opacity .2s}
 
835
  .tiktok-seek-btn:hover{opacity:1}
836
  .tiktok-seek-btn:active{transform:scale(.9)}
837
  .vslide-badge{position:absolute;top:6px;left:6px;font-size:9px;padding:1px 6px;border-radius:3px;font-weight:700;z-index:2}
838
  .vslide-badge-24h{background:#e67e22;color:#fff}
839
- """
840
 
841
- # ══════════════════════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
 
 
842
  HEAD_META = """
843
  <meta name="description" content="Tin tức tổng hợp nhanh nhất - VnExpress, BongDaPlus, 24h">
844
  <meta property="og:title" content="Tin Tức Việt Nam - Tổng Hợp">
@@ -882,6 +918,48 @@ window.bdpSlideScroll=function(dir,trackId){
882
  if(track){track.scrollBy({left:dir*260,behavior:'smooth'});}
883
  };
884
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
885
  function gc(a){try{return JSON.parse(localStorage.getItem('bdp_cmt_'+a))||[];}catch(e){return[];}}
886
  function sc(a,c){try{localStorage.setItem('bdp_cmt_'+a,JSON.stringify(c));}catch(e){}}
887
  window.bdpRenderCmt=function(a){var l=document.getElementById('cmt-list-'+a);if(!l)return;var c=gc(a);if(!c.length){l.innerHTML='<div class="bdp-cmt-empty">Chưa có bình luận. Hãy là người đầu tiên!</div>';return;}var h='';for(var i=c.length-1;i>=0;i--){var x=c[i];h+='<div class="bdp-cmt-item"><span class="bdp-cmt-author">'+x.name+'</span><span class="bdp-cmt-date">'+x.date+'</span><div class="bdp-cmt-body">'+x.text.replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</div></div>';}l.innerHTML=h;};
@@ -907,39 +985,32 @@ window.bdpSeek=function(btn,sec){
907
  window.bdpOpenTikTok=function(url,aid){
908
  /* Switch to Video tab and scroll TikTok feed to matching video */
909
  try{localStorage.setItem('bdp_url_'+aid,url);}catch(e){}
910
- /* Find the dropdown and switch to Video Tổng Hợp */
911
- var dd=document.querySelector('.controls-row select, .controls-row input[role="listbox"]');
912
- if(!dd){
913
- /* Gradio dropdown: find the actual element */
914
- var labels=document.querySelectorAll('.controls-row .wrap');
915
- labels.forEach(function(l){
916
- var inp=l.querySelector('input');
917
- if(inp){
918
- inp.value='🎬 Video Tổng Hợp';
919
- inp.dispatchEvent(new Event('input',{bubbles:true}));
920
- /* Also try clicking the option */
921
- setTimeout(function(){
922
- var opts=document.querySelectorAll('[role="option"]');
923
- opts.forEach(function(o){if(o.textContent.indexOf('Video')>-1)o.click();});
924
- },200);
925
- }
926
- });
927
- }
928
- /* After switching, scroll to the matching video in TikTok feed */
929
- setTimeout(function(){
930
  var feed=document.querySelector('.tiktok-fullscreen-feed');
931
- if(!feed) return;
932
- /* Find slide with matching link */
933
- var slides=feed.querySelectorAll('.tiktok-slide');
934
- var targetIdx=-1;
935
- slides.forEach(function(sl,i){
936
- if(sl.getAttribute('data-aid')===aid) targetIdx=i;
937
- });
938
- if(targetIdx>=0 && slides[targetIdx]){
939
- slides[targetIdx].scrollIntoView({behavior:'smooth'});
 
 
940
  }
941
- window.scrollTo({top:0,behavior:'smooth'});
942
- },1500);
943
  };
944
  window.bdpTikTokUnmute=function(hint){
945
  var feed=hint.closest('.tiktok-fullscreen-feed')||hint.closest('.tiktok-feed');
@@ -1019,15 +1090,23 @@ function initTikTokFullscreen(container){
1019
  /* Start first video after short delay for HLS init */
1020
  setTimeout(function(){activateSlide(0);},800);
1021
 
1022
- /* Tap to pause/play */
1023
  slides.forEach(function(sl){
1024
  var v=sl.querySelector('.tiktok-video');
 
 
 
 
 
 
1025
  if(v){
1026
  v.addEventListener('click',function(e){
1027
  e.preventDefault();
1028
  if(v.paused){tryPlay(v);sl.classList.remove('paused');}
1029
  else{v.pause();sl.classList.add('paused');}
 
1030
  });
 
1031
  }
1032
  });
1033
  }
@@ -1049,31 +1128,54 @@ setInterval(function(){
1049
  },1500);
1050
 
1051
  var hh=window.location.hash;
1052
- if(hh&&hh.startsWith('#/')){var ps=hh.slice(2).split('/');if(ps.length>=2){var aid=ps[ps.length-1];try{var url=localStorage.getItem('bdp_url_'+aid);if(url)setTimeout(function(){window.bdpOpen(url,aid,ps.slice(0,-1).join('/'));},2000);}catch(e){}}}
 
 
 
 
 
 
 
 
 
 
 
 
1053
  }
1054
  """
1055
 
1056
  # ══════════════════════════════════════════════════════════════════════════════
 
 
 
 
 
 
 
 
 
 
1057
  with gr.Blocks(title="Tin Tức Việt Nam",css=CSS,head=HEAD_META,js=JS_FUNC,theme=gr.themes.Base(),fill_width=True) as demo:
1058
  gr.HTML('<div class="bdp-header"><h1>📰 Tin Tức Việt Nam</h1><p>VnExpress · BongDaPlus · 24h · Thời sự · Thế giới · Kinh doanh · Công nghệ · Thể thao · Giải trí · Video</p></div>')
 
1059
  article_url=gr.Textbox(value="",visible=False,elem_id="article-url-input")
1060
  with gr.Row(elem_classes=["controls-row"]):
1061
- cat=gr.Dropdown(choices=list(CATEGORIES.keys()),value="🏠 Trang Chủ (Nổi Bật)",label="Chuyên mục",scale=3,interactive=True)
1062
  ref_btn=gr.Button("🔄 Làm mới",variant="primary",scale=1)
1063
- back_btn=gr.Button("← Quay lại",variant="secondary",scale=1,visible=False)
1064
  news_list=gr.HTML()
1065
  article_view=gr.HTML(visible=False)
1066
  read_btn=gr.Button("Đọc",visible=False,elem_id="btn-read-article")
1067
  def show_article(url):
1068
  if not url or url=="#" or len(url)<10:
1069
- return gr.update(visible=True),gr.update(visible=False),gr.update(visible=False),gr.update(visible=False),""
1070
- return (gr.update(visible=False),gr.update(value=read_article(url),visible=True),gr.update(visible=False),gr.update(visible=True),"")
1071
  def show_list(c):
1072
- return (gr.update(value=fetch_news_list(c),visible=True),gr.update(visible=False),gr.update(visible=True),gr.update(visible=False))
1073
- read_btn.click(fn=show_article,inputs=[article_url],outputs=[news_list,article_view,ref_btn,back_btn,article_url])
1074
- back_btn.click(fn=show_list,inputs=[cat],outputs=[news_list,article_view,ref_btn,back_btn])
1075
- ref_btn.click(fn=show_list,inputs=[cat],outputs=[news_list,article_view,ref_btn,back_btn])
1076
- cat.change(fn=show_list,inputs=[cat],outputs=[news_list,article_view,ref_btn,back_btn])
1077
  timer=gr.Timer(value=REFRESH_SECONDS,active=True)
1078
  timer.tick(fn=fetch_news_list,inputs=cat,outputs=news_list)
1079
  demo.load(fn=fetch_news_list,inputs=cat,outputs=news_list)
 
37
  "🇻🇳 Bóng Đá VN": "bdp::https://bongdaplus.vn/bong-da-viet-nam::Bóng Đá",
38
  "🔄 Chuyển Nhượng": "bdp::https://bongdaplus.vn/tin-chuyen-nhuong::Bóng Đá",
39
  }
40
+
41
+ # Mapping for icon grid: (icon, short_label, hash_slug)
42
+ CAT_ICONS = [
43
+ ("🏠","Trang Chủ","trang-chu"),
44
+ ("🎬","Video","video"),
45
+ ("📰","Thời Sự","thoi-su"),
46
+ ("🌍","Thế Giới","the-gioi"),
47
+ ("💰","Kinh Doanh","kinh-doanh"),
48
+ ("💻","Công Nghệ","cong-nghe"),
49
+ ("🔬","Khoa Học","khoa-hoc"),
50
+ ("🎬","Giải Trí","giai-tri"),
51
+ ("🏥","Sức Khỏe","suc-khoe"),
52
+ ("🎓","Giáo Dục","giao-duc"),
53
+ ("✈️","Du Lịch","du-lich"),
54
+ ("⚽","Thể Thao","the-thao"),
55
+ ("⚽","Bóng Đá QT","bong-da-qt"),
56
+ ("🏴󠁧󠁢󠁥󠁮󠁧󠁿","Ngoại Hạng Anh","ngoai-hang-anh"),
57
+ ("🇪🇸","La Liga","la-liga"),
58
+ ("🏆","Champions League","champions-league"),
59
+ ("🇻🇳","Bóng Đá VN","bong-da-vn"),
60
+ ("🔄","Chuyển Nhượng","chuyen-nhuong"),
61
+ ]
62
+ CAT_KEYS = list(CATEGORIES.keys())
63
+ CAT_HASH_TO_KEY = {ci[2]: CAT_KEYS[i] for i, ci in enumerate(CAT_ICONS)}
64
  HOMEPAGE_SOURCES = [
65
  ("vne","https://vnexpress.net/thoi-su","Thời Sự"),
66
  ("vne","https://vnexpress.net/the-gioi","Thế Giới"),
 
504
  link = v.get("link","#")
505
  title = v.get("title","")
506
  aid = make_id(link); sl = slug(title)
507
+ click_js = f"window.bdpOpenTikTok('{esc(link)}','{aid}')"
508
  items.append(f'''<div class="vslide-item" onclick="{click_js}">
509
  <div class="vslide-thumb"><img src="{img}" alt="" class="bdp-lazy-img">
510
  <div class="vslide-play">▶</div><span class="vslide-badge vslide-badge-24h">24h</span></div>
 
746
  .bdp-header h1{color:#fff;font-size:20px;margin:0;font-weight:800;text-shadow:0 2px 6px rgba(0,0,0,.4)}
747
  .bdp-header p{color:rgba(255,255,255,.6);font-size:11px;margin:2px 0 0}
748
  @media(min-width:768px){.bdp-header h1{font-size:26px}.bdp-header{padding:20px}}
749
+ .controls-row{padding:0!important;background:#111!important;height:0!important;overflow:hidden!important;margin:0!important;opacity:0;pointer-events:none;position:absolute}
750
  .controls-row>div{background:#111!important;border:none!important}
751
  .controls-row label{color:#aaa!important;font-size:12px!important}
752
  .controls-row .wrap{border-color:#333!important;background:#1a1a1a!important}
 
854
  .tiktok-unmute-hint{position:absolute;top:12px;right:12px;background:rgba(0,0,0,.6);color:#fff;font-size:12px;padding:6px 12px;border-radius:18px;cursor:pointer;z-index:4;backdrop-filter:blur(4px);transition:opacity .3s}
855
  .tiktok-pause-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:70px;height:70px;background:rgba(0,0,0,.5);border-radius:50%;color:#fff;font-size:28px;z-index:5;pointer-events:none;display:none;align-items:center;justify-content:center;line-height:70px;text-align:center}
856
  .tiktok-slide.paused .tiktok-pause-icon{display:block}
857
+ .tiktok-seek-controls{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;gap:40px;z-index:6;pointer-events:auto;opacity:0;transition:opacity .3s;pointer-events:none}
858
+ .tiktok-slide.show-controls .tiktok-seek-controls{opacity:1;pointer-events:auto}
859
+ .tiktok-seek-btn{background:rgba(0,0,0,.4);color:#fff;border:none;padding:8px 14px;border-radius:20px;font-size:12px;cursor:pointer;backdrop-filter:blur(4px);font-weight:600;transition:opacity .2s}
860
  .tiktok-seek-btn:hover{opacity:1}
861
  .tiktok-seek-btn:active{transform:scale(.9)}
862
  .vslide-badge{position:absolute;top:6px;left:6px;font-size:9px;padding:1px 6px;border-radius:3px;font-weight:700;z-index:2}
863
  .vslide-badge-24h{background:#e67e22;color:#fff}
 
864
 
865
+ /* ══ Category Icon Grid ══ */
866
+ .cat-grid-wrap{padding:6px 8px 2px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;background:#111}
867
+ .cat-grid-wrap::-webkit-scrollbar{display:none}
868
+ .cat-grid{display:flex;gap:6px;min-width:max-content}
869
+ .cat-icon-btn{display:flex;flex-direction:column;align-items:center;justify-content:center;min-width:62px;padding:7px 4px 5px;border-radius:10px;cursor:pointer;transition:background .15s,transform .1s;background:#1a1a1a;border:1.5px solid transparent;user-select:none;-webkit-tap-highlight-color:transparent;text-decoration:none}
870
+ .cat-icon-btn:hover{background:#252525;transform:scale(1.04)}
871
+ .cat-icon-btn:active{transform:scale(.95)}
872
+ .cat-icon-btn.active{background:#1a3a2a;border-color:#5cb87a}
873
+ .cat-icon-emoji{font-size:22px;line-height:1.2}
874
+ .cat-icon-label{font-size:9.5px;color:#aaa;margin-top:2px;white-space:nowrap;font-weight:500;text-align:center;max-width:68px;overflow:hidden;text-overflow:ellipsis}
875
+ .cat-icon-btn.active .cat-icon-label{color:#5cb87a;font-weight:700}
876
+ @media(min-width:768px){.cat-icon-btn{min-width:72px;padding:8px 6px 6px}.cat-icon-emoji{font-size:25px}.cat-icon-label{font-size:10.5px;max-width:76px}}
877
+ """
878
  HEAD_META = """
879
  <meta name="description" content="Tin tức tổng hợp nhanh nhất - VnExpress, BongDaPlus, 24h">
880
  <meta property="og:title" content="Tin Tức Việt Nam - Tổng Hợp">
 
918
  if(track){track.scrollBy({left:dir*260,behavior:'smooth'});}
919
  };
920
 
921
+ /* ══ Category Icon Grid ══ */
922
+ window.bdpSelectCat=function(catKey,hashSlug){
923
+ window.location.hash='#cat/'+hashSlug;
924
+ /* Update active state visually */
925
+ document.querySelectorAll('.cat-icon-btn').forEach(function(b){
926
+ b.classList.toggle('active',b.getAttribute('data-cat')===catKey);
927
+ });
928
+ /* Trigger Gradio dropdown change */
929
+ window._bdpSetDropdown(catKey);
930
+ };
931
+
932
+ window._bdpSetDropdown=function(catKey){
933
+ var container=document.getElementById('cat-dropdown');
934
+ if(!container){
935
+ var all=document.querySelectorAll('.controls-row .wrap, .controls-row [data-testid]');
936
+ container=all.length?all[0].closest('[id]'):null;
937
+ }
938
+ if(!container) container=document.querySelector('.controls-row');
939
+ if(!container) return;
940
+ /* Gradio 4+ dropdown: find the input, set value, fire events */
941
+ var inp=container.querySelector('input');
942
+ if(!inp) return;
943
+ try{
944
+ var nativeSet=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;
945
+ nativeSet.call(inp,catKey);
946
+ }catch(e){inp.value=catKey;}
947
+ inp.dispatchEvent(new Event('input',{bubbles:true}));
948
+ inp.dispatchEvent(new Event('change',{bubbles:true}));
949
+ /* Force open dropdown and click the matching option */
950
+ inp.focus();
951
+ inp.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowDown',bubbles:true}));
952
+ setTimeout(function(){
953
+ var opts=document.querySelectorAll('[role="option"], li.item');
954
+ for(var i=0;i<opts.length;i++){
955
+ if(opts[i].textContent.trim()===catKey||opts[i].innerText.trim()===catKey){
956
+ opts[i].click();
957
+ return;
958
+ }
959
+ }
960
+ },150);
961
+ };
962
+
963
  function gc(a){try{return JSON.parse(localStorage.getItem('bdp_cmt_'+a))||[];}catch(e){return[];}}
964
  function sc(a,c){try{localStorage.setItem('bdp_cmt_'+a,JSON.stringify(c));}catch(e){}}
965
  window.bdpRenderCmt=function(a){var l=document.getElementById('cmt-list-'+a);if(!l)return;var c=gc(a);if(!c.length){l.innerHTML='<div class="bdp-cmt-empty">Chưa có bình luận. Hãy là người đầu tiên!</div>';return;}var h='';for(var i=c.length-1;i>=0;i--){var x=c[i];h+='<div class="bdp-cmt-item"><span class="bdp-cmt-author">'+x.name+'</span><span class="bdp-cmt-date">'+x.date+'</span><div class="bdp-cmt-body">'+x.text.replace(/</g,'&lt;').replace(/>/g,'&gt;')+'</div></div>';}l.innerHTML=h;};
 
985
  window.bdpOpenTikTok=function(url,aid){
986
  /* Switch to Video tab and scroll TikTok feed to matching video */
987
  try{localStorage.setItem('bdp_url_'+aid,url);}catch(e){}
988
+ window.location.hash='#cat/video';
989
+ /* Update icon grid active state */
990
+ document.querySelectorAll('.cat-icon-btn').forEach(function(b){
991
+ b.classList.toggle('active',b.getAttribute('data-hash')==='video');
992
+ });
993
+ /* Trigger Gradio dropdown to Video Tổng Hợp */
994
+ window._bdpSetDropdown('\uD83C\uDFAC Video Tổng Hợp');
995
+ /* After content loads, scroll to the matching video in TikTok feed */
996
+ var attempts=0;
997
+ var finder=setInterval(function(){
998
+ attempts++;
 
 
 
 
 
 
 
 
 
999
  var feed=document.querySelector('.tiktok-fullscreen-feed');
1000
+ if(feed){
1001
+ clearInterval(finder);
1002
+ var slides=feed.querySelectorAll('.tiktok-slide');
1003
+ var targetIdx=-1;
1004
+ slides.forEach(function(sl,i){
1005
+ if(sl.getAttribute('data-aid')===aid) targetIdx=i;
1006
+ });
1007
+ if(targetIdx>=0 && slides[targetIdx]){
1008
+ slides[targetIdx].scrollIntoView({behavior:'smooth'});
1009
+ }
1010
+ window.scrollTo({top:0,behavior:'smooth'});
1011
  }
1012
+ if(attempts>30) clearInterval(finder);
1013
+ },300);
1014
  };
1015
  window.bdpTikTokUnmute=function(hint){
1016
  var feed=hint.closest('.tiktok-fullscreen-feed')||hint.closest('.tiktok-feed');
 
1090
  /* Start first video after short delay for HLS init */
1091
  setTimeout(function(){activateSlide(0);},800);
1092
 
1093
+ /* Tap to pause/play + show/hide seek controls */
1094
  slides.forEach(function(sl){
1095
  var v=sl.querySelector('.tiktok-video');
1096
+ var hideTimer=null;
1097
+ function showSeekControls(){
1098
+ sl.classList.add('show-controls');
1099
+ if(hideTimer) clearTimeout(hideTimer);
1100
+ hideTimer=setTimeout(function(){sl.classList.remove('show-controls');},3000);
1101
+ }
1102
  if(v){
1103
  v.addEventListener('click',function(e){
1104
  e.preventDefault();
1105
  if(v.paused){tryPlay(v);sl.classList.remove('paused');}
1106
  else{v.pause();sl.classList.add('paused');}
1107
+ showSeekControls();
1108
  });
1109
+ v.addEventListener('touchstart',function(){showSeekControls();},{passive:true});
1110
  }
1111
  });
1112
  }
 
1128
  },1500);
1129
 
1130
  var hh=window.location.hash;
1131
+ if(hh&&hh.startsWith('#cat/')){
1132
+ /* Category hash: #cat/video, #cat/thoi-su, etc. */
1133
+ var catSlug=hh.slice(5);
1134
+ var catMap={""" + ",".join(f"'{ci[2]}':'{esc(CAT_KEYS[i])}'" for i,ci in enumerate(CAT_ICONS)) + """};
1135
+ if(catMap[catSlug]){
1136
+ setTimeout(function(){
1137
+ window._bdpSetDropdown(catMap[catSlug]);
1138
+ document.querySelectorAll('.cat-icon-btn').forEach(function(b){
1139
+ b.classList.toggle('active',b.getAttribute('data-hash')===catSlug);
1140
+ });
1141
+ },1500);
1142
+ }
1143
+ } else if(hh&&hh.startsWith('#/')){var ps=hh.slice(2).split('/');if(ps.length>=2){var aid=ps[ps.length-1];try{var url=localStorage.getItem('bdp_url_'+aid);if(url)setTimeout(function(){window.bdpOpen(url,aid,ps.slice(0,-1).join('/'));},2000);}catch(e){}}}
1144
  }
1145
  """
1146
 
1147
  # ══════════════════════════════════════════════════════════════════════════════
1148
+ def _build_cat_grid_html():
1149
+ """Build the category icon grid HTML."""
1150
+ items = []
1151
+ for i, (icon, label, hslug) in enumerate(CAT_ICONS):
1152
+ cat_key = CAT_KEYS[i]
1153
+ active = " active" if i == 0 else ""
1154
+ click_js = f"window.bdpSelectCat('{esc(cat_key)}','{hslug}')"
1155
+ items.append(f'<div class="cat-icon-btn{active}" data-cat="{esc(cat_key)}" data-hash="{hslug}" onclick="{click_js}"><span class="cat-icon-emoji">{icon}</span><span class="cat-icon-label">{label}</span></div>')
1156
+ return f'<div class="cat-grid-wrap"><div class="cat-grid">{"".join(items)}</div></div>'
1157
+
1158
  with gr.Blocks(title="Tin Tức Việt Nam",css=CSS,head=HEAD_META,js=JS_FUNC,theme=gr.themes.Base(),fill_width=True) as demo:
1159
  gr.HTML('<div class="bdp-header"><h1>📰 Tin Tức Việt Nam</h1><p>VnExpress · BongDaPlus · 24h · Thời sự · Thế giới · Kinh doanh · Công nghệ · Thể thao · Giải trí · Video</p></div>')
1160
+ gr.HTML(_build_cat_grid_html())
1161
  article_url=gr.Textbox(value="",visible=False,elem_id="article-url-input")
1162
  with gr.Row(elem_classes=["controls-row"]):
1163
+ cat=gr.Dropdown(choices=list(CATEGORIES.keys()),value="🏠 Trang Chủ (Nổi Bật)",label="Chuyên mục",scale=3,interactive=True,elem_id="cat-dropdown")
1164
  ref_btn=gr.Button("🔄 Làm mới",variant="primary",scale=1)
1165
+ back_btn=gr.Button("← Quay lại",variant="secondary",visible=False)
1166
  news_list=gr.HTML()
1167
  article_view=gr.HTML(visible=False)
1168
  read_btn=gr.Button("Đọc",visible=False,elem_id="btn-read-article")
1169
  def show_article(url):
1170
  if not url or url=="#" or len(url)<10:
1171
+ return gr.update(visible=True),gr.update(visible=False),gr.update(visible=False),""
1172
+ return (gr.update(visible=False),gr.update(value=read_article(url),visible=True),gr.update(visible=True),"")
1173
  def show_list(c):
1174
+ return (gr.update(value=fetch_news_list(c),visible=True),gr.update(visible=False),gr.update(visible=False))
1175
+ read_btn.click(fn=show_article,inputs=[article_url],outputs=[news_list,article_view,back_btn,article_url])
1176
+ back_btn.click(fn=show_list,inputs=[cat],outputs=[news_list,article_view,back_btn])
1177
+ ref_btn.click(fn=show_list,inputs=[cat],outputs=[news_list,article_view,back_btn])
1178
+ cat.change(fn=show_list,inputs=[cat],outputs=[news_list,article_view,back_btn])
1179
  timer=gr.Timer(value=REFRESH_SECONDS,active=True)
1180
  timer.tick(fn=fetch_news_list,inputs=cat,outputs=news_list)
1181
  demo.load(fn=fetch_news_list,inputs=cat,outputs=news_list)