From 3933cf42c0a24dd206a2bbd421aa3351fbe82a8d Mon Sep 17 00:00:00 2001 From: thomas Date: Mon, 18 May 2026 14:58:09 +0800 Subject: [PATCH] 1 --- public/site-links-client.js | 310 +++++++++++++++++++++++++++++++ src/components/DownloadCTA.astro | 29 ++- src/components/Hero.astro | 14 +- src/i18n/translations.ts | 51 +++++ src/layouts/Base.astro | 1 + src/pages/[lang]/index.astro | 2 +- src/pages/index.astro | 2 +- src/styles/download.css | 15 ++ 8 files changed, 415 insertions(+), 9 deletions(-) create mode 100644 public/site-links-client.js diff --git a/public/site-links-client.js b/public/site-links-client.js new file mode 100644 index 0000000..c536de9 --- /dev/null +++ b/public/site-links-client.js @@ -0,0 +1,310 @@ +/** + * Mirrors talkpro static site: /api/site-links, APK default, in-app + App Store modals. + * Strings come from #site-links-i18n (JSON) rendered by DownloadCTA.astro. + */ +(function () { + const DEFAULT_APK = 'https://talkspro.xyz/download'; + let apkDownloadUrl = DEFAULT_APK; + + function getApkDownloadUrl() { + return apkDownloadUrl; + } + + function getI18n() { + const el = document.getElementById('site-links-i18n'); + if (!el || !el.textContent.trim()) return {}; + try { + return JSON.parse(el.textContent); + } catch { + return {}; + } + } + + function detectInAppBrowser() { + const ua = navigator.userAgent || ''; + if (/MicroMessenger/i.test(ua)) return 'wechat'; + try { + if (typeof window.WeixinJSBridge !== 'undefined') return 'wechat'; + } catch { + /* ignore */ + } + if (/QQ\//i.test(ua) && /MQQBrowser/i.test(ua)) return 'qq'; + if (/Weibo/i.test(ua)) return 'weibo'; + return ''; + } + + function isIOSDevice() { + const ua = navigator.userAgent || ''; + if (/iPhone|iPad|iPod/i.test(ua)) return true; + if (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) return true; + return false; + } + + function isAndroidDevice() { + const ua = navigator.userAgent || ''; + return /Android/i.test(ua) && !isIOSDevice(); + } + + const MODAL_WRAP = + 'position:fixed;inset:0;z-index:10050;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box;'; + const MODAL_BACK = 'position:absolute;inset:0;background:rgba(0,0,0,.45);cursor:pointer;'; + const MODAL_PANEL = + 'position:relative;max-width:420px;width:100%;margin:auto;padding:20px 22px;border-radius:12px;background:#fff;color:#1a1a1a;font:15px/1.45 system-ui,-apple-system,sans-serif;box-shadow:0 12px 40px rgba(0,0,0,.18);'; + const MODAL_TITLE = 'margin:0 0 10px;font-size:18px;font-weight:700;line-height:1.25;'; + const MODAL_BODY = 'margin:0 0 18px;font-size:14px;color:#444;'; + const MODAL_ACTIONS = 'display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end;'; + const BTN = + 'cursor:pointer;border-radius:8px;padding:10px 14px;font-size:14px;font-weight:600;border:1px solid #ccc;background:#f5f5f5;color:#1a1a1a;'; + const BTN_PRIMARY = 'border-color:#1a6cff;background:#1a6cff;color:#fff;'; + + function initInAppBrowserModal() { + if (document.getElementById('inapp-browser-modal')) return; + const wrap = document.createElement('div'); + wrap.id = 'inapp-browser-modal'; + wrap.setAttribute('role', 'dialog'); + wrap.setAttribute('aria-modal', 'true'); + wrap.setAttribute('aria-hidden', 'true'); + wrap.style.cssText = MODAL_WRAP + 'display:none;'; + wrap.innerHTML = + '
' + + '
' + + '

' + + '

' + + '
' + + '' + + '' + + '' + + '
'; + document.body.appendChild(wrap); + + const close = () => { + wrap.style.display = 'none'; + wrap.setAttribute('aria-hidden', 'true'); + }; + wrap.querySelectorAll('[data-inapp-close]').forEach((el) => { + el.addEventListener('click', close); + }); + + const i18n = () => getI18n(); + document.getElementById('inapp-copy-apk').addEventListener('click', async () => { + const btn = document.getElementById('inapp-copy-apk'); + const dict = i18n(); + const done = dict.inapp_browser_copied || 'Copied'; + try { + await navigator.clipboard.writeText(getApkDownloadUrl()); + const prev = btn.textContent; + btn.textContent = done; + setTimeout(() => { + btn.textContent = dict.inapp_browser_copy || prev; + }, 1600); + } catch { + window.prompt(dict.inapp_browser_copy || 'Copy', getApkDownloadUrl()); + } + }); + + document.getElementById('inapp-try-chrome').addEventListener('click', () => { + const enc = encodeURIComponent(getApkDownloadUrl()); + const intent = + getApkDownloadUrl().replace(/^https:/i, 'intent:') + + '#Intent;' + + 'scheme=https;action=android.intent.action.VIEW;category=android.intent.category.BROWSABLE;' + + 'package=com.android.chrome;S.browser_fallback_url=' + + enc + + ';end'; + window.location.href = intent; + }); + } + + function openInAppBrowserModal() { + const modal = document.getElementById('inapp-browser-modal'); + if (!modal) return; + const dict = getI18n(); + const tryChrome = modal.querySelector('#inapp-try-chrome'); + const showChrome = isAndroidDevice() && !!detectInAppBrowser(); + if (tryChrome) { + tryChrome.hidden = !showChrome; + tryChrome.setAttribute('aria-hidden', showChrome ? 'false' : 'true'); + } + modal.style.display = 'flex'; + modal.setAttribute('aria-hidden', 'false'); + const titleEl = modal.querySelector('#inapp-modal-title'); + const bodyEl = modal.querySelector('#inapp-modal-body'); + if (titleEl) titleEl.textContent = dict.inapp_browser_title || ''; + if (bodyEl) { + const iosWechat = isIOSDevice() && detectInAppBrowser() === 'wechat'; + const bodyKey = iosWechat ? 'inapp_browser_body_ios' : 'inapp_browser_body'; + bodyEl.innerHTML = + dict[bodyKey] || dict.inapp_browser_body || ''; + } + const copyBtn = modal.querySelector('#inapp-copy-apk'); + const gotBtn = modal.querySelector('#inapp-got-it'); + if (copyBtn) copyBtn.textContent = dict.inapp_browser_copy || ''; + if (tryChrome) tryChrome.textContent = dict.inapp_browser_try_chrome || ''; + if (gotBtn) gotBtn.textContent = dict.inapp_browser_got_it || 'OK'; + } + + function initAppStoreSoonModal() { + if (document.getElementById('app-store-soon-modal')) return; + const wrap = document.createElement('div'); + wrap.id = 'app-store-soon-modal'; + wrap.setAttribute('role', 'dialog'); + wrap.setAttribute('aria-modal', 'true'); + wrap.setAttribute('aria-hidden', 'true'); + wrap.style.cssText = MODAL_WRAP + 'display:none;'; + wrap.innerHTML = + '
' + + '
' + + '

' + + '

' + + '
' + + '' + + '
'; + document.body.appendChild(wrap); + const close = () => { + wrap.style.display = 'none'; + wrap.setAttribute('aria-hidden', 'true'); + }; + wrap.querySelectorAll('[data-app-soon-close]').forEach((el) => { + el.addEventListener('click', close); + }); + } + + function openAppStoreSoonModal() { + const modal = document.getElementById('app-store-soon-modal'); + if (!modal) return; + const dict = getI18n(); + const tEl = modal.querySelector('#app-soon-title'); + const bEl = modal.querySelector('#app-soon-body'); + const ok = modal.querySelector('button[data-app-soon-close]'); + if (tEl) tEl.textContent = dict.app_store_soon_title || ''; + if (bEl) bEl.innerHTML = dict.app_store_soon_body || ''; + if (ok) ok.textContent = dict.app_store_soon_ok || 'OK'; + modal.style.display = 'flex'; + modal.setAttribute('aria-hidden', 'false'); + } + + function bindBrowserDownloadLinks() { + document.querySelectorAll('a.store-badge--browser').forEach((a) => { + const openForInApp = (e) => { + if (!detectInAppBrowser()) return; + e.preventDefault(); + e.stopPropagation(); + openInAppBrowserModal(); + }; + a.addEventListener('click', openForInApp); + a.addEventListener( + 'touchend', + (e) => { + if (!detectInAppBrowser()) return; + e.preventDefault(); + openInAppBrowserModal(); + }, + { passive: false } + ); + }); + } + + function bindAppStoreAppleBadges() { + document.querySelectorAll('a.store-badge--apple[data-app-soon]').forEach((a) => { + const stopAndOpen = (e) => { + e.preventDefault(); + e.stopPropagation(); + openAppStoreSoonModal(); + }; + a.addEventListener('click', stopAndOpen); + a.addEventListener( + 'touchend', + (e) => { + e.preventDefault(); + e.stopPropagation(); + openAppStoreSoonModal(); + }, + { passive: false } + ); + }); + } + + function loadSiteLinksThenBind() { + if (window.location.protocol === 'file:') { + bindBrowserDownloadLinks(); + bindAppStoreAppleBadges(); + return; + } + fetch('/api/site-links', { credentials: 'same-origin' }) + .then((r) => { + if (!r.ok) throw new Error('bad status'); + return r.json(); + }) + .then((j) => { + const apk = String(j.apk_download_url || '').trim(); + const ios = String(j.app_store_url || '').trim(); + if (apk.startsWith('https://')) apkDownloadUrl = apk; + document.querySelectorAll('a.store-badge--browser').forEach((a) => { + if (apk.startsWith('https://')) a.setAttribute('href', apk); + }); + document.querySelectorAll('a.store-badge--apple').forEach((a) => { + if (ios.startsWith('https://')) { + a.setAttribute('href', ios); + a.removeAttribute('data-app-soon'); + a.setAttribute('rel', 'noopener noreferrer'); + a.setAttribute('target', '_blank'); + } + }); + const meta = document.getElementById('site-links-meta'); + if (meta) { + const dict = getI18n(); + const u = String(j.updated_at || '').trim(); + if (u) { + meta.textContent = + (dict.linksMetaPrefix || 'Store links file updated: ') + + u + + (dict.linksMetaSuffix || ' (GMT+8)'); + meta.hidden = false; + } else { + meta.hidden = true; + } + } + }) + .catch(() => { + /* keep defaults */ + }) + .finally(() => { + bindBrowserDownloadLinks(); + bindAppStoreAppleBadges(); + }); + } + + initInAppBrowserModal(); + initAppStoreSoonModal(); + loadSiteLinksThenBind(); +})(); diff --git a/src/components/DownloadCTA.astro b/src/components/DownloadCTA.astro index 8166d5a..49923fc 100644 --- a/src/components/DownloadCTA.astro +++ b/src/components/DownloadCTA.astro @@ -3,14 +3,17 @@ import type { Translations } from '../i18n/translations' export interface Props { t: Translations['download'] + siteLinks: Translations['siteLinks'] } -const { t } = Astro.props +const { t, siteLinks } = Astro.props const bgPattern = "/assets/cta-bg-pattern.svg"; const talkproLogo = "/assets/cta-talkpro-logo.svg"; const androidIcon = "/assets/cta-android-icon.svg"; const appleIcon = "/assets/cta-apple-icon.svg"; const phoneArt = "/assets/cta-phone-art.png"; +const defaultApkHref = "https://talkspro.xyz/download"; +const siteLinksJson = JSON.stringify(siteLinks); ---
@@ -35,7 +38,12 @@ const phoneArt = "/assets/cta-phone-art.png"; - - + +
@@ -63,3 +80,5 @@ const phoneArt = "/assets/cta-phone-art.png";
+ +