diff --git a/cmd b/cmd index 8aaa20981..ac702e757 100755 --- a/cmd +++ b/cmd @@ -168,15 +168,12 @@ run_electron() { rm -rf "./electron/public" fi # + BUILD_FRONTEND="build" if [ "$argv" == "dev" ]; then switch_debug "$argv" - else - mkdir -p ./electron/public - cp ./electron/index.html ./electron/public/index.html - npx vite build -- fromcmd electronBuild - echo "" + BUILD_FRONTEND="dev" fi - node ./electron/build.js $argv + env BUILD_FRONTEND=$BUILD_FRONTEND node ./electron/build.js $argv } run_exec() { diff --git a/electron/.gitignore b/electron/.gitignore index 4ba7a076a..2e2add236 100644 --- a/electron/.gitignore +++ b/electron/.gitignore @@ -6,6 +6,7 @@ package-lock.json build/ dist/ updater/* +cache/* .devload .native diff --git a/electron/build.js b/electron/build.js index b364102c4..ad99e6d7b 100644 --- a/electron/build.js +++ b/electron/build.js @@ -11,7 +11,7 @@ const utils = require('./utils'); const config = require('../package.json') const env = require('dotenv').config({ path: './.env' }) const argv = process.argv; -const {APPLEID, APPLEIDPASS, GITHUB_TOKEN, GITHUB_REPOSITORY, PUBLISH_KEY} = process.env; +const {BUILD_FRONTEND, APPLEID, APPLEIDPASS, GITHUB_TOKEN, GITHUB_REPOSITORY, PUBLISH_KEY} = process.env; const electronDir = path.resolve(__dirname, "public"); const nativeCachePath = path.resolve(__dirname, ".native"); @@ -21,6 +21,9 @@ const packageBakFile = path.resolve(__dirname, "package-bak.json"); const platforms = ["build-mac", "build-win"]; const architectures = ["arm64", "x64"]; +let buildChecked = false, + updaterChecked = false; + /** * 检测并下载更新器 */ @@ -459,6 +462,13 @@ function genericPublish({url, key, version, output}) { * @param data */ async function startBuild(data) { + if (BUILD_FRONTEND === 'build' && !buildChecked) { + buildChecked = true + fs.mkdirSync(electronDir, { recursive: true }); + fse.copySync(path.resolve(__dirname, "index.html"), path.resolve(electronDir, "index.html")) + child_process.spawnSync("npx", ["vite", "build", "--", "fromcmd", "electronBuild"], {stdio: "inherit"}); + } + // const {platform, archs, publish, release, notarize} = data.configure // system info const systemInfo = { @@ -493,7 +503,10 @@ async function startBuild(data) { // drawio cloneDrawio(systemInfo) // updater - await detectAndDownloadUpdater() + if (!updaterChecked) { + updaterChecked = true + await detectAndDownloadUpdater() + } } // language fse.copySync(path.resolve(__dirname, "../public/language"), path.resolve(electronDir, "language")) @@ -569,6 +582,7 @@ async function startBuild(data) { if (appName === "public") appName = "DooTask" appConfig.name = data.name; appConfig.version = config.version; + appConfig.appId = data.id; appConfig.build.appId = data.id; appConfig.build.artifactName = appName + "-v${version}-${os}-${arch}.${ext}"; appConfig.build.nsis.artifactName = appName + "-v${version}-${os}-${arch}.${ext}"; diff --git a/electron/electron.js b/electron/electron.js index 95d253582..61eeb8676 100644 --- a/electron/electron.js +++ b/electron/electron.js @@ -20,7 +20,8 @@ const isMac = process.platform === 'darwin' const isWin = process.platform === 'win32' const allowedUrls = /^(?:https?|mailto|tel|callto):/i; const allowedCalls = /^(?:mailto|tel|callto):/i; -let updaterLockFile = path.join(os.tmpdir(), '.dootask_updater.lock'); +const cacheDir = path.join(os.tmpdir(), 'dootask-cache') +let updaterLockFile = path.join(cacheDir, '.dootask_updater.lock'); let enableStoreBkp = true; let dialogOpen = false; let enablePlugins = false; @@ -53,6 +54,10 @@ if (fs.existsSync(devloadCachePath)) { devloadUrl = fs.readFileSync(devloadCachePath, 'utf8') } +if (!fs.existsSync(cacheDir)) { + fs.mkdirSync(cacheDir, { recursive: true }); +} + // 在最开始就注册协议为特权协议 protocol.registerSchemesAsPrivileged([ { @@ -85,7 +90,7 @@ function createProtocol() { } const data = await fs.promises.readFile(filePath) - const mimeType = getMimeType(filePath) + const mimeType = utils.getMimeType(filePath) return new Response(data, { headers: { @@ -99,24 +104,6 @@ function createProtocol() { }) } -/** - * MIME类型判断 - * @param filePath - * @returns {*|string} - */ -function getMimeType(filePath) { - const ext = path.extname(filePath).toLowerCase() - const mimeTypes = { - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.gif': 'image/gif', - '.svg': 'image/svg+xml', - '.webp': 'image/webp' - } - return mimeTypes[ext] || 'application/octet-stream' -} - /** * 创建主窗口 */ @@ -201,7 +188,7 @@ function createUpdaterWindow(updateTitle) { } else { updaterPath = path.join(process.resourcesPath, 'updater', 'updater'); } - + // 检查updater应用是否存在 if (!fs.existsSync(updaterPath)) { console.log('Updater not found:', updaterPath); @@ -222,7 +209,7 @@ function createUpdaterWindow(updateTitle) { try { spawn('chmod', ['+x', updaterPath], {stdio: 'inherit'}); } catch (e) { - console.log('Failed to set executable permission:', e); + console.log('Failed to set executable permission:', e); } } } @@ -728,6 +715,7 @@ if (!getTheLock) { }) app.on('ready', () => { isReady = true + isWin && app.setAppUserModelId(config.appId) // SameSite utils.useCookie() // 创建协议 @@ -756,7 +744,7 @@ if (!getTheLock) { mainTray.setContextMenu(trayMenu) } } - // 删除updater锁文件(如果存在) + // 删除updater锁文件(如果存在) if (fs.existsSync(updaterLockFile)) { try { fs.unlinkSync(updaterLockFile); @@ -764,10 +752,6 @@ if (!getTheLock) { //忽略错误 } } - // - if (process.platform === 'win32') { - app.setAppUserModelId(config.name) - } // 截图对象 screenshotObj = new Screenshots({ singleWindow: true, @@ -1150,7 +1134,7 @@ ipcMain.on('copyImageAt', (event, args) => { try { event.sender.copyImageAt(args.x, args.y); } catch (e) { - // log.error(e) + // loger.error(e) } event.returnValue = "ok" }) @@ -1212,14 +1196,7 @@ ipcMain.on('closeScreenshot', (event) => { * 通知 */ ipcMain.on('openNotification', (event, args) => { - const notifiy = new Notification(args); - notifiy.addListener('click', _ => { - mainWindow.webContents.send("clickNotification", args) - }) - notifiy.addListener('reply', (event, reply) => { - mainWindow.webContents.send("replyNotification", Object.assign(args, {reply})) - }) - notifiy.show() + utils.showNotification(args, mainWindow) event.returnValue = "ok" }) @@ -1306,13 +1283,11 @@ ipcMain.on('updateQuitAndInstall', (event, args) => { // 启动更新子窗口 createUpdaterWindow(args.updateTitle) - // 隐藏主窗口 - mainWindow.hide() - // 退出并安装更新 setTimeout(_ => { + mainWindow.hide() autoUpdater.quitAndInstall(true, true) - }, 300) + }, 600) }) //================================================================ diff --git a/electron/utils.js b/electron/utils.js index eb52d5daf..79595b157 100644 --- a/electron/utils.js +++ b/electron/utils.js @@ -1,8 +1,14 @@ const fs = require("fs"); +const os = require("os"); +const path = require('path') const dayjs = require("dayjs"); -const {shell, dialog, session} = require("electron"); +const http = require('http') +const https = require('https') +const crypto = require('crypto') +const {shell, dialog, session, Notification} = require("electron"); +const loger = require("electron-log"); -module.exports = { +const utils = { /** * 时间对象 * @param v @@ -160,7 +166,7 @@ module.exports = { leftDelete(string, find, lower = false) { string += ""; find += ""; - if (this.leftExists(string, find, lower)) { + if (utils.leftExists(string, find, lower)) { string = string.substring(find.length) } return string ? string : ''; @@ -185,33 +191,33 @@ module.exports = { /** * 打开文件 - * @param path + * @param filePath */ - openFile(path) { - if (!fs.existsSync(path)) { + openFile(filePath) { + if (!fs.existsSync(filePath)) { return } - shell.openPath(path).then(() => { + shell.openPath(filePath).then(() => { }) }, /** * 删除文件夹及文件 - * @param path + * @param filePath */ - deleteFile(path) { + deleteFile(filePath) { let files = []; - if (fs.existsSync(path)) { - files = fs.readdirSync(path); - files.forEach(function (file, index) { - let curPath = path + "/" + file; + if (fs.existsSync(filePath)) { + files = fs.readdirSync(filePath); + files.forEach(function (file) { + let curPath = filePath + "/" + file; if (fs.statSync(curPath).isDirectory()) { - deleteFile(curPath); + utils.deleteFile(curPath); } else { fs.unlinkSync(curPath); } }); - fs.rmdirSync(path); + fs.rmdirSync(filePath); } }, @@ -225,14 +231,14 @@ module.exports = { let rs = fs.createReadStream(srcPath) rs.on('error', function (err) { if (err) { - console.log('read error', srcPath) + loger.log('read error', srcPath) } cb && cb(err) }) let ws = fs.createWriteStream(tarPath) ws.on('error', function (err) { if (err) { - console.log('write error', tarPath) + loger.log('write error', tarPath) } cb && cb(err) }) @@ -296,7 +302,7 @@ module.exports = { const contents = app.webContents if (contents != null) { contents.executeJavaScript('if(typeof window.__onBeforeUnload === \'function\'){window.__onBeforeUnload()}', true).then(options => { - if (this.isJson(options)) { + if (utils.isJson(options)) { let choice = dialog.showMessageBoxSync(app, options) if (choice === 1) { contents.executeJavaScript('if(typeof window.__removeBeforeUnload === \'function\'){window.__removeBeforeUnload()}', true).catch(() => {}); @@ -414,7 +420,7 @@ module.exports = { * electron15 后,解决跨域cookie无法携带, */ useCookie() { - const filter = {urls: ['https://*/*']}; + const filter = {urls: ['https://*/*', 'http://*/*']}; session.defaultSession.webRequest.onHeadersReceived(filter, (details, callback) => { if (details.responseHeaders && details.responseHeaders['Set-Cookie']) { for (let i = 0; i < details.responseHeaders['Set-Cookie'].length; i++) { @@ -436,5 +442,170 @@ module.exports = { } else { return input.meta } - } + }, + + /** + * MIME类型判断 + * @param filePath + * @returns {*|string} + */ + getMimeType(filePath) { + const ext = path.extname(filePath).toLowerCase() + const mimeTypes = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.svg': 'image/svg+xml', + '.webp': 'image/webp' + } + return mimeTypes[ext] || 'application/octet-stream' + }, + + /** + * 显示系统通知 + * @param {Object} args - 通知参数 + * @param {string} args.title - 通知标题 + * @param {string} args.body - 通知内容 + * @param {string} [args.icon] - 通知图标路径或URL + * @param {Electron.BrowserWindow} [window] - 主窗口实例 + * @returns {Promise} + */ + async showNotification(args, window = null) { + try { + // 如果是网络图片,进行缓存处理(仅Windows) + if (process.platform === 'win32' && args.icon && /^https?:\/\//i.test(args.icon)) { + args.icon = await utils.getCachedImage(args.icon); + } + + const notifiy = new Notification(args); + notifiy.addListener('click', _ => { + if (window && window.webContents) { + window.webContents.send("clickNotification", args) + if (!window.isVisible()) { + window.show(); + } + window.focus(); + } + }) + notifiy.addListener('reply', (event, reply) => { + if (window && window.webContents) { + window.webContents.send("replyNotification", Object.assign(args, {reply})) + } + }) + notifiy.show() + } catch (error) { + loger.error('显示通知失败:', error); + } + }, + + /** + * 获取缓存的图片路径 + * @param {string} imageUrl - 图片URL + * @returns {Promise} 缓存的图片路径 + */ + async getCachedImage(imageUrl) { + // 生成图片URL的唯一标识 + const urlHash = crypto.createHash('md5').update(imageUrl).digest('hex'); + const cacheDir = path.join(os.tmpdir(), 'dootask-cache', 'images'); + const cachePath = path.join(cacheDir, `${urlHash}.png`); + + try { + // 确保缓存目录存在 + if (!fs.existsSync(cacheDir)) { + fs.mkdirSync(cacheDir, { recursive: true }); + } + + // 检查缓存是否存在 + if (!fs.existsSync(cachePath)) { + await utils.downloadImage(imageUrl, cachePath); + } + + return cachePath; + } catch (error) { + loger.error('处理缓存图片失败:', error); + return ''; // 返回空字符串,通知将使用默认图标 + } + }, + + /** + * 下载图片 + * @param {string} url - 图片URL + * @param {string} filePath - 保存路径 + * @returns {Promise} + */ + downloadImage(url, filePath) { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(filePath); + + // 根据协议选择http或https + const protocol = url.startsWith('https') ? https : http; + + const request = protocol.get(url, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + }, (response) => { + // 处理重定向 + if (response.statusCode === 301 || response.statusCode === 302) { + file.close(); + fs.unlink(filePath, () => {}); + return utils.downloadImage(response.headers.location, filePath) + .then(resolve) + .catch(reject); + } + + // 检查内容类型 + const contentType = response.headers['content-type']; + if (!contentType || !contentType.startsWith('image/')) { + file.close(); + fs.unlink(filePath, () => {}); + reject(new Error(`非图片类型: ${contentType}`)); + return; + } + + if (response.statusCode !== 200) { + file.close(); + fs.unlink(filePath, () => {}); + reject(new Error(`下载失败,状态码: ${response.statusCode}`)); + return; + } + + let downloadedBytes = 0; + response.on('data', (chunk) => { + downloadedBytes += chunk.length; + }); + + response.pipe(file); + + file.on('finish', () => { + // 检查文件大小 + if (downloadedBytes === 0) { + file.close(); + fs.unlink(filePath, () => {}); + reject(new Error('下载的文件大小为0')); + return; + } + file.close(); + resolve(); + }); + }); + + request.on('error', (err) => { + file.close(); + fs.unlink(filePath, () => {}); + reject(err); + }); + + // 设置超时 + request.setTimeout(30000, () => { + request.destroy(); + file.close(); + fs.unlink(filePath, () => {}); + reject(new Error('下载超时')); + }); + }); + }, } + +module.exports = utils; diff --git a/resources/assets/js/pages/manage.vue b/resources/assets/js/pages/manage.vue index 10ba122a4..49d7dafd2 100644 --- a/resources/assets/js/pages/manage.vue +++ b/resources/assets/js/pages/manage.vue @@ -1075,32 +1075,26 @@ export default { const body = $A.getMsgSimpleDesc(data); this.__notificationId = id; // - const notificationFuncA = (title) => { - if (dialog_type === 'group') { - let tempUser = this.cacheUserBasic.find(item => item.userid == userid); - if (tempUser) { - notificationFuncB(`${title} (${tempUser.nickname})`) - } else { - this.$store.dispatch("call", { + const notificationFuncA = async (title) => { + let tempUser = this.cacheUserBasic.find(item => item.userid == userid); + if (!tempUser) { + try { + const {data} = await this.$store.dispatch("call", { url: 'users/basic', data: { userid: [userid] }, skipAuthError: true - }).then(({data}) => { - tempUser = data.find(item => item.userid == userid); - if (tempUser) { - notificationFuncB(`${title} (${tempUser.nickname})`) - } - }).catch(_ => { - notificationFuncB(title) }); - } - } else { - notificationFuncB(title) + tempUser = data.find(item => item.userid == userid); + } catch (_) {} } + if (dialog_type === 'group' && tempUser) { + title = `${title} (${tempUser.nickname})` + } + notificationFuncB(title, tempUser?.userimg) } - const notificationFuncB = (title) => { + const notificationFuncB = (title, userimg) => { if (this.__notificationId === id) { this.__notificationId = null if (this.$isEEUiApp) { @@ -1115,7 +1109,7 @@ export default { }) } else if (this.$Electron) { this.$Electron.sendMessage('openNotification', { - icon: $A.originUrl('images/logo.png'), + icon: userimg || $A.originUrl('images/logo.png'), title, body, data, @@ -1125,7 +1119,7 @@ export default { }) } else { this.notificationManage.replaceOptions({ - icon: $A.originUrl('images/logo.png'), + icon: userimg || $A.originUrl('images/logo.png'), body: body, data: data, tag: "dialog",