mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-31 04:57:47 +08:00
187 lines
5.0 KiB
TypeScript
187 lines
5.0 KiB
TypeScript
/**
|
|
* 应用活动状态:active 允许交互与装饰动效;idle 表示前台静置;passive 表示短时失焦;
|
|
* suspended 表示隐藏或持续失焦,需要释放高成本渲染资源。
|
|
*/
|
|
export type AppActivityState = 'active' | 'idle' | 'passive' | 'suspended'
|
|
|
|
export const APP_ACTIVITY_IDLE_DELAY_MS = 3 * 60_000
|
|
export const APP_ACTIVITY_SUSPEND_DELAY_MS = 60_000
|
|
|
|
type AppActivityStateListener = (state: AppActivityState) => void
|
|
|
|
const activityEvents = ['keydown', 'pointerdown', 'pointermove', 'scroll', 'touchstart', 'wheel'] as const
|
|
|
|
/**
|
|
* 统一管理页面活动状态,确保装饰动效、定时器和高成本渲染共享相同的前后台语义。
|
|
*/
|
|
export class AppActivityLifecycle {
|
|
private state: AppActivityState = 'active'
|
|
private listeners = new Set<AppActivityStateListener>()
|
|
private idleTimer: number | null = null
|
|
private suspendTimer: number | null = null
|
|
private lastActivityAt = Date.now()
|
|
private acquireCount = 0
|
|
private started = false
|
|
|
|
private readonly handleActivity = () => {
|
|
if (document.visibilityState !== 'visible' || !document.hasFocus()) return
|
|
|
|
this.lastActivityAt = Date.now()
|
|
this.setState('active')
|
|
this.scheduleIdle()
|
|
}
|
|
|
|
private readonly handleBlur = () => {
|
|
if (document.visibilityState === 'hidden') return
|
|
|
|
this.clearIdleTimer()
|
|
this.setState('passive')
|
|
this.scheduleSuspend()
|
|
}
|
|
|
|
private readonly handleFocus = () => {
|
|
if (document.visibilityState !== 'visible') return
|
|
|
|
this.clearSuspendTimer()
|
|
this.lastActivityAt = Date.now()
|
|
this.setState('active')
|
|
this.scheduleIdle()
|
|
}
|
|
|
|
private readonly handleVisibilityChange = () => {
|
|
if (document.visibilityState === 'hidden') {
|
|
this.clearTimers()
|
|
this.setState('suspended')
|
|
return
|
|
}
|
|
|
|
if (document.hasFocus()) {
|
|
this.handleFocus()
|
|
} else {
|
|
this.handleBlur()
|
|
}
|
|
}
|
|
|
|
/** 获取当前生命周期状态。 */
|
|
getState() {
|
|
return this.state
|
|
}
|
|
|
|
/** 获取最近一次有效前台交互时间。 */
|
|
getLastActivityAt() {
|
|
return this.lastActivityAt
|
|
}
|
|
|
|
/** 订阅状态变化;订阅时立即回放当前状态。 */
|
|
subscribe(listener: AppActivityStateListener) {
|
|
this.listeners.add(listener)
|
|
listener(this.state)
|
|
|
|
return () => {
|
|
this.listeners.delete(listener)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取生命周期使用权;首个消费者注册全局监听,最后一个消费者释放全部资源。
|
|
*/
|
|
acquire() {
|
|
this.acquireCount += 1
|
|
if (!this.started) this.start()
|
|
|
|
let released = false
|
|
|
|
return () => {
|
|
if (released) return
|
|
|
|
released = true
|
|
this.acquireCount = Math.max(0, this.acquireCount - 1)
|
|
if (this.acquireCount === 0) this.stop()
|
|
}
|
|
}
|
|
|
|
/** 供路由或测试路径显式登记一次有效前台活动。 */
|
|
markActivity() {
|
|
this.handleActivity()
|
|
}
|
|
|
|
private start() {
|
|
if (this.started) return
|
|
|
|
this.started = true
|
|
activityEvents.forEach(event => document.addEventListener(event, this.handleActivity, { passive: true }))
|
|
document.addEventListener('visibilitychange', this.handleVisibilityChange)
|
|
window.addEventListener('blur', this.handleBlur)
|
|
window.addEventListener('focus', this.handleFocus)
|
|
|
|
this.handleVisibilityChange()
|
|
}
|
|
|
|
private stop() {
|
|
if (!this.started) return
|
|
|
|
this.clearTimers()
|
|
activityEvents.forEach(event => document.removeEventListener(event, this.handleActivity))
|
|
document.removeEventListener('visibilitychange', this.handleVisibilityChange)
|
|
window.removeEventListener('blur', this.handleBlur)
|
|
window.removeEventListener('focus', this.handleFocus)
|
|
this.started = false
|
|
}
|
|
|
|
private setState(state: AppActivityState) {
|
|
if (this.state === state) return
|
|
|
|
this.state = state
|
|
this.listeners.forEach(listener => listener(state))
|
|
}
|
|
|
|
private scheduleIdle() {
|
|
if (this.idleTimer !== null) return
|
|
|
|
const elapsed = Date.now() - this.lastActivityAt
|
|
const delay = Math.max(0, APP_ACTIVITY_IDLE_DELAY_MS - elapsed)
|
|
|
|
this.idleTimer = window.setTimeout(() => {
|
|
this.idleTimer = null
|
|
if (document.visibilityState !== 'visible' || !document.hasFocus()) return
|
|
|
|
const remaining = APP_ACTIVITY_IDLE_DELAY_MS - (Date.now() - this.lastActivityAt)
|
|
if (remaining > 0) {
|
|
this.scheduleIdle()
|
|
return
|
|
}
|
|
|
|
this.setState('idle')
|
|
}, delay)
|
|
}
|
|
|
|
private scheduleSuspend() {
|
|
this.clearSuspendTimer()
|
|
this.suspendTimer = window.setTimeout(() => {
|
|
this.suspendTimer = null
|
|
if (document.visibilityState === 'visible' && !document.hasFocus()) this.setState('suspended')
|
|
}, APP_ACTIVITY_SUSPEND_DELAY_MS)
|
|
}
|
|
|
|
private clearIdleTimer() {
|
|
if (this.idleTimer === null) return
|
|
|
|
window.clearTimeout(this.idleTimer)
|
|
this.idleTimer = null
|
|
}
|
|
|
|
private clearSuspendTimer() {
|
|
if (this.suspendTimer === null) return
|
|
|
|
window.clearTimeout(this.suspendTimer)
|
|
this.suspendTimer = null
|
|
}
|
|
|
|
private clearTimers() {
|
|
this.clearIdleTimer()
|
|
this.clearSuspendTimer()
|
|
}
|
|
}
|
|
|
|
export const appActivityLifecycle = new AppActivityLifecycle()
|