Skip to content

Add plugin 截图置顶 v0.1.0#248

Open
Timqaq wants to merge 4 commits into
ZToolsCenter:mainfrom
Timqaq:plugin/top-screenshot
Open

Add plugin 截图置顶 v0.1.0#248
Timqaq wants to merge 4 commits into
ZToolsCenter:mainfrom
Timqaq:plugin/top-screenshot

Conversation

@Timqaq

@Timqaq Timqaq commented Jun 8, 2026

Copy link
Copy Markdown

插件信息

  • 名称: 截图置顶
  • 插件ID: top-screenshot
  • 版本: 0.1.0
  • 描述: 框选屏幕区域并原地置顶显示截图
  • 作者: N/A
  • 类型: 新增

本次变更

  • docs: add screenshot pin design and implementation plan
  • chore: scaffold ztools screenshot plugin
  • test: add screenshot geometry primitives
  • test: add screenshot route and storage state
  • test: add typed ztools bridge
  • feat: add capture overlay and pin windows
  • feat: launch screenshot capture from ztools entry
  • fix: harden screenshot pin lifecycle
  • chore: package ztools plugin for installation
  • fix: stabilize screenshot pin packaging and dragging
  • fix: ignore nested worktrees in test discovery
  • 添加README

截图 / 演示

自检清单

  • plugin.json 的 name / title / version / description / author 字段均已检查
  • 已移除调试日志、未使用文件、敏感信息(.env、token、密钥等)
  • 本次 PR 的 diff 仅涉及 plugins/top-screenshot/ 目录
  • 已在本地 ZTools 客户端实际加载并测试过此插件,主要功能正常
  • 同意以仓库声明的开源协议发布此插件

此 PR 由 ztools-plugin-cli 自动管理:每次 ztools publish 在分支上追加一个 commit,PR 链接保持不变。

- docs: add screenshot pin design and implementation plan
- chore: scaffold ztools screenshot plugin
- test: add screenshot geometry primitives
- test: add screenshot route and storage state
- test: add typed ztools bridge
- feat: add capture overlay and pin windows
- feat: launch screenshot capture from ztools entry
- fix: harden screenshot pin lifecycle
- chore: package ztools plugin for installation
- fix: stabilize screenshot pin packaging and dragging
- fix: ignore nested worktrees in test discovery
- 添加README

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new ZTools plugin, '截图置顶' (top-screenshot), built with Vue 3, TypeScript, and Vite, which allows users to capture screen regions and display them as always-on-top, draggable, and zoomable windows. Feedback on the implementation highlights a critical bug in CaptureView.vue where the screenshot image (<img>) is missing from the template, rendering the overlay transparent and breaking the capture flow; this also requires updating the corresponding test in captureView.test.ts. Additionally, improvements are suggested for PinView.vue to bind drag events to the window rather than the main element for smoother dragging, and to properly clean up the beforeunload event listener in onBeforeUnmount to prevent potential memory leaks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +115 to +127
<main class="capture-view" tabindex="0" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp">
<div
v-if="selection"
class="selection-box"
:style="{
left: `${selection.x}px`,
top: `${selection.y}px`,
width: `${selection.width}px`,
height: `${selection.height}px`,
}"
/>
<p v-if="!snapshot" class="capture-error">没有找到当前显示器截图。</p>
</main>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

CaptureView.vue 的模板中,缺少了用于渲染屏幕截图的 <img> 标签。这会导致截图覆盖层打开时完全透明,用户无法看到自己正在框选和裁剪的画面,从而无法进行正确的截图操作。建议将 snapshot.imageDataUrl 渲染为背景图片,以恢复截图视角。

  <main class="capture-view" tabindex="0" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp">
    <img v-if="snapshot" class="capture-image" :src="snapshot.imageDataUrl" alt="屏幕截图" draggable="false" />
    <div
      v-if="selection"
      class="selection-box"
      :style="{
        left: `${selection.x}px`,
        top: `${selection.y}px`,
        width: `${selection.width}px`,
        height: `${selection.height}px`,
      }"
    />
    <p v-if="!snapshot" class="capture-error">没有找到当前显示器截图。</p>
  </main>

Comment on lines +87 to +118
function onMouseDown(event: MouseEvent): void {
if (!pinState.value || event.button !== 0) {
return;
}

dragStart.value = { x: event.screenX, y: event.screenY };
dragStartBounds.value = pinState.value.currentBounds;
activate();
}

function onMouseMove(event: MouseEvent): void {
if (!pinState.value || !dragStart.value || !dragStartBounds.value) {
return;
}

const deltaX = event.screenX - dragStart.value.x;
const deltaY = event.screenY - dragStart.value.y;
const nextImageBounds = translateRect(dragStartBounds.value, deltaX, deltaY);
const nextState = {
...pinState.value,
currentBounds: nextImageBounds,
lastActiveAt: Date.now(),
};

persist(nextState);
applyWindowBounds(nextImageBounds);
}

function onMouseUp(): void {
dragStart.value = null;
dragStartBounds.value = null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

PinView.vue 中,拖拽事件监听器(mousemovemouseup)是直接绑定在 main 元素上的。由于置顶窗口会被缩放和调整大小,当用户快速拖拽鼠标时,鼠标指针很容易离开窗口范围,导致拖拽事件停止触发、窗口卡住。建议在 mousedown 时将这些事件动态绑定到 window 上,并在 mouseup 时移除,以提供更平滑、稳定的拖拽体验。

function onMouseDown(event: MouseEvent): void {
  if (!pinState.value || event.button !== 0) {
    return;
  }

  dragStart.value = { x: event.screenX, y: event.screenY };
  dragStartBounds.value = pinState.value.currentBounds;
  activate();
  window.addEventListener('mousemove', onMouseMove);
  window.addEventListener('mouseup', onMouseUp);
}

function onMouseMove(event: MouseEvent): void {
  if (!pinState.value || !dragStart.value || !dragStartBounds.value) {
    return;
  }

  const deltaX = event.screenX - dragStart.value.x;
  const deltaY = event.screenY - dragStart.value.y;
  const nextImageBounds = translateRect(dragStartBounds.value, deltaX, deltaY);
  const nextState = {
    ...pinState.value,
    currentBounds: nextImageBounds,
    lastActiveAt: Date.now(),
  };

  persist(nextState);
  applyWindowBounds(nextImageBounds);
}

function onMouseUp(): void {
  dragStart.value = null;
  dragStartBounds.value = null;
  window.removeEventListener('mousemove', onMouseMove);
  window.removeEventListener('mouseup', onMouseUp);
}

Comment on lines +25 to +45
it('does not render the captured screen image behind the selection overlay', async () => {
const session: CaptureSession = {
id: 'session-1',
createdAt: 1780898400000,
completed: false,
displays: [
{
displayId: 'display-1',
bounds: { x: 100, y: 80, width: 800, height: 600 },
imageDataUrl: 'data:image/png;base64,screen',
scaleFactor: 1,
},
],
};
window.localStorage.setItem('capture-session:session-1', JSON.stringify(session));

const { default: CaptureView } = await import('../src/views/CaptureView.vue');
const wrapper = mount(CaptureView);

expect(wrapper.find('.capture-image').exists()).toBe(false);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

由于 CaptureView.vue 中漏掉了 <img> 标签,测试用例 does not render the captured screen image behind the selection overlay 错误地断言了 .capture-image 不存在。为了保证截图功能的正确性,应该渲染该图片,并在此测试中将其断言修改为存在。

  it('renders the captured screen image behind the selection overlay', async () => {
    const session: CaptureSession = {
      id: 'session-1',
      createdAt: 1780898400000,
      completed: false,
      displays: [
        {
          displayId: 'display-1',
          bounds: { x: 100, y: 80, width: 800, height: 600 },
          imageDataUrl: 'data:image/png;base64,screen',
          scaleFactor: 1,
        },
      ],
    };
    window.localStorage.setItem('capture-session:session-1', JSON.stringify(session));

    const { default: CaptureView } = await import('../src/views/CaptureView.vue');
    const wrapper = mount(CaptureView);

    expect(wrapper.find('.capture-image').exists()).toBe(true);
    expect(wrapper.find('.capture-image').attributes('src')).toBe('data:image/png;base64,screen');
  });

Comment on lines +141 to +149
<main
v-if="pinState"
class="pin-window"
tabindex="0"
@mousedown="onMouseDown"
@mousemove="onMouseMove"
@mouseup="onMouseUp"
@wheel="onWheel"
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

配合将拖拽事件监听器移至 window 的修改,需要从模板的 main 元素中移除 @mousemove@mouseup 属性绑定。

  <main
    v-if="pinState"
    class="pin-window"
    tabindex="0"
    @mousedown="onMouseDown"
    @wheel="onWheel"
  >

Comment on lines +131 to +138
window.addEventListener('keydown', onKeyDown);
window.addEventListener('blur', onMouseUp);
window.addEventListener('beforeunload', () => removePinWindow(window.localStorage, pinId));
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('blur', onMouseUp);
});
</script>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

PinView.vue 中,beforeunload 事件监听器使用了匿名箭头函数绑定到 window 上,但在 onBeforeUnmount 中没有进行清理。这会导致潜在的内存泄漏。此外,如果应用了将拖拽事件绑定到 window 的优化,也应该在组件卸载时一并清理 mousemovemouseup 监听器。建议将 beforeunload 提取为命名函数,并在 onBeforeUnmount 中进行完整的清理。

const handleBeforeUnload = () => {
  removePinWindow(window.localStorage, pinId);
};

window.addEventListener('keydown', onKeyDown);
window.addEventListener('blur', onMouseUp);
window.addEventListener('beforeunload', handleBeforeUnload);
onBeforeUnmount(() => {
  window.removeEventListener('keydown', onKeyDown);
  window.removeEventListener('blur', onMouseUp);
  window.removeEventListener('beforeunload', handleBeforeUnload);
  window.removeEventListener('mousemove', onMouseMove);
  window.removeEventListener('mouseup', onMouseUp);
});

- docs: add screenshot pin design and implementation plan
- chore: scaffold ztools screenshot plugin
- test: add screenshot geometry primitives
- test: add screenshot route and storage state
- test: add typed ztools bridge
- feat: add capture overlay and pin windows
- feat: launch screenshot capture from ztools entry
- fix: harden screenshot pin lifecycle
- chore: package ztools plugin for installation
- fix: stabilize screenshot pin packaging and dragging
- fix: ignore nested worktrees in test discovery
- 添加README
- 添加插件作者字段
@Timqaq

Timqaq commented Jun 8, 2026

Copy link
Copy Markdown
Author
20260608160857_rec_ 已验证功能正常

@Timqaq Timqaq marked this pull request as ready for review June 8, 2026 08:30
Timqaq added 2 commits June 8, 2026 16:39
- docs: add screenshot pin design and implementation plan
- chore: scaffold ztools screenshot plugin
- test: add screenshot geometry primitives
- test: add screenshot route and storage state
- test: add typed ztools bridge
- feat: add capture overlay and pin windows
- feat: launch screenshot capture from ztools entry
- fix: harden screenshot pin lifecycle
- chore: package ztools plugin for installation
- fix: stabilize screenshot pin packaging and dragging
- fix: ignore nested worktrees in test discovery
- 添加README
- 添加插件作者字段
- 修改插件命令描述
- docs: add screenshot pin design and implementation plan
- chore: scaffold ztools screenshot plugin
- test: add screenshot geometry primitives
- test: add screenshot route and storage state
- test: add typed ztools bridge
- feat: add capture overlay and pin windows
- feat: launch screenshot capture from ztools entry
- fix: harden screenshot pin lifecycle
- chore: package ztools plugin for installation
- fix: stabilize screenshot pin packaging and dragging
- fix: ignore nested worktrees in test discovery
- 添加README
- 添加插件作者字段
- 修改插件命令描述
- 在无截图被置顶的时候主动关闭进程节省资源 修复bug
@lzx8589561

Copy link
Copy Markdown
Contributor

感谢pr,经测试执行build之后dist内构建不完整,需要复制plugin.json preload.js icon 等到dist目录,需要dist压缩为zip拖动到ztools可以安装才行

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants