import { defineStore } from 'pinia' import { ref } from 'vue' /** Apps that set X-Frame-Options and/or don't support subpath proxy - open in new tab for correct display */ function mustOpenInNewTab(url: string): boolean { try { const u = new URL(url) return ( u.port === '23000' || // BTCPay u.port === '8123' || // Home Assistant u.port === '8085' || // Nextcloud (subpath breaks CSS/assets) u.port === '2283' // Immich (subpath breaks SPA) ) } catch { return false } } /** Rewrite to same-origin proxy so iframe can embed (nginx strips X-Frame-Options) */ function toEmbeddableUrl(url: string): string { try { const u = new URL(url) const origin = window.location.origin // Only Vaultwarden and Penpot support subpath proxy; Nextcloud/Immich open in new tab if (u.port === '8082') return `${origin}/app/vaultwarden/` if (u.port === '9001') return `${origin}/app/penpot/` } catch { /* ignore */ } return url } export const useAppLauncherStore = defineStore('appLauncher', () => { const isOpen = ref(false) const url = ref('') const title = ref('') let previousActiveElement: HTMLElement | null = null function open(payload: { url: string; title: string; openInNewTab?: boolean }) { if (payload.openInNewTab || mustOpenInNewTab(payload.url)) { window.open(payload.url, '_blank', 'noopener,noreferrer') return } previousActiveElement = (document.activeElement as HTMLElement) || null url.value = toEmbeddableUrl(payload.url) title.value = payload.title isOpen.value = true } function close() { const toRestore = previousActiveElement previousActiveElement = null isOpen.value = false url.value = '' title.value = '' if (toRestore && typeof toRestore.focus === 'function') { requestAnimationFrame(() => { toRestore.focus() }) } } return { isOpen, url, title, open, close, } })