diff --git a/package-lock.json b/package-lock.json index a01e644..70cb38e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "shotcut-app", - "version": "1.0.5", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shotcut-app", - "version": "1.0.5", + "version": "1.0.1", "hasInstallScript": true, "dependencies": { "@electron-toolkit/preload": "^3.0.1", @@ -21,6 +21,7 @@ "electron-updater": "^6.3.9", "moment": "^2.30.1", "pusher-js": "^8.4.0", + "uuid": "^13.0.0", "windows-shortcuts": "^0.1.6" }, "devDependencies": { @@ -10726,6 +10727,19 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 6fc1cf5..22b07f1 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "electron-updater": "^6.3.9", "moment": "^2.30.1", "pusher-js": "^8.4.0", + "uuid": "^13.0.0", "windows-shortcuts": "^0.1.6" }, "devDependencies": { diff --git a/src/main/index.ts b/src/main/index.ts index 66c0e61..09784a7 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,20 +1,114 @@ /* eslint-disable prettier/prettier */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { electronApp, optimizer } from '@electron-toolkit/utils' -import { app, ipcMain, Menu, Tray } from 'electron' +import { electronApp, is, optimizer } from '@electron-toolkit/utils' +import { app, BrowserWindow, globalShortcut, ipcMain, Menu, Tray, screen, shell } from 'electron' import fs from 'fs' -import path from 'path' +import path, { join } from 'path' import ws from 'windows-shortcuts' import icon from '../../resources/icon.png?asset' -import { registerGlobalShortcuts, unregisterGlobalShortcuts } from './shortcut' +import { registerGlobalShortcuts, unregisterGlobalShortcuts } from './shotcut' + +let mainWindow: null | BrowserWindow = null +// eslint-disable-next-line prefer-const +let isQuiting = false + +function createWindow(): void { + // Get Screen width, height + const width = 800 + const height = 600 + + mainWindow = new BrowserWindow({ + width, + height, + show: false, + autoHideMenuBar: true, + ...(process.platform === 'linux' ? { icon } : {}), + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + sandbox: false + } + }) + + // Set App Show Position + mainWindow.setPosition(width / 2, height / 2) + + mainWindow.on('ready-to-show', () => { + mainWindow?.show() + + // 🚀 Mở DevTools khi sẵn sàng + mainWindow?.webContents.openDevTools({ mode: 'detach' }) + }) + + mainWindow.webContents.setWindowOpenHandler((details) => { + shell.openExternal(details.url) + return { action: 'deny' } + }) + + // Make the window always on top + mainWindow.setAlwaysOnTop(true, 'normal') + + // mainWindow.webContents.openDevTools() + + // Inspect element with shortcut + mainWindow.webContents.once('did-finish-load', () => { + const shortcut = 'Control+Shift+C' + + mainWindow?.on('focus', () => { + globalShortcut.register(shortcut, () => { + const pos = screen.getCursorScreenPoint() + mainWindow?.webContents.inspectElement(pos.x, pos.y) + mainWindow?.webContents.focus() // optional: refocus back + }) + }) + + mainWindow?.on('blur', () => { + globalShortcut.unregister(shortcut) + }) + + mainWindow?.on('closed', () => { + globalShortcut.unregister(shortcut) + }) + }) + + // HMR for renderer base on electron-vite cli. + // Load the remote URL for development or the local html file for production. + if (is.dev && process.env['ELECTRON_RENDERER_URL']) { + mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) + } else { + mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + } + + // Khi bấm dấu X + mainWindow.on('close', (event) => { + if (!isQuiting) { + event.preventDefault() + mainWindow?.hide() + } + }) +} function createTray() { const tray = new Tray(icon) const contextMenu = Menu.buildFromTemplate([ + { + label: 'Show', + click: () => { + if (mainWindow) { + if (mainWindow.isMinimized()) { + mainWindow.restore() + } + mainWindow.show() + mainWindow.focus() + } else { + createWindow() + } + } + }, { label: 'Quit', click: () => { + isQuiting = true app.quit() } } @@ -69,3 +163,57 @@ app.whenReady().then(() => { app.on('will-quit', () => { unregisterGlobalShortcuts() }) + +// Quit when all windows are closed, except on macOS. There, it's common +// for applications and their menu bar to stay active until the user quits +// explicitly with Cmd + Q. +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } +}) + +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. +// Custom events +ipcMain.handle('save-config-file', async (_event, jsonContent: any) => { + // const filePath = path.join(__dirname, 'config-data.json') + const userDataPath = app.getPath('userData') + const filePath = path.join(userDataPath, 'config-data.json') + + try { + // 📦 Ghi dữ liệu mới + const newData = jsonContent + + fs.writeFileSync(filePath, JSON.stringify(newData, null, 2), 'utf-8') + + // Đăng ký lại global shortcuts + unregisterGlobalShortcuts() + registerGlobalShortcuts() + + return { success: true, path: filePath } + } catch (error: any) { + console.error('Error saving file:', error) + return { success: false, error: error.message } + } +}) + +ipcMain.handle('get-config-file', async () => { + const userDataPath = app.getPath('userData') + const filePath = path.join(userDataPath, 'config-data.json') + + try { + // 📂 Kiểm tra file có tồn tại không + if (!fs.existsSync(filePath)) { + return { success: false, error: 'Config file does not exist' } + } + + const rawData = fs.readFileSync(filePath, 'utf-8') + const fileData = JSON.parse(rawData) + + return fileData + } catch (error: any) { + console.error('Error reading config file:', error) + return null + } +}) diff --git a/src/main/shortcut.ts b/src/main/shortcut.ts deleted file mode 100644 index db3b62e..0000000 --- a/src/main/shortcut.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { globalShortcut, clipboard, shell } from 'electron' - -export interface IShortcut { - shortcut: string - links: string[] -} - -// Danh sách shortcut mẫu -const shortcuts: IShortcut[] = [ - { - links: [ - 'https://int.ipsupply.com.au/erptools/001_search-vpn?search=${query}', - 'https://www.ebay.com/sch/i.html?_nkw=${query}&_sop=15' - ], - shortcut: 'CommandOrControl+Shift+1' - }, - { - links: ['https://esearch.danielvu.com?keyword=${query}'], - shortcut: 'CommandOrControl+Shift+2' - } -] - -/** - * Mở tất cả link gắn với shortcut và thay ${query} bằng clipboard text - */ -const handleOpenBrowserByLink = (data: IShortcut) => { - const text = clipboard.readText().trim() - - if (!text) { - console.log(`[${new Date().toLocaleTimeString()}] Clipboard is empty.`) - return - } - - const query = encodeURIComponent(text) - - data.links.forEach((link) => { - const url = link.replace(/\$\{query\}/g, query) // Thay ${query} trong link - console.log(`[${new Date().toLocaleTimeString()}] Opening URL: ${url}`) - shell.openExternal(url) // Mở trong browser mặc định - }) -} - -/** - * Đăng ký tất cả global shortcut - */ -export function registerGlobalShortcuts() { - shortcuts.forEach((sc) => { - const ret = globalShortcut.register(sc.shortcut, () => { - console.log(`🚀 Triggered shortcut: ${sc.shortcut}`) - handleOpenBrowserByLink(sc) - }) - - if (!ret) { - console.log(`❌ Failed to register shortcut: ${sc.shortcut}`) - } else { - console.log(`✅ Registered shortcut: ${sc.shortcut}`) - } - }) -} - -/** - * Xóa tất cả global shortcut khi thoát app - */ -export function unregisterGlobalShortcuts() { - globalShortcut.unregisterAll() - console.log('ℹ️ All global shortcuts unregistered.') -} diff --git a/src/main/shotcut.ts b/src/main/shotcut.ts new file mode 100644 index 0000000..28818eb --- /dev/null +++ b/src/main/shotcut.ts @@ -0,0 +1,126 @@ +// shotcut.ts +import { globalShortcut, clipboard, shell, app } from 'electron' +import fs from 'fs' +import path from 'path' +import { v4 as uuid } from 'uuid' + +export interface IShortcut { + id: string + shortcut: string + links: string[] +} + +// Danh sách shortcut mặc định +export const defaultShortcuts: IShortcut[] = [ + { + id: uuid(), + links: [ + 'https://int.ipsupply.com.au/erptools/001_search-vpn?search=${keyword}', + 'https://www.ebay.com/sch/i.html?_nkw=${keyword}&_sop=15' + ], + shortcut: 'CommandOrControl+Shift+1' + }, + { + id: uuid(), + links: ['https://esearch.danielvu.com?keyword=${keyword}'], + shortcut: 'CommandOrControl+Shift+2' + } +] + +// Lấy đường dẫn file config trong thư mục userData +const getConfigFilePath = () => { + return path.join(app.getPath('userData'), 'config-data.json') +} + +/** + * Đảm bảo file config tồn tại + * Nếu chưa có, tạo mới với dữ liệu defaultShortcuts + */ +const ensureConfigFile = () => { + const filePath = getConfigFilePath() + + if (!fs.existsSync(filePath)) { + try { + fs.writeFileSync(filePath, JSON.stringify(defaultShortcuts, null, 2), 'utf-8') + console.log(`📝 Created default config file at: ${filePath}`) + } catch (err) { + console.error('❌ Failed to create default config file:', err) + } + } +} + +/** + * Đọc danh sách shortcut từ file config + */ +const loadShortcutsFromConfig = (): IShortcut[] => { + const filePath = getConfigFilePath() + + ensureConfigFile() // 🔹 Đảm bảo luôn có file trước khi đọc + + try { + const rawData = fs.readFileSync(filePath, 'utf-8') + const shortcuts = JSON.parse(rawData) as IShortcut[] + + if (!Array.isArray(shortcuts)) { + throw new Error('Config file format is invalid: must be an array') + } + + return shortcuts + } catch (err) { + console.error('❌ Failed to read or parse config file:', err) + return [] + } +} + +/** + * Mở tất cả link gắn với shortcut và thay ${query} bằng clipboard text + */ +const handleOpenBrowserByLink = (data: IShortcut) => { + const text = clipboard.readText().trim() + + if (!text) { + console.log(`[${new Date().toLocaleTimeString()}] Clipboard is empty.`) + return + } + + const query = encodeURIComponent(text) + + data.links.forEach((link) => { + const url = link.replace(/\$\{keyword\}/g, query) // Thay ${query} trong link + console.log(`[${new Date().toLocaleTimeString()}] Opening URL: ${url}`) + shell.openExternal(url) // Mở trong browser mặc định + }) +} + +/** + * Đăng ký tất cả global shortcut từ file config + */ +export function registerGlobalShortcuts() { + const shortcuts = loadShortcutsFromConfig() + + if (shortcuts.length === 0) { + console.log('⚠️ No shortcuts found to register.') + return + } + + shortcuts.forEach((sc) => { + const ret = globalShortcut.register(sc.shortcut, () => { + console.log(`🚀 Triggered shortcut: ${sc.shortcut}`) + handleOpenBrowserByLink(sc) + }) + + if (!ret) { + console.log(`❌ Failed to register shortcut: ${sc.shortcut}`) + } else { + console.log(`✅ Registered shortcut: ${sc.shortcut}`) + } + }) +} + +/** + * Xóa tất cả global shortcut khi thoát app + */ +export function unregisterGlobalShortcuts() { + globalShortcut.unregisterAll() + console.log('ℹ️ All global shortcuts unregistered.') +} diff --git a/src/renderer/index.html b/src/renderer/index.html index e69de29..9e9fc64 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -0,0 +1,17 @@ + + +
+ +