update show hide window
This commit is contained in:
parent
6f9442a02e
commit
bdc9a49da2
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "new-item-app",
|
"name": "new-item-app",
|
||||||
"version": "1.0.3",
|
"version": "1.0.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "new-item-app",
|
"name": "new-item-app",
|
||||||
"version": "1.0.3",
|
"version": "1.0.5",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron-toolkit/preload": "^3.0.1",
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ function createTray() {
|
||||||
label: 'Show',
|
label: 'Show',
|
||||||
click: () => {
|
click: () => {
|
||||||
mainWindow?.show()
|
mainWindow?.show()
|
||||||
|
mainWindow?.focus()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -147,6 +148,16 @@ app.whenReady().then(() => {
|
||||||
// Create tray icon
|
// Create tray icon
|
||||||
createTray()
|
createTray()
|
||||||
|
|
||||||
|
// Lắng nghe khi hiện cửa sổ
|
||||||
|
mainWindow?.on('show', () => {
|
||||||
|
mainWindow?.webContents.send('window-event', 'show')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Lắng nghe khi ẩn cửa sổ
|
||||||
|
mainWindow?.on('hide', () => {
|
||||||
|
mainWindow?.webContents.send('window-event', 'hide')
|
||||||
|
})
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
// On macOS it's common to re-create a window in the app when the
|
// On macOS it's common to re-create a window in the app when the
|
||||||
// dock icon is clicked and there are no other windows open.
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
|
@ -175,7 +186,6 @@ ipcMain.handle('save-config-file', async (_event, jsonContent: any) => {
|
||||||
try {
|
try {
|
||||||
let fileData: any = {}
|
let fileData: any = {}
|
||||||
|
|
||||||
// 👀 Kiểm tra nếu file đã tồn tại
|
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
const rawData = fs.readFileSync(filePath, 'utf-8')
|
const rawData = fs.readFileSync(filePath, 'utf-8')
|
||||||
fileData = JSON.parse(rawData)
|
fileData = JSON.parse(rawData)
|
||||||
|
|
@ -221,6 +231,7 @@ ipcMain.handle('get-config-file', async () => {
|
||||||
|
|
||||||
ipcMain.handle('show-window', async () => {
|
ipcMain.handle('show-window', async () => {
|
||||||
mainWindow?.show()
|
mainWindow?.show()
|
||||||
|
mainWindow?.focus()
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('hide-window', async () => {
|
ipcMain.handle('hide-window', async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,31 @@
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { contextBridge, ipcRenderer } from 'electron'
|
import { contextBridge, ipcRenderer, shell } from 'electron'
|
||||||
import { electronAPI } from '@electron-toolkit/preload'
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
|
|
||||||
// Custom APIs for renderer
|
const customAPI = {
|
||||||
const api = {}
|
ipcRenderer: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args),
|
||||||
|
on: (channel: string, listener: (...args: any[]) => void) =>
|
||||||
|
ipcRenderer.on(channel, (_, ...args) => listener(...args)),
|
||||||
|
removeAllListeners: (channel: string) => ipcRenderer.removeAllListeners(channel)
|
||||||
|
},
|
||||||
|
openExternal: (url: string) => shell.openExternal(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gộp cả electronAPI và customAPI
|
||||||
|
const mergedAPI = {
|
||||||
|
...electronAPI,
|
||||||
|
...customAPI
|
||||||
|
}
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
|
||||||
// renderer only if context isolation is enabled, otherwise
|
|
||||||
// just add to the DOM global.
|
|
||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
contextBridge.exposeInMainWorld('electron', mergedAPI)
|
||||||
contextBridge.exposeInMainWorld('api', api)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error('Failed to expose electron API:', error)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore (define in dts)
|
// @ts-ignore
|
||||||
window.electron = electronAPI
|
window.electron = mergedAPI
|
||||||
// @ts-ignore (define in dts)
|
|
||||||
window.api = api
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron', {
|
|
||||||
ipcRenderer: {
|
|
||||||
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import { Notifications } from '@mantine/notifications'
|
||||||
import '@mantine/notifications/styles.css'
|
import '@mantine/notifications/styles.css'
|
||||||
import { Product } from '@renderer/types/Product'
|
import { Product } from '@renderer/types/Product'
|
||||||
import { IconDotsVertical } from '@tabler/icons-react'
|
import { IconDotsVertical } from '@tabler/icons-react'
|
||||||
import { Suspense, useCallback, useEffect, useState } from 'react'
|
import { Suspense, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { listHotItem } from './api/products'
|
import { listHotItem } from './api/products'
|
||||||
import CardItem from './components/CardItem'
|
import CardItem from './components/CardItem'
|
||||||
import ConfigModal from './components/ConfigModal'
|
import ConfigModal from './components/ConfigModal'
|
||||||
|
|
@ -32,20 +32,36 @@ function App(): React.JSX.Element {
|
||||||
const [newItems, setnewItems] = useState<Array<Product>>([])
|
const [newItems, setnewItems] = useState<Array<Product>>([])
|
||||||
const [hotItem, setHotItem] = useState<any>([])
|
const [hotItem, setHotItem] = useState<any>([])
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||||
const [timeoutNoti, setTimeoutNoti] = useState(2000)
|
const [jsonData, setJsonData] = useState({
|
||||||
|
noti_timeout: 2000,
|
||||||
|
delay_hide: 10000
|
||||||
|
})
|
||||||
|
const [mouseEnter, setmouseEnter] = useState(false)
|
||||||
|
|
||||||
const [opened, { open, close }] = useDisclosure(false)
|
const [opened, { open, close }] = useDisclosure(false)
|
||||||
|
|
||||||
|
const refMouseEnterTimeout = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const refNotiTimeout = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const appStartedRef = useRef(false)
|
||||||
|
|
||||||
|
const jsonDataRef = useRef(jsonData)
|
||||||
|
|
||||||
const loadTimeOutData = async () => {
|
const loadTimeOutData = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await window.electron.ipcRenderer.invoke('get-config-file')
|
const data = await window.electron.ipcRenderer.invoke('get-config-file')
|
||||||
|
|
||||||
if (!data?.noti_timeout) {
|
if (!data?.noti_timeout) {
|
||||||
await window.electron.ipcRenderer.invoke('save-config-file', { noti_timeout: 2 })
|
await window.electron.ipcRenderer.invoke('save-config-file', {
|
||||||
|
noti_timeout: 2,
|
||||||
|
delay_hide: 10
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeoutNoti(data.noti_timeout * 1000)
|
setJsonData({
|
||||||
|
noti_timeout: data.noti_timeout * 1000,
|
||||||
|
delay_hide: data.delay_hide * 1000
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('%csrc/renderer/src/App.tsx:48 error', 'color: #007acc;', error)
|
console.log('%csrc/renderer/src/App.tsx:48 error', 'color: #007acc;', error)
|
||||||
}
|
}
|
||||||
|
|
@ -65,17 +81,13 @@ function App(): React.JSX.Element {
|
||||||
|
|
||||||
window.electron.ipcRenderer.invoke('show-window')
|
window.electron.ipcRenderer.invoke('show-window')
|
||||||
|
|
||||||
let timeoutId: NodeJS.Timeout | null = null
|
clearTimeouts()
|
||||||
|
|
||||||
if (timeoutId) {
|
if (isHotItem(data.data) || mouseEnter) return
|
||||||
clearTimeout(timeoutId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isHotItem(data.data)) return
|
refNotiTimeout.current = setTimeout(() => {
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
window.electron.ipcRenderer.invoke('hide-window')
|
window.electron.ipcRenderer.invoke('hide-window')
|
||||||
}, timeoutNoti)
|
}, jsonData.noti_timeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,7 +98,7 @@ function App(): React.JSX.Element {
|
||||||
pusher.unsubscribe(PUSHER_CHANNEL)
|
pusher.unsubscribe(PUSHER_CHANNEL)
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [timeoutNoti, hotItem])
|
}, [jsonData.noti_timeout, hotItem, mouseEnter])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// load timeout config form local
|
// load timeout config form local
|
||||||
|
|
@ -116,6 +128,77 @@ function App(): React.JSX.Element {
|
||||||
[hotItem]
|
[hotItem]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleMouseEnter = useCallback(() => {
|
||||||
|
setmouseEnter(true)
|
||||||
|
|
||||||
|
clearTimeouts()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Đây là hàm không phụ thuộc jsonData nữa, lấy delay_hide từ ref
|
||||||
|
const handleMouseLeave = useCallback(() => {
|
||||||
|
setmouseEnter(false)
|
||||||
|
if (refMouseEnterTimeout.current) {
|
||||||
|
clearTimeout(refMouseEnterTimeout.current)
|
||||||
|
}
|
||||||
|
refMouseEnterTimeout.current = setTimeout(() => {
|
||||||
|
window.electron.ipcRenderer.invoke('hide-window')
|
||||||
|
}, jsonDataRef.current.delay_hide)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const initApp = useCallback(() => {
|
||||||
|
if (appStartedRef.current) return
|
||||||
|
appStartedRef.current = true
|
||||||
|
|
||||||
|
window.focus()
|
||||||
|
|
||||||
|
document.addEventListener('mouseenter', handleMouseEnter)
|
||||||
|
document.addEventListener('mouseleave', handleMouseLeave)
|
||||||
|
}, [handleMouseEnter, handleMouseLeave])
|
||||||
|
|
||||||
|
// Phần event lắng nghe window-event thì giữ nguyên
|
||||||
|
|
||||||
|
// Phần clean up khi window ẩn:
|
||||||
|
const cleanUpApp = useCallback(() => {
|
||||||
|
document.removeEventListener('mouseenter', handleMouseEnter)
|
||||||
|
document.removeEventListener('mouseleave', handleMouseLeave)
|
||||||
|
clearTimeouts()
|
||||||
|
}, [handleMouseEnter, handleMouseLeave])
|
||||||
|
|
||||||
|
const clearTimeouts = () => {
|
||||||
|
const timeoutIds = [refMouseEnterTimeout, refNotiTimeout]
|
||||||
|
|
||||||
|
timeoutIds.forEach((timeout) => {
|
||||||
|
if (!timeout.current) return
|
||||||
|
|
||||||
|
clearTimeout(timeout.current)
|
||||||
|
timeout.current = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (type: any) => {
|
||||||
|
console.log('Window event:', type)
|
||||||
|
if (type === 'show') {
|
||||||
|
initApp()
|
||||||
|
}
|
||||||
|
if (type === 'hide') {
|
||||||
|
appStartedRef.current = false
|
||||||
|
cleanUpApp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.electron.ipcRenderer.on('window-event', handler)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.electron.ipcRenderer.removeAllListeners('window-event')
|
||||||
|
}
|
||||||
|
}, [initApp, cleanUpApp])
|
||||||
|
|
||||||
|
// Cập nhật ref mỗi khi jsonData thay đổi
|
||||||
|
useEffect(() => {
|
||||||
|
jsonDataRef.current = jsonData
|
||||||
|
}, [jsonData])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineProvider theme={theme}>
|
<MantineProvider theme={theme}>
|
||||||
<Notifications limit={10} store={myNotificationStore} position="top-right" />
|
<Notifications limit={10} store={myNotificationStore} position="top-right" />
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-overflow-1-line:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.item-picture {
|
.item-picture {
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Anchor, Box, Button, Card, Grid, Text, Title } from '@mantine/core'
|
||||||
import { Product } from '@renderer/types/Product'
|
import { Product } from '@renderer/types/Product'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import TimeCounter from './TimeCounter'
|
import TimeCounter from './TimeCounter'
|
||||||
|
import { linkToItem } from '@renderer/utils/fn'
|
||||||
|
|
||||||
interface CardItemProps {
|
interface CardItemProps {
|
||||||
item: Product
|
item: Product
|
||||||
|
|
@ -9,6 +10,9 @@ interface CardItemProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardItem: React.FC<CardItemProps> = ({ item, hotItem }) => {
|
const CardItem: React.FC<CardItemProps> = ({ item, hotItem }) => {
|
||||||
|
const handleClickExternalLink = () => {
|
||||||
|
window.electron?.openExternal(linkToItem(item))
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Card className="item-card" bg={'#f1f3f5'} mb={8} px={8} py={'xl'}>
|
<Card className="item-card" bg={'#f1f3f5'} mb={8} px={8} py={'xl'}>
|
||||||
<Grid>
|
<Grid>
|
||||||
|
|
@ -55,7 +59,13 @@ const CardItem: React.FC<CardItemProps> = ({ item, hotItem }) => {
|
||||||
<img src={item?.picture} alt={item?.title} className="item-picture" />
|
<img src={item?.picture} alt={item?.title} className="item-picture" />
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={10}>
|
<Grid.Col span={10}>
|
||||||
<Title order={4} c="#0202bf" mb={4} className="text-overflow-1-line">
|
<Title
|
||||||
|
onClick={handleClickExternalLink}
|
||||||
|
order={4}
|
||||||
|
c="#0202bf"
|
||||||
|
mb={4}
|
||||||
|
className="text-overflow-1-line"
|
||||||
|
>
|
||||||
{item?.title}
|
{item?.title}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ export interface IConfigModalProps extends ModalProps {
|
||||||
export default function ConfigModal(props: IConfigModalProps) {
|
export default function ConfigModal(props: IConfigModalProps) {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
timeout: 30 // default timeout 30s
|
timeout: 10, // default timeout 30s
|
||||||
|
delay_hide: 10
|
||||||
},
|
},
|
||||||
validate: {
|
validate: {
|
||||||
timeout: (value) => (value <= 0 ? 'Timeout must be greater than 0' : null)
|
timeout: (value) => (value <= 0 ? 'Timeout must be greater than 0' : null),
|
||||||
|
delay_hide: (value) => (value <= 0 ? 'Delay must be greater than 0' : null)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -20,7 +22,8 @@ export default function ConfigModal(props: IConfigModalProps) {
|
||||||
if (!form.isValid()) return // Nếu form lỗi thì không save
|
if (!form.isValid()) return // Nếu form lỗi thì không save
|
||||||
|
|
||||||
const jsonContent = {
|
const jsonContent = {
|
||||||
noti_timeout: form.values.timeout
|
noti_timeout: form.values.timeout,
|
||||||
|
delay_hide: form.values.delay_hide
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -43,7 +46,7 @@ export default function ConfigModal(props: IConfigModalProps) {
|
||||||
|
|
||||||
if (!data) return
|
if (!data) return
|
||||||
|
|
||||||
form.setValues({ timeout: data?.noti_timeout })
|
form.setValues({ timeout: data?.noti_timeout, delay_hide: data.delay_hide })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(
|
console.log(
|
||||||
'%csrc/renderer/src/components/ConfigModal.tsx:41 error',
|
'%csrc/renderer/src/components/ConfigModal.tsx:41 error',
|
||||||
|
|
@ -69,6 +72,12 @@ export default function ConfigModal(props: IConfigModalProps) {
|
||||||
min={1}
|
min={1}
|
||||||
{...form.getInputProps('timeout')}
|
{...form.getInputProps('timeout')}
|
||||||
/>
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label="Delay (seconds)"
|
||||||
|
placeholder="Enter timeout in seconds"
|
||||||
|
min={1}
|
||||||
|
{...form.getInputProps('delay_hide')}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button type="submit" fullWidth>
|
<Button type="submit" fullWidth>
|
||||||
Save
|
Save
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
export const linkToItem = (item: any) => {
|
||||||
|
switch (item.from_site) {
|
||||||
|
case 'EBAY_AU':
|
||||||
|
return `https://www.ebay.com.au/itm/${item.id}`
|
||||||
|
case 'EBAY_ENCA':
|
||||||
|
return `https://www.ebay.ca/itm/${item.id}`
|
||||||
|
case 'EBAY_GB':
|
||||||
|
return `https://www.ebay.co.uk/itm/${item.id}`
|
||||||
|
default:
|
||||||
|
return `https://www.ebay.com/itm/${item.id}`
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue