Compare commits

...

23 Commits

Author SHA1 Message Date
muwoo
3b4ca0e289 Update LESS loader options in vue.config.js 2025-11-27 18:15:37 +08:00
muwoo
b75188fdb8 Update Yarn command to install Vue CLI 2025-11-27 18:06:42 +08:00
muwoo
bf3ae7c9ba Update main.yml 2025-11-27 17:53:32 +08:00
muwoo
b7b16e2f3e Update main.yml 2025-11-27 17:47:04 +08:00
muwoo
3fec3665b6 Update Vue CLI version in GitHub Actions workflow 2025-11-27 17:42:16 +08:00
muwoo
b7587a454f Merge pull request #477 from lanxiuyun/support-copy-file
windows 支持复制文件到剪贴板
2025-11-27 17:34:48 +08:00
lanxiuyun
f5f3f030ce 添加注释 2025-11-27 17:32:14 +08:00
lanxiuyun
0bddb33fde 优化可读性 2025-11-27 16:29:53 +08:00
lanxiuyun
9005d070aa windows复制文件到剪切板 2025-11-27 16:01:58 +08:00
lanxiuyun
9adfa84cab 简化复制代码 2025-11-27 15:32:42 +08:00
lanxiuyun
15c160e45d 支持复制文件 2025-11-27 14:47:47 +08:00
muwoo
6bf613042a Update Vue CLI version in workflow 2025-11-20 21:48:34 +08:00
muwoo
04fe2e03a6 Pin Vue CLI version to 5.0.8 in workflow 2025-11-20 21:43:41 +08:00
muwoo
6c0d34fc4f Update package.json 2025-11-19 20:21:55 +08:00
muwoo
3fa9bb0384 Merge pull request #472 from 25juan/feature/volta
fix:修复当使用 volta 管理 node 版本的时候安装插件失败
2025-11-19 20:21:12 +08:00
muwoo
791115901a Merge pull request #473 from lanxiuyun/fix-detach
Fix: detach bug
2025-11-19 20:20:36 +08:00
lanxiuyun
a879ed6555 fix-detach 2025-11-11 13:56:52 +08:00
sgellar
28b58e7976 fix:修复当使用 volta 管理 node 版本的时候安装插件失败 2025-11-11 12:20:57 +08:00
muwoo
dc54b25f84 Merge pull request #464 from lanxiuyun/fix-feature-npm-i
fix: feature中的npm i
2025-09-16 10:21:04 +08:00
lanxiuyun
fc51a383bf 更新 less-loader 版本 2025-09-13 15:41:28 +08:00
muwoo
a546bc0d59 Update package.json 2025-07-25 18:40:09 +08:00
muwoo
c732e448c3 Merge pull request #449 from siriusol/master
fix #448
2025-07-25 18:39:43 +08:00
Ther
fbc7da0606 fix #448 2025-07-25 18:24:52 +08:00
9 changed files with 222 additions and 21 deletions

View File

@@ -52,7 +52,7 @@ jobs:
run: |
yarn
yarn global add xvfb-maybe
yarn global add @vue/cli
yarn global add @vue/cli@4.5.0 --frozen-lockfile --ignore-engines
- name: Build feature
run: |
cd ./feature

View File

@@ -42,7 +42,7 @@
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.0.0",
"less": "^4.1.3",
"less-loader": "5.0.0",
"less-loader": "^6.2.0",
"prettier": "^2.2.1",
"typescript": "~4.1.5"
},

View File

@@ -7,7 +7,9 @@ module.exports = {
// 向预处理器 Loader 传递配置选项
less: {
// 配置less其他样式解析用法一致
javascriptEnabled: true, // 设置为true
lessOptions: {
javascriptEnabled: true, // 设置为true
},
},
},
},

View File

@@ -1,6 +1,6 @@
{
"name": "rubick",
"version": "4.3.4",
"version": "4.3.7",
"author": "muwoo <2424880409@qq.com>",
"private": true,
"scripts": {

View File

@@ -36,7 +36,14 @@ class AdapterHandler {
fs.mkdirsSync(options.baseDir);
fs.writeFileSync(
`${options.baseDir}/package.json`,
'{"dependencies":{}}'
// '{"dependencies":{}}'
// fix 插件安装时node版本问题
JSON.stringify({
dependencies: {},
volta: {
node: '16.19.1',
},
})
);
}
this.baseDir = options.baseDir;

View File

@@ -82,7 +82,7 @@ export default () => {
};
const init = (plugin, window: BrowserWindow) => {
if (view === null || view === undefined) {
if (view === null || view === undefined || view.inDetach) {
createView(plugin, window);
// if (viewInstance.getView(plugin.name) && !commonConst.dev()) {
// view = viewInstance.getView(plugin.name).view;
@@ -176,14 +176,29 @@ export default () => {
const removeView = (window: BrowserWindow) => {
if (!view) return;
executeHooks('PluginOut', null);
// 先记住这次要移除的视图,防止后面异步代码里全局引用被换掉
const snapshotView = view;
setTimeout(() => {
window.removeBrowserView(view);
if (!view.inDetach) {
window.setBrowserView(null);
view.webContents?.destroy();
// 获取当前视图,判断是否已经换成了新视图
const currentView = window.getBrowserView?.();
window.removeBrowserView(snapshotView);
// 主窗口的插件视图仍然挂着旧实例时,需要还原主窗口 UI
if (!snapshotView.inDetach) {
// 如果窗口还挂着旧视图,说明还没换掉,需要把主窗口恢复到初始状态
if (currentView === snapshotView) {
window.setBrowserView(null);
if (view === snapshotView) {
window.webContents?.executeJavaScript(`window.initRubick()`);
view = undefined;
}
}
snapshotView.webContents?.destroy();
}
// 分离窗口只需释放全局引用,视图由分离窗口继续管理
else if (view === snapshotView) {
view = undefined;
}
window.webContents?.executeJavaScript(`window.initRubick()`);
view = undefined;
}, 0);
};

View File

@@ -27,6 +27,30 @@ import DBInstance from './db';
import getWinPosition from './getWinPosition';
import path from 'path';
import commonConst from '@/common/utils/commonConst';
import { copyFilesToWindowsClipboard } from './windowsClipboard';
/**
* sanitize input files 剪贴板文件合法性校验
* @param input
* @returns
*/
const sanitizeInputFiles = (input: unknown): string[] => {
const candidates = Array.isArray(input)
? input
: typeof input === 'string'
? [input]
: [];
return candidates
.map((filePath) => (typeof filePath === 'string' ? filePath.trim() : ''))
.filter((filePath) => {
if (!filePath) return false;
try {
return fs.existsSync(filePath);
} catch {
return false;
}
});
};
const runnerInstance = runner();
const detachInstance = detach();
@@ -230,13 +254,28 @@ class API extends DBInstance {
}
public copyFile({ data }) {
if (data.file && fs.existsSync(data.file)) {
clipboard.writeBuffer(
'NSFilenamesPboardType',
Buffer.from(plist.build([data.file]))
);
return true;
const targetFiles = sanitizeInputFiles(data?.file);
if (!targetFiles.length) {
return false;
}
if (process.platform === 'darwin') {
try {
clipboard.writeBuffer(
'NSFilenamesPboardType',
Buffer.from(plist.build(targetFiles))
);
return true;
} catch {
return false;
}
}
if (process.platform === 'win32') {
return copyFilesToWindowsClipboard(targetFiles);
}
return false;
}

View File

@@ -110,9 +110,14 @@ const registerHotKey = (mainWindow: BrowserWindow): void => {
// mainWindow.show();
});
globalShortcut.register('CommandOrControl+W', () => {
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.isFocused()) {
mainWindow.hide();
// 添加局部快捷键监听
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.key.toLowerCase() === 'w'
&& (input.control || input.meta) && !input.alt && !input.shift) {
event.preventDefault();
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.hide();
}
}
});

View File

@@ -0,0 +1,133 @@
import { clipboard } from 'electron';
import path from 'path';
// 仅在 Windows 平台辅助操作剪贴板多文件格式。
type ClipboardExModule = typeof import('electron-clipboard-ex');
const DROPFILES_HEADER_SIZE = 20;
let clipboardExModule: ClipboardExModule | null = null;
/**
* Windows 平台专用:尝试加载第三方库 electron-clipboard-ex。
* 这个库能够调用系统底层接口写入“文件复制”数据,成功率更高。
* 其他系统无需加载它,因此这里做了“按需加载”的处理。
*/
const ensureClipboardEx = (): ClipboardExModule | null => {
if (process.platform !== 'win32') return null;
if (clipboardExModule) return clipboardExModule;
try {
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
clipboardExModule = require('electron-clipboard-ex');
} catch {
clipboardExModule = null;
}
return clipboardExModule;
};
/**
* 把一组文件路径变成 Windows 规定的文本格式。
* 要求:每个路径之间用单个空字符分隔,最后再额外放两个空字符,表示列表结束。
* Windows 资源管理器会按这个格式解析我们复制到剪贴板的文件。
*/
const buildWindowsFileListPayload = (files: string[]): Buffer =>
Buffer.from(`${files.join('\0')}\0\0`, 'utf16le');
/**
* 构造 CF_HDROP 专用的二进制数据。
* 这是 Windows 复制文件时的底层格式,前 20 字节是固定的结构头,
* 后面紧跟着具体的文件路径(由 buildWindowsFileListPayload 生成)。
* 只要把这个内容写入剪贴板,任何支持粘贴文件的程序都能理解。
*/
const buildWindowsFileDropBuffer = (files: string[]): Buffer => {
const payload = buildWindowsFileListPayload(files);
const header = Buffer.alloc(DROPFILES_HEADER_SIZE);
header.writeUInt32LE(DROPFILES_HEADER_SIZE, 0);
header.writeInt32LE(0, 4);
header.writeInt32LE(0, 8);
header.writeUInt32LE(0, 12);
header.writeUInt32LE(1, 16);
const result = Buffer.alloc(header.length + payload.length);
for (let i = 0; i < header.length; i += 1) {
result[i] = header[i];
}
for (let i = 0; i < payload.length; i += 1) {
result[header.length + i] = payload[i];
}
return result;
};
/**
* 复制/移动/创建快捷方式 等不同操作在 Windows 中对应不同的“意图”值。
* Preferred DropEffect 告诉系统:当前剪贴板数据应该以何种方式处理。
* 我们默认写入“copy”相当于普通的复制粘贴。
*/
const buildDropEffectBuffer = (effect: 'copy' | 'move' | 'link' = 'copy') => {
const effectMap = {
copy: 1,
move: 2,
link: 4,
} as const;
const buffer = Buffer.alloc(4);
buffer.writeUInt32LE(effectMap[effect], 0);
return buffer;
};
/**
* 直接使用 Electron 内置 API 写入多种剪贴板格式。
* 步骤:
* 1. 写入二进制的 CF_HDROP含头部与路径列表
* 2. 写入纯文本形式的 FileNameW备选格式
* 3. 写入 Preferred DropEffect告诉系统“这是复制”
* 全部成功后,读取一次 CF_HDROP 的长度,确认剪贴板里确实有内容。
*/
const writeWindowsBuffers = (files: string[]): boolean => {
try {
clipboard.writeBuffer('CF_HDROP', buildWindowsFileDropBuffer(files));
clipboard.writeBuffer('FileNameW', buildWindowsFileListPayload(files));
clipboard.writeBuffer('Preferred DropEffect', buildDropEffectBuffer('copy'));
return clipboard.readBuffer('CF_HDROP').length > 0;
} catch {
return false;
}
};
/**
* 如果项目中安装了 electron-clipboard-ex我们优先使用它。
* 理由:该库通过原生方式与系统交互,兼容性往往优于 Electron 的 JS 层写入。
* 调用成功后,必要时读回文件列表做一次数量校验,确保复制的文件数量正确。
*/
const writeWithClipboardEx = (files: string[]): boolean => {
const clipboardEx = ensureClipboardEx();
if (!clipboardEx) return false;
try {
clipboardEx.writeFilePaths(files);
if (typeof clipboardEx.readFilePaths === 'function') {
const result = clipboardEx.readFilePaths();
return Array.isArray(result) && result.length === files.length;
}
return true;
} catch {
return false;
}
};
/**
* 对外暴露的唯一入口。
* 1. 先把所有路径换成 Windows 可识别的标准形式path.normalize
* 2. 尝试使用 electron-clipboard-ex 写入,如果成功就结束。
* 3. 若第三方库不可用或失败,再退回 Electron 原生写入流程。
* 这一层屏蔽了所有细节,外部调用者只需传入字符串数组即可。
*/
export const copyFilesToWindowsClipboard = (files: string[]): boolean => {
const normalizedFiles = files
.map((filePath) => path.normalize(filePath))
.filter(Boolean);
if (!normalizedFiles.length) return false;
if (writeWithClipboardEx(normalizedFiles)) {
return true;
}
return writeWindowsBuffers(normalizedFiles);
};