mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-08-02 13:56:23 +00:00
update 1.2.4
This commit is contained in:
@@ -1,21 +1,24 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
import { Download, Plus, Share, X } from "lucide-react"
|
import { Plus, Share, X } from "lucide-react"
|
||||||
|
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
// PwaInstallPrompt
|
// PwaInstallPrompt
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
// Bottom-sheet shown on mobile when the Monitor is opened in
|
// Bottom-sheet shown on mobile when the Monitor is opened in
|
||||||
// a browser (not launched as an installed PWA). Two variants:
|
// a browser (not launched as an installed PWA). Two variants:
|
||||||
// iOS Safari → manual 3-step instructions (no browser API)
|
// iOS Safari → manual 3-step instructions
|
||||||
// Android → native install prompt when the browser
|
// Android → generic "browser menu → Add to Home Screen"
|
||||||
// fires `beforeinstallprompt` (Chromium 89+
|
|
||||||
// delivers it based on manifest validity alone,
|
|
||||||
// no Service Worker required), with the manual
|
|
||||||
// "browser menu → Add to Home Screen" steps
|
|
||||||
// always shown below as a fallback.
|
|
||||||
//
|
//
|
||||||
|
// No `beforeinstallprompt` handling: capturing the event to
|
||||||
|
// drive a custom Install button interacts badly with Chrome's
|
||||||
|
// own "Add to Home Screen" menu — Chromium degrades the manual
|
||||||
|
// path to a plain shortcut when a page has intercepted the
|
||||||
|
// event but hasn't yet called `prompt()`. Installation goes
|
||||||
|
// through the browser's own menu entry and produces a real PWA.
|
||||||
|
//
|
||||||
|
// Never shown on desktop, or when already running standalone.
|
||||||
// Dismissal options:
|
// Dismissal options:
|
||||||
// "Not now" → temporary, hidden for 30 days
|
// "Not now" → temporary, hidden for 30 days
|
||||||
// "Don't show again" → permanent (no expiry)
|
// "Don't show again" → permanent (no expiry)
|
||||||
@@ -27,13 +30,6 @@ const DISMISSED_FOREVER_KEY = "proxmenux-install-dismissed"
|
|||||||
const DISMISSED_UNTIL_KEY = "proxmenux-install-dismissed-until"
|
const DISMISSED_UNTIL_KEY = "proxmenux-install-dismissed-until"
|
||||||
const NOT_NOW_DAYS = 30
|
const NOT_NOW_DAYS = 30
|
||||||
|
|
||||||
// `BeforeInstallPromptEvent` isn't in the TS DOM lib yet.
|
|
||||||
interface BeforeInstallPromptEvent extends Event {
|
|
||||||
readonly platforms: string[]
|
|
||||||
readonly userChoice: Promise<{ outcome: "accepted" | "dismissed"; platform: string }>
|
|
||||||
prompt(): Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMobileDevice(): boolean {
|
function isMobileDevice(): boolean {
|
||||||
if (typeof window === "undefined") return false
|
if (typeof window === "undefined") return false
|
||||||
// Prefer feature detection (coarse pointer + touch) over UA sniffing,
|
// Prefer feature detection (coarse pointer + touch) over UA sniffing,
|
||||||
@@ -64,7 +60,6 @@ function isIOS(): boolean {
|
|||||||
export function PwaInstallPrompt() {
|
export function PwaInstallPrompt() {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [platform, setPlatform] = useState<"ios" | "android" | null>(null)
|
const [platform, setPlatform] = useState<"ios" | "android" | null>(null)
|
||||||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") return
|
if (typeof window === "undefined") return
|
||||||
@@ -87,28 +82,6 @@ export function PwaInstallPrompt() {
|
|||||||
setOpen(true)
|
setOpen(true)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window === "undefined") return
|
|
||||||
const onBeforeInstall = (e: Event) => {
|
|
||||||
// Suppress the browser's own mini-infobar so the Install button
|
|
||||||
// in the bottom sheet is the primary path.
|
|
||||||
e.preventDefault()
|
|
||||||
setDeferredPrompt(e as BeforeInstallPromptEvent)
|
|
||||||
}
|
|
||||||
const onInstalled = () => {
|
|
||||||
// The browser confirms the install landed on the home screen —
|
|
||||||
// close the sheet and drop the deferred event.
|
|
||||||
setDeferredPrompt(null)
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
window.addEventListener("beforeinstallprompt", onBeforeInstall)
|
|
||||||
window.addEventListener("appinstalled", onInstalled)
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("beforeinstallprompt", onBeforeInstall)
|
|
||||||
window.removeEventListener("appinstalled", onInstalled)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleNotNow = useCallback(() => {
|
const handleNotNow = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
const until = Date.now() + NOT_NOW_DAYS * 24 * 60 * 60 * 1000
|
const until = Date.now() + NOT_NOW_DAYS * 24 * 60 * 60 * 1000
|
||||||
@@ -137,25 +110,6 @@ export function PwaInstallPrompt() {
|
|||||||
setOpen(false)
|
setOpen(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleInstall = useCallback(async () => {
|
|
||||||
if (!deferredPrompt) return
|
|
||||||
try {
|
|
||||||
await deferredPrompt.prompt()
|
|
||||||
const { outcome } = await deferredPrompt.userChoice
|
|
||||||
// A `beforeinstallprompt` event can only be used once; drop the
|
|
||||||
// reference either way so the button hides.
|
|
||||||
setDeferredPrompt(null)
|
|
||||||
if (outcome === "accepted") {
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Browser refused to run the prompt (already handled, race with
|
|
||||||
// appinstalled, etc.) — leave the sheet open so the operator can
|
|
||||||
// fall back to the manual steps rendered below.
|
|
||||||
setDeferredPrompt(null)
|
|
||||||
}
|
|
||||||
}, [deferredPrompt])
|
|
||||||
|
|
||||||
if (!open || !platform) return null
|
if (!open || !platform) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -236,33 +190,11 @@ export function PwaInstallPrompt() {
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<div className="mb-4 rounded-lg border border-border bg-muted/50 px-3.5 py-3 text-[13px] leading-relaxed text-muted-foreground">
|
||||||
{deferredPrompt && (
|
Open the browser menu <b className="text-foreground">⋮</b> →{" "}
|
||||||
<button
|
<b className="text-foreground">Add to Home Screen</b> → confirm by tapping{" "}
|
||||||
type="button"
|
<b className="text-foreground">Install</b>.
|
||||||
onClick={handleInstall}
|
</div>
|
||||||
className="mb-3 flex w-full items-center justify-center gap-2 rounded-xl bg-primary px-4 py-3 text-[14px] font-semibold text-primary-foreground shadow-sm hover:opacity-90 active:opacity-80 transition-opacity"
|
|
||||||
>
|
|
||||||
<Download className="h-4 w-4" aria-hidden="true" />
|
|
||||||
Install
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div className="mb-4 rounded-lg border border-border bg-muted/50 px-3.5 py-3 text-[13px] leading-relaxed text-muted-foreground">
|
|
||||||
{deferredPrompt ? (
|
|
||||||
<>
|
|
||||||
Or install manually: browser menu <b className="text-foreground">⋮</b> →{" "}
|
|
||||||
<b className="text-foreground">Add to Home Screen</b> → confirm by tapping{" "}
|
|
||||||
<b className="text-foreground">Install</b>.
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
Open the browser menu <b className="text-foreground">⋮</b> →{" "}
|
|
||||||
<b className="text-foreground">Add to Home Screen</b> → confirm by tapping{" "}
|
|
||||||
<b className="text-foreground">Install</b>.
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-1 flex flex-col gap-1 border-t border-border pt-3">
|
<div className="mt-1 flex flex-col gap-1 border-t border-border pt-3">
|
||||||
|
|||||||
+2
-4
@@ -20,10 +20,8 @@ This release adds two in-dashboard improvements — a one-click Proxmox update t
|
|||||||
|
|
||||||
## 📱 In-app Install prompt for mobile
|
## 📱 In-app Install prompt for mobile
|
||||||
|
|
||||||
- First-time visitors on **Android** (Chrome / Brave / Edge / Samsung Internet) and **iOS Safari** now see a bottom-sheet with clear instructions for adding the Monitor as a PWA to their home screen.
|
- First-time visitors on **Android** (Chrome / Brave) and **iOS Safari** now see a bottom-sheet with clear instructions for adding the Monitor as a PWA to their home screen.
|
||||||
- On Android, when the browser deems the Monitor installable and fires `beforeinstallprompt` (Chromium 89+ delivers it on manifest validity alone, no Service Worker required), an **Install** button in the sheet drives the native install flow with a single tap; the browser's own in-page install banner is suppressed so the sheet is the primary path. The manual "browser menu → Add to Home Screen" steps are still shown as a fallback for browsers that don't fire the event (Firefox Android) or cases where the operator prefers the manual path.
|
- Installation goes through the browser's own menu entry ("Add to Home Screen"), which produces a real installed PWA that launches in standalone mode. The sheet doesn't intercept the browser's `beforeinstallprompt` event — intercepting it and not calling `prompt()` degrades the manual menu path to a plain shortcut, which is what showed up in field testing.
|
||||||
- iOS Safari keeps the manual 3-step instructions — no equivalent JS API is available on iOS.
|
|
||||||
- The sheet closes automatically when the browser fires `appinstalled` — no lingering prompt after the install lands on the home screen.
|
|
||||||
- Two dismissal levels: **Not now** (temporary, reappears in 30 days) and **Don't show again** (permanent, stored in `localStorage`).
|
- Two dismissal levels: **Not now** (temporary, reappears in 30 days) and **Don't show again** (permanent, stored in `localStorage`).
|
||||||
- Never shown on desktop, or once the Monitor is already running standalone.
|
- Never shown on desktop, or once the Monitor is already running standalone.
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,8 @@ Esta versión suma dos mejoras visibles en el propio dashboard — un botón par
|
|||||||
|
|
||||||
## 📱 Prompt de instalación en la app para móvil
|
## 📱 Prompt de instalación en la app para móvil
|
||||||
|
|
||||||
- Los visitantes por primera vez en **Android** (Chrome / Brave / Edge / Samsung Internet) e **iOS Safari** ven ahora una bottom-sheet con instrucciones claras para añadir el Monitor a su pantalla de inicio como PWA.
|
- Los visitantes por primera vez en **Android** (Chrome / Brave) e **iOS Safari** ven ahora una bottom-sheet con instrucciones claras para añadir el Monitor a su pantalla de inicio como PWA.
|
||||||
- En Android, cuando el navegador considera el Monitor instalable y dispara `beforeinstallprompt` (Chromium 89+ lo entrega solo con manifest válido, sin necesidad de Service Worker), un botón **Install** en la bottom-sheet dispara el flujo nativo de instalación con un solo tap; el propio banner de instalación in-page del navegador se suprime para que la bottom-sheet sea el camino principal. Las instrucciones manuales de "menú del navegador → Añadir a pantalla de inicio" siguen mostrándose como fallback para navegadores que no disparan el evento (Firefox Android) o casos en que el operador prefiere la ruta manual.
|
- La instalación se hace a través de la propia entrada del menú del navegador ("Añadir a pantalla de inicio"), que produce una PWA real instalada que se lanza en modo standalone. La bottom-sheet no intercepta el evento `beforeinstallprompt` del navegador — interceptarlo sin llegar a llamar a `prompt()` degrada la ruta manual del menú a un acceso directo simple, que es lo que apareció en las pruebas de campo.
|
||||||
- iOS Safari mantiene las instrucciones manuales de 3 pasos — no existe una API JS equivalente en iOS.
|
|
||||||
- La bottom-sheet se cierra automáticamente cuando el navegador dispara `appinstalled` — no queda ningún prompt colgado tras la instalación en la pantalla de inicio.
|
|
||||||
- Dos niveles de descarte: **Not now** (temporal, reaparece a los 30 días) y **Don't show again** (permanente, almacenado en `localStorage`).
|
- Dos niveles de descarte: **Not now** (temporal, reaparece a los 30 días) y **Don't show again** (permanente, almacenado en `localStorage`).
|
||||||
- No se muestra en escritorio, ni cuando el Monitor ya se está ejecutando como aplicación instalada.
|
- No se muestra en escritorio, ni cuando el Monitor ya se está ejecutando como aplicación instalada.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user