Departure commit

This commit is contained in:
mio 2023-05-01 15:52:32 +08:00
commit 92cafdadb8
61 changed files with 9597 additions and 0 deletions

97
.electron-vite/build.js Normal file

@ -0,0 +1,97 @@
'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()
}

@ -0,0 +1,198 @@
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()

@ -0,0 +1,64 @@
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'
],
}
}

63
.electron-vite/update.js Normal file

@ -0,0 +1,63 @@
const fs = require('fs-extra')
const path = require('path')
const crypto = require('crypto')
const AdmZip = require('adm-zip')
const { version } = require('../package.json')
const hash = (data, type = 'sha256') => {
const hmac = crypto.createHmac(type, 'hk4e')
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-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)
await fs.outputFile('./build/update/CNAME', 'star-rail-warp-export.css.moe')
createZip(appPath, zipPath)
const buffer = await fs.readFile(zipPath)
const sha256 = hash(buffer)
const hashName = sha256.slice(7, 12)
await fs.copy(zipPath, path.resolve(outputPath, `${hashName}.zip`))
await fs.remove(zipPath)
await fs.outputJSON(path.join(outputPath, 'manifest.json'), {
active: true,
version,
from: '0.1.5',
name: `${hashName}.zip`,
hash: sha256
})
copyHTML()
}
const copyAppZip = () => {
try {
const dir = path.resolve('./build')
const filePath = path.resolve(dir, `StarRailWarpExport-${version}-win.zip`)
fs.copySync(filePath, path.join(dir, 'app.zip'))
} catch (e) {}
}
const copyHTML = () => {
try {
const output = path.resolve('./build/update/')
const dir = path.resolve('./src/web/')
fs.copySync(dir, output)
} catch (e) {
console.error(e)
}
}
start()

@ -0,0 +1,37 @@
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

31
.github/workflows/build-update.yml vendored Normal file

@ -0,0 +1,31 @@
name: Build Update
on:
workflow_dispatch:
push:
branches: [ main ]
jobs:
main:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '16.x'
- name: Build Update
run: |
yarn --frozen-lockfile
yarn build:dir
yarn build-update
- name: Deploy
if: success()
uses: crazy-max/ghaction-github-pages@v2
with:
commit_message: Update app
build_dir: ./build/update
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

54
.github/workflows/release.yml vendored Normal file

@ -0,0 +1,54 @@
on:
push:
# Sequence of patterns matched against refs/tags
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
name: Upload Release Asset
jobs:
build:
name: Upload Release Asset
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:win64
yarn build-update
- name: Create Release
if: success()
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ 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.GITHUB_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: StarRailWarpExport.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.GITHUB_TOKEN }}

26
.gitignore vendored Normal file

@ -0,0 +1,26 @@
.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
.npmrc Normal file

@ -0,0 +1 @@
ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron/

21
LICENSE Normal file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 biuuu
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.

52
README.md Normal file

@ -0,0 +1,52 @@
# 星穹铁道跃迁记录导出工具
中文 | [English](https://github.com/biuuu/star-rail-warp-export/blob/main/docs/README_EN.md)
这个项目由[genshin-wish-export](https://github.com/biuuu/genshin-wish-export/)修改而来,功能基本一致。
一个使用 Electron 制作的小工具,需要在 Windows 64位操作系统上运行。
通过读取游戏日志或者代理模式获取访问游戏跃迁记录 API 所需的 authKey然后再使用获取到的 authKey 来读取游戏跃迁记录。
## 其它语言
修改`src/i18n/`目录下的 json 文件就可以翻译到对应的语言。如果觉得已有的翻译有不准确或可以改进的地方,可以随时修改发 Pull Request。
## 使用说明
1. 下载工具后解压 - 下载地址: [Github](https://github.com/biuuu/star-rail-warp-export/releases/latest/download/StarRailWarpExport.zip) / [蓝奏云]()
2. 打开游戏的跃迁历史记录
3. 点击工具的“加载数据”按钮
![加载数据](/docs/load-data.png)
如果没出什么问题的话,你会看到正在读取数据的提示,最终效果如下图所示
<details>
<summary>展开图片</summary>
![预览](/docs/preview.png)
</details>
如果需要导出多个账号的数据,可以点击旁边的加号按钮。
然后游戏切换的新账号,再打开跃迁历史记录,工具再点击“加载数据”按钮。
## Devlopment
```
# 安装模块
yarn install
# 开发模式
yarn dev
# 构建一个可以运行的程序
yarn build
```
## License
[MIT](https://github.com/biuuu/star-rail-warp-export/blob/main/LICENSE)

BIN
build/icons/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
build/icons/icon.icns Normal file

Binary file not shown.

BIN
build/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

54
docs/README_EN.md Normal file

@ -0,0 +1,54 @@
# Star Rail Warp History Exporter
[中文](https://github.com/biuuu/star-rail-warp-export/blob/main/README.md) | English
This project is modified from the [genshin-wish-export](https://github.com/biuuu/genshin-wish-export/) repository, and its functions are basically the same.
A tool made from Electron that runs on the Windows 64 bit operating system.
Read the game log or proxy to get the authKey needed to access the game warp history API, and then use the authKey to read the game wish history.
## Other languages
Modify the JSON file in the `src/i18n/` directory to translate into the appropriate language.
If you feel that the existing translation is inappropriate, you can send a pull request to modify it at any time.
## Usage
1. Unzip after downloading the tool - [Download](https://github.com/biuuu/star-rail-warp-export/releases/latest/download/StarRailWarpExport.zip)
2. Open the warp history of the game
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
```
# install node modules
yarn install
# develop
yarn dev
# Build a program that can run
yarn build
```
## License
[MIT](https://github.com/biuuu/star-rail-warp-export/blob/main/LICENSE)

BIN
docs/load-data-en.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
docs/load-data.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
docs/preview-en.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
docs/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/wish-history-en.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
docs/wish-history.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

118
package.json Normal file

@ -0,0 +1,118 @@
{
"name": "star-rail-warp-export",
"version": "0.0.1",
"main": "./dist/electron/main/main.js",
"author": "biuuu <https://github.com/biuuu>",
"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",
"dev:web": "cross-env TARGET=web node .electron-vite/dev-runner.js",
"start": "electron ./src/main/main.js",
"build-update": "node .electron-vite/update.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": "StarRailWarpExport",
"appId": "org.biuuu.star-rail-warp-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": "^0.2.6",
"@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": "^22.14.5",
"electron-fetch": "^1.7.4",
"electron-unhandled": "^3.0.2",
"electron-window-state": "^5.0.3",
"element-plus": "^1.3.0-beta.7",
"fs-extra": "^10.0.0",
"get-stream": "^6.0.1",
"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
postcss.config.js Normal file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

79
src/i18n/Deutsch.json Normal file

@ -0,0 +1,79 @@
{
"symbol.colon": ": ",
"ui.button.load": "Lade Daten",
"ui.button.update": "Aktualisieren",
"ui.button.excel": "In Excel exportieren",
"ui.button.url": "Eingabe URL",
"ui.button.setting": "Einstellungen",
"ui.button.option": "Optionen",
"ui.button.startProxy": "Proxy modus",
"ui.select.newAccount": "Neuer Nutzer",
"ui.hint.newAccount": "Daten von anderen Nutzern exportieren",
"ui.hint.init": "Bitte öffne deinen Wunschlverlauf im Spiel bevor du versuchst deine Wunschdaten zu laden",
"ui.hint.lastUpdate": "Letzte Aktualisierung",
"ui.hint.failed": "Oops, irgendetwas ist schief gelaufen",
"ui.win.title": "",
"ui.data.total": "Total",
"ui.data.times": "Wünsche",
"ui.data.sum": "Angehäuft",
"ui.data.no5star": "Wünsche ohne 5 Sterne",
"ui.data.character": "Character",
"ui.data.weapon": "",
"ui.data.star5": "5 Sterne",
"ui.data.star4": "4 Sterne",
"ui.data.star3": "3 Sterne",
"ui.data.history": "5 Sterne verlauf",
"ui.data.average": "Durschnittlicher 5 Sterne",
"ui.data.chara5": "5 Sterne Character",
"ui.data.chara4": "4 Sterne Character",
"ui.data.weapon5": "",
"ui.data.weapon4": "",
"ui.data.weapon3": "",
"ui.setting.title": "Einstellungen",
"ui.setting.language": "Sprache",
"ui.setting.languageHint": "Wenn eine Übersetzung fehlt, wird Englisch als Standardsparche ausgewählt.",
"ui.setting.logType": "Aufzeichnungstyp",
"ui.setting.auto": "Automatisch",
"ui.setting.cnServer": "CN Server",
"ui.setting.seaServer": "Globaler Server",
"ui.setting.logTypeHint": "Wähle aus, welche von dem Server generierten Aufzeichnungen benutzt werden sollen, wenn zum ersten mal die URL von den Spielaufzeichnungen erworben wird",
"ui.setting.autoUpdate": "Automatische Aktualisierung",
"ui.setting.proxyMode": "Proxy modus",
"ui.setting.proxyModeHint": "Wenn das Erwerben der URL von den Systemaufzeichnungen scheitert, nutz den Systemproxy",
"ui.setting.closeProxy": "Deaktiviere den Systemproxy",
"ui.setting.closeProxyHint": "Wenn der Proxymodus aktiviert ist und das Programm abstürzt kann es zu unerwünschten Folgen für dein System führen. Du kannst diesen Knopf drücken, um die Systemproxy Einstellungen zurückzusetzen.",
"ui.about.title": "Über uns",
"ui.about.license": "Diese Software ist Open-Source und nutzt die MIT Lizenz.",
"ui.urlDialog.title": "Gebe die URL manuell ein",
"ui.urlDialog.hint": "Diese Funktion sollte nur benutzt werden, falls Sie wissen, welche URL hier benötigt wird",
"ui.urlDialog.placeholder": "Bitte gebe die URL mit den Authentifizierungsinformationen ein",
"ui.common.cancel": "Abbrechen",
"ui.common.ok": "Weiter",
"log.save.failed": "Lokale Daten konnten nicht gespeichert werden",
"log.file.notFound": "Die Wunschaufzeichnungen konnten nicht gefunden werden, stelle sicher, dass du im Spiel deinen Wunschverlauf schon geöffnet hast",
"log.url.notFound": "Konnte die URL nicht finden",
"log.file.readFailed": "Konnte die Aufzeichnungen nicht lesen",
"log.fetch.retry": "Das Verarbeiten von ${name} auf Seite ${page} ist gescheitertversuche in 5 Sekunden erneut, zum ${count}. mal…",
"log.fetch.retryFailed": "Das Verarbeiten von ${name} auf Seite ${page} ist gescheitert, maximale Versuche wurden erreicht",
"log.fetch.interval": "Verarbeite ${name} auf Seite ${page}1 Sekunde Timeout für 10 Seiten…",
"log.fetch.current": "Verarbeite ${name} auf Seite ${page}",
"log.fetch.authTimeout": "Die Nutzer Authentifizierung ist abgelaufen, bitte öffne im Spiel die Wunschaufzeichnungen erneut.",
"log.fetch.gachaType": "Wunschtyp wird erworben, bitte warten",
"log.fetch.gachaTypeOk": "Wunschtyp erworben",
"log.url.lackAuth": "Der Authentifizierungsschlüssel konnte in der URL nicht aufgefunden werden",
"log.proxy.hint": "Nutze den Proxymodus [${ip}:${port}] um die URL zu erwerben, bitte öffne im Spiel die Wunschaufzeichnungen erneut.",
"log.url.notFound2": "URL konnte nicht gefunden werden, bitte stelle sicher, dass du deinen Wunschverlauf schon einmal im Spiel geöffnet hast",
"log.url.incorrect": "URL Parameter konnten nicht erworben werden",
"log.autoUpdate.success": "Die automatische aktualisierung war erfolgreich, bitte starten sie das Programm neu",
"excel.header.time": "zeit",
"excel.header.name": "name",
"excel.header.type": "typ",
"excel.header.rank": "seltenheit",
"excel.header.total": "ingesammt",
"excel.header.pity": "innerhalb von pity",
"excel.header.remark": "bemerkung",
"excel.wish2": "Wunsch 2",
"excel.customFont": "Arial",
"excel.filePrefix": "",
"excel.fileType": "Excel Datei"
}

88
src/i18n/English.json Normal file

@ -0,0 +1,88 @@
{
"symbol.colon": ": ",
"ui.button.load": "Load data",
"ui.button.update": "Update",
"ui.button.directUpdate": "Direct update",
"ui.button.excel": "Export Excel",
"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.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": "Star Rail Warp History Exporter",
"ui.data.total": "Total",
"ui.data.times": "Pulls",
"ui.data.sum": "Accumulated",
"ui.data.no5star": "pulls without a 5 star",
"ui.data.character": "Character",
"ui.data.weapon": "Light Cone",
"ui.data.star5": "5 star",
"ui.data.star4": "4 star",
"ui.data.star3": "3 star",
"ui.data.history": "5 star history",
"ui.data.average": "5 star on average",
"ui.data.chara5": "5 star character",
"ui.data.chara4": "4 star character",
"ui.data.weapon5": "5 star light cone",
"ui.data.weapon4": "4 star light cone",
"ui.data.weapon3": "3 star light cone",
"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.autoUpdate": "Auto update",
"ui.setting.hideNovice": "Hide Departure Warp",
"ui.setting.proxyMode": "Proxy mode",
"ui.setting.proxyModeHint": "When we fail to get the URL from system logs, use the system proxy",
"ui.setting.fetchFullHistory": "Get complete data",
"ui.setting.fetchFullHistoryHint": "When this option is enabled, click the \"Update Data\" button to get all the card draw records within 6 months. When there are incorrect data within 6 months, this function can be used to repair.",
"ui.setting.closeProxy": "Disable system proxy",
"ui.setting.closeProxyHint": "When you choose proxy mode, if the program crashes it can cause unwanted results that may affect your system. You can click this button to clear the system proxy settings.",
"ui.about.title": "About",
"ui.about.license": "This software is opensource using MIT license.",
"ui.urlDialog.title": "Input URL manually",
"ui.urlDialog.hint": "This function should only be used when you understand what URL is needed here",
"ui.urlDialog.placeholder": "Please enter the URL with authentication information",
"ui.common.cancel": "Cancel",
"ui.common.ok": "OK",
"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 warp type, please wait",
"log.fetch.gachaTypeOk": "Warp 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": "Warp 2",
"excel.customFont": "Arial",
"excel.filePrefix": "Star Rail Warp logger",
"excel.fileType": "Excel file",
"ui.extra.cacheClean": "1. Confirm whether the warp 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 Star Rail \n3. Click the \"Open Web Cache Folder\" button above to open the \"Cache\" folder \n4. Delete the \"Cache_ Data\" folder \n5. Start the Star Rail game and open the warp 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/Star Rail/Game/StarRail_Data/webCaches/Cache/\""
}

77
src/i18n/Español.json Normal file

@ -0,0 +1,77 @@
{
"symbol.colon": ": ",
"ui.button.load": "Obtener datos",
"ui.button.update": "Actualizar",
"ui.button.excel": "Exportar a Excel",
"ui.button.url": "Introducir URL",
"ui.button.setting": "Ajustes",
"ui.button.option": "Opciones",
"ui.button.startProxy": "Modo proxy",
"ui.select.newAccount": "Nueva cuenta",
"ui.hint.newAccount": "Exportar datos de otras cuentas",
"ui.hint.init": "Por favor, abre el historial de deseos en el juego antes de pulsar en el botón 'Obtener datos'.",
"ui.hint.lastUpdate": "Última actualización",
"ui.hint.failed": "Ups, algo ha fallado",
"ui.win.title": "",
"ui.data.total": "Total",
"ui.data.times": "tiradas",
"ui.data.sum": "acumuladas.",
"ui.data.no5star": "tiradas sin un 5 estrellas",
"ui.data.character": "Personaje",
"ui.data.weapon": "",
"ui.data.star5": "5 estrellas",
"ui.data.star4": "4 estrellas",
"ui.data.star3": "3 estrellas",
"ui.data.history": "Historial de 5 estrellas",
"ui.data.average": "Promedio de tiradas para un 5 estrellas",
"ui.data.chara5": "Personaje 5 estrellas",
"ui.data.chara4": "Personaje 4 estrellas",
"ui.data.weapon5": "",
"ui.data.weapon4": "",
"ui.data.weapon3": "",
"ui.setting.title": "Ajustes",
"ui.setting.language": "Idiomas",
"ui.setting.languageHint": "Si no se encuentra una traducción se mostrará en inglés por defecto.",
"ui.setting.logType": "Tipo de log",
"ui.setting.auto": "Auto",
"ui.setting.cnServer": "Servidor CN",
"ui.setting.seaServer": "Servidor global",
"ui.setting.logTypeHint": "Elige qué logs generados por el servidor se utilizarán primero al obtener la URL de los logs del juego.",
"ui.setting.autoUpdate": "Actualización automática",
"ui.setting.proxyMode": "Modo proxy",
"ui.setting.proxyModeHint": "Cuando no se pueda obtener la URL de los logs del sistema utiliza el modo proxy.",
"ui.setting.closeProxy": "Desactivar proxy del sistema",
"ui.setting.closeProxyHint": "Al seleccionar el modo proxy si el programa falla puede causar resultados no deseados que pueden afectar a tu sistema. Puede hacer click en este botón para borrar la configuración del proxy del sistema.",
"ui.about.title": "",
"ui.about.license": "Este software es opensource con licencia MIT.",
"ui.urlDialog.title": "Introducir URL manualmente",
"ui.urlDialog.hint": "Utiliza esta función solo si sabes qué URL se necesita introducir",
"ui.urlDialog.placeholder": "Introduce la URL con la información de autenticación",
"ui.common.cancel": "Cancelar",
"ui.common.ok": "OK",
"log.save.failed": "Error al guardar datos locales",
"log.file.notFound": "No se han podido encontrar los logs del juego, asegurate de que has abierto el historial de deseos dentro del juego.",
"log.url.notFound": "No se ha podido encontrar la URL",
"log.file.readFailed": "Error al leer los logs",
"log.fetch.retry": "Error al procesar ${name} en la página ${page}. Reintentando en 5 segudos por ${count} vez",
"log.fetch.retryFailed": "Error al procesar ${name} en la página ${page}alcanzado el número máximo de intentos",
"log.fetch.interval": "Procesando ${name} en la página ${page}1 segundo de tiempo de espera cada 10 páginas",
"log.fetch.current": "Procesando ${name} en la página ${page}",
"log.fetch.authTimeout": "La autenticación ha expirado, Abre de nuevo el historial de deseos en el juego.",
"log.fetch.gachaType": "Obteniendo el tipo de deseo",
"log.fetch.gachaTypeOk": "Tipo de deseo obtenido",
"log.url.lackAuth": "No se encuentra la Authkey en la URL",
"log.proxy.hint": "Usando modo proxy [${ip}:${port}] para obtener la URL, Abre de nuevo el historial de deseos en el juego.",
"log.url.notFound2": "Error al obtener la URL, asegurate de que has abierto el historial de deseos dentro del juego.",
"log.url.incorrect": "Error al obtener los parámetros de la URL",
"log.autoUpdate.success": "Actualizado correctamente, reinicia el programa",
"excel.header.time": "tiempo",
"excel.header.name": "nombre",
"excel.header.type": "tipo",
"excel.header.rank": "rareza",
"excel.header.total": "total",
"excel.header.pity": "pity",
"excel.customFont": "Arial",
"excel.filePrefix": "",
"excel.fileType": "Excel file"
}

84
src/i18n/Français.json Normal file

@ -0,0 +1,84 @@
{
"symbol.colon": " : ",
"ui.button.load": "Charger les données",
"ui.button.update": "Mettre à jour",
"ui.button.directUpdate": "Mise à jour directe",
"ui.button.excel": "Exporter vers Excel",
"ui.button.url": "URL d'import",
"ui.button.setting": "Paramètres",
"ui.button.option": "Options",
"ui.button.startProxy": "Mode Proxy",
"ui.select.newAccount": "Nouveau compte",
"ui.hint.newAccount": "Charger les données d'autres comptes",
"ui.hint.init": "Veuillez ouvrir votre historique de vœux depuis le client du jeu avant de cliquer sur le bouton 'Charger les données'.",
"ui.hint.lastUpdate": "Dernière mise à jour",
"ui.hint.failed": "Oups, une erreur est survenue...",
"ui.hint.relaunchHint": "La mise à jour est terminée, elle prendra effet après avoir cliqué sur le bouton permettant le redémarrage de l'outil",
"ui.win.title": "",
"ui.data.total": "Total de",
"ui.data.times": "tirages.",
"ui.data.sum": "Vous avez effectué",
"ui.data.no5star": "tirages sans objet 5★.",
"ui.data.character": "Personnage",
"ui.data.weapon": "Arme",
"ui.data.star5": "5★",
"ui.data.star4": "4★",
"ui.data.star3": "3★",
"ui.data.history": "Historique de 5★",
"ui.data.average": "Moyenne de tirages d'objet 5★",
"ui.data.chara5": "Personnage 5★",
"ui.data.chara4": "Personnage 4★",
"ui.data.weapon5": "Arme 5★",
"ui.data.weapon4": "Arme 4★",
"ui.data.weapon3": "Arme 3★",
"ui.setting.title": "Paramètres",
"ui.setting.language": "Langue",
"ui.setting.languageHint": "L'anglais sera utilisé par défaut si la traduction sélectionnée n'est pas disponible.",
"ui.setting.logType": "Type de journalisation",
"ui.setting.auto": "Automatique",
"ui.setting.cnServer": "Serveur Chinois",
"ui.setting.seaServer": "Serveur Global",
"ui.setting.logTypeHint": "Choisissez les journaux générés par le serveur à utiliser en priorité lors de la récupération de l'URL à partir des journaux du jeu.",
"ui.setting.autoUpdate": "Mise à jour automatique",
"ui.setting.hideNovice": "Masquer les vœux du débutant",
"ui.setting.proxyMode": "Mode Proxy",
"ui.setting.proxyModeHint": "Si la récupération de l'URL depuis les journaux système échoue, utilisez le proxy système.",
"ui.setting.fetchFullHistory": "Récupérer l'intégralité des données",
"ui.setting.fetchFullHistoryHint": "Lorsque cette option est active, cliquez sur le bouton \"Mettre à jour les données\" pour récupérer tous les enregistrements des tirages des 6 derniers mois. Cette fonction peut être utilisée si des erreurs figurent dans les données des 6 derniers mois.",
"ui.setting.closeProxy": "Désactiver le proxy système",
"ui.setting.closeProxyHint": "Si le programme se bloque lorsque vous choisissez le mode proxy, des résultats indésirables susceptibles d'affecter votre système peuvent survenir. Vous pouvez cliquer sur ce bouton pour réinitialiser les paramètres du proxy système.",
"ui.about.title": "À propos",
"ui.about.license": "Ce logiciel est open source et sous licence MIT.",
"ui.urlDialog.title": "Saisir l'URL d'import manuellement",
"ui.urlDialog.hint": "Cette fonctionnalité ne doit être utilisée que lorsque vous savez quel type d'URL est nécessaire ici.",
"ui.urlDialog.placeholder": "Veuillez saisir l'URL avec les informations d'authentification.",
"ui.common.cancel": "Annuler",
"ui.common.ok": "OK",
"log.save.failed": "Échec de la sauvegarde des données locales.",
"log.file.notFound": "Les journaux du jeu sont introuvables, veuillez vous assurer que vous avez déjà ouvert l'historique de vœux dans le client du jeu.",
"log.url.notFound": "URL introuvable.",
"log.file.readFailed": "Échec de la lecture des journaux.",
"log.fetch.retry": "Échec de la récupération des ${name} - page ${page}, nouvelle tentative dans 5 secondes pour la ${count}e fois……",
"log.fetch.retryFailed": "Échec de la récupération des ${name} - page ${page}, nombre de tentatives maximum atteint.",
"log.fetch.interval": "Récupération des ${name} - page ${page}, délai de 1 seconde toutes les 10 pages……",
"log.fetch.current": "Récupération des ${name} - page ${page}.",
"log.fetch.authTimeout": "L'authentification de l'utilisateur a expiré, veuillez rouvrir l'historique des vœux dans le client du jeu.",
"log.fetch.gachaType": "Récupération du type de vœux, veuillez patienter.",
"log.fetch.gachaTypeOk": "Le type de vœux a été récupéré.",
"log.url.lackAuth": "Clé d'authentification introuvable dans l'URL.",
"log.proxy.hint": "Utilisation du mode proxy [${ip}:${port}] pour obtenir l'URL, veuillez rouvrir l'historique des vœux dans le client du jeu.",
"log.url.notFound2": "URL introuvable, veuillez vous assurer que vous avez déjà ouvert l'historique de vœux dans le client du jeu.",
"log.url.incorrect": "Impossible d'obtenir les paramètres d'URL.",
"log.autoUpdate.success": "Mise à jour automatique réussie, veuillez redémarrer le programme.",
"excel.header.time": "Date",
"excel.header.name": "Nom",
"excel.header.type": "Type",
"excel.header.rank": "Rareté",
"excel.header.total": "Tirages",
"excel.header.pity": "Pity 5★",
"excel.header.remark": "Commentaire",
"excel.wish2": "",
"excel.customFont": "Arial",
"excel.filePrefix": "",
"excel.fileType": "Classeur Excel"
}

77
src/i18n/Indonesia.json Normal file

@ -0,0 +1,77 @@
{
"symbol.colon": ": ",
"ui.button.load": "Muat data",
"ui.button.update": "Perbarui",
"ui.button.excel": "Ekspor Excel",
"ui.button.url": "Masukkan URL",
"ui.button.setting": "Pengaturan",
"ui.button.option": "Pilihan",
"ui.button.startProxy": "Mode proksi",
"ui.select.newAccount": "Akun baru",
"ui.hint.newAccount": "Ekspor data dari akun lain",
"ui.hint.init": "Silakan buka riwayat permohonan anda di dalam klien permainan sebelum klik pada tombol 'Muat data' ",
"ui.hint.lastUpdate": "Terakhir diperbarui",
"ui.hint.failed": "Aduhh, tampaknya gagal",
"ui.win.title": "",
"ui.data.total": "Total",
"ui.data.times": "Pulls",
"ui.data.sum": "Akumulasi",
"ui.data.no5star": "pulls tanpa ★ 5",
"ui.data.character": "Karakter",
"ui.data.weapon": "★ 5",
"ui.data.star4": "★ 4",
"ui.data.star3": "★ 3",
"ui.data.history": "Riwayat ★ 5",
"ui.data.average": "★ 5 dalam rata - rata",
"ui.data.chara5": "★ 5 karakter",
"ui.data.chara4": "★ 4 karakter",
"ui.data.weapon5": "★ 5 senjata",
"ui.data.weapon4": "★ 4 senjata",
"ui.data.weapon3": "★ 3 senjata",
"ui.setting.title": "Pengatuan",
"ui.setting.language": "Bahasa",
"ui.setting.languageHint": "Jika terjemahan hilang, Bahasa Inggris akan ditampilkan secara default.",
"ui.setting.logType": "Tipe catatan",
"ui.setting.auto": "Auto",
"ui.setting.cnServer": "Server China",
"ui.setting.seaServer": "Server Global",
"ui.setting.logTypeHint": "Pilih catatan yang dihasilkan dari server mana yang akan digunakan pertama kali saat memperoleh URL di dalam catatan permainan",
"ui.setting.autoUpdate": "Automatis perbarui",
"ui.setting.proxyMode": "Mode proksi",
"ui.setting.proxyModeHint": "Ketika kita gagal mengambil URL dari catatan sistem, gunakan proksi sistem",
"ui.setting.closeProxy": "Matikan sistem proksi",
"ui.setting.closeProxyHint": "Ketika anda memilih mode proksi, jika program macet dapat menyebabkan hasil yang tidak diinginkan yang dapat mempengaruhi sistem anda. Anda dapat klik tombol ini untuk menghapus pengaturan sistem proksi.",
"ui.about.title": "Tentang",
"ui.about.license": "Perangkat lunak ini opensource menggunakan lisensi MIT.",
"ui.urlDialog.title": "Masukkan URL secara manual",
"ui.urlDialog.hint": "Fungsi ini hanya boleh digunakan jika anda memahami URL apa yang dibutuhkan di sini",
"ui.urlDialog.placeholder": "Silakan masukkan URL dengan informasi autentikasi",
"ui.common.cancel": "Batalkan",
"ui.common.ok": "OK",
"log.save.failed": "Gagal untuk menyimpan data lokal",
"log.file.notFound": "Tidak bisa menemukan catatan permainan, pastikan anda telah membuka riwayat permohonan di dalam klien permainan",
"log.url.notFound": "Tidak dapat menemukan URL",
"log.file.readFailed": "Gagal membaca catatan",
"log.fetch.retry": "Proses ${name} dari halaman $ {page} gagal, mencoba lagi dalam 5 detik untuk waktu ${count} ……",
"log.fetch.retryFailed": "Proses ${name} dari halaman $ {page} gagal, waktu coba lagi telah mencapai batas maksimum",
"log.fetch.interval": "Proses ${name} dari halaman ${page} waktu tunggu 1 detik setiap 10 halaman ……",
"log.fetch.current": "Proses ${name} dari halaman $ {page}",
"log.fetch.authTimeout": "Autentikasi pengguna kedaluwarsa, buka kembali riwayat permohonan di dalam klien permainan.",
"log.fetch.gachaType": "Sedang mengambil tipe permohonan, silahkan tunggu",
"log.fetch.gachaTypeOk": "Jenis permohonan diperoleh",
"log.url.lackAuth": "Authkey tidak ditemukan di URL",
"log.proxy.hint": "Menggunakan mode proksi [${ip}:${port}] untuk mendapatkan URL, buka kembali riwayat permohonan di dalam klien permainan.",
"log.url.notFound2": "Tidak dapat menemukan URL, pastikan Anda telah membuka riwayat permohonan di dalam klien permainan",
"log.url.incorrect": "Tidak bisa mendapatkan parameter URL",
"log.autoUpdate.success": "Pembaruan otomatis berhasil, mulai ulang program",
"excel.header.time": "waktu",
"excel.header.name": "nama",
"excel.header.type": "tipe",
"excel.header.rank": "rarity",
"excel.header.total": "total",
"excel.header.pity": "dengan pity",
"excel.customFont": "Arial",
"excel.filePrefix": "",
"excel.fileType": "Excel file"
}

69
src/i18n/Português.json Normal file

@ -0,0 +1,69 @@
{
"symbol.colon": ": ",
"ui.button.load": "Carregar dados",
"ui.button.update": "Atualizar",
"ui.button.excel": "Exportar Planilha",
"ui.button.setting": "Configurações",
"ui.select.newAccount": "Nova conta",
"ui.hint.newAccount": "Exportar dados de outras contas",
"ui.hint.init": "Abra o Histórico de Desejos dentro do jogo antes de clicar no botão 'Carregar dados'",
"ui.hint.lastUpdate": "Última atualização",
"ui.hint.failed": "Ops falha inesperada!",
"ui.win.title": "",
"ui.data.total": "Total",
"ui.data.times": "Desejos",
"ui.data.sum": "Acumulados",
"ui.data.no5star": "Desejos sem 5 estrelas",
"ui.data.character": "Personagem",
"ui.data.weapon": "Arma",
"ui.data.star5": "5 estrelas",
"ui.data.star4": "4 estrelas",
"ui.data.star3": "3 estrelas",
"ui.data.history": "Histórico 5 estrelas",
"ui.data.average": "Média 5 estrelas",
"ui.data.chara5": "Personagem 5 estrelas",
"ui.data.chara4": "Personagem 4 estrelas",
"ui.data.weapon5": "Arma 5 estrelas",
"ui.data.weapon4": "Arma 4 estrelas",
"ui.data.weapon3": "Arma 3 estrelas",
"ui.setting.title": "Configurações",
"ui.setting.language": "Idioma",
"ui.setting.languageHint": "Caso uma tradução esteja faltando, por padrão é exibido o idioma Inglês.",
"ui.setting.logType": "Tipo de registro",
"ui.setting.auto": "Auto",
"ui.setting.cnServer": "Servidor CN",
"ui.setting.seaServer": "Servidor Global",
"ui.setting.logTypeHint": "Selecione o servidor em que serão gerados os registros ao adquirir a URL dos registros dentro do jogo.",
"ui.setting.autoUpdate": "Atualizar automáticamente",
"ui.setting.proxyMode": "Modo Proxy",
"ui.setting.proxyModeHint": "Caso não seja possível obter a URL registros do sistema, use o proxy do sistema.",
"ui.setting.closeProxy": "Desativar proxy do sistema",
"ui.setting.closeProxyHint": "Ao escolher o modo proxy, caso o programa pare de funcionar possa ser que ocorra resultados indesejados que afetem o seu sistema. Caso isso aconteça, clique neste botão para limpar as configurações de proxy do sistema.",
"ui.about.title": "Sobre",
"ui.about.license": "Este é um software de código aberto usando licença MIT.",
"log.save.failed": "Falha ao salvar dados",
"log.file.notFound": "Não foi possível encontrar registros do jogo, tenha certeza de ter aberto o Histórico de Desejos dentro do jogo",
"log.url.notFound": "Não foi possível encontrar a URL",
"log.file.readFailed": "Falha ao ler registros",
"log.fetch.retry": "Processando ${name} da página ${page} falhoutentando novamente em 5 segundos pela ${count} vez……",
"log.fetch.retryFailed": "Processando ${name} da página ${page} falhou, número de tentativas atingiu o limite",
"log.fetch.interval": "Processando ${name} da página ${page}intervalo de 1 segundo a cada 10 páginas……",
"log.fetch.current": "Processando ${name} da página ${page}",
"log.fetch.authTimeout": "Autenticação do usuário expirou, reabra novamente o Histórico de Desejos em seu jogo.",
"log.fetch.gachaType": "Carregando tipo de Desejo, aguarde.",
"log.fetch.gachaTypeOk": "Tipo de Desejo adquirido",
"log.url.lackAuth": "Chave-acesso não encontrada na URL",
"log.proxy.hint": "Usando modo proxy [${ip}:${port}] para conseguir a URL, reabra novamente o Histórico de Desejos em seu jogo.",
"log.url.notFound2": "Não foi possível encontrar a URL, tenha certeza de ter aberto o Histórico de Desejos dentro do jogo",
"log.url.incorrect": "Não foi possível conseguir os parametros de URL",
"log.autoUpdate.success": "Atualização automática bem-sucedida, por gentiliza reabra o programa",
"excel.header.time": "DATA/HORÁRIO",
"excel.header.name": "NOME",
"excel.header.type": "TIPO",
"excel.header.rank": "RARIDADE",
"excel.header.total": "TOTAL",
"excel.header.pity": "DENTRO DO PITY",
"excel.customFont": "Arial",
"excel.filePrefix": "",
"excel.fileType": "Excel file"
}

@ -0,0 +1,72 @@
{
"symbol.colon": ": ",
"ui.button.load": "Загрузить данные",
"ui.button.update": "Обновить данные",
"ui.button.excel": "Экспортировать в Excel",
"ui.button.setting": "Настройки",
"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.no5star": "Молитв после выпадения 5*",
"ui.data.character": "Персонаж",
"ui.data.weapon": "Оружие",
"ui.data.star5": "5 Звёзд",
"ui.data.star4": "4 Звезды",
"ui.data.star3": "3 Звезды",
"ui.data.history": "Полученные 5* награды",
"ui.data.average": "Среднее число получения 5*",
"ui.data.chara5": "5* Персонаж",
"ui.data.chara4": "4* Персонаж",
"ui.data.weapon5": "5* Оружие",
"ui.data.weapon4": "4* Оружие",
"ui.data.weapon3": "3* Оружие",
"ui.setting.title": "Настройки",
"ui.setting.language": "Язык",
"ui.setting.languageHint": "Если перевод отсутствует, то по умолчанию будет использован английский язык.",
"ui.setting.logType": "Тип журнала",
"ui.setting.auto": "Автоматически",
"ui.setting.cnServer": "CN сервер",
"ui.setting.seaServer": "Глобальный сервер",
"ui.setting.logTypeHint": "Выберите, какие сгенерированные сервером файлы журнала, будут использоваться в первую очередь при получении URL из игрового журнала.",
"ui.setting.autoUpdate": "Авто обновление",
"ui.setting.proxyMode": "Прокси-режим",
"ui.setting.proxyModeHint": "Если нам не удаётся получить URL из системного журнала, воспользуйтесь системным Прокси.",
"ui.setting.closeProxy": "Отключить системный Прокси",
"ui.setting.closeProxyHint": "Если при выборе Прокси-режима, программа выйдет из строя, это может привести к негативным результатам, которые могут повлиять на Вашу систему. Вы можете нажать эту кнопку, чтобы очистить настройки системного прокси.",
"ui.setting.hideNovice": "Скрыть молитвы новичка",
"ui.setting.fetchFullHistory": "Получить полные данные",
"ui.setting.fetchFullHistoryHint": "Когда эта опция включена, нажмите кнопку \"Обновить данные\" чтобы получить все данные о молитвах в течение 6 месяцев. При наличии неправильных данных в течение 6 месяцев эту функцию можно использовать для исправления.",
"ui.about.title": "О нас",
"ui.about.license": "Это программное обеспечение с открытым исходным кодом, использующее MIT-лицензию.",
"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}1 повторный тайм-аут через каждые 10 страниц...",
"log.fetch.current": "Обработка ${name} страницы ${page}",
"log.fetch.authTimeout": "Идентификация пользователя истекла. Пожалуйста, откройте историю Молитв внутри игрового клиента.",
"log.fetch.gachaType": "Получение типа Банера. Пожалуйста, подождите...",
"log.fetch.gachaTypeOk": "Тип Банера определён",
"log.url.lackAuth": "Ключ авторизации не найден в URL",
"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": "Молитв после выпадения 5*",
"excel.customFont": "Times New Roman",
"excel.filePrefix": "",
"excel.fileType": "Excel file"
}

@ -0,0 +1,77 @@
{
"symbol.colon": ": ",
"ui.button.load": "Tải dữ liệu",
"ui.button.update": "Cập nhật",
"ui.button.excel": "Xuất tập tin Excel",
"ui.button.url": "Nhập URL",
"ui.button.setting": "Cài đặt",
"ui.button.option": "Tùy chọn",
"ui.button.startProxy": "Chế độ Proxy",
"ui.select.newAccount": "Chọn tài khoản",
"ui.hint.newAccount": "Xuất dữ liệu từ tài khoản khác",
"ui.hint.init": "Vui lòng mở lịch sử cầu nguyện của bạn bên trong trò chơi trước khi nhấp vào nút 'Tải dữ liệu'",
"ui.hint.lastUpdate": "Lần cập nhật cuối",
"ui.hint.failed": "Rất tiếc, đã xảy ra lỗi",
"ui.win.title": "",
"ui.data.total": "Tổng cộng",
"ui.data.times": "lần.",
"ui.data.sum": "Đã tích luỹ",
"ui.data.no5star": "lần chưa ra 5 sao",
"ui.data.character": "Nhân vật",
"ui.data.weapon": "Vũ khí",
"ui.data.star5": "5 sao",
"ui.data.star4": "4 sao",
"ui.data.star3": "3 sao",
"ui.data.history": "Lịch sử 5 sao",
"ui.data.average": "Tỉ lệ 5 sao trung bình",
"ui.data.chara5": "Nhân vật 5 sao",
"ui.data.chara4": "Nhân vật 4 sao",
"ui.data.weapon5": "Vũ khí 5 sao",
"ui.data.weapon4": "Vũ khí 4 sao",
"ui.data.weapon3": "Vũ khí 3 sao",
"ui.setting.title": "Cài đặt",
"ui.setting.language": "Ngôn ngữ",
"ui.setting.languageHint": "Khi bản dịch bị thiếu, tiếng Anh sẽ được hiển thị theo mặc định.",
"ui.setting.logType": "Loại nhật ký",
"ui.setting.auto": "Tự động",
"ui.setting.cnServer": "Máy chủ Trung Quốc",
"ui.setting.seaServer": "Máy chủ toàn cầu",
"ui.setting.logTypeHint": "Chọn nhật ký do máy chủ tạo sẽ được sử dụng đầu tiên khi lấy URL từ nhật ký trò chơi",
"ui.setting.autoUpdate": "Tự động cập nhật",
"ui.setting.proxyMode": "Chế độ Proxy",
"ui.setting.proxyModeHint": "Khi chúng tôi không lấy được URL từ nhật ký hệ thống, hãy sử dụng proxy hệ thống",
"ui.setting.closeProxy": "Tắt proxy hệ thống",
"ui.setting.closeProxyHint": "Khi bạn chọn chế độ proxy, nếu chương trình bị treo, nó có thể gây ra các kết quả không mong muốn có thể ảnh hưởng đến hệ thống của bạn. Bạn có thể nhấp vào nút này để tắt cài đặt proxy hệ thống.",
"ui.about.title": "Về tác giả",
"ui.about.license": "Phần mềm này là mã nguồn mở sử dụng giấy phép MIT.",
"ui.urlDialog.title": "Nhập URL thủ công",
"ui.urlDialog.hint": "Chức năng này chỉ nên được sử dụng khi bạn hiểu URL nào là cần thiết ở đây",
"ui.urlDialog.placeholder": "Vui lòng nhập URL với thông tin xác thực",
"ui.common.cancel": "Hủy bỏ",
"ui.common.ok": "Đồng ý",
"log.save.failed": "Không lưu được dữ liệu cục bộ",
"log.file.notFound": "Không thể tìm thấy nhật ký trò chơi, vui lòng đảm bảo rằng bạn đã mở lịch sử cầu nguyện bên trong trò chơi",
"log.url.notFound": "Không thể tìm thấy URL",
"log.file.readFailed": "Không đọc được nhật ký",
"log.fetch.retry": "Xử lý ${name} trang ${page} không thành công, sẽ thử lại sau 5 giây với thời gian ${count}...",
"log.fetch.retryFailed": "Xử lý ${name} trang ${page} không thành công, số lần thử lại đã hết",
"log.fetch.interval": "Đang xử lý ${name} trang ${page}, gian chờ 1 giây sau mỗi 10 trang...",
"log.fetch.current": "Đang xử lý ${name} trang ${page}",
"log.fetch.authTimeout": "Xác thực người dùng đã hết hạn, vui lòng mở lại lịch sử cầu nguyện bên trong trò chơi.",
"log.fetch.gachaType": "Đang nhận loại cầu nguyện, vui lòng đợi",
"log.fetch.gachaTypeOk": "Nhận loại cầu nguyện thành công",
"log.url.lackAuth": "Không tìm thấy mã xác thực trong URL",
"log.proxy.hint": "Sử dụng chế độ proxy [${ip}:${port}] để nhận URL, vui lòng mở lại lịch sử cầu nguyện bên trong trò chơi.",
"log.url.notFound2": "Không thể tìm thấy URL, vui lòng đảm bảo rằng bạn đã mở lịch sử cầu nguyện bên trong trò chơi",
"log.url.incorrect": "Không thể nhận thông số URL",
"log.autoUpdate.success": "Tự động cập nhật thành công, vui lòng khởi động lại chương trình",
"excel.header.time": "Thời gian",
"excel.header.name": "Tên",
"excel.header.type": "Loại",
"excel.header.rank": "Sao",
"excel.header.total": "Số lần",
"excel.header.pity": "Bảo hiểm",
"excel.customFont": "Arial",
"excel.filePrefix": "",
"excel.fileType": "Tập tin Excel"
}

@ -0,0 +1,80 @@
{
"symbol.colon": ": ",
"ui.button.load": "โหลดข้อมูล",
"ui.button.update": "อัพเดตข้อมูล",
"ui.button.directUpdate": "อัพเดตโดยตรง",
"ui.button.excel": "แปลงเป็น Excel",
"ui.button.url": "กรอก URL",
"ui.button.setting": "ตั้งค่า",
"ui.button.option": "ตัวเลือก",
"ui.button.startProxy": "โหมด Proxy",
"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.no5star": "โดยที่ไม่ได้รับ 5 ดาว",
"ui.data.character": "ตัวละคร",
"ui.data.weapon": "อาวุธ",
"ui.data.star5": "5 ดาว",
"ui.data.star4": "4 ดาว",
"ui.data.star3": "3 ดาว",
"ui.data.history": "ประวัติ 5 ดาว",
"ui.data.average": "ค่าเฉลี่ย 5 ดาว",
"ui.data.chara5": "ตัวละคร 5 ดาว",
"ui.data.chara4": "ตัวละคร 4 ดาว",
"ui.data.weapon5": "อาวุธ 5 ดาว",
"ui.data.weapon4": "อาวุธ 4 ดาว",
"ui.data.weapon3": "อาวุธ 3 ดาว",
"ui.setting.title": "ตั้งค่า",
"ui.setting.language": "ภาษา",
"ui.setting.languageHint": "หากภาษาไม่แสดงผล ภาษาอังกฤษจะถูกแสดงแทนภาษานั้น",
"ui.setting.logType": "ชนิด Log",
"ui.setting.auto": "ออโต้",
"ui.setting.cnServer": "เซิฟจีน (CN)",
"ui.setting.seaServer": "เซิฟต่างประเทศ (Global)",
"ui.setting.logTypeHint": "เลือกเซิฟเวอร์ที่จะให้เซิฟเวอร์สร้าง Log ก่อนที่จะดึงข้อมูลประวัติตู้อธิษฐาน",
"ui.setting.autoUpdate": "เปิดอัพเดตอัตโนมัติ",
"ui.setting.proxyMode": "โหมดพร็อกซี่",
"ui.setting.proxyModeHint": "ในกรณีที่ไม่สามารถดึงข้อมูลจาก URL ที่ระบุได้ ให้ใช้โหมดพร็อกซี่",
"ui.setting.closeProxy": "ิปิดโหมดพร็อกซี่",
"ui.setting.closeProxyHint": "ในขณะที่เลือกโหมดพร็อกซี่ หากโปรแกรมเกิดขัดข้อง อาจทำให้เกิดผลลัพธ์ไม่ถูกต้องซึ่งอาจส่งผลกระทบระบบของคุณ คุณสามารถคลิกที่นี่เพื่อล้างการตั้งค่าพร็อกซี่",
"ui.setting.fetchFullHistory": "ดึงข้อมูลทั้งหมด",
"ui.setting.fetchFullHistoryHint": "เมื่อการตั้งค่านี้ถูกเปิดใช้งาน กดปุ่ม \"อัพเดตข้อมูล\" เพื่อดึงข้อมูลอธิษฐานในช่วง 6 เดือนที่ผ่านมา หากพบว่าข้อมูลที่ดึงมานั้นไม่ถูกต้องในช่วง 6 เดือนที่ผ่านมา คุณสามารถใช้ฟังชั่นนี้้เพื่อเรียกซ่อมแซมได้",
"ui.about.title": "เกี่ยวกับ",
"ui.about.license": "โปรแกรมนี้เป็น Open Source โดยอยู่ในอยู่ในใบอนุญาต MIT",
"ui.urlDialog.title": "กรอก URL ด้วยตัวเอง",
"ui.urlDialog.hint": "ฟังชั่นนี้ควรจะใช้ก็ต่อเมื่อคุณเข้าใจว่าแหล่ง URL นี้มาจากไหน",
"ui.urlDialog.placeholder": "โปรดกรอก URL ที่มีคีย์ข้อมูลของคุณ",
"ui.common.cancel": "ยกเลิก",
"ui.common.ok": "โอเค",
"log.save.failed": "บันทึกข้อมูลไม่สำเร็จ",
"log.file.notFound": "ไม่พบ Log ข้อมูลเกมส์, โปรดตรวจสอบให้แน่ใจว่าคุณได้เปิดประวัติตู้อธิษฐานในเกมส์ของคุณแล้ว",
"log.url.notFound": "ไม่พบ URL",
"log.file.readFailed": "ไม่สามารถอ่านไฟล์ข้อมูลได้",
"log.fetch.retry": "ดึงข้อมูลตู้อธิษฐาน ${name} ในหน้าที่ ${page} ไม่สำเร็จ กำลังดึงข้อมูลใหม่ภายใน 5 วินาทีในครั้งที่ ${count}",
"log.fetch.retryFailed": "ดึงข้อมูลตู้อธิษฐาน ${name} ในหน้าที่ ${page} ไม่สำเร็จ หมดเวลาการดึงข้อมูล",
"log.fetch.interval": "ดึงข้อมูลตู้อธิษฐาน ${name} ในหน้าที่ ${page} ทุก ๆ 1 วินาทีต่อ 10 หน้า",
"log.fetch.current": "ดึงข้อมูลตู้อธิษฐาน ${name} ในหน้าที่ ${page}",
"log.fetch.authTimeout": "ข้อมูลยืนยันตัวตนหมดอายุ โปรดทำการเปิดประวัติตู้อธิษฐานใหม่ภายในเกมส์",
"log.fetch.gachaType": "กำลังดึงข้อมูลตู้อธิษฐาน",
"log.fetch.gachaTypeOk": "ได้ข้อมูลตู้อธิษฐานสำเร็จ",
"log.url.lackAuth": "ไม่พบสิทธิ์การเข้าถึงข้อมูล",
"log.proxy.hint": "กำลังใช้โหมดพร็อกซี [${ip}:${port}] เพื่อดึงข้อมูลตู้อธิษฐาน โปรดทำการเปิดประวัติตู้อธิษฐานใหม่ภายในเกมส์",
"log.url.notFound2": "ไม่พบ URL ที่ต้องการ โปรดตรวจสอบให้แน่ใจว่าคุณได้เปิดประวัติตู้กาชาในเกมส์ของคุณแล้ว",
"log.url.incorrect": "ไม่พบพารามิเตอร์ที่ต้องการ",
"log.autoUpdate.success": "อัพเดตสำเร็จ,โปรดรีสตาร์จโปรแกรม",
"excel.header.time": "เวลา",
"excel.header.name": "ชื่อ",
"excel.header.type": "ประเภท",
"excel.header.rank": "แรงค์",
"excel.header.total": "ทั้งหมด",
"excel.header.pity": "การันตี",
"excel.customFont": "Arial",
"excel.filePrefix": "",
"excel.fileType": "ไฟล์ Excel"
}

77
src/i18n/日本語.json Normal file

@ -0,0 +1,77 @@
{
"symbol.colon": "",
"ui.button.load": "データの読み込み",
"ui.button.update": "更新データ",
"ui.button.excel": "Excelにエクスポート",
"ui.button.url": "URL入力",
"ui.button.setting": "設定",
"ui.button.option": "オプション",
"ui.button.startProxy": "プロキシモード",
"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.no5star": "連星5取得しません",
"ui.data.character": "キャラ",
"ui.data.weapon": "武器",
"ui.data.star5": "星5",
"ui.data.star4": "星4",
"ui.data.star3": "星3",
"ui.data.history": "星5跳躍記録",
"ui.data.average": "星5取得平均回数",
"ui.data.chara5": "星5キャラ",
"ui.data.chara4": "星4キャラ",
"ui.data.weapon5": "星5武器",
"ui.data.weapon4": "星4武器",
"ui.data.weapon3": "星3武器",
"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.autoUpdate": "自動更新",
"ui.setting.proxyMode": "プロキシモード",
"ui.setting.proxyModeHint": "システムプロキシの設定によるURLの取得、ログから有効なURLを取得できない場合にプロキシを起動します。",
"ui.setting.closeProxy": "システムプロキシをオフにする",
"ui.setting.closeProxyHint": "システムプロキシの設定が異常な場合に、このボタンで設定されたシステムプロキシをクリアします。",
"ui.about.title": "About",
"ui.about.license": "本ソフトウェアは、MITライセンスによるオープンソースです。",
"ui.urlDialog.title": "URLを手動で入力する",
"ui.urlDialog.hint": "この機能は、ここで必要とされるURLを理解している場合にのみ使用してください。",
"ui.urlDialog.placeholder": "authkey付きのURLを入力してください。",
"ui.common.cancel": "キャンセル",
"ui.common.ok": "確認",
"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": "authkeyの有効期限が切れているので、跳躍記録を再開してください。",
"log.fetch.gachaType": "跳躍のタイプ取得中",
"log.fetch.gachaTypeOk": "跳躍のタイプに成功を得る",
"log.url.lackAuth": "URLにauthkeyが含まれていない",
"log.proxy.hint": "URLを取得するためにプロキシモード[${ip}:${port}]を使用しています、跳躍記録を再開してください。",
"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.customFont": "メイリオ",
"excel.filePrefix": "",
"excel.fileType": "Excelファイル"
}

@ -0,0 +1,88 @@
{
"symbol.colon": "",
"ui.button.load": "加载数据",
"ui.button.update": "更新数据",
"ui.button.directUpdate": "直接更新",
"ui.button.excel": "导出Excel",
"ui.button.url": "输入URL",
"ui.button.setting": "设置",
"ui.button.option": "选项",
"ui.button.startProxy": "代理模式",
"ui.button.solution": "解决办法",
"ui.button.cacheFolder": "打开网页缓存文件夹",
"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.no5star": "抽未出5星",
"ui.data.character": "角色",
"ui.data.weapon": "武器",
"ui.data.star5": "5星",
"ui.data.star4": "4星",
"ui.data.star3": "3星",
"ui.data.history": "5星历史记录",
"ui.data.average": "5星平均出货次数为",
"ui.data.chara5": "5星角色",
"ui.data.chara4": "4星角色",
"ui.data.weapon5": "5星武器",
"ui.data.weapon4": "4星武器",
"ui.data.weapon3": "3星武器",
"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.autoUpdate": "自动更新",
"ui.setting.hideNovice": "隐藏始发跃迁",
"ui.setting.proxyMode": "代理模式",
"ui.setting.proxyModeHint": "通过设置系统代理来获取URL无法从日志中获取到有效的URL时才会启动代理服务器。",
"ui.setting.fetchFullHistory": "获取完整数据",
"ui.setting.fetchFullHistoryHint": "开启时点击“更新数据”按钮会完整获取6个月内所有的抽卡记录当记录里有6个月范围以内的错误数据时可以通过这个功能修复。",
"ui.setting.closeProxy": "关闭系统代理",
"ui.setting.closeProxyHint": "如果使用过代理模式时工具非正常关闭,可能导致系统代理设置没能清除,可以通过这个按钮来清除设置过的系统代理。",
"ui.about.title": "关于",
"ui.about.license": "本工具为开源软件,源代码使用 MIT 协议授权",
"ui.urlDialog.title": "手动输入URL",
"ui.urlDialog.hint": "这个功能应当只在你理解这里需要什么URL时使用",
"ui.urlDialog.placeholder": "请输入带有身份认证信息的URL",
"ui.common.cancel": "取消",
"ui.common.ok": "确定",
"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文件",
"ui.extra.cacheClean": "1. 确认是否已经打开游戏内的抽卡历史记录,如果仍然出现“身份认证已过期”的错误,再尝试下面的步骤\n2. 关闭原神的游戏窗口\n3. 点击上方的“打开缓存文件夹”按钮打开Cache文件夹\n4. 删除Cache_Data文件夹\n5. 启动原神游戏,打开游戏内抽卡历史记录页面\n6. 关闭这个对话框,再点击“更新数据”按钮",
"ui.extra.findCacheFolder": "如果点“打开缓存文件夹”按钮没有反应,可以手动找到游戏的网页缓存文件夹,目录为“你的游戏安装路径/Star Rail/Game/StarRail_Data/webCaches/Cache/”"
}

@ -0,0 +1,88 @@
{
"symbol.colon": "",
"ui.button.load": "載入資料",
"ui.button.update": "更新資料",
"ui.button.directUpdate": "直接更新",
"ui.button.excel": "匯出 Excel",
"ui.button.url": "輸入 URL",
"ui.button.setting": "設定",
"ui.button.option": "選項",
"ui.button.startProxy": "Proxy 模式",
"ui.button.solution": "解決方案",
"ui.button.cacheFolder": "開啟快取資料夾",
"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.no5star": "抽未出5星",
"ui.data.character": "角色",
"ui.data.weapon": "武器",
"ui.data.star5": "5星",
"ui.data.star4": "4星",
"ui.data.star3": "3星",
"ui.data.history": "5星歷史紀錄",
"ui.data.average": "5星平均出貨次數為",
"ui.data.chara5": "5星角色",
"ui.data.chara4": "4星角色",
"ui.data.weapon5": "5星武器",
"ui.data.weapon4": "4星武器",
"ui.data.weapon3": "3星武器",
"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.autoUpdate": "自動更新",
"ui.setting.hideNovice": "",
"ui.setting.proxyMode": "Proxy 模式",
"ui.setting.proxyModeHint": "透過設定系統 Proxy 以取得 URL將會在從系統記錄中取得 URL 失敗時啟動。",
"ui.setting.fetchFullHistory": "取得完整資料",
"ui.setting.fetchFullHistoryHint": "開啟時按下「更新資料」按鈕將會完整取得 6 個月內所有的抽卡紀錄,紀錄內有 6 個月範圍以內的錯誤資料時可以透過此功能修復。",
"ui.setting.closeProxy": "停用系統 Proxy",
"ui.setting.closeProxyHint": "如果使用 Proxy 模式時程式當機,可能會影響系統網路,可以透過這個按鈕以清除系統 Proxy 設定。",
"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": "確定",
"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 中找不到驗證金鑰",
"log.proxy.hint": "正在使用 Proxy 模式 [${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 檔案",
"ui.extra.cacheClean": "1. 確認是否已經開啟遊戲內的躍遷歷史紀錄,如果仍然出現「身分驗證已過期」的錯誤,再嘗試下面的步驟\n2. 關閉原神的遊戲視窗\n3. 按一下上方的「開啟快取資料夾」按鈕開啟「Cache」資料夾\n4. 刪除「Cache_Data」資料夾\n5. 啟動原神遊戲,開啟遊戲內躍遷歷史紀錄頁面\n6. 關閉這個對話方塊,再按下「更新資料」按鈕",
"ui.extra.findCacheFolder": "如果按下「開啟快取資料夾」按鈕沒有回應,可以手動找到遊戲的網頁快取資料夾,目錄為「您的遊戲安裝路徑/Star Rail/Game/StarRail_Data/webCaches/Cache/」"
}

77
src/i18n/한국어.json Normal file

@ -0,0 +1,77 @@
{
"symbol.colon": ": ",
"ui.button.load": "데이터 로드",
"ui.button.update": "업데이트",
"ui.button.excel": "엑셀로 내보내기",
"ui.button.url": "URL 입력",
"ui.button.setting": "설정",
"ui.button.option": "옵션",
"ui.button.startProxy": "프록시 모드",
"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.no5star": "기원(천장: 90)",
"ui.data.character": "캐릭터",
"ui.data.weapon": "무기",
"ui.data.star5": "5★",
"ui.data.star4": "4★",
"ui.data.star3": "3★",
"ui.data.history": "5★ 기록",
"ui.data.average": "5★ 뽑기 평균",
"ui.data.chara5": "5★ 캐릭터",
"ui.data.chara4": "4★ 캐릭터",
"ui.data.weapon5": "5★ 무기",
"ui.data.weapon4": "4★ 무기",
"ui.data.weapon3": "3★ 무기",
"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.autoUpdate": "자동 업데이트",
"ui.setting.proxyMode": "프록시 모드",
"ui.setting.proxyModeHint": "시스템 로그에서 URL을 가져오지 못한 경우 시스템 프록시를 사용합니다.",
"ui.setting.closeProxy": "시스템 프록시 사용 안 함",
"ui.setting.closeProxyHint": "프록시 모드를 선택할 때 프로그램이 충돌하면 시스템에 영향을 줄 수 있는 원하지 않는 결과가 발생할 수 있습니다. 이 버튼을 눌러 시스템 프록시 설정을 지울 수 있습니다.",
"ui.about.title": "About",
"ui.about.license": "This software is opensource using MIT license.",
"ui.urlDialog.title": "수동으로 URL 입력",
"ui.urlDialog.hint": "이 기능은 여기서 필요한 URL을 알고 있는 경우에만 사용해야 합니다.",
"ui.urlDialog.placeholder": "인증 정보가 포함된 URL을 입력하세요.",
"ui.common.cancel": "Cancel",
"ui.common.ok": "OK",
"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에서 인증 키를 찾을 수 없습니다.",
"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": "기원(천장: 90)",
"excel.customFont": "Arial",
"excel.filePrefix": "",
"excel.fileType": "Excel file"
}

89
src/main/UIGFJson.js Normal file

@ -0,0 +1,89 @@
const { app, ipcMain, dialog } = require('electron')
const fs = require('fs-extra')
const path = require('path')
const getData = require('./getData').getData
const { version } = require('../../package.json')
const 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 fakeIdFn = () => {
let id = 1000000000000000000n
return () => {
id = id + 1n
return id.toString()
}
}
const shouldBeString = (value) => {
if (typeof value !== 'string') {
return ''
}
return value
}
const start = async () => {
const { dataMap, current } = await getData()
const data = dataMap.get(current)
if (!data.result.size) {
throw new Error('数据为空')
}
const fakeId = fakeIdFn()
const result = {
info: {
uid: data.uid,
lang: data.lang,
export_time: formatDate(new Date()),
export_timestamp: Date.now(),
export_app: 'genshin-wish-export',
export_app_version: `v${version}`,
uigf_version: 'v2.2'
},
list: []
}
const listTemp = []
for (let [type, arr] of data.result) {
arr.forEach(item => {
listTemp.push({
gacha_type: shouldBeString(item[4]) || type,
time: item[0],
timestamp: new Date(item[0]).getTime(),
name: item[1],
item_type: item[2],
rank_type: `${item[3]}`,
id: shouldBeString(item[5]) || '',
uigf_gacha_type: type
})
})
}
listTemp.sort((a, b) => a.timestamp - b.timestamp)
listTemp.forEach(item => {
delete item.timestamp
result.list.push({
...item,
id: item.id || fakeId()
})
})
const filePath = dialog.showSaveDialogSync({
defaultPath: path.join(app.getPath('downloads'), `UIGF_${data.uid}_${getTimeString()}`),
filters: [
{ name: 'JSON文件', extensions: ['json'] }
]
})
if (filePath) {
await fs.ensureFile(filePath)
await fs.writeFile(filePath, JSON.stringify(result))
}
}
ipcMain.handle('EXPORT_UIGF_JSON', async () => {
await start()
})

77
src/main/config.js Normal file

@ -0,0 +1,77 @@
const { readJSON, saveJSON, decipherAes, cipherAes, detectLocale } = require('./utils')
const config = {
urls: [],
logType: 0,
lang: detectLocale(),
current: 0,
proxyPort: 8325,
proxyMode: false,
autoUpdate: true,
fetchFullHistory: false,
hideNovice: false
}
const getLocalConfig = async () => {
const localConfig = await readJSON('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

167
src/main/excel.js Normal file

@ -0,0 +1,167 @@
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')
const cloneDeep = require('lodash-es/cloneDeep').default
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, {views: [{state: 'frozen', ySplit: 1}]})
let width = [24, 14, 8, 8, 8, 8, 8]
if (!data.lang.includes('zh-')) {
width = [24, 32, 16, 12, 12, 12, 8]
}
const excelKeys = ['time', 'name', 'type', 'rank', 'total', 'pity', 'remark']
sheet.columns = excelKeys.map((key, index) => {
return {
header: header[key],
key,
width: width[index]
}
})
// get gacha logs
const logs = value
let total = 0
let pity = 0
const temp = []
for (let log of logs) {
const arr = []
total += 1
pity += 1
arr.push(log.time)
arr.push(log.name)
arr.push(log.item_type)
arr.push(log.rank_type)
arr.push(total)
arr.push(pity)
temp.push(arr)
if (log.rank_type === 5) {
pity = 0
}
// if (key === '301') {
// if (log.gacha_type === '400') {
// log.push(wish2)
// }
// }
}
sheet.addRows(temp)
// set xlsx hearer style
;(["A", "B", "C", "D","E","F", "G"]).forEach((v) => {
sheet.getCell(`${v}1`).border = {
top: {style:'thin', color: {argb:'ffc4c2bf'}},
left: {style:'thin', color: {argb:'ffc4c2bf'}},
bottom: {style:'thin', color: {argb:'ffc4c2bf'}},
right: {style:'thin', color: {argb:'ffc4c2bf'}}
}
sheet.getCell(`${v}1`).fill = {
type: 'pattern',
pattern:'solid',
fgColor:{argb:'ffdbd7d3'},
}
sheet.getCell(`${v}1`).font ={
name: customFont,
color: { argb: "ff757575" },
bold : true
}
})
// set xlsx cell style
logs.forEach((v, i) => {
;(["A", "B", "C", "D","E","F", "G"]).forEach((c) => {
sheet.getCell(`${c}${i + 2}`).border = {
top: {style:'thin', color: {argb:'ffc4c2bf'}},
left: {style:'thin', color: {argb:'ffc4c2bf'}},
bottom: {style:'thin', color: {argb:'ffc4c2bf'}},
right: {style:'thin', color: {argb:'ffc4c2bf'}}
}
sheet.getCell(`${c}${i + 2}`).fill = {
type: 'pattern',
pattern:'solid',
fgColor:{argb:'ffebebeb'},
}
// rare rank background color
const rankColor = {
3: "ff8e8e8e",
4: "ffa256e1",
5: "ffbd6932",
}
sheet.getCell(`${c}${i + 2}`).font = {
name: customFont,
color: { argb: rankColor[v.rank_type] },
bold : v.rank_type != "3"
}
})
})
}
addRawSheet(workbook, data)
const buffer = await workbook.xlsx.writeBuffer()
const filePath = dialog.showSaveDialogSync({
defaultPath: path.join(app.getPath('downloads'), `${filePrefix}_${getTimeString()}`),
filters: [
{ name: fileType, extensions: ['xlsx'] }
]
})
if (filePath) {
await fs.ensureFile(filePath)
await fs.writeFile(filePath, buffer)
}
}
ipcMain.handle('SAVE_EXCEL', async () => {
await start()
})

495
src/main/getData.js Normal file

@ -0,0 +1,495 @@
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, 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 dataMap = new Map()
const order = ['11', '12', '1', '2']
let apiDomain = 'https://api-takumi.mihoyo.com'
const saveData = async (data, url) => {
const obj = Object.assign({}, data)
obj.result = [...obj.result]
obj.typeMap = [...obj.typeMap]
config.urls.set(data.uid, url)
await config.save()
await saveJSON(`gacha-list-${data.uid}.json`, obj)
}
const defaultTypeMap = new Map([
['11', '角色活动跃迁'],
['12', '光锥活动跃迁'],
['1', '群星跃迁'],
['2', '始发跃迁']
])
let localDataReaded = false
const readdir = util.promisify(fs.readdir)
const readData = async () => {
if (localDataReaded) return
localDataReaded = true
await fs.ensureDir(userDataPath)
const files = await readdir(userDataPath)
for (let name of files) {
if (/^gacha-list-\d+\.json$/.test(name)) {
try {
const data = await readJSON(name)
data.typeMap = new Map(data.typeMap) || defaultTypeMap
data.result = new Map(data.result)
if (data.uid) {
dataMap.set(data.uid, data)
}
} catch (e) {
sendMsg(e, 'ERROR')
}
}
}
if ((!config.current && dataMap.size) || (config.current && dataMap.size && !dataMap.has(config.current))) {
await changeCurrent(dataMap.keys().next().value)
}
}
const changeCurrent = async (uid) => {
config.current = uid
await config.save()
}
const detectGameLocale = async (userPath) => {
let list = []
const lang = app.getLocale()
const arr = ['/miHoYo/崩坏:星穹铁道/', '/Cognosphere/Star Rail/']
arr.forEach(str => {
try {
const pathname = path.join(userPath, '/AppData/LocalLow/', str, 'Player-prev.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 gamePathMch = logText.match(/\w:\/.+(Star\sRail\/Game\/StarRail_Data)/)
if (gamePathMch) {
const cacheText = await fs.readFile(path.join(gamePathMch[0], '/webCaches/Cache/Cache_Data/data_2'), 'utf8')
const urlMch = cacheText.match(/https.+?&auth_appid=webview_gacha&.+?authkey=.+?&game_biz=hkrpg_.+/g)
if (urlMch) {
cacheFolder = path.join(gamePathMch[0], '/webCaches/Cache/')
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}&gacha_type=${key}&page=${page}&size=${20}${endId ? '&end_id=' + endId : ''}`)
return res.data.list
} 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 = []
let uid = ''
let region = ''
let region_time_zone = ''
let endId = '0'
const url = `${apiDomain}/common/gacha_record/api/getGachaLog?${queryString}`
do {
if (page % 10 === 0) {
sendMsg(i18n.parse(text.fetch.interval, { name, page }))
await sleep(1)
}
sendMsg(i18n.parse(text.fetch.current, { name, page }))
res = await getGachaLog({ key, page, name, url, endId, retryCount: 5 })
await sleep(0.3)
if (!uid && res.length) {
uid = res[0].uid
}
if (!region) {
region = res.region
}
if (!region_time_zone) {
region_time_zone = res.region_time_zone
}
list.push(...res)
page += 1
if (res.length) {
endId = res[res.length - 1].id
}
if (!config.fetchFullHistory && res.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
res.forEach(item => {
if (item.id === localLatestId) {
shouldBreak = true
}
})
if (shouldBreak) {
break
}
}
}
}
}
} while (res.length > 0)
return { list, uid, region, region_time_zone }
}
const checkResStatus = (res) => {
const text = i18n.log
if (res.retcode !== 0) {
let message = res.message
if (res.message === 'authkey timeout') {
message = text.fetch.authTimeout
sendMsg(true, 'AUTHKEY_TIMEOUT')
}
sendMsg(message)
throw new Error(message)
}
sendMsg(false, 'AUTHKEY_TIMEOUT')
return res
}
const tryGetUid = async (queryString) => {
const url = `${apiDomain}/common/gacha_record/api/getGachaLog?${queryString}`
try {
for (let [key] of defaultTypeMap) {
const res = await request(`${url}&gacha_type=${key}&page=1&size=6`)
checkResStatus(res)
if (res.data.list && res.data.list.length) {
return res.data.list[0].uid
}
}
} catch (e) {}
return config.current
}
const gachaTypeMap = new Map(JSON.parse('[["de-de",[{"key":"11","name":"Figuren-Aktionswarp"},{"key":"12","name":"Lichtkegel-Aktionswarp"},{"key":"1","name":"Stellarwarp"},{"key":"2","name":"Startwarp"}]],["ru-ru",[{"key":"11","name":"Прыжок события: Персонаж"},{"key":"12","name":"Прыжок события: Световой конус"},{"key":"1","name":"Звёздный Прыжок"},{"key":"2","name":"Отправной Прыжок"}]],["th-th",[{"key":"11","name":"กิจกรรมวาร์ปตัวละคร"},{"key":"12","name":"กิจกรรมวาร์ป Light Cone"},{"key":"1","name":"วาร์ปสู่ดวงดาว"},{"key":"2","name":"ก้าวแรกแห่งการวาร์ป"}]],["zh-cn",[{"key":"11","name":"角色活动跃迁"},{"key":"12","name":"光锥活动跃迁"},{"key":"1","name":"群星跃迁"},{"key":"2","name":"始发跃迁"}]],["zh-tw",[{"key":"11","name":"角色活動躍遷"},{"key":"12","name":"光錐活動躍遷"},{"key":"1","name":"群星躍遷"},{"key":"2","name":"始發躍遷"}]],["en-us",[{"key":"11","name":"Character Event Warp"},{"key":"12","name":"Light Cone Event Warp"},{"key":"1","name":"Stellar Warp"},{"key":"2","name":"Departure Warp"}]],["es-es",[{"key":"11","name":"Salto de evento de personaje"},{"key":"12","name":"Salto de evento de cono de luz"},{"key":"1","name":"Salto estelar"},{"key":"2","name":"Salto de partida"}]],["fr-fr",[{"key":"11","name":"Saut hyperespace événement de personnage"},{"key":"12","name":"Saut hyperespace événement de cônes de lumière"},{"key":"1","name":"Saut stellaire"},{"key":"2","name":"Saut hyperespace de départ"}]],["id-id",[{"key":"11","name":"Event Warp Karakter"},{"key":"12","name":"Event Warp Light Cone"},{"key":"1","name":"Warp Bintang-Bintang"},{"key":"2","name":"Warp Keberangkatan"}]],["ja-jp",[{"key":"11","name":"イベント跳躍・キャラクター"},{"key":"12","name":"イベント跳躍・光円錐"},{"key":"1","name":"群星跳躍"},{"key":"2","name":"始発跳躍"}]],["ko-kr",[{"key":"11","name":"캐릭터 이벤트 워프"},{"key":"12","name":"광추 이벤트 워프"},{"key":"1","name":"뭇별의 워프"},{"key":"2","name":"초행길 워프"}]],["pt-pt",[{"key":"11","name":"Salto Hiperespacial de Evento de Personagem"},{"key":"12","name":"Salto Hiperespacial de Evento de Cone de Luz"},{"key":"1","name":"Salto Hiperespacial Estelar"},{"key":"2","name":"Salto Hiperespacial de Novatos"}]],["vi-vn",[{"key":"11","name":"Bước Nhảy Sự Kiện Nhân Vật"},{"key":"12","name":"Bước Nhảy Sự Kiện Nón Ánh Sáng"},{"key":"1","name":"Bước Nhảy Chòm Sao"},{"key":"2","name":"Bước Nhảy Đầu Tiên"}]]]'))
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('hkrpg-api-os') || host.includes("api-os-takumi")) {
apiDomain = 'https://api-os-takumi.mihoyo.com'
} else {
apiDomain = 'https://api-takumi.mihoyo.com'
}
const authkey = searchParams.get('authkey')
if (!authkey) {
sendMsg(text.url.lackAuth)
return false
}
searchParams.delete('page')
searchParams.delete('size')
searchParams.delete('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&gacha_type=1&end_id=0`
try {
const res = await request(gachaTypeUrl)
if (res.retcode !== 0) {
return false
}
return true
} 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()
} else if (url) {
const result = await tryRequest(url)
if (!result && 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)
}
const searchParams = getQuerystring(url)
if (!searchParams) {
const message = text.url.incorrect
sendMsg(message)
throw new Error(message)
}
let queryString = searchParams.toString()
const vUid = await tryGetUid(queryString)
const localLang = dataMap.has(vUid) ? dataMap.get(vUid).lang : ''
if (localLang) {
searchParams.set('lang', localLang)
}
queryString = searchParams.toString()
const gachaType = await getGachaType(searchParams.get('lang'))
const result = new Map()
const typeMap = new Map()
const lang = searchParams.get('lang')
let originUid = ''
let originRegion = ''
let originTimeZone = ''
for (const type of gachaType) {
const { list, uid, region, region_time_zone } = await getGachaLogs(type, queryString)
const logs = list.map((item) => {
const { id, item_id, item_type, name, rank_type, time, gacha_id, gacha_type } = item
return { id, item_id, item_type, name, rank_type, time, gacha_id, gacha_type }
})
logs.reverse()
typeMap.set(type.key, type.name)
result.set(type.key, logs)
if (!originUid) {
originUid = uid
}
if (!originRegion) {
originRegion = region
}
if (!originTimeZone) {
originTimeZone = region_time_zone
}
}
const data = { result, time: Date.now(), typeMap, uid: originUid, lang, region: originRegion, region_time_zone: originTimeZone }
const localData = dataMap.get(originUid)
const mergedResult = mergeData(localData, data)
data.result = mergedResult
dataMap.set(originUid, data)
await changeCurrent(originUid)
await saveData(data, url)
}
let proxyStarted = false
const fetchDataByProxy = async () => {
if (proxyStarted) return
proxyStarted = true
const url = await useProxy()
await fetchData(url)
}
ipcMain.handle('FETCH_DATA', async (event, param) => {
try {
if (param === 'proxy') {
await fetchDataByProxy()
} else {
await fetchData(param)
}
return {
dataMap,
current: config.current
}
} catch (e) {
sendMsg(e, 'ERROR')
console.error(e)
}
return false
})
ipcMain.handle('READ_DATA', async () => {
await readData()
return {
dataMap,
current: config.current
}
})
ipcMain.handle('CHANGE_UID', (event, uid) => {
config.current = 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
}
}

96
src/main/i18n.js Normal file

@ -0,0 +1,96 @@
const raw = {
'zh-cn': require('../i18n/简体中文.json'),
'zh-tw': require('../i18n/繁體中文.json'),
'de-de': require('../i18n/Deutsch.json'),
'en-us': require('../i18n/English.json'),
'es-es': require('../i18n/Español.json'),
'fr-fr': require('../i18n/Français.json'),
'id-id': require('../i18n/Indonesia.json'),
'ja-jp': require('../i18n/日本語.json'),
'ko-kr': require('../i18n/한국어.json'),
'pt-pt': require('../i18n/Português.json'),
'ru-ru': require('../i18n/Pусский.json'),
'th-th': require('../i18n/ภาษาไทย.json'),
'vi-vn': require('../i18n/Tiếng Việt.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 i18nMap = new Map()
const prepareData = () => {
for (let key in raw) {
let temp = {}
if (key === 'zh-tw') {
Object.assign(temp, raw['zh-cn'], raw[key])
} else {
Object.assign(temp, raw['zh-cn'], 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'
]
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

69
src/main/main.js Normal file

@ -0,0 +1,69 @@
const { app, BrowserWindow, ipcMain } = require('electron')
const { initWindow } = require('./utils')
const { disableProxy, proxyStatus } = require('./module/system-proxy')
require('./getData')
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
src/main/module/exceljs.min.js vendored Normal file

File diff suppressed because one or more lines are too long

@ -0,0 +1,185 @@
// Copyright (c) 2014 Max Ogden and other contributors
// All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
// * Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// https://github.com/maxogden/extract-zip
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const { createWriteStream, promises: fs } = require('original-fs')
const getStream = require('get-stream')
const path = require('path')
const { promisify } = require('util')
const stream = require('stream')
const yauzl = require('yauzl')
const openZip = promisify(yauzl.open)
const pipeline = promisify(stream.pipeline)
class Extractor {
constructor (zipPath, opts) {
this.zipPath = zipPath
this.opts = opts
}
async extract () {
this.zipfile = await openZip(this.zipPath, { lazyEntries: true })
this.canceled = false
return new Promise((resolve, reject) => {
this.zipfile.on('error', err => {
this.canceled = true
reject(err)
})
this.zipfile.readEntry()
this.zipfile.on('close', () => {
if (!this.canceled) {
resolve()
}
})
this.zipfile.on('entry', async entry => {
/* istanbul ignore if */
if (this.canceled) {
return
}
if (entry.fileName.startsWith('__MACOSX/')) {
this.zipfile.readEntry()
return
}
const destDir = path.dirname(path.join(this.opts.dir, entry.fileName))
try {
await fs.mkdir(destDir, { recursive: true })
const canonicalDestDir = await fs.realpath(destDir)
const relativeDestDir = path.relative(this.opts.dir, canonicalDestDir)
if (relativeDestDir.split(path.sep).includes('..')) {
throw new Error(`Out of bound path "${canonicalDestDir}" found while processing file ${entry.fileName}`)
}
await this.extractEntry(entry)
this.zipfile.readEntry()
} catch (err) {
this.canceled = true
this.zipfile.close()
reject(err)
}
})
})
}
async extractEntry (entry) {
/* istanbul ignore if */
if (this.canceled) {
return
}
if (this.opts.onEntry) {
this.opts.onEntry(entry, this.zipfile)
}
const dest = path.join(this.opts.dir, entry.fileName)
// convert external file attr int into a fs stat mode int
const mode = (entry.externalFileAttributes >> 16) & 0xFFFF
// check if it's a symlink or dir (using stat mode constants)
const IFMT = 61440
const IFDIR = 16384
const IFLNK = 40960
const symlink = (mode & IFMT) === IFLNK
let isDir = (mode & IFMT) === IFDIR
// Failsafe, borrowed from jsZip
if (!isDir && entry.fileName.endsWith('/')) {
isDir = true
}
// check for windows weird way of specifying a directory
// https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566
const madeBy = entry.versionMadeBy >> 8
if (!isDir) isDir = (madeBy === 0 && entry.externalFileAttributes === 16)
const procMode = this.getExtractedMode(mode, isDir) & 0o777
// always ensure folders are created
const destDir = isDir ? dest : path.dirname(dest)
const mkdirOptions = { recursive: true }
if (isDir) {
mkdirOptions.mode = procMode
}
await fs.mkdir(destDir, mkdirOptions)
if (isDir) return
const readStream = await promisify(this.zipfile.openReadStream.bind(this.zipfile))(entry)
if (symlink) {
const link = await getStream(readStream)
await fs.symlink(link, dest)
} else {
await pipeline(readStream, createWriteStream(dest, { mode: procMode }))
}
}
getExtractedMode (entryMode, isDir) {
let mode = entryMode
// Set defaults, if necessary
if (mode === 0) {
if (isDir) {
if (this.opts.defaultDirMode) {
mode = parseInt(this.opts.defaultDirMode, 10)
}
if (!mode) {
mode = 0o755
}
} else {
if (this.opts.defaultFileMode) {
mode = parseInt(this.opts.defaultFileMode, 10)
}
if (!mode) {
mode = 0o644
}
}
}
return mode
}
}
module.exports = async function (zipPath, opts) {
if (!path.isAbsolute(opts.dir)) {
throw new Error('Target directory is expected to be absolute')
}
await fs.mkdir(opts.dir, { recursive: true })
opts.dir = await fs.realpath(opts.dir)
return new Extractor(zipPath, opts).extract()
}

File diff suppressed because one or more lines are too long

@ -0,0 +1,39 @@
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
src/main/update/index.js Normal file

@ -0,0 +1,65 @@
const { app } = require('electron')
const fetch = require('electron-fetch').default
const semver = require('semver')
const util = require('util')
const path = require('path')
const fs = require('fs-extra')
const extract = require('../module/extract-zip')
const { version } = require('../../../package.json')
const { hash, sendMsg } = require('../utils')
const config = require('../config')
const i18n = require('../i18n')
const streamPipeline = util.promisify(require('stream').pipeline)
async function download(url, filePath) {
const response = await fetch(url)
if (!response.ok) throw new Error(`unexpected response ${response.statusText}`)
await streamPipeline(response.body, fs.createWriteStream(filePath))
}
const updateInfo = {
status: 'init'
}
const isDev = !app.isPackaged
const appPath = isDev ? path.resolve(__dirname, '../../', 'update-dev/app'): app.getAppPath()
const updatePath = isDev ? path.resolve(__dirname, '../../', 'update-dev/download') : path.resolve(appPath, '..', '..', 'update')
const update = async () => {
if (isDev) return
try {
const url = 'https://star-rail-warp-export.css.moe/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

207
src/main/utils.js Normal file

@ -0,0 +1,207 @@
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 Registry = require('winreg')
const isDev = !app.isPackaged
const userPath = app.getPath('userData')
const appRoot = isDev ? path.resolve(__dirname, '..', '..') : userPath
const userDataPath = path.resolve(appRoot, '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,
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.outputFileSync(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', '繁體中文'],
['de-de', 'Deutsch'],
['en-us', 'English'],
['es-es', 'Español'],
['fr-fr', 'Français'],
['id-id', 'Indonesia'],
['ja-jp', '日本語'],
['ko-kr', '한국어'],
['pt-pt', 'Português'],
['ru-ru', 'Pусский'],
['th-th', 'ภาษาไทย'],
['vi-vn', 'Tiếng Việt']
])
const localeMap = new Map([
['zh-cn', ['zh', 'zh-CN']],
['zh-tw', ['zh-TW']],
['de-de', ['de-AT', 'de-CH', 'de-DE', 'de']],
['en-us', ['en-AU', 'en-CA', 'en-GB', 'en-NZ', 'en-US', 'en-ZA', 'en']],
['es-es', ['es', 'es-419']],
['fr-fr', ['fr-CA', 'fr-CH', 'fr-FR', 'fr']],
['id-id', ['id']],
['ja-jp', ['ja']],
['ko-kr', ['ko']],
['pt-pt', ['pt-BR', 'pt-PT', 'pt']],
['ru-ru', ['ru']],
['th-th', ['th']],
['vi-vn', ['vi']]
])
const detectLocale = (value) => {
const locale = value || app.getLocale()
let result = 'zh-cn'
for (let [key, list] of localeMap) {
if (list.includes(locale)) {
result = key
break
}
}
return result
}
const saveJSON = async (name, data) => {
try {
await fs.outputJSON(path.join(userDataPath, name), data, {
spaces: 2
})
} catch (e) {
sendMsg(e, 'ERROR')
await sleep(3)
}
}
const readJSON = async (name) => {
let data = null
try {
data = await fs.readJSON(path.join(userDataPath, name))
} catch (e) {}
return data
}
const hash = (data, type = 'sha256') => {
const hmac = crypto.createHmac(type, 'hk4e')
hmac.update(data)
return hmac.digest('hex')
}
const scryptKey = crypto.scryptSync(userPath, 'hk4e', 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'
}
module.exports = {
sleep, request, hash, cipherAes, decipherAes, saveLog,
sendMsg, readJSON, saveJSON, initWindow, getWin, localIp, userPath, detectLocale, langMap,
appRoot, userDataPath
}

@ -0,0 +1,40 @@
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 }

@ -0,0 +1,72 @@
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",
}
])
})

268
src/renderer/App.vue Normal file

@ -0,0 +1,268 @@
<template>
<div v-if="ui" class="relative">
<div class="flex justify-between">
<div>
<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-button icon="folder-opened" @click="saveExcel" class="focus:outline-none" :disabled="!gachaData" type="success" plain>{{ui.button.excel}}</el-button>
<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' && state.dataMap && (state.dataMap.size > 1 || (state.dataMap.size === 1 && state.current === 0))" class="w-44" @change="changeCurrent" v-model="uidSelectText">
<el-option
v-for="item of state.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 :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" @changeLang="getI18nData()" @close="showSetting(false)"></Setting>
<el-dialog :title="ui.urlDialog.title" v-model="state.showUrlDlg" width="90%" custom-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%" custom-class="max-w-md">
<el-button plain icon="folder" type="primary" @click="openCacheFolder">{{ui.button.cacheFolder}}</el-button>
<p class="my-4 leading-2 text-gray-600 text-sm whitespace-pre-line">{{ui.extra.cacheClean}}</p>
<p class="my-2 text-gray-400 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'
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 ui = computed(() => {
if (state.i18n) {
return state.i18n.ui
}
})
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 * 60) {
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 = state.dataMap.get(state.current)
if (data) {
return gachaDetail(data.result)
}
})
const typeMap = computed(() => {
const data = state.dataMap.get(state.current)
return data.typeMap
})
const fetchData = async (url) => {
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 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')
}
}
const setTitle = () => {
document.title = `${state.i18n.ui.win.title} - v${version}`
}
const updateConfig = async () => {
state.config = await ipcRenderer.invoke('GET_CONFIG')
}
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>

@ -0,0 +1,91 @@
<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.no5star}}</span>
</p>
<p class="text-gray-600 text-xs mb-1">
<span :title="`${text.character}${colon}${detail.count5c}\n${text.weapon}${colon}${detail.count5w}`" class="mr-3 whitespace-pre cursor-help text-yellow-500">
<span class="min-w-10 inline-block">{{text.star5}}{{colon}}{{detail.count5}}</span>
[{{percent(detail.count5, detail.total)}}]
</span>
<br><span :title="`${text.character}${colon}${detail.count4c}\n${text.weapon}${colon}${detail.count4w}`" class="mr-3 whitespace-pre cursor-help text-purple-600">
<span class="min-w-10 inline-block">{{text.star4}}{{colon}}{{detail.count4}}</span>
[{{percent(detail.count4, detail.total)}}]
</span>
<br><span class="text-blue-500 whitespace-pre">
<span class="min-w-10 inline-block">{{text.star3}}{{colon}}{{detail.count3}}</span>
[{{percent(detail.count3, 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>

@ -0,0 +1,125 @@
<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", "#ee6666", "#5470c6", "#91cc75", "#73c0de"];
const parseData = (detail, type) => {
const text = props.i18n.ui.data;
const keys = [
[text.chara5, "count5c"],
[text.weapon5, "count5w"],
[text.chara4, "count4c"],
[text.weapon4, "count4w"],
[text.weapon3, "count3w"],
];
const result = [];
const color = [];
const selected = {
[text.weapon3]: 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("5")) === -1
) {
selected[text.weapon3] = 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>

@ -0,0 +1,129 @@
<template>
<div class="bg-white pt-2 pb-4 px-6 w-full h-full absolute inset-0">
<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 relative -right-4 -top-2 shadow-md focus:shadow-none focus:outline-none"></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="text.autoUpdate">
<el-switch
@change="saveSetting"
v-model="settingForm.autoUpdate">
</el-switch>
</el-form-item>
<el-form-item :label="text.hideNovice">
<el-switch
@change="saveSetting"
v-model="settingForm.hideNovice">
</el-switch>
</el-form-item>
<el-form-item :label="text.fetchFullHistory">
<el-switch
@change="saveSetting"
v-model="settingForm.fetchFullHistory">
</el-switch>
<p class="text-gray-400 text-xs m-1.5">{{text.fetchFullHistoryHint}}</p>
</el-form-item>
<el-form-item :label="text.proxyMode">
<el-switch
@change="saveSetting"
v-model="settingForm.proxyMode">
</el-switch>
<p class="text-gray-400 text-xs m-1.5">{{text.proxyModeHint}}</p>
<el-button class="focus:outline-none" @click="disableProxy">{{text.closeProxy}}</el-button>
<p class="text-gray-400 text-xs m-1.5">{{text.closeProxyHint}}</p>
</el-form-item>
</el-form>
<h3 class="text-lg my-4">{{about.title}}</h3>
<p class="text-gray-600 text-xs mt-1">{{about.license}}</p>
<p class="text-gray-600 text-xs mt-1 pb-6">Github: <a @click="openGithub" class="cursor-pointer text-blue-400">https://github.com/biuuu/star-rail-warp-export</a></p>
</div>
</template>
<script setup>
const { ipcRenderer, shell } = require('electron')
import { reactive, onMounted, computed } from 'vue'
const emit = defineEmits(['close', 'changeLang'])
const props = defineProps({
i18n: Object
})
const data = reactive({
langMap: new Map()
})
const settingForm = reactive({
lang: 'zh-cn',
logType: 1,
proxyMode: true,
autoUpdate: true,
fetchFullHistory: false,
hideNovice: true
})
const text = computed(() => props.i18n.ui.setting)
const about = computed(() => props.i18n.ui.about)
const saveSetting = async () => {
const keys = ['lang', 'logType', 'proxyMode', 'autoUpdate', 'fetchFullHistory', 'hideNovice']
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/biuuu/star-rail-warp-export')
const openLink = (link) => shell.openExternal(link)
const exportUIGFJSON = () => {
ipcRenderer.invoke('EXPORT_UIGF_JSON')
}
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>

@ -0,0 +1,71 @@
import { isWeapon, isCharacter } 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 = {
count3: 0, count4: 0, count5: 0,
count3w: 0, count4w: 0, count5w: 0, count4c: 0, count5c: 0,
weapon3: new Map(), weapon4: new Map(), weapon5: new Map(),
char4: new Map(), char5: 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 === '3') {
detail.count3++
detail.countMio++
if (isWeapon(type)) {
detail.count3w++
itemCount(detail.weapon3, name)
}
} else if (rank === '4') {
detail.count4++
detail.countMio++
if (isWeapon(type)) {
detail.count4w++
itemCount(detail.weapon4, name)
} else if (isCharacter(type)) {
detail.count4c++
itemCount(detail.char4, name)
}
} else if (rank === '5') {
detail.ssrPos.push([name, index + 1 - lastSSR, time, key])
lastSSR = index + 1
detail.count5++
detail.countMio = 0
if (isWeapon(type)) {
detail.count5w++
itemCount(detail.weapon5, name)
} else if (isCharacter(type)) {
detail.count5c++
itemCount(detail.char5, name)
}
}
})
detail.date = [dateMin, dateMax]
if (detail.total) {
detailMap.set(key, detail)
}
}
return detailMap
}
export default gachaDetail

16
src/renderer/index.css Normal file

@ -0,0 +1,16 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--el-font-size-base: 12px !important;
}
body::-webkit-scrollbar {
width: 6px;
height: 6px;
}
body::-webkit-scrollbar-thumb {
@apply rounded-full bg-gray-300;
}
}

12
src/renderer/index.html Normal file

@ -0,0 +1,12 @@
<!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
src/renderer/main.js Normal file

@ -0,0 +1,11 @@
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')

24
src/renderer/utils.js Normal file

@ -0,0 +1,24 @@
import * as IconComponents from '@element-plus/icons-vue'
const weaponTypeNames = new Set([
'光锥', 'Light Cone', '光錐', 'Lichtkegel', 'Conos de luz', 'cônes de lumière', '光円錐', '광추', 'Cones de Luz', 'Световые конусы', 'Nón Ánh Sáng'
])
const characterTypeNames = new Set([
'角色', 'Character', '캐릭터', 'キャラクター', 'Personaje', 'Personnage', 'Персонажи', 'ตัวละคร', 'Nhân Vật', 'Figur', 'Karakter', 'Personagem'
])
const isCharacter = (name) => characterTypeNames.has(name)
const isWeapon = (name) => weaponTypeNames.has(name)
const IconInstaller = (app) => {
Object.values(IconComponents).forEach(component => {
app.component(component.name, component)
})
}
export {
isWeapon,
isCharacter,
IconInstaller,
}

16
tailwind.config.js Normal file

@ -0,0 +1,16 @@
module.exports = {
content: ['./src/renderer/index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {
minWidth: {
'10': '60px'
}
},
},
variants: {
extend: {
backgroundColor: ['active']
}
},
plugins: [],
}

5308
yarn.lock Normal file

File diff suppressed because it is too large Load Diff