electron-multi-app-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +611 -0
  3. package/dist/core/context.d.ts +15 -0
  4. package/dist/core/eventBus.d.ts +2 -0
  5. package/dist/core/lifecycle.d.ts +9 -0
  6. package/dist/create-app.d.ts +30 -0
  7. package/dist/index-CtN4cHF_.mjs +4 -0
  8. package/dist/index.d.ts +28 -0
  9. package/dist/index.js +1 -0
  10. package/dist/modules/autoLaunch.d.ts +47 -0
  11. package/dist/modules/batchDownloader.d.ts +89 -0
  12. package/dist/modules/downloader.d.ts +38 -0
  13. package/dist/modules/downloader.optimized.d.ts +45 -0
  14. package/dist/modules/instance.d.ts +8 -0
  15. package/dist/modules/ipc/builtins/dialog.d.ts +6 -0
  16. package/dist/modules/ipc/builtins/downloader.d.ts +6 -0
  17. package/dist/modules/ipc/builtins/fs-api.d.ts +4 -0
  18. package/dist/modules/ipc/builtins/notification.d.ts +5 -0
  19. package/dist/modules/ipc/builtins/shell.d.ts +5 -0
  20. package/dist/modules/ipc/builtins/window-control.d.ts +5 -0
  21. package/dist/modules/ipc/builtins/window-move.d.ts +7 -0
  22. package/dist/modules/ipc/index.d.ts +15 -0
  23. package/dist/modules/session.d.ts +6 -0
  24. package/dist/modules/tray.d.ts +7 -0
  25. package/dist/modules/updater.d.ts +5 -0
  26. package/dist/modules/window/manager.d.ts +103 -0
  27. package/dist/modules/window/useFloatWin.d.ts +6 -0
  28. package/dist/modules/window/useWinDrag.d.ts +8 -0
  29. package/dist/preload/base.d.ts +34 -0
  30. package/dist/preload/events.d.ts +6 -0
  31. package/dist/preload/index.d.ts +1 -0
  32. package/dist/preload/index.js +1 -0
  33. package/dist/renderer/composable/useIpcRenderer.d.ts +28 -0
  34. package/dist/renderer/composable/useWindowMove.d.ts +23 -0
  35. package/dist/renderer/index.d.ts +4 -0
  36. package/dist/renderer/index.js +1 -0
  37. package/dist/renderer/style.css +1 -0
  38. package/dist/types.d.ts +127 -0
  39. package/dist/utils/clone.d.ts +4 -0
  40. package/dist/utils/debounce.d.ts +4 -0
  41. package/dist/utils/merge.d.ts +7 -0
  42. package/package.json +62 -0
@@ -0,0 +1,28 @@
1
+ export { createElectronApp } from './create-app';
2
+ export type { ElectronCliConfig, ElectronCliPaths, WindowDefinition, IpcHandlerDefinition, IpcOptions, BuiltinIpcModule, LifecycleHooks, SessionConfig, TrayConfig, InstanceConfig, UpdaterConfig, } from './types';
3
+ export { AppEvent } from './core/eventBus';
4
+ export { WindowManager } from './modules/window/manager';
5
+ export type { WindowManagerOptions, WindowBaseType } from './modules/window/manager';
6
+ export { DownloadManager } from './modules/downloader';
7
+ export type { DownloadTask, DownloadState } from './modules/downloader';
8
+ export { BatchDownloadManager } from './modules/batchDownloader';
9
+ export type { BatchDownloadState, BatchFileDownloadState, BatchDownloadFileInput, BatchDownloadConfig, BatchFileDownloadTask, BatchDownloadTask } from './modules/batchDownloader';
10
+ export { useIpcEvents } from './modules/ipc/index';
11
+ export type { IpcContext } from './modules/ipc/index';
12
+ export { useSession } from './modules/session';
13
+ export { useSingleInstance } from './modules/instance';
14
+ export { useTray } from './modules/tray';
15
+ export { useUpdater } from './modules/updater';
16
+ export { useLifeCycle } from './core/lifecycle';
17
+ export { createContext } from './core/context';
18
+ export type { ElectronCliContext } from './core/context';
19
+ export { useWinDrag } from './modules/window/useWinDrag';
20
+ export { useFloatWin } from './modules/window/useFloatWin';
21
+ export { AutoLaunch } from './modules/autoLaunch';
22
+ export { useWindowMove } from './modules/ipc/builtins/window-move';
23
+ export { useWindowControl } from './modules/ipc/builtins/window-control';
24
+ export { useFsApi } from './modules/ipc/builtins/fs-api';
25
+ export { useDialogApi } from './modules/ipc/builtins/dialog';
26
+ export { useShellApi } from './modules/ipc/builtins/shell';
27
+ export { useDownloaderApi } from './modules/ipc/builtins/downloader';
28
+ export { useNotificationApi } from './modules/ipc/builtins/notification';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{ipcMain as e,BrowserWindow as t,app as s,dialog as n,session as i,nativeImage as o,Tray as a,Menu as r,protocol as d,screen as l,shell as c,Notification as h,net as u}from"electron";import p from"node:os";import w from"node:path";import{autoUpdater as g}from"electron-updater";import m from"electron-log";import f from"node:fs";import y from"mime-types";import v from"node:crypto";import b from"events";function B(e,t,s={}){const n={...e},{deepMerge:i=!1}=s;for(const o of Object.keys(t)){const e=t[o],a=n[o];!i||null===e||"object"!=typeof e||Array.isArray(e)||null===a||"object"!=typeof a||Array.isArray(a)?void 0!==e&&(n[o]=e):n[o]=B(a,e,s)}return n}class W{windows=/* @__PURE__ */new Map;windowsConfig=/* @__PURE__ */new Map;definitions=/* @__PURE__ */new Map;mainWindowType;paths;lifecycle;defaultOptions;_defaultWindowConfig={webPreferences:{contextIsolation:!0,nodeIntegration:!1,backgroundThrottling:!1}};confirmQuit=!0;constructor(e){this.mainWindowType=e.mainWindowType,this.paths=e.paths,this.lifecycle=e.lifecycle,this.defaultOptions=B(this._defaultWindowConfig,e.defaultOptions??{});for(const t of e.windows)this.definitions.set(t.type,t),this.windows.set(t.type,null)}create(t){const s=t??this.mainWindowType,n=this.definitions.get(s);if(!n)throw new Error(`[electron-multi-app-kit] Unknown window type: "${s}". Did you define it in config.windows?`);const i=this.mergeConfig(n),o=this.createBrowserWindow(i);if(this.windows.set(s,o),this.windowsConfig.set(o.id,i),o.webContents.windowType=s,this.applyCommonSettings(o),o.webContents.on("did-finish-load",()=>{o.webContents.send("main-process-message",/* @__PURE__ */(new Date).toLocaleString())}),o.on("closed",()=>{this.windows.set(s,null)}),!i.webPreferences?.nodeIntegration){const e=this.paths.userAgent;e&&o.webContents.setUserAgent(e)}return n.onCreate?.(o),this.lifecycle?.onCreateWindow?.(o,s),o.once("ready-to-show",()=>{n.onReady?.(o)}),e.once("renderer-ready",()=>{n.onRendererReady?.(o)}),o}openMainWindow(){return this.openWindow(this.mainWindowType)}openWindow(e){const t=e??this.mainWindowType;let s=this.getWindow(t);if(s){let e=s.isVisible();e||s.show(),s.isMinimized()?s.restore():!s.isMaximized()&&s.maximizable&&s.isVisible()&&e&&s.maximize(),s.focus()}else s=this.create(t);return s}openWindowPage(e="",t={},s){return new Promise(n=>{const i=B(this.defaultOptions,t,{deepMerge:!0});i.webPreferences=i.webPreferences||{},i.webPreferences.webviewTag=!0;const o=this.createBrowserWindow(i);this.windowsConfig.set(o.id,i),this.loadHashRoute(o,e,t).then(()=>n());const a=t.parent;a?.webContents&&(o.once("ready-to-show",()=>{t.modal&&a.webContents.executeJavaScript("document.body.parentElement.classList.add('MODAL-OPENED')")}),o.once("closed",()=>{t.modal&&a.webContents.executeJavaScript("document.body.parentElement.classList.remove('MODAL-OPENED')")})),s?.(o)})}openUrl(e,t={}){const s=B(this.defaultOptions,t,{deepMerge:!0});s.webPreferences=s.webPreferences||{},s.webPreferences.webviewTag=!0;const n=this.createBrowserWindow(s);return this.windowsConfig.set(n.id,s),this.loadHashRoute(n,e,t),n.once("ready-to-show",()=>{n.show()}),n}getWindow(e){return this.windows.get(e??this.mainWindowType)??null}getMain(){return this.getWindow(this.mainWindowType)??this.create(this.mainWindowType)}getWindowByWebContents(e){return t.fromWebContents(e)}getWindowById(e){return t.fromId(e)}getAllWindows(){return t.getAllWindows()}getWebContents(e){const t=this.getWindow(e);return t?.webContents??null}getWindowType(e){for(const[t,s]of this.windows)if(s===e)return t;return null}getWindowConfig(e){return e instanceof t?this.windowsConfig.get(e.id)||null:this.mergeConfig(e)}closeWindow(e,t=!0){t||(this.confirmQuit=!1);const s=this.getWindow(e);s&&(s.close(),this.windows.set(e,null))}closeAllWindows(e=!0){for(const t of this.getAllWindows())e||(this.confirmQuit=!1),t.close();this.windows.clear()}appQuit(){this.confirmQuit=!1,s.quit()}mergeConfig(e){return B(this.defaultOptions,e.options,{deepMerge:!0})}createBrowserWindow(e){const s=new t(e);return this.applyWindowSettings(s,e),this.applyCommonSettings(s),s.webContents.on("did-finish-load",()=>{s.webContents.send("main-process-message",/* @__PURE__ */(new Date).toLocaleString())}),this.lifecycle?.onCreateWindow?.(s,null),s}applyWindowSettings(e,t){e.webContents.setWindowOpenHandler(e=>this.windowOpenHandler(e)),e.webContents.on("did-attach-webview",(e,t)=>{t.setWindowOpenHandler(e=>this.windowOpenHandler(e))}),e.setMenuBarVisibility(!1),t&&t.show&&e.once("ready-to-show",()=>{e.show()}),e.on("enter-full-screen",()=>e.webContents.send("fullscreen-change",!0)),e.on("leave-full-screen",()=>e.webContents.send("fullscreen-change",!1)),e.on("maximize",()=>e.webContents.send("maximize",!1)),e.on("unmaximize",()=>e.webContents.send("unmaximize",!1)),e.on("minimize",()=>e.webContents.send("minimize")),e.on("restore",()=>e.webContents.send("restore")),e.on("always-on-top-changed",(t,s)=>e.webContents.send("always-on-top-changed",s))}windowOpenHandler(e){let t=this.parseWindowFeatures(e.features);return e.url.startsWith("https:")||e.url.startsWith("http:")?(this.openUrl(`webview/index?url=${encodeURIComponent(e.url)}`,t),{action:"deny"}):e.url.startsWith("local://")?(this.openUrl(e.url,t),{action:"deny"}):{action:"deny"}}applyCommonSettings(e){}async loadHashRoute(e,t,s){return this.lifecycle?.onLoadHashRoute?await this.lifecycle.onLoadHashRoute(e,t,s):Promise.resolve()}parseWindowFeatures(e){const t={};if("string"!=typeof e||!e.trim())return t;return e.split(",").forEach(e=>{const s=e.indexOf("=");if(-1!==s){const n=e.substring(0,s).trim();let i=e.substring(s+1).trim();"true"===i||"yes"===i||"1"===i?i=!0:"false"===i||"no"===i||"0"===i?i=!1:isNaN(i)||""===i||(i=Number(i)),t[n]=i}else t[e.trim()]=!0}),t}isMainWindow(e){return this.getWindowType(e)===this.mainWindowType}confirmExit(e){n.showMessageBox(e,{type:"question",buttons:["隐藏至托盘","取消","退出"],title:"提示",message:"确定要退出吗?",noLink:!0,cancelId:1}).then(t=>{switch(t.response){case 0:e.hide();break;case 2:this.confirmQuit=!1,s.quit()}})}}function k(e){return{config:e,windowManager:new W({windows:e.windows,mainWindowType:e.mainWindowType,paths:e.paths,defaultOptions:e.defaultWindowOptions,lifecycle:e.lifecycle}),trayCreated:!1,isDev:e.isDev||"development"===process.env.NODE_ENV}}function P(e){i.defaultSession.webRequest.onBeforeSendHeaders((t,s)=>{e?.onBeforeSendHeaders?e.onBeforeSendHeaders(t,s):s({requestHeaders:t.requestHeaders})})}function I(e,t){if(!t?.enabled)return;const{trayIconPath:n,iconPath:i}=e.config.paths;let d,l;"string"!=n&&n?l=n:(d="string"==n?n:i,l=o.createFromPath(d));const c=new a(l);t.tooltip&&c.setToolTip(t.tooltip);let h="function"==typeof t.menuItems?t.menuItems():t.menuItems;const u=r.buildFromTemplate(h??[{label:"Home",click:()=>e.windowManager.openMainWindow()},{label:"Quit",click:()=>{e.windowManager.confirmQuit=!1,s.quit()}}]);c.setContextMenu(u),c.on("double-click",()=>{t.onDoubleClick?t.onDoubleClick():e.windowManager.openMainWindow()}),e.trayCreated=!0,t.setup?.(c,u,e)}function C(e){e?.enabled&&(g.logger=m,g.autoDownload=!1,g.autoInstallOnAppQuit=!1,e.feedUrl&&g.setFeedURL({provider:"generic",url:e.feedUrl}),e.channel&&(g.channel=e.channel),g.on("update-available",t=>{e.onUpdateAvailable&&e.onUpdateAvailable({version:t.version,releaseNotes:"string"==typeof t.releaseNotes?t.releaseNotes:Array.isArray(t.releaseNotes)?t.releaseNotes.map(e=>e.note??e).join("\n"):void 0})}),g.on("error",t=>{e.onUpdateError&&e.onUpdateError(t)}),g.on("update-downloaded",()=>{e.onUpdateDownloaded?e.onUpdateDownloaded():n.showMessageBox({type:"info",title:"更新已下载",message:"新版本已下载完成,是否立即重启应用?",buttons:["立即重启","稍后"]}).then(e=>{0===e.response&&g.quitAndInstall()})}),g.checkForUpdates().catch(e=>{m.error("[electron-multi-app-kit] updater check error:",e)}))}function S(e){const n=e.config;d.registerSchemesAsPrivileged([{scheme:"local",privileges:{standard:!0,secure:!0,supportFetchAPI:!0,corsEnabled:!0,allowServiceWorkers:!0}}]),s.on("window-all-closed",()=>{n.lifecycle?.onAllWindowsClosed?n.lifecycle.onAllWindowsClosed():"darwin"!==process.platform&&s.quit()}),s.on("activate",()=>{if(n.lifecycle?.onActivate)n.lifecycle.onActivate();else{const s=t.getAllWindows();0===s.length?e.windowManager.create(n.mainWindowType):s[0].focus()}}),s.whenReady().then(async()=>{if(d.registerFileProtocol("local",(e,t)=>{const s=new URL(e.url);let i=decodeURIComponent(s.pathname);"win32"===process.platform&&i.startsWith("/")&&(i=i.slice(1));const o=w.join(n.paths.rendererDist,i);t({path:w.normalize(o)})}),n.lifecycle?.onReady&&n.lifecycle.onReady(),P(n.session),e.windowManager.create(n.mainWindowType),n.tray?.enabled&&I(e,n.tray),n.updater?.enabled){const t=e.windowManager.getMain();t&&t.webContents.on("did-finish-load",()=>{C(n.updater)})}n.lifecycle?.afterReady&&await n.lifecycle.afterReady()})}function T(t){let s=0,n=0,i={width:0,height:0};function o(e){const o=t.getWindowByWebContents(e.sender);if(!o)return;const[a,r]=o.getPosition();s=a,n=r,i=o.getBounds()}e.on("window-move-start",o),e.on("window-move-end",o),e.on("window-move",(e,{x:o,y:a})=>{const r=t.getWindowByWebContents(e.sender);r&&r.setBounds({...A(s+o,n+a,r),width:i.width,height:i.height})})}function A(e,t,s){const{width:n,height:i}=l.getPrimaryDisplay().workArea,o=s.getSize(),a=n-o[0],r=i-o[1];return{x:Math.max(Math.min(e,a),0),y:Math.max(Math.min(t,r),0)}}const x=["toggleMaximize","toggleAlwaysOnTop","$props"];function M(t){e.handle("window-control",(e,s,...n)=>{const i=t.getWindowByWebContents(e.sender);if(!i)return;const o=s in i,a=x.includes(s);if(!o&&!a)throw new Error(`BrowserWindowInstance.${s} is not exist`);switch(s){case"minimize":i.minimize();break;case"maximize":i.maximize();break;case"toggleMaximize":i.isMaximized()?i.restore():i.maximize();break;case"restore":i.restore();break;case"close":i.close();break;case"hide":i.hide();break;case"toggleAlwaysOnTop":i.setAlwaysOnTop(!i.isAlwaysOnTop(),...n.slice(1));break;case"setAlwaysOnTop":i.setAlwaysOnTop(!!n[0],...n.slice(1));break;case"isAlwaysOnTop":return i.isAlwaysOnTop();case"isFullScreen":return i.isFullScreen();case"isMaximized":return i.isMaximized();case"isVisible":return i.isVisible();case"minimizable":return i.minimizable;case"maximizable":return i.maximizable;case"closable":return i.closable;case"resizable":return i.resizable;case"$props":return{minimizable:i.minimizable,maximizable:i.maximizable,closable:i.closable,resizable:i.resizable,isAlwaysOnTop:i.isAlwaysOnTop(),isFullScreen:i.isFullScreen(),isMaximized:i.isMaximized()};default:return`WindowManager.${s} is not allowed to use`}})}const D=["readFileAsBase64"];function U(){e.handle("fs-api",(e,t,...s)=>{const n=t in f,i=D.includes(t);if(!n&&!i)throw new Error(`fs.${t} is not exist`);if("readFileAsBase64"===t)return new Promise((e,t)=>{f.readFile(s[0],{encoding:"base64"},(n,i)=>{n&&t(n);const o=y.lookup(s[0])||"application/octet-stream";e(`data:${o};base64,${i}`)})});const o=f[t];return"function"==typeof o?o(...s):""})}function F(t){e.handle("open-file-dialog",async(e,s,i)=>{const o=i?t.getWindowByWebContents(e.sender):null;return(await n.showOpenDialog(o,s)).filePaths}),e.handle("open-file-dialog-sync",(e,s,i)=>{const o=i?t.getWindowByWebContents(e.sender):null;return n.showOpenDialogSync(o,s)}),e.handle("show-message-box",(e,s,i)=>{const o=i?t.getWindowByWebContents(e.sender):null;return n.showMessageBox(o,s)})}function z(){e.handle("open-file",(e,t)=>c.openPath(t)),e.handle("open-external",(e,t)=>c.openExternal(t)),e.handle("show-file",(e,t)=>{if(c.showItemInFolder(t),!f.existsSync(t))throw new Error("File not found")})}function O(t,i){e.handle("download-file",(e,o,a={})=>{const r=t.getWindowByWebContents(e.sender),d=n.showSaveDialogSync(r,{defaultPath:i.getUniqueFilename(w.join(a.savePath||s.getPath("downloads"),a.fileName||w.basename(o)))});if(d){const t=i.addTask(o,{fileName:w.basename(d),savePath:d});i.startTask(t,e.sender)}}),e.handle("download-controler",(e,t)=>{switch(t.action){case"pause":i.pauseTask(t.taskId);break;case"resume":i.resumeTask(t.taskId);break;case"cancel":i.cancelTask(t.taskId);break;case"remove":i.removeTask(t.taskId)}}),e.handle("get-download-tasks",()=>i.getAllTasks())}function R(t){e.on("show-notification",(e,s)=>{new h({icon:o.createFromPath(t.iconPath),...s}).show()})}function N(t,s={}){const n=new Set(s.disabledBuiltins??[]);n.has("window-move")||T(t.windowManager),n.has("window-control")||M(t.windowManager),n.has("fs-api")||U(),n.has("dialog")||F(t.windowManager),n.has("shell")||z(),n.has("downloader")||O(t.windowManager,t.downloader),n.has("notification")||R(t.paths),function(t){const{windowManager:s,paths:n}=t;e.handle("get-preload",async()=>n.preloadScript),e.handle("get-renderer-dist",async()=>n.rendererDist),e.handle("get-main-dist",async()=>n.mainDist),e.handle("open-win",(e,t)=>{s.openUrl(t)}),e.handle("open-main-window",()=>{s.openMainWindow()}),e.on("set-process",(e,t)=>{const n=s.getWindowByWebContents(e.sender);try{n?.setProgressBar(t)}catch(i){console.error(i)}}),e.handle("read-file",(e,...t)=>import("fs").then(e=>e.promises.readFile(...t))),e.handle("open-window-page",(e,t,n)=>{const i=s.getWindowByWebContents(e.sender);return s.openWindowPage(t.url,{parent:i,modal:!!n,...t.windowOptions})}),e.on("send-to-parent",(e,t,...n)=>{const i=s.getWindowByWebContents(e.sender)?.getParentWindow();i&&i.webContents.send(t,...n)}),e.on("send-to-child",(e,t,...n)=>{const i=s.getWindowByWebContents(e.sender),o=i?.getChildWindows();o&&o.forEach(e=>{e.webContents.send(t,...n)})}),e.on("send-to-renderer",(e,t,...n)=>{s.getAllWindows().forEach(s=>{s.webContents!==e.sender&&s.webContents.send(t,...n)})}),e.on("set-ignore-mouse-events",(e,t,n)=>{const i=s.getWindowByWebContents(e.sender);i?.setIgnoreMouseEvents(t,n)}),e.handle("window-manager-api",(e,t,...n)=>{if(!(t in s))throw new Error(`WindowManager.${t} is not exist`);const i=s.getWindowByWebContents(e.sender),o=i?s.getWindowType(i):null;switch(t){case"openWindow":s.openWindow(...n);break;case"closeWindow":s.closeWindow(n[0]||o,n[1]??!0);break;case"closeAllWindows":s.closeAllWindows(...n);break;case"getWindowConfig":return i?s.getWindowConfig(i):null;default:return`WindowManager.${t} is not allowed to use`}})}(t);for(const i of s.extraHandlers??[])"handle"===i.type?e.handle(i.channel,i.handler):e.on(i.channel,i.handler);for(const[i,o]of Object.entries(s.overrides??{}))"handle"===o.type?e.handle(o.channel,o.handler):e.on(o.channel,o.handler);s.setup&&s.setup(e,t)}function E(e,t){if(!t?.enabled)return;s.requestSingleInstanceLock()?s.on("second-instance",()=>{t.onSecondInstance?t.onSecondInstance():e.windowManager.openMainWindow()}):s.quit()}class ${tasks=[];savePath;debouncedSave;onPersist;constructor(e){this.savePath=w.join(s.getPath("userData"),"downloadTasks.json"),this.onPersist=e,this.load(),this.debouncedSave=function(e,t){let s=null;return(...n)=>{null!==s&&clearTimeout(s),s=setTimeout(()=>{e(...n),s=null},t)}}(()=>this.save(),1e3)}load(){try{if(f.existsSync(this.savePath)){const e=f.readFileSync(this.savePath,"utf-8");this.tasks=JSON.parse(e)}}catch{this.tasks=[]}}save(){try{f.writeFileSync(this.savePath,JSON.stringify(this.tasks,null,2),"utf-8"),this.onPersist?.(this.getAll())}catch(e){console.error("[electron-multi-app-kit] Failed to save download tasks:",e)}}getAll(){return e=this.tasks,JSON.parse(JSON.stringify(e));var e}add(e){this.tasks.push(e),this.debouncedSave()}update(e,t){const s=this.tasks.findIndex(t=>t.id===e);-1!==s&&(this.tasks[s]={...this.tasks[s],...t},this.debouncedSave())}remove(e){this.tasks=this.tasks.filter(t=>t.id!==e),this.debouncedSave()}}class L{storage;constructor(e){this.storage=new $(e)}addTask(e,t={}){const s=v.randomUUID(),n={id:s,url:e,fileName:t.fileName,savePath:t.savePath,state:"pending",progress:0,speed:0,receivedBytes:0,totalBytes:0};return this.storage.add(n),s}startTask(e,t){const s=this.storage.getAll().find(t=>t.id===e);s&&(t.downloadURL(s.url),t.session.once("will-download",(t,n)=>{s.savePath&&n.setSavePath(s.savePath),s.item=n,this.storage.update(e,{state:"downloading"});let i=0,o=Date.now();n.on("updated",()=>{const t=Date.now(),s=(t-o)/1e3,a=n.getReceivedBytes()-i,r=s>0?a/s:0;i=n.getReceivedBytes(),o=t,this.storage.update(e,{progress:n.getTotalBytes()>0?n.getReceivedBytes()/n.getTotalBytes():0,speed:r,receivedBytes:n.getReceivedBytes(),totalBytes:n.getTotalBytes()})}),n.on("done",(t,i)=>{this.storage.update(e,{state:{completed:"completed",cancelled:"interrupted",interrupted:"interrupted"}[i]??"failed",fileName:s.fileName??n.getFilename(),savePath:n.getSavePath(),speed:0,receivedBytes:n.getReceivedBytes(),totalBytes:n.getTotalBytes(),progress:n.getTotalBytes()>0?n.getReceivedBytes()/n.getTotalBytes():0})})}))}pauseTask(e){const t=this.storage.getAll().find(t=>t.id===e);t?.item?.canResume()&&(t.item.pause(),this.storage.update(e,{state:"paused"}))}resumeTask(e){const t=this.storage.getAll().find(t=>t.id===e);t?.item?.canResume()&&(t.item.resume(),this.storage.update(e,{state:"downloading"}))}cancelTask(e){const t=this.storage.getAll().find(t=>t.id===e);t?.item&&t.item.cancel()}removeTask(e){this.storage.remove(e)}getAllTasks(){return this.storage.getAll()}getUniqueFilename(e){if(!f.existsSync(e))return e;const t=w.dirname(e),s=w.extname(e),n=w.basename(e,s);let i=1,o=w.join(t,`${n} (${i})${s}`);for(;f.existsSync(o);)i++,o=w.join(t,`${n} (${i})${s}`);return o}}function j(e){p.release().startsWith("6.1")&&(s.disableHardwareAcceleration(),s.commandLine.appendSwitch("disable-gpu"),s.commandLine.appendSwitch("no-sandbox")),"win32"===process.platform&&s.setAppUserModelId(s.getName());const t=k(e);E(t,e.instance);const n=new L(e=>{const s=t.windowManager.getMain();s?.webContents.send("download-tasks",e)});return N({windowManager:t.windowManager,downloader:n,paths:e.paths},e.ipc),S(t),t}const q=new b,H=e=>JSON.parse(JSON.stringify(e));class J{tasks=[];savePath;debouncedSave;onPersist;constructor(e){this.savePath=w.join(s.getPath("userData"),"batchDownloadTasks.json"),this.onPersist=e,this.load(),this.debouncedSave=function(e,t=1e3){let s=null;return function(){let n=this,i=arguments;s&&clearTimeout(s),s=setTimeout(function(){e.apply(n,i)},t)}}(()=>this.save(),1e3)}load(){try{if(f.existsSync(this.savePath)){const e=f.readFileSync(this.savePath,"utf-8");this.tasks=JSON.parse(e)}}catch{this.tasks=[]}}save(){try{f.writeFileSync(this.savePath,JSON.stringify(this.tasks,null,2),"utf-8")}catch(e){console.error("[electron-app-downloader] Failed to save batch download tasks:",e)}}getAll(){return H(this.tasks)}getById(e){const t=this.tasks.find(t=>t.id===e);return t?H(t):void 0}add(e){this.tasks.push(e),this.change()}update(e,t){const s=this.tasks.findIndex(t=>t.id===e);-1!==s&&(this.tasks[s]={...this.tasks[s],...t,updatedAt:Date.now()},this.change())}updateFile(e,t,s){const n=this.tasks.findIndex(t=>t.id===e);if(-1===n)return;const i=this.tasks[n],o=i.files.map(e=>e.id===t?{...e,...s}:e),a=J.calcSummary(o);this.tasks[n]={...i,files:o,...a,updatedAt:Date.now()},this.change()}remove(e){this.tasks=this.tasks.filter(t=>t.id!==e),this.change()}change(){this.onPersist?.(this.getAll()),this.debouncedSave()}static calcSummary(e){const t=e.length,s=e.filter(e=>"completed"===e.state).length,n=e.reduce((e,t)=>e+t.speed,0),i=e.reduce((e,t)=>e+t.receivedBytes,0),o=e.length>0&&e.every(e=>J.isFileTotalBytesKnown(e)),a=o?e.reduce((e,t)=>e+J.getEffectiveTotalBytes(t),0):0,r=o&&a>0?i/a:e.reduce((e,t)=>{if("completed"===t.state)return e+1;const s=J.getEffectiveTotalBytes(t),n=s>0?t.receivedBytes/s:t.progress;return e+Math.min(Math.max(n,0),1)},0)/Math.max(t,1);return{progress:Math.min(Math.max(r,0),1),speed:n,receivedBytes:i,totalBytes:a,completedCount:s,totalCount:t}}static isFileTotalBytesKnown(e){return e.totalBytes>0||"completed"===e.state}static getEffectiveTotalBytes(e){return e.totalBytes>0?e.totalBytes:"completed"===e.state?e.receivedBytes:0}}class Q{storage;activeItems=/* @__PURE__ */new Map;runningBatchIds=/* @__PURE__ */new Set;pausedBatchIds=/* @__PURE__ */new Set;cancelledBatchIds=/* @__PURE__ */new Set;resumeWaiters=/* @__PURE__ */new Map;constructor(e){this.storage=new J(e)}async addTask(e,t,n){if(!e.length)throw new Error("files cannot be empty");if(!t.outputName?.trim())throw new Error("config.outputName is required");const i=v.randomUUID(),o=t.selectSaveDir??!1,a=o?await this.selectSaveDir(t.saveDir,n):t.saveDir??s.getPath("downloads");if(!a)return;const r=this.sanitizeFilename(t.outputName),d=t.shouldZip??!1,l=t.removeSourceAfterZip??!0,c=t.openAfterCompleted??!1,h=d?this.getUniquePath(w.join(this.getTempWorkRoot(),`${r}-${i}`)):this.getUniquePath(w.join(a,r)),u=d?this.getUniquePath(w.join(a,`${r}.zip`)):h;f.mkdirSync(h,{recursive:!0});const p=/* @__PURE__ */new Set,g=e.map(e=>{const t=this.sanitizeRelativeFilePath(e.fileName??this.getFilenameFromUrl(e.url)),s=this.getUniquePathInSet(w.join(h,t),p);this.ensurePathInsideDir(h,s),f.mkdirSync(w.dirname(s),{recursive:!0}),p.add(s);const{url:n,fileName:i,...o}=e;return{id:v.randomUUID(),url:n,fileName:t,savePath:s,state:"pending",progress:0,speed:0,receivedBytes:0,totalBytes:0,meta:o}}),m=Date.now(),y={id:i,state:"pending",config:{shouldZip:d,saveDir:a,outputName:r,removeSourceAfterZip:l,selectSaveDir:o,openAfterCompleted:c},workDir:h,outputPath:u,files:g,...J.calcSummary(g),createdAt:m,updatedAt:m};return this.storage.add(y),i}async startTask(e,t){const s=this.storage.getById(e);if(s&&!this.runningBatchIds.has(e)){this.runningBatchIds.add(e),this.cancelledBatchIds.delete(e),this.storage.update(e,{state:"downloading",error:void 0});try{await this.prefetchTotalBytes(e,t);const n=this.storage.getById(e),i=n?.files??s.files;for(const s of i){await this.waitIfPaused(e);const n=this.storage.getById(e),i=n?.files.find(e=>e.id===s.id);if(!n||!i)break;if(this.cancelledBatchIds.has(e))throw new Error("cancelled");"completed"!==i.state&&await this.downloadOne(e,i.id,t)}if(this.cancelledBatchIds.has(e))throw new Error("cancelled");const o=this.storage.getById(e);if(!o)return;if(o.files.some(e=>["failed","interrupted","cancelled"].includes(e.state)))return void this.storage.update(e,{state:"failed"});o.config.shouldZip&&(this.storage.update(e,{state:"zipping",speed:0}),await this.zipDirectory(o.workDir,o.outputPath)),this.storage.update(e,{state:"completed",progress:1,speed:0}),o.config.openAfterCompleted&&await this.openCompletedLocation(o)}catch(n){const t=this.cancelledBatchIds.has(e)||"cancelled"===n.message;this.storage.update(e,{state:t?"cancelled":"failed",speed:0,error:t?void 0:n.message})}finally{await this.removeTempWorkDir(s),this.runningBatchIds.delete(e),this.pausedBatchIds.delete(e),this.cancelledBatchIds.delete(e)}}}pauseTask(e){this.pausedBatchIds.add(e),this.storage.update(e,{state:"paused",speed:0});const t=this.storage.getById(e),s=t?.files.find(e=>"downloading"===e.state);if(!s)return;const n=this.activeItems.get(s.id);if(n)try{n.pause(),this.storage.updateFile(e,s.id,{state:"paused",speed:0})}catch(i){}}resumeTask(e){this.pausedBatchIds.delete(e),this.storage.update(e,{state:"downloading"});const t=this.storage.getById(e),s=t?.files.find(e=>"paused"===e.state);if(s){const t=this.activeItems.get(s.id);t?.canResume()&&(t.resume(),this.storage.updateFile(e,s.id,{state:"downloading"}))}this.flushResumeWaiters(e)}cancelTask(e){this.cancelledBatchIds.add(e),this.pausedBatchIds.delete(e),this.flushResumeWaiters(e);const t=this.storage.getById(e),s=t?.files.find(e=>["downloading","paused"].includes(e.state));s&&(this.activeItems.get(s.id)?.cancel(),this.storage.updateFile(e,s.id,{state:"cancelled",speed:0})),this.storage.update(e,{state:"cancelled",speed:0}),t&&this.removeTempWorkDir(t)}removeTask(e){this.cancelTask(e),this.storage.remove(e)}getTask(e){return this.storage.getById(e)}getAllTasks(){return this.storage.getAll()}getProgress(e){const t=this.storage.getById(e);if(t)return{id:t.id,state:t.state,progress:t.progress,speed:t.speed,receivedBytes:t.receivedBytes,totalBytes:t.totalBytes,completedCount:t.completedCount,totalCount:t.totalCount,files:t.files,outputPath:t.outputPath,error:t.error}}getUniquePath(e){if(!f.existsSync(e))return e;const t=w.dirname(e),s=w.extname(e),n=w.basename(e,s);let i=1,o=w.join(t,`${n} (${i})${s}`);for(;f.existsSync(o);)i++,o=w.join(t,`${n} (${i})${s}`);return o}getUniquePathInSet(e,t){let s=this.getUniquePath(e);if(!t.has(s))return s;const n=w.dirname(e),i=w.extname(e),o=w.basename(e,i);let a=1;do{s=this.getUniquePath(w.join(n,`${o} (${a})${i}`)),a++}while(t.has(s));return s}ensurePathInsideDir(e,t){const s=w.resolve(e),n=w.resolve(t);if(n===s||!n.startsWith(`${s}${w.sep}`))throw new Error("fileName must be a relative path inside the download directory")}getTempWorkRoot(){return w.join(s.getPath("temp"),s.getName(),"batch-downloads")}async prefetchTotalBytes(e,t,s=5){const n=this.storage.getById(e);if(!n)return;const i=n.files.filter(e=>e.totalBytes<=0);if(!i.length)return;let o=0;const a=Math.min(Math.max(s,1),i.length),r=Array.from({length:a},async()=>{for(;o<i.length;){if(this.cancelledBatchIds.has(e))return;const s=i[o++],n=await this.fetchContentLength(s.url,t);n>0&&this.storage.updateFile(e,s.id,{totalBytes:n})}});await Promise.allSettled(r)}fetchContentLength(e,t){return new Promise(s=>{let n=!1;const i=e=>{n||(n=!0,s(Number.isFinite(e)&&e>0?e:0))},o=u.request({method:"HEAD",url:e,session:t.session});o.on("response",e=>{const t=e.headers["content-length"],s=Array.isArray(t)?t[0]:t;i(Number(s))}),o.on("error",()=>i(0)),o.setHeader("Cache-Control","no-cache"),o.end()})}downloadOne(e,t,s){const n=this.storage.getById(e),i=n?.files.find(e=>e.id===t);return n&&i?new Promise((n,o)=>{let a=i.receivedBytes,r=Date.now(),d=!1;const l=()=>{s.session.removeListener("will-download",c),this.activeItems.delete(t)},c=(s,c)=>{c.getURL()!==i.url||d||(d=!0,c.setSavePath(i.savePath),this.activeItems.set(t,c),this.storage.updateFile(e,t,{state:"downloading",error:void 0}),c.on("updated",(s,n)=>{if("interrupted"!==n){if("progressing"===n){const s=Date.now(),n=(s-r)/1e3,o=c.getReceivedBytes(),d=c.getTotalBytes(),l=this.storage.getById(e)?.files.find(e=>e.id===t),h=d>0?d:l?.totalBytes??0,u=n>0?(o-a)/n:0;a=o,r=s,this.storage.updateFile(e,t,{state:this.pausedBatchIds.has(e)?"paused":"downloading",progress:h>0?o/h:l?.progress??i.progress,speed:this.pausedBatchIds.has(e)?0:u,receivedBytes:o,totalBytes:h})}}else this.storage.updateFile(e,t,{state:"interrupted",speed:0,receivedBytes:c.getReceivedBytes(),totalBytes:c.getTotalBytes()>0?c.getTotalBytes():this.storage.getById(e)?.files.find(e=>e.id===t)?.totalBytes??0})}),c.once("done",(s,i)=>{if(l(),"completed"===i)return this.storage.updateFile(e,t,{state:"completed",progress:1,speed:0,receivedBytes:c.getReceivedBytes(),totalBytes:c.getTotalBytes()>0?c.getTotalBytes():this.storage.getById(e)?.files.find(e=>e.id===t)?.totalBytes??0}),void n();const a=this.cancelledBatchIds.has(e)||"cancelled"===i,r=a?"cancelled":"interrupted";this.storage.updateFile(e,t,{state:r,speed:0,receivedBytes:c.getReceivedBytes(),totalBytes:c.getTotalBytes()>0?c.getTotalBytes():this.storage.getById(e)?.files.find(e=>e.id===t)?.totalBytes??0,error:a?void 0:i}),o(new Error(a?"cancelled":i))}))};s.session.on("will-download",c),s.downloadURL(i.url)}):Promise.resolve()}async waitIfPaused(e){this.pausedBatchIds.has(e)&&await new Promise(t=>{const s=this.resumeWaiters.get(e)??[];s.push(t),this.resumeWaiters.set(e,s)})}flushResumeWaiters(e){(this.resumeWaiters.get(e)??[]).forEach(e=>e()),this.resumeWaiters.delete(e)}async selectSaveDir(e,i){const o=i?t.fromWebContents(i)??void 0:void 0;if(!o)return;const a=await n.showOpenDialog(o,{title:"选择下载目录",defaultPath:e??s.getPath("downloads"),properties:["openDirectory","createDirectory"]});return a.canceled?void 0:a.filePaths[0]}async openCompletedLocation(e){if(f.existsSync(e.outputPath))return void c.showItemInFolder(e.outputPath);await c.openPath(w.dirname(e.outputPath))}async removeTempWorkDir(e){if(e.config.shouldZip&&e.config.removeSourceAfterZip)try{await f.promises.rm(e.workDir,{recursive:!0,force:!0})}catch(t){}}async zipDirectory(e,t){await f.promises.mkdir(w.dirname(t),{recursive:!0});const{ZipArchive:s}=await import("./index-CtN4cHF_.mjs");await new Promise((n,i)=>{const o=f.createWriteStream(t),a=new s({zlib:{level:9}});o.on("close",()=>n()),o.on("error",i),a.on("warning",i),a.on("error",i),a.pipe(o),a.directory(e,!1),a.finalize()})}getFilenameFromUrl(e){try{const t=new URL(e);return w.basename(decodeURIComponent(t.pathname))||`${v.randomUUID()}.download`}catch{return w.basename(e)||`${v.randomUUID()}.download`}}sanitizeFilename(e){return e.replace(/[\\/:*?"<>|]/g,"_").trim()||`${v.randomUUID()}.download`}sanitizeRelativeFilePath(e){const t=e.trim();if(!t)return`${v.randomUUID()}.download`;if(w.isAbsolute(t)||w.win32.isAbsolute(t)||w.posix.isAbsolute(t))throw new Error("fileName must be a relative path");const s=t.replace(/\\/g,"/").split("/").filter(e=>e&&"."!==e);if(!s.length)return`${v.randomUUID()}.download`;if(s.some(e=>".."===e))throw new Error("fileName cannot contain ..");return w.join(...s.map(e=>this.sanitizeFilename(e)))}}function Z(e){let t=null,s=null,n=!1;e.webContents.on("before-input-event",(i,o)=>{if("mouseDown"===o.type){s=l.getCursorScreenPoint();const i=e.getPosition();t={x:i[0],y:i[1]},n=!1}else if("mouseMove"===o.type&&s&&t){const i=l.getCursorScreenPoint(),o=i.x-s.x,a=i.y-s.y;if((Math.abs(o)>2||Math.abs(a)>2)&&(n=!0),n){const s=t.x+o,n=t.y+a;e.setPosition(s,n)}}else"mouseUp"===o.type&&(n&&i.preventDefault?.(),s=null,t=null,n=!1)})}function V(e){e.setResizable(!1),e.setBackgroundColor("#00000000"),e.setIgnoreMouseEvents(!0,{forward:!0}),e.setVisibleOnAllWorkspaces(!0),e.setAlwaysOnTop(!0,"screen-saver")}const _="--startup-hidden",K="--auto-launch";const G=new class{options={hidden:!0,args:[]};init(e){this.options={...this.options,...e,args:e?.args||this.options.args}}enable(e){const t={...this.options,...e,args:e?.args||this.options.args};s.setLoginItemSettings({openAtLogin:!0,path:"win32"===process.platform?s.getPath("exe"):void 0,args:this.getArgs(t),openAsHidden:"darwin"===process.platform?!!t.hidden:void 0})}disable(){s.setLoginItemSettings({openAtLogin:!1,path:"win32"===process.platform?s.getPath("exe"):void 0})}isEnabled(){return s.getLoginItemSettings().openAtLogin}isHiddenStartup(){return process.argv.includes(_)}isAutoLaunchStartup(){return process.argv.includes(K)}getSettings(){return s.getLoginItemSettings()}getArgs(e){const t=[...e.args,K];return e.hidden&&t.push(_),Array.from(new Set(t))}};export{q as AppEvent,G as AutoLaunch,Q as BatchDownloadManager,L as DownloadManager,W as WindowManager,k as createContext,j as createElectronApp,F as useDialogApi,O as useDownloaderApi,V as useFloatWin,U as useFsApi,N as useIpcEvents,S as useLifeCycle,R as useNotificationApi,P as useSession,z as useShellApi,E as useSingleInstance,I as useTray,C as useUpdater,Z as useWinDrag,M as useWindowControl,T as useWindowMove};
@@ -0,0 +1,47 @@
1
+ export interface AutoLaunchOptions {
2
+ /**
3
+ * Windows 开机自启时是否隐藏启动
4
+ * macOS 下会映射到 openAsHidden
5
+ */
6
+ hidden?: boolean;
7
+ /**
8
+ * 额外启动参数
9
+ */
10
+ args?: string[];
11
+ }
12
+ declare class AutoLaunchService {
13
+ private options;
14
+ init(options?: AutoLaunchOptions): void;
15
+ enable(options?: AutoLaunchOptions): void;
16
+ disable(): void;
17
+ isEnabled(): boolean;
18
+ isHiddenStartup(): boolean;
19
+ isAutoLaunchStartup(): boolean;
20
+ getSettings(): Electron.LoginItemSettings;
21
+ private getArgs;
22
+ }
23
+ /**
24
+ *
25
+ * 开机自启控制
26
+ *
27
+ * @example
28
+ * ```ts
29
+ *app.whenReady().then(() => {
30
+ * AutoLaunch.init({
31
+ * hidden: true,
32
+ * })
33
+ * createWindow()
34
+ *})
35
+ * ```
36
+ * 创建窗口时添加判断,根据是否隐藏启动来判断是否显示窗口
37
+ * ```ts
38
+ * const shouldShowWindow = !AutoLaunch.isHiddenStartup()
39
+ * mainWindow = new BrowserWindow({
40
+ * width: 1000,
41
+ * height: 700,
42
+ * show: shouldShowWindow,
43
+ * })
44
+ ```
45
+ */
46
+ export declare const AutoLaunch: AutoLaunchService;
47
+ export default AutoLaunch;
@@ -0,0 +1,89 @@
1
+ import { WebContents } from 'electron';
2
+ export type BatchDownloadState = 'pending' | 'downloading' | 'paused' | 'zipping' | 'completed' | 'failed' | 'cancelled' | 'interrupted';
3
+ export type BatchFileDownloadState = 'pending' | 'downloading' | 'paused' | 'completed' | 'failed' | 'cancelled' | 'interrupted';
4
+ export interface BatchDownloadFileInput {
5
+ url: string;
6
+ fileName?: string;
7
+ [key: string]: unknown;
8
+ }
9
+ export interface BatchDownloadConfig {
10
+ /** 下载完成后是否打包为 zip;false 时会保留为文件夹 */
11
+ shouldZip?: boolean;
12
+ /** 保存目录,默认系统 Downloads 目录 */
13
+ saveDir?: string;
14
+ /** 压缩包/文件夹名称,不需要带 .zip */
15
+ outputName: string;
16
+ /** shouldZip=true 时,打包完成后是否删除临时文件夹,默认 true */
17
+ removeSourceAfterZip?: boolean;
18
+ /** 是否选择保存目录 */
19
+ selectSaveDir?: boolean;
20
+ /** 下载完后自动打开所在目录 */
21
+ openAfterCompleted?: boolean;
22
+ }
23
+ export interface BatchFileDownloadTask {
24
+ id: string;
25
+ url: string;
26
+ fileName: string;
27
+ savePath: string;
28
+ state: BatchFileDownloadState;
29
+ progress: number;
30
+ speed: number;
31
+ receivedBytes: number;
32
+ totalBytes: number;
33
+ error?: string;
34
+ /** 透传 files 中除 url/fileName 外的自定义字段 */
35
+ meta?: Record<string, unknown>;
36
+ }
37
+ export interface BatchDownloadTask {
38
+ id: string;
39
+ state: BatchDownloadState;
40
+ config: Required<BatchDownloadConfig>;
41
+ /** 实际下载文件夹。zip 模式下这是临时目录;非 zip 模式下这是最终目录 */
42
+ workDir: string;
43
+ /** 最终产物路径:zip 文件或文件夹 */
44
+ outputPath: string;
45
+ files: BatchFileDownloadTask[];
46
+ progress: number;
47
+ speed: number;
48
+ receivedBytes: number;
49
+ totalBytes: number;
50
+ completedCount: number;
51
+ totalCount: number;
52
+ error?: string;
53
+ createdAt: number;
54
+ updatedAt: number;
55
+ }
56
+ export declare class BatchDownloadManager {
57
+ private storage;
58
+ private activeItems;
59
+ private runningBatchIds;
60
+ private pausedBatchIds;
61
+ private cancelledBatchIds;
62
+ private resumeWaiters;
63
+ constructor(onPersist?: (tasks: BatchDownloadTask[]) => void);
64
+ addTask(files: BatchDownloadFileInput[], config: BatchDownloadConfig, webContents?: WebContents): Promise<string | undefined>;
65
+ startTask(batchId: string, webContents: WebContents): Promise<void>;
66
+ pauseTask(batchId: string): void;
67
+ resumeTask(batchId: string): void;
68
+ cancelTask(batchId: string): void;
69
+ removeTask(batchId: string): void;
70
+ getTask(batchId: string): BatchDownloadTask | undefined;
71
+ getAllTasks(): BatchDownloadTask[];
72
+ getProgress(batchId: string): Pick<BatchDownloadTask, 'id' | 'state' | 'progress' | 'speed' | 'receivedBytes' | 'totalBytes' | 'completedCount' | 'totalCount' | 'files' | 'outputPath' | 'error'> | undefined;
73
+ getUniquePath(targetPath: string): string;
74
+ private getUniquePathInSet;
75
+ private ensurePathInsideDir;
76
+ private getTempWorkRoot;
77
+ private prefetchTotalBytes;
78
+ private fetchContentLength;
79
+ private downloadOne;
80
+ private waitIfPaused;
81
+ private flushResumeWaiters;
82
+ private selectSaveDir;
83
+ private openCompletedLocation;
84
+ private removeTempWorkDir;
85
+ private zipDirectory;
86
+ private getFilenameFromUrl;
87
+ private sanitizeFilename;
88
+ private sanitizeRelativeFilePath;
89
+ }
@@ -0,0 +1,38 @@
1
+ import { DownloadItem } from 'electron';
2
+ export type DownloadState = 'pending' | 'downloading' | 'paused' | 'completed' | 'failed' | 'interrupted';
3
+ export interface DownloadTask {
4
+ id: string;
5
+ url: string;
6
+ /**
7
+ * 展示用文件名。
8
+ * 不建议在 addTask 阶段从 URL 推导默认值,真实文件名应优先来自 DownloadItem.getFilename()。
9
+ */
10
+ fileName?: string;
11
+ /**
12
+ * 最终保存路径。
13
+ * 只有调用方明确传入完整、合法路径时,才会主动调用 item.setSavePath(savePath)。
14
+ * 未传时交给 Electron / 系统默认下载流程处理。
15
+ */
16
+ savePath?: string;
17
+ state: DownloadState;
18
+ progress: number;
19
+ speed: number;
20
+ receivedBytes: number;
21
+ totalBytes: number;
22
+ item?: DownloadItem;
23
+ }
24
+ export declare class DownloadManager {
25
+ private storage;
26
+ constructor(onPersist?: (tasks: DownloadTask[]) => void);
27
+ addTask(url: string, options?: {
28
+ fileName?: string;
29
+ savePath?: string;
30
+ }): string;
31
+ startTask(taskId: string, webContents: Electron.WebContents): void;
32
+ pauseTask(taskId: string): void;
33
+ resumeTask(taskId: string): void;
34
+ cancelTask(taskId: string): void;
35
+ removeTask(taskId: string): void;
36
+ getAllTasks(): DownloadTask[];
37
+ getUniqueFilename(filePath: string): string;
38
+ }
@@ -0,0 +1,45 @@
1
+ export type DownloadState = 'pending' | 'downloading' | 'paused' | 'completed' | 'failed' | 'interrupted';
2
+ export interface DownloadTask {
3
+ id: string;
4
+ url: string;
5
+ /**
6
+ * 展示用文件名。
7
+ * 不建议在 addTask 阶段从 URL 推导默认值,真实文件名应优先来自 DownloadItem.getFilename()。
8
+ */
9
+ fileName?: string;
10
+ /**
11
+ * 最终保存路径。
12
+ * 只有调用方明确传入完整、合法路径时,才会主动调用 item.setSavePath(savePath)。
13
+ * 未传时交给 Electron / 系统默认下载流程处理。
14
+ */
15
+ savePath?: string;
16
+ state: DownloadState;
17
+ progress: number;
18
+ speed: number;
19
+ receivedBytes: number;
20
+ totalBytes: number;
21
+ }
22
+ export interface AddDownloadTaskOptions {
23
+ /** 展示用文件名;不会强制作为最终保存文件名 */
24
+ fileName?: string;
25
+ /** 明确指定完整保存路径时才传;不传则交给 Electron / 系统处理 */
26
+ savePath?: string;
27
+ }
28
+ export declare class DownloadManager {
29
+ private storage;
30
+ /**
31
+ * DownloadItem 不能被 JSON 持久化,也不应该放进 DownloadTask 里 clone。
32
+ * 运行期能力单独维护,暂停 / 继续 / 取消时从这里取。
33
+ */
34
+ private activeItems;
35
+ constructor(onPersist?: (tasks: DownloadTask[]) => void);
36
+ addTask(url: string, options?: AddDownloadTaskOptions): string;
37
+ startTask(taskId: string, webContents: Electron.WebContents): void;
38
+ pauseTask(taskId: string): void;
39
+ resumeTask(taskId: string): void;
40
+ cancelTask(taskId: string): void;
41
+ removeTask(taskId: string): void;
42
+ getAllTasks(): DownloadTask[];
43
+ getUniqueFilename(filePath: string): string;
44
+ private getValidExplicitSavePath;
45
+ }
@@ -0,0 +1,8 @@
1
+ import { InstanceConfig } from '../types';
2
+ import { ElectronCliContext } from '../core/context';
3
+ import { WindowBaseType } from './window/manager';
4
+ /**
5
+ * 单实例锁模块 — 从 electron/main/app/instance.ts 移植
6
+ * 确保同一时间只有一个应用实例运行
7
+ */
8
+ export declare function useSingleInstance<W extends WindowBaseType = WindowBaseType>(ctx: ElectronCliContext<W>, config?: InstanceConfig): void;
@@ -0,0 +1,6 @@
1
+ import { WindowBaseType, WindowManager } from '../../window/manager';
2
+ /**
3
+ * Dialog IPC 模块 — 从 electron/main/events/ipcEvents.ts 移植
4
+ * 提供 open-file-dialog, open-file-dialog-sync, show-message-box
5
+ */
6
+ export declare function useDialogApi<W extends WindowBaseType>(windowManager: WindowManager<W>): void;
@@ -0,0 +1,6 @@
1
+ import { WindowBaseType, WindowManager } from '../../window/manager';
2
+ import { DownloadManager } from '../../downloader';
3
+ /**
4
+ * 下载管理 IPC 模块 — 从 electron/main/events/ipcEvents.ts 移植
5
+ */
6
+ export declare function useDownloaderApi<W extends WindowBaseType>(windowManager: WindowManager<W>, downloader: DownloadManager): void;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 文件系统 IPC 桥接模块 — 从 electron/main/events/api/fs-api.ts 移植
3
+ */
4
+ export declare function useFsApi(): void;
@@ -0,0 +1,5 @@
1
+ import { ElectronCliPaths } from '../../../types';
2
+ /**
3
+ * 通知 IPC 模块 — 从 electron/main/events/ipcEvents.ts 移植
4
+ */
5
+ export declare function useNotificationApi(paths: ElectronCliPaths): void;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Shell IPC 模块 — 从 electron/main/events/ipcEvents.ts 移植
3
+ * 提供 open-file, open-external, show-file
4
+ */
5
+ export declare function useShellApi(): void;
@@ -0,0 +1,5 @@
1
+ import { WindowBaseType, WindowManager } from '../../window/manager';
2
+ /**
3
+ * BrowserWindow 控制 IPC 模块 — 从 electron/main/events/api/window-control.ts 移植
4
+ */
5
+ export declare function useWindowControl<W extends WindowBaseType>(windowManager: WindowManager<W>): void;
@@ -0,0 +1,7 @@
1
+ import { WindowBaseType, WindowManager } from '../../window/manager';
2
+ /**
3
+ * 窗口拖拽 IPC 模块 — 从 electron/main/events/controler.ts 移植
4
+ * 用于无边框窗口通过渲染进程自定义标题栏拖拽移动
5
+ * 配合渲染进程的useWindowMove使用
6
+ */
7
+ export declare function useWindowMove<W extends WindowBaseType>(windowManager: WindowManager<W>): void;
@@ -0,0 +1,15 @@
1
+ import { IpcOptions, ElectronCliPaths } from '../../types';
2
+ import { WindowBaseType, WindowManager } from '../window/manager';
3
+ import { DownloadManager } from '../downloader';
4
+ export interface IpcContext<W extends WindowBaseType> {
5
+ windowManager: WindowManager<W>;
6
+ downloader: DownloadManager;
7
+ paths: ElectronCliPaths;
8
+ }
9
+ /**
10
+ * IPC 事件注册入口 — 从 electron/main/events/ipcEvents.ts 移植
11
+ *
12
+ * 注册所有内置 IPC handler,同时允许用户通过 options 添加自定义 handler
13
+ * (即 useIpcEvents(ipcMain) 的核心能力)和禁用/覆盖内置模块。
14
+ */
15
+ export declare function useIpcEvents<W extends WindowBaseType>(ctx: IpcContext<W>, options?: IpcOptions<W>): void;
@@ -0,0 +1,6 @@
1
+ import { SessionConfig } from '../types';
2
+ /**
3
+ * Session 配置模块 — 从 electron/main/app/session.ts 移植
4
+ * 注册 session.defaultSession.webRequest.onBeforeSendHeaders 拦截器
5
+ */
6
+ export declare function useSession(config?: SessionConfig): void;
@@ -0,0 +1,7 @@
1
+ import { TrayConfig } from '../types';
2
+ import { ElectronCliContext } from '../core/context';
3
+ import { WindowBaseType } from './window/manager';
4
+ /**
5
+ * 系统托盘模块 — 从 electron/main/app/tray.ts 移植
6
+ */
7
+ export declare function useTray<W extends WindowBaseType>(ctx: ElectronCliContext<W>, config?: TrayConfig<W>): void;
@@ -0,0 +1,5 @@
1
+ import { UpdaterConfig } from '../types';
2
+ /**
3
+ * 自动更新模块 — 从 electron/main/app/updater.ts 移植
4
+ */
5
+ export declare function useUpdater(config?: UpdaterConfig): void;
@@ -0,0 +1,103 @@
1
+ import { BrowserWindow, BrowserWindowConstructorOptions } from 'electron';
2
+ import { ElectronCliPaths, LifecycleHooks, WindowDefinition } from '../../types';
3
+ export type WindowBaseType = string;
4
+ export interface WindowManagerOptions<W extends WindowBaseType = WindowBaseType> {
5
+ /** 窗口定义列表 */
6
+ windows: WindowDefinition<W>[];
7
+ /** 主窗口的 type */
8
+ mainWindowType: W;
9
+ /** 路径配置 */
10
+ paths: ElectronCliPaths;
11
+ /** 生命周期钩子 */
12
+ lifecycle?: LifecycleHooks<W>;
13
+ /** 默认 BrowserWindow 选项(会与各窗口的 options 合并) */
14
+ defaultOptions?: BrowserWindowConstructorOptions;
15
+ }
16
+ /**
17
+ * 可配置的窗口管理器 — 从 electron/main/window/manager.ts 的 WindowsManager 单例改造
18
+ *
19
+ * 变化:
20
+ * - WindowType 枚举 → 泛型 WindowBaseType
21
+ * - 硬编码 windowOptions → 用户通过 WindowDefinition[] 配置
22
+ * - 硬编码 onCreate* 回调 → 用户通过 WindowDefinition.onCreate 配置
23
+ * - 硬编码路径 → 通过 ElectronCliPaths 配置
24
+ */
25
+ export declare class WindowManager<W extends WindowBaseType = WindowBaseType> {
26
+ private windows;
27
+ private windowsConfig;
28
+ private definitions;
29
+ private mainWindowType;
30
+ private paths;
31
+ private lifecycle?;
32
+ private defaultOptions;
33
+ private _defaultWindowConfig;
34
+ /** 退出确认标志(对应原 confirmQuit) */
35
+ confirmQuit: boolean;
36
+ constructor(options: WindowManagerOptions<W>);
37
+ /**
38
+ * 创建指定类型的窗口
39
+ */
40
+ create(type?: W): BrowserWindow;
41
+ /**
42
+ * 打开主窗口(如果不存在则创建)
43
+ */
44
+ openMainWindow(): BrowserWindow;
45
+ /**
46
+ * 打开指定类型的窗口
47
+ */
48
+ openWindow(type?: W): BrowserWindow;
49
+ /**
50
+ * 打开一个子窗口/模态窗口页面
51
+ */
52
+ openWindowPage(hash?: string, options?: BrowserWindowConstructorOptions, callback?: (win: BrowserWindow) => void): Promise<void>;
53
+ /**
54
+ * 打开一个 URL 页面
55
+ */
56
+ openUrl(hash: string, options?: BrowserWindowConstructorOptions): BrowserWindow;
57
+ getWindow(type?: W): BrowserWindow | null;
58
+ getMain(): BrowserWindow;
59
+ getWindowByWebContents(web: Electron.WebContents): BrowserWindow | null;
60
+ getWindowById(id: number): BrowserWindow | null;
61
+ getAllWindows(): BrowserWindow[];
62
+ getWebContents(type?: W): Electron.WebContents | null;
63
+ /**
64
+ * 反向查找窗口类型
65
+ */
66
+ getWindowType(win: BrowserWindow): W | null;
67
+ /**
68
+ * 获取窗口的合并配置
69
+ */
70
+ getWindowConfig(winOrDef: BrowserWindow | WindowDefinition<W>): BrowserWindowConstructorOptions | null;
71
+ closeWindow(type: W, confirm?: boolean): void;
72
+ closeAllWindows(confirm?: boolean): void;
73
+ appQuit(): void;
74
+ private mergeConfig;
75
+ private createBrowserWindow;
76
+ /**
77
+ * 所有窗口的通用设置 — 对应原 windowSettings()
78
+ */
79
+ private applyWindowSettings;
80
+ private windowOpenHandler;
81
+ /**
82
+ * 通用设置(在 create 中额外调用,对应原 create() 中非 windowSettings 的逻辑)
83
+ */
84
+ private applyCommonSettings;
85
+ /**
86
+ * 加载 hash 路由(项目特定逻辑,由用户的 onCreate 回调处理)
87
+ * 这里提供一个通用方法供参考
88
+ */
89
+ private loadHashRoute;
90
+ /**
91
+ * 将 window.open 的 features 字符串转换为对象
92
+ * @param {string} featuresString - 例如 "width=300,height=600,frame=no"
93
+ * @returns {object} 解析后的配置对象
94
+ */
95
+ private parseWindowFeatures;
96
+ /**
97
+ * 判断是否为主窗口
98
+ * @param win
99
+ * @returns
100
+ */
101
+ isMainWindow(win: BrowserWindow): boolean;
102
+ confirmExit(win: BrowserWindow): void;
103
+ }
@@ -0,0 +1,6 @@
1
+ import { BrowserWindow } from 'electron';
2
+ /**
3
+ * 悬浮透明窗口样式
4
+ *
5
+ */
6
+ export declare function useFloatWin(win: BrowserWindow): void;
@@ -0,0 +1,8 @@
1
+ import { BrowserWindow } from 'electron';
2
+ /**
3
+ * 窗口的鼠标拖拽工具 — 从 electron/main/window/hooks/useWinDrag.ts 移植
4
+ *
5
+ * 用于 MAIN_FLOAT 类型的透明悬浮窗口,通过监听 before-input-event
6
+ * 实现鼠标拖拽移动窗口,同时保持窗口对鼠标事件透明。
7
+ */
8
+ export declare function useWinDrag(win: BrowserWindow): void;
@@ -0,0 +1,34 @@
1
+ import { ipcRenderer } from 'electron';
2
+ /**
3
+ * IPC 桥接对象 — 供其他 preload 模块组合使用
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * // 在 preload 中构建业务 API
8
+ * import { ipcBridge } from 'electron-multi-app-kit/preload'
9
+ *
10
+ * contextBridge.exposeInMainWorld('MyAPI', {
11
+ * async getData() {
12
+ * return ipcBridge.invoke<string>('get-data')
13
+ * }
14
+ * })
15
+ * ```
16
+ */
17
+ export declare const ipcBridge: {
18
+ on(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void): Electron.IpcRenderer;
19
+ once(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void): Electron.IpcRenderer;
20
+ off(eventName: string | symbol, listener: (...args: any[]) => void): Electron.IpcRenderer;
21
+ send(channel: string, ...args: any[]): void;
22
+ invoke<T = any>(channel: string, ...args: any[]): Promise<T>;
23
+ sendToHost(channel: string, ...args: any[]): void;
24
+ /** 向父窗口发送消息 */
25
+ sendToParent(channel: string, ...args: any[]): void;
26
+ /** 向所有子窗口发送消息 */
27
+ sendToChild(channel: string, ...args: any[]): void;
28
+ /** 广播给所有窗口,除了发送者 */
29
+ sendToRenderer(channel: string, ...args: any[]): void;
30
+ /** 通知主进程加载完毕 */
31
+ ready(channel: string, ...args: any[]): void;
32
+ };
33
+ /** ipcBridge 的类型 — 供渲染进程声明 window.ipcRenderer 类型 */
34
+ export type IpcBridge = typeof ipcBridge;
@@ -0,0 +1,6 @@
1
+ declare function domReady(condition?: DocumentReadyState[]): Promise<unknown>;
2
+ declare function useThroughElement(): void;
3
+ declare const safeDOM: {
4
+ append(parent: HTMLElement, child: HTMLElement): HTMLElement | undefined;
5
+ remove(parent: HTMLElement, child: HTMLElement): HTMLElement | undefined;
6
+ };
@@ -0,0 +1 @@
1
+ export * from './base';
@@ -0,0 +1 @@
1
+ import{contextBridge as e,ipcRenderer as n}from"electron";const r={on(...e){const[r,t]=e;return n.on(r,(e,...n)=>t(e,...n))},once(...e){const[r,t]=e;return n.once(r,(e,...n)=>t(e,...n))},off(...e){const[r,...t]=e;return n.off(r,...t)},send(...e){const[r,...t]=e;return n.send(r,...t)},invoke(...e){const[r,...t]=e;return n.invoke(r,...t)},sendToHost(...e){const[r,...t]=e;return n.sendToHost(r,...t)},sendToParent(...e){const[r,...t]=e;return n.send("send-to-parent",r,...t)},sendToChild(...e){const[r,...t]=e;return n.send("send-to-child",r,...t)},sendToRenderer(...e){const[r,...t]=e;return n.send("send-to-renderer",r,...t)},ready:(...e)=>n.send("renderer-ready",...e)};e.exposeInMainWorld("ipcRenderer",r);const t={append(e,n){if(!Array.from(e.children).find(e=>e===n))return e.appendChild(n)},remove(e,n){if(Array.from(e.children).find(e=>e===n))return e.removeChild(n)}};(function(e=["complete","interactive"]){return new Promise(n=>{e.includes(document.readyState)?n(!0):document.addEventListener("readystatechange",()=>{e.includes(document.readyState)&&n(!0)})})})().then(function(){const e=document.createElement("script");e.id="app-through-element-script",e.innerHTML="\n document.addEventListener('mousemove', (e) => {\n const target = e.target;\n \n const notThroughElement = target?.closest('.electron-element--not-through');\n if (notThroughElement && window.ipcRenderer) {\n window.ipcRenderer.send('set-ignore-mouse-events', false, { forward: true });\n return;\n }\n \n const throughElement = target?.closest('.electron-element--through');\n if (throughElement && window.ipcRenderer) {\n window.ipcRenderer.send('set-ignore-mouse-events', true, { forward: true });\n return;\n }\n \n if (window.ipcRenderer) {\n window.ipcRenderer.send('set-ignore-mouse-events', false, { forward: true });\n }\n });\n ",t.append(document.head,e)});export{r as ipcBridge};
@@ -0,0 +1,28 @@
1
+ export type useIpcRenderer = typeof useIpcRenderer;
2
+ /**
3
+ * Vue IPC Renderer 组合式函数 — 内存安全的 ipcRenderer 封装
4
+ *
5
+ * 自动在组件卸载时清理所有已注册的监听器,防止内存泄漏。
6
+ *
7
+ * @example
8
+ * ```vue
9
+ * <script setup>
10
+ * const ipc = useIpcRenderer()
11
+ *
12
+ * ipc.on('main-message', (event, data) => { ... })
13
+ * const result = await ipc.invoke<string>('get-version')
14
+ * </script>
15
+ * ```
16
+ */
17
+ export declare function useIpcRenderer(): {
18
+ invoke: <T = any>(channel: string, ...args: any[]) => Promise<T>;
19
+ on: (channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => Electron.IpcRenderer;
20
+ once(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => void): Electron.IpcRenderer;
21
+ off(eventName: string | symbol, listener: (...args: any[]) => void): Electron.IpcRenderer;
22
+ send(channel: string, ...args: any[]): void;
23
+ sendToHost(channel: string, ...args: any[]): void;
24
+ sendToParent(channel: string, ...args: any[]): void;
25
+ sendToChild(channel: string, ...args: any[]): void;
26
+ sendToRenderer(channel: string, ...args: any[]): void;
27
+ ready(channel: string, ...args: any[]): void;
28
+ };