6265e56a7f
Learn uses 64-gua card grid; liuyao/bazi/combined use two input cards plus sticky right AI panel; add manifest, service worker, and install prompt. Co-authored-by: Cursor <cursoragent@cursor.com>
97 lines
2.9 KiB
TypeScript
97 lines
2.9 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { Download, X } from "lucide-react";
|
||
import { Button } from "@/components/ui/button";
|
||
|
||
interface BeforeInstallPromptEvent extends Event {
|
||
prompt: () => Promise<void>;
|
||
userChoice: Promise<{ outcome: "accepted" | "dismissed" }>;
|
||
}
|
||
|
||
export default function PwaProvider() {
|
||
const [deferred, setDeferred] = useState<BeforeInstallPromptEvent | null>(
|
||
null,
|
||
);
|
||
const [dismissed, setDismissed] = useState(false);
|
||
const [isIos, setIsIos] = useState(false);
|
||
const [isStandalone, setIsStandalone] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if ("serviceWorker" in navigator) {
|
||
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
||
}
|
||
|
||
setIsStandalone(
|
||
window.matchMedia("(display-mode: standalone)").matches ||
|
||
(window.navigator as Navigator & { standalone?: boolean }).standalone ===
|
||
true,
|
||
);
|
||
|
||
const ua = window.navigator.userAgent;
|
||
setIsIos(/iPad|iPhone|iPod/.test(ua));
|
||
|
||
const handler = (e: Event) => {
|
||
e.preventDefault();
|
||
setDeferred(e as BeforeInstallPromptEvent);
|
||
};
|
||
window.addEventListener("beforeinstallprompt", handler);
|
||
return () => window.removeEventListener("beforeinstallprompt", handler);
|
||
}, []);
|
||
|
||
if (isStandalone || dismissed) {
|
||
return null;
|
||
}
|
||
|
||
if (deferred) {
|
||
return (
|
||
<div className="fixed bottom-4 left-4 right-4 z-50 mx-auto flex max-w-md items-center gap-3 rounded-xl border border-border/60 bg-card/95 p-4 shadow-lg backdrop-blur-md sm:left-auto">
|
||
<Download size={20} className="shrink-0 text-primary" />
|
||
<div className="min-w-0 flex-1 text-sm">
|
||
<p className="font-medium">安装知命阁</p>
|
||
<p className="text-xs text-muted-foreground">
|
||
添加到主屏幕,像 App 一样使用
|
||
</p>
|
||
</div>
|
||
<Button
|
||
size="sm"
|
||
onClick={async () => {
|
||
await deferred.prompt();
|
||
setDeferred(null);
|
||
}}
|
||
>
|
||
安装
|
||
</Button>
|
||
<button
|
||
type="button"
|
||
aria-label="关闭"
|
||
className="text-muted-foreground hover:text-foreground"
|
||
onClick={() => setDismissed(true)}
|
||
>
|
||
<X size={16} />
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (isIos) {
|
||
return (
|
||
<div className="fixed bottom-4 left-4 right-4 z-50 mx-auto max-w-md rounded-xl border border-border/60 bg-card/95 p-4 text-sm shadow-lg backdrop-blur-md sm:left-auto">
|
||
<p className="font-medium">安装到 iPhone / iPad</p>
|
||
<p className="mt-1 text-xs text-muted-foreground">
|
||
点击 Safari 底部分享按钮 → 「添加到主屏幕」
|
||
</p>
|
||
<button
|
||
type="button"
|
||
className="mt-2 text-xs text-muted-foreground underline"
|
||
onClick={() => setDismissed(true)}
|
||
>
|
||
知道了
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return null;
|
||
}
|