diff --git a/app_clipboard.go b/app_clipboard.go new file mode 100644 index 0000000..76273c3 --- /dev/null +++ b/app_clipboard.go @@ -0,0 +1,71 @@ +//go:build windows + +package main + +import ( + "runtime" + "syscall" + "time" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + procIsClipboardFormatAvailable = windows.NewLazySystemDLL("user32.dll").NewProc("IsClipboardFormatAvailable") + procOpenClipboard = windows.NewLazySystemDLL("user32.dll").NewProc("OpenClipboard") + procCloseClipboard = windows.NewLazySystemDLL("user32.dll").NewProc("CloseClipboard") + procGetClipboardData = windows.NewLazySystemDLL("user32.dll").NewProc("GetClipboardData") + procDragQueryFileW = windows.NewLazySystemDLL("shell32.dll").NewProc("DragQueryFileW") +) + +// cfHDrop 剪贴板文件列表格式(资源管理器复制文件/文件夹),Win32 稳定 ABI 值 +const cfHDrop = 15 + +// ReadClipboardFiles 读取系统剪贴板中的文件/文件夹路径列表。 +// 剪贴板为文本/截图等非文件内容时返回空列表,前端据此回落图片粘贴等原有链路。 +func (a *App) ReadClipboardFiles() ([]string, error) { + // OpenClipboard 与 CloseClipboard 必须同线程,否则剪贴板死锁 + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + if r, _, _ := procIsClipboardFormatAvailable.Call(cfHDrop); r == 0 { + return nil, nil + } + + // 剪贴板是全局互斥资源,短重试避开其他进程的瞬时占用 + opened := false + for i := 0; i < 20; i++ { + if r, _, _ := procOpenClipboard.Call(0); r != 0 { + opened = true + break + } + time.Sleep(10 * time.Millisecond) + } + if !opened { + return nil, syscall.EBUSY + } + defer procCloseClipboard.Call() + + h, _, _ := procGetClipboardData.Call(cfHDrop) + if h == 0 { + return nil, nil + } + + count, _, _ := procDragQueryFileW.Call(h, 0xFFFFFFFF, 0, 0) + if count == 0 { + return nil, nil + } + + paths := make([]string, 0, count) + for i := uintptr(0); i < count; i++ { + n, _, _ := procDragQueryFileW.Call(h, i, 0, 0) + if n == 0 { + continue + } + buf := make([]uint16, n+1) + procDragQueryFileW.Call(h, i, uintptr(unsafe.Pointer(&buf[0])), n+1) + paths = append(paths, syscall.UTF16ToString(buf)) + } + return paths, nil +} diff --git a/frontend/bindings/u-desk/app.ts b/frontend/bindings/u-desk/app.ts index d85f291..e2c1693 100644 --- a/frontend/bindings/u-desk/app.ts +++ b/frontend/bindings/u-desk/app.ts @@ -491,6 +491,16 @@ export function PickDirectory(): $CancellablePromise { return $Call.ByID(1347829059); } +/** + * ReadClipboardFiles 读取系统剪贴板中的文件/文件夹路径列表。 + * 剪贴板为文本/截图等非文件内容时返回空列表,前端据此回落图片粘贴等原有链路。 + */ +export function ReadClipboardFiles(): $CancellablePromise { + return $Call.ByID(2636780132).then(($result: any) => { + return $$createType7($result); + }); +} + /** * ReadFile 读取文件 */ @@ -797,3 +807,4 @@ const $$createType3 = filesystem$0.FileOperationResult.createFrom; const $$createType4 = $Create.Nullable($$createType3); const $$createType5 = $Create.Array($$createType2); const $$createType6 = $Create.Map($Create.Any, $Create.Any); +const $$createType7 = $Create.Array($Create.Any); diff --git a/frontend/src/components/FileSystem/index.vue b/frontend/src/components/FileSystem/index.vue index c123314..2bb9c59 100644 --- a/frontend/src/components/FileSystem/index.vue +++ b/frontend/src/components/FileSystem/index.vue @@ -129,6 +129,7 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue' import { getPathSeparator } from '@/utils/fileUtils' import { Message, Modal } from '@arco-design/web-vue' import { On, Off } from '@wailsio/events' +import { ReadClipboardFiles } from '@bindings/u-desk/app' import { renderMarkdown, parseMarkdown, isMarkedReady, preloadMarked, renderMermaidDiagrams, rerenderMermaidDiagrams, setCurrentFileDir, setFileServerBase } from '@/utils/markedExtensions' import { useThemeStore } from '@/stores/theme' import { useTransferStore } from '@/stores/transfer' @@ -2023,6 +2024,17 @@ const handlePaste = async (event: ClipboardEvent) => { const target = event.target as HTMLElement if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return + // 系统剪贴板文件(资源管理器复制,文件+文件夹)优先: + // 后端读 CF_HDROP 拿真实路径(DOM paste 拿不到文件夹),复用拖入导入链路 + try { + const sysPaths = await ReadClipboardFiles() + if (sysPaths && sysPaths.length > 0) { + event.preventDefault() + await handleExternalDrop(sysPaths) + return + } + } catch { /* 剪贴板被占用等,回落图片粘贴 */ } + const items = event.clipboardData?.items if (!items) return