From 6f1c98fb4a811f0b5bee013fe983f66ad5b7c62f Mon Sep 17 00:00:00 2001 From: fofolee Date: Fri, 25 Apr 2025 07:45:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0pin=E7=9A=84=E9=9B=8F?= =?UTF-8?q?=E5=BD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/lib/pin/controller.js | 30 ++++++ plugin/lib/pin/service.js | 108 +++++++++++++++++++++ plugin/lib/pin/style.css | 40 ++++++++ plugin/lib/pin/view.html | 15 +++ plugin/preload.js | 1 + src/App.vue | 4 + src/components/card/CommandCardContent.vue | 65 +++++++++++++ 7 files changed, 263 insertions(+) create mode 100644 plugin/lib/pin/controller.js create mode 100644 plugin/lib/pin/service.js create mode 100644 plugin/lib/pin/style.css create mode 100644 plugin/lib/pin/view.html diff --git a/plugin/lib/pin/controller.js b/plugin/lib/pin/controller.js new file mode 100644 index 0000000..37fce5d --- /dev/null +++ b/plugin/lib/pin/controller.js @@ -0,0 +1,30 @@ +const { ipcRenderer } = require("electron"); + +// 等待 DOM 加载完成 +document.addEventListener("DOMContentLoaded", () => { + let parentId = null; + let windowId = null; + let commandCode = null; + + // 监听父窗口发来的配置 + ipcRenderer.on("window-config", (event, config) => { + parentId = event.senderId; + windowId = config.windowId; + commandCode = config.commandCode; + + // 设置主题 + document.documentElement.setAttribute( + "data-theme", + config.isDark ? "dark" : "light" + ); + + // 设置图标 + document.getElementById("command-icon").src = config.icon; + }); + + // 点击图标执行命令 + document.querySelector(".pin-container").addEventListener("click", () => { + console.log("click", parentId, `pin-execute-${windowId}`); + ipcRenderer.sendTo(parentId, `pin-execute-${windowId}`, commandCode); + }); +}); diff --git a/plugin/lib/pin/service.js b/plugin/lib/pin/service.js new file mode 100644 index 0000000..41ed084 --- /dev/null +++ b/plugin/lib/pin/service.js @@ -0,0 +1,108 @@ +const { ipcRenderer } = require("electron"); +const { createBrowserWindow } = utools; + +const pinPath = "lib/pin/view.html"; +const preloadPath = "lib/pin/controller.js"; + +// 存储所有pin窗口的信息 +const pinWindows = new Map(); + +/** + * 创建pin窗口 + * @param {object} commandInfo - 命令信息 + * @param {object} position - 窗口位置 {x, y} + * @returns {Promise} 返回窗口对象 + */ +const createPinWindow = (commandInfo, position = null) => { + return new Promise((resolve) => { + const windowOptions = { + width: 52, + height: 52, + transparent: true, + frame: false, + resizable: false, + skipTaskbar: true, + alwaysOnTop: true, + focusable: false, + movable: true, + webPreferences: { + preload: preloadPath, + devTools: utools.isDev(), + }, + }; + + // 如果指定了位置,添加到选项中 + if (position) { + windowOptions.x = position.x; + windowOptions.y = position.y; + } + + // 创建窗口 + const UBrowser = createBrowserWindow(pinPath, windowOptions, () => { + const windowId = UBrowser.webContents.id; + UBrowser.webContents.openDevTools({ + mode: "undocked", + }); + + // 监听命令执行请求 + ipcRenderer.once(`pin-execute-${windowId}`, (event, code) => { + // 执行命令 + console.log("execute command", event, code, commandInfo); + }); + + // 保存窗口信息 + pinWindows.set(commandInfo.features.code, { + window: UBrowser, + windowId, + position: UBrowser.getPosition(), + }); + + // 发送配置到子窗口 + ipcRenderer.sendTo(windowId, "window-config", { + isDark: utools.isDarkColors(), + icon: commandInfo.features.icon, + commandCode: commandInfo.features.code, + windowId, + }); + + resolve(UBrowser); + }); + }); +}; + +/** + * 移除pin窗口 + * @param {string} commandCode - 命令代码 + */ +const removePinWindow = (commandCode) => { + const pinInfo = pinWindows.get(commandCode); + if (pinInfo) { + pinInfo.window.destroy(); + pinWindows.delete(commandCode); + } +}; + +/** + * 获取所有pin窗口信息 + * @returns {Map} pin窗口信息Map + */ +const getPinWindows = () => { + return pinWindows; +}; + +/** + * 恢复所有pin窗口 + * @param {Array} pinnedCommands - pin命令列表 + */ +const restorePinWindows = async (pinnedCommands) => { + for (const command of pinnedCommands) { + await createPinWindow(command.info, command.position); + } +}; + +module.exports = { + createPinWindow, + removePinWindow, + getPinWindows, + restorePinWindows, +}; diff --git a/plugin/lib/pin/style.css b/plugin/lib/pin/style.css new file mode 100644 index 0000000..f880bb4 --- /dev/null +++ b/plugin/lib/pin/style.css @@ -0,0 +1,40 @@ +body { + margin: 0; + padding: 0; + background: transparent; + overflow: hidden; + user-select: none; +} + +.pin-container { + width: 52px; + height: 52px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: transform 0.3s ease; +} + +.pin-container:hover { + transform: scale(1.1); +} + +.pin-icon { + width: 45px; + height: 45px; + overflow: hidden; +} + +.pin-icon img { + width: 100%; + height: 100%; + object-fit: contain; +} + +body[data-theme="dark"] .pin-icon { + background: rgba(57, 57, 57, 0.09); + border: 1px solid rgb(59 58 58 / 5%); + box-shadow: 0 1px 5px rgb(0 0 0 / 20%), 0 2px 2px rgb(0 0 0 / 14%), + 0 3px 1px -2px rgb(69 67 67 / 12%); +} diff --git a/plugin/lib/pin/view.html b/plugin/lib/pin/view.html new file mode 100644 index 0000000..9226f7f --- /dev/null +++ b/plugin/lib/pin/view.html @@ -0,0 +1,15 @@ + + + + + Pin + + + +
+
+ command icon +
+
+ + diff --git a/plugin/preload.js b/plugin/preload.js index d99ca42..ec8000c 100644 --- a/plugin/preload.js +++ b/plugin/preload.js @@ -20,6 +20,7 @@ const md5 = (input) => { window.lodashM = require("./lib/lodashMini"); window.pinyinMatch = require("pinyin-match"); +window.pinService = require("./lib/pin/service"); window.DOMPurify = DOMPurify; const createTerminalCommand = require("./lib/createTerminalCommand"); diff --git a/src/App.vue b/src/App.vue index 6359b9e..102619a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -122,6 +122,10 @@ export default defineComponent({ window.quickcommandHttpServer().run(this.nativeProfile.serverPort); console.log("Server Start..."); } + // 恢复固定到桌面的命令 + if (this.nativeProfile.pinnedCommands) { + window.pinService.restorePinWindows(this.nativeProfile.pinnedCommands); + } }, enterPlugin(enter) { this.updateExp(); diff --git a/src/components/card/CommandCardContent.vue b/src/components/card/CommandCardContent.vue index 52e51e9..04e6d86 100644 --- a/src/components/card/CommandCardContent.vue +++ b/src/components/card/CommandCardContent.vue @@ -4,6 +4,20 @@ v-ripple :class="{ [`text-${disabledColor}`]: !isActivated, command: 1 }" > + + + {{ isPinned ? "取消固定到桌面" : "固定到桌面" }} + + + cmd.info.features.code === this.commandInfo.features.code + ) || false + ); + }, + }, + methods: { + async togglePin() { + if (this.isPinned) { + window.pinService.removePinWindow(this.commandInfo.features.code); + this.$root.nativeProfile.pinnedCommands = + this.$root.nativeProfile.pinnedCommands.filter( + (cmd) => cmd.info.features.code !== this.commandInfo.features.code + ); + } else { + await window.pinService.createPinWindow(this.commandInfo); + if (!this.$root.nativeProfile.pinnedCommands) { + this.$root.nativeProfile.pinnedCommands = []; + } + this.$root.nativeProfile.pinnedCommands.push({ + info: this.commandInfo, + position: utools.getCursorScreenPoint(), + }); + } + }, }, }; @@ -80,4 +121,28 @@ export default { box-shadow: 0 1px 5px rgb(0 0 0 / 20%), 0 2px 2px rgb(0 0 0 / 14%), 0 3px 1px -2px rgb(69 67 67 / 12%); } + +.pin-icon { + opacity: 0; + transform: translateY(-5px); + visibility: hidden; + transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); + will-change: transform, opacity, visibility; +} + +.q-card:hover .pin-icon { + opacity: 1; + transform: translateY(0); + visibility: visible; + backdrop-filter: blur(1px); +} + +.pin-icon .q-btn { + transition: transform 0.35s cubic-bezier(0.68, -0.6, 0.32, 1.6); + will-change: transform; +} + +.pin-icon .q-btn:hover { + transform: scale(1.15); +}