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"
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() {

1
electron/.gitignore vendored
View File

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

18
electron/build.js vendored
View File

@ -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}";

55
electron/electron.js vendored
View File

@ -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)
})
//================================================================

211
electron/utils.js vendored
View File

@ -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<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);
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",