perf: 优化桌面端通知图标

This commit is contained in:
kuaifan 2024-11-17 19:34:13 +08:00
parent 694f9a37a5
commit 07a290dbf9
6 changed files with 240 additions and 88 deletions

9
cmd
View File

@ -168,15 +168,12 @@ run_electron() {
rm -rf "./electron/public" rm -rf "./electron/public"
fi fi
# #
BUILD_FRONTEND="build"
if [ "$argv" == "dev" ]; then if [ "$argv" == "dev" ]; then
switch_debug "$argv" switch_debug "$argv"
else BUILD_FRONTEND="dev"
mkdir -p ./electron/public
cp ./electron/index.html ./electron/public/index.html
npx vite build -- fromcmd electronBuild
echo ""
fi fi
node ./electron/build.js $argv env BUILD_FRONTEND=$BUILD_FRONTEND node ./electron/build.js $argv
} }
run_exec() { run_exec() {

1
electron/.gitignore vendored
View File

@ -6,6 +6,7 @@ package-lock.json
build/ build/
dist/ dist/
updater/* updater/*
cache/*
.devload .devload
.native .native

18
electron/build.js vendored
View File

@ -11,7 +11,7 @@ const utils = require('./utils');
const config = require('../package.json') const config = require('../package.json')
const env = require('dotenv').config({ path: './.env' }) const env = require('dotenv').config({ path: './.env' })
const argv = process.argv; 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 electronDir = path.resolve(__dirname, "public");
const nativeCachePath = path.resolve(__dirname, ".native"); 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 platforms = ["build-mac", "build-win"];
const architectures = ["arm64", "x64"]; const architectures = ["arm64", "x64"];
let buildChecked = false,
updaterChecked = false;
/** /**
* 检测并下载更新器 * 检测并下载更新器
*/ */
@ -459,6 +462,13 @@ function genericPublish({url, key, version, output}) {
* @param data * @param data
*/ */
async function startBuild(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 const {platform, archs, publish, release, notarize} = data.configure
// system info // system info
const systemInfo = { const systemInfo = {
@ -493,7 +503,10 @@ async function startBuild(data) {
// drawio // drawio
cloneDrawio(systemInfo) cloneDrawio(systemInfo)
// updater // updater
await detectAndDownloadUpdater() if (!updaterChecked) {
updaterChecked = true
await detectAndDownloadUpdater()
}
} }
// language // language
fse.copySync(path.resolve(__dirname, "../public/language"), path.resolve(electronDir, "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" if (appName === "public") appName = "DooTask"
appConfig.name = data.name; appConfig.name = data.name;
appConfig.version = config.version; appConfig.version = config.version;
appConfig.appId = data.id;
appConfig.build.appId = data.id; appConfig.build.appId = data.id;
appConfig.build.artifactName = appName + "-v${version}-${os}-${arch}.${ext}"; appConfig.build.artifactName = appName + "-v${version}-${os}-${arch}.${ext}";
appConfig.build.nsis.artifactName = appName + "-v${version}-${os}-${arch}.${ext}"; appConfig.build.nsis.artifactName = appName + "-v${version}-${os}-${arch}.${ext}";

51
electron/electron.js vendored
View File

@ -20,7 +20,8 @@ const isMac = process.platform === 'darwin'
const isWin = process.platform === 'win32' const isWin = process.platform === 'win32'
const allowedUrls = /^(?:https?|mailto|tel|callto):/i; const allowedUrls = /^(?:https?|mailto|tel|callto):/i;
const allowedCalls = /^(?: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 enableStoreBkp = true;
let dialogOpen = false; let dialogOpen = false;
let enablePlugins = false; let enablePlugins = false;
@ -53,6 +54,10 @@ if (fs.existsSync(devloadCachePath)) {
devloadUrl = fs.readFileSync(devloadCachePath, 'utf8') devloadUrl = fs.readFileSync(devloadCachePath, 'utf8')
} }
if (!fs.existsSync(cacheDir)) {
fs.mkdirSync(cacheDir, { recursive: true });
}
// 在最开始就注册协议为特权协议 // 在最开始就注册协议为特权协议
protocol.registerSchemesAsPrivileged([ protocol.registerSchemesAsPrivileged([
{ {
@ -85,7 +90,7 @@ function createProtocol() {
} }
const data = await fs.promises.readFile(filePath) const data = await fs.promises.readFile(filePath)
const mimeType = getMimeType(filePath) const mimeType = utils.getMimeType(filePath)
return new Response(data, { return new Response(data, {
headers: { 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'
}
/** /**
* 创建主窗口 * 创建主窗口
*/ */
@ -728,6 +715,7 @@ if (!getTheLock) {
}) })
app.on('ready', () => { app.on('ready', () => {
isReady = true isReady = true
isWin && app.setAppUserModelId(config.appId)
// SameSite // SameSite
utils.useCookie() utils.useCookie()
// 创建协议 // 创建协议
@ -756,7 +744,7 @@ if (!getTheLock) {
mainTray.setContextMenu(trayMenu) mainTray.setContextMenu(trayMenu)
} }
} }
// 删除updater锁文件(如果存在) // 删除updater锁文件(如果存在)
if (fs.existsSync(updaterLockFile)) { if (fs.existsSync(updaterLockFile)) {
try { try {
fs.unlinkSync(updaterLockFile); fs.unlinkSync(updaterLockFile);
@ -764,10 +752,6 @@ if (!getTheLock) {
//忽略错误 //忽略错误
} }
} }
//
if (process.platform === 'win32') {
app.setAppUserModelId(config.name)
}
// 截图对象 // 截图对象
screenshotObj = new Screenshots({ screenshotObj = new Screenshots({
singleWindow: true, singleWindow: true,
@ -1150,7 +1134,7 @@ ipcMain.on('copyImageAt', (event, args) => {
try { try {
event.sender.copyImageAt(args.x, args.y); event.sender.copyImageAt(args.x, args.y);
} catch (e) { } catch (e) {
// log.error(e) // loger.error(e)
} }
event.returnValue = "ok" event.returnValue = "ok"
}) })
@ -1212,14 +1196,7 @@ ipcMain.on('closeScreenshot', (event) => {
* 通知 * 通知
*/ */
ipcMain.on('openNotification', (event, args) => { ipcMain.on('openNotification', (event, args) => {
const notifiy = new Notification(args); utils.showNotification(args, mainWindow)
notifiy.addListener('click', _ => {
mainWindow.webContents.send("clickNotification", args)
})
notifiy.addListener('reply', (event, reply) => {
mainWindow.webContents.send("replyNotification", Object.assign(args, {reply}))
})
notifiy.show()
event.returnValue = "ok" event.returnValue = "ok"
}) })
@ -1306,13 +1283,11 @@ ipcMain.on('updateQuitAndInstall', (event, args) => {
// 启动更新子窗口 // 启动更新子窗口
createUpdaterWindow(args.updateTitle) createUpdaterWindow(args.updateTitle)
// 隐藏主窗口
mainWindow.hide()
// 退出并安装更新 // 退出并安装更新
setTimeout(_ => { setTimeout(_ => {
mainWindow.hide()
autoUpdater.quitAndInstall(true, true) autoUpdater.quitAndInstall(true, true)
}, 300) }, 600)
}) })
//================================================================ //================================================================

211
electron/utils.js vendored
View File

@ -1,8 +1,14 @@
const fs = require("fs"); const fs = require("fs");
const os = require("os");
const path = require('path')
const dayjs = require("dayjs"); 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 * @param v
@ -160,7 +166,7 @@ module.exports = {
leftDelete(string, find, lower = false) { leftDelete(string, find, lower = false) {
string += ""; string += "";
find += ""; find += "";
if (this.leftExists(string, find, lower)) { if (utils.leftExists(string, find, lower)) {
string = string.substring(find.length) string = string.substring(find.length)
} }
return string ? string : ''; return string ? string : '';
@ -185,33 +191,33 @@ module.exports = {
/** /**
* 打开文件 * 打开文件
* @param path * @param filePath
*/ */
openFile(path) { openFile(filePath) {
if (!fs.existsSync(path)) { if (!fs.existsSync(filePath)) {
return return
} }
shell.openPath(path).then(() => { shell.openPath(filePath).then(() => {
}) })
}, },
/** /**
* 删除文件夹及文件 * 删除文件夹及文件
* @param path * @param filePath
*/ */
deleteFile(path) { deleteFile(filePath) {
let files = []; let files = [];
if (fs.existsSync(path)) { if (fs.existsSync(filePath)) {
files = fs.readdirSync(path); files = fs.readdirSync(filePath);
files.forEach(function (file, index) { files.forEach(function (file) {
let curPath = path + "/" + file; let curPath = filePath + "/" + file;
if (fs.statSync(curPath).isDirectory()) { if (fs.statSync(curPath).isDirectory()) {
deleteFile(curPath); utils.deleteFile(curPath);
} else { } else {
fs.unlinkSync(curPath); fs.unlinkSync(curPath);
} }
}); });
fs.rmdirSync(path); fs.rmdirSync(filePath);
} }
}, },
@ -225,14 +231,14 @@ module.exports = {
let rs = fs.createReadStream(srcPath) let rs = fs.createReadStream(srcPath)
rs.on('error', function (err) { rs.on('error', function (err) {
if (err) { if (err) {
console.log('read error', srcPath) loger.log('read error', srcPath)
} }
cb && cb(err) cb && cb(err)
}) })
let ws = fs.createWriteStream(tarPath) let ws = fs.createWriteStream(tarPath)
ws.on('error', function (err) { ws.on('error', function (err) {
if (err) { if (err) {
console.log('write error', tarPath) loger.log('write error', tarPath)
} }
cb && cb(err) cb && cb(err)
}) })
@ -296,7 +302,7 @@ module.exports = {
const contents = app.webContents const contents = app.webContents
if (contents != null) { if (contents != null) {
contents.executeJavaScript('if(typeof window.__onBeforeUnload === \'function\'){window.__onBeforeUnload()}', true).then(options => { 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) let choice = dialog.showMessageBoxSync(app, options)
if (choice === 1) { if (choice === 1) {
contents.executeJavaScript('if(typeof window.__removeBeforeUnload === \'function\'){window.__removeBeforeUnload()}', true).catch(() => {}); contents.executeJavaScript('if(typeof window.__removeBeforeUnload === \'function\'){window.__removeBeforeUnload()}', true).catch(() => {});
@ -414,7 +420,7 @@ module.exports = {
* electron15 解决跨域cookie无法携带 * electron15 解决跨域cookie无法携带
*/ */
useCookie() { useCookie() {
const filter = {urls: ['https://*/*']}; const filter = {urls: ['https://*/*', 'http://*/*']};
session.defaultSession.webRequest.onHeadersReceived(filter, (details, callback) => { session.defaultSession.webRequest.onHeadersReceived(filter, (details, callback) => {
if (details.responseHeaders && details.responseHeaders['Set-Cookie']) { if (details.responseHeaders && details.responseHeaders['Set-Cookie']) {
for (let i = 0; i < details.responseHeaders['Set-Cookie'].length; i++) { for (let i = 0; i < details.responseHeaders['Set-Cookie'].length; i++) {
@ -436,5 +442,170 @@ module.exports = {
} else { } else {
return input.meta 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<void>}
*/
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<string>} 缓存的图片路径
*/
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<void>}
*/
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;

View File

@ -1075,32 +1075,26 @@ export default {
const body = $A.getMsgSimpleDesc(data); const body = $A.getMsgSimpleDesc(data);
this.__notificationId = id; this.__notificationId = id;
// //
const notificationFuncA = (title) => { const notificationFuncA = async (title) => {
if (dialog_type === 'group') { let tempUser = this.cacheUserBasic.find(item => item.userid == userid);
let tempUser = this.cacheUserBasic.find(item => item.userid == userid); if (!tempUser) {
if (tempUser) { try {
notificationFuncB(`${title} (${tempUser.nickname})`) const {data} = await this.$store.dispatch("call", {
} else {
this.$store.dispatch("call", {
url: 'users/basic', url: 'users/basic',
data: { data: {
userid: [userid] userid: [userid]
}, },
skipAuthError: true skipAuthError: true
}).then(({data}) => {
tempUser = data.find(item => item.userid == userid);
if (tempUser) {
notificationFuncB(`${title} (${tempUser.nickname})`)
}
}).catch(_ => {
notificationFuncB(title)
}); });
} tempUser = data.find(item => item.userid == userid);
} else { } catch (_) {}
notificationFuncB(title)
} }
if (dialog_type === 'group' && tempUser) {
title = `${title} (${tempUser.nickname})`
}
notificationFuncB(title, tempUser?.userimg)
} }
const notificationFuncB = (title) => { const notificationFuncB = (title, userimg) => {
if (this.__notificationId === id) { if (this.__notificationId === id) {
this.__notificationId = null this.__notificationId = null
if (this.$isEEUiApp) { if (this.$isEEUiApp) {
@ -1115,7 +1109,7 @@ export default {
}) })
} else if (this.$Electron) { } else if (this.$Electron) {
this.$Electron.sendMessage('openNotification', { this.$Electron.sendMessage('openNotification', {
icon: $A.originUrl('images/logo.png'), icon: userimg || $A.originUrl('images/logo.png'),
title, title,
body, body,
data, data,
@ -1125,7 +1119,7 @@ export default {
}) })
} else { } else {
this.notificationManage.replaceOptions({ this.notificationManage.replaceOptions({
icon: $A.originUrl('images/logo.png'), icon: userimg || $A.originUrl('images/logo.png'),
body: body, body: body,
data: data, data: data,
tag: "dialog", tag: "dialog",