This commit is contained in:
2026-05-18 14:58:09 +08:00
parent df6cff4895
commit 3933cf42c0
8 changed files with 415 additions and 9 deletions

310
public/site-links-client.js Normal file
View File

@@ -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 =
'<div data-inapp-close style="' +
MODAL_BACK +
'"></div>' +
'<div style="' +
MODAL_PANEL +
'">' +
'<h3 id="inapp-modal-title" style="' +
MODAL_TITLE +
'"></h3>' +
'<p id="inapp-modal-body" style="' +
MODAL_BODY +
'"></p>' +
'<div style="' +
MODAL_ACTIONS +
'">' +
'<button type="button" id="inapp-copy-apk" style="' +
BTN +
BTN_PRIMARY +
'"></button>' +
'<button type="button" id="inapp-try-chrome" style="' +
BTN +
'" hidden></button>' +
'<button type="button" id="inapp-got-it" data-inapp-close style="' +
BTN +
'"></button>' +
'</div></div>';
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 =
'<div data-app-soon-close style="' +
MODAL_BACK +
'"></div>' +
'<div style="' +
MODAL_PANEL +
'">' +
'<h3 id="app-soon-title" style="' +
MODAL_TITLE +
'"></h3>' +
'<p id="app-soon-body" style="' +
MODAL_BODY +
'"></p>' +
'<div style="' +
MODAL_ACTIONS +
'">' +
'<button type="button" data-app-soon-close style="' +
BTN +
BTN_PRIMARY +
'"></button>' +
'</div></div>';
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();
})();

View File

@@ -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);
---
<section id="download" class="download-cta">
@@ -35,7 +38,12 @@ const phoneArt = "/assets/cta-phone-art.png";
</div>
<div class="store-badges">
<div class="store-badge store-badge--android">
<a
class="store-badge store-badge--android store-badge--browser"
href={defaultApkHref}
rel="noopener noreferrer"
target="_blank"
>
<div class="store-badge__icon-frame">
<img alt={t.androidAlt} class="store-badge__android-icon" src={androidIcon} />
</div>
@@ -43,8 +51,12 @@ const phoneArt = "/assets/cta-phone-art.png";
<p class="store-badge__platform">{t.android}</p>
<p class="store-badge__label">{t.androidCta}</p>
</div>
</div>
<div class="store-badge store-badge--ios">
</a>
<a
class="store-badge store-badge--ios store-badge--apple"
href="#"
data-app-soon="1"
>
<div class="store-badge__icon-frame">
<img alt={t.appleAlt} class="store-badge__apple-icon" src={appleIcon} />
</div>
@@ -52,8 +64,13 @@ const phoneArt = "/assets/cta-phone-art.png";
<p class="store-badge__platform">{t.ios}</p>
<p class="store-badge__label">{t.iosCta}</p>
</div>
</div>
</a>
</div>
<p
id="site-links-meta"
class="download-cta__links-meta"
hidden
/>
</div>
<div class="download-cta__phone">
@@ -63,3 +80,5 @@ const phoneArt = "/assets/cta-phone-art.png";
</div>
</div>
</section>
<script type="application/json" id="site-links-i18n" set:html={siteLinksJson} />

View File

@@ -11,6 +11,7 @@ const heroBg = "/assets/hero-bg.png";
const phoneMockup = "/assets/hero-phone.png";
const androidIcon = "/assets/cta-android-icon.svg";
const appleIcon = "/assets/cta-apple-icon.svg";
const defaultApkHref = "https://talkspro.xyz/download";
---
<section id="hero" class="hero">
@@ -39,7 +40,12 @@ const appleIcon = "/assets/cta-apple-icon.svg";
{t.description}
</p>
<div class="hero__actions">
<a href="#download" class="store-badge store-badge--android hero__store-badge">
<a
href={defaultApkHref}
class="store-badge store-badge--android store-badge--browser hero__store-badge"
rel="noopener noreferrer"
target="_blank"
>
<div class="store-badge__icon-frame">
<img alt={download.androidAlt} class="store-badge__android-icon" src={androidIcon} />
</div>
@@ -48,7 +54,11 @@ const appleIcon = "/assets/cta-apple-icon.svg";
<p class="store-badge__label">{download.androidCta}</p>
</div>
</a>
<a href="#download" class="store-badge store-badge--ios hero__store-badge">
<a
href="#"
class="store-badge store-badge--ios store-badge--apple hero__store-badge"
data-app-soon="1"
>
<div class="store-badge__icon-frame">
<img alt={download.appleAlt} class="store-badge__apple-icon" src={appleIcon} />
</div>

View File

@@ -281,6 +281,23 @@ export const translations = {
appleAlt: "Apple",
phoneAlt: "TalkPro on phone",
},
siteLinks: {
linksMetaPrefix: "Store links file updated: ",
linksMetaSuffix: " (GMT+8)",
inapp_browser_title: "Open in your system browser",
inapp_browser_body:
"This page is opened inside an <strong>in-app browser</strong>. To download the APK, tap the <strong>menu (··· or ⋮)</strong> in the upper corner, then choose <strong>Open in default browser</strong>, <strong>Open in Safari</strong>, or <strong>Open in Chrome</strong>. Return here and tap the Android badge again—or paste the link you copied.",
inapp_browser_body_ios:
"Youre in <strong>WeChats built-in browser</strong> on iPhone or iPad. Apple and WeChat <strong>do not allow</strong> a website to jump to Safari automatically—this is normal. Tap <strong>···</strong> (top right) → <strong>Open in Safari</strong> or <strong>Open in default browser</strong>, then tap the Android badge again, or use <strong>Copy download link</strong> and paste it in Safari.",
inapp_browser_copy: "Copy download link",
inapp_browser_copied: "Copied",
inapp_browser_got_it: "OK",
inapp_browser_try_chrome: "Try opening in Chrome (Android)",
app_store_soon_title: "Coming soon",
app_store_soon_body:
"The <strong>App Store</strong> download is still in development and is <strong>not available</strong> yet. Please use the <strong>Android</strong> download for now.",
app_store_soon_ok: "OK",
},
footer: {
logoAlt: "TalkPro",
description:
@@ -536,6 +553,23 @@ export const translations = {
appleAlt: "Apple",
phoneAlt: "手机上的 TalkPro",
},
siteLinks: {
linksMetaPrefix: "Store links file updated: ",
linksMetaSuffix: " (GMT+8)",
inapp_browser_title: "请在系统浏览器中打开",
inapp_browser_body:
"当前页面在<strong>应用内置浏览器</strong>中打开,无法可靠下载 APK。请点击右上角<strong>「···」或「⋮」</strong>菜单,选择<strong>「在浏览器打开」「用默认浏览器打开」「在 Safari 打开」</strong>或<strong>「在 Chrome 打开」</strong>,在系统浏览器中再次点击 Android 徽章;或使用下方按钮<strong>复制链接</strong>,粘贴到系统浏览器地址栏打开。",
inapp_browser_body_ios:
"您正在 iPhone/iPad 的<strong>微信内置浏览器</strong>中。微信与 iOS <strong>不允许</strong>网页自动跳转到系统 Safari这是系统限制不是网站故障。请点击右上角<strong>「···」</strong>,选择<strong>「在 Safari 中打开」</strong>或<strong>「在默认浏览器中打开」</strong>,再在 Safari 里点 Android 徽章;或先<strong>复制下载链接</strong>,到 Safari 地址栏粘贴打开。",
inapp_browser_copy: "复制下载链接",
inapp_browser_copied: "已复制",
inapp_browser_got_it: "知道了",
inapp_browser_try_chrome: "尝试用 Chrome 打开Android",
app_store_soon_title: "开发中",
app_store_soon_body:
"<strong>App Store</strong> 下载尚在开发与上架准备中,<strong>暂时未开放</strong>。请先使用 <strong>Android</strong> 下载。带来不便敬请谅解。",
app_store_soon_ok: "知道了",
},
footer: {
logoAlt: "TalkPro",
description:
@@ -699,6 +733,23 @@ export const translations = {
appleAlt: "Apple",
phoneAlt: "手機上的 TalkPro",
},
siteLinks: {
linksMetaPrefix: "Store links file updated: ",
linksMetaSuffix: " (GMT+8)",
inapp_browser_title: "請在系統瀏覽器中開啟",
inapp_browser_body:
"目前頁面在<strong>應用程式內建瀏覽器</strong>中開啟,無法可靠下載 APK。請點選右上角<strong>「···」或「⋮」</strong>選單,選擇<strong>「在瀏覽器開啟」「用預設瀏覽器開啟」「在 Safari 開啟」</strong>或<strong>「在 Chrome 開啟」</strong>,在系統瀏覽器中再次點選 Android 徽章;或使用下方按鈕<strong>複製連結</strong>,貼到系統瀏覽器網址列開啟。",
inapp_browser_body_ios:
"您正在 iPhone/iPad 的<strong>微信內建瀏覽器</strong>中。微信與 iOS <strong>不允許</strong>網頁自動跳轉到系統 Safari這是系統限制不是網站故障。請點選右上角<strong>「···」</strong>,選擇<strong>「在 Safari 中開啟」</strong>或<strong>「在預設瀏覽器中開啟」</strong>,再在 Safari 裡點 Android 徽章;或先<strong>複製下載連結</strong>,到 Safari 網址列貼上開啟。",
inapp_browser_copy: "複製下載連結",
inapp_browser_copied: "已複製",
inapp_browser_got_it: "知道了",
inapp_browser_try_chrome: "嘗試用 Chrome 開啟Android",
app_store_soon_title: "開發中",
app_store_soon_body:
"<strong>App Store</strong> 下載尚在開發與上架準備中,<strong>暫時未開放</strong>。請先使用 <strong>Android</strong> 下載。造成不便敬請見諒。",
app_store_soon_ok: "知道了",
},
footer: {
logoAlt: "TalkPro",
description:

View File

@@ -27,6 +27,7 @@ const {
</head>
<body class="bg-surface font-sans overflow-x-hidden">
<slot />
<script src="/site-links-client.js" defer></script>
<script>
(() => {
const header = document.getElementById('site-header');

View File

@@ -31,6 +31,6 @@ const t = getTranslations(lang)
<UseCases t={t.useCases} />
<Trust t={t.trust} />
<!-- AppPreview section disabled per lead request. -->
<DownloadCTA t={t.download} />
<DownloadCTA t={t.download} siteLinks={t.siteLinks} />
<Footer t={t.footer} />
</Base>

View File

@@ -24,6 +24,6 @@ const t = getTranslations(lang)
<UseCases t={t.useCases} />
<Trust t={t.trust} />
<!-- AppPreview section disabled per lead request. -->
<DownloadCTA t={t.download} />
<DownloadCTA t={t.download} siteLinks={t.siteLinks} />
<Footer t={t.footer} />
</Base>

View File

@@ -129,6 +129,21 @@
border-radius: 20px;
}
a.store-badge {
box-sizing: border-box;
color: inherit;
text-decoration: none;
cursor: pointer;
}
.download-cta__links-meta {
font-size: 13px;
color: #7a726d;
margin: 14px auto 0;
max-width: min(1180px, calc(100% - 40px));
text-align: center;
}
.store-badge--android {
background: #f28a4b;
border: 1px solid #c5834e;