diff --git a/src/app/components/updates-dialog/UpdatesDialog.css.ts b/src/app/components/updates-dialog/UpdatesDialog.css.ts index af1f358..c313196 100644 --- a/src/app/components/updates-dialog/UpdatesDialog.css.ts +++ b/src/app/components/updates-dialog/UpdatesDialog.css.ts @@ -7,6 +7,7 @@ export const PortalLayer = style({ position: 'fixed', inset: 0, zIndex: config.zIndex.Max, + pointerEvents: 'auto', }); /** Dialog + overlay padding must never exceed the viewport height. */ diff --git a/src/app/components/updates-dialog/UpdatesDialog.tsx b/src/app/components/updates-dialog/UpdatesDialog.tsx index 873363c..feecb10 100644 --- a/src/app/components/updates-dialog/UpdatesDialog.tsx +++ b/src/app/components/updates-dialog/UpdatesDialog.tsx @@ -4,7 +4,6 @@ import FocusTrap from 'focus-trap-react'; import { Box, Button, - config, Dialog, IconButton, Overlay, @@ -48,6 +47,9 @@ export function UpdatesDialog({ setShowOlderList(false); }; + const portalTarget = + document.getElementById('portalContainer') ?? document.body; + return createPortal(
}> @@ -55,8 +57,7 @@ export function UpdatesDialog({ @@ -181,6 +182,6 @@ export function UpdatesDialog({
, - document.body + portalTarget ); } diff --git a/src/app/components/updates-dialog/UpdatesDialogHost.tsx b/src/app/components/updates-dialog/UpdatesDialogHost.tsx index 14a9df9..8db2c91 100644 --- a/src/app/components/updates-dialog/UpdatesDialogHost.tsx +++ b/src/app/components/updates-dialog/UpdatesDialogHost.tsx @@ -26,7 +26,10 @@ type UpdaterInfo = { export function useOpenReleaseNotesDialog(): () => void { const setDialogState = useSetAtom(releaseNotesDialogAtom); return useCallback(() => { - setDialogState({ open: true, manual: true }); + // Defer so the opening tap does not trip FocusTrap click-outside on Android. + window.requestAnimationFrame(() => { + setDialogState({ open: true, manual: true }); + }); }, [setDialogState]); } diff --git a/src/app/data/updateNotes.ts b/src/app/data/updateNotes.ts index 497ab41..99b8f1e 100644 --- a/src/app/data/updateNotes.ts +++ b/src/app/data/updateNotes.ts @@ -1,5 +1,12 @@ +import bundledManifest from '../../../public/update/manifest.json'; import { trimTrailingSlash } from '../utils/common'; +const bundledMarkdownByFile = import.meta.glob('../../../public/update/*.md', { + query: '?raw', + import: 'default', + eager: true, +}) as Record; + function getUpdateBaseUrl(): string { const basePath = trimTrailingSlash(import.meta.env.BASE_URL || './'); const relative = @@ -64,9 +71,26 @@ function parseUpdateMarkdown(file: string, source: string): ParsedUpdateDoc { }; } +function loadBundledManifest(): UpdateManifest { + const manifest = bundledManifest as UpdateManifest; + return { + current: manifest.current, + older: (manifest.older ?? []).filter((entry) => entry?.file), + }; +} + +function loadBundledDocument(file: string): ParsedUpdateDoc | null { + const safeFile = file.replace(/^\/+/, ''); + const entry = Object.entries(bundledMarkdownByFile).find(([path]) => + path.endsWith(`/${safeFile}`) + ); + if (!entry) return null; + return parseUpdateMarkdown(safeFile, entry[1]); +} + export function resolveUpdateAssetUrl(src: string | undefined): string | undefined { if (!src) return undefined; - if (/^(https?:|data:|blob:)/i.test(src)) return src; + if (/^(https?:|data:|blob:|capacitor:)/i.test(src)) return src; if (src.startsWith('/')) return src; const normalized = src.replace(/^\.\//, ''); @@ -76,15 +100,21 @@ export function resolveUpdateAssetUrl(src: string | undefined): string | undefin export async function loadUpdateManifest(): Promise { try { const response = await fetch(getUpdateFileUrl('manifest.json'), { cache: 'no-cache' }); - if (!response.ok) return null; + if (response.ok) { + const data = (await response.json()) as UpdateManifest; + if (data?.current && Array.isArray(data.older)) { + return { + current: data.current, + older: data.older.filter((entry) => entry?.file), + }; + } + } + } catch { + // fall through to bundled copy (Capacitor / offline) + } - const data = (await response.json()) as UpdateManifest; - if (!data?.current || !Array.isArray(data.older)) return null; - - return { - current: data.current, - older: data.older.filter((entry) => entry?.file), - }; + try { + return loadBundledManifest(); } catch { return null; } @@ -96,13 +126,15 @@ export async function loadUpdateDocument(file: string): Promise {