2026-02-17 21:10:16 +00:00
|
|
|
import { defineStore } from 'pinia'
|
|
|
|
|
import { ref } from 'vue'
|
|
|
|
|
|
2026-02-25 18:04:41 +00:00
|
|
|
/** Apps that set X-Frame-Options and/or don't support subpath proxy - open in new tab for correct display */
|
2026-02-25 17:23:38 +00:00
|
|
|
function mustOpenInNewTab(url: string): boolean {
|
|
|
|
|
try {
|
|
|
|
|
const u = new URL(url)
|
2026-02-25 18:04:41 +00:00
|
|
|
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)
|
|
|
|
|
)
|
2026-02-25 17:23:38 +00:00
|
|
|
} catch {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 18:04:41 +00:00
|
|
|
/** 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:10:16 +00:00
|
|
|
export const useAppLauncherStore = defineStore('appLauncher', () => {
|
|
|
|
|
const isOpen = ref(false)
|
|
|
|
|
const url = ref('')
|
|
|
|
|
const title = ref('')
|
2026-02-18 11:29:05 +00:00
|
|
|
let previousActiveElement: HTMLElement | null = null
|
2026-02-17 21:10:16 +00:00
|
|
|
|
2026-02-25 17:23:38 +00:00
|
|
|
function open(payload: { url: string; title: string; openInNewTab?: boolean }) {
|
|
|
|
|
if (payload.openInNewTab || mustOpenInNewTab(payload.url)) {
|
|
|
|
|
window.open(payload.url, '_blank', 'noopener,noreferrer')
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-02-18 11:29:05 +00:00
|
|
|
previousActiveElement = (document.activeElement as HTMLElement) || null
|
2026-02-25 18:04:41 +00:00
|
|
|
url.value = toEmbeddableUrl(payload.url)
|
2026-02-17 21:10:16 +00:00
|
|
|
title.value = payload.title
|
|
|
|
|
isOpen.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function close() {
|
2026-02-18 11:29:05 +00:00
|
|
|
const toRestore = previousActiveElement
|
|
|
|
|
previousActiveElement = null
|
2026-02-17 21:10:16 +00:00
|
|
|
isOpen.value = false
|
|
|
|
|
url.value = ''
|
|
|
|
|
title.value = ''
|
2026-02-18 11:29:05 +00:00
|
|
|
if (toRestore && typeof toRestore.focus === 'function') {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
toRestore.focus()
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-02-17 21:10:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
isOpen,
|
|
|
|
|
url,
|
|
|
|
|
title,
|
|
|
|
|
open,
|
|
|
|
|
close,
|
|
|
|
|
}
|
|
|
|
|
})
|