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

feat: og:image in share + reduce TikTok player height for mobile

Browse files
Files changed (1) hide show
  1. 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 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,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
- 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,192 +99,154 @@ def _patched_render_video_page():
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>''')
262
 
263
- list_cards = []
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
- 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
-
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()