Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12f658dfd9 |
No files matched your search
@@ -1,97 +0,0 @@
|
|||||||
'use strict'
|
|
||||||
process.env.NODE_ENV = 'production'
|
|
||||||
|
|
||||||
const { say } = require('cfonts')
|
|
||||||
const { sync } = require('del')
|
|
||||||
|
|
||||||
const chalk = require('chalk')
|
|
||||||
const rollup = require("rollup")
|
|
||||||
const { build } = require('vite')
|
|
||||||
const Multispinner = require('multispinner')
|
|
||||||
|
|
||||||
const mainOptions = require('./rollup.main.config');
|
|
||||||
const rendererOptions = require('./vite.config')
|
|
||||||
const opt = mainOptions(process.env.NODE_ENV);
|
|
||||||
|
|
||||||
const doneLog = chalk.bgGreen.white(' DONE ') + ' '
|
|
||||||
const errorLog = chalk.bgRed.white(' ERROR ') + ' '
|
|
||||||
const okayLog = chalk.bgBlue.white(' OKAY ') + ' '
|
|
||||||
const isCI = process.env.CI || false
|
|
||||||
|
|
||||||
if (process.env.BUILD_TARGET === 'web') web()
|
|
||||||
else unionBuild()
|
|
||||||
|
|
||||||
function clean() {
|
|
||||||
sync(['dist/electron/main/*', 'dist/electron/renderer/*', 'dist/web/*', 'build/*', '!build/icons', '!build/lib', '!build/lib/electron-build.*', '!build/icons/icon.*'])
|
|
||||||
console.log(`\n${doneLog}clear done`)
|
|
||||||
if (process.env.BUILD_TARGET === 'onlyClean') process.exit()
|
|
||||||
}
|
|
||||||
|
|
||||||
function unionBuild() {
|
|
||||||
greeting()
|
|
||||||
if (process.env.BUILD_TARGET === 'clean' || process.env.BUILD_TARGET === 'onlyClean') clean()
|
|
||||||
|
|
||||||
const tasks = ['main', 'renderer']
|
|
||||||
const m = new Multispinner(tasks, {
|
|
||||||
preText: 'building',
|
|
||||||
postText: 'process'
|
|
||||||
})
|
|
||||||
let results = ''
|
|
||||||
|
|
||||||
m.on('success', () => {
|
|
||||||
process.stdout.write('\x1B[2J\x1B[0f')
|
|
||||||
console.log(`\n\n${results}`)
|
|
||||||
console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`)
|
|
||||||
process.exit()
|
|
||||||
})
|
|
||||||
|
|
||||||
rollup.rollup(opt)
|
|
||||||
.then(build => {
|
|
||||||
results += `${doneLog}MainProcess build success` + '\n\n'
|
|
||||||
build.write(opt.output).then(() => {
|
|
||||||
m.success('main')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
m.error('main')
|
|
||||||
console.log(`\n ${errorLog}failed to build main process`)
|
|
||||||
console.error(`\n${error}\n`)
|
|
||||||
process.exit(1)
|
|
||||||
});
|
|
||||||
|
|
||||||
build(rendererOptions).then(res => {
|
|
||||||
results += `${doneLog}RendererProcess build success` + '\n\n'
|
|
||||||
m.success('renderer')
|
|
||||||
}).catch(err => {
|
|
||||||
m.error('renderer')
|
|
||||||
console.log(`\n ${errorLog}failed to build renderer process`)
|
|
||||||
console.error(`\n${err}\n`)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function web() {
|
|
||||||
sync(['dist/web/*', '!.gitkeep'])
|
|
||||||
build(rendererOptions).then(res => {
|
|
||||||
console.log(`${doneLog}RendererProcess build success`)
|
|
||||||
process.exit()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function greeting() {
|
|
||||||
const cols = process.stdout.columns
|
|
||||||
let text = ''
|
|
||||||
|
|
||||||
if (cols > 85) text = `let's-build`
|
|
||||||
else if (cols > 60) text = `let's-|build`
|
|
||||||
else text = false
|
|
||||||
|
|
||||||
if (text && !isCI) {
|
|
||||||
say(text, {
|
|
||||||
colors: ['yellow'],
|
|
||||||
font: 'simple3d',
|
|
||||||
space: false
|
|
||||||
})
|
|
||||||
} else console.log(chalk.yellow.bold(`\n let's-build`))
|
|
||||||
console.log()
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
process.env.NODE_ENV = 'development'
|
|
||||||
|
|
||||||
const chalk = require('chalk')
|
|
||||||
const electron = require('electron')
|
|
||||||
const path = require('path')
|
|
||||||
const rollup = require("rollup")
|
|
||||||
const Portfinder = require("portfinder")
|
|
||||||
|
|
||||||
const { say } = require('cfonts')
|
|
||||||
const { spawn } = require('child_process')
|
|
||||||
const { createServer } = require('vite')
|
|
||||||
|
|
||||||
const rendererOptions = require("./vite.config")
|
|
||||||
const mainOptions = require("./rollup.main.config")
|
|
||||||
const opt = mainOptions(process.env.NODE_ENV);
|
|
||||||
|
|
||||||
let electronProcess = null
|
|
||||||
let manualRestart = false
|
|
||||||
|
|
||||||
function logStats(proc, data) {
|
|
||||||
let log = ''
|
|
||||||
|
|
||||||
log += chalk.yellow.bold(`┏ ${proc} 'Process' ${new Array((19 - proc.length) + 1).join('-')}`)
|
|
||||||
log += '\n\n'
|
|
||||||
|
|
||||||
if (typeof data === 'object') {
|
|
||||||
data.toString({
|
|
||||||
colors: true,
|
|
||||||
chunks: false
|
|
||||||
}).split(/\r?\n/).forEach(line => {
|
|
||||||
log += ' ' + line + '\n'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
log += ` ${data}\n`
|
|
||||||
}
|
|
||||||
|
|
||||||
log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n'
|
|
||||||
console.log(log)
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeJunk(chunk) {
|
|
||||||
// Example: 2018-08-10 22:48:42.866 Electron[90311:4883863] *** WARNING: Textured window <AtomNSWindow: 0x7fb75f68a770>
|
|
||||||
if (/\d+-\d+-\d+ \d+:\d+:\d+\.\d+ Electron(?: Helper)?\[\d+:\d+] /.test(chunk)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example: [90789:0810/225804.894349:ERROR:CONSOLE(105)] "Uncaught (in promise) Error: Could not instantiate: ProductRegistryImpl.Registry", source: chrome-devtools://devtools/bundled/inspector.js (105)
|
|
||||||
if (/\[\d+:\d+\/|\d+\.\d+:ERROR:CONSOLE\(\d+\)\]/.test(chunk)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example: ALSA lib confmisc.c:767:(parse_card) cannot find card '0'
|
|
||||||
if (/ALSA lib [a-z]+\.c:\d+:\([a-z_]+\)/.test(chunk)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startRenderer() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
Portfinder.basePort = 9080
|
|
||||||
Portfinder.getPort(async (err, port) => {
|
|
||||||
if (err) {
|
|
||||||
console.log('PortError:', err)
|
|
||||||
process.exit(1)
|
|
||||||
} else {
|
|
||||||
const server = await createServer(rendererOptions)
|
|
||||||
process.env.PORT = port
|
|
||||||
await server.listen(port)
|
|
||||||
if (process.env.TARGET === 'web') {
|
|
||||||
server.config.logger.info(
|
|
||||||
chalk.cyan(`\n vite v${require('vite/package.json').version}`) +
|
|
||||||
chalk.green(` dev server running at:\n`),
|
|
||||||
{
|
|
||||||
clear: !server.config.logger.hasWarned,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
server.printUrls()
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function startMain() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const watcher = rollup.watch(opt);
|
|
||||||
watcher.on('change', filename => {
|
|
||||||
// 主进程日志部分
|
|
||||||
logStats('Main-FileChange', filename)
|
|
||||||
});
|
|
||||||
watcher.on('event', event => {
|
|
||||||
if (event.code === 'END') {
|
|
||||||
if (electronProcess && electronProcess.kill) {
|
|
||||||
manualRestart = true
|
|
||||||
process.kill(electronProcess.pid)
|
|
||||||
electronProcess = null
|
|
||||||
startElectron()
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
manualRestart = false
|
|
||||||
}, 5000)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve()
|
|
||||||
|
|
||||||
} else if (event.code === 'ERROR') {
|
|
||||||
reject(event.error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function startElectron() {
|
|
||||||
|
|
||||||
var args = [
|
|
||||||
'--inspect=5858',
|
|
||||||
path.join(__dirname, '../dist/electron/main/main.js')
|
|
||||||
]
|
|
||||||
|
|
||||||
// detect yarn or npm and process commandline args accordingly
|
|
||||||
if (process.env.npm_execpath.endsWith('yarn.js')) {
|
|
||||||
args = args.concat(process.argv.slice(3))
|
|
||||||
} else if (process.env.npm_execpath.endsWith('npm-cli.js')) {
|
|
||||||
args = args.concat(process.argv.slice(2))
|
|
||||||
}
|
|
||||||
|
|
||||||
electronProcess = spawn(electron, args)
|
|
||||||
|
|
||||||
electronProcess.stdout.on('data', data => {
|
|
||||||
electronLog(removeJunk(data), 'blue')
|
|
||||||
})
|
|
||||||
electronProcess.stderr.on('data', data => {
|
|
||||||
electronLog(removeJunk(data), 'red')
|
|
||||||
})
|
|
||||||
|
|
||||||
electronProcess.on('close', () => {
|
|
||||||
if (!manualRestart) process.exit()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function electronLog(data, color) {
|
|
||||||
if (data) {
|
|
||||||
let log = ''
|
|
||||||
data = data.toString().split(/\r?\n/)
|
|
||||||
data.forEach(line => {
|
|
||||||
log += ` ${line}\n`
|
|
||||||
})
|
|
||||||
console.log(
|
|
||||||
chalk[color].bold(`┏ Electron -------------------`) +
|
|
||||||
'\n\n' +
|
|
||||||
log +
|
|
||||||
chalk[color].bold('┗ ----------------------------') +
|
|
||||||
'\n'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function greeting() {
|
|
||||||
const cols = process.stdout.columns
|
|
||||||
let text = ''
|
|
||||||
|
|
||||||
if (cols > 104) text = 'electron-vite'
|
|
||||||
else if (cols > 76) text = 'electron-|vite'
|
|
||||||
else text = false
|
|
||||||
|
|
||||||
if (text) {
|
|
||||||
say(text, {
|
|
||||||
colors: ['yellow'],
|
|
||||||
font: 'simple3d',
|
|
||||||
space: false
|
|
||||||
})
|
|
||||||
} else console.log(chalk.yellow.bold('\n electron-vite'))
|
|
||||||
console.log(chalk.blue(`getting ready...`) + '\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
greeting()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await startRenderer()
|
|
||||||
if (process.env.TARGET !== 'web') {
|
|
||||||
await startMain()
|
|
||||||
await startElectron()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
init()
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
const path = require('path')
|
|
||||||
const { nodeResolve } = require('@rollup/plugin-node-resolve')
|
|
||||||
const commonjs = require('@rollup/plugin-commonjs')
|
|
||||||
const esbuild = require('rollup-plugin-esbuild').default
|
|
||||||
const alias = require('@rollup/plugin-alias')
|
|
||||||
const json = require('@rollup/plugin-json')
|
|
||||||
|
|
||||||
module.exports = (env = 'production') => {
|
|
||||||
return {
|
|
||||||
input: path.join(__dirname, '../src/main/main.js'),
|
|
||||||
output: {
|
|
||||||
file: path.join(__dirname, '../dist/electron/main/main.js'),
|
|
||||||
format: 'cjs',
|
|
||||||
name: 'MainProcess',
|
|
||||||
sourcemap: false,
|
|
||||||
exports: 'auto'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
nodeResolve({ jsnext: true, preferBuiltins: true, browser: true }), // 消除碰到 node.js 模块时⚠警告
|
|
||||||
commonjs(),
|
|
||||||
json(),
|
|
||||||
esbuild({
|
|
||||||
// All options are optional
|
|
||||||
include: /\.[jt]sx?$/, // default, inferred from `loaders` option
|
|
||||||
exclude: /node_modules/, // default
|
|
||||||
// watch: process.argv.includes('--watch'), // rollup 中有配置
|
|
||||||
sourceMap: false, // default
|
|
||||||
minify: process.env.NODE_ENV === 'production',
|
|
||||||
target: 'esnext', // default, or 'es20XX', 'esnext'
|
|
||||||
// Like @rollup/plugin-replace
|
|
||||||
define: {
|
|
||||||
__VERSION__: '"x.y.z"'
|
|
||||||
},
|
|
||||||
// Add extra loaders
|
|
||||||
loaders: {
|
|
||||||
// Add .json files support
|
|
||||||
// require @rollup/plugin-commonjs
|
|
||||||
'.json': 'json',
|
|
||||||
// Enable JSX in .js files too
|
|
||||||
'.js': 'jsx'
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
alias({
|
|
||||||
entries: [
|
|
||||||
{ find: '@main', replacement: path.join(__dirname, '../src/main'), },
|
|
||||||
]
|
|
||||||
})
|
|
||||||
],
|
|
||||||
external: [
|
|
||||||
'crypto',
|
|
||||||
'assert',
|
|
||||||
'fs',
|
|
||||||
'util',
|
|
||||||
'os',
|
|
||||||
'events',
|
|
||||||
'child_process',
|
|
||||||
'http',
|
|
||||||
'https',
|
|
||||||
'path',
|
|
||||||
'electron',
|
|
||||||
'original-fs'
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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()
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
const { join } = require("path")
|
|
||||||
const vuePlugin = require("@vitejs/plugin-vue")
|
|
||||||
const { defineConfig } = require("vite")
|
|
||||||
|
|
||||||
function resolve(dir) {
|
|
||||||
return join(__dirname, '..', dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = resolve('src/renderer')
|
|
||||||
|
|
||||||
const config = defineConfig({
|
|
||||||
mode: process.env.NODE_ENV,
|
|
||||||
root,
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@renderer': root,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
base: './',
|
|
||||||
build: {
|
|
||||||
outDir: process.env.BUILD_TARGET === 'web' ? resolve('dist/web') : resolve('dist/electron/renderer'),
|
|
||||||
emptyOutDir: true
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
port: Number(process.env.PORT),
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
vuePlugin({
|
|
||||||
script: {
|
|
||||||
refSugar: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
|
||||||
publicDir: resolve('static')
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = config
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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 }}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
node_modules/
|
|
||||||
build/win-unpacked/
|
|
||||||
build/win-ia32-unpacked/
|
|
||||||
build/Genshin Gacha Export Setup 0.2.4.exe
|
|
||||||
build/Genshin Gacha Export Setup 0.2.4.exe.blockmap
|
|
||||||
build/*.zip
|
|
||||||
build/update/
|
|
||||||
build/builder-debug.yml
|
|
||||||
build/latest.yml
|
|
||||||
build/builder-effective-config.yaml
|
|
||||||
dist/electron/main
|
|
||||||
dist/electron/renderer
|
|
||||||
dist/web
|
|
||||||
userData
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.idea
|
|
||||||
.vscode
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2024 earthjasonlin
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# 绝区零调频记录导出工具
|
|
||||||
|
|
||||||
中文 | [English](https://github.com/earthjasonlin/zzz-signal-search-export/blob/main/docs/README_EN.md)
|
|
||||||
|
|
||||||
这个项目由[star-rail-warp-export](https://github.com/biuuu/star-rail-warp-export/)修改而来,功能基本一致。
|
|
||||||
|
|
||||||
一个使用 Electron 制作的小工具,需要在 Windows 操作系统上运行。
|
|
||||||
|
|
||||||
通过读取游戏日志或者代理模式获取访问游戏跃迁记录 API 所需的 authKey,然后再使用获取到的 authKey 来读取游戏跃迁记录。
|
|
||||||
|
|
||||||
## 其它语言
|
|
||||||
|
|
||||||
修改`src/i18n/`目录下的 json 文件就可以翻译到对应的语言。如果觉得已有的翻译有不准确或可以改进的地方,可以随时修改发 Pull Request。
|
|
||||||
|
|
||||||
## 使用说明
|
|
||||||
|
|
||||||
1. 下载工具后解压 - 下载地址: [GitHub](https://github.com/earthjasonlin/zzz-signal-search-export/releases/latest/download/ZzzSignalSearchExport.zip) / [123云盘](https://www.123pan.com/s/Vs9uVv-ShhE.html) / [蓝奏云(密码:zzzz)](https://www.lanzouh.com/b00eewtvxa)
|
|
||||||
2. 打开游戏的跃迁详情页面
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
3. 点击工具的“加载数据”按钮
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
如果没出什么问题的话,你会看到正在读取数据的提示,最终效果如下图所示
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>展开图片</summary>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
如果需要导出多个账号的数据,可以点击旁边的加号按钮。
|
|
||||||
|
|
||||||
然后游戏切换的新账号,再打开跃迁历史记录,工具再点击“加载数据”按钮。
|
|
||||||
|
|
||||||
## Devlopment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 安装模块
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# 开发模式
|
|
||||||
yarn dev
|
|
||||||
|
|
||||||
# 构建一个可以运行的程序
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](https://github.com/earthjasonlin/zzz-signal-search-export/blob/main/LICENSE)
|
|
||||||
|
Before Width: | Height: | Size: 382 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 429 KiB |
@@ -1,56 +0,0 @@
|
|||||||
# Zenless Zone Zero Signal Search History Exporter
|
|
||||||
|
|
||||||
[中文](https://github.com/earthjasonlin/zzz-signal-search-export) | English
|
|
||||||
|
|
||||||
This project is modified from the [star-rail-warp-export](https://github.com/biuuu/star-rail-warp-export/) repository, and its functions are basically the same.
|
|
||||||
|
|
||||||
A tool made from Electron that runs on the Windows operating system.
|
|
||||||
|
|
||||||
Read the game log or proxy to get the authKey needed to access the game warp history API, and then use the authKey to read the game wish history.
|
|
||||||
|
|
||||||
## Other languages
|
|
||||||
|
|
||||||
Modify the JSON file in the `src/i18n/` directory to translate into the appropriate language.
|
|
||||||
|
|
||||||
If you feel that the existing translation is inappropriate, you can send a pull request to modify it at any time.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. Unzip after downloading the tool - [GitHub](https://github.com/earthjasonlin/zzz-signal-search-export/releases/latest/download/ZzzSignalSearchExport.zip)
|
|
||||||
|
|
||||||
2. Open the warp details page of the game
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
3. Click the tool's "Load data" button
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
If nothing goes wrong, you'll be prompted to read the data, and the final result will look like this
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Expand the picture</summary>
|
|
||||||
|
|
||||||

|
|
||||||
</details>
|
|
||||||
|
|
||||||
If you need to export the data of multiple accounts, you can click the plus button next to it.
|
|
||||||
|
|
||||||
Then switch to the new account of the game, open the wish history, and click the "load data" button in the tool.
|
|
||||||
|
|
||||||
## Devlopment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# install node modules
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# develop
|
|
||||||
yarn dev
|
|
||||||
|
|
||||||
# Build a program that can run
|
|
||||||
yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](https://github.com/earthjasonlin/zzz-signal-search-export/blob/main/LICENSE)
|
|
||||||
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 43 KiB |
@@ -1,120 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "zzz-signal-search-export",
|
|
||||||
"version": "1.0.6",
|
|
||||||
"main": "./dist/electron/main/main.js",
|
|
||||||
"author": "earthjasonlin <https://git.loliquq.cn/earthjasonlin>",
|
|
||||||
"homepage": "https://github.com/earthjasonlin/zzz-signal-search-export",
|
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "node .electron-vite/dev-runner.js",
|
|
||||||
"test": "jest",
|
|
||||||
"build": "cross-env BUILD_TARGET=clean node .electron-vite/build.js && electron-builder",
|
|
||||||
"build:win32": "cross-env BUILD_TARGET=clean node .electron-vite/build.js && electron-builder --win --ia32",
|
|
||||||
"build:win64": "cross-env BUILD_TARGET=clean node .electron-vite/build.js && electron-builder --win --x64",
|
|
||||||
"build:linux": "cross-env BUILD_TARGET=clean node .electron-vite/build.js && electron-builder --linux",
|
|
||||||
"build:mac": "cross-env BUILD_TARGET=clean node .electron-vite/build.js && electron-builder --mac",
|
|
||||||
"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",
|
|
||||||
"postinstall": "electron-builder install-app-deps"
|
|
||||||
},
|
|
||||||
"build": {
|
|
||||||
"nsis": {
|
|
||||||
"oneClick": false,
|
|
||||||
"allowToChangeInstallationDirectory": true
|
|
||||||
},
|
|
||||||
"asar": false,
|
|
||||||
"extraFiles": [],
|
|
||||||
"publish": [
|
|
||||||
{
|
|
||||||
"provider": "generic",
|
|
||||||
"url": "http://127.0.0.1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"productName": "ZzzSignalSearchExport",
|
|
||||||
"appId": "org.earthjasonlin.zzz-signal-search-export",
|
|
||||||
"directories": {
|
|
||||||
"output": "build"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist/electron/**/*"
|
|
||||||
],
|
|
||||||
"dmg": {
|
|
||||||
"contents": [
|
|
||||||
{
|
|
||||||
"x": 410,
|
|
||||||
"y": 150,
|
|
||||||
"type": "link",
|
|
||||||
"path": "/Applications"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 130,
|
|
||||||
"y": 150,
|
|
||||||
"type": "file"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"mac": {
|
|
||||||
"icon": "build/icons/icon.icns"
|
|
||||||
},
|
|
||||||
"win": {
|
|
||||||
"icon": "build/icons/icon.ico",
|
|
||||||
"target": "zip"
|
|
||||||
},
|
|
||||||
"linux": {
|
|
||||||
"target": "deb",
|
|
||||||
"icon": "build/icons"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
|
||||||
"@element-plus/icons-vue": "^2.1.0",
|
|
||||||
"@rollup/plugin-alias": "^3.1.9",
|
|
||||||
"@rollup/plugin-commonjs": "^21.0.1",
|
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
|
||||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
|
||||||
"@types/node": "^17.0.10",
|
|
||||||
"@vitejs/plugin-vue": "2.1.0",
|
|
||||||
"@vue/compiler-sfc": "^3.2.29",
|
|
||||||
"adm-zip": "^0.5.9",
|
|
||||||
"autoprefixer": "^10.4.2",
|
|
||||||
"cfonts": "^2.10.0",
|
|
||||||
"chalk": "^4.1.0",
|
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"del": "^6.0.0",
|
|
||||||
"echarts": "^5.2.2",
|
|
||||||
"electron": "^16.0.7",
|
|
||||||
"electron-builder": "^23.0.2",
|
|
||||||
"electron-fetch": "^1.7.4",
|
|
||||||
"electron-unhandled": "^3.0.2",
|
|
||||||
"electron-window-state": "^5.0.3",
|
|
||||||
"element-plus": "^2.3.7",
|
|
||||||
"fs-extra": "^10.0.0",
|
|
||||||
"get-stream": "^6.0.1",
|
|
||||||
"glob": "^10.3.3",
|
|
||||||
"jest": "^29.5.0",
|
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"moment": "^2.29.1",
|
|
||||||
"multispinner": "^0.2.1",
|
|
||||||
"ora": "^5.3.0",
|
|
||||||
"portfinder": "^1.0.28",
|
|
||||||
"postcss": "^8.4.5",
|
|
||||||
"rollup-plugin-esbuild": "^4.8.2",
|
|
||||||
"semver": "^7.3.5",
|
|
||||||
"tailwindcss": "^3.0.16",
|
|
||||||
"vite": "2.7.13",
|
|
||||||
"vue": "^3.2.29",
|
|
||||||
"winreg": "^1.2.4",
|
|
||||||
"yauzl": "^2.10.0"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"vite",
|
|
||||||
"electron",
|
|
||||||
"vue3",
|
|
||||||
"rollup"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
[
|
|
||||||
[
|
|
||||||
"zh-cn",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"key": "2",
|
|
||||||
"name": "独家频段"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "3",
|
|
||||||
"name": "音擎频段"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "1",
|
|
||||||
"name": "常驻频段"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "5",
|
|
||||||
"name": "邦布频段"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"zh-tw",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"key": "2",
|
|
||||||
"name": "獨家頻段"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "3",
|
|
||||||
"name": "音擎頻段"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "1",
|
|
||||||
"name": "常駐頻段"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "5",
|
|
||||||
"name": "邦布頻段"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"en-us",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"key": "2",
|
|
||||||
"name": "Exclusive Channel"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "3",
|
|
||||||
"name": "W-Engine Channel"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "1",
|
|
||||||
"name": "Stable Channel"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "5",
|
|
||||||
"name": "Bangboo Channel"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
{
|
|
||||||
"symbol.colon": ": ",
|
|
||||||
"ui.button.load": "Load data",
|
|
||||||
"ui.button.update": "Update",
|
|
||||||
"ui.button.directUpdate": "Direct update",
|
|
||||||
"ui.button.files": "Export Files",
|
|
||||||
"ui.button.excel": "Export Excel",
|
|
||||||
"ui.button.srgf": "Export JSON",
|
|
||||||
"ui.button.url": "Input URL",
|
|
||||||
"ui.button.setting": "Settings",
|
|
||||||
"ui.button.option": "Option",
|
|
||||||
"ui.button.startProxy": "Proxy mode",
|
|
||||||
"ui.button.solution": "Solution",
|
|
||||||
"ui.button.cacheFolder": "Open cache folder",
|
|
||||||
"ui.button.copyUrl": "Copy URL",
|
|
||||||
"ui.select.newAccount": "New account",
|
|
||||||
"ui.hint.newAccount": "Export data from other accounts",
|
|
||||||
"ui.hint.init": "Please open your warp history inside the game client before clicking on the 'Load data' button",
|
|
||||||
"ui.hint.lastUpdate": "Last update",
|
|
||||||
"ui.hint.failed": "Oops, something failed",
|
|
||||||
"ui.hint.relaunchHint": "The update has been completed, it will take effect after clicking the button to restart the tool",
|
|
||||||
"ui.win.title": "Zenless Zone Zero Signal Search History Exporter",
|
|
||||||
"ui.data.total": "Total",
|
|
||||||
"ui.data.times": "Pulls",
|
|
||||||
"ui.data.sum": "Accumulated",
|
|
||||||
"ui.data.no4star": "pulls without a S-Rank",
|
|
||||||
"ui.data.character": "Agents",
|
|
||||||
"ui.data.weapon": "W-Engines",
|
|
||||||
"ui.data.bang": "Bangboo",
|
|
||||||
"ui.data.star4": "S-Rank",
|
|
||||||
"ui.data.star3": "A-Rank",
|
|
||||||
"ui.data.star2": "B-Rank",
|
|
||||||
"ui.data.history": "S-Rank history",
|
|
||||||
"ui.data.average": "S-Rank on average",
|
|
||||||
"ui.data.chara4": "S-Rank Agents",
|
|
||||||
"ui.data.chara3": "A-Rank Agents",
|
|
||||||
"ui.data.weapon4": "S-Rank W-Engines",
|
|
||||||
"ui.data.weapon3": "A-Rank W-Engines",
|
|
||||||
"ui.data.weapon2": "B-Rank W-Engines",
|
|
||||||
"ui.data.bang4": "S-Rank Bangboo",
|
|
||||||
"ui.data.bang3": "A-Rank Bangboo",
|
|
||||||
"ui.setting.title": "Settings",
|
|
||||||
"ui.setting.language": "Language",
|
|
||||||
"ui.setting.languageHint": "When the translation is missing, English will be displayed by default.",
|
|
||||||
"ui.setting.logType": "Log type",
|
|
||||||
"ui.setting.auto": "Auto",
|
|
||||||
"ui.setting.cnServer": "CN server",
|
|
||||||
"ui.setting.seaServer": "Global server",
|
|
||||||
"ui.setting.logTypeHint": "Choose which server generated logs to be used first when acquiring URL from game logs",
|
|
||||||
"ui.setting.dataManagerHint": "Unnecessary data can be deleted",
|
|
||||||
"ui.setting.autoUpdate": "Auto update",
|
|
||||||
"ui.setting.hideNovice": "Hide Starter Warp",
|
|
||||||
"ui.setting.proxyMode": "Proxy mode",
|
|
||||||
"ui.setting.proxyModeHint": "When we fail to get the URL from system logs, use the system proxy",
|
|
||||||
"ui.setting.fetchFullHistory": "Get complete data",
|
|
||||||
"ui.setting.fetchFullHistoryHint": "When this option is enabled, click the \"Update Data\" button to get all the card draw records within 6 months. When there are incorrect data within 6 months, this function can be used to repair.",
|
|
||||||
"ui.setting.closeProxy": "Disable system proxy",
|
|
||||||
"ui.setting.closeProxyHint": "When you choose proxy mode, if the program crashes it can cause unwanted results that may affect your system. You can click this button to clear the system proxy settings.",
|
|
||||||
"ui.about.title": "About",
|
|
||||||
"ui.about.license": "This software is opensource using MIT license.",
|
|
||||||
"ui.urlDialog.title": "Input URL manually",
|
|
||||||
"ui.urlDialog.hint": "This function should only be used when you understand what URL is needed here",
|
|
||||||
"ui.urlDialog.placeholder": "Please enter the URL with authentication information",
|
|
||||||
"ui.common.cancel": "Cancel",
|
|
||||||
"ui.common.ok": "OK",
|
|
||||||
"ui.common.data": "Data",
|
|
||||||
"ui.common.dataManage": "Data Management",
|
|
||||||
"ui.common.updateTime": "Update Date",
|
|
||||||
"ui.common.status": "Status",
|
|
||||||
"ui.common.action": "Operation",
|
|
||||||
"ui.common.deleted": "Deleted",
|
|
||||||
"ui.common.normal": "Normal",
|
|
||||||
"ui.common.delete": "Delete",
|
|
||||||
"ui.common.restore": "Restore",
|
|
||||||
"log.save.failed": "Failed to save local data",
|
|
||||||
"log.file.notFound": "Unable to find game logs, please make sure you already opened warp history inside the game client",
|
|
||||||
"log.url.notFound": "Unable to find URL",
|
|
||||||
"log.file.readFailed": "Failed to read logs",
|
|
||||||
"log.fetch.retry": "Processing ${name} of page ${page} failed,retrying in 5 seconds for the ${count} time……",
|
|
||||||
"log.fetch.retryFailed": "Processing ${name} of page ${page} failed,retry times maxed out",
|
|
||||||
"log.fetch.interval": "Processing ${name} of page ${page},1 second timeout every 10 pages……",
|
|
||||||
"log.fetch.current": "Processing ${name} of page ${page}",
|
|
||||||
"log.fetch.authTimeout": "User authentication expired, please reopen warp history inside the game client.",
|
|
||||||
"log.fetch.gachaType": "Getting signal search type, please wait",
|
|
||||||
"log.fetch.gachaTypeOk": "Signal search type acquired",
|
|
||||||
"log.url.lackAuth": "Authkey not found in URL",
|
|
||||||
"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",
|
|
||||||
"excel.header.rank": "rarity",
|
|
||||||
"excel.header.total": "total",
|
|
||||||
"excel.header.pity": "within pity",
|
|
||||||
"excel.header.remark": "remark",
|
|
||||||
"excel.wish2": "Signal Search 2",
|
|
||||||
"excel.customFont": "Arial",
|
|
||||||
"excel.filePrefix": "Zenless Zone Zero Signal Search Log",
|
|
||||||
"excel.fileType": "Excel file",
|
|
||||||
"srgf.fileType": "Zenless Zone Zero Gacha Log Format file (SRGF)",
|
|
||||||
"ui.extra.cacheClean": "1. Confirm whether the search history in the game has been opened, and if the error \"User authentication expired\" still appears, try the following steps \n2. Close the game window of Zenless Zone Zero \n3. Click the \"Open Web Cache Folder\" button above to open the \"Cache\" folder \n4. Delete the \"Cache_Data\" folder \n5. Start the Zenless Zone Zero game and open the search history page in the game \n6. Close this dialog and click the \"Update Data\" button",
|
|
||||||
"ui.extra.findCacheFolder": "If the \"Open cache folder\" button does not respond, you can manually find the game's web cache folder. The directory is \"Your game installation path/ZenlessZoneZero_Data/webCaches/Cache/\"",
|
|
||||||
"ui.extra.urlCopied": "URL Copied"
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
{
|
|
||||||
"symbol.colon": ":",
|
|
||||||
"ui.button.load": "加载数据",
|
|
||||||
"ui.button.update": "更新数据",
|
|
||||||
"ui.button.directUpdate": "直接更新",
|
|
||||||
"ui.button.files": "导出文件",
|
|
||||||
"ui.button.excel": "导出Excel",
|
|
||||||
"ui.button.srgf":"导出JSON",
|
|
||||||
"ui.button.url": "输入URL",
|
|
||||||
"ui.button.setting": "设置",
|
|
||||||
"ui.button.option": "选项",
|
|
||||||
"ui.button.startProxy": "代理模式",
|
|
||||||
"ui.button.solution": "解决办法",
|
|
||||||
"ui.button.cacheFolder": "打开网页缓存文件夹",
|
|
||||||
"ui.button.copyUrl": "复制URL",
|
|
||||||
"ui.select.newAccount": "新账号",
|
|
||||||
"ui.hint.newAccount": "从其它账号导出数据",
|
|
||||||
"ui.hint.init": "请先在游戏里打开任意一个抽卡记录后再点击“加载数据”按钮",
|
|
||||||
"ui.hint.lastUpdate": "上次数据更新时间为",
|
|
||||||
"ui.hint.relaunchHint": "更新已完成,点击按钮重启工具后生效",
|
|
||||||
"ui.hint.failed": "操作失败",
|
|
||||||
"ui.win.title": "绝区零调频记录导出工具",
|
|
||||||
"ui.data.total": "一共",
|
|
||||||
"ui.data.times": "抽",
|
|
||||||
"ui.data.sum": "已累计",
|
|
||||||
"ui.data.no4star": "抽未出S级",
|
|
||||||
"ui.data.character": "代理人",
|
|
||||||
"ui.data.weapon": "音擎",
|
|
||||||
"ui.data.bang": "邦布",
|
|
||||||
"ui.data.star4": "S级",
|
|
||||||
"ui.data.star3": "A级",
|
|
||||||
"ui.data.star2": "B级",
|
|
||||||
"ui.data.history": "S级历史记录",
|
|
||||||
"ui.data.average": "S级平均出货次数为",
|
|
||||||
"ui.data.chara4": "S级代理人",
|
|
||||||
"ui.data.chara3": "A级代理人",
|
|
||||||
"ui.data.weapon4": "S级音擎",
|
|
||||||
"ui.data.weapon3": "A级音擎",
|
|
||||||
"ui.data.weapon2": "B级音擎",
|
|
||||||
"ui.data.bang4": "S级邦布",
|
|
||||||
"ui.data.bang3": "A级邦布",
|
|
||||||
"ui.setting.title": "设置",
|
|
||||||
"ui.setting.language": "语言",
|
|
||||||
"ui.setting.languageHint": "缺少翻译时,会默认显示简体中文",
|
|
||||||
"ui.setting.logType": "日志类型",
|
|
||||||
"ui.setting.auto": "自动",
|
|
||||||
"ui.setting.cnServer": "国服",
|
|
||||||
"ui.setting.seaServer": "外服",
|
|
||||||
"ui.setting.logTypeHint": "使用游戏日志获取URL时,优先选择哪种服务器生成的日志文件。",
|
|
||||||
"ui.setting.dataManagerHint": "可以删除不需要的数据。",
|
|
||||||
"ui.setting.autoUpdate": "自动更新",
|
|
||||||
"ui.setting.proxyMode": "代理模式",
|
|
||||||
"ui.setting.proxyModeHint": "通过设置系统代理来获取URL,无法从日志中获取到有效的URL时才会启动代理服务器。",
|
|
||||||
"ui.setting.fetchFullHistory": "获取完整数据",
|
|
||||||
"ui.setting.fetchFullHistoryHint": "开启时点击“更新数据”按钮会完整获取6个月内所有的抽卡记录,当记录里有6个月范围以内的错误数据时可以通过这个功能修复。",
|
|
||||||
"ui.setting.closeProxy": "关闭系统代理",
|
|
||||||
"ui.setting.closeProxyHint": "如果使用过代理模式时工具非正常关闭,可能导致系统代理设置没能清除,可以通过这个按钮来清除设置过的系统代理。",
|
|
||||||
"ui.about.title": "关于",
|
|
||||||
"ui.about.license": "本工具为开源软件,源代码使用 MIT 协议授权",
|
|
||||||
"ui.urlDialog.title": "手动输入URL",
|
|
||||||
"ui.urlDialog.hint": "这个功能应当只在你理解这里需要什么URL时使用",
|
|
||||||
"ui.urlDialog.placeholder": "请输入带有身份认证信息的URL",
|
|
||||||
"ui.common.cancel": "取消",
|
|
||||||
"ui.common.ok": "确定",
|
|
||||||
"ui.common.data": "数据",
|
|
||||||
"ui.common.dataManage": "数据管理",
|
|
||||||
"ui.common.updateTime": "更新日期",
|
|
||||||
"ui.common.status": "状态",
|
|
||||||
"ui.common.action": "操作",
|
|
||||||
"ui.common.deleted": "已删除",
|
|
||||||
"ui.common.normal": "正常",
|
|
||||||
"ui.common.delete": "删除",
|
|
||||||
"ui.common.restore": "恢复",
|
|
||||||
"log.save.failed": "保存本地数据失败",
|
|
||||||
"log.file.notFound": "未找到游戏日志,确认是否已打开游戏抽卡记录",
|
|
||||||
"log.url.notFound": "未找到URL",
|
|
||||||
"log.file.readFailed": "读取日志失败",
|
|
||||||
"log.fetch.retry": "获取${name}第${page}页失败,5秒后进行第${count}次重试……",
|
|
||||||
"log.fetch.retryFailed": "获取${name}第${page}页失败,已超出重试次数",
|
|
||||||
"log.fetch.interval": "正在获取${name}第${page}页,每10页休息1秒……",
|
|
||||||
"log.fetch.current": "正在获取${name}第${page}页",
|
|
||||||
"log.fetch.authTimeout": "身份认证已过期,请重新打开游戏抽卡记录",
|
|
||||||
"log.fetch.gachaType": "正在获取调频活动类型",
|
|
||||||
"log.fetch.gachaTypeOk": "获取调频活动类型成功",
|
|
||||||
"log.url.lackAuth": "URL中缺少authkey",
|
|
||||||
"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": "类别",
|
|
||||||
"excel.header.rank": "星级",
|
|
||||||
"excel.header.total": "总次数",
|
|
||||||
"excel.header.pity": "保底内",
|
|
||||||
"excel.header.remark": "备注",
|
|
||||||
"excel.wish2": "调频2",
|
|
||||||
"excel.customFont": "微软雅黑",
|
|
||||||
"excel.filePrefix": "绝区零调频记录",
|
|
||||||
"excel.fileType": "Excel文件",
|
|
||||||
"srgf.fileType":"绝区零调频记录格式文件(SRGF)",
|
|
||||||
"ui.extra.cacheClean": "1. 确认是否已经打开游戏内的抽卡历史记录,如果仍然出现“身份认证已过期”的错误,再尝试下面的步骤\n2. 关闭绝区零的游戏窗口\n3. 点击上方的“打开缓存文件夹”按钮,打开Cache文件夹\n4. 删除Cache_Data文件夹\n5. 启动绝区零游戏,打开游戏内抽卡历史记录页面\n6. 关闭这个对话框,再点击“更新数据”按钮",
|
|
||||||
"ui.extra.findCacheFolder": "如果点“打开缓存文件夹”按钮没有反应,可以手动找到游戏的网页缓存文件夹,目录为“你的游戏安装路径/ZenlessZoneZero_Data/webCaches/Cache/”",
|
|
||||||
"ui.extra.urlCopied": "URL已复制"
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
{
|
|
||||||
"symbol.colon": ":",
|
|
||||||
"ui.button.load": "加載數據",
|
|
||||||
"ui.button.update": "更新數據",
|
|
||||||
"ui.button.directUpdate": "直接更新",
|
|
||||||
"ui.button.files": "導出文件",
|
|
||||||
"ui.button.excel": "導出Excel",
|
|
||||||
"ui.button.srgf":"導出JSON",
|
|
||||||
"ui.button.url": "輸入URL",
|
|
||||||
"ui.button.setting": "設置",
|
|
||||||
"ui.button.option": "選項",
|
|
||||||
"ui.button.startProxy": "代理模式",
|
|
||||||
"ui.button.solution": "解決辦法",
|
|
||||||
"ui.button.cacheFolder": "打開網頁緩存文件夾",
|
|
||||||
"ui.button.copyUrl": "復製URL",
|
|
||||||
"ui.select.newAccount": "新賬號",
|
|
||||||
"ui.hint.newAccount": "從其它賬號導出數據",
|
|
||||||
"ui.hint.init": "請先在遊戲裏打開任意一個抽卡記錄後再點擊「加載數據」按鈕",
|
|
||||||
"ui.hint.lastUpdate": "上次數據更新時間為",
|
|
||||||
"ui.hint.failed": "操作失敗",
|
|
||||||
"ui.win.title": "絕區零調頻記錄導出工具",
|
|
||||||
"ui.data.total": "一共",
|
|
||||||
"ui.data.times": "抽",
|
|
||||||
"ui.data.sum": "已累計",
|
|
||||||
"ui.data.no4star": "抽未出S級",
|
|
||||||
"ui.data.character": "代理人",
|
|
||||||
"ui.data.weapon": "音擎",
|
|
||||||
"ui.data.bang": "邦布",
|
|
||||||
"ui.data.star4": "S級",
|
|
||||||
"ui.data.star3": "A級",
|
|
||||||
"ui.data.star2": "B級",
|
|
||||||
"ui.data.history": "S級歷史記錄",
|
|
||||||
"ui.data.average": "S級平均出貨次數為",
|
|
||||||
"ui.data.chara4": "S級代理人",
|
|
||||||
"ui.data.chara3": "A級代理人",
|
|
||||||
"ui.data.weapon4": "S級音擎",
|
|
||||||
"ui.data.weapon3": "A級音擎",
|
|
||||||
"ui.data.weapon2": "B級音擎",
|
|
||||||
"ui.data.bang4": "S級邦布",
|
|
||||||
"ui.data.bang3": "A級邦布",
|
|
||||||
"ui.setting.title": "設置",
|
|
||||||
"ui.setting.language": "語言",
|
|
||||||
"ui.setting.languageHint": "缺少翻譯時,會默認顯示簡體中文",
|
|
||||||
"ui.setting.logType": "日誌類型",
|
|
||||||
"ui.setting.auto": "自動",
|
|
||||||
"ui.setting.cnServer": "國服",
|
|
||||||
"ui.setting.seaServer": "外服",
|
|
||||||
"ui.setting.logTypeHint": "使用遊戲日誌獲取URL時,優先選擇哪種服務器生成的日誌文件。",
|
|
||||||
"ui.setting.dataManagerHint": "可以刪除不需要的數據。",
|
|
||||||
"ui.setting.autoUpdate": "自動更新",
|
|
||||||
"ui.setting.proxyMode": "代理模式",
|
|
||||||
"ui.setting.proxyModeHint": "通過設置系統代理來獲取URL,無法從日誌中獲取到有效的URL時才會啟動代理服務器。",
|
|
||||||
"ui.setting.fetchFullHistory": "獲取完整數據",
|
|
||||||
"ui.setting.fetchFullHistoryHint": "開啟時點擊「更新數據」按鈕會完整獲取6個月內所有的抽卡記錄,當記錄裏有6個月範圍以內的錯誤數據時可以通過這個功能修復。",
|
|
||||||
"ui.setting.closeProxy": "關閉系統代理",
|
|
||||||
"ui.setting.closeProxyHint": "如果使用過代理模式時工具非正常關閉,可能導致系統代理設置沒能清除,可以通過這個按鈕來清除設置過的系統代理。",
|
|
||||||
"ui.about.title": "關於",
|
|
||||||
"ui.about.license": "本工具為開源軟件,源代碼使用 MIT 協議授權",
|
|
||||||
"ui.urlDialog.title": "手動輸入URL",
|
|
||||||
"ui.urlDialog.hint": "這個功能應當只在你理解這裏需要什麽URL時使用",
|
|
||||||
"ui.urlDialog.placeholder": "請輸入帶有身份認證信息的URL",
|
|
||||||
"ui.common.cancel": "取消",
|
|
||||||
"ui.common.ok": "確定",
|
|
||||||
"ui.common.data": "數據",
|
|
||||||
"ui.common.dataManage": "數據管理",
|
|
||||||
"ui.common.updateTime": "更新日期",
|
|
||||||
"ui.common.status": "狀態",
|
|
||||||
"ui.common.action": "操作",
|
|
||||||
"ui.common.deleted": "已刪除",
|
|
||||||
"ui.common.normal": "正常",
|
|
||||||
"ui.common.delete": "刪除",
|
|
||||||
"ui.common.restore": "恢復",
|
|
||||||
"log.save.failed": "保存本地數據失敗",
|
|
||||||
"log.file.notFound": "未找到遊戲日誌,確認是否已打開遊戲抽卡記錄",
|
|
||||||
"log.url.notFound": "未找到URL",
|
|
||||||
"log.file.readFailed": "讀取日誌失敗",
|
|
||||||
"log.fetch.retry": "獲取${name}第${page}頁失敗,5秒後進行第${count}次重試……",
|
|
||||||
"log.fetch.retryFailed": "獲取${name}第${page}頁失敗,已超出重試次數",
|
|
||||||
"log.fetch.interval": "正在獲取${name}第${page}頁,每10頁休息1秒……",
|
|
||||||
"log.fetch.current": "正在獲取${name}第${page}頁",
|
|
||||||
"log.fetch.authTimeout": "身份認證已過期,請重新打開遊戲抽卡記錄",
|
|
||||||
"log.fetch.gachaType": "正在獲取調頻活動類型",
|
|
||||||
"log.fetch.gachaTypeOk": "獲取調頻活動類型成功",
|
|
||||||
"log.url.lackAuth": "URL中缺少authkey",
|
|
||||||
"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": "類別",
|
|
||||||
"excel.header.rank": "星級",
|
|
||||||
"excel.header.total": "總次數",
|
|
||||||
"excel.header.pity": "保底內",
|
|
||||||
"excel.header.remark": "備註",
|
|
||||||
"excel.wish2": "調頻2",
|
|
||||||
"excel.customFont": "微軟雅黑",
|
|
||||||
"excel.filePrefix": "絕區零調頻記錄",
|
|
||||||
"excel.fileType": "Excel文件",
|
|
||||||
"srgf.fileType":"絕區零調頻記錄格式文件(SRGF)",
|
|
||||||
"ui.extra.cacheClean": "1. 確認是否已經打開遊戲內的抽卡歷史記錄,如果仍然出現「身份認證已過期」的錯誤,再嘗試下面的步驟\n2. 關閉絕區零的遊戲窗口\n3. 點擊上方的「打開緩存文件夾」按鈕,打開Cache文件夾\n4. 刪除Cache_Data文件夾\n5. 啟動絕區零遊戲,打開遊戲內抽卡歷史記錄頁面\n6. 關閉這個對話框,再點擊「更新數據」按鈕",
|
|
||||||
"ui.extra.findCacheFolder": "如果點「打開緩存文件夾」按鈕沒有反應,可以手動找到遊戲的網頁緩存文件夾,目錄為「你的遊戲安裝路徑/ZenlessZoneZero_Data/webCaches/Cache/」",
|
|
||||||
"ui.extra.urlCopied": "URL已復製"
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
const { app, ipcMain, dialog } = require('electron')
|
|
||||||
const fs = require('fs-extra')
|
|
||||||
const path = require('path')
|
|
||||||
const getData = require('./getData').getData
|
|
||||||
const { version } = require('../../package.json')
|
|
||||||
const i18n = require('./i18n')
|
|
||||||
|
|
||||||
const getTimeString = () => {
|
|
||||||
return new Date().toLocaleString('sv').replace(/[- :]/g, '').slice(0, -2)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (date) => {
|
|
||||||
let y = date.getFullYear()
|
|
||||||
let m = `${date.getMonth()+1}`.padStart(2, '0')
|
|
||||||
let d = `${date.getDate()}`.padStart(2, '0')
|
|
||||||
return `${y}-${m}-${d} ${date.toLocaleString('zh-cn', { hour12: false }).slice(-8)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = async () => {
|
|
||||||
const { dataMap, current } = await getData()
|
|
||||||
const data = dataMap.get(current)
|
|
||||||
if (!data.result.size) {
|
|
||||||
throw new Error('数据为空')
|
|
||||||
}
|
|
||||||
const result = {
|
|
||||||
info: {
|
|
||||||
uid: data.uid,
|
|
||||||
lang: data.lang,
|
|
||||||
export_time: formatDate(new Date()),
|
|
||||||
export_timestamp: Math.ceil(Date.now() / 1000),
|
|
||||||
export_app: 'zzz-signal-search-export',
|
|
||||||
export_app_version: `v${version}`,
|
|
||||||
region_time_zone: data.region_time_zone,
|
|
||||||
srgf_version: 'v1.0'
|
|
||||||
},
|
|
||||||
list: []
|
|
||||||
}
|
|
||||||
const listTemp = []
|
|
||||||
for (let [type, arr] of data.result) {
|
|
||||||
arr.forEach(log => {
|
|
||||||
listTemp.push({
|
|
||||||
gacha_id: log.gacha_id,
|
|
||||||
gacha_type: log.gacha_type,
|
|
||||||
item_id: log.item_id,
|
|
||||||
count: '1',
|
|
||||||
time: log.time,
|
|
||||||
name: log.name,
|
|
||||||
item_type: log.item_type,
|
|
||||||
rank_type: log.rank_type,
|
|
||||||
id: log.id
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
listTemp.sort((a, b) => Number(BigInt(a.id) - BigInt(b.id)))
|
|
||||||
listTemp.forEach(item => {
|
|
||||||
result.list.push({
|
|
||||||
...item
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const filePath = dialog.showSaveDialogSync({
|
|
||||||
defaultPath: path.join(app.getPath('downloads'), `SRGF_${data.uid}_${getTimeString()}`),
|
|
||||||
filters: [
|
|
||||||
{ name: i18n.srgf.fileType, extensions: ['json'] }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
if (filePath) {
|
|
||||||
await fs.ensureFile(filePath)
|
|
||||||
await fs.writeFile(filePath, JSON.stringify(result))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle('EXPORT_SRGF_JSON', async () => {
|
|
||||||
await start()
|
|
||||||
})
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
const { clipboard, ipcMain } = require('electron')
|
|
||||||
const { getUrl, deleteData } = require('./getData')
|
|
||||||
|
|
||||||
ipcMain.handle('COPY_URL', async () => {
|
|
||||||
const url = await getUrl()
|
|
||||||
if (url) {
|
|
||||||
clipboard.writeText(url)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('DELETE_DATA', async (event, uid, action) => {
|
|
||||||
await deleteData(uid, action)
|
|
||||||
})
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
const { readJSON, saveJSON, decipherAes, cipherAes, detectLocale, userDataPath } = require('./utils')
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
urls: [],
|
|
||||||
logType: 0,
|
|
||||||
lang: detectLocale(),
|
|
||||||
current: 0,
|
|
||||||
proxyPort: 8325,
|
|
||||||
proxyMode: false,
|
|
||||||
autoUpdate: true,
|
|
||||||
fetchFullHistory: false,
|
|
||||||
hideNovice: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLocalConfig = async () => {
|
|
||||||
let localConfig = await readJSON(userDataPath, 'config.json')
|
|
||||||
|
|
||||||
if (!localConfig) return
|
|
||||||
const configTemp = {}
|
|
||||||
for (let key in localConfig) {
|
|
||||||
if (typeof config[key] !== 'undefined') {
|
|
||||||
configTemp[key] = localConfig[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
configTemp.urls.forEach(item => {
|
|
||||||
try {
|
|
||||||
item[1] = decipherAes(item[1])
|
|
||||||
} catch (e) {
|
|
||||||
item[1] = ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
Object.assign(config, configTemp)
|
|
||||||
}
|
|
||||||
|
|
||||||
getLocalConfig()
|
|
||||||
|
|
||||||
let urlsMap = null
|
|
||||||
const setConfig = (key, value) => {
|
|
||||||
Reflect.set(config, key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveConfig = async () => {
|
|
||||||
let configTemp = config
|
|
||||||
if (urlsMap) {
|
|
||||||
const urls = [...urlsMap]
|
|
||||||
urls.forEach(item => {
|
|
||||||
try {
|
|
||||||
item[1] = cipherAes(item[1])
|
|
||||||
} catch (e) {
|
|
||||||
item[1] = ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
configTemp = Object.assign({}, config, { urls })
|
|
||||||
}
|
|
||||||
await saveJSON('config.json', configTemp)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getPlainConfig = () => config
|
|
||||||
|
|
||||||
const configProxy = new Proxy(config, {
|
|
||||||
get: function (obj, prop) {
|
|
||||||
if (prop === 'urls') {
|
|
||||||
if (!urlsMap) {
|
|
||||||
urlsMap = new Map(obj[prop])
|
|
||||||
}
|
|
||||||
return urlsMap
|
|
||||||
} else if (prop === 'set') {
|
|
||||||
return setConfig
|
|
||||||
} else if (prop === 'save') {
|
|
||||||
return saveConfig
|
|
||||||
} else if (prop === 'value') {
|
|
||||||
return getPlainConfig
|
|
||||||
}
|
|
||||||
return obj[prop]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = configProxy
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
const ExcelJS = require('./module/exceljs.min.js')
|
|
||||||
const getData = require('./getData').getData
|
|
||||||
const { app, ipcMain, dialog } = require('electron')
|
|
||||||
const fs = require('fs-extra')
|
|
||||||
const path = require('path')
|
|
||||||
const i18n = require('./i18n')
|
|
||||||
|
|
||||||
function pad(num) {
|
|
||||||
return `${num}`.padStart(2, "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTimeString() {
|
|
||||||
const d = new Date();
|
|
||||||
const YYYY = d.getFullYear();
|
|
||||||
const MM = pad(d.getMonth() + 1);
|
|
||||||
const DD = pad(d.getDate());
|
|
||||||
const HH = pad(d.getHours());
|
|
||||||
const mm = pad(d.getMinutes());
|
|
||||||
const ss = pad(d.getSeconds());
|
|
||||||
return `${YYYY}${MM}${DD}_${HH}${mm}${ss}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const addRawSheet = (workbook, data) => {
|
|
||||||
const sheet = workbook.addWorksheet('rawData', {views: [{state: 'frozen', ySplit: 1}]})
|
|
||||||
const excelKeys = ['gacha_id', 'gacha_type', 'id', 'item_id', 'item_type', 'lang', 'name', 'rank_type', 'time', 'uid']
|
|
||||||
sheet.columns = excelKeys.map((key, index) => {
|
|
||||||
return {
|
|
||||||
header: key,
|
|
||||||
key,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const temp = []
|
|
||||||
for (let [key, value] of data.result) {
|
|
||||||
for (let log of value){
|
|
||||||
const arr = []
|
|
||||||
arr.push(log.gacha_id)
|
|
||||||
arr.push(log.gacha_type)
|
|
||||||
arr.push(log.id)
|
|
||||||
arr.push(log.item_id)
|
|
||||||
arr.push(log.item_type)
|
|
||||||
arr.push(data.lang)
|
|
||||||
arr.push(log.name)
|
|
||||||
arr.push(log.rank_type)
|
|
||||||
arr.push(log.time)
|
|
||||||
arr.push(data.uid)
|
|
||||||
temp.push(arr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sheet.addRows(temp)
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = async () => {
|
|
||||||
const { header, customFont, filePrefix, fileType, wish2 } = i18n.excel
|
|
||||||
const { dataMap, current } = await getData()
|
|
||||||
const data = dataMap.get(current)
|
|
||||||
// https://github.com/sunfkny/genshin-gacha-export-js/blob/main/index.js
|
|
||||||
const workbook = new ExcelJS.Workbook()
|
|
||||||
for (let [key, value] of data.result) {
|
|
||||||
const name = data.typeMap.get(key)
|
|
||||||
const sheet = workbook.addWorksheet(name.replace(/[*?:\/\\]/g, ' '), {views: [{state: 'frozen', ySplit: 1}]})
|
|
||||||
let width = [24, 14, 8, 8, 8, 8, 8]
|
|
||||||
if (!data.lang.includes('zh-')) {
|
|
||||||
width = [24, 32, 16, 12, 12, 12, 8]
|
|
||||||
}
|
|
||||||
const excelKeys = ['time', 'name', 'type', 'rank', 'total', 'pity', 'remark']
|
|
||||||
sheet.columns = excelKeys.map((key, index) => {
|
|
||||||
return {
|
|
||||||
header: header[key],
|
|
||||||
key,
|
|
||||||
width: width[index]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// get gacha logs
|
|
||||||
const logs = value
|
|
||||||
let total = 0
|
|
||||||
let pity = 0
|
|
||||||
const temp = []
|
|
||||||
for (let log of logs) {
|
|
||||||
const arr = []
|
|
||||||
total += 1
|
|
||||||
pity += 1
|
|
||||||
arr.push(log.time)
|
|
||||||
arr.push(log.name)
|
|
||||||
arr.push(log.item_type)
|
|
||||||
arr.push(log.rank_type)
|
|
||||||
arr.push(total)
|
|
||||||
arr.push(pity)
|
|
||||||
temp.push(arr)
|
|
||||||
if (log.rank_type === '5') {
|
|
||||||
pity = 0
|
|
||||||
}
|
|
||||||
// if (key === '301') {
|
|
||||||
// if (log.gacha_type === '400') {
|
|
||||||
// log.push(wish2)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
sheet.addRows(temp)
|
|
||||||
// set xlsx hearer style
|
|
||||||
;(["A", "B", "C", "D","E","F", "G"]).forEach((v) => {
|
|
||||||
sheet.getCell(`${v}1`).border = {
|
|
||||||
top: {style:'thin', color: {argb:'ffc4c2bf'}},
|
|
||||||
left: {style:'thin', color: {argb:'ffc4c2bf'}},
|
|
||||||
bottom: {style:'thin', color: {argb:'ffc4c2bf'}},
|
|
||||||
right: {style:'thin', color: {argb:'ffc4c2bf'}}
|
|
||||||
}
|
|
||||||
sheet.getCell(`${v}1`).fill = {
|
|
||||||
type: 'pattern',
|
|
||||||
pattern:'solid',
|
|
||||||
fgColor:{argb:'ffdbd7d3'},
|
|
||||||
}
|
|
||||||
sheet.getCell(`${v}1`).font ={
|
|
||||||
name: customFont,
|
|
||||||
color: { argb: "ff757575" },
|
|
||||||
bold : true
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
// set xlsx cell style
|
|
||||||
logs.forEach((v, i) => {
|
|
||||||
;(["A", "B", "C", "D","E","F", "G"]).forEach((c) => {
|
|
||||||
sheet.getCell(`${c}${i + 2}`).border = {
|
|
||||||
top: {style:'thin', color: {argb:'ffc4c2bf'}},
|
|
||||||
left: {style:'thin', color: {argb:'ffc4c2bf'}},
|
|
||||||
bottom: {style:'thin', color: {argb:'ffc4c2bf'}},
|
|
||||||
right: {style:'thin', color: {argb:'ffc4c2bf'}}
|
|
||||||
}
|
|
||||||
sheet.getCell(`${c}${i + 2}`).fill = {
|
|
||||||
type: 'pattern',
|
|
||||||
pattern:'solid',
|
|
||||||
fgColor:{argb:'ffebebeb'},
|
|
||||||
}
|
|
||||||
// rare rank background color
|
|
||||||
const rankColor = {
|
|
||||||
3: "ff8e8e8e",
|
|
||||||
4: "ffa256e1",
|
|
||||||
5: "ffbd6932",
|
|
||||||
}
|
|
||||||
sheet.getCell(`${c}${i + 2}`).font = {
|
|
||||||
name: customFont,
|
|
||||||
color: { argb: rankColor[v.rank_type] },
|
|
||||||
bold : v.rank_type != "3"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
addRawSheet(workbook, data)
|
|
||||||
|
|
||||||
const buffer = await workbook.xlsx.writeBuffer()
|
|
||||||
const filePath = dialog.showSaveDialogSync({
|
|
||||||
defaultPath: path.join(app.getPath('downloads'), `${filePrefix}_${getTimeString()}`),
|
|
||||||
filters: [
|
|
||||||
{ name: fileType, extensions: ['xlsx'] }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
if (filePath) {
|
|
||||||
await fs.ensureFile(filePath)
|
|
||||||
await fs.writeFile(filePath, buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle('SAVE_EXCEL', async () => {
|
|
||||||
await start()
|
|
||||||
})
|
|
||||||
@@ -1,524 +0,0 @@
|
|||||||
const fs = require('fs-extra')
|
|
||||||
const util = require('util')
|
|
||||||
const path = require('path')
|
|
||||||
const { URL } = require('url')
|
|
||||||
const { app, ipcMain, shell } = require('electron')
|
|
||||||
const { sleep, request, sendMsg, readJSON, saveJSON, detectLocale, getCacheText, userDataPath, userPath, localIp, langMap } = require('./utils')
|
|
||||||
const config = require('./config')
|
|
||||||
const i18n = require('./i18n')
|
|
||||||
const { enableProxy, disableProxy } = require('./module/system-proxy')
|
|
||||||
const mitmproxy = require('./module/node-mitmproxy')
|
|
||||||
const { mergeData } = require('./utils/mergeData')
|
|
||||||
const gachaTypeRaw = require('../gachaType.json')
|
|
||||||
|
|
||||||
const dataMap = new Map()
|
|
||||||
const order = ['2', '3', '1', '5']
|
|
||||||
let apiDomain = 'https://public-operation-nap.mihoyo.com'
|
|
||||||
|
|
||||||
const saveData = async (data, url) => {
|
|
||||||
const obj = Object.assign({}, data)
|
|
||||||
obj.result = [...obj.result]
|
|
||||||
await config.save()
|
|
||||||
await saveJSON(`gacha-list-${data.uid}.json`, obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultTypeMap = new Map([
|
|
||||||
['2', '独家频段'],
|
|
||||||
['3', '音擎频段'],
|
|
||||||
['1', '常驻频段'],
|
|
||||||
['5', '邦布频段']
|
|
||||||
])
|
|
||||||
|
|
||||||
const findDataFiles = async (dataPath, fileMap) => {
|
|
||||||
const files = await readdir(dataPath)
|
|
||||||
if (files?.length) {
|
|
||||||
for (let name of files) {
|
|
||||||
if (/^gacha-list-\d+\.json$/.test(name) && !fileMap.has(name)) {
|
|
||||||
fileMap.set(name, dataPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const collectDataFiles = async () => {
|
|
||||||
await fs.ensureDir(userDataPath)
|
|
||||||
const fileMap = new Map()
|
|
||||||
await findDataFiles(userDataPath, fileMap)
|
|
||||||
return fileMap
|
|
||||||
}
|
|
||||||
|
|
||||||
let localDataReaded = false
|
|
||||||
const readdir = util.promisify(fs.readdir)
|
|
||||||
const readData = async () => {
|
|
||||||
if (localDataReaded) return
|
|
||||||
localDataReaded = true
|
|
||||||
const fileMap = await collectDataFiles()
|
|
||||||
for (let [name, dataPath] of fileMap) {
|
|
||||||
try {
|
|
||||||
const data = await readJSON(dataPath, name)
|
|
||||||
data.typeMap = new Map(data.typeMap) || defaultTypeMap
|
|
||||||
data.result = new Map(data.result)
|
|
||||||
if (data.uid) {
|
|
||||||
dataMap.set(data.uid, data)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
sendMsg(e, 'ERROR')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((!config.current && dataMap.size) || (config.current && dataMap.size && !dataMap.has(config.current))) {
|
|
||||||
await changeCurrent(dataMap.keys().next().value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteData = async (uid, action) => {
|
|
||||||
const data = dataMap.get(uid)
|
|
||||||
if (data) {
|
|
||||||
data.deleted = action
|
|
||||||
await saveData(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeCurrent = async (uid) => {
|
|
||||||
config.current = uid
|
|
||||||
await config.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
const detectGameLocale = async (userPath) => {
|
|
||||||
let list = []
|
|
||||||
const lang = app.getLocale()
|
|
||||||
const arr = ['/miHoYo/绝区零/', '/Cognosphere/Zenless Zone Zero/']
|
|
||||||
arr.forEach(str => {
|
|
||||||
try {
|
|
||||||
const pathname = path.join(userPath, '/AppData/LocalLow/', str, 'Player.log')
|
|
||||||
fs.accessSync(pathname, fs.constants.F_OK)
|
|
||||||
list.push(pathname)
|
|
||||||
} catch (e) {}
|
|
||||||
})
|
|
||||||
if (config.logType) {
|
|
||||||
if (config.logType === 2) {
|
|
||||||
list.reverse()
|
|
||||||
}
|
|
||||||
list = list.slice(0, 1)
|
|
||||||
} else if (lang !== 'zh-CN') {
|
|
||||||
list.reverse()
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
const getLatestUrl = (list) => {
|
|
||||||
let result = list[list.length - 1]
|
|
||||||
// let time = 0
|
|
||||||
// for (let i = 0; i < list.length; i++) {
|
|
||||||
// const tsMch = list[i].match(/timestamp=(\d+)/)
|
|
||||||
// if (tsMch?.[1]) {
|
|
||||||
// const ts = parseInt(tsMch[1])
|
|
||||||
// if (time <= parseInt(tsMch[1])) {
|
|
||||||
// time = ts
|
|
||||||
// result = list[i]
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
let cacheFolder = null
|
|
||||||
const readLog = async () => {
|
|
||||||
const text = i18n.log
|
|
||||||
try {
|
|
||||||
let userPath
|
|
||||||
if (!process.env.WINEPREFIX) {
|
|
||||||
userPath = app.getPath('home')
|
|
||||||
} else {
|
|
||||||
userPath = path.join(process.env.WINEPREFIX, 'drive_c/users', process.env.USER)
|
|
||||||
}
|
|
||||||
const logPaths = await detectGameLocale(userPath)
|
|
||||||
if (!logPaths.length) {
|
|
||||||
sendMsg(text.file.notFound)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const promises = logPaths.map(async logpath => {
|
|
||||||
const logText = await fs.readFile(logpath, 'utf8')
|
|
||||||
const url = logText.match(/https:\/\/.*?\/info/g)
|
|
||||||
if (url) {
|
|
||||||
return getLatestUrl(url)
|
|
||||||
}
|
|
||||||
const gamePathMch = logText.match(/([A-Z]:\/.*?\/)(?=ZenlessZoneZero_Data)/i)
|
|
||||||
if (gamePathMch) {
|
|
||||||
const[cacheText, cacheFile] = await getCacheText(gamePathMch[0]+"/ZenlessZoneZero_Data")
|
|
||||||
const urlMch = cacheText.match(/https.+?authkey=.+?end_id=/g)
|
|
||||||
if (urlMch) {
|
|
||||||
cacheFolder = cacheFile.replace(/Cache_Data[/\\]data_2$/, '')
|
|
||||||
return getLatestUrl(urlMch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
const result = await Promise.all(promises)
|
|
||||||
for (let url of result) {
|
|
||||||
if (url) {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sendMsg(text.url.notFound)
|
|
||||||
return false
|
|
||||||
} catch (e) {
|
|
||||||
sendMsg(text.file.readFailed)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getGachaLog = async ({ key, page, name, retryCount, url, endId }) => {
|
|
||||||
const text = i18n.log
|
|
||||||
try {
|
|
||||||
const res = await request(`${url}&real_gacha_type=${key}&page=${page}&size=${20}${endId ? '&end_id=' + endId : ''}`)
|
|
||||||
if (res?.data?.list) {
|
|
||||||
return res?.data
|
|
||||||
}
|
|
||||||
throw new Error(res?.message || res)
|
|
||||||
} catch (e) {
|
|
||||||
if (retryCount) {
|
|
||||||
sendMsg(i18n.parse(text.fetch.retry, { name, page, count: 6 - retryCount }))
|
|
||||||
await sleep(5)
|
|
||||||
retryCount--
|
|
||||||
return await getGachaLog({ key, page, name, retryCount, url, endId })
|
|
||||||
} else {
|
|
||||||
sendMsg(i18n.parse(text.fetch.retryFailed, { name, page }))
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getGachaLogs = async ({ name, key }, queryString) => {
|
|
||||||
const text = i18n.log
|
|
||||||
let page = 1
|
|
||||||
let list = []
|
|
||||||
let res = null
|
|
||||||
let logs = []
|
|
||||||
let uid = ''
|
|
||||||
let region = ''
|
|
||||||
let region_time_zone = ''
|
|
||||||
let endId = '0'
|
|
||||||
const url = `${apiDomain}/common/gacha_record/api/getGachaLog?${queryString}`
|
|
||||||
do {
|
|
||||||
if (page % 10 === 0) {
|
|
||||||
sendMsg(i18n.parse(text.fetch.interval, { name, page }))
|
|
||||||
await sleep(1)
|
|
||||||
}
|
|
||||||
sendMsg(i18n.parse(text.fetch.current, { name, page }))
|
|
||||||
res = await getGachaLog({ key, page, name, url, endId, retryCount: 5 })
|
|
||||||
await sleep(0.3)
|
|
||||||
logs = res?.list || []
|
|
||||||
if (!uid && logs.length) {
|
|
||||||
uid = logs[0].uid
|
|
||||||
}
|
|
||||||
if (!region) {
|
|
||||||
region = res.region
|
|
||||||
}
|
|
||||||
if (!region_time_zone) {
|
|
||||||
region_time_zone = res.region_time_zone
|
|
||||||
}
|
|
||||||
list.push(...logs)
|
|
||||||
page += 1
|
|
||||||
|
|
||||||
if (logs.length) {
|
|
||||||
endId = logs[logs.length - 1].id
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.fetchFullHistory && logs.length && uid && dataMap.has(uid)) {
|
|
||||||
const result = dataMap.get(uid).result
|
|
||||||
if (result.has(key)) {
|
|
||||||
const arr = result.get(key)
|
|
||||||
if (arr.length) {
|
|
||||||
const localLatestId = arr[arr.length - 1].id
|
|
||||||
if (localLatestId) {
|
|
||||||
let shouldBreak = false
|
|
||||||
logs.forEach(item => {
|
|
||||||
if (item.id === localLatestId) {
|
|
||||||
shouldBreak = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (shouldBreak) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (logs.length > 0)
|
|
||||||
return { list, uid, region, region_time_zone }
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkResStatus = (res) => {
|
|
||||||
const text = i18n.log
|
|
||||||
if (res.retcode !== 0) {
|
|
||||||
let message = res.message
|
|
||||||
if (res.message === 'authkey timeout') {
|
|
||||||
message = text.fetch.authTimeout
|
|
||||||
sendMsg(true, 'AUTHKEY_TIMEOUT')
|
|
||||||
}
|
|
||||||
sendMsg(message)
|
|
||||||
throw new Error(message)
|
|
||||||
}
|
|
||||||
sendMsg(false, 'AUTHKEY_TIMEOUT')
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
const tryGetUid = async (queryString) => {
|
|
||||||
const url = `${apiDomain}/common/gacha_record/api/getGachaLog?${queryString}`
|
|
||||||
try {
|
|
||||||
for (let [key] of defaultTypeMap) {
|
|
||||||
const res = await request(`${url}&real_gacha_type=${key}&page=1&size=6`)
|
|
||||||
if (res.data.list && res.data.list.length) {
|
|
||||||
return res.data.list[0].uid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
return config.current
|
|
||||||
}
|
|
||||||
|
|
||||||
const gachaTypeMap = new Map(gachaTypeRaw)
|
|
||||||
const getGachaType = (lang) => {
|
|
||||||
const locale = detectLocale(lang)
|
|
||||||
return gachaTypeMap.get(locale || lang)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fixAuthkey = (url) => {
|
|
||||||
const mr = url.match(/authkey=([^&]+)/)
|
|
||||||
if (mr && mr[1] && mr[1].includes('=') && !mr[1].includes('%')) {
|
|
||||||
return url.replace(/authkey=([^&]+)/, `authkey=${encodeURIComponent(mr[1])}`)
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
const getQuerystring = (url) => {
|
|
||||||
const text = i18n.log
|
|
||||||
const { searchParams, host } = new URL(fixAuthkey(url))
|
|
||||||
if (host.includes('webstatic-sea') || host.includes('hoyoverse.com')) {
|
|
||||||
apiDomain = 'https://public-operation-nap-sg.hoyoverse.com'
|
|
||||||
} else {
|
|
||||||
apiDomain = 'https://public-operation-nap.mihoyo.com'
|
|
||||||
}
|
|
||||||
const authkey = searchParams.get('authkey')
|
|
||||||
if (!authkey) {
|
|
||||||
sendMsg(text.url.lackAuth)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
searchParams.delete('page')
|
|
||||||
searchParams.delete('size')
|
|
||||||
searchParams.delete('real_gacha_type')
|
|
||||||
searchParams.delete('end_id')
|
|
||||||
return searchParams
|
|
||||||
}
|
|
||||||
|
|
||||||
const proxyServer = (port) => {
|
|
||||||
return new Promise((rev) => {
|
|
||||||
mitmproxy.createProxy({
|
|
||||||
sslConnectInterceptor: (req, cltSocket, head) => {
|
|
||||||
if (/webstatic([^\.]{2,10})?\.(mihoyo|hoyoverse)\.com/.test(req.url)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
requestInterceptor: (rOptions, req, res, ssl, next) => {
|
|
||||||
next()
|
|
||||||
if (/webstatic([^\.]{2,10})?\.(mihoyo|hoyoverse)\.com/.test(rOptions.hostname)) {
|
|
||||||
if (/authkey=[^&]+/.test(rOptions.path)) {
|
|
||||||
rev(`${rOptions.protocol}//${rOptions.hostname}${rOptions.path}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
responseInterceptor: (req, res, proxyReq, proxyRes, ssl, next) => {
|
|
||||||
next()
|
|
||||||
},
|
|
||||||
getPath: () => path.join(userPath, 'node-mitmproxy'),
|
|
||||||
port
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let proxyServerPromise
|
|
||||||
const useProxy = async () => {
|
|
||||||
const text = i18n.log
|
|
||||||
const ip = localIp()
|
|
||||||
const port = config.proxyPort
|
|
||||||
sendMsg(i18n.parse(text.proxy.hint, { ip, port }))
|
|
||||||
await enableProxy('127.0.0.1', port)
|
|
||||||
if (!proxyServerPromise) {
|
|
||||||
proxyServerPromise = proxyServer(port)
|
|
||||||
}
|
|
||||||
const url = await proxyServerPromise
|
|
||||||
await disableProxy()
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUrlFromConfig = () => {
|
|
||||||
if (config.urls.size) {
|
|
||||||
if (config.current && config.urls.has(config.current)) {
|
|
||||||
const url = config.urls.get(config.current)
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tryRequest = async (url, retry = false) => {
|
|
||||||
const queryString = getQuerystring(url)
|
|
||||||
if (!queryString) return false
|
|
||||||
const gachaTypeUrl = `${apiDomain}/common/gacha_record/api/getGachaLog?${queryString}&page=1&size=5&real_gacha_type=1&end_id=0`
|
|
||||||
try {
|
|
||||||
const res = await request(gachaTypeUrl)
|
|
||||||
checkResStatus(res)
|
|
||||||
} catch (e) {
|
|
||||||
if (e.code === 'ERR_PROXY_CONNECTION_FAILED' && !retry) {
|
|
||||||
await disableProxy()
|
|
||||||
return await tryRequest(url, true)
|
|
||||||
}
|
|
||||||
sendMsg(e.message.replace(url, '***'), 'ERROR')
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUrl = async () => {
|
|
||||||
let url = await readLog()
|
|
||||||
if (!url && config.proxyMode) {
|
|
||||||
url = await useProxy()
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchData = async (urlOverride) => {
|
|
||||||
const text = i18n.log
|
|
||||||
await readData()
|
|
||||||
let url = urlOverride
|
|
||||||
if (!url) {
|
|
||||||
url = await getUrl()
|
|
||||||
}
|
|
||||||
if (!url) {
|
|
||||||
const message = text.url.notFound2
|
|
||||||
sendMsg(message)
|
|
||||||
throw new Error(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
await tryRequest(url)
|
|
||||||
|
|
||||||
const searchParams = getQuerystring(url)
|
|
||||||
if (!searchParams) {
|
|
||||||
const message = text.url.incorrect
|
|
||||||
sendMsg(message)
|
|
||||||
throw new Error(message)
|
|
||||||
}
|
|
||||||
let queryString = searchParams.toString()
|
|
||||||
const vUid = await tryGetUid(queryString)
|
|
||||||
const localLang = dataMap.has(vUid) ? dataMap.get(vUid).lang : ''
|
|
||||||
if (localLang) {
|
|
||||||
searchParams.set('lang', localLang)
|
|
||||||
}
|
|
||||||
queryString = searchParams.toString()
|
|
||||||
const gachaType = await getGachaType(searchParams.get('lang'))
|
|
||||||
|
|
||||||
const result = new Map()
|
|
||||||
const typeMap = new Map()
|
|
||||||
const lang = searchParams.get('lang')
|
|
||||||
let originUid = ''
|
|
||||||
let originRegion = ''
|
|
||||||
let originTimeZone = ''
|
|
||||||
for (const type of gachaType) {
|
|
||||||
const { list, uid, region, region_time_zone } = await getGachaLogs(type, queryString)
|
|
||||||
const logs = list.map((item) => {
|
|
||||||
const { id, item_id, item_type, name, rank_type, time, gacha_id, gacha_type } = item
|
|
||||||
return { id, item_id, item_type, name, rank_type, time, gacha_id, gacha_type }
|
|
||||||
})
|
|
||||||
logs.reverse()
|
|
||||||
typeMap.set(type.key, type.name)
|
|
||||||
result.set(type.key, logs)
|
|
||||||
if (!originUid) {
|
|
||||||
originUid = uid
|
|
||||||
}
|
|
||||||
if (!originRegion) {
|
|
||||||
originRegion = region
|
|
||||||
}
|
|
||||||
if (!originTimeZone) {
|
|
||||||
originTimeZone = region_time_zone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const data = { result, typeMap, time: Date.now(), uid: originUid, lang, region: originRegion, region_time_zone: originTimeZone }
|
|
||||||
const localData = dataMap.get(originUid)
|
|
||||||
const mergedResult = mergeData(localData, data)
|
|
||||||
data.result = mergedResult
|
|
||||||
dataMap.set(originUid, data)
|
|
||||||
await changeCurrent(originUid)
|
|
||||||
await saveData(data, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
let proxyStarted = false
|
|
||||||
const fetchDataByProxy = async () => {
|
|
||||||
if (proxyStarted) return
|
|
||||||
proxyStarted = true
|
|
||||||
const url = await useProxy()
|
|
||||||
await fetchData(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle('FETCH_DATA', async (event, param) => {
|
|
||||||
try {
|
|
||||||
if (param === 'proxy') {
|
|
||||||
await fetchDataByProxy()
|
|
||||||
} else {
|
|
||||||
await fetchData(param)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
dataMap,
|
|
||||||
current: config.current
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
sendMsg(e, 'ERROR')
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('READ_DATA', async () => {
|
|
||||||
await readData()
|
|
||||||
return {
|
|
||||||
dataMap,
|
|
||||||
current: config.current
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('CHANGE_UID', (event, uid) => {
|
|
||||||
changeCurrent(uid)
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('GET_CONFIG', () => {
|
|
||||||
return config.value()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('LANG_MAP', () => {
|
|
||||||
return langMap
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('SAVE_CONFIG', (event, [key, value]) => {
|
|
||||||
config[key] = value
|
|
||||||
config.save()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('DISABLE_PROXY', async () => {
|
|
||||||
await disableProxy()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('I18N_DATA', () => {
|
|
||||||
return i18n.data
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle('OPEN_CACHE_FOLDER', () => {
|
|
||||||
if (cacheFolder) {
|
|
||||||
shell.openPath(cacheFolder)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
exports.getData = () => {
|
|
||||||
return {
|
|
||||||
dataMap,
|
|
||||||
current: config.current
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.getUrl = getUrl
|
|
||||||
exports.deleteData = deleteData
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
const raw = {
|
|
||||||
'zh-cn': require('../i18n/简体中文.json'),
|
|
||||||
'zh-tw': require('../i18n/繁體中文.json'),
|
|
||||||
'en-us': require('../i18n/English.json')
|
|
||||||
}
|
|
||||||
const config = require('./config')
|
|
||||||
const isPlainObject = require('lodash/isPlainObject')
|
|
||||||
|
|
||||||
const addProp = (obj, key) => {
|
|
||||||
if (isPlainObject(obj[key])) {
|
|
||||||
return obj[key]
|
|
||||||
} else if (typeof obj[key] === 'undefined') {
|
|
||||||
let temp = {}
|
|
||||||
obj[key] = temp
|
|
||||||
return temp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseData = (data) => {
|
|
||||||
const result = {}
|
|
||||||
for (let key in data) {
|
|
||||||
let temp = result
|
|
||||||
const arr = key.split('.')
|
|
||||||
arr.forEach((prop, index) => {
|
|
||||||
if (index === arr.length - 1) {
|
|
||||||
temp[prop] = data[key]
|
|
||||||
} else {
|
|
||||||
temp = addProp(temp, prop)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const assignData = (objA, objB) => {
|
|
||||||
const temp = { ...objA }
|
|
||||||
for (let key in objB) {
|
|
||||||
if (objB[key]) {
|
|
||||||
temp[key] = objB[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return temp
|
|
||||||
}
|
|
||||||
|
|
||||||
const i18nMap = new Map()
|
|
||||||
const prepareData = () => {
|
|
||||||
for (let key in raw) {
|
|
||||||
let temp = {}
|
|
||||||
if (key === 'zh-tw') {
|
|
||||||
temp = assignData(raw['zh-cn'], raw[key])
|
|
||||||
} else {
|
|
||||||
temp = assignData(raw['zh-cn'], assignData(raw['en-us'], raw[key]))
|
|
||||||
}
|
|
||||||
i18nMap.set(key, parseData(temp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareData()
|
|
||||||
|
|
||||||
const parseText = (text, data) => {
|
|
||||||
return text.replace(/(\${.+?})/g, function (...args) {
|
|
||||||
const key = args[0].slice(2, args[0].length - 1)
|
|
||||||
if (data[key]) return data[key]
|
|
||||||
return args[0]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainProps = [
|
|
||||||
'symbol', 'ui', 'log', 'excel',"srgf"
|
|
||||||
]
|
|
||||||
|
|
||||||
const i18n = new Proxy(raw, {
|
|
||||||
get (obj, prop) {
|
|
||||||
if (prop === 'data') {
|
|
||||||
return i18nMap.get(config.lang)
|
|
||||||
} else if (mainProps.includes(prop)) {
|
|
||||||
return i18nMap.get(config.lang)[prop]
|
|
||||||
} else if (prop === 'parse') {
|
|
||||||
return parseText
|
|
||||||
}
|
|
||||||
return obj[prop]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports = i18n
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
const { app, BrowserWindow, ipcMain } = require('electron')
|
|
||||||
const { initWindow } = require('./utils')
|
|
||||||
const { disableProxy, proxyStatus } = require('./module/system-proxy')
|
|
||||||
require('./getData')
|
|
||||||
require('./bridge')
|
|
||||||
require('./excel')
|
|
||||||
require('./SRGFJson')
|
|
||||||
const { getUpdateInfo } = require('./update/index')
|
|
||||||
|
|
||||||
const isDev = !app.isPackaged
|
|
||||||
let win = null
|
|
||||||
|
|
||||||
function createWindow() {
|
|
||||||
win = initWindow()
|
|
||||||
win.setMenuBarVisibility(false)
|
|
||||||
isDev ? win.loadURL(`http://localhost:${process.env.PORT}`) : win.loadFile('dist/electron/renderer/index.html')
|
|
||||||
if (isDev) {
|
|
||||||
win.webContents.openDevTools({ mode: 'undocked', activate: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFirstInstance = app.requestSingleInstanceLock()
|
|
||||||
|
|
||||||
if (!isFirstInstance) {
|
|
||||||
app.quit()
|
|
||||||
} else {
|
|
||||||
app.on('second-instance', () => {
|
|
||||||
if (win) {
|
|
||||||
if (win.isMinimized()) win.restore()
|
|
||||||
win.focus()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.whenReady().then(createWindow)
|
|
||||||
|
|
||||||
ipcMain.handle('RELAUNCH', async () => {
|
|
||||||
app.relaunch()
|
|
||||||
app.exit(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('will-quit', (e) => {
|
|
||||||
if (proxyStatus.started) {
|
|
||||||
disableProxy()
|
|
||||||
}
|
|
||||||
if (getUpdateInfo().status === 'moving') {
|
|
||||||
e.preventDefault()
|
|
||||||
setTimeout(() => {
|
|
||||||
app.quit()
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
app.on('quit', () => {
|
|
||||||
if (proxyStatus.started) {
|
|
||||||
disableProxy()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
// 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()
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
const Registry = require('winreg')
|
|
||||||
|
|
||||||
const proxyStatus = {
|
|
||||||
started: false
|
|
||||||
}
|
|
||||||
const setProxy = async (enable, proxyIp = '', ignoreIp = '') => {
|
|
||||||
const regKey = new Registry({
|
|
||||||
hive: Registry.HKCU,
|
|
||||||
key: '\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'
|
|
||||||
})
|
|
||||||
|
|
||||||
const regSet = function (key, type, value) {
|
|
||||||
return new Promise((rev, rej) => {
|
|
||||||
regKey.set(key, type, value, function (err) {
|
|
||||||
if (err) rej(err)
|
|
||||||
rev()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await regSet('ProxyEnable', Registry.REG_DWORD, enable)
|
|
||||||
await regSet('ProxyServer', Registry.REG_SZ, proxyIp)
|
|
||||||
await regSet('ProxyOverride', Registry.REG_SZ, ignoreIp)
|
|
||||||
}
|
|
||||||
|
|
||||||
const enableProxy = async (ip, port) => {
|
|
||||||
const proxyIp = `${ip}:${port}`
|
|
||||||
const ignoreIp = 'localhost;127.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*;<local>'
|
|
||||||
await setProxy('1', proxyIp, ignoreIp)
|
|
||||||
proxyStatus.started = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const disableProxy = async () => {
|
|
||||||
await setProxy('0')
|
|
||||||
proxyStatus.started = false
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
enableProxy, disableProxy, proxyStatus
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
const fs = require('fs-extra')
|
|
||||||
const path = require('path')
|
|
||||||
const fetch = require('electron-fetch').default
|
|
||||||
const { BrowserWindow, app } = require('electron')
|
|
||||||
const crypto = require('crypto')
|
|
||||||
const unhandled = require('electron-unhandled')
|
|
||||||
const windowStateKeeper = require('electron-window-state')
|
|
||||||
const debounce = require('lodash/debounce')
|
|
||||||
const { glob } = require('glob')
|
|
||||||
|
|
||||||
const isDev = !app.isPackaged
|
|
||||||
|
|
||||||
const userPath = app.getPath('userData')
|
|
||||||
const appRoot = isDev ? path.resolve(__dirname, '..', '..') : path.resolve(app.getAppPath(), '..', '..')
|
|
||||||
const userDataPath = path.resolve(appRoot, 'userData')
|
|
||||||
// const globalUserDataPath = path.resolve(userPath, 'userData')
|
|
||||||
|
|
||||||
let win = null
|
|
||||||
const initWindow = () => {
|
|
||||||
let mainWindowState = windowStateKeeper({
|
|
||||||
defaultWidth: 888,
|
|
||||||
defaultHeight: 550
|
|
||||||
})
|
|
||||||
win = new BrowserWindow({
|
|
||||||
x: mainWindowState.x,
|
|
||||||
y: mainWindowState.y,
|
|
||||||
width: mainWindowState.width,
|
|
||||||
height: mainWindowState.height,
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
webPreferences: {
|
|
||||||
contextIsolation:false,
|
|
||||||
nodeIntegration: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const saveState = debounce(mainWindowState.saveState, 500)
|
|
||||||
win.on('resize', () => saveState(win))
|
|
||||||
win.on('move', () => saveState(win))
|
|
||||||
return win
|
|
||||||
}
|
|
||||||
|
|
||||||
const getWin = () => win
|
|
||||||
|
|
||||||
const log = []
|
|
||||||
const sendMsg = (text, type = 'LOAD_DATA_STATUS') => {
|
|
||||||
if (win) {
|
|
||||||
win.webContents.send(type, text)
|
|
||||||
}
|
|
||||||
if (type !== 'LOAD_DATA_STATUS') {
|
|
||||||
log.push([Date.now(), type, text])
|
|
||||||
saveLog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveLog = () => {
|
|
||||||
const text = log.map(item => {
|
|
||||||
const time = new Date(item[0]).toLocaleString()
|
|
||||||
const type = item[1] === 'LOAD_DATA_STATUS' ? 'INFO' : item[1]
|
|
||||||
const text = item[2]
|
|
||||||
return `[${type}][${time}]${text}`
|
|
||||||
}).join('\r\n')
|
|
||||||
fs.outputFile(path.join(userDataPath, 'log.txt'), text)
|
|
||||||
}
|
|
||||||
|
|
||||||
const authkeyMask = (text = '') => {
|
|
||||||
return text.replace(/authkey=[^&]+&/g, 'authkey=***&')
|
|
||||||
}
|
|
||||||
|
|
||||||
unhandled({
|
|
||||||
showDialog: false,
|
|
||||||
logger: function (err) {
|
|
||||||
log.push([Date.now(), 'ERROR', authkeyMask(err.stack)])
|
|
||||||
saveLog()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const request = async (url) => {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
timeout: 15 * 1000
|
|
||||||
})
|
|
||||||
return await res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
const sleep = (sec = 1) => {
|
|
||||||
return new Promise(rev => {
|
|
||||||
setTimeout(rev, sec * 1000)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortData = (data) => {
|
|
||||||
return data.map(item => {
|
|
||||||
const [time, name, type, rank] = item
|
|
||||||
return {
|
|
||||||
time, name, type, rank,
|
|
||||||
timestamp: new Date(time)
|
|
||||||
}
|
|
||||||
}).sort((a, b) => a.timestamp - b.timestamp)
|
|
||||||
.map(item => {
|
|
||||||
const { time, name, type, rank } = item
|
|
||||||
return [time, name, type, rank]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const langMap = new Map([
|
|
||||||
['zh-cn', '简体中文'],
|
|
||||||
['zh-tw', '繁體中文'],
|
|
||||||
['en-us', 'English']
|
|
||||||
])
|
|
||||||
|
|
||||||
const localeMap = new Map([
|
|
||||||
['zh-cn', ['zh', 'zh-CN']],
|
|
||||||
['zh-tw', ['zh-TW']],
|
|
||||||
['en-us', ['en-AU', 'en-CA', 'en-GB', 'en-NZ', 'en-US', 'en-ZA', 'en']]
|
|
||||||
])
|
|
||||||
|
|
||||||
const detectLocale = (value) => {
|
|
||||||
const locale = value || app.getLocale()
|
|
||||||
let result = 'zh-cn'
|
|
||||||
for (let [key, list] of localeMap) {
|
|
||||||
if (locale === key || list.includes(locale)) {
|
|
||||||
result = key
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveJSON = async (name, data) => {
|
|
||||||
try {
|
|
||||||
await fs.outputJSON(path.join(userDataPath, name), data)
|
|
||||||
} catch (e) {
|
|
||||||
sendMsg(e, 'ERROR')
|
|
||||||
await sleep(3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const readJSON = async (dataPath, name) => {
|
|
||||||
let data = null
|
|
||||||
try {
|
|
||||||
data = await fs.readJSON(path.join(dataPath, name))
|
|
||||||
} catch (e) {}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
const hash = (data, type = 'sha256') => {
|
|
||||||
const hmac = crypto.createHmac(type, 'nap')
|
|
||||||
hmac.update(data)
|
|
||||||
return hmac.digest('hex')
|
|
||||||
}
|
|
||||||
|
|
||||||
const scryptKey = crypto.scryptSync(userPath, 'nap', 24)
|
|
||||||
const cipherAes = (data) => {
|
|
||||||
const algorithm = 'aes-192-cbc'
|
|
||||||
const iv = Buffer.alloc(16, 0)
|
|
||||||
const cipher = crypto.createCipheriv(algorithm, scryptKey, iv)
|
|
||||||
let encrypted = cipher.update(data, 'utf8', 'hex')
|
|
||||||
encrypted += cipher.final('hex')
|
|
||||||
return encrypted
|
|
||||||
}
|
|
||||||
|
|
||||||
const decipherAes = (encrypted) => {
|
|
||||||
const algorithm = 'aes-192-cbc'
|
|
||||||
const iv = Buffer.alloc(16, 0)
|
|
||||||
const decipher = crypto.createDecipheriv(algorithm, scryptKey, iv)
|
|
||||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8')
|
|
||||||
decrypted += decipher.final('utf8')
|
|
||||||
return decrypted
|
|
||||||
}
|
|
||||||
|
|
||||||
const interfaces = require('os').networkInterfaces()
|
|
||||||
const localIp = () => {
|
|
||||||
for (var devName in interfaces) {
|
|
||||||
var iface = interfaces[devName]
|
|
||||||
|
|
||||||
for (var i = 0; i < iface.length; i++) {
|
|
||||||
var alias = iface[i]
|
|
||||||
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal)
|
|
||||||
return alias.address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '127.0.0.1'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCacheText(gamePath) {
|
|
||||||
const results = await glob(path.join(gamePath, '/webCaches{/,/*/}Cache/Cache_Data/data_2'), {
|
|
||||||
stat: true,
|
|
||||||
withFileTypes: true,
|
|
||||||
nodir: true,
|
|
||||||
windowsPathsNoEscape: true
|
|
||||||
})
|
|
||||||
const timeSortedFiles = results
|
|
||||||
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
||||||
.map(path => path.fullpath())
|
|
||||||
const cacheText = await fs.readFile(path.join(timeSortedFiles[0]), 'utf8')
|
|
||||||
|
|
||||||
return [cacheText, timeSortedFiles[0]]
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
sleep, request, hash, cipherAes, decipherAes, saveLog, getCacheText,
|
|
||||||
sendMsg, readJSON, saveJSON, initWindow, getWin, localIp, userPath, detectLocale, langMap,
|
|
||||||
appRoot, userDataPath
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
const mergeList = (a, b) => {
|
|
||||||
if (!a || !a.length) return b || []
|
|
||||||
if (!b || !b.length) return a
|
|
||||||
const list = [...b, ...a]
|
|
||||||
const result = []
|
|
||||||
const idSet = new Set()
|
|
||||||
list.forEach(item => {
|
|
||||||
if (!idSet.has(item.id)) {
|
|
||||||
result.push(item)
|
|
||||||
}
|
|
||||||
idSet.add(item.id)
|
|
||||||
})
|
|
||||||
return result.sort((m, n) => {
|
|
||||||
const num = BigInt(m.id) - BigInt(n.id)
|
|
||||||
if (num > 0) {
|
|
||||||
return 1
|
|
||||||
} else if (num < 0) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergeData = (local, origin) => {
|
|
||||||
if (local && local.result) {
|
|
||||||
const localResult = local.result
|
|
||||||
const localUid = local.uid
|
|
||||||
const originUid = origin.uid
|
|
||||||
if (localUid !== originUid) return origin.result
|
|
||||||
const originResult = new Map()
|
|
||||||
for (let [key, value] of origin.result) {
|
|
||||||
const newVal = mergeList(localResult.get(key), value)
|
|
||||||
originResult.set(key, newVal)
|
|
||||||
}
|
|
||||||
return originResult
|
|
||||||
}
|
|
||||||
return origin.result
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { mergeData, mergeList }
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
const { mergeList } = require('./mergeData')
|
|
||||||
|
|
||||||
test('mergeList successed', () => {
|
|
||||||
const listA = [{
|
|
||||||
"id": "1682521800010412850",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1682521800010412950",
|
|
||||||
}]
|
|
||||||
|
|
||||||
const listB = [{
|
|
||||||
"id": "1682521800010412900",
|
|
||||||
}]
|
|
||||||
|
|
||||||
expect(mergeList(listA, listB)).toEqual([
|
|
||||||
{
|
|
||||||
"id": "1682521800010412850",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1682521800010412900",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1682521800010412950",
|
|
||||||
}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('mergeList with repeated data successed', () => {
|
|
||||||
const listA = [{
|
|
||||||
"id": "1682521800010412850",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1682521800010412950",
|
|
||||||
}]
|
|
||||||
|
|
||||||
const listB = [{
|
|
||||||
"id": "1682521800010412950",
|
|
||||||
}]
|
|
||||||
|
|
||||||
expect(mergeList(listA, listB)).toEqual([
|
|
||||||
{
|
|
||||||
"id": "1682521800010412850",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "1682521800010412950",
|
|
||||||
}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('mergeList empty successed', () => {
|
|
||||||
const listA = []
|
|
||||||
const listB = [{
|
|
||||||
"id": "1682521800010412900",
|
|
||||||
}]
|
|
||||||
expect(mergeList(listA, listB)).toEqual([
|
|
||||||
{
|
|
||||||
"id": "1682521800010412900",
|
|
||||||
}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('mergeList empty 2 successed', () => {
|
|
||||||
const listA = [{
|
|
||||||
"id": "1682521800010412900",
|
|
||||||
}]
|
|
||||||
const listB = []
|
|
||||||
expect(mergeList(listA, listB)).toEqual([
|
|
||||||
{
|
|
||||||
"id": "1682521800010412900",
|
|
||||||
}
|
|
||||||
])
|
|
||||||
})
|
|
||||||
@@ -1,341 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="ui" class="relative">
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div class="space-x-3">
|
|
||||||
<el-button type="primary" :icon="state.status === 'init' ? 'milk-tea': 'refresh-right'" class="focus:outline-none" :disabled="!allowClick()" plain @click="fetchData()" :loading="state.status === 'loading'">{{state.status === 'init' ? ui.button.load: ui.button.update}}</el-button>
|
|
||||||
<el-dropdown :disabled="!gachaData" @command="exportCommand">
|
|
||||||
<el-button :disabled="!gachaData" icon="folder-opened" class="focus:outline-none" type="success" plain>
|
|
||||||
{{ui.button.files}}
|
|
||||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item command="excel">{{ui.button.excel}}</el-dropdown-item>
|
|
||||||
<el-dropdown-item command="srgf-json">{{ui.button.srgf}}</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
<el-tooltip v-if="detail && state.status !== 'loading'" :content="ui.hint.newAccount" placement="bottom">
|
|
||||||
<el-button @click="newUser()" plain icon="plus" class="focus:outline-none"></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip v-if="state.status === 'updated'" :content="ui.hint.relaunchHint" placement="bottom">
|
|
||||||
<el-button @click="relaunch()" type="success" icon="refresh" class="focus:outline-none" style="margin-left: 48px">{{ui.button.directUpdate}}</el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<el-select v-if="state.status !== 'loading' && dataMap && (dataMap.size > 1 || (dataMap.size === 1 && state.current === 0))" class="w-44" @change="changeCurrent" v-model="uidSelectText">
|
|
||||||
<el-option
|
|
||||||
v-for="item of dataMap"
|
|
||||||
:key="item[0]"
|
|
||||||
:label="maskUid(item[0])"
|
|
||||||
:value="item[0]">
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
|
||||||
<el-dropdown @command="optionCommand">
|
|
||||||
<el-button @click="showSetting(true)" class="focus:outline-none" plain type="info" icon="more" >{{ui.button.option}}</el-button>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item command="setting" icon="setting">{{ui.button.setting}}</el-dropdown-item>
|
|
||||||
<el-dropdown-item :disabled="!allowClick() || state.status === 'loading'" command="url" icon="link">{{ui.button.url}}</el-dropdown-item>
|
|
||||||
<el-dropdown-item command="copyUrl" icon="DocumentCopy">{{ui.button.copyUrl}}</el-dropdown-item>
|
|
||||||
<el-dropdown-item :disabled="!allowClick() || state.status === 'loading'" command="proxy" icon="position">{{ui.button.startProxy}}</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-400 my-2 text-xs">{{hint}}<el-button @click="(state.showCacheCleanDlg=true)" v-if="state.authkeyTimeout" style="margin-left: 8px;" size="small" plain round>{{ui.button.solution}}</el-button></p>
|
|
||||||
<div v-if="detail" class="gap-4 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 2xl:grid-cols-4">
|
|
||||||
<div class="mb-4" v-for="(item, i) of detail" :key="i">
|
|
||||||
<div :class="{hidden: state.config.hideNovice && item[0] === '2'}">
|
|
||||||
<p class="text-center text-gray-600 my-2">{{typeMap.get(item[0])}}</p>
|
|
||||||
<pie-chart :data="item" :i18n="state.i18n" :typeMap="typeMap"></pie-chart>
|
|
||||||
<gacha-detail :i18n="state.i18n" :data="item" :typeMap="typeMap"></gacha-detail>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Setting v-show="state.showSetting" :i18n="state.i18n" :gacha-data-info="dataInfo" @refreshData="readData()" @changeLang="getI18nData()" @close="showSetting(false)"></Setting>
|
|
||||||
|
|
||||||
<el-dialog :title="ui.urlDialog.title" v-model="state.showUrlDlg" width="90%" class="max-w-md">
|
|
||||||
<p class="mb-4 text-gray-500">{{ui.urlDialog.hint}}</p>
|
|
||||||
<el-input type="textarea" :autosize="{minRows: 4, maxRows: 6}" :placeholder="ui.urlDialog.placeholder" v-model="state.urlInput" spellcheck="false"></el-input>
|
|
||||||
<template #footer>
|
|
||||||
<span class="dialog-footer">
|
|
||||||
<el-button @click="state.showUrlDlg = false" class="focus:outline-none">{{ui.common.cancel}}</el-button>
|
|
||||||
<el-button type="primary" @click="state.showUrlDlg = false, fetchData(state.urlInput)" class="focus:outline-none">{{ui.common.ok}}</el-button>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog :title="ui.button.solution" v-model="state.showCacheCleanDlg" width="90%" class="max-w-md cache-clean-dialog">
|
|
||||||
<el-button plain icon="folder" type="success" @click="openCacheFolder">{{ui.button.cacheFolder}}</el-button>
|
|
||||||
<p class="my-2 flex flex-col text-teal-800 text-[13px]">
|
|
||||||
<span class="my-1" v-for="txt of cacheCleanTextList">{{ txt }}</span>
|
|
||||||
</p>
|
|
||||||
<p class="my-2 text-gray-500 text-xs">{{ui.extra.findCacheFolder}}</p>
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer text-center">
|
|
||||||
<el-button type="primary" @click="state.showCacheCleanDlg = false" class="focus:outline-none">{{ui.common.ok}}</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const { ipcRenderer } = require('electron')
|
|
||||||
import { reactive, computed, watch, onMounted } from 'vue'
|
|
||||||
import PieChart from './components/PieChart.vue'
|
|
||||||
import GachaDetail from './components/GachaDetail.vue'
|
|
||||||
import Setting from './components/Setting.vue'
|
|
||||||
import gachaDetail from './gachaDetail'
|
|
||||||
import { version } from '../../package.json'
|
|
||||||
import gachaType from '../gachaType.json'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
status: 'init',
|
|
||||||
log: '',
|
|
||||||
data: null,
|
|
||||||
dataMap: new Map(),
|
|
||||||
current: 0,
|
|
||||||
showSetting: false,
|
|
||||||
i18n: null,
|
|
||||||
showUrlDlg: false,
|
|
||||||
showCacheCleanDlg: false,
|
|
||||||
urlInput: '',
|
|
||||||
authkeyTimeout: false,
|
|
||||||
config: {}
|
|
||||||
})
|
|
||||||
|
|
||||||
const dataMap = computed(() => {
|
|
||||||
const result = new Map()
|
|
||||||
for (let [uid, data] of state.dataMap) {
|
|
||||||
if (!data.deleted) {
|
|
||||||
result.set(uid, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
const dataInfo = computed(() => {
|
|
||||||
const result = []
|
|
||||||
for (let [uid, data] of state.dataMap) {
|
|
||||||
result.push({
|
|
||||||
uid, time: data.time, deleted: data.deleted
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
const ui = computed(() => {
|
|
||||||
if (state.i18n) {
|
|
||||||
return state.i18n.ui
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const cacheCleanTextList = computed(() => {
|
|
||||||
if (ui.value) {
|
|
||||||
return ui.value.extra?.cacheClean?.split('\n')
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
const gachaData = computed(() => {
|
|
||||||
return state.dataMap.get(state.current)
|
|
||||||
})
|
|
||||||
|
|
||||||
const uidSelectText = computed(() => {
|
|
||||||
if (state.current === 0) {
|
|
||||||
return state.i18n.ui.select.newAccount
|
|
||||||
} else {
|
|
||||||
return state.current
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const allowClick = () => {
|
|
||||||
const data = state.dataMap.get(state.current)
|
|
||||||
if (!data) return true
|
|
||||||
if (Date.now() - data.time < 1000 * 10) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const hint = computed(() => {
|
|
||||||
const data = state.dataMap.get(state.current)
|
|
||||||
if (!state.i18n) {
|
|
||||||
return 'Loading...'
|
|
||||||
}
|
|
||||||
const { hint } = state.i18n.ui
|
|
||||||
const { colon } = state.i18n.symbol
|
|
||||||
if (state.status === 'init') {
|
|
||||||
return hint.init
|
|
||||||
} else if (state.status === 'loaded') {
|
|
||||||
return `${hint.lastUpdate}${colon}${new Date(data.time).toLocaleString()}`
|
|
||||||
} else if (state.status === 'loading') {
|
|
||||||
return state.log || 'Loading...'
|
|
||||||
} else if (state.status === 'updated') {
|
|
||||||
return state.log
|
|
||||||
} else if (state.status === 'failed') {
|
|
||||||
return state.log + ` - ${hint.failed}`
|
|
||||||
}
|
|
||||||
return ' '
|
|
||||||
})
|
|
||||||
|
|
||||||
const detail = computed(() => {
|
|
||||||
const data = dataMap.value.get(state.current)
|
|
||||||
if (data) {
|
|
||||||
return gachaDetail(data.result)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const typeMap = computed(() => {
|
|
||||||
const gachaTypeMap = new Map(gachaType)
|
|
||||||
const type = gachaTypeMap.get(state.config.lang)
|
|
||||||
const result = new Map()
|
|
||||||
if (type) {
|
|
||||||
for (let { key, name } of type) {
|
|
||||||
result.set(key, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
const fetchData = async (url) => {
|
|
||||||
state.log = ''
|
|
||||||
state.status = 'loading'
|
|
||||||
const data = await ipcRenderer.invoke('FETCH_DATA', url)
|
|
||||||
if (data) {
|
|
||||||
state.dataMap = data.dataMap
|
|
||||||
state.current = data.current
|
|
||||||
state.status = 'loaded'
|
|
||||||
} else {
|
|
||||||
state.status = 'failed'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const readData = async () => {
|
|
||||||
const data = await ipcRenderer.invoke('READ_DATA')
|
|
||||||
if (data) {
|
|
||||||
state.dataMap = data.dataMap
|
|
||||||
state.current = data.current
|
|
||||||
if (data.dataMap.get(data.current)) {
|
|
||||||
state.status = 'loaded'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getI18nData = async () => {
|
|
||||||
const data = await ipcRenderer.invoke('I18N_DATA')
|
|
||||||
if (data) {
|
|
||||||
state.i18n = data
|
|
||||||
setTitle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveExcel = async () => {
|
|
||||||
await ipcRenderer.invoke('SAVE_EXCEL')
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportSRGFJSON = () => {
|
|
||||||
ipcRenderer.invoke('EXPORT_SRGF_JSON')
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportCommand = (type) => {
|
|
||||||
if (type === 'excel') {
|
|
||||||
saveExcel()
|
|
||||||
} else if (type === 'srgf-json') {
|
|
||||||
exportSRGFJSON()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const openCacheFolder = async () => {
|
|
||||||
await ipcRenderer.invoke('OPEN_CACHE_FOLDER')
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeCurrent = async (uid) => {
|
|
||||||
if (uid === 0) {
|
|
||||||
state.status = 'init'
|
|
||||||
} else {
|
|
||||||
state.status = 'loaded'
|
|
||||||
}
|
|
||||||
state.current = uid
|
|
||||||
await ipcRenderer.invoke('CHANGE_UID', uid)
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = async () => {
|
|
||||||
await changeCurrent(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const relaunch = async () => {
|
|
||||||
await ipcRenderer.invoke('RELAUNCH')
|
|
||||||
}
|
|
||||||
|
|
||||||
const maskUid = (uid) => {
|
|
||||||
return `${uid}`.replace(/(.{3})(.+)(.{3})$/, '$1***$3')
|
|
||||||
}
|
|
||||||
|
|
||||||
const showSetting = (show) => {
|
|
||||||
if (show) {
|
|
||||||
state.showSetting = true
|
|
||||||
} else {
|
|
||||||
state.showSetting = false
|
|
||||||
updateConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const optionCommand = (type) => {
|
|
||||||
if (type === 'setting') {
|
|
||||||
showSetting(true)
|
|
||||||
} else if (type === 'url') {
|
|
||||||
state.urlInput = ''
|
|
||||||
state.showUrlDlg = true
|
|
||||||
} else if (type === 'proxy') {
|
|
||||||
fetchData('proxy')
|
|
||||||
} else if (type === 'copyUrl') {
|
|
||||||
copyUrl()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setTitle = () => {
|
|
||||||
document.title = `${state.i18n.ui.win.title} - v${version}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateConfig = async () => {
|
|
||||||
state.config = await ipcRenderer.invoke('GET_CONFIG')
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyUrl = async () => {
|
|
||||||
const successed = await ipcRenderer.invoke('COPY_URL')
|
|
||||||
if (successed) {
|
|
||||||
ElMessage.success(ui.value.extra.urlCopied)
|
|
||||||
} else {
|
|
||||||
ElMessage.error(state.i18n.log.url.notFound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await readData()
|
|
||||||
await getI18nData()
|
|
||||||
|
|
||||||
ipcRenderer.on('LOAD_DATA_STATUS', (event, message) => {
|
|
||||||
state.log = message
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcRenderer.on('ERROR', (event, err) => {
|
|
||||||
console.error(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcRenderer.on('UPDATE_HINT', (event, message) => {
|
|
||||||
state.log = message
|
|
||||||
state.status = 'updated'
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcRenderer.on('AUTHKEY_TIMEOUT', (event, message) => {
|
|
||||||
state.authkeyTimeout = message
|
|
||||||
})
|
|
||||||
|
|
||||||
await updateConfig()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<template>
|
|
||||||
<p class="text-gray-500 text-xs mb-2 text-center whitespace-nowrap">
|
|
||||||
<span class="mx-2" :title="new Date(detail.date[0]).toLocaleString()">{{new Date(detail.date[0]).toLocaleDateString()}}</span>
|
|
||||||
-
|
|
||||||
<span class="mx-2" :title="new Date(detail.date[1]).toLocaleString()">{{new Date(detail.date[1]).toLocaleDateString()}}</span>
|
|
||||||
</p>
|
|
||||||
<p class="text-gray-600 text-xs mb-1">
|
|
||||||
<span class="mr-1">{{text.total}}
|
|
||||||
<span class="text-blue-600">{{detail.total}}</span> {{text.times}}
|
|
||||||
</span>
|
|
||||||
<span v-if="type !== '100'">{{text.sum}}<span class="mx-1 text-green-600">{{detail.countMio}}</span>{{text.no4star}}</span>
|
|
||||||
</p>
|
|
||||||
<p class="text-gray-600 text-xs mb-1">
|
|
||||||
<span :title="`${text.character}${colon}${detail.count4c}\n${text.weapon}${colon}${detail.count4w}\n${text.bang}${colon}${detail.count4b}`" class="mr-3 whitespace-pre cursor-help text-yellow-500">
|
|
||||||
<span class="min-w-10 inline-block">{{text.star4}}{{colon}}{{detail.count4}}</span>
|
|
||||||
[{{percent(detail.count4, detail.total)}}]
|
|
||||||
</span>
|
|
||||||
<br><span :title="`${text.character}${colon}${detail.count3c}\n${text.weapon}${colon}${detail.count3w}\n${text.bang}${colon}${detail.count3b}`" class="mr-3 whitespace-pre cursor-help text-purple-600">
|
|
||||||
<span class="min-w-10 inline-block">{{text.star3}}{{colon}}{{detail.count3}}</span>
|
|
||||||
[{{percent(detail.count3, detail.total)}}]
|
|
||||||
</span>
|
|
||||||
<br><span class="text-blue-500 whitespace-pre">
|
|
||||||
<span class="min-w-10 inline-block">{{text.star2}}{{colon}}{{detail.count2}}</span>
|
|
||||||
[{{percent(detail.count2, detail.total)}}]
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="text-gray-600 text-xs mb-1" v-if="detail.ssrPos.length">
|
|
||||||
{{text.history}}{{colon}}
|
|
||||||
<span :title="`${item[2]}${item[3] === '400' ? '\n' + props.i18n.excel.wish2 : ''}`" :class="{wish2: item[3] === '400'}" class="cursor-help mr-1" :style="`color:${colorList[index]}`"
|
|
||||||
v-for="(item, index) of detail.ssrPos" :key="item"
|
|
||||||
>
|
|
||||||
{{item[0]}}[{{item[1]}}]
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p v-if="detail.ssrPos.length" class="text-gray-600 text-xs">{{text.average}}{{colon}}<span class="text-green-600">{{avg5(detail.ssrPos)}}</span></p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
data: Object,
|
|
||||||
typeMap: Map,
|
|
||||||
i18n: Object
|
|
||||||
})
|
|
||||||
|
|
||||||
const type = computed(() => props.data[0])
|
|
||||||
const detail = computed(() => props.data[1])
|
|
||||||
const text = computed(() => props.i18n.ui.data)
|
|
||||||
const colon = computed(() => props.i18n.symbol.colon)
|
|
||||||
|
|
||||||
const avg5 = (list) => {
|
|
||||||
let n = 0
|
|
||||||
list.forEach(item => {
|
|
||||||
n += item[1]
|
|
||||||
})
|
|
||||||
return parseInt((n / list.length) * 100) / 100
|
|
||||||
}
|
|
||||||
|
|
||||||
const percent = (num, total) => {
|
|
||||||
return `${Math.round(num / total * 10000) / 100}%`
|
|
||||||
}
|
|
||||||
|
|
||||||
const colors = [
|
|
||||||
'#5470c6', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#2ab7ca',
|
|
||||||
'#005b96', '#ff8b94', '#72a007','#b60d1b', '#16570d'
|
|
||||||
]
|
|
||||||
|
|
||||||
const colorList = computed(() => {
|
|
||||||
let colorsTemp = [...colors]
|
|
||||||
const result = []
|
|
||||||
const map = new Map()
|
|
||||||
props.data[1].ssrPos.forEach(item => {
|
|
||||||
if (map.has(item[0])) {
|
|
||||||
return result.push(map.get(item[0]))
|
|
||||||
}
|
|
||||||
const num = Math.abs(hashCode(`${Math.floor(Date.now() / (1000 * 60 * 10))}-${item[0]}`))
|
|
||||||
if (!colorsTemp.length) colorsTemp = [...colors]
|
|
||||||
const color = colorsTemp.splice(num % colorsTemp.length, 1)[0]
|
|
||||||
map.set(item[0], color)
|
|
||||||
result.push(color)
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
function hashCode(str) {
|
|
||||||
return Array.from(str)
|
|
||||||
.reduce((s, c) => Math.imul(31, s) + c.charCodeAt(0) | 0, 0)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="chart mb-2 relative h-48 lg:h-56 xl:h-64 2xl:h-72">
|
|
||||||
<div ref="chart" class="absolute inset-0"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { reactive, computed, ref, onMounted, onUpdated } from "vue";
|
|
||||||
import { use, init } from "echarts/core";
|
|
||||||
import {
|
|
||||||
TitleComponent,
|
|
||||||
TooltipComponent,
|
|
||||||
LegendComponent,
|
|
||||||
} from "echarts/components";
|
|
||||||
import { PieChart } from "echarts/charts";
|
|
||||||
import { CanvasRenderer } from "echarts/renderers";
|
|
||||||
import throttle from "lodash-es/throttle";
|
|
||||||
|
|
||||||
use([
|
|
||||||
TitleComponent,
|
|
||||||
TooltipComponent,
|
|
||||||
LegendComponent,
|
|
||||||
PieChart,
|
|
||||||
CanvasRenderer,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
data: Object,
|
|
||||||
typeMap: Map,
|
|
||||||
i18n: Object,
|
|
||||||
});
|
|
||||||
|
|
||||||
const chart = ref(null);
|
|
||||||
|
|
||||||
const colors = ["#eeaa66", "#fac858", "#ee6666", "#5470c6", "#ba66ee", "#91cc75", "#73c0de"];
|
|
||||||
|
|
||||||
const parseData = (detail, type) => {
|
|
||||||
const text = props.i18n.ui.data;
|
|
||||||
const keys = [
|
|
||||||
[text.bang4, "count4b"],
|
|
||||||
[text.chara4, "count4c"],
|
|
||||||
[text.weapon4, "count4w"],
|
|
||||||
[text.chara3, "count3c"],
|
|
||||||
[text.bang3, "count3b"],
|
|
||||||
[text.weapon3, "count3w"],
|
|
||||||
[text.weapon2, "count2w"]
|
|
||||||
];
|
|
||||||
const result = [];
|
|
||||||
const color = [];
|
|
||||||
const selected = {
|
|
||||||
[text.weapon2]: false,
|
|
||||||
};
|
|
||||||
keys.forEach((key, index) => {
|
|
||||||
if (!detail[key[1]]) return;
|
|
||||||
result.push({
|
|
||||||
value: detail[key[1]],
|
|
||||||
name: key[0],
|
|
||||||
});
|
|
||||||
color.push(colors[index]);
|
|
||||||
});
|
|
||||||
if (
|
|
||||||
type === "100" ||
|
|
||||||
result.findIndex((item) => item.name.includes("S")) === -1
|
|
||||||
) {
|
|
||||||
selected[text.weapon2] = true;
|
|
||||||
}
|
|
||||||
return [result, color, selected];
|
|
||||||
};
|
|
||||||
|
|
||||||
let pieChart = null;
|
|
||||||
const updateChart = throttle(() => {
|
|
||||||
if (!pieChart) {
|
|
||||||
pieChart = init(chart.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const colon = props.i18n.symbol.colon;
|
|
||||||
const result = parseData(props.data[1], props.data[0]);
|
|
||||||
|
|
||||||
const option = {
|
|
||||||
tooltip: {
|
|
||||||
trigger: "item",
|
|
||||||
formatter: `{b0}${colon}{c0}`,
|
|
||||||
padding: 4,
|
|
||||||
textStyle: {
|
|
||||||
fontSize: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
top: "2%",
|
|
||||||
left: "center",
|
|
||||||
selected: result[2],
|
|
||||||
},
|
|
||||||
selectedMode: "single",
|
|
||||||
color: result[1],
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: props.typeMap.get(props.data[0]),
|
|
||||||
type: "pie",
|
|
||||||
top: 50,
|
|
||||||
startAngle: 70,
|
|
||||||
radius: ["0%", "90%"],
|
|
||||||
// avoidLabelOverlap: false,
|
|
||||||
labelLine: {
|
|
||||||
length: 0,
|
|
||||||
length2: 10,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
overflow: "break",
|
|
||||||
},
|
|
||||||
data: result[0],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
pieChart.setOption(option);
|
|
||||||
pieChart.resize();
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
onUpdated(() => {
|
|
||||||
updateChart();
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
updateChart();
|
|
||||||
window.addEventListener("resize", updateChart);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="bg-white py-4 px-6 w-screen h-screen fixed inset-0 overflow-y-auto">
|
|
||||||
<div class="flex content-center items-center mb-4 justify-between">
|
|
||||||
<h3 class="text-lg">{{text.title}}</h3>
|
|
||||||
<el-button icon="close" @click="closeSetting" plain circle type="default" class="w-8 h-8 shadow-md focus:shadow-none focus:outline-none fixed top-4 right-6"></el-button>
|
|
||||||
</div>
|
|
||||||
<el-form :model="settingForm" label-width="120px">
|
|
||||||
<el-form-item :label="text.language">
|
|
||||||
<el-select @change="saveLang" v-model="settingForm.lang">
|
|
||||||
<el-option v-for="item of data.langMap" :key="item[0]" :label="item[1]" :value="item[0]"></el-option>
|
|
||||||
</el-select>
|
|
||||||
<p class="text-gray-400 text-xs m-1.5">{{text.languageHint}}</p>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="text.logType">
|
|
||||||
<el-radio-group @change="saveSetting" v-model.number="settingForm.logType">
|
|
||||||
<el-radio-button :label="0">{{text.auto}}</el-radio-button>
|
|
||||||
<el-radio-button :label="1">{{text.cnServer}}</el-radio-button>
|
|
||||||
<el-radio-button :label="2">{{text.seaServer}}</el-radio-button>
|
|
||||||
</el-radio-group>
|
|
||||||
<p class="text-gray-400 text-xs m-1.5">{{text.logTypeHint}}</p>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="common.data">
|
|
||||||
<el-button type="primary" plain @click="state.showDataDialog = true">{{common.dataManage}}</el-button>
|
|
||||||
<p class="text-gray-400 text-xs m-1.5">{{text.dataManagerHint}}</p>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="text.autoUpdate">
|
|
||||||
<el-switch
|
|
||||||
@change="saveSetting"
|
|
||||||
v-model="settingForm.autoUpdate">
|
|
||||||
</el-switch>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="text.fetchFullHistory">
|
|
||||||
<el-switch
|
|
||||||
@change="saveSetting"
|
|
||||||
v-model="settingForm.fetchFullHistory">
|
|
||||||
</el-switch>
|
|
||||||
<p class="text-gray-400 text-xs m-1.5">{{text.fetchFullHistoryHint}}</p>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="text.proxyMode">
|
|
||||||
<el-switch
|
|
||||||
@change="saveSetting"
|
|
||||||
v-model="settingForm.proxyMode">
|
|
||||||
</el-switch>
|
|
||||||
<p class="text-gray-400 text-xs m-1.5">{{text.proxyModeHint}}</p>
|
|
||||||
<el-button class="focus:outline-none" @click="disableProxy">{{text.closeProxy}}</el-button>
|
|
||||||
<p class="text-gray-400 text-xs m-1.5">{{text.closeProxyHint}}</p>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<h3 class="text-lg my-4">{{about.title}}</h3>
|
|
||||||
<p class="text-gray-600 text-xs mt-1">{{about.license}}</p>
|
|
||||||
<p class="text-gray-600 text-xs mt-1 pb-6">Github: <a @click="openGithub" class="cursor-pointer text-blue-400">https://github.com/earthjasonlin/zzz-signal-search-export</a></p>
|
|
||||||
<el-dialog v-model="state.showDataDialog" :title="common.dataManage" width="90%">
|
|
||||||
<div class="">
|
|
||||||
<el-table :data="gachaDataInfo" border stripe>
|
|
||||||
<el-table-column property="uid" label="UID" width="128" />
|
|
||||||
<el-table-column property="time" :label="common.updateTime">
|
|
||||||
<template #default="scope">
|
|
||||||
{{ new Date(scope.row.time).toLocaleString() }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column property="deleted" :label="common.status" width="128">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-tag type="info" size="small" v-if="scope.row.deleted">{{common.deleted}}</el-tag>
|
|
||||||
<el-tag type="success" size="small" v-else>{{common.normal}}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column property="deleted" :label="common.action" width="128">
|
|
||||||
<template #default="scope">
|
|
||||||
<el-tooltip :content="scope.row.deleted ? common.restore : common.delete" placement="top">
|
|
||||||
<el-button :loading="state.dataActionLoading" size="small" icon="refresh" plain type="success" @click="deleteData(scope.row.uid, false)" v-if="scope.row.deleted"></el-button>
|
|
||||||
<el-button :loading="state.dataActionLoading" size="small" icon="delete" plain type="danger" @click="deleteData(scope.row.uid, true)" v-else></el-button>
|
|
||||||
</el-tooltip>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
const { ipcRenderer, shell } = require('electron')
|
|
||||||
import { reactive, onMounted, computed } from 'vue'
|
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'changeLang', 'refreshData'])
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
i18n: Object,
|
|
||||||
gachaDataInfo: Array
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = reactive({
|
|
||||||
langMap: new Map(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const settingForm = reactive({
|
|
||||||
lang: 'zh-cn',
|
|
||||||
logType: 1,
|
|
||||||
proxyMode: true,
|
|
||||||
autoUpdate: true,
|
|
||||||
fetchFullHistory: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
showDataDialog: false,
|
|
||||||
dataActionLoading: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const common = computed(() => props.i18n.ui.common)
|
|
||||||
const text = computed(() => props.i18n.ui.setting)
|
|
||||||
const about = computed(() => props.i18n.ui.about)
|
|
||||||
|
|
||||||
const saveSetting = async () => {
|
|
||||||
const keys = ['lang', 'logType', 'proxyMode', 'autoUpdate', 'fetchFullHistory']
|
|
||||||
for (let key of keys) {
|
|
||||||
await ipcRenderer.invoke('SAVE_CONFIG', [key, settingForm[key]])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const saveLang = async () => {
|
|
||||||
await saveSetting()
|
|
||||||
emit('changeLang')
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeSetting = () => emit('close')
|
|
||||||
|
|
||||||
const disableProxy = async () => {
|
|
||||||
await ipcRenderer.invoke('DISABLE_PROXY')
|
|
||||||
}
|
|
||||||
|
|
||||||
const openGithub = () => shell.openExternal('https://github.com/earthjasonlin/zzz-signal-search-export')
|
|
||||||
const openLink = (link) => shell.openExternal(link)
|
|
||||||
|
|
||||||
const deleteData = async (uid, action) => {
|
|
||||||
state.dataActionLoading = true
|
|
||||||
await ipcRenderer.invoke('DELETE_DATA', uid, action)
|
|
||||||
state.dataActionLoading = false
|
|
||||||
emit('refreshData')
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
data.langMap = await ipcRenderer.invoke('LANG_MAP')
|
|
||||||
const config = await ipcRenderer.invoke('GET_CONFIG')
|
|
||||||
Object.assign(settingForm, config)
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.el-form-item__label {
|
|
||||||
line-height: normal !important;
|
|
||||||
position: relative;
|
|
||||||
top: 6px;
|
|
||||||
}
|
|
||||||
.el-form-item__content {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: start !important;
|
|
||||||
}
|
|
||||||
.el-form-item--default {
|
|
||||||
margin-bottom: 14px !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { isWeapon, isCharacter, isBangboo } from './utils'
|
|
||||||
|
|
||||||
const itemCount = (map, name) => {
|
|
||||||
if (!map.has(name)) {
|
|
||||||
map.set(name, 1)
|
|
||||||
} else {
|
|
||||||
map.set(name, map.get(name) + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const gachaDetail = (data) => {
|
|
||||||
const detailMap = new Map()
|
|
||||||
for (let [key, value] of data) {
|
|
||||||
let detail = {
|
|
||||||
count2: 0, count3: 0, count4: 0,
|
|
||||||
count2w: 0, count3w: 0, count4w: 0, count3c: 0, count4c: 0, count3b: 0, count4b: 0,
|
|
||||||
weapon2: new Map(), weapon3: new Map(), weapon4: new Map(),
|
|
||||||
char3: new Map(), char4: new Map(),
|
|
||||||
bang3: new Map(), bang4: new Map(),
|
|
||||||
date: [],
|
|
||||||
ssrPos: [], countMio: 0, total: value.length,
|
|
||||||
}
|
|
||||||
let lastSSR = 0
|
|
||||||
let dateMin = 0
|
|
||||||
let dateMax = 0
|
|
||||||
value.forEach((item, index) => {
|
|
||||||
const { time, name, item_type: type, rank_type: rank } = item
|
|
||||||
const timestamp = new Date(time).getTime()
|
|
||||||
if (!dateMin) dateMin = timestamp
|
|
||||||
if (!dateMax) dateMax = timestamp
|
|
||||||
if (dateMin > timestamp) dateMin = timestamp
|
|
||||||
if (dateMax < timestamp) dateMax = timestamp
|
|
||||||
if (rank === '2') {
|
|
||||||
detail.count2++
|
|
||||||
detail.countMio++
|
|
||||||
if (isWeapon(type)) {
|
|
||||||
detail.count2w++
|
|
||||||
itemCount(detail.weapon2, name)
|
|
||||||
}
|
|
||||||
} else if (rank === '3') {
|
|
||||||
detail.count3++
|
|
||||||
detail.countMio++
|
|
||||||
if (isWeapon(type)) {
|
|
||||||
detail.count3w++
|
|
||||||
itemCount(detail.weapon3, name)
|
|
||||||
} else if (isBangboo(type)) {
|
|
||||||
detail.count3b++
|
|
||||||
itemCount(detail.bang3, name)
|
|
||||||
} else if (isCharacter(type)) {
|
|
||||||
detail.count3c++
|
|
||||||
itemCount(detail.char3, name)
|
|
||||||
}
|
|
||||||
} else if (rank === '4') {
|
|
||||||
detail.ssrPos.push([name, index + 1 - lastSSR, time, key])
|
|
||||||
lastSSR = index + 1
|
|
||||||
detail.count4++
|
|
||||||
detail.countMio = 0
|
|
||||||
if (isWeapon(type)) {
|
|
||||||
detail.count4w++
|
|
||||||
itemCount(detail.weapon4, name)
|
|
||||||
} else if (isBangboo(type)) {
|
|
||||||
detail.count4b++
|
|
||||||
itemCount(detail.bang4, name)
|
|
||||||
} else if (isCharacter(type)) {
|
|
||||||
detail.count4c++
|
|
||||||
itemCount(detail.char4, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
detail.date = [dateMin, dateMax]
|
|
||||||
if (detail.total) {
|
|
||||||
detailMap.set(key, detail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return detailMap
|
|
||||||
}
|
|
||||||
|
|
||||||
export default gachaDetail
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
:root {
|
|
||||||
--el-font-size-base: 12px !important;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
@apply rounded-full bg-gray-300;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
.cache-clean-dialog .el-dialog__body {
|
|
||||||
padding: 0 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title></title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app" class="pt-4 px-6"></div>
|
|
||||||
<script type="module" src="/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { createApp } from 'vue'
|
|
||||||
import App from './App.vue'
|
|
||||||
import './index.css'
|
|
||||||
import ElementPlus from 'element-plus'
|
|
||||||
import 'element-plus/dist/index.css'
|
|
||||||
import { IconInstaller } from './utils'
|
|
||||||
|
|
||||||
const app = createApp(App)
|
|
||||||
app.use(ElementPlus)
|
|
||||||
IconInstaller(app)
|
|
||||||
app.mount('#app')
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import * as IconComponents from '@element-plus/icons-vue'
|
|
||||||
|
|
||||||
const weaponTypeNames = new Set([
|
|
||||||
'音擎', 'W-Engines', '音擎'
|
|
||||||
])
|
|
||||||
|
|
||||||
const bangbooTypeNames = new Set([
|
|
||||||
'邦布', 'Bangboo', '邦布'
|
|
||||||
])
|
|
||||||
|
|
||||||
const characterTypeNames = new Set([
|
|
||||||
'代理人', 'Agents', '代理人'
|
|
||||||
])
|
|
||||||
|
|
||||||
const isCharacter = (name) => characterTypeNames.has(name)
|
|
||||||
const isWeapon = (name) => weaponTypeNames.has(name)
|
|
||||||
const isBangboo = (name) => bangbooTypeNames.has(name)
|
|
||||||
|
|
||||||
const IconInstaller = (app) => {
|
|
||||||
Object.values(IconComponents).forEach(component => {
|
|
||||||
app.component(component.name, component)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
isWeapon,
|
|
||||||
isCharacter,
|
|
||||||
isBangboo,
|
|
||||||
IconInstaller,
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
content: ['./src/renderer/index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
minWidth: {
|
|
||||||
'10': '60px'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
extend: {
|
|
||||||
backgroundColor: ['active']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"active":true,"version":"1.1.21","from":"1.1.0","name":"50cd7.zip","hash":"2d619a250cd7a33d83b1601f9c592312b28084bbbf46526aa76243fa3d30250f"}
|
||||||