diff --git a/electron/electron-down.js b/electron/electron-down.js index 9b8fc6ea8..1b2302ecd 100644 --- a/electron/electron-down.js +++ b/electron/electron-down.js @@ -1,149 +1,14 @@ +const {BrowserWindow, screen} = require('electron') +const path = require('path'); +const Store = require("electron-store"); const loger = require("electron-log"); -const Store = require('electron-store'); -const store = new Store({ - name: 'download-manager', - defaults: { - downloadHistory: [], - } -}); -const electronDl = require("@dootask/electron-dl").default; - -class DownloadManager { - constructor() { - this.downloadHistory = store.get('downloadHistory', []); - } - - /** - * 转换下载项格式 - * @param {Electron.DownloadItem} downloadItem - */ - convertItem(downloadItem) { - return { - filename: downloadItem.getFilename(), - path: downloadItem.getSavePath(), - url: downloadItem.getURL(), - urls: downloadItem.getURLChain(), - mine: downloadItem.getMimeType(), - received: downloadItem.getReceivedBytes(), - total: downloadItem.getTotalBytes(), - percent: downloadItem.getPercentComplete(), - speed: downloadItem.getCurrentBytesPerSecond(), - state: downloadItem.getState(), - paused: downloadItem.isPaused(), - startTime: downloadItem.getStartTime(), - endTime: downloadItem.getEndTime(), - } - } - - /** - * 添加下载项 - * @param {Electron.DownloadItem} downloadItem - */ - addDownloadItem(downloadItem) { - this.downloadHistory.unshift({ - ...this.convertItem(downloadItem), - _source: downloadItem, - }); - store.set('downloadHistory', this.downloadHistory.slice(0, 100)); // 限制最多100个下载项 - loger.info(`Download item added: ${downloadItem.getSavePath()}`); - } - - /** - * 更新下载项 - * @param {string} path - */ - updateDownloadItem(path) { - const item = this.downloadHistory.find(d => d.path === path) - if (!item) { - loger.warn(`Download item not found for path: ${path}`); - return; - } - const downloadItem = item._source; - if (!downloadItem) { - loger.warn(`Download item not found for path: ${path}`); - return; - } - Object.assign(item, this.convertItem(downloadItem)) - store.set('downloadHistory', this.downloadHistory); - loger.info(`Download item updated: ${path} - ${item.state} (${item.percent}%)`); - } - - /** - * 暂停下载项 - * @param {string} path - */ - pauseDownloadItem(path) { - const item = this.downloadHistory.find(d => d.path === path) - if (!item) { - loger.warn(`Download item not found for path: ${path}`); - return; - } - const downloadItem = item._source; - if (!downloadItem) { - loger.warn(`Download item not found for path: ${path}`); - return; - } - downloadItem.pause(); - this.updateDownloadItem(path); - } - - /** - * 恢复下载项 - * @param {string} path - */ - resumeDownloadItem(path) { - const item = this.downloadHistory.find(d => d.path === path) - if (!item) { - loger.warn(`Download item not found for path: ${path}`); - return; - } - const downloadItem = item._source; - if (!downloadItem) { - loger.warn(`Download item not found for path: ${path}`); - return; - } - downloadItem.resume(); - this.updateDownloadItem(path); - } - - /** - * 取消下载项 - * @param {string} path - */ - cancelDownloadItem(path) { - const item = this.downloadHistory.find(d => d.path === path) - if (!item) { - loger.warn(`Download item not found for path: ${path}`); - return; - } - const downloadItem = item._source; - if (!downloadItem) { - loger.warn(`Download item not found for path: ${path}`); - return; - } - downloadItem.cancel(); - this.updateDownloadItem(path); - } - - /** - * 取消所有下载项 - */ - cancelAllDownloadItems() { - this.downloadHistory.forEach(item => { - this.cancelDownloadItem(item.path); - }); - } - - /** - * 清空下载历史 - */ - clearHistory() { - this.downloadHistory = []; - store.set('downloadHistory', []); - } -} +const {default: electronDl, download} = require("@dootask/electron-dl"); +const utils = require("./utils"); +const {DownloadManager} = require("./utils/download"); +const store = new Store(); const downloadManager = new DownloadManager(); +let downloadWindow = null; function initialize(options = {}) { // 下载配置 @@ -155,20 +20,181 @@ function initialize(options = {}) { onStarted: (item) => { downloadManager.addDownloadItem(item); + syncDownloadItems(); }, onProgress: (item) => { downloadManager.updateDownloadItem(item.path); + syncDownloadItems(); }, onCancel: (item) => { downloadManager.updateDownloadItem(item.getSavePath()) + syncDownloadItems(); }, onCompleted: (item) => { downloadManager.updateDownloadItem(item.path); + syncDownloadItems(); } }); } +function syncDownloadItems() { + // 同步下载项到渲染进程 + if (downloadWindow) { + downloadWindow.webContents.send('download-items', downloadManager.getDownloadItems()); + } +} + +function getLanguagePack(codeOrPack) { + if (codeOrPack && typeof codeOrPack === 'object') { + return codeOrPack; + } + const code = (codeOrPack || 'zh').toString(); + return { + code, + title: '下载管理器', + // todo + } +} + +async function open(language = 'zh', theme = 'light') { + // 获取语言包 + const finalLanguage = getLanguagePack(language); + + // 如果窗口已存在,直接显示 + if (downloadWindow) { + // 更新窗口数据 + await updateDownloadWindow(language, theme) + // 显示窗口并聚焦 + downloadWindow.show(); + downloadWindow.focus(); + return; + } + + // 窗口默认参数 + const downloadWindowOptions = { + width: 700, + height: 480, + minWidth: 500, + minHeight: 350, + center: true, + show: false, + autoHideMenuBar: true, + title: finalLanguage.title, + backgroundColor: utils.getDefaultBackgroundColor(), + webPreferences: { + preload: path.join(__dirname, 'electron-preload.js'), + webSecurity: true, + nodeIntegration: true, + contextIsolation: true, + } + } + + // 恢复窗口位置 + const downloadWindowBounds = store.get('downloadWindowBounds', {}); + if ( + downloadWindowBounds.width !== undefined && + downloadWindowBounds.height !== undefined && + downloadWindowBounds.x !== undefined && + downloadWindowBounds.y !== undefined + ) { + // 获取所有显示器的可用区域 + const displays = screen.getAllDisplays(); + // 检查窗口是否在任意一个屏幕内 + let isInScreen = false; + for (const display of displays) { + const area = display.workArea; + if ( + downloadWindowBounds.x + downloadWindowBounds.width > area.x && + downloadWindowBounds.x < area.x + area.width && + downloadWindowBounds.y + downloadWindowBounds.height > area.y && + downloadWindowBounds.y < area.y + area.height + ) { + isInScreen = true; + break; + } + } + // 如果超出所有屏幕,则移动到主屏幕可见区域 + if (!isInScreen) { + const primaryArea = screen.getPrimaryDisplay().workArea; + downloadWindowBounds.x = primaryArea.x + 50; + downloadWindowBounds.y = primaryArea.y + 50; + // 防止窗口太大超出屏幕 + downloadWindowBounds.width = Math.min(downloadWindowBounds.width, primaryArea.width - 100); + downloadWindowBounds.height = Math.min(downloadWindowBounds.height, primaryArea.height - 100); + } + downloadWindowOptions.width = downloadWindowBounds.width; + downloadWindowOptions.height = downloadWindowBounds.height; + downloadWindowOptions.center = false; + downloadWindowOptions.x = downloadWindowBounds.x; + downloadWindowOptions.y = downloadWindowBounds.y; + } + + // 创建窗口 + downloadWindow = new BrowserWindow(downloadWindowOptions); + + // 禁止修改窗口标题 + downloadWindow.on('page-title-updated', (event) => { + event.preventDefault() + }) + + // 监听窗口关闭保存窗口位置 + downloadWindow.on('close', () => { + const bounds = downloadWindow.getBounds(); + store.set('downloadWindowBounds', bounds); + }); + + // 监听窗口关闭事件 + downloadWindow.on('closed', () => { + downloadWindow = null; + }); + + // 加载下载管理器页面 + const htmlPath = path.join(__dirname, 'render', 'download', 'index.html'); + const themeParam = (theme === 'dark' ? 'dark' : 'light'); + await downloadWindow.loadFile(htmlPath, {query: {theme: themeParam}}); + + // 将语言包发送到渲染进程 + downloadWindow.webContents.once('dom-ready', () => { + updateDownloadWindow(language, theme) + }); + + // 显示窗口 + downloadWindow.show(); +} + +function close() { + if (downloadWindow) { + downloadWindow.close(); + downloadWindow = null; + } +} + +function destroy() { + if (downloadWindow) { + downloadWindow.destroy(); + downloadWindow = null; + } +} + +async function updateDownloadWindow(language, theme) { + if (downloadWindow) { + try { + const finalLanguage = getLanguagePack(language); + downloadWindow.setTitle(finalLanguage.title); + downloadWindow.webContents.send('download-theme', theme); + downloadWindow.webContents.send('download-language', finalLanguage); + syncDownloadItems() + } catch (error) { + loger.error(error); + } + } +} module.exports = { - initialize + initialize, + download, + open, + close, + destroy, + updateDownloadWindow } diff --git a/electron/electron-preload.js b/electron/electron-preload.js index 29f1a10b4..d9c865e22 100644 --- a/electron/electron-preload.js +++ b/electron/electron-preload.js @@ -32,7 +32,13 @@ contextBridge.exposeInMainWorld( 'electron', { request: (msg, callback, error) => { msg.reqId = reqId++; - reqInfo[msg.reqId] = {callback: callback, error: error}; + if (typeof callback !== "function") { + callback = function () {}; + } + if (typeof error !== "function") { + error = function () {}; + } + reqInfo[msg.reqId] = {callback, error}; if (msg.action == 'watchFile') { fileChangedListeners[msg.path] = msg.listener; delete msg.listener; diff --git a/electron/electron.js b/electron/electron.js index 582603365..2cbc84219 100644 --- a/electron/electron.js +++ b/electron/electron.js @@ -123,9 +123,6 @@ if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }); } -// 初始化下载配置 -electronDown.initialize() - /** * 启动web服务 */ @@ -345,10 +342,10 @@ function createMainWindow() { // 新窗口处理 mainWindow.webContents.setWindowOpenHandler(({url}) => { if (allowedCalls.test(url)) { - openExternal(url) + openExternal(url).catch(() => {}) } else { utils.onBeforeOpenWindow(mainWindow.webContents, url).then(() => { - openExternal(url) + openExternal(url).catch(() => {}) }) } return {action: 'deny'} @@ -587,10 +584,10 @@ function createChildWindow(args) { // 新窗口处理 browser.webContents.setWindowOpenHandler(({url}) => { if (allowedCalls.test(url)) { - openExternal(url) + openExternal(url).catch(() => {}) } else { utils.onBeforeOpenWindow(browser.webContents, url).then(() => { - openExternal(url) + openExternal(url).catch(() => {}) }) } return {action: 'deny'} @@ -866,7 +863,7 @@ function createWebTabWindow(args) { }) browserView.webContents.setWindowOpenHandler(({url}) => { if (allowedCalls.test(url)) { - openExternal(url) + openExternal(url).catch(() => {}) } else { createWebTabWindow({url}) } @@ -1102,6 +1099,8 @@ if (!getTheLock) { } // SameSite utils.useCookie() + // 初始化下载 + electronDown.initialize() // 创建主窗口 createMainWindow() // 预创建子窗口 @@ -1293,7 +1292,7 @@ ipcMain.on('webTabExternal', (event) => { if (!item) { return } - openExternal(item.view.webContents.getURL()) + openExternal(item.view.webContents.getURL()).catch(() => {}) event.returnValue = "ok" }) @@ -1358,6 +1357,7 @@ ipcMain.on('childWindowCloseAll', (event) => { }) preloadWindow?.close() mediaWindow?.close() + electronDown.close() event.returnValue = "ok" }) @@ -1370,6 +1370,7 @@ ipcMain.on('childWindowDestroyAll', (event) => { }) preloadWindow?.destroy() mediaWindow?.destroy() + electronDown.destroy() event.returnValue = "ok" }) @@ -1714,6 +1715,7 @@ ipcMain.on('updateQuitAndInstall', (event, args) => { }) preloadWindow?.destroy() mediaWindow?.destroy() + electronDown.destroy() // 启动更新子窗口 createUpdaterWindow(args.updateTitle) @@ -2611,7 +2613,7 @@ function getPluginFile(plugin) { return null; } -function uninstallPlugin(plugin) { +async function uninstallPlugin(plugin) { const pluginFile = getPluginFile(plugin); if (pluginFile != null) { @@ -2672,7 +2674,7 @@ async function deleteFile(file) { } } -function windowAction(method) { +async function windowAction(method) { let win = BrowserWindow.getFocusedWindow(); if (win) { @@ -2692,16 +2694,14 @@ function windowAction(method) { } } -function openExternal(url) { +async function openExternal(url) { //Only open http(s), mailto, tel, and callto links if (allowedUrls.test(url)) { - shell.openExternal(url).catch(_ => {}); - return true; + await shell.openExternal(url) } - return false; } -function watchFile(path) { +async function watchFile(path) { let win = BrowserWindow.getFocusedWindow(); if (win) { @@ -2713,12 +2713,13 @@ function watchFile(path) { prev: prev }); } catch (e) { - } // Ignore + // Ignore + } }); } } -function unwatchFile(path) { +async function unwatchFile(path) { fs.unwatchFile(path); } @@ -2747,7 +2748,7 @@ ipcMain.on("rendererReq", async (event, args) => { ret = await getDocumentsFolder(); break; case 'checkFileExists': - ret = await checkFileExists(args.pathParts); + ret = checkFileExists(args.pathParts); break; case 'showOpenDialog': dialogOpen = true; @@ -2768,7 +2769,7 @@ ipcMain.on("rendererReq", async (event, args) => { ret = await uninstallPlugin(args.plugin); break; case 'getPluginFile': - ret = await getPluginFile(args.plugin); + ret = getPluginFile(args.plugin); break; case 'isPluginsEnabled': ret = enablePlugins; @@ -2780,7 +2781,7 @@ ipcMain.on("rendererReq", async (event, args) => { ret = await readFile(args.filename, args.encoding); break; case 'clipboardAction': - ret = await clipboardAction(args.method, args.data); + ret = clipboardAction(args.method, args.data); break; case 'deleteFile': ret = await deleteFile(args.file); @@ -2797,6 +2798,15 @@ ipcMain.on("rendererReq", async (event, args) => { case 'openExternal': ret = await openExternal(args.url); break; + case 'openDownloadWindow': + ret = await electronDown.open(args.language || 'zh', args.theme || 'light'); + break; + case 'createDownloadTask': + ret = await electronDown.download(mainWindow, args.url, args.options || {}); + break; + case 'updateDownloadWindow': + ret = await electronDown.updateDownloadWindow(args.language, args.theme); + break; case 'watchFile': ret = await watchFile(args.path); break; @@ -2804,12 +2814,13 @@ ipcMain.on("rendererReq", async (event, args) => { ret = await unwatchFile(args.path); break; case 'getCurDir': - ret = await getCurDir(); + ret = getCurDir(); break; } event.reply('mainResp', {success: true, data: ret, reqId: args.reqId}); } catch (e) { event.reply('mainResp', {error: true, msg: e.message, e: e, reqId: args.reqId}); + loger.error('Renderer request error', e.message, e.stack); } }); diff --git a/electron/package.json b/electron/package.json index 46216c362..4b78c77a8 100755 --- a/electron/package.json +++ b/electron/package.json @@ -79,10 +79,10 @@ "files": [ "render/**/*", "public/**/*", + "utils/**/*", "electron-menu.js", "electron-preload.js", - "electron.js", - "utils.js" + "electron.js" ], "extraFiles": [ { diff --git a/electron/render/download/index.html b/electron/render/download/index.html new file mode 100644 index 000000000..190a18037 --- /dev/null +++ b/electron/render/download/index.html @@ -0,0 +1 @@ +123 diff --git a/electron/utils/download.js b/electron/utils/download.js new file mode 100644 index 000000000..f74f47580 --- /dev/null +++ b/electron/utils/download.js @@ -0,0 +1,161 @@ +const loger = require("electron-log"); +const Store = require('electron-store'); +const store = new Store({ + name: 'download-manager', + defaults: { + downloadHistory: [], + } +}); + +class DownloadManager { + constructor() { + this.downloadHistory = store.get('downloadHistory', []); + } + + /** + * 转换下载项格式 + * @param {Electron.DownloadItem} downloadItem + */ + convertItem(downloadItem) { + return { + filename: downloadItem.getFilename(), + path: downloadItem.getSavePath(), + url: downloadItem.getURL(), + urls: downloadItem.getURLChain(), + mine: downloadItem.getMimeType(), + received: downloadItem.getReceivedBytes(), + total: downloadItem.getTotalBytes(), + percent: downloadItem.getPercentComplete(), + speed: downloadItem.getCurrentBytesPerSecond(), + state: downloadItem.getState(), + paused: downloadItem.isPaused(), + startTime: downloadItem.getStartTime(), + endTime: downloadItem.getEndTime(), + } + } + + /** + * 添加下载项 + * @param {Electron.DownloadItem} downloadItem + */ + addDownloadItem(downloadItem) { + this.downloadHistory.unshift({ + ...this.convertItem(downloadItem), + _source: downloadItem, + }); + if (this.downloadHistory.length > 1000) { + this.downloadHistory = this.downloadHistory.slice(0, 1000); + } + store.set('downloadHistory', this.downloadHistory); + loger.info(`Download item added: ${downloadItem.getSavePath()}`); + } + + /** + * 获取下载列表 + * @returns {*} + */ + getDownloadItems() { + return this.downloadHistory.map(item => { + return { + ...item, + _source: undefined, // 移除源对象,避免序列化问题 + }; + }); + } + + /** + * 更新下载项 + * @param {string} path + */ + updateDownloadItem(path) { + const item = this.downloadHistory.find(d => d.path === path) + if (!item) { + loger.warn(`Download item not found for path: ${path}`); + return; + } + const downloadItem = item._source; + if (!downloadItem) { + loger.warn(`Download item not found for path: ${path}`); + return; + } + Object.assign(item, this.convertItem(downloadItem)) + store.set('downloadHistory', this.downloadHistory); + loger.info(`Download item updated: ${path} - ${item.state} (${item.percent}%)`); + } + + /** + * 暂停下载项 + * @param {string} path + */ + pauseDownloadItem(path) { + const item = this.downloadHistory.find(d => d.path === path) + if (!item) { + loger.warn(`Download item not found for path: ${path}`); + return; + } + const downloadItem = item._source; + if (!downloadItem) { + loger.warn(`Download item not found for path: ${path}`); + return; + } + downloadItem.pause(); + this.updateDownloadItem(path); + } + + /** + * 恢复下载项 + * @param {string} path + */ + resumeDownloadItem(path) { + const item = this.downloadHistory.find(d => d.path === path) + if (!item) { + loger.warn(`Download item not found for path: ${path}`); + return; + } + const downloadItem = item._source; + if (!downloadItem) { + loger.warn(`Download item not found for path: ${path}`); + return; + } + downloadItem.resume(); + this.updateDownloadItem(path); + } + + /** + * 取消下载项 + * @param {string} path + */ + cancelDownloadItem(path) { + const item = this.downloadHistory.find(d => d.path === path) + if (!item) { + loger.warn(`Download item not found for path: ${path}`); + return; + } + const downloadItem = item._source; + if (!downloadItem) { + loger.warn(`Download item not found for path: ${path}`); + return; + } + downloadItem.cancel(); + this.updateDownloadItem(path); + } + + /** + * 取消所有下载项 + */ + cancelAllDownloadItems() { + this.downloadHistory.forEach(item => { + this.cancelDownloadItem(item.path); + }); + } + + /** + * 清空下载历史 + */ + clearHistory() { + this.downloadHistory = []; + store.set('downloadHistory', []); + } +} + +module.exports = { DownloadManager }; diff --git a/electron/utils.js b/electron/utils/index.js similarity index 100% rename from electron/utils.js rename to electron/utils/index.js diff --git a/language/original-web.txt b/language/original-web.txt index 115fdf6ed..1b3642d36 100644 --- a/language/original-web.txt +++ b/language/original-web.txt @@ -2150,3 +2150,4 @@ API URL 需要先设置 AI 助理 打开签到机器人 +下载内容 diff --git a/resources/assets/js/App.vue b/resources/assets/js/App.vue index 1961c13b6..56158f045 100755 --- a/resources/assets/js/App.vue +++ b/resources/assets/js/App.vue @@ -401,20 +401,14 @@ export default { if (this.isMeetingUrlStrict(url)) { return 1; } + // 同域名下载链接 + if (this.isDownloadUrl(url)) { + return 1; + } // 同域名规则 if ($A.getDomain(url) == $A.getDomain($A.mainUrl())) { try { const {pathname, searchParams} = new URL(url); - // uploads/ 上传文件 - // api/dialog/msg/download 会话文件 - // api/project/task/filedown 任务文件 - if (/^\/(uploads|api\/dialog\/msg\/download|api\/project\/task\/filedown)/.test(pathname)) { - return 1; - } - // api/file/content?down=yes 文件下载 - if (/^\/api\/file\/content/.test(pathname) && searchParams.get('down') === 'yes') { - return 1; - } // meeting/1234567890/xxxxx 会议 if (/^\/meeting\/\d+\/\S+$/.test(pathname)) { const meetingId = pathname.split('/')[2]; @@ -438,6 +432,32 @@ export default { return 0; }, + isDownloadUrl(url) { + if ($A.getDomain(url) == $A.getDomain($A.mainUrl())) { + try { + const {pathname, searchParams} = new URL(url); + // 匹配常见的下载相关路径 + const downloadPathPatterns = [ + '/uploads', // 上传文件 + '/api/dialog/msg/download', // 会话文件 + '/api/project/task/filedown', // 任务文件 + '/api/file/download/pack', // 文件打包下载 + '/api/approve/down', // 审批导出下载 + '/api/project/task/down', // 任务导出下载 + '/api/system/checkin/down' // 签到导出下载 + ]; + if (downloadPathPatterns.some(pattern => $A.leftExists(pathname, pattern))) { + return true; + } + // 匹配文件内容下载(/api/file/content 带参数 down=yes) + if ($A.leftExists(pathname, '/api/file/content') && searchParams.get('down') === 'yes') { + return true; + } + } catch (e) {} + } + return false; + }, + isApplicationProtocol(url) { const protocols = [ 'thunder:', // 迅雷专有链接 @@ -576,6 +596,10 @@ export default { return true; } else if (urlType === 1) { // 使用默认浏览器打开 + if (this.isDownloadUrl(url)) { + this.$store.dispatch('downUrl', url) + return true; + } return false; } // 使用内置浏览器打开 diff --git a/resources/assets/js/components/MicroApps/modal.vue b/resources/assets/js/components/MicroApps/modal.vue index a8080af78..ca2a8d04c 100644 --- a/resources/assets/js/components/MicroApps/modal.vue +++ b/resources/assets/js/components/MicroApps/modal.vue @@ -9,7 +9,7 @@