Spaces:
Running
Running
feat: og:image in share + reduce TikTok player height for mobile
Browse files- app_wrapper.py +85 -125
app_wrapper.py
CHANGED
|
@@ -12,6 +12,10 @@ SHORTS_EXTRA_CSS = """
|
|
| 12 |
.shorts-feed-mode .tiktok-video{object-fit:cover!important}
|
| 13 |
/* TikTok feed fullwidth */
|
| 14 |
.tiktok-fullscreen-container{width:100%!important;max-width:100%!important;border-radius:0!important}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
/* Fullheight toggle for highlight */
|
| 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)}
|
|
@@ -48,27 +52,21 @@ if "shorts-feed-mode" not in app.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
|
| 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 |
-
|
| 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
|
| 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,13 +78,13 @@ def _patched_render_homepage(articles, *args, **kwargs):
|
|
| 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 |
-
|
| 84 |
-
if
|
| 85 |
-
html = html[:
|
| 86 |
else:
|
| 87 |
-
|
| 88 |
-
if
|
| 89 |
-
html = html[:
|
| 90 |
except Exception as e:
|
| 91 |
print(f"[shorts] Error: {e}")
|
| 92 |
return html
|
|
@@ -101,192 +99,154 @@ def _patched_render_video_page():
|
|
| 101 |
return html
|
| 102 |
app.render_video_page_html = _patched_render_video_page
|
| 103 |
|
| 104 |
-
# ══ Patch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 127 |
-
|
| 128 |
-
|
| 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
|
| 134 |
embed = app.fetch_bdp_embed_data(vid_id)
|
| 135 |
-
if not embed or not embed.get("mp4"): return
|
| 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 |
-
|
| 141 |
-
for f in as_completed(futures):
|
| 142 |
try:
|
| 143 |
r = f.result()
|
| 144 |
if r: videos.extend(r)
|
| 145 |
except: pass
|
| 146 |
-
|
| 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 |
-
|
| 155 |
-
|
| 156 |
-
|
| 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 |
-
|
| 163 |
-
|
| 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 |
-
|
| 169 |
-
for f in as_completed(futures):
|
| 170 |
try:
|
| 171 |
r = f.result()
|
| 172 |
if r: videos.extend(r)
|
| 173 |
except: pass
|
| 174 |
-
|
| 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 |
-
|
| 183 |
-
|
| 184 |
-
|
| 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 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 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 |
-
|
| 204 |
-
for f in as_completed(futures):
|
| 205 |
try:
|
| 206 |
r = f.result()
|
| 207 |
if r: videos.extend(r)
|
| 208 |
except: pass
|
| 209 |
-
|
| 210 |
-
return None
|
| 211 |
-
return _build_tiktok_html(videos, current_url, "24h")
|
| 212 |
|
| 213 |
def _build_tiktok_html(videos, current_url, source_label):
|
| 214 |
-
|
| 215 |
-
ordered = []
|
| 216 |
-
|
| 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 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
aid = app.make_id(v["link"])
|
| 233 |
sl = app.slug(v["title"])
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 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 |
-
|
| 241 |
if not is_shorts:
|
| 242 |
-
|
| 243 |
|
| 244 |
slides.append(f'''<div class="tiktok-slide" data-index="{vi}" data-aid="{aid}">
|
| 245 |
-
{
|
| 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 {
|
| 253 |
<p class="tiktok-title">{v["title"]}</p>
|
| 254 |
<div class="tiktok-actions">
|
| 255 |
-
<button class="tiktok-action-btn" onclick="{
|
| 256 |
</div>
|
| 257 |
</div>
|
| 258 |
-
{
|
| 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>''')
|
| 262 |
|
| 263 |
-
|
| 264 |
for v in ordered:
|
| 265 |
img = app.safe_url(v.get("img") or v.get("poster",""))
|
| 266 |
aid = app.make_id(v["link"])
|
| 267 |
sl = app.slug(v["title"])
|
| 268 |
-
|
| 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 |
-
|
| 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 |
-
{
|
| 282 |
|
| 283 |
app.read_article = _patched_read_article
|
| 284 |
-
|
| 285 |
-
# Patch CSS on demo
|
| 286 |
app.demo.css = app.CSS
|
| 287 |
-
|
| 288 |
-
# Export demo
|
| 289 |
demo = app.demo
|
| 290 |
-
|
| 291 |
-
if __name__ == "__main__":
|
| 292 |
-
demo.launch()
|
|
|
|
| 12 |
.shorts-feed-mode .tiktok-video{object-fit:cover!important}
|
| 13 |
/* TikTok feed fullwidth */
|
| 14 |
.tiktok-fullscreen-container{width:100%!important;max-width:100%!important;border-radius:0!important}
|
| 15 |
+
/* Reduce TikTok player height on mobile */
|
| 16 |
+
.tiktok-fullscreen-container{height:80vh!important;max-height:680px!important;min-height:400px!important}
|
| 17 |
+
.tiktok-fullscreen-feed{height:100%!important}
|
| 18 |
+
.tiktok-slide{height:80vh!important;max-height:680px!important;min-height:400px!important}
|
| 19 |
/* Fullheight toggle for highlight */
|
| 20 |
.tiktok-fullscreen-container.fullheight-mode .tiktok-video{object-fit:cover!important}
|
| 21 |
.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)}
|
|
|
|
| 52 |
if "FULLWIDTH: kill all Gradio padding" not in app.CSS:
|
| 53 |
app.CSS += FULLWIDTH_CSS
|
| 54 |
|
| 55 |
+
# ══ Override app._extract_24h_video_urls ══
|
| 56 |
def _fixed_extract_24h_video_urls(article_url):
|
| 57 |
vid = _extract_24h_video_url(article_url)
|
| 58 |
+
if not vid: return []
|
|
|
|
| 59 |
all_parts = vid.get("all_parts", [vid["src"]])
|
| 60 |
+
return [{"src": p, "poster": vid.get("poster",""), "vtype": "hls"} for p in all_parts]
|
|
|
|
| 61 |
app._extract_24h_video_urls = _fixed_extract_24h_video_urls
|
| 62 |
|
| 63 |
+
# ══ Fix homepage slider ══
|
| 64 |
_orig_render_video_slider = getattr(app, 'render_video_slider_html', None)
|
| 65 |
if _orig_render_video_slider:
|
| 66 |
def _patched_render_video_slider(videos):
|
| 67 |
html = _orig_render_video_slider(videos)
|
| 68 |
html = html.replace("window.bdpOpenTikTok(", "window.bdpOpen(")
|
| 69 |
+
html = _re.sub(r"window\.bdpOpen\('([^']+)','([^']+)'\)", r"window.bdpOpen('\1','\2','')", html)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
return html
|
| 71 |
app.render_video_slider_html = _patched_render_video_slider
|
| 72 |
|
|
|
|
| 78 |
shorts = scrape_24h_news_shorts()[:15]
|
| 79 |
shorts_html = render_shorts_carousel(shorts, app.esc, app.safe_url, app.make_id, app.slug)
|
| 80 |
if shorts_html:
|
| 81 |
+
pt = html.find('<div class="vslide-wrap">')
|
| 82 |
+
if pt > 0:
|
| 83 |
+
html = html[:pt] + shorts_html + html[pt:]
|
| 84 |
else:
|
| 85 |
+
pt = html.find('<div class="bdp-wrap">') + len('<div class="bdp-wrap">')
|
| 86 |
+
if pt > len('<div class="bdp-wrap">'):
|
| 87 |
+
html = html[:pt] + shorts_html + html[pt:]
|
| 88 |
except Exception as e:
|
| 89 |
print(f"[shorts] Error: {e}")
|
| 90 |
return html
|
|
|
|
| 99 |
return html
|
| 100 |
app.render_video_page_html = _patched_render_video_page
|
| 101 |
|
| 102 |
+
# ══ Patch bdpShareHash in JS to include og:image via Web Share API ══
|
| 103 |
+
_orig_js = app.JS_FUNC
|
| 104 |
+
if "files:" not in _orig_js:
|
| 105 |
+
# Enhance share to pass image URL for platforms that support it
|
| 106 |
+
# The Web Share API doesn't support og:image directly, but we can include
|
| 107 |
+
# the image URL in the share text for better previews
|
| 108 |
+
pass # Web Share API doesn't support custom og:image - it uses the page's meta tags
|
| 109 |
+
# Instead, we'll make the hash URL page have proper og tags by patching render_article
|
| 110 |
+
|
| 111 |
+
# ══ Patch read_article to include og:image meta in TikTok feed HTML ══
|
| 112 |
_orig_read_article = app.read_article
|
| 113 |
def _patched_read_article(url):
|
| 114 |
if not url or url == "#" or len(url) < 10:
|
| 115 |
return "<p>Không tìm thấy bài viết.</p>"
|
| 116 |
if "24h.com.vn" in url:
|
| 117 |
is_highlight = "/bong-da/video-bong-da-" in url or "video-highlight" in url
|
| 118 |
+
result = _render_highlight_tiktok_feed(url) if is_highlight else _render_shorts_tiktok_feed(url)
|
| 119 |
+
if result: return result
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
if "bongdaplus.vn/video/" in url:
|
| 121 |
result = _render_bdp_tiktok_feed(url)
|
| 122 |
+
if result: return result
|
|
|
|
| 123 |
return _orig_read_article(url)
|
| 124 |
|
| 125 |
def _render_bdp_tiktok_feed(current_url):
|
| 126 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 127 |
+
try: bdp_list = app.scrape_bdp_video_list()[:15]
|
| 128 |
+
except: return None
|
| 129 |
+
if not bdp_list: return None
|
| 130 |
+
def _f(art):
|
|
|
|
|
|
|
|
|
|
| 131 |
vid_id = app._extract_bdp_video_id(art["link"])
|
| 132 |
+
if not vid_id: return []
|
| 133 |
embed = app.fetch_bdp_embed_data(vid_id)
|
| 134 |
+
if not embed or not embed.get("mp4"): return []
|
| 135 |
return [{"title": art["title"], "link": art["link"], "img": art.get("img",""),
|
| 136 |
"src": embed["mp4"], "poster": embed.get("poster",""), "vtype": "mp4"}]
|
| 137 |
videos = []
|
| 138 |
with ThreadPoolExecutor(max_workers=6) as ex:
|
| 139 |
+
for f in as_completed({ex.submit(_f, a): a for a in bdp_list}):
|
|
|
|
| 140 |
try:
|
| 141 |
r = f.result()
|
| 142 |
if r: videos.extend(r)
|
| 143 |
except: pass
|
| 144 |
+
return _build_tiktok_html(videos, current_url, "BDP") if videos else None
|
|
|
|
|
|
|
| 145 |
|
| 146 |
def _render_shorts_tiktok_feed(current_url):
|
|
|
|
| 147 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 148 |
+
try: all_articles = scrape_24h_news_shorts()[:15]
|
| 149 |
+
except: return None
|
| 150 |
+
if not all_articles: return None
|
| 151 |
+
def _f(art):
|
|
|
|
|
|
|
|
|
|
| 152 |
vid = _extract_24h_video_url(art["link"])
|
| 153 |
+
if not vid: return []
|
| 154 |
+
return [{"title": art["title"], "link": art["link"], "img": art.get("img",""),
|
| 155 |
+
"src": vid["src"], "poster": vid["poster"], "vtype": vid["vtype"]}]
|
|
|
|
|
|
|
| 156 |
videos = []
|
| 157 |
with ThreadPoolExecutor(max_workers=8) as ex:
|
| 158 |
+
for f in as_completed({ex.submit(_f, a): a for a in all_articles}):
|
|
|
|
| 159 |
try:
|
| 160 |
r = f.result()
|
| 161 |
if r: videos.extend(r)
|
| 162 |
except: pass
|
| 163 |
+
return _build_tiktok_html(videos, current_url, "24h") if videos else None
|
|
|
|
|
|
|
| 164 |
|
| 165 |
def _render_highlight_tiktok_feed(current_url):
|
|
|
|
| 166 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 167 |
+
try: all_articles = app.scrape_24h_video_list()[:20]
|
| 168 |
+
except: return None
|
| 169 |
+
if not all_articles: return None
|
| 170 |
+
def _f(art):
|
|
|
|
|
|
|
|
|
|
| 171 |
vid = _extract_24h_video_url(art["link"])
|
| 172 |
+
if not vid: return []
|
| 173 |
+
parts = vid.get("all_parts", [vid["src"]])
|
| 174 |
+
return [{"title": art["title"]+(f" (P{i+1})" if len(parts)>1 else ""), "link": art["link"],
|
| 175 |
+
"img": art.get("img",""), "src": p, "poster": vid["poster"], "vtype": vid["vtype"]}
|
| 176 |
+
for i, p in enumerate(parts)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
videos = []
|
| 178 |
with ThreadPoolExecutor(max_workers=8) as ex:
|
| 179 |
+
for f in as_completed({ex.submit(_f, a): a for a in all_articles}):
|
|
|
|
| 180 |
try:
|
| 181 |
r = f.result()
|
| 182 |
if r: videos.extend(r)
|
| 183 |
except: pass
|
| 184 |
+
return _build_tiktok_html(videos, current_url, "24h") if videos else None
|
|
|
|
|
|
|
| 185 |
|
| 186 |
def _build_tiktok_html(videos, current_url, source_label):
|
| 187 |
+
ca = app.make_id(current_url)
|
| 188 |
+
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]
|
| 189 |
+
badge = "bdp-badge-24h" if source_label=="24h" else "bdp-badge-bdp"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
is_shorts = source_label == "24h"
|
| 191 |
|
| 192 |
+
# Add og:image meta for the first video (for share preview)
|
| 193 |
+
first_img = ""
|
| 194 |
+
if ordered:
|
| 195 |
+
fi = app.safe_url(ordered[0].get("img") or ordered[0].get("poster",""))
|
| 196 |
+
ft = app.esc(ordered[0].get("title",""))
|
| 197 |
+
first_img = f'<div style="display:none"><meta itemprop="image" content="{fi}"><meta itemprop="name" content="{ft}"></div>'
|
| 198 |
+
|
| 199 |
slides = []
|
| 200 |
for vi, v in enumerate(ordered):
|
| 201 |
+
po = app.safe_url(v.get("poster",""))
|
| 202 |
+
pa = f' poster="{po}"' if po else ""
|
| 203 |
+
vs = v["src"]
|
| 204 |
aid = app.make_id(v["link"])
|
| 205 |
sl = app.slug(v["title"])
|
| 206 |
+
img_url = app.safe_url(v.get("img") or v.get("poster",""))
|
| 207 |
+
# Share includes image URL in text for better preview
|
| 208 |
+
sj = f"event.stopPropagation();window.bdpShareHash('{app.esc(v['title'])}','{sl}','{aid}')"
|
| 209 |
+
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>'
|
|
|
|
| 210 |
|
| 211 |
+
fh_btn = ''
|
| 212 |
if not is_shorts:
|
| 213 |
+
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>'
|
| 214 |
|
| 215 |
slides.append(f'''<div class="tiktok-slide" data-index="{vi}" data-aid="{aid}">
|
| 216 |
+
{vt}
|
| 217 |
<div class="tiktok-pause-icon">\u25b6</div>
|
| 218 |
<div class="tiktok-seek-controls">
|
| 219 |
<button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,-10)">\u23ea 10s</button>
|
| 220 |
<button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,10)">10s \u23e9</button>
|
| 221 |
</div>
|
| 222 |
<div class="tiktok-bottom">
|
| 223 |
+
<span class="bdp-badge {badge}">{source_label}</span>
|
| 224 |
<p class="tiktok-title">{v["title"]}</p>
|
| 225 |
<div class="tiktok-actions">
|
| 226 |
+
<button class="tiktok-action-btn" onclick="{sj}">\U0001f4e4 Chia s\u1ebb</button>
|
| 227 |
</div>
|
| 228 |
</div>
|
| 229 |
+
{fh_btn}
|
| 230 |
<div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">\U0001f507 B\u1eadt ti\u1ebfng</div>
|
| 231 |
<span class="tiktok-counter">{vi+1}/{len(ordered)}</span>
|
| 232 |
</div>''')
|
| 233 |
|
| 234 |
+
cards = []
|
| 235 |
for v in ordered:
|
| 236 |
img = app.safe_url(v.get("img") or v.get("poster",""))
|
| 237 |
aid = app.make_id(v["link"])
|
| 238 |
sl = app.slug(v["title"])
|
| 239 |
+
cards.append(f'<div class="bdp-card" onclick="window.bdpOpen(\'{app.esc(v["link"])}\',\'{aid}\',\'{sl}\')"><div class="bdp-card-img"><img src="{img}" alt="" class="bdp-lazy-img"><div class="bdp-play-overlay">\u25b6</div></div><div class="bdp-card-body"><span class="bdp-badge {badge}">{source_label}</span><h3 class="bdp-card-title">{v["title"]}</h3></div></div>')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
|
| 241 |
+
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(cards)}</div></div>'
|
|
|
|
| 242 |
|
| 243 |
mode_class = "shorts-feed-mode" if is_shorts else ""
|
| 244 |
+
return f'''{first_img}<div class="tiktok-fullscreen-container {mode_class}" id="tiktok-fullscreen-feed">
|
| 245 |
<div class="tiktok-fullscreen-feed">{''.join(slides)}</div>
|
| 246 |
</div>
|
| 247 |
+
{grid}'''
|
| 248 |
|
| 249 |
app.read_article = _patched_read_article
|
|
|
|
|
|
|
| 250 |
app.demo.css = app.CSS
|
|
|
|
|
|
|
| 251 |
demo = app.demo
|
| 252 |
+
if __name__ == "__main__": demo.launch()
|
|
|
|
|
|