From 34cd7d9a8b072e60e9c575288d1ba0b403501f7d Mon Sep 17 00:00:00 2001 From: wangzihao Date: Mon, 31 Aug 2026 16:45:26 +0800 Subject: [PATCH 1/7] =?UTF-8?q?fix(paste):=20=E4=BF=AE=E5=A4=8D=E8=B7=A8?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E5=BF=AB=E6=8D=B7=E9=94=AE=E4=B8=8E=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E9=94=AE=E7=9B=98=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI-Co-Authored-By: Codex --- .../pasteboard-pro/apps/ztools/src/App.vue | 116 +++++++++++++++--- .../apps/ztools/src/components/PasteCard.vue | 30 ++++- .../ztools/src/components/PinboardStrip.vue | 11 +- .../apps/ztools/src/components/SearchBar.vue | 13 +- .../apps/ztools/src/components/Toolbar.vue | 20 ++- .../apps/ztools/src/keyboard-routing.ts | 20 +++ .../apps/ztools/src/list-control-key.ts | 5 +- .../apps/ztools/src/platform-shortcuts.ts | 76 ++++++++++++ .../pasteboard-pro/apps/ztools/src/state.ts | 28 +++-- 9 files changed, 283 insertions(+), 36 deletions(-) create mode 100644 plugins/pasteboard-pro/apps/ztools/src/keyboard-routing.ts create mode 100644 plugins/pasteboard-pro/apps/ztools/src/platform-shortcuts.ts diff --git a/plugins/pasteboard-pro/apps/ztools/src/App.vue b/plugins/pasteboard-pro/apps/ztools/src/App.vue index 1eaf7edf8..83ad8da00 100644 --- a/plugins/pasteboard-pro/apps/ztools/src/App.vue +++ b/plugins/pasteboard-pro/apps/ztools/src/App.vue @@ -34,6 +34,12 @@ import { } from "./state"; import { queryAfterTypeToSearch } from "./type-to-search"; import { isListNavigationKey, shouldResumeListControl } from "./list-control-key"; +import { shelfKeyboardRoute } from "./keyboard-routing"; +import { + hasAnyCommandModifier, + matchesPrimaryShortcut, + resolveShortcutPlatform, +} from "./platform-shortcuts"; import { themeCssVariables } from "./theme"; import { applyListOrder, @@ -57,6 +63,13 @@ const panelMode = ? panel : undefined; const isShelfMode = params.get("shelf") === "1"; +const hostCompatibility = window.pasteboardPro?.getHostCompatibility?.(); +const unsupportedHost = hostCompatibility?.supported === false; +const supportsScreenshot = + window.pasteboardPro?.getPlatformCapabilities().supportsScreenCapture ?? false; +const shortcutPlatform = resolveShortcutPlatform( + window.pasteboardPro?.getPlatformCapabilities().platform, +); const dockValue = params.get("dock"); const edge: DockEdge = dockValue === "top" || @@ -70,6 +83,7 @@ const state = reactive( createPasteboardState({ items: [], dockEdge: edge, + shortcutPlatform, }), ); const pinboards = ref([]); @@ -207,6 +221,23 @@ async function pasteItem( } } +async function captureScreenshot(): Promise { + status.value = "请选择截图区域"; + try { + const result = await window.pasteboardPro?.captureScreenshot(); + if (result === undefined) { + status.value = "已取消截图"; + return; + } + const bounds = result.bounds; + status.value = bounds === undefined + ? "截图已加入剪贴板" + : `截图已加入剪贴板(${Math.round(bounds.width)} × ${Math.round(bounds.height)})`; + } catch (error) { + status.value = error instanceof Error ? error.message : "截图失败"; + } +} + async function pasteItems( itemIds: readonly string[], plainText = false, @@ -378,12 +409,12 @@ function onKeydown(event: KeyboardEvent): void { } if (!isShelfMode) return; if (settingsOpen.value) return; - if (event.metaKey && event.key.toLowerCase() === "f") { + if (matchesPrimaryShortcut(event, shortcutPlatform, "f")) { event.preventDefault(); document.querySelector("[data-pb-search]")?.focus(); return; } - if (event.metaKey && !event.shiftKey && event.key.toLowerCase() === "n") { + if (matchesPrimaryShortcut(event, shortcutPlatform, "n")) { event.preventDefault(); createTextItem(); return; @@ -420,33 +451,50 @@ function onKeydown(event: KeyboardEvent): void { return; } } - if (event.metaKey && !event.shiftKey && event.key.toLowerCase() === "c") { + if (matchesPrimaryShortcut(event, shortcutPlatform, "c")) { event.preventDefault(); void copySelection(); return; } - if (event.metaKey && !event.shiftKey && event.key.toLowerCase() === "t") { + if (matchesPrimaryShortcut(event, shortcutPlatform, "t")) { event.preventDefault(); void togglePause(); return; } - if (event.metaKey && !event.shiftKey && event.key.toLowerCase() === "e" && focusedItem.value !== undefined) { + if (matchesPrimaryShortcut(event, shortcutPlatform, "e") && focusedItem.value !== undefined) { event.preventDefault(); editItem(focusedItem.value.id); return; } - if (event.metaKey && !event.shiftKey && event.key.toLowerCase() === "r" && focusedItem.value !== undefined) { + if (matchesPrimaryShortcut(event, shortcutPlatform, "r") && focusedItem.value !== undefined) { event.preventDefault(); renameItem(focusedItem.value.id); return; } - const isSpaceActivation = - event.key === " " && - event.target instanceof Element && - event.target.closest( - "button, a[href], summary, [role='button'], [role='menuitem']", - ) !== null; - const nextQuery = isSpaceActivation + const previousSelection = state.selection.selected.join("\0"); + const keyboardTarget = event.target instanceof Element ? event.target : undefined; + const listCardTarget = keyboardTarget?.closest("[data-pb-item-id]"); + const keyboardRoute = shelfKeyboardRoute(event.key, { + nativeControl: + keyboardTarget !== undefined && + keyboardTarget.closest( + "button, a[href], summary, [role='button'], [role='menuitem']", + ) !== null, + listCard: listCardTarget !== undefined && listCardTarget !== null, + }); + if (keyboardRoute === "native-control") return; + if ( + keyboardRoute === "list-card" && + !hasAnyCommandModifier(event) && + listCardTarget !== undefined && + listCardTarget !== null + ) { + const itemId = listCardTarget.dataset.pbItemId; + if (itemId !== undefined && !state.selection.selected.includes(itemId)) { + state.replaceSelection(itemId); + } + } + const nextQuery = keyboardRoute === "list-card" ? undefined : queryAfterTypeToSearch(query.value, event); if (nextQuery !== undefined) { @@ -459,11 +507,11 @@ function onKeydown(event: KeyboardEvent): void { ); return; } - const previousSelection = state.selection.selected.join("\0"); const effect = state.handleKeyboard( { key: event.key, metaKey: event.metaKey, + ctrlKey: event.ctrlKey, shiftKey: event.shiftKey, altKey: event.altKey, }, @@ -915,8 +963,17 @@ onBeforeUnmount(() => { 'stage--image-background': hasImageBackground, }" > + { :paste-stack-count="state.pasteStack.itemIds.length" :paste-stack-direction="state.pasteStack.direction" :reorder-enabled="reorderEnabled" + :supports-screenshot="supportsScreenshot" @update:query="updateQuery" @select="selectItem" @paste="pasteItem" @@ -948,12 +1006,13 @@ onBeforeUnmount(() => { @clear-stack="updatePasteStack({ type: 'clear' })" @open-privacy-settings="openPrivacySettings" @create-text="createTextItem" + @capture-screenshot="captureScreenshot" @edit-item="editItem" @rename-item="renameItem" @reorder="reorderVisibleItems" /> { @history-cleared="onHistoryCleared" /> { @save="saveEditor" /> { @edit="editItem" @rename="renameItem" /> -

{{ status }}

+

{{ status }}

@@ -1009,6 +1068,25 @@ onBeforeUnmount(() => { background-size: cover; } +.host-upgrade-notice { + align-self: center; + justify-self: center; + display: grid; + max-width: 460px; + gap: 10px; + margin: 24px; + padding: 28px; + border: 1px solid var(--pb-line); + border-radius: 20px; + background: var(--pb-glass-strong); + box-shadow: 0 20px 48px var(--pb-shadow); + text-align: center; +} + +.host-upgrade-notice > span { color: var(--pb-violet); font-size: 30px; font-weight: 850; } +.host-upgrade-notice h1 { margin: 0; color: var(--pb-ink); font-size: 20px; } +.host-upgrade-notice p { margin: 0; color: var(--pb-muted); font-size: 13px; line-height: 1.7; } + .stage--panel { position: relative; height: 100%; diff --git a/plugins/pasteboard-pro/apps/ztools/src/components/PasteCard.vue b/plugins/pasteboard-pro/apps/ztools/src/components/PasteCard.vue index edd977f33..e0375ee20 100644 --- a/plugins/pasteboard-pro/apps/ztools/src/components/PasteCard.vue +++ b/plugins/pasteboard-pro/apps/ztools/src/components/PasteCard.vue @@ -10,6 +10,7 @@ import { import { containContextMenuKeydown } from "../context-menu-keyboard"; import { writeSourceDragData } from "../drag-content"; import { LIST_REORDER_MIME } from "../list-order"; +import { hasPrimaryShortcutModifier, resolveShortcutPlatform } from "../platform-shortcuts"; const props = defineProps<{ item: PasteItem; @@ -38,6 +39,9 @@ const card = ref(); const thumbnailUrl = ref(); const thumbnailRequested = ref(false); const reorderDragging = ref(false); +const shortcutPlatform = resolveShortcutPlatform( + window.pasteboardPro?.getPlatformCapabilities().platform, +); const contextMenu = ref<{ x: number; y: number }>(); let stopObservingThumbnail: (() => void) | undefined; let reorderDragPreview: HTMLElement | undefined; @@ -71,6 +75,15 @@ function openContextMenu(event: MouseEvent): void { window.addEventListener("keydown", closeContextMenuOnEscape); } +function selectCard(event: MouseEvent): void { + emit( + "select", + props.item.id, + event.shiftKey, + hasPrimaryShortcutModifier(event, shortcutPlatform), + ); +} + function assignToPinboard(pinboardId: string | undefined): void { emit("assignPinboard", { pinboardId, itemId: props.item.id }); closeContextMenu(); @@ -235,9 +248,16 @@ function beginNativeFileDrag(event: DragEvent): void { // The nested element has a browser-native drag behavior that exposes // its thumbnail URL. Always cancel that default payload; the host API below // supplies the original image/file as a native file drag instead. - window.pasteboardPro?.startNativeFileDrag(props.item.id); - event.preventDefault(); - event.stopPropagation(); + const started = window.pasteboardPro?.startNativeFileDrag(props.item.id) === true; + if (started) { + event.preventDefault(); + event.stopPropagation(); + return; + } + // ZTools 2.4-3.1 do not provide startDrag. Leave the browser drag alive so + // the card never becomes a silent no-op; copy/paste and Quick Look remain + // the supported file actions on those hosts. + if (event.dataTransfer !== null) writeSourceDragData(props.item, event.dataTransfer); } const bodyText = computed(() => { @@ -292,13 +312,11 @@ onBeforeUnmount(() => { role="option" tabindex="0" :draggable="reorderEnabled !== false" - @click="emit('select', item.id, $event.shiftKey, $event.metaKey)" + @click="selectCard" @dblclick="emit('paste', item.id)" @contextmenu.prevent.stop="openContextMenu" @dragstart="beginReorderDrag" @dragend="finishReorderDrag" - @keydown.enter="emit('paste', item.id)" - @keydown.space.prevent="emit('preview', item.id)" >
{{ item.kind.replace('_', ' ') }} diff --git a/plugins/pasteboard-pro/apps/ztools/src/components/PinboardStrip.vue b/plugins/pasteboard-pro/apps/ztools/src/components/PinboardStrip.vue index 8ffd22462..8e75cb4fb 100644 --- a/plugins/pasteboard-pro/apps/ztools/src/components/PinboardStrip.vue +++ b/plugins/pasteboard-pro/apps/ztools/src/components/PinboardStrip.vue @@ -2,6 +2,8 @@ import { computed, onBeforeUnmount, ref } from "vue"; import type { Pinboard } from "@pasteboard-pro/core"; + +import { primaryModifierLabel, resolveShortcutPlatform } from "../platform-shortcuts"; import type { SmartPinboard } from "../smart-pinboards"; import { containContextMenuKeydown } from "../context-menu-keyboard"; @@ -20,6 +22,13 @@ const emit = defineEmits<{ assign: [pinboardId: string | undefined, itemId: string]; }>(); +const quickPasteShortcutLabel = computed(() => { + const platform = resolveShortcutPlatform( + window.pasteboardPro?.getPlatformCapabilities().platform, + ); + return `${primaryModifierLabel(platform)} 1–9 快捷粘贴`; +}); + const creating = ref(false); const editingId = ref(); const draft = ref(""); @@ -204,7 +213,7 @@ onBeforeUnmount(closeManagementMenu); @blur="commitCreate" /> - ⌘ 1–9 快捷粘贴 + {{ quickPasteShortcutLabel }}
+import { computed } from "vue"; + +import { primaryModifierLabel, resolveShortcutPlatform } from "../platform-shortcuts"; + defineProps<{ modelValue: string }>(); const emit = defineEmits<{ "update:modelValue": [value: string] }>(); + +const searchShortcutLabel = computed(() => { + const platform = resolveShortcutPlatform( + window.pasteboardPro?.getPlatformCapabilities().platform, + ); + return `${primaryModifierLabel(platform)} F`; +}); diff --git a/plugins/pasteboard-pro/apps/ztools/src/components/Toolbar.vue b/plugins/pasteboard-pro/apps/ztools/src/components/Toolbar.vue index a7c03f4ab..a3e34f440 100644 --- a/plugins/pasteboard-pro/apps/ztools/src/components/Toolbar.vue +++ b/plugins/pasteboard-pro/apps/ztools/src/components/Toolbar.vue @@ -1,6 +1,8 @@ @@ -1068,25 +1036,6 @@ onBeforeUnmount(() => { background-size: cover; } -.host-upgrade-notice { - align-self: center; - justify-self: center; - display: grid; - max-width: 460px; - gap: 10px; - margin: 24px; - padding: 28px; - border: 1px solid var(--pb-line); - border-radius: 20px; - background: var(--pb-glass-strong); - box-shadow: 0 20px 48px var(--pb-shadow); - text-align: center; -} - -.host-upgrade-notice > span { color: var(--pb-violet); font-size: 30px; font-weight: 850; } -.host-upgrade-notice h1 { margin: 0; color: var(--pb-ink); font-size: 20px; } -.host-upgrade-notice p { margin: 0; color: var(--pb-muted); font-size: 13px; line-height: 1.7; } - .stage--panel { position: relative; height: 100%; diff --git a/plugins/pasteboard-pro/apps/ztools/src/components/PasteCard.vue b/plugins/pasteboard-pro/apps/ztools/src/components/PasteCard.vue index e0375ee20..a4eb12c72 100644 --- a/plugins/pasteboard-pro/apps/ztools/src/components/PasteCard.vue +++ b/plugins/pasteboard-pro/apps/ztools/src/components/PasteCard.vue @@ -248,16 +248,9 @@ function beginNativeFileDrag(event: DragEvent): void { // The nested element has a browser-native drag behavior that exposes // its thumbnail URL. Always cancel that default payload; the host API below // supplies the original image/file as a native file drag instead. - const started = window.pasteboardPro?.startNativeFileDrag(props.item.id) === true; - if (started) { - event.preventDefault(); - event.stopPropagation(); - return; - } - // ZTools 2.4-3.1 do not provide startDrag. Leave the browser drag alive so - // the card never becomes a silent no-op; copy/paste and Quick Look remain - // the supported file actions on those hosts. - if (event.dataTransfer !== null) writeSourceDragData(props.item, event.dataTransfer); + window.pasteboardPro?.startNativeFileDrag(props.item.id); + event.preventDefault(); + event.stopPropagation(); } const bodyText = computed(() => { diff --git a/plugins/pasteboard-pro/apps/ztools/src/components/Toolbar.vue b/plugins/pasteboard-pro/apps/ztools/src/components/Toolbar.vue index a3e34f440..b9675bc57 100644 --- a/plugins/pasteboard-pro/apps/ztools/src/components/Toolbar.vue +++ b/plugins/pasteboard-pro/apps/ztools/src/components/Toolbar.vue @@ -10,7 +10,6 @@ defineProps<{ paused: boolean; compact: boolean; edge: DockEdge; - supportsScreenshot: boolean; }>(); const emit = defineEmits<{ @@ -19,7 +18,6 @@ const emit = defineEmits<{ toggleCompact: []; openPrivacySettings: []; createText: []; - captureScreenshot: []; }>(); const createTextShortcutTitle = computed(() => { @@ -39,13 +37,6 @@ const createTextShortcutTitle = computed(() => {
- +