mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-09-06 16:07:06 +08:00
Enhance memory monitoring and fix memory leaks
Co-authored-by: Kuingsmile <96409857+Kuingsmile@users.noreply.github.com>
This commit is contained in:
co-authored by
Kuingsmile
parent
90bdbb6458
commit
e5b9cf8b5b
@@ -17,17 +17,21 @@ import { T as $t } from '~/i18n'
|
|||||||
import { getClipboardFilePath, showNotification } from '~/utils/common'
|
import { getClipboardFilePath, showNotification } from '~/utils/common'
|
||||||
import { configPaths } from '~/utils/configPaths'
|
import { configPaths } from '~/utils/configPaths'
|
||||||
import { ICOREBuildInEvent, IWindowList } from '~/utils/enum'
|
import { ICOREBuildInEvent, IWindowList } from '~/utils/enum'
|
||||||
|
import { IpcManager } from '~/utils/ipcManager'
|
||||||
import { CLIPBOARD_IMAGE_FOLDER } from '~/utils/static'
|
import { CLIPBOARD_IMAGE_FOLDER } from '~/utils/static'
|
||||||
|
|
||||||
const waitForRename = (window: BrowserWindow, id: number): Promise<string | null> => {
|
const waitForRename = (window: BrowserWindow, id: number): Promise<string | null> => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
ipcMain.once(`${RENAME_FILE_NAME}${id}`, (_: IpcMainEvent, newName: string) => {
|
// Use IpcManager for better cleanup tracking
|
||||||
|
const cleanup = IpcManager.once(`${RENAME_FILE_NAME}${id}`, (_: IpcMainEvent, newName: string) => {
|
||||||
resolve(newName)
|
resolve(newName)
|
||||||
window.close()
|
window.close()
|
||||||
})
|
}, `rename-${id}`)
|
||||||
|
|
||||||
window.on('close', () => {
|
window.on('close', () => {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
ipcMain.removeAllListeners(`${RENAME_FILE_NAME}${id}`)
|
// Clean up the specific rename context
|
||||||
|
IpcManager.cleanupContext(`rename-${id}`)
|
||||||
windowManager.deleteById(window.id)
|
windowManager.deleteById(window.id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -72,17 +76,21 @@ class Uploader {
|
|||||||
: item.fileName
|
: item.fileName
|
||||||
if (rename) {
|
if (rename) {
|
||||||
const window = windowManager.create(IWindowList.RENAME_WINDOW)!
|
const window = windowManager.create(IWindowList.RENAME_WINDOW)!
|
||||||
ipcMain.on(GET_RENAME_FILE_NAME, (evt, _) => {
|
const windowId = window.webContents.id
|
||||||
|
|
||||||
|
// Use IpcManager for safer listener management
|
||||||
|
IpcManager.on(GET_RENAME_FILE_NAME, (evt, _) => {
|
||||||
try {
|
try {
|
||||||
if (evt.sender.id === window.webContents.id) {
|
if (evt.sender.id === windowId) {
|
||||||
logger.info('rename window ready, wait for rename...')
|
logger.info('rename window ready, wait for rename...')
|
||||||
window.webContents.send(RENAME_FILE_NAME, fileName, item.fileName, window.webContents.id)
|
window.webContents.send(RENAME_FILE_NAME, fileName, item.fileName, windowId)
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
}
|
}
|
||||||
})
|
}, `rename-window-${windowId}`)
|
||||||
name = await waitForRename(window, window.webContents.id)
|
|
||||||
|
name = await waitForRename(window, windowId)
|
||||||
}
|
}
|
||||||
item.fileName = name || fileName
|
item.fileName = name || fileName
|
||||||
})
|
})
|
||||||
@@ -168,7 +176,8 @@ class Uploader {
|
|||||||
}, 500)
|
}, 500)
|
||||||
return false
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
ipcMain.removeAllListeners(GET_RENAME_FILE_NAME)
|
// Use IpcManager for safer cleanup
|
||||||
|
IpcManager.removeAllListeners(GET_RENAME_FILE_NAME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +200,8 @@ class Uploader {
|
|||||||
}, 500)
|
}, 500)
|
||||||
return false
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
ipcMain.removeAllListeners(GET_RENAME_FILE_NAME)
|
// Use IpcManager for safer cleanup
|
||||||
|
IpcManager.removeAllListeners(GET_RENAME_FILE_NAME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,8 +166,12 @@ class LifeCycle {
|
|||||||
rpcServer.start()
|
rpcServer.start()
|
||||||
busEventList.listen()
|
busEventList.listen()
|
||||||
|
|
||||||
|
// Start memory monitoring in all environments
|
||||||
|
// More frequent monitoring in development, less frequent in production
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
MemoryMonitor.start(30000)
|
MemoryMonitor.start(30000) // Every 30 seconds in development
|
||||||
|
} else {
|
||||||
|
MemoryMonitor.start(300000) // Every 5 minutes in production
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { ipcMain, IpcMainEvent } from 'electron'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IPC Manager utility to prevent memory leaks from accumulating IPC listeners
|
||||||
|
*/
|
||||||
|
export class IpcManager {
|
||||||
|
private static listeners = new Map<string, Set<{ event: string; handler: Function }>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an IPC listener with automatic cleanup tracking
|
||||||
|
*/
|
||||||
|
static on(channel: string, handler: (event: IpcMainEvent, ...args: any[]) => void, context?: string): () => void {
|
||||||
|
const contextKey = context || 'global'
|
||||||
|
|
||||||
|
if (!this.listeners.has(contextKey)) {
|
||||||
|
this.listeners.set(contextKey, new Set())
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenerEntry = { event: channel, handler }
|
||||||
|
this.listeners.get(contextKey)!.add(listenerEntry)
|
||||||
|
|
||||||
|
ipcMain.on(channel, handler)
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => {
|
||||||
|
ipcMain.removeListener(channel, handler)
|
||||||
|
this.listeners.get(contextKey)?.delete(listenerEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a one-time IPC listener with automatic cleanup tracking
|
||||||
|
*/
|
||||||
|
static once(channel: string, handler: (event: IpcMainEvent, ...args: any[]) => void, context?: string): () => void {
|
||||||
|
const contextKey = context || 'global'
|
||||||
|
|
||||||
|
if (!this.listeners.has(contextKey)) {
|
||||||
|
this.listeners.set(contextKey, new Set())
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrappedHandler = (...args: Parameters<typeof handler>) => {
|
||||||
|
const listenerEntry = { event: channel, handler: wrappedHandler }
|
||||||
|
this.listeners.get(contextKey)?.delete(listenerEntry)
|
||||||
|
handler(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenerEntry = { event: channel, handler: wrappedHandler }
|
||||||
|
this.listeners.get(contextKey)!.add(listenerEntry)
|
||||||
|
|
||||||
|
ipcMain.once(channel, wrappedHandler)
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => {
|
||||||
|
ipcMain.removeListener(channel, wrappedHandler)
|
||||||
|
this.listeners.get(contextKey)?.delete(listenerEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove specific listener
|
||||||
|
*/
|
||||||
|
static removeListener(channel: string, handler: Function, context?: string) {
|
||||||
|
const contextKey = context || 'global'
|
||||||
|
ipcMain.removeListener(channel, handler as any)
|
||||||
|
|
||||||
|
const contextListeners = this.listeners.get(contextKey)
|
||||||
|
if (contextListeners) {
|
||||||
|
for (const listener of contextListeners) {
|
||||||
|
if (listener.event === channel && listener.handler === handler) {
|
||||||
|
contextListeners.delete(listener)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up all listeners for a specific context
|
||||||
|
*/
|
||||||
|
static cleanupContext(context: string) {
|
||||||
|
const contextListeners = this.listeners.get(context)
|
||||||
|
if (!contextListeners) return
|
||||||
|
|
||||||
|
for (const listener of contextListeners) {
|
||||||
|
ipcMain.removeListener(listener.event, listener.handler as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listeners.delete(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all listeners for a specific channel
|
||||||
|
*/
|
||||||
|
static removeAllListeners(channel: string) {
|
||||||
|
ipcMain.removeAllListeners(channel)
|
||||||
|
|
||||||
|
// Clean up from tracking
|
||||||
|
for (const [context, listeners] of this.listeners.entries()) {
|
||||||
|
const toRemove = Array.from(listeners).filter(l => l.event === channel)
|
||||||
|
toRemove.forEach(l => listeners.delete(l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current listener statistics for debugging
|
||||||
|
*/
|
||||||
|
static getListenerStats() {
|
||||||
|
const stats: Record<string, { contexts: number; totalListeners: number; channels: string[] }> = {}
|
||||||
|
|
||||||
|
for (const [context, listeners] of this.listeners.entries()) {
|
||||||
|
const channels = Array.from(new Set(Array.from(listeners).map(l => l.event)))
|
||||||
|
|
||||||
|
if (!stats.global) stats.global = { contexts: 0, totalListeners: 0, channels: [] }
|
||||||
|
|
||||||
|
stats[context] = {
|
||||||
|
contexts: 1,
|
||||||
|
totalListeners: listeners.size,
|
||||||
|
channels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up all tracked listeners (should be called on app quit)
|
||||||
|
*/
|
||||||
|
static cleanupAll() {
|
||||||
|
for (const context of this.listeners.keys()) {
|
||||||
|
this.cleanupContext(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get potentially leaked listeners (ones that have been around too long)
|
||||||
|
*/
|
||||||
|
static detectPotentialLeaks(): { context: string; channel: string; count: number }[] {
|
||||||
|
const channelCounts = new Map<string, Map<string, number>>()
|
||||||
|
|
||||||
|
for (const [context, listeners] of this.listeners.entries()) {
|
||||||
|
if (!channelCounts.has(context)) {
|
||||||
|
channelCounts.set(context, new Map())
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextChannels = channelCounts.get(context)!
|
||||||
|
|
||||||
|
for (const listener of listeners) {
|
||||||
|
const count = contextChannels.get(listener.event) || 0
|
||||||
|
contextChannels.set(listener.event, count + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const potentialLeaks: { context: string; channel: string; count: number }[] = []
|
||||||
|
|
||||||
|
for (const [context, channels] of channelCounts.entries()) {
|
||||||
|
for (const [channel, count] of channels.entries()) {
|
||||||
|
// Flag channels with more than 10 listeners as potential leaks
|
||||||
|
if (count > 10) {
|
||||||
|
potentialLeaks.push({ context, channel, count })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return potentialLeaks
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import { IpcManager } from './ipcManager'
|
||||||
|
|
||||||
export class MemoryMonitor {
|
export class MemoryMonitor {
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
private static interval: NodeJS.Timeout | null = null
|
private static interval: NodeJS.Timeout | null = null
|
||||||
|
private static previousMemory: NodeJS.MemoryUsage | null = null
|
||||||
|
private static memoryHistory: Array<{ timestamp: number; memory: NodeJS.MemoryUsage }> = []
|
||||||
|
private static readonly HISTORY_LIMIT = 60 // Keep last 60 measurements for leak detection
|
||||||
|
|
||||||
static start(intervalMs: number = 30000) {
|
static start(intervalMs: number = 30000) {
|
||||||
if (this.interval) return
|
if (this.interval) return
|
||||||
@@ -13,14 +18,36 @@ export class MemoryMonitor {
|
|||||||
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
|
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
|
||||||
external: Math.round(memUsage.external / 1024 / 1024)
|
external: Math.round(memUsage.external / 1024 / 1024)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track memory history for leak detection
|
||||||
|
this.memoryHistory.push({ timestamp: Date.now(), memory: memUsage })
|
||||||
|
if (this.memoryHistory.length > this.HISTORY_LIMIT) {
|
||||||
|
this.memoryHistory.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect memory leaks
|
||||||
|
const leakInfo = this.detectMemoryLeak()
|
||||||
|
if (leakInfo.hasLeak) {
|
||||||
|
console.warn(`[Memory Leak Detected] ${leakInfo.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for IPC listener leaks
|
||||||
|
const ipcLeaks = IpcManager.detectPotentialLeaks()
|
||||||
|
if (ipcLeaks.length > 0) {
|
||||||
|
console.warn(`[IPC Leak Detected] ${ipcLeaks.length} potential IPC listener leaks:`, ipcLeaks)
|
||||||
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[Memory] RSS: ${mbUsage.rss}MB, Heap: ${mbUsage.heapUsed}/${mbUsage.heapTotal}MB, External: ${mbUsage.external}MB`
|
`[Memory] RSS: ${mbUsage.rss}MB, Heap: ${mbUsage.heapUsed}/${mbUsage.heapTotal}MB, External: ${mbUsage.external}MB${leakInfo.hasLeak ? ' ⚠️' : ''}${ipcLeaks.length > 0 ? ' 📡' : ''}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Trigger garbage collection if memory usage is high
|
||||||
if (mbUsage.heapUsed / mbUsage.heapTotal > 0.8 && global.gc) {
|
if (mbUsage.heapUsed / mbUsage.heapTotal > 0.8 && global.gc) {
|
||||||
console.log('[Memory] Triggering garbage collection')
|
console.log('[Memory] Triggering garbage collection')
|
||||||
global.gc()
|
global.gc()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.previousMemory = memUsage
|
||||||
}, intervalMs)
|
}, intervalMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,5 +56,72 @@ export class MemoryMonitor {
|
|||||||
clearInterval(this.interval)
|
clearInterval(this.interval)
|
||||||
this.interval = null
|
this.interval = null
|
||||||
}
|
}
|
||||||
|
this.previousMemory = null
|
||||||
|
this.memoryHistory = []
|
||||||
|
|
||||||
|
// Clean up all IPC listeners when stopping monitoring
|
||||||
|
IpcManager.cleanupAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private static detectMemoryLeak(): { hasLeak: boolean; message: string } {
|
||||||
|
if (this.memoryHistory.length < 10) {
|
||||||
|
return { hasLeak: false, message: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const recent = this.memoryHistory.slice(-10)
|
||||||
|
const oldest = this.memoryHistory[0]
|
||||||
|
|
||||||
|
// Check for steady memory growth over time
|
||||||
|
const rssGrowth = recent[recent.length - 1].memory.rss - oldest.memory.rss
|
||||||
|
const heapGrowth = recent[recent.length - 1].memory.heapUsed - oldest.memory.heapUsed
|
||||||
|
const externalGrowth = recent[recent.length - 1].memory.external - oldest.memory.external
|
||||||
|
|
||||||
|
const rssGrowthMB = Math.round(rssGrowth / 1024 / 1024)
|
||||||
|
const heapGrowthMB = Math.round(heapGrowth / 1024 / 1024)
|
||||||
|
const externalGrowthMB = Math.round(externalGrowth / 1024 / 1024)
|
||||||
|
|
||||||
|
// Thresholds for leak detection
|
||||||
|
const LEAK_THRESHOLD_MB = 50 // 50MB growth over monitoring period
|
||||||
|
|
||||||
|
if (rssGrowthMB > LEAK_THRESHOLD_MB) {
|
||||||
|
return {
|
||||||
|
hasLeak: true,
|
||||||
|
message: `RSS memory grew by ${rssGrowthMB}MB over monitoring period`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (heapGrowthMB > LEAK_THRESHOLD_MB) {
|
||||||
|
return {
|
||||||
|
hasLeak: true,
|
||||||
|
message: `Heap memory grew by ${heapGrowthMB}MB over monitoring period`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (externalGrowthMB > LEAK_THRESHOLD_MB) {
|
||||||
|
return {
|
||||||
|
hasLeak: true,
|
||||||
|
message: `External memory grew by ${externalGrowthMB}MB over monitoring period`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { hasLeak: false, message: '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
static getMemoryStats() {
|
||||||
|
if (this.memoryHistory.length === 0) return null
|
||||||
|
|
||||||
|
const current = this.memoryHistory[this.memoryHistory.length - 1]
|
||||||
|
const oldest = this.memoryHistory[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
current: current.memory,
|
||||||
|
growth: {
|
||||||
|
rss: Math.round((current.memory.rss - oldest.memory.rss) / 1024 / 1024),
|
||||||
|
heapUsed: Math.round((current.memory.heapUsed - oldest.memory.heapUsed) / 1024 / 1024),
|
||||||
|
external: Math.round((current.memory.external - oldest.memory.external) / 1024 / 1024)
|
||||||
|
},
|
||||||
|
historyLength: this.memoryHistory.length,
|
||||||
|
ipcStats: IpcManager.getListenerStats()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ function handlePageScroll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let ro: ResizeObserver | null = null
|
let ro: ResizeObserver | null = null
|
||||||
|
let documentRo: ResizeObserver | null = null
|
||||||
const lastScrollTime = ref(0)
|
const lastScrollTime = ref(0)
|
||||||
|
|
||||||
function updateContainerMetrics() {
|
function updateContainerMetrics() {
|
||||||
@@ -172,13 +173,22 @@ function updateContainerMetrics() {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const el = containerRef.value
|
const el = containerRef.value
|
||||||
if (!el) return
|
if (!el) return
|
||||||
|
|
||||||
|
// Set up main container ResizeObserver
|
||||||
ro = new ResizeObserver(updateContainerMetrics)
|
ro = new ResizeObserver(updateContainerMetrics)
|
||||||
ro.observe(el)
|
ro.observe(el)
|
||||||
|
|
||||||
if (props.pageMode) {
|
if (props.pageMode) {
|
||||||
ro.observe(document.documentElement)
|
// Set up document ResizeObserver separately to avoid conflicts
|
||||||
|
documentRo = new ResizeObserver(updateContainerMetrics)
|
||||||
|
documentRo.observe(document.documentElement)
|
||||||
|
|
||||||
|
// Add scroll listeners with passive option for better performance
|
||||||
window.addEventListener('scroll', handlePageScroll, { passive: true })
|
window.addEventListener('scroll', handlePageScroll, { passive: true })
|
||||||
|
|
||||||
|
// Find and add scroll listeners to parent elements
|
||||||
let parent = el.parentElement
|
let parent = el.parentElement
|
||||||
while (parent) {
|
while (parent && parent !== document.documentElement) {
|
||||||
if (parent.scrollHeight > parent.clientHeight) {
|
if (parent.scrollHeight > parent.clientHeight) {
|
||||||
parent.addEventListener('scroll', handlePageScroll, { passive: true })
|
parent.addEventListener('scroll', handlePageScroll, { passive: true })
|
||||||
parentScrollListeners.value.push(parent)
|
parentScrollListeners.value.push(parent)
|
||||||
@@ -186,17 +196,32 @@ onMounted(() => {
|
|||||||
parent = parent.parentElement
|
parent = parent.parentElement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateContainerMetrics()
|
updateContainerMetrics()
|
||||||
|
|
||||||
if (props.pageMode) {
|
if (props.pageMode) {
|
||||||
window.addEventListener('resize', updateContainerMetrics, { passive: true })
|
window.addEventListener('resize', updateContainerMetrics, { passive: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (ro) ro.disconnect()
|
// Clean up ResizeObservers
|
||||||
|
if (ro) {
|
||||||
|
ro.disconnect()
|
||||||
|
ro = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (documentRo) {
|
||||||
|
documentRo.disconnect()
|
||||||
|
documentRo = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up window listeners
|
||||||
window.removeEventListener('resize', updateContainerMetrics)
|
window.removeEventListener('resize', updateContainerMetrics)
|
||||||
|
|
||||||
if (props.pageMode) {
|
if (props.pageMode) {
|
||||||
window.removeEventListener('scroll', handlePageScroll)
|
window.removeEventListener('scroll', handlePageScroll)
|
||||||
|
// Clean up parent scroll listeners
|
||||||
parentScrollListeners.value.forEach(parent => {
|
parentScrollListeners.value.forEach(parent => {
|
||||||
parent.removeEventListener('scroll', handlePageScroll)
|
parent.removeEventListener('scroll', handlePageScroll)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref, onUnmounted } from 'vue'
|
||||||
|
|
||||||
import type { IStringKeyMap } from '#/types/types'
|
import type { IStringKeyMap } from '#/types/types'
|
||||||
|
|
||||||
@@ -12,6 +12,10 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<string | undefined>()
|
const error = ref<string | undefined>()
|
||||||
|
|
||||||
|
// Track media query listener for cleanup
|
||||||
|
let mediaQueryListener: ((e: MediaQueryListEvent) => void) | null = null
|
||||||
|
let mediaQuery: MediaQueryList | null = null
|
||||||
|
|
||||||
function clearError() {
|
function clearError() {
|
||||||
error.value = undefined
|
error.value = undefined
|
||||||
}
|
}
|
||||||
@@ -24,22 +28,34 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
applyTheme(settings.value.app.theme || 'light')
|
applyTheme(settings.value.app.theme || 'light')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanupMediaQueryListener() {
|
||||||
|
if (mediaQuery && mediaQueryListener) {
|
||||||
|
mediaQuery.removeEventListener('change', mediaQueryListener)
|
||||||
|
mediaQuery = null
|
||||||
|
mediaQueryListener = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyTheme(theme: string) {
|
function applyTheme(theme: string) {
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
root.classList.remove('light', 'dark', 'auto')
|
root.classList.remove('light', 'dark', 'auto')
|
||||||
|
|
||||||
|
// Clean up existing listener before adding new one
|
||||||
|
cleanupMediaQueryListener()
|
||||||
|
|
||||||
if (theme === 'auto') {
|
if (theme === 'auto') {
|
||||||
root.classList.add('auto')
|
root.classList.add('auto')
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
root.classList.add(prefersDark ? 'dark' : 'light')
|
root.classList.add(prefersDark ? 'dark' : 'light')
|
||||||
|
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
mediaQuery.addEventListener('change', e => {
|
mediaQueryListener = (e: MediaQueryListEvent) => {
|
||||||
if (settings.value.app.theme === 'auto') {
|
if (settings.value.app.theme === 'auto') {
|
||||||
root.classList.remove('light', 'dark')
|
root.classList.remove('light', 'dark')
|
||||||
root.classList.add(e.matches ? 'dark' : 'light')
|
root.classList.add(e.matches ? 'dark' : 'light')
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
mediaQuery.addEventListener('change', mediaQueryListener)
|
||||||
} else {
|
} else {
|
||||||
root.classList.add(theme)
|
root.classList.add(theme)
|
||||||
}
|
}
|
||||||
@@ -68,6 +84,11 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up when store is unmounted
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanupMediaQueryListener()
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
init,
|
init,
|
||||||
loadSettings,
|
loadSettings,
|
||||||
|
|||||||
Reference in New Issue
Block a user