Compare commits
No commits in common. "main" and "zelda.add-tray-icon" have entirely different histories.
main
...
zelda.add-
|
|
@ -1,59 +1,43 @@
|
|||
appId: com.electron.app
|
||||
productName: New Item
|
||||
|
||||
directories:
|
||||
buildResources: build
|
||||
|
||||
buildResources: build
|
||||
files:
|
||||
- '!**/.vscode/*'
|
||||
- '!src/*'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
|
||||
- '!**/.vscode/*'
|
||||
- '!src/*'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
|
||||
- resources/**
|
||||
win:
|
||||
target: nsis
|
||||
executableName: New-item
|
||||
publisherName: apactech
|
||||
|
||||
portable:
|
||||
artifactName: ${name}-${version}.exe
|
||||
|
||||
nsis: # vẫn giữ để dùng nếu muốn cài đặt, nhưng sẽ không tạo nếu chỉ chọn portable
|
||||
artifactName: ${name}-${version}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
|
||||
executableName: New-item
|
||||
nsis:
|
||||
artifactName: ${name}-${version}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
uninstallDisplayName: ${productName}
|
||||
createDesktopShortcut: always
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
notarize: false
|
||||
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
notarize: false
|
||||
dmg:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
appImage:
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
npmRebuild: false
|
||||
|
||||
publish:
|
||||
provider: generic
|
||||
url: https://apactech.io/auto-updates
|
||||
provider: generic
|
||||
url: https://example.com/auto-updates
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "new-item-app",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "new-item-app",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
|
|
@ -20,8 +20,7 @@
|
|||
"axios": "^1.9.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"moment": "^2.30.1",
|
||||
"pusher-js": "^8.4.0",
|
||||
"windows-shortcuts": "^0.1.6"
|
||||
"pusher-js": "^8.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
|
|
@ -10980,12 +10979,6 @@
|
|||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||
}
|
||||
},
|
||||
"node_modules/windows-shortcuts": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/windows-shortcuts/-/windows-shortcuts-0.1.6.tgz",
|
||||
"integrity": "sha512-kjkb3Hmmmg7jwnOb+29AOmoEEA1L/JeLsMOYovpLxYpuc+fN0R+pr8sMwep3JFhUZloxyw1XTzq8n3HugXkqBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"name": "new-item-app",
|
||||
"version": "1.0.5",
|
||||
"description": "new item application is active",
|
||||
"version": "1.0.0",
|
||||
"description": "An Electron application with React and TypeScript",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "apactech.io",
|
||||
"author": "example.com",
|
||||
"homepage": "https://electron-vite.org",
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
|
|
@ -32,8 +32,7 @@
|
|||
"axios": "^1.9.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"moment": "^2.30.1",
|
||||
"pusher-js": "^8.4.0",
|
||||
"windows-shortcuts": "^0.1.6"
|
||||
"pusher-js": "^8.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
|
|
|
|||
|
|
@ -5,11 +5,9 @@ import { app, BrowserWindow, globalShortcut, ipcMain, Menu, screen, shell, Tray
|
|||
import fs from 'fs'
|
||||
import path, { join } from 'path'
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
import ws from 'windows-shortcuts'
|
||||
|
||||
let mainWindow: null | BrowserWindow = null
|
||||
let isQuiting = false
|
||||
|
||||
function createWindow(): void {
|
||||
// Get Screen width, height
|
||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize
|
||||
|
|
@ -41,7 +39,10 @@ function createWindow(): void {
|
|||
// Make the window always on top
|
||||
mainWindow.setAlwaysOnTop(true, 'normal')
|
||||
|
||||
// mainWindow.webContents.openDevTools()
|
||||
// Right-click to Inspect Element
|
||||
// mainWindow.webContents.on('context-menu', (_, params) => {
|
||||
// mainWindow?.webContents.inspectElement(params.x, params.y)
|
||||
// })
|
||||
|
||||
// Inspect element with shortcut
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
|
|
@ -89,7 +90,6 @@ function createTray() {
|
|||
label: 'Show',
|
||||
click: () => {
|
||||
mainWindow?.show()
|
||||
mainWindow?.focus()
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -113,22 +113,6 @@ function createTray() {
|
|||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
// const startupFlagFile = path.join(app.getPath('userData'), 'startup-set.flag')
|
||||
const startupFolder = path.join(
|
||||
app.getPath('appData'),
|
||||
'Microsoft\\Windows\\Start Menu\\Programs\\Startup'
|
||||
)
|
||||
const shortcutPath = path.join(startupFolder, 'New Item.lnk')
|
||||
|
||||
if (!fs.existsSync(shortcutPath)) {
|
||||
ws.create(shortcutPath, {
|
||||
target: process.execPath,
|
||||
workingDir: path.dirname(process.execPath),
|
||||
runStyle: 1,
|
||||
desc: 'Start New Item with Windows',
|
||||
icon: process.execPath
|
||||
})
|
||||
}
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId('com.electron')
|
||||
|
||||
|
|
@ -148,16 +132,6 @@ app.whenReady().then(() => {
|
|||
// Create tray icon
|
||||
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 () {
|
||||
// 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.
|
||||
|
|
@ -186,6 +160,7 @@ ipcMain.handle('save-config-file', async (_event, jsonContent: any) => {
|
|||
try {
|
||||
let fileData: any = {}
|
||||
|
||||
// 👀 Kiểm tra nếu file đã tồn tại
|
||||
if (fs.existsSync(filePath)) {
|
||||
const rawData = fs.readFileSync(filePath, 'utf-8')
|
||||
fileData = JSON.parse(rawData)
|
||||
|
|
@ -228,12 +203,3 @@ ipcMain.handle('get-config-file', async () => {
|
|||
return null
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('show-window', async () => {
|
||||
mainWindow?.show()
|
||||
mainWindow?.focus()
|
||||
})
|
||||
|
||||
ipcMain.handle('hide-window', async () => {
|
||||
mainWindow?.hide()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,31 +1,29 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { contextBridge, ipcRenderer, shell } from 'electron'
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
|
||||
const customAPI = {
|
||||
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
|
||||
}
|
||||
// Custom APIs for renderer
|
||||
const api = {}
|
||||
|
||||
// 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) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', mergedAPI)
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
} catch (error) {
|
||||
console.error('Failed to expose electron API:', error)
|
||||
console.error(error)
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore
|
||||
window.electron = mergedAPI
|
||||
// @ts-ignore (define in dts)
|
||||
window.electron = electronAPI
|
||||
// @ts-ignore (define in dts)
|
||||
window.api = api
|
||||
}
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
ipcRenderer: {
|
||||
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
LoadingOverlay,
|
||||
MantineProvider,
|
||||
Skeleton,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip
|
||||
} from '@mantine/core'
|
||||
|
|
@ -17,13 +18,18 @@ import { Notifications } from '@mantine/notifications'
|
|||
import '@mantine/notifications/styles.css'
|
||||
import { Product } from '@renderer/types/Product'
|
||||
import { IconDotsVertical } from '@tabler/icons-react'
|
||||
import { Suspense, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import moment from 'moment'
|
||||
import { Suspense, useCallback, useEffect, useState } from 'react'
|
||||
import { listHotItem } from './api/products'
|
||||
import CardItem from './components/CardItem'
|
||||
import ConfigModal from './components/ConfigModal'
|
||||
import { pusher } from './pusher'
|
||||
import { theme } from './theme'
|
||||
import { myNotificationStore, playNotificationSound } from './utils/Notificaton'
|
||||
import {
|
||||
myNotificationStore,
|
||||
playNotificationSound,
|
||||
prependNotification
|
||||
} from './utils/Notificaton'
|
||||
|
||||
const PUSHER_CHANNEL = import.meta.env.VITE_PUSHER_CHANNEL
|
||||
const PUSHER_EVENT = 'App\\Events\\MessagePushed'
|
||||
|
|
@ -32,36 +38,20 @@ function App(): React.JSX.Element {
|
|||
const [newItems, setnewItems] = useState<Array<Product>>([])
|
||||
const [hotItem, setHotItem] = useState<any>([])
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const [jsonData, setJsonData] = useState({
|
||||
noti_timeout: 2000,
|
||||
delay_hide: 10000
|
||||
})
|
||||
const [mouseEnter, setmouseEnter] = useState(false)
|
||||
const [timeout, setTimeout] = useState(2000)
|
||||
|
||||
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 () => {
|
||||
try {
|
||||
const data = await window.electron.ipcRenderer.invoke('get-config-file')
|
||||
|
||||
if (!data?.noti_timeout) {
|
||||
await window.electron.ipcRenderer.invoke('save-config-file', {
|
||||
noti_timeout: 2,
|
||||
delay_hide: 10
|
||||
})
|
||||
await window.electron.ipcRenderer.invoke('save-config-file', { noti_timeout: 2 })
|
||||
return
|
||||
}
|
||||
|
||||
setJsonData({
|
||||
noti_timeout: data.noti_timeout * 1000,
|
||||
delay_hide: data.delay_hide * 1000
|
||||
})
|
||||
setTimeout(data.noti_timeout * 1000)
|
||||
} catch (error) {
|
||||
console.log('%csrc/renderer/src/App.tsx:48 error', 'color: #007acc;', error)
|
||||
}
|
||||
|
|
@ -76,18 +66,35 @@ function App(): React.JSX.Element {
|
|||
if (data.data && data.data?.id) {
|
||||
setnewItems((prev: Array<Product>) => [data.data, ...prev])
|
||||
|
||||
// Show notification
|
||||
prependNotification(
|
||||
{
|
||||
title: (
|
||||
<Title order={4} className="text-overflow-1-line">
|
||||
{data.data?.title}
|
||||
</Title>
|
||||
),
|
||||
message: (
|
||||
<Box>
|
||||
<Text style={{ fontSize: 14 }} mb={2}>
|
||||
Price: <span className="bold-text">${data.data?.price}</span>,
|
||||
Site: <span className="bold-text">{data.data?.from_site}</span>,
|
||||
Seller: <span className="bold-text">{data.data?.seller}</span>
|
||||
</Text>
|
||||
|
||||
<Text style={{ fontSize: 12 }}>
|
||||
{moment(new Date()).format('HH:mm A')}
|
||||
</Text>
|
||||
</Box>
|
||||
),
|
||||
autoClose: isHotItem(data.data) ? false : timeout,
|
||||
color: 'orange'
|
||||
},
|
||||
() => isHotItem(data.data)
|
||||
)
|
||||
|
||||
// Play sound effect
|
||||
playNotificationSound()
|
||||
|
||||
window.electron.ipcRenderer.invoke('show-window')
|
||||
|
||||
clearTimeouts()
|
||||
|
||||
if (isHotItem(data.data) || mouseEnter) return
|
||||
|
||||
refNotiTimeout.current = setTimeout(() => {
|
||||
window.electron.ipcRenderer.invoke('hide-window')
|
||||
}, jsonData.noti_timeout)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +105,7 @@ function App(): React.JSX.Element {
|
|||
pusher.unsubscribe(PUSHER_CHANNEL)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [jsonData.noti_timeout, hotItem, mouseEnter])
|
||||
}, [timeout, hotItem])
|
||||
|
||||
useEffect(() => {
|
||||
// load timeout config form local
|
||||
|
|
@ -128,77 +135,6 @@ function App(): React.JSX.Element {
|
|||
[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 (
|
||||
<MantineProvider theme={theme}>
|
||||
<Notifications limit={10} store={myNotificationStore} position="top-right" />
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
@keyframes borderPulse {
|
||||
0% {
|
||||
border-color: #e03131;
|
||||
border-color: red;
|
||||
}
|
||||
50% {
|
||||
border-color: black;
|
||||
}
|
||||
100% {
|
||||
border-color: #e03131;
|
||||
border-color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.item-card {
|
||||
animation: borderPulse 1s infinite;
|
||||
border: 4px solid #e03131;
|
||||
border: 4px solid red;
|
||||
border-radius: 4px;
|
||||
transition: border-color 0.3s ease;
|
||||
padding: 10px;
|
||||
|
|
@ -34,11 +34,6 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.text-overflow-1-line:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-picture {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { Anchor, Box, Button, Card, Grid, Text, Title } from '@mantine/core'
|
||||
import { Card, Grid, Title, Text, Badge, Button, Anchor } from '@mantine/core'
|
||||
import { Product } from '@renderer/types/Product'
|
||||
import moment from 'moment'
|
||||
import TimeCounter from './TimeCounter'
|
||||
import { linkToItem } from '@renderer/utils/fn'
|
||||
|
||||
interface CardItemProps {
|
||||
item: Product
|
||||
|
|
@ -10,62 +9,21 @@ interface CardItemProps {
|
|||
}
|
||||
|
||||
const CardItem: React.FC<CardItemProps> = ({ item, hotItem }) => {
|
||||
const handleClickExternalLink = () => {
|
||||
window.electron?.openExternal(linkToItem(item))
|
||||
}
|
||||
return (
|
||||
<Card className="item-card" bg={'#f1f3f5'} mb={8} px={8} py={'xl'}>
|
||||
<Card className="item-card" mb={4} p={8}>
|
||||
<Grid>
|
||||
<Grid.Col
|
||||
span={2}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Grid.Col span={2}>
|
||||
{hotItem ? (
|
||||
<Box
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: 80,
|
||||
height: 80,
|
||||
backgroundColor: '#e03131',
|
||||
clipPath: 'polygon(100% 0, 100% 100%, 0 0)',
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
alignItems: 'flex-start',
|
||||
paddingTop: 10
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
c="white"
|
||||
fw={700}
|
||||
size="xs"
|
||||
style={{
|
||||
transform: 'rotate(45deg)',
|
||||
paddingTop: '10px',
|
||||
paddingRight: '8px'
|
||||
}}
|
||||
>
|
||||
HOT
|
||||
</Text>
|
||||
</Box>
|
||||
<Badge size="md" color="red" pos="absolute" radius={0}>
|
||||
Hot
|
||||
</Badge>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<img src={item?.picture} alt={item?.title} className="item-picture" />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={10}>
|
||||
<Title
|
||||
onClick={handleClickExternalLink}
|
||||
order={4}
|
||||
c="#0202bf"
|
||||
mb={4}
|
||||
className="text-overflow-1-line"
|
||||
>
|
||||
<Title order={4} c="#0202bf" mb={4} className="text-overflow-1-line">
|
||||
{item?.title}
|
||||
</Title>
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,10 @@ export interface IConfigModalProps extends ModalProps {
|
|||
export default function ConfigModal(props: IConfigModalProps) {
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
timeout: 10, // default timeout 30s
|
||||
delay_hide: 10
|
||||
timeout: 30 // default timeout 30s
|
||||
},
|
||||
validate: {
|
||||
timeout: (value) => (value <= 0 ? 'Timeout must be greater than 0' : null),
|
||||
delay_hide: (value) => (value <= 0 ? 'Delay must be greater than 0' : null)
|
||||
timeout: (value) => (value <= 0 ? 'Timeout must be greater than 0' : null)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -22,8 +20,7 @@ export default function ConfigModal(props: IConfigModalProps) {
|
|||
if (!form.isValid()) return // Nếu form lỗi thì không save
|
||||
|
||||
const jsonContent = {
|
||||
noti_timeout: form.values.timeout,
|
||||
delay_hide: form.values.delay_hide
|
||||
noti_timeout: form.values.timeout
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -44,9 +41,14 @@ export default function ConfigModal(props: IConfigModalProps) {
|
|||
try {
|
||||
const data = await window.electron.ipcRenderer.invoke('get-config-file')
|
||||
|
||||
console.log(
|
||||
'%csrc/renderer/src/components/ConfigModal.tsx:44 data',
|
||||
'color: #007acc;',
|
||||
data
|
||||
)
|
||||
if (!data) return
|
||||
|
||||
form.setValues({ timeout: data?.noti_timeout, delay_hide: data.delay_hide })
|
||||
form.setValues({ timeout: data?.noti_timeout })
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'%csrc/renderer/src/components/ConfigModal.tsx:41 error',
|
||||
|
|
@ -72,12 +74,6 @@ export default function ConfigModal(props: IConfigModalProps) {
|
|||
min={1}
|
||||
{...form.getInputProps('timeout')}
|
||||
/>
|
||||
<NumberInput
|
||||
label="Delay (seconds)"
|
||||
placeholder="Enter timeout in seconds"
|
||||
min={1}
|
||||
{...form.getInputProps('delay_hide')}
|
||||
/>
|
||||
|
||||
<Button type="submit" fullWidth>
|
||||
Save
|
||||
|
|
|
|||
|
|
@ -1,3 +1,30 @@
|
|||
// /* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
// import { createNotificationsStore, NotificationData } from '@mantine/notifications'
|
||||
// import notifySound from '../assets/notifty.mp3'
|
||||
|
||||
// export const myNotificationStore = createNotificationsStore()
|
||||
|
||||
// export function prependNotification(notification: NotificationData, callback?: () => boolean) {
|
||||
// const id = notification.id ?? crypto.randomUUID()
|
||||
// myNotificationStore.updateState((current) => {
|
||||
// const existing = current.notifications.filter((n) => n.id !== id)
|
||||
// return {
|
||||
// ...current,
|
||||
// notifications: [
|
||||
// {
|
||||
// ...notification,
|
||||
// id
|
||||
// },
|
||||
// ...existing
|
||||
// ]
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
// export const playNotificationSound = () => {
|
||||
// const audio = new Audio(notifySound)
|
||||
// audio.play().catch((e) => console.error('Audio play failed:', e))
|
||||
// }
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { createNotificationsStore, NotificationData } from '@mantine/notifications'
|
||||
import notifySound from '../assets/notifty.mp3'
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
/* 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