修复pwa检测
This commit is contained in:
parent
3469f53d79
commit
a255b23be3
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const PWA_INSTALL_MARKER_KEY = "dh-pwa-installed";
|
||||||
|
|
||||||
type BeforeInstallPromptEvent = Event & {
|
type BeforeInstallPromptEvent = Event & {
|
||||||
prompt: () => Promise<void>;
|
prompt: () => Promise<void>;
|
||||||
userChoice: Promise<{
|
userChoice: Promise<{
|
||||||
@ -31,18 +33,68 @@ function urlBase64ToUint8Array(base64String: string) {
|
|||||||
return outputArray;
|
return outputArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStandaloneDisplayMode() {
|
||||||
|
const navigatorWithStandalone = navigator as Navigator & {
|
||||||
|
standalone?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
window.matchMedia("(display-mode: standalone)").matches ||
|
||||||
|
window.matchMedia("(display-mode: fullscreen)").matches ||
|
||||||
|
window.matchMedia("(display-mode: minimal-ui)").matches ||
|
||||||
|
Boolean(navigatorWithStandalone.standalone) ||
|
||||||
|
document.referrer.startsWith("android-app://")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readInstalledMarker() {
|
||||||
|
try {
|
||||||
|
return window.localStorage.getItem(PWA_INSTALL_MARKER_KEY) === "1";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeInstalledMarker() {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(PWA_INSTALL_MARKER_KEY, "1");
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function PwaControls() {
|
export function PwaControls() {
|
||||||
const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || "";
|
const vapidPublicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || "";
|
||||||
const [installPromptEvent, setInstallPromptEvent] =
|
const [installPromptEvent, setInstallPromptEvent] =
|
||||||
useState<BeforeInstallPromptEvent | null>(null);
|
useState<BeforeInstallPromptEvent | null>(null);
|
||||||
const [installing, setInstalling] = useState(false);
|
const [installing, setInstalling] = useState(false);
|
||||||
const [isStandalone, setIsStandalone] = useState(false);
|
const [isStandalone, setIsStandalone] = useState(false);
|
||||||
|
const [hasInstalledHint, setHasInstalledHint] = useState(false);
|
||||||
const [notificationState, setNotificationState] =
|
const [notificationState, setNotificationState] =
|
||||||
useState<NotificationState>("checking");
|
useState<NotificationState>("checking");
|
||||||
const [notice, setNotice] = useState<string | null>(null);
|
const [notice, setNotice] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
|
let delayedSyncTimer: number | null = null;
|
||||||
|
|
||||||
|
function syncInstallState() {
|
||||||
|
const standalone = isStandaloneDisplayMode();
|
||||||
|
const installed = standalone || readInstalledMarker();
|
||||||
|
|
||||||
|
if (standalone) {
|
||||||
|
writeInstalledMarker();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
return standalone;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsStandalone(standalone);
|
||||||
|
setHasInstalledHint(installed);
|
||||||
|
|
||||||
|
return standalone;
|
||||||
|
}
|
||||||
|
|
||||||
function handleBeforeInstallPrompt(event: Event) {
|
function handleBeforeInstallPrompt(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -53,6 +105,30 @@ export function PwaControls() {
|
|||||||
setInstallPromptEvent(event as BeforeInstallPromptEvent);
|
setInstallPromptEvent(event as BeforeInstallPromptEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAppInstalled() {
|
||||||
|
writeInstalledMarker();
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasInstalledHint(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleVisibilityChange() {
|
||||||
|
if (!document.hidden) {
|
||||||
|
syncInstallState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageShow() {
|
||||||
|
syncInstallState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFocus() {
|
||||||
|
syncInstallState();
|
||||||
|
}
|
||||||
|
|
||||||
async function syncExistingSubscription(subscription: PushSubscription) {
|
async function syncExistingSubscription(subscription: PushSubscription) {
|
||||||
await fetch("/api/push/subscribe", {
|
await fetch("/api/push/subscribe", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -66,10 +142,7 @@ export function PwaControls() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const standalone =
|
syncInstallState();
|
||||||
window.matchMedia("(display-mode: standalone)").matches ||
|
|
||||||
Boolean((navigator as Navigator & { standalone?: boolean }).standalone);
|
|
||||||
setIsStandalone(standalone);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!("serviceWorker" in navigator) ||
|
!("serviceWorker" in navigator) ||
|
||||||
@ -109,11 +182,27 @@ export function PwaControls() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
||||||
|
window.addEventListener("appinstalled", handleAppInstalled);
|
||||||
|
window.addEventListener("pageshow", handlePageShow);
|
||||||
|
window.addEventListener("focus", handleFocus);
|
||||||
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
|
||||||
void bootstrap();
|
void bootstrap();
|
||||||
|
delayedSyncTimer = window.setTimeout(() => {
|
||||||
|
syncInstallState();
|
||||||
|
}, 350);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
window.removeEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
window.removeEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
|
||||||
|
window.removeEventListener("appinstalled", handleAppInstalled);
|
||||||
|
window.removeEventListener("pageshow", handlePageShow);
|
||||||
|
window.removeEventListener("focus", handleFocus);
|
||||||
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
|
||||||
|
if (delayedSyncTimer) {
|
||||||
|
window.clearTimeout(delayedSyncTimer);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [vapidPublicKey]);
|
}, [vapidPublicKey]);
|
||||||
|
|
||||||
@ -207,6 +296,7 @@ export function PwaControls() {
|
|||||||
|
|
||||||
if (choice.outcome === "accepted") {
|
if (choice.outcome === "accepted") {
|
||||||
setInstallPromptEvent(null);
|
setInstallPromptEvent(null);
|
||||||
|
setHasInstalledHint(true);
|
||||||
setNotice("安装提示已经确认,完成后就能像应用一样直接打开。\n");
|
setNotice("安装提示已经确认,完成后就能像应用一样直接打开。\n");
|
||||||
} else {
|
} else {
|
||||||
setNotice("这次先关闭了安装提示,之后仍可从浏览器菜单安装。\n");
|
setNotice("这次先关闭了安装提示,之后仍可从浏览器菜单安装。\n");
|
||||||
@ -230,7 +320,10 @@ export function PwaControls() {
|
|||||||
|
|
||||||
<div className="mt-4 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
|
<div className="mt-4 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
|
||||||
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
||||||
{isStandalone ? "已安装到桌面" : "当前在浏览器中打开"}
|
{isStandalone ? "当前正以桌面应用方式运行" : "当前正以浏览器方式运行"}
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
||||||
|
{hasInstalledHint ? "这台设备已有主屏幕安装记录" : "暂未检测到主屏幕安装记录"}
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
|
||||||
{notificationState === "enabled"
|
{notificationState === "enabled"
|
||||||
@ -247,10 +340,16 @@ export function PwaControls() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
void installApp();
|
void installApp();
|
||||||
}}
|
}}
|
||||||
disabled={installing}
|
disabled={installing || isStandalone}
|
||||||
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--ink)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--ink-soft)] disabled:cursor-not-allowed disabled:opacity-60"
|
className="inline-flex h-12 items-center justify-center rounded-full bg-[var(--ink)] px-5 text-sm font-semibold text-white transition hover:bg-[var(--ink-soft)] disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
>
|
>
|
||||||
{installing ? "正在呼起安装" : "安装到桌面"}
|
{isStandalone
|
||||||
|
? "已从桌面版打开"
|
||||||
|
: installing
|
||||||
|
? "正在呼起安装"
|
||||||
|
: hasInstalledHint
|
||||||
|
? "查看安装说明"
|
||||||
|
: "安装到桌面"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{notificationState === "enabled" ? (
|
{notificationState === "enabled" ? (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user