这一小节,我们将我们的程序改造成通知栏程序。
其实通知栏的程序就是去掉窗口的边框,然后定位到通知栏小图标的下面,通知栏是可以获得它的位置坐标的,我们可以基于这个坐标进行计算来获得。这个我们可以使用electron-positioner 来帮助我们进行计算,大家也可以参考 menubar 项目进行改造。
npm install electron-positioner --save
别忘记自己添加一下定义文件,electron-positioner.d.ts
declare module 'electron-positioner'
由于我们需要控制顺序,ready
里面的顺序分开来看不是特别明显,所以我们提取到 index.ts
中,我们需要自定义 Tray 小图标的一些单击,双击,右键的事件,当计算的距离是 tray
开头的时候,需要传入 tray.getBounds()
获取的小图标坐标点。
import Positioner from 'electron-positioner'
import { opensetting } from './tray'
let tray: Tray
let positioner: any
function setPostion(win: BrowserWindow) {
// 得到位置
positioner = new Positioner(win)
positioner.move('trayCenter', tray.getBounds())
win.show()
}
function createTray() {
// 创建通知栏图标
tray = new Tray(resolve(__dirname, 'tray_w24h24.png'))
const contextMenu = Menu.buildFromTemplate([
{ label: '设置', click: opensetting },
{
label: '退出',
role: 'quit'
}
])
const toggle = () => {
// 显示隐藏
if (!mainWindow) {
return
}
if (mainWindow.isVisible()) {
return mainWindow.hide()
}
positioner.move('trayCenter', tray.getBounds())
mainWindow.show()
}
tray.on('click', toggle) // 单击
tray.on('double-click', toggle) // 双击
tray.on('right-click', () => {
// 右键菜单
tray.popUpContextMenu(contextMenu)
})
}
async function ready() {
mainWindow = createMainWindow({
width: 400,
height: 560,
frame: false,
transparent: true,
show: false
})
createTray()
setPostion(mainWindow)
pluginSetUp()
crawlSetUp()
}
这个时候有一个小 bug , 所有的跳转按钮是可拖动的,我们需要阻止一下默认事件。
this.refs.link.addEventListener('dragstart', e => {
e.preventDefault()
})
我们需要一个状态来标记是否已经开始了队列。以及添加一个警告的方法。
const store = new Store({
currentPage: Main,
msg: {
type: 'success',
content: ''
},
start: false // 开始队列否
})
function warring(content, timer = 1000) {
store.set({
msg: {
type: 'warring',
content
}
})
setTimeout(resetMsg, timer) // 自动关闭消息
}
store.warring = warring.bind(store)
添加模板逻辑,访问全局状态,以 $ 开头。需要一个 canvas 来画进度框。包裹一层是为了居中,一定要在属性上面给确定的大小,要不然会画出来的东西就看不到了。对于 canvas 我也有录制过视屏,在这里,不过不是免费的内容。
canvas
一定要通过 css 控制显隐,要不然会很难操作,动态挂载生命周期极其难控制。
<Back/>
<div class="wrap">
{#if !$start}
<label>文件名</label>
<input bind:value=folderName />
<label>爬取网址</label>
<input bind:value=url />
{/if}
<div class="oprator">
{#if !$start}
<button on:click="download()">下载</button>
{:else}
<button on:click="stop()">中断</button>
{/if}
</div>
{#if status.type }
<div class="type">
{status.type == 'crawl' && status.step == 'chapter' ? '爬取章节': ''}
{status.type == 'crawl' && status.step == 'text'? '爬取内容': ''}
{status.type == 'audio'? '转换音频': ''}
</div>
{/if}
{#if status.title}
<div class="title">
{status.title}
</div>
{/if}
<div class="box {$start?'show':''}">
<canvas ref:canvas id="canvas" width="250" height="250"></canvas>
</div>
</div>
定义了颜色 color
它代表每一阶段进度条的颜色,然后在 oncreate 的时候,绘制 canvas。
这里我对所有的错误相关的进行了重构,错误消息都是 type: 'error'
的结构。当中止队列的时候不要忘记清空状态。once
是用来控制消息提示只调用一次,调用多次会出现 Bug,暂时无法找到何处出了问题,猜测是内置动画的原因。
对于绘制进度条,使用了两个圆,通过 arc
API 进行了绘制,然后通过 requestAnimationFrame
逐帧绘制。每次绘制都会去状态里面取最新的值。
对于百分比其实很简单,2 * PI / 100
就是百分之一 ,乘以百分比即可。为了保证画布的干净每次都需要 clearRect
<script>
import { ipcRenderer } from "electron";
const color = {
text: "#00CC99",
audio: "#3399CC",
stop: "#FF3333"
};
export default {
data() {
return {
url: "https://www.ybdu.com/xiaoshuo/0/910/",
folderName: "123",
status: {
type: " ",
title: " ",
percent: 0
}
};
},
components: {
Back: "../components/Back.svelte"
},
oncreate() {
let once = false;
ipcRenderer.on("download-status", (event, args) => { // 接受下载状态,并渲染到 canvas
if (
(args.typw == "audio" && args.percent == 100) ||
args.type == "stop"
) {
this.set({
status: {
type: "",
percent: 0
}
});
this.store.set({ start: false });
if (once) {
once = false;
return;
}
this.store.success(args.message);
once = true;
return;
}
if (args.type == "error") {
this.store.warring(args.message);
this.store.set({ start: false });
return;
}
this.set({
status: args
});
this.store.set({
start: true
});
});
this.draw();
const drawFrame = () => {
if (!this) {
return;
}
this.context.clearRect(0, 0, this.centerX * 2, this.centerY * 2);
this.text();
this.whiteCircle();
this.blueCircle();
this && window.requestAnimationFrame(drawFrame); // 每帧自动渲染
};
drawFrame();
},
ondestroy() {
ipcRenderer.removeAllListeners("download-status");
},
methods: {
download() {
const { url, folderName } = this.get();
if (url.length && folderName.length) {
ipcRenderer.send("download", {
url,
folderName
});
this.store.set({ start: true });
}
},
stop() {
ipcRenderer.send("stop");
},
draw() {
let canvas = document.querySelector("#canvas");
this.context = canvas.getContext("2d");
this.centerX = canvas.width / 2;
this.centerY = canvas.height / 2;
this.rad = Math.PI * 2 / 100;
},
text() {
const { status } = this.get();
if (!status) {
return;
}
this.context.save(); // 保存之前的转态。
this.context.fillStyle = "#888";
this.context.font = "40px Arial";
this.context.textAlign = "center";
this.context.textBaseline = "middle";
this.context.fillText(status.percent + "%", this.centerX, this.centerY);
this.context.restore(); // 恢复之前的转态。
},
blueCircle() { // 上层进度全
const { status } = this.get();
if (!status) {
return;
}
this.context.save();
this.context.beginPath();
this.context.strokeStyle = color[status.step] || "#9900FF"; // 绘制颜色
this.context.lineWidth = 12;
this.context.arc( // 绘制圆
this.centerX,
this.centerY,
100,
-Math.PI / 2,
-Math.PI / 2 + status.percent * this.rad,
false
);
this.context.stroke();
this.context.restore();
},
whiteCircle() { // 底层白圈
this.context.save();
this.context.beginPath();
this.context.strokeStyle = "#383a41";
this.context.lineWidth = 12;
this.context.arc(this.centerX, this.centerY, 100, 0, Math.PI * 2, false);
this.context.stroke();
this.context.closePath();
this.context.restore();
}
}
};
</script>
有的时候会报 getaddrinfo ENOTFOUND
错误,我们添加一个重试的机制。
const getMp3Data: any = async (text: string, opts: any = {}) => {
const textArr = splitText(text)
return Promise.all(
textArr.map(async chunk => {
const { data } = await client.text2audio(chunk, opts)
return data
})
).catch(() => getMp3Data(text, opts))
}
再添加一个等待机制,自行到 tray.ts
里面添加配置哦,这样可以减小网络出错的概率。
await timer(TTS_WAIT).toPromise()
在测试的时候,发现 总是有的时候会遇见 mp3-concat
有错误,会弹出选项框,这就很不友好,但是实际内容是没有丢失的,我们可以直接忽略掉它,修改 index.ts
process.on('unhandledRejection', console.log)
process.on('uncaughtException', console.log)