diff --git a/.electron-vite/update.js b/.electron-vite/update.js
new file mode 100644
index 0000000..b0079bb
--- /dev/null
+++ b/.electron-vite/update.js
@@ -0,0 +1,51 @@
+const fs = require('fs-extra')
+const path = require('path')
+const crypto = require('crypto')
+const AdmZip = require('adm-zip')
+const { version } = require('../package.json')
+
+const hash = (data, type = 'sha256') => {
+ const hmac = crypto.createHmac(type, 'nap')
+ hmac.update(data)
+ return hmac.digest('hex')
+}
+
+const createZip = (filePath, dest) => {
+ const zip = new AdmZip()
+ zip.addLocalFolder(filePath)
+ zip.toBuffer()
+ zip.writeZip(dest)
+}
+
+const start = async () => {
+ copyAppZip()
+ const appPath = './build/win-ia32-unpacked/resources/app'
+ const name = 'app.zip'
+ const outputPath = path.resolve('./build/update/update/')
+ const zipPath = path.resolve(outputPath, name)
+ await fs.ensureDir(outputPath)
+ await fs.emptyDir(outputPath)
+ createZip(appPath, zipPath)
+ const buffer = await fs.readFile(zipPath)
+ const sha256 = hash(buffer)
+ const hashName = sha256.slice(7, 12)
+ await fs.copy(zipPath, path.resolve(outputPath, `${hashName}.zip`))
+ await fs.remove(zipPath)
+ await fs.outputJSON(path.join(outputPath, 'manifest.json'), {
+ active: true,
+ version,
+ from: '0.0.1',
+ name: `${hashName}.zip`,
+ hash: sha256
+ })
+}
+
+const copyAppZip = () => {
+ try {
+ const dir = path.resolve('./build')
+ const filePath = path.resolve(dir, `ZzzSignalSearchExport-${version}-ia32-win.zip`)
+ fs.copySync(filePath, path.join(dir, 'app.zip'))
+ } catch (e) {}
+}
+
+start()
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..e20f124
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,55 @@
+on:
+ workflow_dispatch:
+ push:
+ # Sequence of patterns matched against refs/tag
+ tags:
+ - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
+
+name: Release
+
+jobs:
+ build:
+ name: Release
+ runs-on: windows-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ - name: Use Node.js
+ uses: actions/setup-node@v1
+ with:
+ node-version: '16.x'
+ - name: Build App
+ run: |
+ yarn --frozen-lockfile
+ yarn build:win32
+ yarn build-update
+ - name: Create Release
+ if: success()
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.TOKEN }}
+ with:
+ tag_name: ${{ github.ref }}
+ release_name: ZzzSignalSearchExport ${{ github.ref }}
+ draft: false
+ prerelease: false
+ - name: Upload Release Asset
+ if: success()
+ id: upload-release-asset
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.TOKEN }}
+ with:
+ upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
+ asset_path: ./build/app.zip
+ asset_name: ZzzSignalSearchExport.zip
+ asset_content_type: application/zip
+ - name: Deploy update
+ if: success()
+ uses: crazy-max/ghaction-github-pages@v2
+ with:
+ commit_message: Update app
+ build_dir: ./build/update
+ env:
+ GITHUB_TOKEN: ${{ secrets.TOKEN }}
\ No newline at end of file
diff --git a/package.json b/package.json
index b4a4356..dd91a02 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"build:dir": "cross-env BUILD_TARGET=clean node .electron-vite/build.js && electron-builder --dir",
"build:clean": "cross-env BUILD_TARGET=onlyClean node .electron-vite/build.js",
"build:web": "cross-env BUILD_TARGET=web node .electron-vite/build.js",
+ "build-update": "node .electron-vite/update.js",
"dev:web": "cross-env TARGET=web node .electron-vite/dev-runner.js",
"start": "electron ./src/main/main.js",
"dep:upgrade": "yarn upgrade-interactive --latest",
diff --git a/src/i18n/English.json b/src/i18n/English.json
index 909f9bf..3052163 100644
--- a/src/i18n/English.json
+++ b/src/i18n/English.json
@@ -87,6 +87,7 @@
"log.proxy.hint": "Using proxy mode [${ip}:${port}] to get URL,please reopen warp history inside the game client.",
"log.url.notFound2": "Unable to find URL, please make sure you already opened warp history inside the game client",
"log.url.incorrect": "Unable to get URL parameters",
+ "log.autoUpdate.success": "Auto update successful,please restart the program",
"excel.header.time": "time",
"excel.header.name": "name",
"excel.header.type": "type",
diff --git a/src/i18n/简体中文.json b/src/i18n/简体中文.json
index 47138e9..0fbbe89 100644
--- a/src/i18n/简体中文.json
+++ b/src/i18n/简体中文.json
@@ -86,6 +86,7 @@
"log.proxy.hint": "正在使用代理模式[${ip}:${port}]获取URL,请重新打开游戏抽卡记录。",
"log.url.notFound2": "未找到URL,请确认是否已打开游戏抽卡记录",
"log.url.incorrect": "获取URL参数失败",
+ "log.autoUpdate.success": "自动更新已完成,重启工具后生效",
"excel.header.time": "时间",
"excel.header.name": "名称",
"excel.header.type": "类别",
diff --git a/src/main/main.js b/src/main/main.js
index 8171b29..fb5d17f 100644
--- a/src/main/main.js
+++ b/src/main/main.js
@@ -5,6 +5,7 @@ require('./getData')
require('./bridge')
require('./excel')
require('./SRGFJson')
+const { getUpdateInfo } = require('./update/index')
const isDev = !app.isPackaged
let win = null
@@ -53,6 +54,12 @@ if (!isFirstInstance) {
if (proxyStatus.started) {
disableProxy()
}
+ if (getUpdateInfo().status === 'moving') {
+ e.preventDefault()
+ setTimeout(() => {
+ app.quit()
+ }, 3000)
+ }
})
app.on('quit', () => {
diff --git a/src/main/module/extract-zip.js b/src/main/module/extract-zip.js
new file mode 100644
index 0000000..78d371c
--- /dev/null
+++ b/src/main/module/extract-zip.js
@@ -0,0 +1,185 @@
+// Copyright (c) 2014 Max Ogden and other contributors
+// All rights reserved.
+
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+
+// * Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+
+// * Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+// https://github.com/maxogden/extract-zip
+// eslint-disable-next-line node/no-unsupported-features/node-builtins
+const { createWriteStream, promises: fs } = require('original-fs')
+const getStream = require('get-stream')
+const path = require('path')
+const { promisify } = require('util')
+const stream = require('stream')
+const yauzl = require('yauzl')
+
+const openZip = promisify(yauzl.open)
+const pipeline = promisify(stream.pipeline)
+
+class Extractor {
+ constructor (zipPath, opts) {
+ this.zipPath = zipPath
+ this.opts = opts
+ }
+
+ async extract () {
+
+ this.zipfile = await openZip(this.zipPath, { lazyEntries: true })
+ this.canceled = false
+
+ return new Promise((resolve, reject) => {
+ this.zipfile.on('error', err => {
+ this.canceled = true
+ reject(err)
+ })
+ this.zipfile.readEntry()
+
+ this.zipfile.on('close', () => {
+ if (!this.canceled) {
+ resolve()
+ }
+ })
+
+ this.zipfile.on('entry', async entry => {
+ /* istanbul ignore if */
+ if (this.canceled) {
+ return
+ }
+
+
+ if (entry.fileName.startsWith('__MACOSX/')) {
+ this.zipfile.readEntry()
+ return
+ }
+
+ const destDir = path.dirname(path.join(this.opts.dir, entry.fileName))
+
+ try {
+ await fs.mkdir(destDir, { recursive: true })
+
+ const canonicalDestDir = await fs.realpath(destDir)
+ const relativeDestDir = path.relative(this.opts.dir, canonicalDestDir)
+
+ if (relativeDestDir.split(path.sep).includes('..')) {
+ throw new Error(`Out of bound path "${canonicalDestDir}" found while processing file ${entry.fileName}`)
+ }
+
+ await this.extractEntry(entry)
+ this.zipfile.readEntry()
+ } catch (err) {
+ this.canceled = true
+ this.zipfile.close()
+ reject(err)
+ }
+ })
+ })
+ }
+
+ async extractEntry (entry) {
+ /* istanbul ignore if */
+ if (this.canceled) {
+ return
+ }
+
+ if (this.opts.onEntry) {
+ this.opts.onEntry(entry, this.zipfile)
+ }
+
+ const dest = path.join(this.opts.dir, entry.fileName)
+
+ // convert external file attr int into a fs stat mode int
+ const mode = (entry.externalFileAttributes >> 16) & 0xFFFF
+ // check if it's a symlink or dir (using stat mode constants)
+ const IFMT = 61440
+ const IFDIR = 16384
+ const IFLNK = 40960
+ const symlink = (mode & IFMT) === IFLNK
+ let isDir = (mode & IFMT) === IFDIR
+
+ // Failsafe, borrowed from jsZip
+ if (!isDir && entry.fileName.endsWith('/')) {
+ isDir = true
+ }
+
+ // check for windows weird way of specifying a directory
+ // https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566
+ const madeBy = entry.versionMadeBy >> 8
+ if (!isDir) isDir = (madeBy === 0 && entry.externalFileAttributes === 16)
+
+
+ const procMode = this.getExtractedMode(mode, isDir) & 0o777
+
+ // always ensure folders are created
+ const destDir = isDir ? dest : path.dirname(dest)
+
+ const mkdirOptions = { recursive: true }
+ if (isDir) {
+ mkdirOptions.mode = procMode
+ }
+ await fs.mkdir(destDir, mkdirOptions)
+ if (isDir) return
+
+ const readStream = await promisify(this.zipfile.openReadStream.bind(this.zipfile))(entry)
+
+ if (symlink) {
+ const link = await getStream(readStream)
+ await fs.symlink(link, dest)
+ } else {
+ await pipeline(readStream, createWriteStream(dest, { mode: procMode }))
+ }
+ }
+
+ getExtractedMode (entryMode, isDir) {
+ let mode = entryMode
+ // Set defaults, if necessary
+ if (mode === 0) {
+ if (isDir) {
+ if (this.opts.defaultDirMode) {
+ mode = parseInt(this.opts.defaultDirMode, 10)
+ }
+
+ if (!mode) {
+ mode = 0o755
+ }
+ } else {
+ if (this.opts.defaultFileMode) {
+ mode = parseInt(this.opts.defaultFileMode, 10)
+ }
+
+ if (!mode) {
+ mode = 0o644
+ }
+ }
+ }
+
+ return mode
+ }
+}
+
+module.exports = async function (zipPath, opts) {
+
+ if (!path.isAbsolute(opts.dir)) {
+ throw new Error('Target directory is expected to be absolute')
+ }
+
+ await fs.mkdir(opts.dir, { recursive: true })
+ opts.dir = await fs.realpath(opts.dir)
+ return new Extractor(zipPath, opts).extract()
+}
diff --git a/src/main/update/index.js b/src/main/update/index.js
new file mode 100644
index 0000000..a130227
--- /dev/null
+++ b/src/main/update/index.js
@@ -0,0 +1,65 @@
+const { app } = require('electron')
+const fetch = require('electron-fetch').default
+const semver = require('semver')
+const util = require('util')
+const path = require('path')
+const fs = require('fs-extra')
+const extract = require('../module/extract-zip')
+const { version } = require('../../../package.json')
+const { hash, sendMsg } = require('../utils')
+const config = require('../config')
+const i18n = require('../i18n')
+const streamPipeline = util.promisify(require('stream').pipeline)
+
+async function download(url, filePath) {
+ const response = await fetch(url)
+ if (!response.ok) throw new Error(`unexpected response ${response.statusText}`)
+ await streamPipeline(response.body, fs.createWriteStream(filePath))
+}
+
+const updateInfo = {
+ status: 'init'
+}
+
+const isDev = !app.isPackaged
+const appPath = isDev ? path.resolve(__dirname, '../../', 'update-dev/app'): app.getAppPath()
+const updatePath = isDev ? path.resolve(__dirname, '../../', 'update-dev/download') : path.resolve(appPath, '..', '..', 'update')
+
+const update = async () => {
+ if (isDev) return
+ try {
+ const url = 'https://earthjasonlin.github.io/zzz-signal-search-export/update'
+ const res = await fetch(`${url}/manifest.json?t=${Math.floor(Date.now() / (1000 * 60 * 10))}`)
+ const data = await res.json()
+ if (!data.active) return
+ if (semver.gt(data.version, version) && semver.gte(version, data.from)) {
+ await fs.emptyDir(updatePath)
+ const filePath = path.join(updatePath, data.name)
+ if (!config.autoUpdate) {
+ sendMsg(data.version, 'NEW_VERSION')
+ return
+ }
+ updateInfo.status = 'downloading'
+ await download(`${url}/${data.name}`, filePath)
+ const buffer = await fs.readFile(filePath)
+ const sha256 = hash(buffer)
+ if (sha256 !== data.hash) return
+ const appPathTemp = path.join(updatePath, 'app')
+ await extract(filePath, { dir: appPathTemp })
+ updateInfo.status = 'moving'
+ await fs.emptyDir(appPath)
+ await fs.copy(appPathTemp, appPath)
+ updateInfo.status = 'finished'
+ sendMsg(i18n.log.autoUpdate.success, 'UPDATE_HINT')
+ }
+ } catch (e) {
+ updateInfo.status = 'failed'
+ sendMsg(e, 'ERROR')
+ }
+}
+
+const getUpdateInfo = () => updateInfo
+
+setTimeout(update, 1000)
+
+exports.getUpdateInfo = getUpdateInfo
\ No newline at end of file
diff --git a/src/renderer/App.vue b/src/renderer/App.vue
index 3d9c1b1..59f6d58 100644
--- a/src/renderer/App.vue
+++ b/src/renderer/App.vue
@@ -327,6 +327,11 @@ onMounted(async () => {
console.error(err)
})
+ ipcRenderer.on('UPDATE_HINT', (event, message) => {
+ state.log = message
+ state.status = 'updated'
+ })
+
ipcRenderer.on('AUTHKEY_TIMEOUT', (event, message) => {
state.authkeyTimeout = message
})
diff --git a/src/renderer/components/Setting.vue b/src/renderer/components/Setting.vue
index 3f52e20..35a60b7 100644
--- a/src/renderer/components/Setting.vue
+++ b/src/renderer/components/Setting.vue
@@ -23,6 +23,12 @@
{{text.dataManagerHint}}
+