♻️ refactor(client): 浏览器半拆成 src 模块,client.js 改由构建产出
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Every browser and platform fact this plugin reads, in one seam.
|
||||
*
|
||||
* Nothing here owns state or decides anything: the window's visibility, the
|
||||
* notification permission, the notification icon, and the one platform hint the
|
||||
* settings copy uses. Each read is contained, because a page a plugin does not
|
||||
* control may hide any of them — and because a contained read is what lets the
|
||||
* delivery path keep going when the answer is unreadable.
|
||||
*
|
||||
* @module @dsh-plugin/session-notify/client/platform
|
||||
*/
|
||||
|
||||
/**
|
||||
* The window's Notification constructor, or undefined when this environment has
|
||||
* none (a plain Node test, or a browser without the API).
|
||||
* @returns the constructor, or undefined.
|
||||
*/
|
||||
export function notificationApi() {
|
||||
const Ctor = globalThis.Notification
|
||||
return typeof Ctor === 'function' ? Ctor : undefined
|
||||
}
|
||||
|
||||
/** Permission lookup that never throws in a browser without the API. */
|
||||
export function notificationPermission() {
|
||||
const Ctor = notificationApi()
|
||||
if (Ctor === undefined) return 'unsupported'
|
||||
const permission = Ctor.permission
|
||||
if (permission === 'granted' || permission === 'denied' || permission === 'default') return permission
|
||||
return 'default'
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Harness window is somewhere the user cannot see the app.
|
||||
*
|
||||
* Two independent browser facts are consulted, and ANY of them counts as "not
|
||||
* in the foreground": the page's visibility and the document's focus. They are
|
||||
* read at delivery time rather than remembered from an event, because a missed
|
||||
* blur in the desktop shell made this plugin believe the window was in front
|
||||
* and swallow the alert.
|
||||
* @returns whether the alert must go to the system notification channel.
|
||||
*/
|
||||
export function windowIsAway() {
|
||||
try {
|
||||
if (document.visibilityState === 'hidden') return true
|
||||
} catch {
|
||||
/* an unreadable visibility state leaves the focus fact */
|
||||
}
|
||||
try {
|
||||
if (typeof document.hasFocus === 'function') return !document.hasFocus()
|
||||
} catch {
|
||||
/* an unreadable focus state leaves the default below */
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Read the window's live focus state rather than a cached value. */
|
||||
export function currentFocus() {
|
||||
return !windowIsAway()
|
||||
}
|
||||
|
||||
/** Focus the window without letting a refusal stop the navigation. */
|
||||
export function focusWindow() {
|
||||
try {
|
||||
globalThis.focus?.()
|
||||
} catch {
|
||||
/* focus is best effort on every platform */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The system-notification icon, built once when this bundle materializes: this
|
||||
* deployment's own static URL when the page serves this package's assets, else
|
||||
* the SVG inline. Icons are cosmetic on every platform, so each step is
|
||||
* contained — and materialization happens on first import, not when the bundle
|
||||
* script runs, so a page that never activates the plugin never gets here.
|
||||
* @type {string | undefined} an icon URL, or undefined when neither form works.
|
||||
*/
|
||||
export const systemIcon = (() => {
|
||||
try {
|
||||
const url = new URL('./icon.svg', document.baseURI).href
|
||||
if (url !== '') return url
|
||||
} catch {
|
||||
/* fall through to the inline form */
|
||||
}
|
||||
try {
|
||||
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M8 2.35C5.7 2.35 3.83 4.22 3.83 6.52V9.02L3.06 11.1C2.99 11.29 3.13 11.48 3.33 11.48H12.67C12.87 11.48 13.01 11.29 12.94 11.1L12.17 9.02V6.52C12.17 4.22 10.3 2.35 8 2.35Z" fill="none" stroke="%23247bbf" stroke-width="1.2"/><path d="M6.3 12.75C6.52 13.66 7.2 14.28 8 14.28C8.8 14.28 9.48 13.66 9.7 12.75" fill="none" stroke="%23247bbf" stroke-width="1.2" stroke-linecap="round"/></svg>'
|
||||
return `data:image/svg+xml,${svg}`
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
|
||||
/** One best-effort platform hint, used for settings help text only. */
|
||||
export function platformKey() {
|
||||
try {
|
||||
const agent = navigator.userAgentData
|
||||
const platform = String((agent?.platform ?? navigator.platform) || navigator.userAgent || '')
|
||||
if (/win/i.test(platform)) return 'win'
|
||||
if (/mac|iphone|ipad/i.test(platform)) return 'mac'
|
||||
if (/linux|x11/i.test(platform)) return 'linux'
|
||||
} catch {
|
||||
/* an unreadable navigator keeps the neutral hint */
|
||||
}
|
||||
return 'plain'
|
||||
}
|
||||
Reference in New Issue
Block a user