bep40 commited on
Commit
2e5ded4
·
verified ·
1 Parent(s): ecfc3fd

revert: app_wrapper.py back to previous (shorts routing, no comment/share changes)

Browse files
Files changed (1) hide show
  1. app_wrapper.py +112 -120
app_wrapper.py CHANGED
@@ -16,17 +16,6 @@ SHORTS_EXTRA_CSS = """
16
  .tiktok-fullscreen-container.fullheight-mode .tiktok-video{object-fit:cover!important}
17
  .tiktok-fullheight-btn{position:absolute;bottom:70px;right:12px;background:rgba(255,255,255,.15);color:#fff;border:none;padding:6px 12px;border-radius:16px;font-size:11px;cursor:pointer;z-index:8;backdrop-filter:blur(6px)}
18
  .tiktok-fullheight-btn.active{background:rgba(92,184,122,.5)}
19
- /* Comment button on right side */
20
- .tiktok-comment-btn{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:rgba(255,255,255,.15);color:#fff;border:none;width:44px;height:44px;border-radius:50%;font-size:20px;cursor:pointer;z-index:8;backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;transition:background .2s}
21
- .tiktok-comment-btn:hover{background:rgba(255,255,255,.3)}
22
- .tiktok-comment-panel{position:absolute;right:0;top:0;bottom:0;width:280px;background:rgba(17,17,17,.95);z-index:20;padding:12px;overflow-y:auto;transform:translateX(100%);transition:transform .3s;border-left:1px solid #333}
23
- .tiktok-comment-panel.open{transform:translateX(0)}
24
- .tiktok-comment-panel h4{color:#fff;font-size:14px;margin:0 0 10px}
25
- .tiktok-comment-panel .cmt-input{width:100%;padding:8px;background:#222;border:1px solid #444;border-radius:6px;color:#eee;font-size:12px;margin-bottom:6px;box-sizing:border-box}
26
- .tiktok-comment-panel .cmt-send{background:#2d8659;color:#fff;border:none;padding:6px 14px;border-radius:14px;font-size:12px;cursor:pointer}
27
- .tiktok-comment-panel .cmt-list{margin-top:10px}
28
- .tiktok-comment-panel .cmt-item{background:#1a1a1a;border-radius:6px;padding:6px 8px;margin-bottom:6px;font-size:11px;color:#ccc}
29
- .tiktok-comment-panel .cmt-close{position:absolute;top:8px;right:8px;background:none;border:none;color:#aaa;font-size:18px;cursor:pointer}
30
  """
31
 
32
  FULLWIDTH_CSS = """
@@ -59,21 +48,27 @@ if "shorts-feed-mode" not in app.CSS:
59
  if "FULLWIDTH: kill all Gradio padding" not in app.CSS:
60
  app.CSS += FULLWIDTH_CSS
61
 
62
- # ══ Override app._extract_24h_video_urls ══
63
  def _fixed_extract_24h_video_urls(article_url):
64
  vid = _extract_24h_video_url(article_url)
65
- if not vid: return []
 
66
  all_parts = vid.get("all_parts", [vid["src"]])
67
- return [{"src": p, "poster": vid.get("poster",""), "vtype": "hls"} for p in all_parts]
 
68
  app._extract_24h_video_urls = _fixed_extract_24h_video_urls
69
 
70
- # ══ Fix homepage slider ══
71
  _orig_render_video_slider = getattr(app, 'render_video_slider_html', None)
72
  if _orig_render_video_slider:
73
  def _patched_render_video_slider(videos):
74
  html = _orig_render_video_slider(videos)
75
  html = html.replace("window.bdpOpenTikTok(", "window.bdpOpen(")
76
- html = _re.sub(r"window\.bdpOpen\('([^']+)','([^']+)'\)", r"window.bdpOpen('\1','\2','')", html)
 
 
 
 
77
  return html
78
  app.render_video_slider_html = _patched_render_video_slider
79
 
@@ -85,13 +80,13 @@ def _patched_render_homepage(articles, *args, **kwargs):
85
  shorts = scrape_24h_news_shorts()[:15]
86
  shorts_html = render_shorts_carousel(shorts, app.esc, app.safe_url, app.make_id, app.slug)
87
  if shorts_html:
88
- pt = html.find('<div class="vslide-wrap">')
89
- if pt > 0:
90
- html = html[:pt] + shorts_html + html[pt:]
91
  else:
92
- pt = html.find('<div class="bdp-wrap">') + len('<div class="bdp-wrap">')
93
- if pt > len('<div class="bdp-wrap">'):
94
- html = html[:pt] + shorts_html + html[pt:]
95
  except Exception as e:
96
  print(f"[shorts] Error: {e}")
97
  return html
@@ -106,131 +101,161 @@ def _patched_render_video_page():
106
  return html
107
  app.render_video_page_html = _patched_render_video_page
108
 
109
- # ══ Patch read_article ══
110
  _orig_read_article = app.read_article
111
  def _patched_read_article(url):
112
  if not url or url == "#" or len(url) < 10:
113
  return "<p>Không tìm thấy bài viết.</p>"
114
  if "24h.com.vn" in url:
115
  is_highlight = "/bong-da/video-bong-da-" in url or "video-highlight" in url
116
- result = _render_highlight_tiktok_feed(url) if is_highlight else _render_shorts_tiktok_feed(url)
117
- if result: return result
 
 
 
 
118
  if "bongdaplus.vn/video/" in url:
119
  result = _render_bdp_tiktok_feed(url)
120
- if result: return result
 
121
  return _orig_read_article(url)
122
 
123
  def _render_bdp_tiktok_feed(current_url):
124
  from concurrent.futures import ThreadPoolExecutor, as_completed
125
- try: bdp_list = app.scrape_bdp_video_list()[:15]
126
- except: return None
127
- if not bdp_list: return None
128
- def _f(art):
 
 
 
129
  vid_id = app._extract_bdp_video_id(art["link"])
130
- if not vid_id: return []
131
  embed = app.fetch_bdp_embed_data(vid_id)
132
- if not embed or not embed.get("mp4"): return []
133
  return [{"title": art["title"], "link": art["link"], "img": art.get("img",""),
134
  "src": embed["mp4"], "poster": embed.get("poster",""), "vtype": "mp4"}]
135
  videos = []
136
  with ThreadPoolExecutor(max_workers=6) as ex:
137
- for f in as_completed({ex.submit(_f, a): a for a in bdp_list}):
 
138
  try:
139
  r = f.result()
140
  if r: videos.extend(r)
141
  except: pass
142
- return _build_tiktok_html(videos, current_url, "BDP") if videos else None
 
 
143
 
144
  def _render_shorts_tiktok_feed(current_url):
 
145
  from concurrent.futures import ThreadPoolExecutor, as_completed
146
- try: all_articles = scrape_24h_news_shorts()[:15]
147
- except: return None
148
- if not all_articles: return None
149
- def _f(art):
 
 
 
150
  vid = _extract_24h_video_url(art["link"])
151
- if not vid: return []
152
- return [{"title": art["title"], "link": art["link"], "img": art.get("img",""),
153
- "src": vid["src"], "poster": vid["poster"], "vtype": vid["vtype"]}]
 
 
154
  videos = []
155
  with ThreadPoolExecutor(max_workers=8) as ex:
156
- for f in as_completed({ex.submit(_f, a): a for a in all_articles}):
 
157
  try:
158
  r = f.result()
159
  if r: videos.extend(r)
160
  except: pass
161
- return _build_tiktok_html(videos, current_url, "24h") if videos else None
 
 
162
 
163
  def _render_highlight_tiktok_feed(current_url):
 
164
  from concurrent.futures import ThreadPoolExecutor, as_completed
165
- try: all_articles = app.scrape_24h_video_list()[:20]
166
- except: return None
167
- if not all_articles: return None
168
- def _f(art):
 
 
 
169
  vid = _extract_24h_video_url(art["link"])
170
- if not vid: return []
171
- parts = vid.get("all_parts", [vid["src"]])
172
- return [{"title": art["title"]+(f" (P{i+1})" if len(parts)>1 else ""), "link": art["link"],
173
- "img": art.get("img",""), "src": p, "poster": vid["poster"], "vtype": vid["vtype"]}
174
- for i, p in enumerate(parts)]
 
 
 
 
 
 
 
175
  videos = []
176
  with ThreadPoolExecutor(max_workers=8) as ex:
177
- for f in as_completed({ex.submit(_f, a): a for a in all_articles}):
 
178
  try:
179
  r = f.result()
180
  if r: videos.extend(r)
181
  except: pass
182
- return _build_tiktok_html(videos, current_url, "24h") if videos else None
 
 
183
 
184
  def _build_tiktok_html(videos, current_url, source_label):
185
- ca = app.make_id(current_url)
186
- ordered = [v for v in videos if app.make_id(v["link"])==ca] + [v for v in videos if app.make_id(v["link"])!=ca]
187
- badge = "bdp-badge-24h" if source_label=="24h" else "bdp-badge-bdp"
 
 
 
 
 
 
 
 
188
  is_shorts = source_label == "24h"
189
 
190
  slides = []
191
  for vi, v in enumerate(ordered):
192
- po = app.safe_url(v.get("poster",""))
193
- pa = f' poster="{po}"' if po else ""
194
- vs = v["src"]
195
  aid = app.make_id(v["link"])
196
  sl = app.slug(v["title"])
197
- # Share includes image URL for og:image when platform supports it
198
- img_for_share = app.safe_url(v.get("img") or v.get("poster",""))
199
- sj = f"event.stopPropagation();window.bdpShareWithImage('{app.esc(v['title'])}','{sl}','{aid}','{img_for_share}')"
200
- vt = f'<video class="tiktok-video" playsinline preload="metadata"{pa} data-hls-src="{vs}" muted loop></video>' if v.get("vtype")=="hls" else f'<video class="tiktok-video" playsinline preload="metadata"{pa} muted loop><source src="{app.safe_url(vs)}" type="video/mp4"></video>'
 
201
 
202
- fh_btn = ''
203
  if not is_shorts:
204
- fh_btn = '<button class="tiktok-fullheight-btn" onclick="event.stopPropagation();var c=this.closest(\'.tiktok-fullscreen-container\');c.classList.toggle(\'fullheight-mode\');var on=c.classList.contains(\'fullheight-mode\');c.querySelectorAll(\'.tiktok-fullheight-btn\').forEach(function(b){b.classList.toggle(\'active\',on);b.textContent=on?\'\\u2B07 Contain\':\'\\u2922 Fullheight\';});">\u2922 Fullheight</button>'
205
-
206
- # Comment button + panel
207
- cmt_btn = f'<button class="tiktok-comment-btn" onclick="event.stopPropagation();var p=this.nextElementSibling;p.classList.toggle(\'open\');">\U0001f4ac</button>'
208
- cmt_panel = f'''<div class="tiktok-comment-panel" onclick="event.stopPropagation();">
209
- <button class="cmt-close" onclick="this.parentElement.classList.remove('open')">\u2715</button>
210
- <h4>\U0001f4ac B\u00ecnh lu\u1eadn</h4>
211
- <div class="cmt-list" id="tiktok-cmt-{aid}"></div>
212
- <input class="cmt-input" id="tiktok-cmt-name-{aid}" placeholder="T\u00ean..." maxlength="30">
213
- <textarea class="cmt-input" id="tiktok-cmt-text-{aid}" placeholder="Vi\u1ebft b\u00ecnh lu\u1eadn..." rows="2" maxlength="300" style="resize:none"></textarea>
214
- <button class="cmt-send" onclick="window.bdpAddCmt('{aid}');window.bdpRenderCmtTiktok('{aid}');">G\u1eedi</button>
215
- </div>'''
216
 
217
  slides.append(f'''<div class="tiktok-slide" data-index="{vi}" data-aid="{aid}">
218
- {vt}
219
  <div class="tiktok-pause-icon">\u25b6</div>
220
  <div class="tiktok-seek-controls">
221
  <button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,-10)">\u23ea 10s</button>
222
  <button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,10)">10s \u23e9</button>
223
  </div>
224
  <div class="tiktok-bottom">
225
- <span class="bdp-badge {badge}">{source_label}</span>
226
  <p class="tiktok-title">{v["title"]}</p>
227
  <div class="tiktok-actions">
228
- <button class="tiktok-action-btn" onclick="{sj}">\U0001f4e4</button>
229
  </div>
230
  </div>
231
- {fh_btn}
232
- {cmt_btn}
233
- {cmt_panel}
234
  <div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">\U0001f507 B\u1eadt ti\u1ebfng</div>
235
  <span class="tiktok-counter">{vi+1}/{len(ordered)}</span>
236
  </div>''')
@@ -243,53 +268,20 @@ def _build_tiktok_html(videos, current_url, source_label):
243
  click = f"window.bdpOpen('{app.esc(v['link'])}','{aid}','{sl}')"
244
  list_cards.append(f'''<div class="bdp-card" onclick="{click}">
245
  <div class="bdp-card-img"><img src="{img}" alt="" class="bdp-lazy-img"><div class="bdp-play-overlay">\u25b6</div></div>
246
- <div class="bdp-card-body"><span class="bdp-badge {badge}">{source_label}</span>
247
  <h3 class="bdp-card-title">{v["title"]}</h3></div></div>''')
248
 
249
- grid = f'<div class="bdp-wrap"><div class="bdp-topbar"><span>\U0001f3ac Video</span><span>{len(ordered)} video</span></div><div class="bdp-grid">{"".join(list_cards)}</div></div>'
 
250
 
251
  mode_class = "shorts-feed-mode" if is_shorts else ""
252
  return f'''<div class="tiktok-fullscreen-container {mode_class}" id="tiktok-fullscreen-feed">
253
  <div class="tiktok-fullscreen-feed">{''.join(slides)}</div>
254
  </div>
255
- {grid}'''
256
 
257
  app.read_article = _patched_read_article
258
 
259
- # ══ Patch JS: add bdpShareWithImage and bdpRenderCmtTiktok functions ══
260
- # These are added via CSS (actually head meta won't work, but we can add to JS_FUNC)
261
- _orig_js = app.JS_FUNC
262
- _extra_js = """
263
- /* Share with image for og:image */
264
- window.bdpShareWithImage=async function(title,sl,aid,imgUrl){
265
- var hashUrl=SPACE_BASE+'/#/'+sl+'/'+aid;
266
- var sd={title:title,url:hashUrl,text:title};
267
- if(navigator.share){try{await navigator.share(sd);return;}catch(e){if(e.name==='AbortError')return;}}
268
- if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(hashUrl);window.bdpToast('\\u2705 \\u0110\\u00e3 sao ch\\u00e9p li\\u00ean k\\u1ebft!');return;}catch(e){}}
269
- var ta=document.createElement('textarea');ta.value=hashUrl;ta.style.cssText='position:fixed;opacity:0';
270
- document.body.appendChild(ta);ta.focus();ta.select();
271
- try{document.execCommand('copy');window.bdpToast('\\u2705 \\u0110\\u00e3 sao ch\\u00e9p!');}catch(e){}
272
- document.body.removeChild(ta);
273
- };
274
- /* Render comments in TikTok panel */
275
- window.bdpRenderCmtTiktok=function(a){
276
- var l=document.getElementById('tiktok-cmt-'+a);if(!l)return;
277
- try{var c=JSON.parse(localStorage.getItem('bdp_cmt_'+a))||[];}catch(e){var c=[];}
278
- if(!c.length){l.innerHTML='<div style="color:#666;font-size:11px;">Ch\\u01b0a c\\u00f3 b\\u00ecnh lu\\u1eadn</div>';return;}
279
- var h='';for(var i=c.length-1;i>=0;i--){var x=c[i];h+='<div class="cmt-item"><b style="color:#5cb87a">'+x.name+'</b> <span style="color:#555;font-size:10px">'+x.date+'</span><div>'+x.text.replace(/</g,'&lt;')+'</div></div>';}
280
- l.innerHTML=h;
281
- };
282
- """
283
-
284
- # Inject extra JS before the closing } of the JS_FUNC
285
- if "bdpShareWithImage" not in _orig_js:
286
- # Insert before the last closing brace and the hash routing section
287
- insert_point = _orig_js.rfind("var hh=window.location.hash")
288
- if insert_point > 0:
289
- app.JS_FUNC = _orig_js[:insert_point] + _extra_js + "\n" + _orig_js[insert_point:]
290
- else:
291
- app.JS_FUNC = _orig_js.rstrip().rstrip("}") + _extra_js + "\n}"
292
-
293
  # Patch CSS on demo
294
  app.demo.css = app.CSS
295
 
 
16
  .tiktok-fullscreen-container.fullheight-mode .tiktok-video{object-fit:cover!important}
17
  .tiktok-fullheight-btn{position:absolute;bottom:70px;right:12px;background:rgba(255,255,255,.15);color:#fff;border:none;padding:6px 12px;border-radius:16px;font-size:11px;cursor:pointer;z-index:8;backdrop-filter:blur(6px)}
18
  .tiktok-fullheight-btn.active{background:rgba(92,184,122,.5)}
 
 
 
 
 
 
 
 
 
 
 
19
  """
20
 
21
  FULLWIDTH_CSS = """
 
48
  if "FULLWIDTH: kill all Gradio padding" not in app.CSS:
49
  app.CSS += FULLWIDTH_CSS
50
 
51
+ # ══ Override app._extract_24h_video_urls with fixed multi-part version ══
52
  def _fixed_extract_24h_video_urls(article_url):
53
  vid = _extract_24h_video_url(article_url)
54
+ if not vid:
55
+ return []
56
  all_parts = vid.get("all_parts", [vid["src"]])
57
+ poster = vid.get("poster", "")
58
+ return [{"src": p, "poster": poster, "vtype": "hls"} for p in all_parts]
59
  app._extract_24h_video_urls = _fixed_extract_24h_video_urls
60
 
61
+ # ══ Fix homepage slider: bdpOpenTikTok → bdpOpen with 3 params ══
62
  _orig_render_video_slider = getattr(app, 'render_video_slider_html', None)
63
  if _orig_render_video_slider:
64
  def _patched_render_video_slider(videos):
65
  html = _orig_render_video_slider(videos)
66
  html = html.replace("window.bdpOpenTikTok(", "window.bdpOpen(")
67
+ html = _re.sub(
68
+ r"window\.bdpOpen\('([^']+)','([^']+)'\)",
69
+ r"window.bdpOpen('\1','\2','')",
70
+ html
71
+ )
72
  return html
73
  app.render_video_slider_html = _patched_render_video_slider
74
 
 
80
  shorts = scrape_24h_news_shorts()[:15]
81
  shorts_html = render_shorts_carousel(shorts, app.esc, app.safe_url, app.make_id, app.slug)
82
  if shorts_html:
83
+ insert_point = html.find('<div class="vslide-wrap">')
84
+ if insert_point > 0:
85
+ html = html[:insert_point] + shorts_html + html[insert_point:]
86
  else:
87
+ insert_point = html.find('<div class="bdp-wrap">') + len('<div class="bdp-wrap">')
88
+ if insert_point > len('<div class="bdp-wrap">'):
89
+ html = html[:insert_point] + shorts_html + html[insert_point:]
90
  except Exception as e:
91
  print(f"[shorts] Error: {e}")
92
  return html
 
101
  return html
102
  app.render_video_page_html = _patched_render_video_page
103
 
104
+ # ══ Patch read_article: video URLs → TikTok feed ══
105
  _orig_read_article = app.read_article
106
  def _patched_read_article(url):
107
  if not url or url == "#" or len(url) < 10:
108
  return "<p>Không tìm thấy bài viết.</p>"
109
  if "24h.com.vn" in url:
110
  is_highlight = "/bong-da/video-bong-da-" in url or "video-highlight" in url
111
+ if is_highlight:
112
+ result = _render_highlight_tiktok_feed(url)
113
+ else:
114
+ result = _render_shorts_tiktok_feed(url)
115
+ if result:
116
+ return result
117
  if "bongdaplus.vn/video/" in url:
118
  result = _render_bdp_tiktok_feed(url)
119
+ if result:
120
+ return result
121
  return _orig_read_article(url)
122
 
123
  def _render_bdp_tiktok_feed(current_url):
124
  from concurrent.futures import ThreadPoolExecutor, as_completed
125
+ try:
126
+ bdp_list = app.scrape_bdp_video_list()[:15]
127
+ except:
128
+ return None
129
+ if not bdp_list:
130
+ return None
131
+ def _fetch_bdp(art):
132
  vid_id = app._extract_bdp_video_id(art["link"])
133
+ if not vid_id: return None
134
  embed = app.fetch_bdp_embed_data(vid_id)
135
+ if not embed or not embed.get("mp4"): return None
136
  return [{"title": art["title"], "link": art["link"], "img": art.get("img",""),
137
  "src": embed["mp4"], "poster": embed.get("poster",""), "vtype": "mp4"}]
138
  videos = []
139
  with ThreadPoolExecutor(max_workers=6) as ex:
140
+ futures = {ex.submit(_fetch_bdp, a): a for a in bdp_list}
141
+ for f in as_completed(futures):
142
  try:
143
  r = f.result()
144
  if r: videos.extend(r)
145
  except: pass
146
+ if not videos:
147
+ return None
148
+ return _build_tiktok_html(videos, current_url, "BDP")
149
 
150
  def _render_shorts_tiktok_feed(current_url):
151
+ """TikTok feed using SHORTS articles (from video-tin-tuc page)."""
152
  from concurrent.futures import ThreadPoolExecutor, as_completed
153
+ try:
154
+ all_articles = scrape_24h_news_shorts()[:15]
155
+ except:
156
+ all_articles = []
157
+ if not all_articles:
158
+ return None
159
+ def _fetch_video(art):
160
  vid = _extract_24h_video_url(art["link"])
161
+ if not vid:
162
+ return []
163
+ return [{"title": art["title"], "link": art["link"],
164
+ "img": art.get("img", ""), "src": vid["src"],
165
+ "poster": vid["poster"], "vtype": vid["vtype"]}]
166
  videos = []
167
  with ThreadPoolExecutor(max_workers=8) as ex:
168
+ futures = {ex.submit(_fetch_video, a): a for a in all_articles}
169
+ for f in as_completed(futures):
170
  try:
171
  r = f.result()
172
  if r: videos.extend(r)
173
  except: pass
174
+ if not videos:
175
+ return None
176
+ return _build_tiktok_html(videos, current_url, "24h")
177
 
178
  def _render_highlight_tiktok_feed(current_url):
179
+ """TikTok feed using HIGHLIGHT articles (video-bong-da)."""
180
  from concurrent.futures import ThreadPoolExecutor, as_completed
181
+ try:
182
+ all_articles = app.scrape_24h_video_list()[:20]
183
+ except:
184
+ all_articles = []
185
+ if not all_articles:
186
+ return None
187
+ def _fetch_video(art):
188
  vid = _extract_24h_video_url(art["link"])
189
+ if not vid:
190
+ return []
191
+ results = []
192
+ all_parts = vid.get("all_parts", [vid["src"]])
193
+ for pi, part_url in enumerate(all_parts):
194
+ label = f" (Phần {pi+1})" if len(all_parts) > 1 else ""
195
+ results.append({
196
+ "title": art["title"] + label, "link": art["link"],
197
+ "img": art.get("img", ""), "src": part_url,
198
+ "poster": vid["poster"], "vtype": vid["vtype"]
199
+ })
200
+ return results
201
  videos = []
202
  with ThreadPoolExecutor(max_workers=8) as ex:
203
+ futures = {ex.submit(_fetch_video, a): a for a in all_articles}
204
+ for f in as_completed(futures):
205
  try:
206
  r = f.result()
207
  if r: videos.extend(r)
208
  except: pass
209
+ if not videos:
210
+ return None
211
+ return _build_tiktok_html(videos, current_url, "24h")
212
 
213
  def _build_tiktok_html(videos, current_url, source_label):
214
+ current_aid = app.make_id(current_url)
215
+ ordered = []
216
+ rest = []
217
+ for v in videos:
218
+ if app.make_id(v["link"]) == current_aid:
219
+ ordered.append(v)
220
+ else:
221
+ rest.append(v)
222
+ ordered.extend(rest)
223
+
224
+ badge_cls = "bdp-badge-24h" if source_label == "24h" else "bdp-badge-bdp"
225
  is_shorts = source_label == "24h"
226
 
227
  slides = []
228
  for vi, v in enumerate(ordered):
229
+ poster = app.safe_url(v.get("poster", ""))
230
+ poster_attr = f' poster="{poster}"' if poster else ""
231
+ vsrc = v["src"]
232
  aid = app.make_id(v["link"])
233
  sl = app.slug(v["title"])
234
+ share_js = f"event.stopPropagation();window.bdpShareHash('{app.esc(v['title'])}','{sl}','{aid}')"
235
+ if v.get("vtype") == "hls":
236
+ vtag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} data-hls-src="{vsrc}" muted loop></video>'
237
+ else:
238
+ vtag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} muted loop><source src="{app.safe_url(vsrc)}" type="video/mp4"></video>'
239
 
240
+ fullheight_btn = ''
241
  if not is_shorts:
242
+ fullheight_btn = '<button class="tiktok-fullheight-btn" onclick="event.stopPropagation();var c=this.closest(\'.tiktok-fullscreen-container\');c.classList.toggle(\'fullheight-mode\');var on=c.classList.contains(\'fullheight-mode\');c.querySelectorAll(\'.tiktok-fullheight-btn\').forEach(function(b){b.classList.toggle(\'active\',on);b.textContent=on?\'\\u2B07 Contain\':\'\\u2922 Fullheight\';});">\u2922 Fullheight</button>'
 
 
 
 
 
 
 
 
 
 
 
243
 
244
  slides.append(f'''<div class="tiktok-slide" data-index="{vi}" data-aid="{aid}">
245
+ {vtag}
246
  <div class="tiktok-pause-icon">\u25b6</div>
247
  <div class="tiktok-seek-controls">
248
  <button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,-10)">\u23ea 10s</button>
249
  <button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,10)">10s \u23e9</button>
250
  </div>
251
  <div class="tiktok-bottom">
252
+ <span class="bdp-badge {badge_cls}">{source_label}</span>
253
  <p class="tiktok-title">{v["title"]}</p>
254
  <div class="tiktok-actions">
255
+ <button class="tiktok-action-btn" onclick="{share_js}">\U0001f4e4 Chia s\u1ebb</button>
256
  </div>
257
  </div>
258
+ {fullheight_btn}
 
 
259
  <div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">\U0001f507 B\u1eadt ti\u1ebfng</div>
260
  <span class="tiktok-counter">{vi+1}/{len(ordered)}</span>
261
  </div>''')
 
268
  click = f"window.bdpOpen('{app.esc(v['link'])}','{aid}','{sl}')"
269
  list_cards.append(f'''<div class="bdp-card" onclick="{click}">
270
  <div class="bdp-card-img"><img src="{img}" alt="" class="bdp-lazy-img"><div class="bdp-play-overlay">\u25b6</div></div>
271
+ <div class="bdp-card-body"><span class="bdp-badge {badge_cls}">{source_label}</span>
272
  <h3 class="bdp-card-title">{v["title"]}</h3></div></div>''')
273
 
274
+ grid_html = f'''<div class="bdp-wrap"><div class="bdp-topbar"><span>\U0001f3ac Video</span><span>{len(ordered)} video</span></div>
275
+ <div class="bdp-grid">{''.join(list_cards)}</div></div>'''
276
 
277
  mode_class = "shorts-feed-mode" if is_shorts else ""
278
  return f'''<div class="tiktok-fullscreen-container {mode_class}" id="tiktok-fullscreen-feed">
279
  <div class="tiktok-fullscreen-feed">{''.join(slides)}</div>
280
  </div>
281
+ {grid_html}'''
282
 
283
  app.read_article = _patched_read_article
284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  # Patch CSS on demo
286
  app.demo.css = app.CSS
287