feat: 无限画布滚轮缩放 + 项目管理入口 (0.0.26)

画布交互:
- 滚轮缩放视口(2%~1600%),纯滚轮 10%/tick,Ctrl/Cmd+滚轮 3%/tick 精细控制
- 鼠标拖拽平移画布,Ctrl/Cmd+0 重置缩放
- 预览打开时锁定底层视口(previewActiveRef 事件穿透防护)
- 预览内图片独立滚轮缩放 + 拖拽平移 + 缩放指示器

文件拖拽:
- 画布缩略图/预览支持原生文件拖拽到外部应用(startDrag IPC)

Bug 修复:
- local-media:// 协议支持 UNC 网络路径
- passive event listener 报错:window 级原生 wheel listener 替代 React onWheel
- Zustand v5 getState() 批处理旧值:改用 ref(currentZoomRef + setZoomRef)读写

主进程(electron/main.ts):
- 画布窗口 before-input-event 拦截 Chromium 页面缩放
- 新增 canvas-ipc.ts(startDrag、文件扫描等 IPC handler)
This commit is contained in:
2026-06-23 20:11:50 +08:00
parent dd8f84ae27
commit 4ea414c9c7
38 changed files with 5193 additions and 17 deletions

View File

@@ -21,6 +21,18 @@ declare namespace NodeJS {
}
}
/** sharp 图片处理库(可选依赖,运行时动态加载) */
declare module 'sharp' {
interface SharpInstance {
metadata(): Promise<{ width?: number; height?: number; format?: string }>;
resize(width?: number, height?: number, options?: { fit?: string; withoutEnlargement?: boolean }): SharpInstance;
jpeg(options?: { quality?: number }): SharpInstance;
toBuffer(): Promise<Buffer>;
}
function sharp(input: string | Buffer): SharpInstance;
export = sharp;
}
// Used in Renderer process, expose in `preload.ts`
declare interface Window {
ipcRenderer: import('electron').IpcRenderer;
@@ -34,4 +46,41 @@ declare interface Window {
/** 登录/登出后更新窗口标题中的版控后缀 */
setWindowEdition(edition: string | null): void;
};
/** 无限画布 API由 preload 注入) */
canvas: {
/** 打开画布窗口 */
openWindow(projectPath?: string): void;
/** 扫描目录 */
scanDirectory(params: {
roots: string[];
extensions: string[];
recursive: boolean;
}): Promise<{ success: boolean; data: Array<{ path: string; fileName: string; ext: string; fileSize: number; modifiedAt: string }>; error?: string }>;
/** 读取 sidecar 元数据 */
readSidecar(params: { filePath: string }): Promise<{ success: boolean; data: object | null }>;
/** 写入 sidecar 元数据 */
writeSidecar(params: { filePath: string; metadata: object }): Promise<{ success: boolean; error?: string }>;
/** 生成缩略图 */
generateThumbnail(params: { filePath: string; size: number }): Promise<{
success: boolean;
data: { data: string; mimeType: string; width: number; height: number };
error?: string;
}>;
/** 扫描项目 */
scanProject(params: { projectPath: string; stage: string; assetTypeFilter: string; shotQuery: string }): Promise<{ success: boolean; data: Array<{ path: string; fileName: string; ext: string; fileSize: number; modifiedAt: string }>; error?: string }>;
/** 复制文件 */
copyFile(params: { src: string; destDir: string; fileName: string }): Promise<{ success: boolean; data: string; error?: string }>;
/** 列出目录下的子文件夹(非递归,排除隐藏) */
listSubdirs(params: { parentPath: string }): Promise<{ success: boolean; data: string[]; error?: string }>;
/** 扫描阶段文件(单个 episode/shot/stage 组合) */
scanStageFiles(params: { projectPath: string; episode: string; shot: string; stage: string }): Promise<{ success: boolean; data: Array<{ path: string; fileName: string; ext: string; fileSize: number; modifiedAt: string }>; error?: string }>;
/** 扫描资产文件(深度递归扫描 assets/ 目录) */
scanAssetFiles(params: { projectPath: string; assetTypeFilter: string }): Promise<{ success: boolean; data: Array<{ path: string; fileName: string; ext: string; fileSize: number; modifiedAt: string }>; error?: string }>;
/** 创建目录 */
createDirectory(params: { parentPath: string; dirName: string }): Promise<{ success: boolean; data: string; error?: string }>;
/** 将本地文件路径转换为 local-media:// 协议 URL */
getMediaUrl(filePath: string): string;
/** 启动原生文件拖拽(从缩略图拖放媒体文件到外部应用) */
startDrag(params: { filePath: string; fileName: string }): Promise<{ success: boolean; error?: string }>;
};
}