章节内容
历史介绍
开发环境
代码封装
函数式
获取小说命令行版(上)
获取小说命令行版(中)
获取小说命令行版(下)
嵌入到应用中
插件机制
文字转语音
设置与信号中断
插件商店
下载与删除(上)
下载与删除(下)
通知栏程序
播放功能
美化界面
数据可视化(上)
数据可视化(中)
数据可视化(下)
嵌入到应用中
打包应用

这一个章节会用到我写的一个插件,electron-auto-setting 当我们传递配置项,它可以自动生成页面。

它的原理其实就是从配置项里面读取到表单的选项,构造出表单,然后当修改的时候,通过统一的 IPC 信道,将修改的 key 和 value 发送回去。

# 安装依赖

npm install electron-auto-setting --save

# 新建定义文件

declare module 'electron-auto-setting' {
  const all: any
  const init: any
  const store: any
  export { init, store }
  export default all
}

store 的方法全部来自于 electron-store

# 实现逻辑

新建 tray.ts 文件,将配置项传递给 init,而 create 则是创建窗口的函数,第一个是参数配置项,第二个参数是是否开启 DEBUG 模式,这里我们开启它。

import create, { init, store } from 'electron-auto-setting'
import { app, Tray, Menu } from 'electron'
import { resolve } from 'path'
import { fromEvent } from 'rxjs'

let win = null
let tray = null

let setting = [
  {
    icon: 'icon-setting',
    label: '普通设置',
    configs: {
      SAVA_PATH: {
        type: 'path',
        label: ' 输出路径',
        defaultValue: resolve(app.getPath('home'), 'xiaoshuo')
      },
      CONCURRENCE: {
        type: 'string',
        label: '下载并发量',
        defaultValue: 5
      }
    }
  },
  {
    icon: 'icon-download',
    label: 'API 设置',
    configs: {
      APP_ID: {
        type: 'string',
        label: '应用 ID',
        defaultValue: ''
      },
      API_KEY: {
        type: 'string',
        label: '应用秘钥',
        defaultValue: ''
      },
      SECRET_KEY: {
        type: 'string',
        label: '加密秘钥',
        defaultValue: ''
      }
    }
  }
]

init(setting)

function opensetting() {
  win = create({}, true)
}

fromEvent(<any>app, 'ready').subscribe(() => {
  tray = new Tray(resolve(__dirname, 'tray_w24h24.png'))
  const contextMenu = Menu.buildFromTemplate([
    { label: '设置', click: opensetting }
  ])
  tray.setContextMenu(contextMenu)
})

export { store, tray, win }

当 app ready 的时候,我们创建一个通知栏的小图标,这个小图标是 24x24 大小的 png 图片,我是从 http://www.iconfont.cn/ 下载的,大家可以自行下载。

然后设置通知栏菜单,通过 setContextMenu 进行初始化,这是 Electron API 提供的方法。

# 读取配置

在 index.ts 中 ,记得使用了 await ,一定要给函数加上 async 哦。此时可以把 on 函数提取到 helper 里面去,具体请看仓库源码。

import { store } from './tray'

const { folderName, url } = args
const savePath = resolve(
  store.get('SAVA_PATH', app.getPath('music')),
  folderName
)

await download(url, plugin, { path: savePath })
await transform(savePath)

修改 download.ts ,让默认的配置项可以读取到并发量。

opts = Object.assign(
  {},
  {
    concurrence: store.get('CONCURRENCE', 5)
  },
  defaultOptions,
  crawl.opts,
  opts
)

修改 TTS.ts ,我们不希望每次设置完要重启,这里我们订阅一下更新。

let APP_ID = store.get('APP_ID')
let API_KEY = store.get('API_KEY')
let SECRET_KEY = store.get('SECRET_KEY')

// 每次修改更新值
store.onDidChange('APP_ID', (newValue: any) => (APP_ID = newValue))
store.onDidChange('API_KEY', (newValue: any) => (API_KEY = newValue))
store.onDidChange('SECRET_KEY', (newValue: any) => (SECRET_KEY = newValue))

let client: any = null

try {
  client = new speech(APP_ID, API_KEY, SECRET_KEY)
} catch (e) {
  log({ type: 'audio', step: 'new_speech_error', message: e.message })
}

# 前端界面

新建 Download.svelte , 然后像之前一样,在 App.svlete 里面引入并设置值。

<input bind:value=folderName />
<input bind:value=url />
<button on:click="download()">下载</button>
<button on:click="stop()">中断</button>

<script>
    import {
        ipcRenderer
    } from 'electron'
    export default {
        data() {
            return {
                url: '',
                folderName: ''
            }
        },
        methods: {
            download() {
                const {
                    url,
                    folderName
                } = this.get()
                if (url.length && folderName.length) {
                    console.log(url);
                    console.log(folderName);
                    ipcRenderer.send('download', {
                        url,
                        folderName
                    })
                }
            },
            stop() {
                ipcRenderer.send('stop') // 发送停止事件信号
            }
        }
    }
</script>

此时我们是没有办法对正在下载的进行中断想要实现中断,必须要有一个信号量来控制跳出循环,我们可以把这个信号量放到 store 里面去,只要我们不配置 setting 的配置项,其实他是不会显示出来的。

# 中断处理函数

修改 helper.ts,此时你的 helper 应该会像下面这样,中断了的时候,我们应该把下载的给删除,这样可以保证目录的干净。

import { Subject } from 'rxjs'
import { ipcMain as ipc } from 'electron'
import { store } from './tray'
import { remove } from 'fs-extra'
import log from './statusLog'

function ready() {
  let resolveFN, rejectFN
  let promise = new Promise(
    (resolve, reject) => ([resolveFN, rejectFN] = [resolve, reject])
  )
  return [resolveFN, rejectFN, promise]
}

interface CombineEvent {
  event: any
  args: any
}

function on(channel: string): Subject<CombineEvent> {
  const eventListner = new Subject<CombineEvent>()
  ipc.on(channel, (event: any, args: any) => eventListner.next({ event, args }))
  return eventListner
}

async function handleStop(path: string, logInfo: any) {
  if (store.get('stop')) {
    path && (await remove(path)) // 删除目录
    log(logInfo)
    return true
  }
  return false
}

export { ready, on, handleStop }

修改 download.ts 在循环里面添加跳出代码,并发送消息显示给前端

if (
  await handleStop(resolve(path, 'text'), {
    type: 'stop',
    message: '已停止该队列'
  })
) {
  // 每次循环都要判断是否有停止信号量。
  break
}

同样修改 TTS.ts ,添加跳出代码

if (
  await handleStop(chapterSavePath, {
    type: 'stop',
    message: '已停止该队列'
  })
) {
  break
}

那么在开始与结束的时候应该添加重置信号量的代码

store.set('stop', false)
await download(url, plugin, { path: savePath })
await transform(savePath)
store.set('stop', false)

以及设置信号量为停止的代码

on('stop').subscribe(() => {
  store.set('stop', true)
})