Compare commits

..
1 Commits
Author SHA1 Message Date
github-actions[bot] 12f658dfd9 Update app 2026-07-29 02:43:50 +00:00
57 changed files with 1 additions and 12807 deletions

No files matched your search

-97
View File
@@ -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()
}
-198
View File
@@ -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()
-64
View File
@@ -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'
],
}
}
-51
View File
@@ -1,51 +0,0 @@
const fs = require('fs-extra')
const path = require('path')
const crypto = require('crypto')
const AdmZip = require('adm-zip')
const { version, autoUpdateActive, autoUpdateFrom } = 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: autoUpdateActive,
version: version,
from: autoUpdateFrom,
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()
-37
View File
@@ -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
-55
View File
@@ -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 }}
-26
View File
@@ -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
View File
@@ -1 +0,0 @@
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
-21
View File
@@ -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.
-58
View File
@@ -1,58 +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. 打开游戏的调频详情页面
![详情页面](/docs/wish-history.jpg)
3. 点击工具的“加载数据”按钮
![加载数据](/docs/load-data.png)
如果没出什么问题的话,你会看到正在读取数据的提示,最终效果如下图所示
<details>
<summary>展开图片</summary>
![预览](/docs/preview.png)
</details>
如果需要导出多个账号的数据,可以点击旁边的加号按钮。
然后游戏切换的新账号,再打开调频历史记录,工具再点击“加载数据”按钮。
## Stargazers over time
[![Stargazers over time](https://starchart.cc/earthjasonlin/zzz-signal-search-export.svg)](https://starchart.cc/earthjasonlin/zzz-signal-search-export)
## Devlopment
```bash
# 安装模块
yarn install
# 开发模式
yarn dev
# 构建一个可以运行的程序
yarn build
```
## License
[MIT](https://github.com/earthjasonlin/zzz-signal-search-export/blob/main/LICENSE)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 KiB

-56
View File
@@ -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 signal search 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 signal search details page of the game
![warp details](/docs/wish-history-en.jpg)
3. Click the tool's "Load data" button
![load data](/docs/load-data-en.png)
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>
![preview](/docs/preview-en.png)
</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)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

-122
View File
@@ -1,122 +0,0 @@
{
"name": "zzz-signal-search-export",
"version": "1.1.12",
"autoUpdateActive": true,
"autoUpdateFrom": "1.1.0",
"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"
]
}
-6
View File
@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
-65
View File
@@ -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"
}
]
]
]
-109
View File
@@ -1,109 +0,0 @@
{
"symbol.colon": ": ",
"ui.button.load": "Load data",
"ui.button.update": "Update",
"ui.button.directUpdate": "Direct update",
"ui.button.files": "Import/Export",
"ui.button.excel": "Export Excel",
"ui.button.uigf": "Export UIGF",
"ui.button.import": "Import UIGF",
"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.setting.idVersion": "ID database version",
"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} failedretrying in 5 seconds for the ${count} time……",
"log.fetch.retryFailed": "Processing ${name} of page ${page} failedretry 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 URLplease 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 successfulplease 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",
"uigf.fileType": "Uniformed Interchangeable GachaLog Format v4.0 (Beta)",
"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",
"ui.uigf.title": "Please select the UID(s) you want to export"
}
-108
View File
@@ -1,108 +0,0 @@
{
"symbol.colon": "",
"ui.button.load": "加载数据",
"ui.button.update": "更新数据",
"ui.button.directUpdate": "直接更新",
"ui.button.files": "导入/导出",
"ui.button.excel": "导出Excel",
"ui.button.uigf":"导出UIGF",
"ui.button.import":"导入UIGF",
"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.setting.idVersion": "ID 数据库版本",
"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文件",
"uigf.fileType":"统一可交换抽卡记录标准 v4.0Beta",
"ui.extra.cacheClean": "1. 确认是否已经打开游戏内的抽卡历史记录,如果仍然出现“身份认证已过期”的错误,再尝试下面的步骤\n2. 关闭绝区零的游戏窗口\n3. 点击上方的“打开缓存文件夹”按钮,打开Cache文件夹\n4. 删除Cache_Data文件夹\n5. 启动绝区零游戏,打开游戏内抽卡历史记录页面\n6. 关闭这个对话框,再点击“更新数据”按钮",
"ui.extra.findCacheFolder": "如果点“打开缓存文件夹”按钮没有反应,可以手动找到游戏的网页缓存文件夹,目录为“你的游戏安装路径/ZenlessZoneZero_Data/webCaches/Cache/”",
"ui.extra.urlCopied": "URL已复制",
"ui.uigf.title": "请选择要导出的UID"
}
-107
View File
@@ -1,107 +0,0 @@
{
"symbol.colon": "",
"ui.button.load": "加載數據",
"ui.button.update": "更新數據",
"ui.button.directUpdate": "直接更新",
"ui.button.files": "導入/匯出",
"ui.button.excel": "導出Excel",
"ui.button.uigf":"導出UIGF",
"ui.button.import":"導入UIGF",
"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.setting.idVersion": "ID 數據庫版本",
"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文件",
"uigf.fileType":"統一可交換抽卡記錄標準 v4.0Beta",
"ui.extra.cacheClean": "1. 確認是否已經打開遊戲內的抽卡歷史記錄,如果仍然出現「身份認證已過期」的錯誤,再嘗試下面的步驟\n2. 關閉絕區零的遊戲窗口\n3. 點擊上方的「打開緩存文件夾」按鈕,打開Cache文件夾\n4. 刪除Cache_Data文件夾\n5. 啟動絕區零遊戲,打開遊戲內抽卡歷史記錄頁面\n6. 關閉這個對話框,再點擊「更新數據」按鈕",
"ui.extra.findCacheFolder": "如果點「打開緩存文件夾」按鈕沒有反應,可以手動找到遊戲的網頁緩存文件夾,目錄為「你的遊戲安裝路徑/ZenlessZoneZero_Data/webCaches/Cache/」",
"ui.extra.urlCopied": "URL已復製",
"ui.uigf.title": "請選擇要導出的UID"
}
-3438
View File
File diff suppressed because it is too large. Load diff
-174
View File
@@ -1,174 +0,0 @@
const { app, ipcMain, dialog } = require('electron')
const fs = require('fs-extra')
const path = require('path')
const { getData, saveData, changeCurrent, convertTimeZone } = require('./getData')
const config = require('./config')
const { name, version } = require('../../package.json')
const i18n = require('./i18n')
const { mergeData } = require('./utils/mergeData')
const { sendMsg } = require('./utils')
const idJson = require('../idJson.json')
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 exportUIGF = async (uids) => {
const result = {
info: {
export_timestamp: Math.ceil(Date.now() / 1000),
export_app: `${name}`,
export_app_version: `v${version}`,
version: "v4.0"
},
nap: []
}
const { dataMap, current } = await getData()
let fulldata = []
uids.forEach(uid => {
fulldata.push(dataMap.get(uid))
})
if (!fulldata.length) {
throw new Error('数据为空')
}
fulldata.forEach(data => {
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: log.count,
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)))
let dataTemp = {
uid: data.uid,
timezone: data.region_time_zone,
lang: data.lang,
list: []
}
listTemp.forEach(item => {
dataTemp.list.push({
...item
})
})
result.nap.push(dataTemp)
})
const filePath = dialog.showSaveDialogSync({
defaultPath: path.join(app.getPath('downloads'), fulldata.length > 1 ? `UIGF_${getTimeString()}` : `UIGF_${fulldata[0].uid}_${getTimeString()}`),
filters: [
{ name: i18n.uigf.fileType, extensions: ['json'] }
]
})
if (filePath) {
await fs.ensureFile(filePath)
await fs.writeFile(filePath, JSON.stringify(result))
}
}
const importUIGF = async () => {
const filepath = await dialog.showOpenDialogSync({
properties: ['openFile'],
filters: [
{ name: i18n.uigf.fileType, extensions: ['json'] }
]
})
if (!filepath) return
const { dataMap, current } = await getData()
try {
const jsonData = fs.readJsonSync(filepath[0])
if('info' in jsonData && 'version' in jsonData.info) {
if (jsonData.info.version !== 'v4.0') {
sendMsg('不支持此版本UIGF')
console.error('不支持此版本UIGF')
return
}
} else {
sendMsg('UIGF格式错误')
console.error('UIGF格式错误')
return
}
jsonData.nap.forEach(uidData => {
const resultTemp = []
const isNew = !Boolean(dataMap.has(uidData.uid))
let region_time_zone
if (!isNew) region_time_zone = dataMap.get(uidData.uid).region_time_zone
else region_time_zone = uidData.timezone
let targetLang
if (!isNew) targetLang = dataMap.get(uidData.uid).lang
else targetLang = uidData.lang
if(!idJson[targetLang] && (!uidData.list[0].name || !uidData.list[0].item_type || !uidData.list[0].rank_type)) targetLang = config.lang
let idTargetLangJson = idJson[targetLang]
uidData.list.forEach(recordEntry => {
let rank_type
if (idTargetLangJson?.[recordEntry.item_id].rank_type) rank_type = String(idTargetLangJson[recordEntry.item_id].rank_type)
else rank_type = recordEntry.rank_type
resultTemp.push({
gacha_id: recordEntry.gacha_id,
gacha_type: recordEntry.gacha_type,
item_id: recordEntry.item_id,
count: recordEntry.count ?? "1",
time: convertTimeZone(recordEntry.time, uidData.timezone, region_time_zone),
name: idTargetLangJson?.[recordEntry.item_id].name ?? recordEntry.name,
item_type: idTargetLangJson?.[recordEntry.item_id].item_type ?? recordEntry.item_type,
rank_type: rank_type,
id: recordEntry.id
})
})
const resultTempGrouped = resultTemp.reduce((acc, curr) => {
if (!acc[curr.gacha_type]) {
acc[curr.gacha_type] = []
}
acc[curr.gacha_type].push(curr)
return acc;
}, {})
const resultTempMap = new Map(Object.entries(resultTempGrouped))
const resultMap = { result: resultTempMap, uid: uidData.uid}
let data
const mergedData = mergeData(dataMap.get(uidData.uid), resultMap)
if (isNew) {
data = { result: mergedData, time: Date.now(), uid: uidData.uid, lang: targetLang, region_time_zone: uidData.timezone, deleted: false }
} else {
data = { result: mergedData, time: Date.now(), uid: dataMap.get(uidData.uid).uid, lang: targetLang, region_time_zone: dataMap.get(uidData.uid).region_time_zone, deleted: dataMap.get(uidData.uid).deleted }
}
saveData(data, '')
changeCurrent(uidData.uid)
dataMap.set(uidData.uid, data)
})
return {
dataMap,
current: config.current
}
} catch (error) {
sendMsg(error, 'ERROR')
console.error(error)
}
}
ipcMain.handle('EXPORT_UIGF_JSON', async (event, uids) => {
await exportUIGF(uids)
})
ipcMain.handle('IMPORT_UIGF_JSON', async () => {
return await importUIGF()
})
-15
View File
@@ -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)
})
-78
View File
@@ -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
-172
View File
@@ -1,172 +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)
if(log.rank_type === '2') {
arr.push(i18n.ui.data.star2)
} else if(log.rank_type === '3') {
arr.push(i18n.ui.data.star3)
} else {
arr.push(i18n.ui.data.star4)
}
arr.push(total)
arr.push(pity)
temp.push(arr)
if (log.rank_type === '4') {
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 = {
2: "ff8e8e8e",
3: "ffa256e1",
4: "ffbd6932",
}
sheet.getCell(`${c}${i + 2}`).font = {
name: customFont,
color: { argb: rankColor[v.rank_type] },
bold : v.rank_type != "2"
}
})
})
}
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()
})
-561
View File
@@ -1,561 +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 serverTimeZone = new Map([
["prod_gf_cn", 8],
["prod_gf_jp", 8],
["prod_gf_us", -5],
["prod_gf_eu", 1],
["prod_gf_sg", 8]
])
const convertTimeZone = (dateTimeStr, fromTimeZoneOffset, toTimeZoneOffset) => {
let date = new Date(dateTimeStr.replace(' ', 'T') + 'Z');
let utcDate = new Date(date.getTime() - fromTimeZoneOffset * 60 * 60 * 1000);
let targetDate = new Date(utcDate.getTime() + toTimeZoneOffset * 60 * 60 * 1000);
let year = targetDate.getUTCFullYear();
let month = String(targetDate.getUTCMonth() + 1).padStart(2, '0');
let day = String(targetDate.getUTCDate()).padStart(2, '0');
let hours = String(targetDate.getUTCHours()).padStart(2, '0');
let minutes = String(targetDate.getUTCMinutes()).padStart(2, '0');
let seconds = String(targetDate.getUTCSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
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)
data.result.forEach((value, key) => {
value.forEach(item => {
if (!('count' in item)) {
item.count = "1";
}
});
});
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/绝区零/', '/miHoYo/ZenlessZoneZero/']
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 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
}
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 }
}
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 localTimeZone
for (const type of gachaType) {
const { list, uid, region} = await getGachaLogs(type, queryString)
const region_time_zone = serverTimeZone.get(region)
if(!region_time_zone) {
sendMsg('不支持此服务器')
console.error('不支持此服务器')
return
}
if (localTimeZone === undefined) {
localTimeZone = dataMap.get(uid)?.region_time_zone
if (localTimeZone === undefined) {
localTimeZone = region_time_zone
}
}
localTimeZone === Number(localTimeZone)
list.forEach(item => {
item.time = convertTimeZone(item.time, region_time_zone, localTimeZone)
})
const logs = list.map((item) => {
const { id, item_id, item_type, name, rank_type, time, gacha_id, gacha_type, count} = item
return { id, item_id, item_type, name, rank_type, time, gacha_id, gacha_type, count }
})
logs.reverse()
typeMap.set(type.key, type.name)
result.set(type.key, logs)
if (!originUid) {
originUid = uid
}
}
const data = { result, typeMap, time: Date.now(), uid: originUid, lang, region_time_zone: localTimeZone }
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.serverTimeZone = serverTimeZone
exports.getUrl = getUrl
exports.deleteData = deleteData
exports.saveData = saveData
exports.changeCurrent = changeCurrent
exports.convertTimeZone = convertTimeZone
-96
View File
@@ -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',"uigf"
]
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
-70
View File
@@ -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('./UIGFJson')
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()
}
})
}
-39
View File
File diff suppressed because one or more lines are too long.
-185
View File
@@ -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()
}
File diff suppressed because one or more lines are too long.
-39
View File
@@ -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
}
-65
View File
@@ -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
-202
View File
@@ -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
}
-40
View File
@@ -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 }
-72
View File
@@ -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",
}
])
})
-385
View File
@@ -1,385 +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="uigf-json">{{ui.button.uigf}}</el-dropdown-item>
<el-dropdown-item command="import-json" divided>{{ui.button.import}}</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, ElMessageBox } 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 exportUIGFJSON = () => {
let uidList = []
dataMap.value.forEach(item => {
uidList.push(item.uid)
})
ElMessageBox({
title: state.i18n.ui.uigf.title,
message: `
<div>
${uidList.map(uid => `
<div>
<input type="checkbox" id="${uid}" value="${uid}" />
<label for="${uid}">${uid}</label>
</div>
`).join('')}
</div>
`,
dangerouslyUseHTMLString: true,
showCancelButton: true,
confirmButtonText: state.i18n.ui.common.ok,
cancelButtonText: state.i18n.ui.common.cancel,
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
const selected_uids = uidList.filter(uid => document.getElementById(uid).checked);
ipcRenderer.invoke('EXPORT_UIGF_JSON', selected_uids);
}
done();
}
}).then(() => {
}).catch(() => {
});
}
const importData = async () => {
state.status = 'loading'
const data = await ipcRenderer.invoke('IMPORT_UIGF_JSON')
if (data) {
state.dataMap = data.dataMap
state.current = data.current
state.status = 'loaded'
} else {
state.status = 'failed'
}
}
const exportCommand = (type) => {
if (type === 'excel') {
saveExcel()
} else if (type === 'uigf-json') {
exportUIGFJSON()
} else if (type === 'import-json') {
importData()
}
}
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>
-91
View File
@@ -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>
-127
View File
@@ -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 = ["#fac858", "#fac858", "#ee6666", "#5470c6", "#5470c6", "#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>
-167
View File
@@ -1,167 +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">{{text.idVersion}} {{idJson.version}}</p>
<p class="text-gray-600 text-xs mt-1">{{about.license}}</p>
<p class="text-gray-600 text-xs mt-1">GitHub: <a @click="openGithub" class="cursor-pointer text-blue-400">https://github.com/earthjasonlin/zzz-signal-search-export</a></p>
<p class="text-gray-600 text-xs mt-1 pb-6">UIGF: <a @click="openUIGF" class="cursor-pointer text-blue-400">https://uigf.org/</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 idJson from '../../idJson.json'
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 openUIGF = () => shell.openExternal('https://uigf.org/')
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>
-78
View File
@@ -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
-22
View File
@@ -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;
}
}
-12
View File
@@ -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>
-11
View File
@@ -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')
-30
View File
@@ -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,
}
-16
View File
@@ -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: [],
}
-81
View File
@@ -1,81 +0,0 @@
# pylint: disable=C0116, C0103, C0201
"""Download and process data from the Hakushin API"""
import json
import requests
from opencc import OpenCC
# 初始化 OpenCC 转换器
cc = OpenCC('s2t')
# 获取 JSON 数据
weapon_url = 'https://api.hakush.in/zzz/data/weapon.json'
character_url = 'https://api.hakush.in/zzz/data/character.json'
bangboo_url = 'https://api.hakush.in/zzz/data/bangboo.json'
version_url = 'https://api.hakush.in/zzz/new.json'
# 语言映射配置
language_map = {
"zh-cn": "CHS",
"zh-tw": "CHS", # 简体转繁体
"en-us": "EN",
"ja-jp": "JA",
"ko-kr": "KO"
}
# 类型映射配置
type_map = {
"weapon": {"zh-cn": "音擎", "zh-tw": "音擎", "en-us": "W-Engines", "ja-jp": "音動機", "ko-kr": "W-엔진"},
"character": {"zh-cn": "代理人", "zh-tw": "代理人", "en-us": "Agents",
"ja-jp": "エージェント", "ko-kr": "에이전트"},
"bangboo": {"zh-cn": "邦布", "zh-tw": "邦布", "en-us": "Bangboo",
"ja-jp": "ボンプ", "ko-kr": "「Bangboo」"}
}
def fetch_json(url):
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
def transform_data(data, item_type):
transformed = {lang: {} for lang in language_map.keys()}
for id_, item in data.items():
for lang, key in language_map.items():
name = item[key] if lang != 'zh-tw' else cc.convert(item['CHS'])
transformed[lang][id_] = {
"name": name,
"item_type": type_map[item_type][lang],
"rank_type": item['rank']
}
return transformed
def main():
try:
weapon_data = fetch_json(weapon_url)
character_data = fetch_json(character_url)
bangboo_data = fetch_json(bangboo_url)
version_data = fetch_json(version_url)
transformed_data = {lang: {} for lang in language_map.keys()}
transformed_data["version"] = version_data["version"]
weapon_transformed = transform_data(weapon_data, "weapon")
character_transformed = transform_data(character_data, "character")
bangboo_transformed = transform_data(bangboo_data, "bangboo")
for lang in language_map.keys():
transformed_data[lang].update(weapon_transformed[lang])
transformed_data[lang].update(character_transformed[lang])
transformed_data[lang].update(bangboo_transformed[lang])
with open('./src/idJson.json', 'w', encoding='utf-8') as f:
json.dump(transformed_data, f, ensure_ascii=False, indent=2)
print("Data successfully transformed and saved")
except requests.RequestException as e:
print(f"Error fetching data: {e}")
if __name__ == "__main__":
main()
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
{"active":true,"version":"1.1.21","from":"1.1.0","name":"50cd7.zip","hash":"2d619a250cd7a33d83b1601f9c592312b28084bbbf46526aa76243fa3d30250f"}
-5329
View File
File diff suppressed because it is too large. Load diff