bep40 commited on
Commit
a8cd931
·
verified ·
1 Parent(s): 204c16c

fix: shorts uses same tiktok-slide classes (auto-init by existing JS), fullwidth, no inline script

Browse files
Files changed (1) hide show
  1. app_wrapper.py +38 -125
app_wrapper.py CHANGED
@@ -1,4 +1,4 @@
1
- """Entry point that adds shorts TikTok feed + carousel, then imports app."""
2
  import sys, os
3
 
4
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@@ -7,31 +7,19 @@ from shorts_carousel import scrape_24h_news_shorts, render_shorts_carousel, SHOR
7
  import app
8
 
9
  # ══ Extra CSS ══
10
- SHORTS_TIKTOK_CSS = """
11
- /* Shorts TikTok Feed (9:16 fullscreen) */
12
- .shorts-tiktok-container{width:100%;background:#000;position:relative;height:calc(100vh - 60px);max-height:900px;min-height:500px}
13
- .shorts-tiktok-feed{height:100%;overflow-y:scroll;scroll-snap-type:y mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none}
14
- .shorts-tiktok-feed::-webkit-scrollbar{display:none}
15
- .shorts-slide{height:calc(100vh - 60px);max-height:900px;min-height:500px;scroll-snap-align:start;scroll-snap-stop:always;position:relative;display:flex;align-items:center;justify-content:center;background:#000}
16
- .shorts-slide video{width:100%;height:100%;object-fit:cover;display:block}
17
- .shorts-slide .tiktok-bottom{position:absolute;bottom:0;left:0;right:0;padding:16px 14px 24px;background:linear-gradient(transparent,rgba(0,0,0,.85));z-index:3}
18
- .shorts-slide .tiktok-title{color:#fff;font-size:14px;font-weight:600;margin:4px 0 8px;line-height:1.4;text-shadow:0 1px 4px rgba(0,0,0,.8);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
19
- .shorts-slide .tiktok-counter{position:absolute;top:12px;left:12px;background:rgba(0,0,0,.5);color:#fff;font-size:11px;padding:3px 10px;border-radius:12px;z-index:4}
20
- .shorts-slide .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}
21
- .shorts-slide .shorts-seek{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;gap:40px;z-index:6;opacity:0;transition:opacity .3s;pointer-events:none}
22
- .shorts-slide.show-controls .shorts-seek{opacity:1;pointer-events:auto}
23
- .shorts-seek button{background:rgba(0,0,0,.5);color:#fff;border:none;padding:8px 14px;border-radius:20px;font-size:12px;cursor:pointer;font-weight:600}
24
- .shorts-slide .shorts-pause{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;line-height:70px;text-align:center}
25
- .shorts-slide.paused .shorts-pause{display:block}
26
- .shorts-back-btn{position:fixed;top:70px;left:50%;transform:translateX(-50%);background:rgba(255,255,255,.2);color:#fff;border:none;padding:6px 16px;border-radius:18px;font-size:12px;cursor:pointer;z-index:10;backdrop-filter:blur(4px)}
27
  """
28
 
29
  if "vslide-shorts-item" not in app.CSS:
30
  app.CSS += SHORTS_CSS
31
- if "shorts-tiktok-container" not in app.CSS:
32
- app.CSS += SHORTS_TIKTOK_CSS
33
 
34
- # ══ Patch render_homepage_html ══
35
  _orig_render_homepage = app.render_homepage_html
36
  def _patched_render_homepage(articles, *args, **kwargs):
37
  html = _orig_render_homepage(articles, *args, **kwargs)
@@ -51,99 +39,20 @@ def _patched_render_homepage(articles, *args, **kwargs):
51
  return html
52
  app.render_homepage_html = _patched_render_homepage
53
 
54
- # ══ Inline JS for shorts feed (embedded in HTML, not in head) ══
55
- SHORTS_INLINE_JS = '''<script>
56
- (function(){
57
- function initSF(){
58
- var c=document.getElementById('shorts-tiktok-feed');
59
- if(!c||c._sInit)return;c._sInit=true;
60
- var feed=c.querySelector('.shorts-tiktok-feed');
61
- if(!feed)return;
62
- var slides=feed.querySelectorAll('.shorts-slide');
63
- if(!slides.length)return;
64
-
65
- function initHls(v){
66
- if(v._hlsI)return;v._hlsI=true;
67
- var src=v.getAttribute('data-hls-src');if(!src)return;
68
- if(v.canPlayType('application/vnd.apple.mpegURL')){v.src=src;}
69
- else if(window.Hls&&Hls.isSupported()){var h=new Hls();h.loadSource(src);h.attachMedia(v);}
70
- else{v.src=src;}
71
- }
72
-
73
- slides.forEach(function(sl){
74
- var v=sl.querySelector('video[data-hls-src]');
75
- if(v)initHls(v);
76
- var v2=sl.querySelector('video:not([data-hls-src])');
77
- if(v2&&!v2._ld){v2._ld=true;v2.load();}
78
- });
79
-
80
- var cur=-1;
81
- function tryP(v){var p=v.play();if(p&&p.catch)p.catch(function(){setTimeout(function(){v.play().catch(function(){});},500);});}
82
- function activate(idx){
83
- if(idx===cur)return;
84
- slides.forEach(function(sl,i){
85
- var v=sl.querySelector('video');if(!v)return;
86
- if(i===idx){v.currentTime=0;tryP(v);sl.classList.remove('paused');}
87
- else{v.pause();sl.classList.remove('paused');}
88
- });
89
- cur=idx;
90
- }
91
-
92
- var sT=null;
93
- feed.addEventListener('scroll',function(){
94
- if(sT)clearTimeout(sT);
95
- sT=setTimeout(function(){
96
- var rect=feed.getBoundingClientRect();
97
- var ctr=rect.top+rect.height/2;
98
- var best=-1,bestD=99999;
99
- slides.forEach(function(sl,i){
100
- var r=sl.getBoundingClientRect();
101
- var d=Math.abs(r.top+r.height/2-ctr);
102
- if(d<bestD){bestD=d;best=i;}
103
- });
104
- if(best>=0)activate(best);
105
- },150);
106
- });
107
- setTimeout(function(){activate(0);},800);
108
-
109
- slides.forEach(function(sl){
110
- var v=sl.querySelector('video');
111
- if(v){
112
- var hT=null;
113
- v.addEventListener('click',function(e){
114
- e.preventDefault();
115
- if(sl.classList.contains('show-controls')){
116
- if(v.paused){tryP(v);sl.classList.remove('paused');}
117
- else{v.pause();sl.classList.add('paused');}
118
- sl.classList.remove('show-controls');
119
- }else{
120
- sl.classList.add('show-controls');
121
- if(hT)clearTimeout(hT);
122
- hT=setTimeout(function(){sl.classList.remove('show-controls');},3000);
123
- }
124
- });
125
- }
126
- });
127
- }
128
- // Run on load and poll
129
- initSF();
130
- setInterval(initSF,1000);
131
- var obs=new MutationObserver(function(){initSF();});
132
- obs.observe(document.body,{childList:true,subtree:true});
133
- })();
134
- </script>'''
135
-
136
- # ══ Patch read_article: 24h articles → shorts TikTok feed ══
137
  _orig_read_article = app.read_article
138
  def _patched_read_article(url):
139
  if not url or url == "#" or len(url) < 10:
140
  return "<p>Không tìm thấy bài viết.</p>"
141
  if "24h.com.vn" in url:
142
- return _render_shorts_tiktok_feed(url)
 
 
143
  return _orig_read_article(url)
144
 
145
  def _render_shorts_tiktok_feed(current_url):
146
- """Render fullscreen TikTok feed of ALL shorts. Clicked video first."""
 
147
  from concurrent.futures import ThreadPoolExecutor, as_completed
148
 
149
  try:
@@ -152,8 +61,7 @@ def _render_shorts_tiktok_feed(current_url):
152
  all_shorts = []
153
 
154
  if not all_shorts:
155
- # Fallback to normal article view
156
- return app.render_article_html(app.scrape_24h_article(current_url))
157
 
158
  # Fetch video URLs in parallel
159
  def _fetch_video(art):
@@ -173,7 +81,7 @@ def _render_shorts_tiktok_feed(current_url):
173
  except: pass
174
 
175
  if not videos:
176
- return app.render_article_html(app.scrape_24h_article(current_url))
177
 
178
  # Reorder: clicked video first
179
  current_aid = app.make_id(current_url)
@@ -186,7 +94,8 @@ def _render_shorts_tiktok_feed(current_url):
186
  rest.append(v)
187
  ordered.extend(rest)
188
 
189
- # Build TikTok slides
 
190
  slides = []
191
  for vi, v in enumerate(ordered):
192
  poster = app.safe_url(v.get("poster", ""))
@@ -197,26 +106,29 @@ def _render_shorts_tiktok_feed(current_url):
197
  share_js = f"event.stopPropagation();window.bdpShareHash('{app.esc(v['title'])}','{sl}','{aid}')"
198
 
199
  if v.get("vtype") == "hls":
200
- vtag = f'<video playsinline preload="metadata"{poster_attr} data-hls-src="{vsrc}" muted loop></video>'
201
  else:
202
- vtag = f'<video playsinline preload="metadata"{poster_attr} muted loop><source src="{app.safe_url(vsrc)}" type="video/mp4"></video>'
203
 
204
- slides.append(f'''<div class="shorts-slide" data-index="{vi}">
205
  {vtag}
206
- <div class="shorts-pause">\u25b6</div>
207
- <div class="shorts-seek">
208
- <button onclick="event.stopPropagation();var v=this.closest('.shorts-slide').querySelector('video');v.currentTime=Math.max(0,v.currentTime-10)">\u23ea 10s</button>
209
- <button onclick="event.stopPropagation();var v=this.closest('.shorts-slide').querySelector('video');v.currentTime=Math.min(v.duration||9999,v.currentTime+10)">10s \u23e9</button>
210
  </div>
211
  <div class="tiktok-bottom">
 
212
  <p class="tiktok-title">{v["title"]}</p>
 
213
  <button class="tiktok-action-btn" onclick="{share_js}">\U0001f4e4 Chia s\u1ebb</button>
214
  </div>
215
- <div class="tiktok-unmute-hint" onclick="var vd=this.closest('.shorts-slide').querySelector('video');vd.muted=!vd.muted;this.textContent=vd.muted?'\U0001f507 B\u1eadt ti\u1ebfng':'\U0001f50a \u0110ang ph\u00e1t'">\U0001f507 B\u1eadt ti\u1ebfng</div>
 
216
  <span class="tiktok-counter">{vi+1}/{len(ordered)}</span>
217
  </div>''')
218
 
219
- # Build video list cards below the feed
220
  list_cards = []
221
  for v in ordered:
222
  img = app.safe_url(v.get("img") or v.get("poster",""))
@@ -225,17 +137,18 @@ def _render_shorts_tiktok_feed(current_url):
225
  click = f"window.bdpOpen('{app.esc(v['link'])}','{aid}','{sl}')"
226
  list_cards.append(f'''<div class="bdp-card" onclick="{click}">
227
  <div class="bdp-card-img"><img src="{img}" alt="" class="bdp-lazy-img"><div class="bdp-play-overlay">\u25b6</div></div>
228
- <div class="bdp-card-body"><span class="bdp-badge bdp-badge-24h">24h</span>
229
  <h3 class="bdp-card-title">{v["title"]}</h3></div></div>''')
230
 
231
- grid_html = f'''<div class="bdp-wrap"><div class="bdp-topbar"><span>\U0001f4f1 Shorts</span><span>{len(ordered)} video</span></div>
232
  <div class="bdp-grid">{''.join(list_cards)}</div></div>'''
233
 
234
- return f'''<div class="shorts-tiktok-container" id="shorts-tiktok-feed">
235
- <div class="shorts-tiktok-feed">{''.join(slides)}</div>
 
 
236
  </div>
237
- {grid_html}
238
- {SHORTS_INLINE_JS}'''
239
 
240
  app.read_article = _patched_read_article
241
 
 
1
+ """Entry point: shorts carousel + TikTok-style shorts playback (reusing existing JS)."""
2
  import sys, os
3
 
4
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
 
7
  import app
8
 
9
  # ══ Extra CSS ══
10
+ SHORTS_EXTRA_CSS = """
11
+ /* Shorts feed: override video to 9:16 crop center */
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
  """
16
 
17
  if "vslide-shorts-item" not in app.CSS:
18
  app.CSS += SHORTS_CSS
19
+ if "shorts-feed-mode" not in app.CSS:
20
+ app.CSS += SHORTS_EXTRA_CSS
21
 
22
+ # ══ Patch render_homepage_html: add shorts carousel ══
23
  _orig_render_homepage = app.render_homepage_html
24
  def _patched_render_homepage(articles, *args, **kwargs):
25
  html = _orig_render_homepage(articles, *args, **kwargs)
 
39
  return html
40
  app.render_homepage_html = _patched_render_homepage
41
 
42
+ # ══ Patch read_article: 24h URLs shorts TikTok feed ══
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  _orig_read_article = app.read_article
44
  def _patched_read_article(url):
45
  if not url or url == "#" or len(url) < 10:
46
  return "<p>Không tìm thấy bài viết.</p>"
47
  if "24h.com.vn" in url:
48
+ result = _render_shorts_tiktok_feed(url)
49
+ if result:
50
+ return result
51
  return _orig_read_article(url)
52
 
53
  def _render_shorts_tiktok_feed(current_url):
54
+ """Render shorts as TikTok feed using SAME classes as highlight TikTok
55
+ so the existing JS (initTikTokFullscreen in app.py) auto-initializes it."""
56
  from concurrent.futures import ThreadPoolExecutor, as_completed
57
 
58
  try:
 
61
  all_shorts = []
62
 
63
  if not all_shorts:
64
+ return None # fallback to normal article
 
65
 
66
  # Fetch video URLs in parallel
67
  def _fetch_video(art):
 
81
  except: pass
82
 
83
  if not videos:
84
+ return None # fallback
85
 
86
  # Reorder: clicked video first
87
  current_aid = app.make_id(current_url)
 
94
  rest.append(v)
95
  ordered.extend(rest)
96
 
97
+ # Build slides using SAME classes as highlight TikTok feed
98
+ # This way initTikTokFullscreen() from app.py will auto-init it
99
  slides = []
100
  for vi, v in enumerate(ordered):
101
  poster = app.safe_url(v.get("poster", ""))
 
106
  share_js = f"event.stopPropagation();window.bdpShareHash('{app.esc(v['title'])}','{sl}','{aid}')"
107
 
108
  if v.get("vtype") == "hls":
109
+ vtag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} data-hls-src="{vsrc}" muted loop></video>'
110
  else:
111
+ vtag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} muted loop><source src="{app.safe_url(vsrc)}" type="video/mp4"></video>'
112
 
113
+ slides.append(f'''<div class="tiktok-slide" data-index="{vi}" data-aid="{aid}">
114
  {vtag}
115
+ <div class="tiktok-pause-icon">\u25b6</div>
116
+ <div class="tiktok-seek-controls">
117
+ <button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,-10)">\u23ea 10s</button>
118
+ <button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,10)">10s \u23e9</button>
119
  </div>
120
  <div class="tiktok-bottom">
121
+ <span class="bdp-badge bdp-badge-24h">24h</span>
122
  <p class="tiktok-title">{v["title"]}</p>
123
+ <div class="tiktok-actions">
124
  <button class="tiktok-action-btn" onclick="{share_js}">\U0001f4e4 Chia s\u1ebb</button>
125
  </div>
126
+ </div>
127
+ <div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">\U0001f507 B\u1eadt ti\u1ebfng</div>
128
  <span class="tiktok-counter">{vi+1}/{len(ordered)}</span>
129
  </div>''')
130
 
131
+ # Video list cards below
132
  list_cards = []
133
  for v in ordered:
134
  img = app.safe_url(v.get("img") or v.get("poster",""))
 
137
  click = f"window.bdpOpen('{app.esc(v['link'])}','{aid}','{sl}')"
138
  list_cards.append(f'''<div class="bdp-card" onclick="{click}">
139
  <div class="bdp-card-img"><img src="{img}" alt="" class="bdp-lazy-img"><div class="bdp-play-overlay">\u25b6</div></div>
140
+ <div class="bdp-card-body"><span class="bdp-badge bdp-badge-24h">24h Shorts</span>
141
  <h3 class="bdp-card-title">{v["title"]}</h3></div></div>''')
142
 
143
+ grid_html = f'''<div class="bdp-wrap"><div class="bdp-topbar"><span>\U0001f4f1 Shorts Tin T\u1ee9c</span><span>{len(ordered)} video</span></div>
144
  <div class="bdp-grid">{''.join(list_cards)}</div></div>'''
145
 
146
+ # Use class "tiktok-fullscreen-container" + "tiktok-fullscreen-feed" + "shorts-feed-mode"
147
+ # so existing JS auto-inits AND CSS applies object-fit:cover for 9:16 crop
148
+ return f'''<div class="tiktok-fullscreen-container shorts-feed-mode" id="tiktok-fullscreen-feed">
149
+ <div class="tiktok-fullscreen-feed">{''.join(slides)}</div>
150
  </div>
151
+ {grid_html}'''
 
152
 
153
  app.read_article = _patched_read_article
154