Compare commits
76 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
d5a31fdf85 | |
|
|
fcfa346969 | |
|
|
cdaf7f3f1f | |
|
|
116243eac8 | |
|
|
3a599717de | |
|
|
e1b3703401 | |
|
|
812e7ab611 | |
|
|
858f9a3bcc | |
|
|
f026571844 | |
|
|
f81232ce86 | |
|
|
c2f42863b8 | |
|
|
feb486e775 | |
|
|
5fd0dbbd14 | |
|
|
f04f88a02a | |
|
|
e78d3cd1cf | |
|
|
c524887fc6 | |
|
|
234356d39c | |
|
|
2af6b0628c | |
|
|
f42892c7b1 | |
|
|
e1737f7c30 | |
|
|
32d4586cce | |
|
|
261b19c40b | |
|
|
c4242e9f66 | |
|
|
f3085af9aa | |
|
|
0acc59b918 | |
|
|
61728e6838 | |
|
|
f66bba53d4 | |
|
|
147524e44e | |
|
|
1c83368257 | |
|
|
ccd2439420 | |
|
|
8f62e777ec | |
|
|
afa239df64 | |
|
|
772af8da0d | |
|
|
63635cda07 | |
|
|
3fdc66c384 | |
|
|
4c458861c6 | |
|
|
fb14cf5d71 | |
|
|
85a3241b66 | |
|
|
6d5b0f6bb2 | |
|
|
f8af485c90 | |
|
|
989fd4b30b | |
|
|
e3fea3f815 | |
|
|
043bcb3ae4 | |
|
|
77c621b499 | |
|
|
16a4546710 | |
|
|
7532fde754 | |
|
|
f5c565396d | |
|
|
3c06216f34 | |
|
|
8a913e35aa | |
|
|
3b36a6b7d8 | |
|
|
cffc2a98a5 | |
|
|
90aeaefae7 | |
|
|
27729dacc6 | |
|
|
4a081e1237 | |
|
|
a98e02f15e | |
|
|
830456ea62 | |
|
|
01c0478a15 | |
|
|
5f9ee2e3fd | |
|
|
0cb33a7527 | |
|
|
d9992d1644 | |
|
|
7da5de6c1e | |
|
|
80c0e26b3e | |
|
|
8df1224ea9 | |
|
|
1890e3a2f4 | |
|
|
64e11aa389 | |
|
|
259aa852d8 | |
|
|
abb46979da | |
|
|
586df102a9 | |
|
|
a39e7df069 | |
|
|
beee24e367 | |
|
|
b76faea133 | |
|
|
401fd35bd0 | |
|
|
bafec72d37 | |
|
|
73eeb73680 | |
|
|
e171609bc7 | |
|
|
1e5aeac212 |
|
|
@ -4,9 +4,10 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
deno: [1.x, "1.22.x", canary]
|
deno: [1.x, "1.31.x", canary]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: denoland/setup-deno@v1
|
- uses: denoland/setup-deno@v1
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
name: GUI
|
||||||
|
on: [push, pull_request, workflow_dispatch]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [macos-latest, ubuntu-20.04, windows-latest]
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install dependencies (ubuntu only)
|
||||||
|
if: matrix.platform == 'ubuntu-20.04'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
|
||||||
|
|
||||||
|
- name: Rust setup
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- uses: denoland/setup-deno@v1
|
||||||
|
with:
|
||||||
|
deno-version: 1.x
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 7.29.1
|
||||||
|
|
||||||
|
- name: Sync node version and setup cache
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
cache: "pnpm"
|
||||||
|
cache-dependency-path: gui/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Rust cache
|
||||||
|
uses: swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: "./gui/src-tauri -> target"
|
||||||
|
cache-on-failure: true
|
||||||
|
|
||||||
|
- name: Compile s3si.ts
|
||||||
|
run: deno run -A ./scripts/compile.ts
|
||||||
|
|
||||||
|
- name: Install app dependencies
|
||||||
|
working-directory: ./gui
|
||||||
|
run: pnpm i
|
||||||
|
|
||||||
|
- name: Build the app
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
if: ${{ !startsWith(github.ref, 'refs/tags/gui-v') }}
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
projectPath: ./gui
|
||||||
|
|
||||||
|
- name: Prepare to release the app
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/gui-v') }}
|
||||||
|
run: deno run -A ./gui/scripts/enable-updater.ts
|
||||||
|
|
||||||
|
- name: Release the app
|
||||||
|
uses: tauri-apps/tauri-action@v0
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/gui-v') }}
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
|
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
with:
|
||||||
|
tagName: ${{ github.ref_name }}
|
||||||
|
releaseName: "s3si.ts v__VERSION__"
|
||||||
|
releaseBody: "See the assets to download and install this version."
|
||||||
|
releaseDraft: true
|
||||||
|
prerelease: false
|
||||||
|
projectPath: ./gui
|
||||||
|
|
@ -3,4 +3,4 @@ profile.json
|
||||||
export/
|
export/
|
||||||
cache/
|
cache/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.json
|
/*.json
|
||||||
|
|
|
||||||
32
CHANGELOG.md
|
|
@ -1,3 +1,35 @@
|
||||||
|
## 0.4.1
|
||||||
|
|
||||||
|
feat: add support for Challenges
|
||||||
|
([#72](https://github.com/spacemeowx2/s3si.ts/issues/72))
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
feat: update `callImink`
|
||||||
|
|
||||||
|
feat: update VersionData
|
||||||
|
|
||||||
|
## 0.3.6
|
||||||
|
|
||||||
|
feat: update `WEB_VIEW_VERSION` and query hashes for 4.0.0
|
||||||
|
|
||||||
|
## 0.3.5
|
||||||
|
|
||||||
|
fix: wrong ability keys in some languages
|
||||||
|
|
||||||
|
## 0.3.4
|
||||||
|
|
||||||
|
fix(gui): GUI ja translation about VS and Salmon Run
|
||||||
|
([#68](https://github.com/spacemeowx2/s3si.ts/issues/68))
|
||||||
|
|
||||||
|
## 0.3.3
|
||||||
|
|
||||||
|
feat: add eggstra work mode
|
||||||
|
|
||||||
|
## 0.3.2
|
||||||
|
|
||||||
|
feat: update `WEB_VIEW_VERSION` and query hashes
|
||||||
|
|
||||||
## 0.3.1
|
## 0.3.1
|
||||||
|
|
||||||
feat: use ID for uniform(https://stat.ink/api-info/salmon-uniform3)
|
feat: use ID for uniform(https://stat.ink/api-info/salmon-uniform3)
|
||||||
|
|
|
||||||
15
deno.json
|
|
@ -14,7 +14,20 @@
|
||||||
"cache/",
|
"cache/",
|
||||||
".vscode/",
|
".vscode/",
|
||||||
".github/",
|
".github/",
|
||||||
"profile.json"
|
"profile.json",
|
||||||
|
"gui/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"files": {
|
||||||
|
"exclude": [
|
||||||
|
"export/",
|
||||||
|
"cache/",
|
||||||
|
".vscode/",
|
||||||
|
".github/",
|
||||||
|
"profile.json",
|
||||||
|
"gui/"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
deno.lock
|
|
@ -31,6 +31,7 @@
|
||||||
"https://deno.land/std@0.160.0/path/posix.ts": "6b63de7097e68c8663c84ccedc0fd977656eb134432d818ecd3a4e122638ac24",
|
"https://deno.land/std@0.160.0/path/posix.ts": "6b63de7097e68c8663c84ccedc0fd977656eb134432d818ecd3a4e122638ac24",
|
||||||
"https://deno.land/std@0.160.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9",
|
"https://deno.land/std@0.160.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9",
|
||||||
"https://deno.land/std@0.160.0/path/win32.ts": "ee8826dce087d31c5c81cd414714e677eb68febc40308de87a2ce4b40e10fb8d",
|
"https://deno.land/std@0.160.0/path/win32.ts": "ee8826dce087d31c5c81cd414714e677eb68febc40308de87a2ce4b40e10fb8d",
|
||||||
|
"https://deno.land/std@0.160.0/streams/conversion.ts": "328afbedee0a7e0c330ac4c7b4c1af569ee53974f970230f6a78f545b93abb9b",
|
||||||
"https://deno.land/std@0.160.0/uuid/_common.ts": "76e1fdfb03aecf733f7b3a5edc900f5734f2433b359fdb1535f8de72873bdb3f",
|
"https://deno.land/std@0.160.0/uuid/_common.ts": "76e1fdfb03aecf733f7b3a5edc900f5734f2433b359fdb1535f8de72873bdb3f",
|
||||||
"https://deno.land/std@0.160.0/uuid/mod.ts": "e57ba10200d75f2b17570f13eba19faa6734b1be2da5091e2c01039df41274a5",
|
"https://deno.land/std@0.160.0/uuid/mod.ts": "e57ba10200d75f2b17570f13eba19faa6734b1be2da5091e2c01039df41274a5",
|
||||||
"https://deno.land/std@0.160.0/uuid/v1.ts": "7123410ef9ce980a4f2e54a586ccde5ed7063f6f119a70d86eebd92f8e100295",
|
"https://deno.land/std@0.160.0/uuid/v1.ts": "7123410ef9ce980a4f2e54a586ccde5ed7063f6f119a70d86eebd92f8e100295",
|
||||||
|
|
@ -70,7 +71,7 @@
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
"mongodb": "mongodb@5.1.0",
|
"mongodb": "mongodb@5.5.0",
|
||||||
"splatnet3-types": "splatnet3-types@0.2.20230227204004"
|
"splatnet3-types": "splatnet3-types@0.2.20230227204004"
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|
@ -93,6 +94,10 @@
|
||||||
"integrity": "sha512-y09gBGusgHtinMon/GVbv1J6FrXhnr/+6hqLlSmEFzkz6PodqF6TxjyvfvY3AfO+oG1mgUtbC86xSbOlwvM62Q==",
|
"integrity": "sha512-y09gBGusgHtinMon/GVbv1J6FrXhnr/+6hqLlSmEFzkz6PodqF6TxjyvfvY3AfO+oG1mgUtbC86xSbOlwvM62Q==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
|
"bson@5.3.0": {
|
||||||
|
"integrity": "sha512-ukmCZMneMlaC5ebPHXIkP8YJzNl5DC41N5MAIvKDqLggdao342t4McltoJBQfQya/nHBWAcSsYRqlXPoQkTJag==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
"ip@2.0.0": {
|
"ip@2.0.0": {
|
||||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
|
|
@ -117,6 +122,15 @@
|
||||||
"socks": "socks@2.7.1"
|
"socks": "socks@2.7.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mongodb@5.5.0": {
|
||||||
|
"integrity": "sha512-XgrkUgAAdfnZKQfk5AsYL8j7O99WHd4YXPxYxnh8dZxD+ekYWFRA3JktUsBnfg+455Smf75/+asoU/YLwNGoQQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"bson": "bson@5.3.0",
|
||||||
|
"mongodb-connection-string-url": "mongodb-connection-string-url@2.6.0",
|
||||||
|
"saslprep": "saslprep@1.0.3",
|
||||||
|
"socks": "socks@2.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"punycode@2.3.0": {
|
"punycode@2.3.0": {
|
||||||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
|
|
|
||||||
1
deps.ts
|
|
@ -15,3 +15,4 @@ export { Mutex } from "https://deno.land/x/semaphore@v1.1.1/mod.ts";
|
||||||
export type { DeepReadonly } from "https://deno.land/x/ts_essentials@v9.1.2/mod.ts";
|
export type { DeepReadonly } from "https://deno.land/x/ts_essentials@v9.1.2/mod.ts";
|
||||||
export * as MongoDB from "npm:mongodb";
|
export * as MongoDB from "npm:mongodb";
|
||||||
export * as splatNet3Types from "npm:splatnet3-types/splatnet3";
|
export * as splatNet3Types from "npm:splatnet3-types/splatnet3";
|
||||||
|
export { writeAll } from "https://deno.land/std@0.160.0/streams/conversion.ts";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
indent_size = 4
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
binaries/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Tauri + React + Typescript
|
||||||
|
|
||||||
|
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link href="/src/main.css" rel="stylesheet" />
|
||||||
|
<title>s3si.ts</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root">
|
||||||
|
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024"
|
||||||
|
class="animate-spin my-2 mx-auto" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M512 1024c-69.1 0-136.2-13.5-199.3-40.2C251.7 958 197 921 150 874c-47-47-84-101.7-109.8-162.7C13.5 648.2 0 581.1 0 512c0-19.9 16.1-36 36-36s36 16.1 36 36c0 59.4 11.6 117 34.6 171.3 22.2 52.4 53.9 99.5 94.3 139.9 40.4 40.4 87.5 72.2 139.9 94.3C395 940.4 452.6 952 512 952c59.4 0 117-11.6 171.3-34.6 52.4-22.2 99.5-53.9 139.9-94.3 40.4-40.4 72.2-87.5 94.3-139.9C940.4 629 952 571.4 952 512c0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 0 0-94.3-139.9 437.71 437.71 0 0 0-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.2C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3s-13.5 136.2-40.2 199.3C958 772.3 921 827 874 874c-47 47-101.8 83.9-162.7 109.7-63.1 26.8-130.2 40.3-199.3 40.3z">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 280 KiB |
|
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"name": "s3si-ts",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri",
|
||||||
|
"lint": "eslint --max-warnings=0 src"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^1.3.0",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
|
"daisyui": "^2.52.0",
|
||||||
|
"i18next": "^22.5.0",
|
||||||
|
"i18next-browser-languagedetector": "^7.0.2",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-i18next": "^12.3.1",
|
||||||
|
"react-icons": "^4.9.0",
|
||||||
|
"react-router-dom": "^6.11.2",
|
||||||
|
"react-use": "^17.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^1.3.1",
|
||||||
|
"@types/node": "^20.2.5",
|
||||||
|
"@types/react": "^18.0.15",
|
||||||
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "^8.41.0",
|
||||||
|
"eslint-config-react-app": "^7.0.1",
|
||||||
|
"i18next-http-backend": "^2.2.1",
|
||||||
|
"postcss": "^8.4.24",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
|
"typescript": "^5.0.4",
|
||||||
|
"vite": "^4.3.9",
|
||||||
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
|
"vite-tsconfig-paths": "^4.2.0"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"packageExtensions": {
|
||||||
|
"eslint-plugin-flowtype": {
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@babel/plugin-syntax-flow": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@babel/plugin-transform-react-jsx": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 43 KiB |
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"i18n-backend": "deno run -A ./i18n-backend.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
{
|
||||||
|
"version": "2",
|
||||||
|
"remote": {
|
||||||
|
"https://deno.land/std@0.178.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
|
||||||
|
"https://deno.land/std@0.178.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
|
||||||
|
"https://deno.land/std@0.178.0/async/deferred.ts": "42790112f36a75a57db4a96d33974a936deb7b04d25c6084a9fa8a49f135def8",
|
||||||
|
"https://deno.land/std@0.178.0/bytes/bytes_list.ts": "b4cbdfd2c263a13e8a904b12d082f6177ea97d9297274a4be134e989450dfa6a",
|
||||||
|
"https://deno.land/std@0.178.0/bytes/concat.ts": "d26d6f3d7922e6d663dacfcd357563b7bf4a380ce5b9c2bbe0c8586662f25ce2",
|
||||||
|
"https://deno.land/std@0.178.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219",
|
||||||
|
"https://deno.land/std@0.178.0/bytes/ends_with.ts": "4228811ebc71615d27f065c54b5e815ec1972538772b0f413c0efe05245b472e",
|
||||||
|
"https://deno.land/std@0.178.0/bytes/equals.ts": "b87494ce5442dc786db46f91378100028c402f83a14a2f7bbff6bda7810aefe3",
|
||||||
|
"https://deno.land/std@0.178.0/bytes/includes_needle.ts": "76a8163126fb2f8bf86fd7f22192c3bb04bf6a20b987a095127c2ca08adf3ba6",
|
||||||
|
"https://deno.land/std@0.178.0/bytes/index_of_needle.ts": "65c939607df609374c4415598fa4dad04a2f14c4d98cd15775216f0aaf597f24",
|
||||||
|
"https://deno.land/std@0.178.0/bytes/last_index_of_needle.ts": "7181072883cb4908c6ce8f7a5bb1d96787eef2c2ab3aa94fe4268ab326a53cbf",
|
||||||
|
"https://deno.land/std@0.178.0/bytes/mod.ts": "e869bba1e7a2e3a9cc6c2d55471888429a544e70a840c087672e656e7ba21815",
|
||||||
|
"https://deno.land/std@0.178.0/bytes/repeat.ts": "6f5e490d8d72bcbf8d84a6bb04690b9b3eb5822c5a11687bca73a2318a842294",
|
||||||
|
"https://deno.land/std@0.178.0/bytes/starts_with.ts": "3e607a70c9c09f5140b7a7f17a695221abcc7244d20af3eb47ccbb63f5885135",
|
||||||
|
"https://deno.land/std@0.178.0/crypto/keystack.ts": "877ab0f19eb7d37ad6495190d3c3e39f58e9c52e0b6a966f82fd6df67ca55f90",
|
||||||
|
"https://deno.land/std@0.178.0/crypto/timing_safe_equal.ts": "29a3e05afa48277ab4d9588c0b61f4afe542529302af180c866a4f2a09524169",
|
||||||
|
"https://deno.land/std@0.178.0/encoding/base64.ts": "7de04c2f8aeeb41453b09b186480be90f2ff357613b988e99fabb91d2eeceba1",
|
||||||
|
"https://deno.land/std@0.178.0/encoding/base64url.ts": "3f1178f6446834457b16bfde8b559c1cd3481727fe384d3385e4a9995dc2d851",
|
||||||
|
"https://deno.land/std@0.178.0/http/_negotiation/common.ts": "14d1a52427ab258a4b7161cd80e1d8a207b7cc64b46e911780f57ead5f4323c6",
|
||||||
|
"https://deno.land/std@0.178.0/http/_negotiation/encoding.ts": "ff747d107277c88cb7a6a62a08eeb8d56dad91564cbcccb30694d5dc126dcc53",
|
||||||
|
"https://deno.land/std@0.178.0/http/_negotiation/language.ts": "7bcddd8db3330bdb7ce4fc00a213c5547c1968139864201efd67ef2d0d51887d",
|
||||||
|
"https://deno.land/std@0.178.0/http/_negotiation/media_type.ts": "58847517cd549384ad677c0fe89e0a4815be36fe7a303ea63cee5f6a1d7e1692",
|
||||||
|
"https://deno.land/std@0.178.0/http/cookie_map.ts": "8cf428c03ef17c197196a5c382495cd4adecbfb597f6f8f3cd7e1775c3e79ffd",
|
||||||
|
"https://deno.land/std@0.178.0/http/http_errors.ts": "57169d9bdf4cda1982a3742693c146ab1bf2cbc88df003b40ac905a30013d4cb",
|
||||||
|
"https://deno.land/std@0.178.0/http/http_status.ts": "8a7bcfe3ac025199ad804075385e57f63d055b2aed539d943ccc277616d6f932",
|
||||||
|
"https://deno.land/std@0.178.0/http/negotiation.ts": "32761c921afa7847cf767fe81c81785721abccc0db0fc51c7ec2a45868b4ee4a",
|
||||||
|
"https://deno.land/std@0.178.0/io/buf_reader.ts": "90a7adcb3638d8e1361695cdf844d58bcd97c41711dc6f9f8acc0626ebe097f5",
|
||||||
|
"https://deno.land/std@0.178.0/io/buf_writer.ts": "2fcaadd9f157970fede6e79c8ea9a58556d8cf3c8a686c3fcaaf3875460092cc",
|
||||||
|
"https://deno.land/std@0.178.0/io/buffer.ts": "e2b7564f684dad625cab08f5106f33572d325705d19a36822b3272fbdfb8f726",
|
||||||
|
"https://deno.land/std@0.178.0/io/copy_n.ts": "c498021ce291576a68b5bae9f9d3a27f97644f4af6c1047fb1cff054af19e436",
|
||||||
|
"https://deno.land/std@0.178.0/io/limited_reader.ts": "d709b5b3113d4cbf934415ba242596e0ecb130e8868fb47197217e09dbb59558",
|
||||||
|
"https://deno.land/std@0.178.0/io/mod.ts": "2665bcccc1fd6e8627cca167c3e92aaecbd9897556b6f69e6d258070ef63fd9b",
|
||||||
|
"https://deno.land/std@0.178.0/io/multi_reader.ts": "5f7ef6e987486322b38c72e206b8fbc8916d55a87fbcdc97a8b2596386c28d44",
|
||||||
|
"https://deno.land/std@0.178.0/io/read_delim.ts": "7e102c66f00a118fa1e1ccd4abb080496f43766686907fd8b9522fdf85443586",
|
||||||
|
"https://deno.land/std@0.178.0/io/read_int.ts": "7cb8bcdfaf1107586c3bacc583d11c64c060196cb070bb13ae8c2061404f911f",
|
||||||
|
"https://deno.land/std@0.178.0/io/read_lines.ts": "baee9e35034f2fdfccf63bc24b7e3cb45aa1c1c5de26d178f7bcbc572e87772f",
|
||||||
|
"https://deno.land/std@0.178.0/io/read_long.ts": "f0aaa420e3da1261c5d33c5e729f09922f3d9fa49f046258d4ff7a00d800c71e",
|
||||||
|
"https://deno.land/std@0.178.0/io/read_range.ts": "28152daf32e43dd9f7d41d8466852b0d18ad766cd5c4334c91fef6e1b3a74eb5",
|
||||||
|
"https://deno.land/std@0.178.0/io/read_short.ts": "805cb329574b850b84bf14a92c052c59b5977a492cd780c41df8ad40826c1a20",
|
||||||
|
"https://deno.land/std@0.178.0/io/read_string_delim.ts": "46eb0c9db3547caf8c759631effa200bbe48924f9b34f41edc627bde36cee52d",
|
||||||
|
"https://deno.land/std@0.178.0/io/slice_long_to_bytes.ts": "b096472afa3a0dd90fa84584dde7706ed29fc16d48009a581c49368f09fe70f4",
|
||||||
|
"https://deno.land/std@0.178.0/io/string_reader.ts": "ad9cbecb8509732afcf3d73bb72fa551ec0ccc34f9b8127826247f0190753a65",
|
||||||
|
"https://deno.land/std@0.178.0/io/string_writer.ts": "8a03c5858c24965a54c6538bed15f32a7c72f5704a12bda56f83a40e28e5433e",
|
||||||
|
"https://deno.land/std@0.178.0/media_types/_db.ts": "7606d83e31f23ce1a7968cbaee852810c2cf477903a095696cdc62eaab7ce570",
|
||||||
|
"https://deno.land/std@0.178.0/media_types/_util.ts": "916efbd30b6148a716f110e67a4db29d6949bf4048997b754415dd7e42c52378",
|
||||||
|
"https://deno.land/std@0.178.0/media_types/content_type.ts": "c682589a0aeb016bfed355cc1ed6fbb3ead2ea48fc0000ac5de6a5730613ad1c",
|
||||||
|
"https://deno.land/std@0.178.0/media_types/extension.ts": "7a4ef2813d7182f724a941f38161525996e4a67abc3cf6a0f9bc2168d73a0f0e",
|
||||||
|
"https://deno.land/std@0.178.0/media_types/extensions_by_type.ts": "4358023feac696e6e9d49c0f1e76a859f03ca254df57812f31f8536890c3a443",
|
||||||
|
"https://deno.land/std@0.178.0/media_types/format_media_type.ts": "1e35e16562e5c417401ffc388a9f8f421f97f0ee06259cbe990c51bae4e6c7a8",
|
||||||
|
"https://deno.land/std@0.178.0/media_types/get_charset.ts": "8be15a1fd31a545736b91ace56d0e4c66ea0d7b3fdc5c90760e8202e7b4b1fad",
|
||||||
|
"https://deno.land/std@0.178.0/media_types/mod.ts": "d3f0b99f85053bc0b98ecc24eaa3546dfa09b856dc0bbaf60d8956d2cdd710c8",
|
||||||
|
"https://deno.land/std@0.178.0/media_types/parse_media_type.ts": "bed260d868ea271445ae41d748e7afed9b5a7f407d2777ead08cecf73e9278de",
|
||||||
|
"https://deno.land/std@0.178.0/media_types/type_by_extension.ts": "6076a7fc63181d70f92ec582fdea2c927eb2cfc7f9c9bee9d6add2aca86f2355",
|
||||||
|
"https://deno.land/std@0.178.0/media_types/vendor/mime-db.v1.52.0.ts": "6925bbcae81ca37241e3f55908d0505724358cda3384eaea707773b2c7e99586",
|
||||||
|
"https://deno.land/std@0.178.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
|
||||||
|
"https://deno.land/std@0.178.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
|
||||||
|
"https://deno.land/std@0.178.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
|
||||||
|
"https://deno.land/std@0.178.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
|
||||||
|
"https://deno.land/std@0.178.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
|
||||||
|
"https://deno.land/std@0.178.0/path/mod.ts": "4b83694ac500d7d31b0cdafc927080a53dc0c3027eb2895790fb155082b0d232",
|
||||||
|
"https://deno.land/std@0.178.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
|
||||||
|
"https://deno.land/std@0.178.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
|
||||||
|
"https://deno.land/std@0.178.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
|
||||||
|
"https://deno.land/std@0.178.0/streams/_common.ts": "f45cba84f0d813de3326466095539602364a9ba521f804cc758f7a475cda692d",
|
||||||
|
"https://deno.land/std@0.178.0/streams/buffer.ts": "7e7676c29e0e72f6821c3b5fede2540886a216bb91c849bb5db20bb82a01d8a1",
|
||||||
|
"https://deno.land/std@0.178.0/streams/byte_slice_stream.ts": "cf5785b0d9223ebb51fcf6679d881dfaf614c3b288fb4577b511b6f7801a01aa",
|
||||||
|
"https://deno.land/std@0.178.0/streams/copy.ts": "de0de21701d8cceba84ca01d9731c77f4b3597bb9de6a1b08f32250353feeae8",
|
||||||
|
"https://deno.land/std@0.178.0/streams/delimiter_stream.ts": "de8f822a1c394cdb805a97e364400955cd1757cc282d932b4040a0f78fba3b5f",
|
||||||
|
"https://deno.land/std@0.178.0/streams/early_zip_readable_streams.ts": "64828085be5812ec5f4896c10b59f14e5a322b2c0439be9331dde332ae9c91de",
|
||||||
|
"https://deno.land/std@0.178.0/streams/iterate_reader.ts": "06491ed8f1bb1c619abbfa04c10b173ff95a93e51fe5037b7c1ad0b5cc01fc7d",
|
||||||
|
"https://deno.land/std@0.178.0/streams/limited_bytes_transform_stream.ts": "3bc04143b8b91a923f5ee81a3c618b6606ac7da66ccbcde62a67aaa0375cbc71",
|
||||||
|
"https://deno.land/std@0.178.0/streams/limited_transform_stream.ts": "b336f5d649a06e35e2692849e3682a673bb32531738443eb2ce9f57538722f75",
|
||||||
|
"https://deno.land/std@0.178.0/streams/merge_readable_streams.ts": "5d6302888f4bb0616dafb5768771be0aec9bedc05fbae6b3d726d05ffbec5b15",
|
||||||
|
"https://deno.land/std@0.178.0/streams/mod.ts": "c07ec010e700b9ea887dc36ca08729828bc7912f711e4054e24d33fd46282252",
|
||||||
|
"https://deno.land/std@0.178.0/streams/read_all.ts": "bfa220b0e1d06fa4d0cb5178baba8f8b466019a7411511982bfa2320ca292815",
|
||||||
|
"https://deno.land/std@0.178.0/streams/readable_stream_from_iterable.ts": "cae337ddafd2abc5e3df699ef2af888ac04091f12732ae658448fba2c7b187e8",
|
||||||
|
"https://deno.land/std@0.178.0/streams/readable_stream_from_reader.ts": "9aceaeefa9e04b08f56b2d07272baedc3b6432840b66198d72fa2ada3e6608ea",
|
||||||
|
"https://deno.land/std@0.178.0/streams/reader_from_iterable.ts": "05f7759b9336fd4c233d9daadf92aec9a7d2c07a05986da466a935cec2dd79d9",
|
||||||
|
"https://deno.land/std@0.178.0/streams/reader_from_stream_reader.ts": "3fda9390ec8520c8a9ea2aba2579208b18880a7663d7a9feec8f193b7af14e41",
|
||||||
|
"https://deno.land/std@0.178.0/streams/text_delimiter_stream.ts": "ee216316360366c3744197f5665a066a25e6baa8cfe836fbe9a0033e079e089e",
|
||||||
|
"https://deno.land/std@0.178.0/streams/text_line_stream.ts": "a9dd2636c6b90e626e19df26c97034c5f638bdd957cbd5c531d6278fe1d08e90",
|
||||||
|
"https://deno.land/std@0.178.0/streams/to_transform_stream.ts": "31c8cce967a2f602be5f164973a5c6cedd4c76e1d2fbc22ae0081b084f73fb0e",
|
||||||
|
"https://deno.land/std@0.178.0/streams/writable_stream_from_writer.ts": "0320b759aa343f9f4f58b014fe301d9a7abcbfb8413d260502a885995e6a0776",
|
||||||
|
"https://deno.land/std@0.178.0/streams/write_all.ts": "3b2e1ce44913f966348ce353d02fa5369e94115181037cd8b602510853ec3033",
|
||||||
|
"https://deno.land/std@0.178.0/streams/writer_from_stream_writer.ts": "31126a6bf2e678c5a718011d4831dbe75dbdbd885965d3dbd5dd105e6f20f976",
|
||||||
|
"https://deno.land/std@0.178.0/streams/zip_readable_streams.ts": "9eb82070d83055fe6f077192fb204dc7612695a4b330148e9aa376df1a65e708",
|
||||||
|
"https://deno.land/std@0.178.0/types.d.ts": "220ed56662a0bd393ba5d124aa6ae2ad36a00d2fcbc0e8666a65f4606aaa9784",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/application.ts": "641c071bf14b476d603be18a984573e8a7c0da9c1bfc141ae44dad8012f2c5d6",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/body.ts": "c7392f1dae04a360838f43b9cdd2f83d29c1eff4e6071d5f0cf1f3999b1602bc",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/buf_reader.ts": "7cf96aa0ac670b75098113cf88a291a68332cc45efa8a9698f064ac5b8098a0f",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/content_disposition.ts": "8b8c3cb2fba7138cd5b7f82fc3b5ea39b33db924a824b28261659db7e164621e",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/context.ts": "81b97dab6b35ab872d5b2ff5221b4bc71b425f7069ce1a1dcc1d20c50a09865f",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/deps.ts": "29134b39303d85527915650d6f11909c1e29c4cd208dd2ad9887ce2537167b42",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/etag.ts": "19918f5e1964e3fe6c9fe524a88ffbf9900ce1dfe4146b187b2a86256bb6b663",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/headers.ts": "f50fb05614432bda971021633129aa2e8737e0844e0f01c27a937997b4d8dd4f",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/helpers.ts": "42212afa07a560b2958359cc19577417e89d9574d6579551a0af36ff7f00cc6e",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/http_request.ts": "9f2cc5f4b9e72c5156dd4710875d9f54f992d69ca1a8ad5e0915a773f0e26e8b",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/http_server_flash.ts": "6afeb6bfe77a08d99b205ebf09d18c98b4050ce9cbc66e24be1fba6e46bc7a4f",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/http_server_native.ts": "0141e1339ed9a33bc26ce537ddab5adbb3542b35916d92de286aed4937e4a6d6",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/http_server_native_request.ts": "be315d476550e149c58d7ccd2812be30f373ceedc9c323c300eef03b7c071aa9",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/isMediaType.ts": "62d638abcf837ece3a8f07a4b7ca59794135cb0d4b73194c7d5837efd4161005",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/mediaTyper.ts": "042b853fc8e9c3f6c628dd389e03ef481552bf07242efc3f8a1af042102a6105",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/middleware.ts": "de14f045a2ddfe845d89b5d3140ff52cbcc6f3b3965391106ce04480f9786737",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/middleware/proxy.ts": "b927232f97ec18af4185d7912e45b1191e3ffe24a9c875262ad524211b1274c9",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/mod.ts": "210619d431e41c763486467b0498ef50de79119d750abc57331f76e36f642fc1",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/multipart.ts": "98fe9f226de8c26a16d067027b69fb1e34ad8c4055767dd157907d06cea36f9a",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/range.ts": "68a6df7ab3b868843e33f52deb94c3d4cab25cb9ef369691990c2ac15b04fafb",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/request.ts": "5852ad36389b48e0428a6f3c90854d01f10d1b15949b56001e1e75c2a00ef0f9",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/response.ts": "867d81f7eb0c65c7b8e0e0e9e145ededd5b6daa9ad922e6adc6a36a525f439a6",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/router.ts": "5b266091e55f634c9130e6de5dd331ddfc4c190ee7916a25e0a0f75502edbc32",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/send.ts": "c592a6782c82442d011923297a724c2cba8de14c4a9384c6eb9cebb766817e92",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/server_sent_event.ts": "948b0fe4cb3fe38c7db15e476eb3b7671ef20e566d130e9f701d7c0146aa47dd",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/structured_clone.ts": "9c2d21c62f616400305a60cbd29eb06764ee97edc423223424b6cf55df0e8be2",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/testing.ts": "a0be5c84981afde666de29630f34b09d944ca1a2fe6a5185644b60ad95e16d18",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/types.d.ts": "41951a18c3bfdb11e40707cab75da078ba8a4907cd7d4e11d8536bc2db0dde05",
|
||||||
|
"https://deno.land/x/oak@v12.1.0/util.ts": "3af8c4ed04c6cc2bedbe66e562a77fc59c72df31c55a902a63885861ca1639d6",
|
||||||
|
"https://deno.land/x/path_to_regexp@v6.2.1/index.ts": "894060567837bae8fc9c5cbd4d0a05e9024672083d5883b525c031eea940e556"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"specifiers": {
|
||||||
|
"@octokit/rest@19.0.7": "@octokit/rest@19.0.7_@octokit+core@4.2.0"
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@octokit/auth-token@3.0.3": {
|
||||||
|
"integrity": "sha512-/aFM2M4HVDBT/jjDBa84sJniv1t9Gm/rLkalaz9htOm+L+8JMj1k9w0CkUdcxNyNxZPlTxKPVko+m1VlM58ZVA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/types": "@octokit/types@9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@octokit/core@4.2.0": {
|
||||||
|
"integrity": "sha512-AgvDRUg3COpR82P7PBdGZF/NNqGmtMq2NiPqeSsDIeCfYFOZ9gddqWNQHnFdEUf+YwOj4aZYmJnlPp7OXmDIDg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/auth-token": "@octokit/auth-token@3.0.3",
|
||||||
|
"@octokit/graphql": "@octokit/graphql@5.0.5",
|
||||||
|
"@octokit/request": "@octokit/request@6.2.3",
|
||||||
|
"@octokit/request-error": "@octokit/request-error@3.0.3",
|
||||||
|
"@octokit/types": "@octokit/types@9.0.0",
|
||||||
|
"before-after-hook": "before-after-hook@2.2.3",
|
||||||
|
"universal-user-agent": "universal-user-agent@6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@octokit/endpoint@7.0.5": {
|
||||||
|
"integrity": "sha512-LG4o4HMY1Xoaec87IqQ41TQ+glvIeTKqfjkCEmt5AIwDZJwQeVZFIEYXrYY6yLwK+pAScb9Gj4q+Nz2qSw1roA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/types": "@octokit/types@9.0.0",
|
||||||
|
"is-plain-object": "is-plain-object@5.0.0",
|
||||||
|
"universal-user-agent": "universal-user-agent@6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@octokit/graphql@5.0.5": {
|
||||||
|
"integrity": "sha512-Qwfvh3xdqKtIznjX9lz2D458r7dJPP8l6r4GQkIdWQouZwHQK0mVT88uwiU2bdTU2OtT1uOlKpRciUWldpG0yQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/request": "@octokit/request@6.2.3",
|
||||||
|
"@octokit/types": "@octokit/types@9.0.0",
|
||||||
|
"universal-user-agent": "universal-user-agent@6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@octokit/openapi-types@16.0.0": {
|
||||||
|
"integrity": "sha512-JbFWOqTJVLHZSUUoF4FzAZKYtqdxWu9Z5m2QQnOyEa04fOFljvyh7D3GYKbfuaSWisqehImiVIMG4eyJeP5VEA==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"@octokit/plugin-paginate-rest@6.0.0_@octokit+core@4.2.0": {
|
||||||
|
"integrity": "sha512-Sq5VU1PfT6/JyuXPyt04KZNVsFOSBaYOAq2QRZUwzVlI10KFvcbUo8lR258AAQL1Et60b0WuVik+zOWKLuDZxw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/core": "@octokit/core@4.2.0",
|
||||||
|
"@octokit/types": "@octokit/types@9.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@octokit/plugin-request-log@1.0.4_@octokit+core@4.2.0": {
|
||||||
|
"integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/core": "@octokit/core@4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@octokit/plugin-rest-endpoint-methods@7.0.1_@octokit+core@4.2.0": {
|
||||||
|
"integrity": "sha512-pnCaLwZBudK5xCdrR823xHGNgqOzRnJ/mpC/76YPpNP7DybdsJtP7mdOwh+wYZxK5jqeQuhu59ogMI4NRlBUvA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/core": "@octokit/core@4.2.0",
|
||||||
|
"@octokit/types": "@octokit/types@9.0.0",
|
||||||
|
"deprecation": "deprecation@2.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@octokit/request-error@3.0.3": {
|
||||||
|
"integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/types": "@octokit/types@9.0.0",
|
||||||
|
"deprecation": "deprecation@2.3.1",
|
||||||
|
"once": "once@1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@octokit/request@6.2.3": {
|
||||||
|
"integrity": "sha512-TNAodj5yNzrrZ/VxP+H5HiYaZep0H3GU0O7PaF+fhDrt8FPrnkei9Aal/txsN/1P7V3CPiThG0tIvpPDYUsyAA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/endpoint": "@octokit/endpoint@7.0.5",
|
||||||
|
"@octokit/request-error": "@octokit/request-error@3.0.3",
|
||||||
|
"@octokit/types": "@octokit/types@9.0.0",
|
||||||
|
"is-plain-object": "is-plain-object@5.0.0",
|
||||||
|
"node-fetch": "node-fetch@2.6.9",
|
||||||
|
"universal-user-agent": "universal-user-agent@6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@octokit/rest@19.0.7_@octokit+core@4.2.0": {
|
||||||
|
"integrity": "sha512-HRtSfjrWmWVNp2uAkEpQnuGMJsu/+dBr47dRc5QVgsCbnIc1+GFEaoKBWkYG+zjrsHpSqcAElMio+n10c0b5JA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/core": "@octokit/core@4.2.0",
|
||||||
|
"@octokit/plugin-paginate-rest": "@octokit/plugin-paginate-rest@6.0.0_@octokit+core@4.2.0",
|
||||||
|
"@octokit/plugin-request-log": "@octokit/plugin-request-log@1.0.4_@octokit+core@4.2.0",
|
||||||
|
"@octokit/plugin-rest-endpoint-methods": "@octokit/plugin-rest-endpoint-methods@7.0.1_@octokit+core@4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@octokit/types@9.0.0": {
|
||||||
|
"integrity": "sha512-LUewfj94xCMH2rbD5YJ+6AQ4AVjFYTgpp6rboWM5T7N3IsIF65SBEOVcYMGAEzO/kKNiNaW4LoWtoThOhH06gw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@octokit/openapi-types": "@octokit/openapi-types@16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"before-after-hook@2.2.3": {
|
||||||
|
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"deprecation@2.3.1": {
|
||||||
|
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"is-plain-object@5.0.0": {
|
||||||
|
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"node-fetch@2.6.9": {
|
||||||
|
"integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "whatwg-url@5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"once@1.4.0": {
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "wrappy@1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tr46@0.0.3": {
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"universal-user-agent@6.0.0": {
|
||||||
|
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"webidl-conversions@3.0.1": {
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"whatwg-url@5.0.0": {
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "tr46@0.0.3",
|
||||||
|
"webidl-conversions": "webidl-conversions@3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wrappy@1.0.2": {
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import * as path from "https://deno.land/std@0.178.0/path/mod.ts";
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
const __dirname = path.dirname(path.fromFileUrl(import.meta.url));
|
||||||
|
const tauriConf = path.join(__dirname, '../src-tauri/tauri.conf.json');
|
||||||
|
const tauriConfContent = await Deno.readTextFile(tauriConf);
|
||||||
|
const tauriConfJson = JSON.parse(tauriConfContent);
|
||||||
|
tauriConfJson.tauri.updater.active = true;
|
||||||
|
await Deno.writeTextFile(tauriConf, JSON.stringify(tauriConfJson, null, 2));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { Application, Router } from "https://deno.land/x/oak@v12.1.0/mod.ts";
|
||||||
|
import * as path from "https://deno.land/std@0.178.0/path/mod.ts";
|
||||||
|
|
||||||
|
const PORT = 1421;
|
||||||
|
const __dirname = path.dirname(path.fromFileUrl(import.meta.url));
|
||||||
|
const app = new Application();
|
||||||
|
const router = new Router();
|
||||||
|
|
||||||
|
const keys: Set<string> = new Set();
|
||||||
|
|
||||||
|
async function updateFile() {
|
||||||
|
delayId = null;
|
||||||
|
for (const lng of ["en", 'ja', "zh-CN"]) {
|
||||||
|
const translationPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
`../src/i18n/translation/${lng}.json`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let translations: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
translations = JSON.parse(await Deno.readTextFile(translationPath));
|
||||||
|
} catch (error) {}
|
||||||
|
const toAdd = [...keys].filter((k) =>
|
||||||
|
!Object.keys(translations).includes(k)
|
||||||
|
);
|
||||||
|
|
||||||
|
translations = Object.fromEntries(
|
||||||
|
[
|
||||||
|
...Object.entries(translations),
|
||||||
|
...toAdd
|
||||||
|
.map((i) => [i, i] as const),
|
||||||
|
]
|
||||||
|
.sort(([a], [b]) => a.localeCompare(b)),
|
||||||
|
);
|
||||||
|
console.log("Add keys:", toAdd, "for", lng);
|
||||||
|
|
||||||
|
await Deno.writeTextFile(
|
||||||
|
translationPath,
|
||||||
|
JSON.stringify(translations, null, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
keys.clear();
|
||||||
|
}
|
||||||
|
let delayId: number | null = null;
|
||||||
|
|
||||||
|
router.post("/locales/add/:lng/:ns", async (context) => {
|
||||||
|
try {
|
||||||
|
// ns, lng is ignored
|
||||||
|
const body: Record<string, string> = await context.request.body({
|
||||||
|
type: "json",
|
||||||
|
}).value;
|
||||||
|
for (const key of Object.keys(body)) {
|
||||||
|
keys.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delayId !== null) {
|
||||||
|
clearTimeout(delayId);
|
||||||
|
}
|
||||||
|
delayId = setTimeout(updateFile, 1000);
|
||||||
|
|
||||||
|
context.response.status = 200;
|
||||||
|
context.response.body = { message: "Translation added." };
|
||||||
|
} catch (error) {
|
||||||
|
context.response.status = 500;
|
||||||
|
context.response.body = { message: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(router.routes());
|
||||||
|
app.use(router.allowedMethods());
|
||||||
|
|
||||||
|
console.log(`Listening on port ${PORT}...`);
|
||||||
|
await app.listen({ port: PORT });
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { Octokit } from "npm:@octokit/rest@19.0.7";
|
||||||
|
|
||||||
|
const TAG_PREFIX = "gui-";
|
||||||
|
|
||||||
|
type Platform =
|
||||||
|
| "darwin-x86_64"
|
||||||
|
| "darwin-aarch64"
|
||||||
|
| "linux-x86_64"
|
||||||
|
| "windows-x86_64";
|
||||||
|
const PLATFORMS: Platform[] = [
|
||||||
|
"darwin-x86_64",
|
||||||
|
"darwin-aarch64",
|
||||||
|
"linux-x86_64",
|
||||||
|
"windows-x86_64",
|
||||||
|
];
|
||||||
|
|
||||||
|
const PlatformSuffix: Record<Platform, string> = {
|
||||||
|
"darwin-x86_64": ".app.tar.gz",
|
||||||
|
"darwin-aarch64": ".app.tar.gz",
|
||||||
|
"linux-x86_64": ".AppImage.tar.gz",
|
||||||
|
"windows-x86_64": ".msi.zip",
|
||||||
|
};
|
||||||
|
|
||||||
|
type File = {
|
||||||
|
signature: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdateJson = {
|
||||||
|
version: string;
|
||||||
|
notes: string;
|
||||||
|
pub_date: string;
|
||||||
|
platforms: Record<Platform, File>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const REPO = {
|
||||||
|
owner: "spacemeowx2",
|
||||||
|
repo: "s3si.ts",
|
||||||
|
};
|
||||||
|
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: Deno.env.get("GITHUB_TOKEN"),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function findFirstGuiRelease() {
|
||||||
|
let page = 1;
|
||||||
|
while (true) {
|
||||||
|
const { data: list } = await octokit.repos.listReleases({
|
||||||
|
...REPO,
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (list.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const release of list) {
|
||||||
|
if (release.tag_name.startsWith(TAG_PREFIX)) {
|
||||||
|
return release;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const release = await findFirstGuiRelease();
|
||||||
|
|
||||||
|
const version = release?.tag_name.slice(TAG_PREFIX.length) ?? "unknown";
|
||||||
|
const notes = release?.body ?? "unknown";
|
||||||
|
const pub_date = release?.published_at ?? "unknown";
|
||||||
|
|
||||||
|
async function makePlatforms(r: typeof release) {
|
||||||
|
const assets = r?.assets ?? [];
|
||||||
|
const platforms = Object.fromEntries(PLATFORMS.map((p) => {
|
||||||
|
const asset = assets.find((i) => i.name.endsWith(PlatformSuffix[p]));
|
||||||
|
|
||||||
|
if (!asset) {
|
||||||
|
throw new Error(`Asset not found for ${p}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [p, {
|
||||||
|
signature: asset.browser_download_url + ".sig",
|
||||||
|
url: asset.browser_download_url,
|
||||||
|
}];
|
||||||
|
})) as Record<Platform, File>;
|
||||||
|
|
||||||
|
return platforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateJson: UpdateJson = {
|
||||||
|
version,
|
||||||
|
notes,
|
||||||
|
pub_date,
|
||||||
|
platforms: await makePlatforms(release),
|
||||||
|
};
|
||||||
|
|
||||||
|
// fetch signatures
|
||||||
|
for (const platform of PLATFORMS) {
|
||||||
|
const file = updateJson.platforms[platform];
|
||||||
|
const res = await fetch(file.signature);
|
||||||
|
file.signature = await res.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(updateJson, null, 2));
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "s3si-ts"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Export your battles from SplatNet to https://stat.ink"
|
||||||
|
authors = ["you"]
|
||||||
|
license = ""
|
||||||
|
repository = ""
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "1.2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "1.2", features = ["fs-all", "path-all", "process-relaunch", "shell-execute", "shell-open", "shell-sidecar", "window-all"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
tokio = { version = "1.0", features = ["time"] }
|
||||||
|
urlencoding = "2.1.2"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||||
|
# DO NOT REMOVE!!
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
|
@ -0,0 +1,121 @@
|
||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
use std::{
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tauri::{window::WindowBuilder, WindowEvent};
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
const INIT_SCRIPT: &str = r#"
|
||||||
|
function onSelectUserClick(e) {
|
||||||
|
const element = document.getElementById('authorize-switch-approval-link');
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// very hacky way...
|
||||||
|
window.ipc.postMessage(JSON.stringify({
|
||||||
|
"cmd":"tauri",
|
||||||
|
"callback":0,
|
||||||
|
"error":0,
|
||||||
|
"__tauriModule":"Event",
|
||||||
|
"message":{"cmd":"emit","event":"login","payload":{"url":element.href}}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
function detectAndInject() {
|
||||||
|
const element = document.getElementById('authorize-switch-approval-link');
|
||||||
|
if (!element) {
|
||||||
|
window.setTimeout(detectAndInject, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
element.addEventListener('click', onSelectUserClick);
|
||||||
|
}
|
||||||
|
detectAndInject();
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn open_splatnet(app: tauri::AppHandle, gtoken: String, lang: Option<String>) -> Option<()> {
|
||||||
|
let ui_lang = lang.unwrap_or_else(|| "en-US".to_string());
|
||||||
|
let _window = WindowBuilder::new(
|
||||||
|
&app,
|
||||||
|
"splatnet3",
|
||||||
|
tauri::WindowUrl::App(format!("https://api.lp1.av5ja.srv.nintendo.net/?lang={ui_lang}").into()),
|
||||||
|
)
|
||||||
|
.title("Splatnet3")
|
||||||
|
.center()
|
||||||
|
.inner_size(400.0, 700.0)
|
||||||
|
.initialization_script(&format!(
|
||||||
|
r##"
|
||||||
|
const gtoken = "_gtoken={gtoken}";
|
||||||
|
if (!document.cookie.includes(gtoken)) {{
|
||||||
|
document.cookie = gtoken;
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {{
|
||||||
|
// insert css
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.innerHTML = `
|
||||||
|
[class^="App_App_"] , [class^="InAppContent_children_"] , [class^="SwipableView_swipableViewItem_"] ,
|
||||||
|
[class^="MainWrapper_wrapper_"] , [class^="FriendList_wrapper_"] {{
|
||||||
|
overflow: auto;
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}});
|
||||||
|
"##
|
||||||
|
))
|
||||||
|
.build()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn open_login_window(app: tauri::AppHandle, url: String) -> Option<String> {
|
||||||
|
let encoded = urlencoding::encode(&url);
|
||||||
|
let window = WindowBuilder::new(
|
||||||
|
&app,
|
||||||
|
"login",
|
||||||
|
tauri::WindowUrl::App(format!("/redirect?url={encoded}").into()),
|
||||||
|
)
|
||||||
|
.title("Login")
|
||||||
|
.center()
|
||||||
|
.inner_size(1040.0, 960.0)
|
||||||
|
.initialization_script(INIT_SCRIPT)
|
||||||
|
.build()
|
||||||
|
.ok()?;
|
||||||
|
let result: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
||||||
|
let r2 = result.clone();
|
||||||
|
let r3 = result.clone();
|
||||||
|
|
||||||
|
window.listen("login", move |e| {
|
||||||
|
let mut result = r2.lock().unwrap();
|
||||||
|
*result = e.payload().map(ToString::to_string);
|
||||||
|
});
|
||||||
|
window.on_window_event(move |e| {
|
||||||
|
if let WindowEvent::Destroyed = e {
|
||||||
|
let mut result = r3.lock().unwrap();
|
||||||
|
if result.is_none() {
|
||||||
|
*result = Some("".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loop {
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
let result = result.lock().unwrap();
|
||||||
|
if result.is_some() {
|
||||||
|
window.close().ok();
|
||||||
|
return result.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.invoke_handler(tauri::generate_handler![open_login_window, open_splatnet])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
"beforeBuildCommand": "pnpm build",
|
||||||
|
"devPath": "http://localhost:1420",
|
||||||
|
"distDir": "../dist",
|
||||||
|
"withGlobalTauri": false
|
||||||
|
},
|
||||||
|
"package": {
|
||||||
|
"productName": "s3si-ts",
|
||||||
|
"version": "0.4.1"
|
||||||
|
},
|
||||||
|
"tauri": {
|
||||||
|
"allowlist": {
|
||||||
|
"all": false,
|
||||||
|
"shell": {
|
||||||
|
"sidecar": true,
|
||||||
|
"execute": true,
|
||||||
|
"scope": [
|
||||||
|
{
|
||||||
|
"name": "../binaries/s3si",
|
||||||
|
"sidecar": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deno",
|
||||||
|
"cmd": "deno",
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"-A",
|
||||||
|
"../../src/daemon.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"all": false,
|
||||||
|
"open": true
|
||||||
|
},
|
||||||
|
"window": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"fs": {
|
||||||
|
"scope": [
|
||||||
|
"$APPCONFIG/**/*",
|
||||||
|
"$APPDATA/**/*",
|
||||||
|
"$APPCACHE/**/*"
|
||||||
|
],
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"all": true
|
||||||
|
},
|
||||||
|
"process": {
|
||||||
|
"all": false,
|
||||||
|
"relaunch": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
],
|
||||||
|
"identifier": "cn.imspace.s3si.ts",
|
||||||
|
"targets": "all",
|
||||||
|
"externalBin": [
|
||||||
|
"../binaries/s3si"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
},
|
||||||
|
"updater": {
|
||||||
|
"active": false,
|
||||||
|
"endpoints": [
|
||||||
|
"https://s3si-update.imspace.cn/v1/{{target}}/{{arch}}/{{current_version}}",
|
||||||
|
"https://gist.githubusercontent.com/spacemeowx2/a67078487d3450b75927953f6edc14e2/raw/update-request.json"
|
||||||
|
],
|
||||||
|
"dialog": true,
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IERBMzkzNjFEQ0ZCMjc5QjAKUldTd2ViTFBIVFk1MmdubFh6UTc0NndsZnBQV2hnR3NNSThTYlNudENWYm5yZ0JBRDM0MlloRjAK"
|
||||||
|
},
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"fullscreen": false,
|
||||||
|
"resizable": true,
|
||||||
|
"title": "s3si.ts",
|
||||||
|
"width": 500,
|
||||||
|
"height": 600,
|
||||||
|
"minWidth": 400,
|
||||||
|
"minHeight": 500,
|
||||||
|
"visible": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import 'i18n/config';
|
||||||
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
import { Layout } from "components/Layout";
|
||||||
|
import { Home } from "pages/Home";
|
||||||
|
import { Settings } from "pages/Settings";
|
||||||
|
import { Guide } from 'pages/Guide';
|
||||||
|
import { RedirectLogin } from 'pages/RedirectLogin';
|
||||||
|
import { useShowWindow } from 'hooks/useShowWindow';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
useShowWindow();
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path='/' element={<Layout />}>
|
||||||
|
<Route index element={<Home />} />
|
||||||
|
<Route path='/settings' element={<Settings />} />
|
||||||
|
<Route path='/guide' element={<Guide />} />
|
||||||
|
<Route path='/redirect' element={<RedirectLogin />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { checkUpdate } from '@tauri-apps/api/updater'
|
||||||
|
|
||||||
|
export const CheckUpdate: React.FC<{ className?: string, children?: ReactNode }> = ({ className, children }) => {
|
||||||
|
const onClick = async () => {
|
||||||
|
try {
|
||||||
|
await checkUpdate()
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<button className={className} onClick={onClick}>{children}</button>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type CheckboxProps = {
|
||||||
|
disabled?: boolean
|
||||||
|
children?: React.ReactNode
|
||||||
|
value?: boolean
|
||||||
|
onChange?: (value: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checkbox: React.FC<CheckboxProps> = ({ disabled, value, onChange, children }) => {
|
||||||
|
return <div className="form-control">
|
||||||
|
<label className="label cursor-pointer">
|
||||||
|
<span className="label-text">{children}</span>
|
||||||
|
<input type="checkbox" checked={value ?? false} disabled={disabled} onChange={() => onChange?.(!value)} className="checkbox" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { AiOutlineWarning } from 'react-icons/ai'
|
||||||
|
|
||||||
|
type ErrorContentProps = {
|
||||||
|
error: any
|
||||||
|
retry?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ErrorContent: React.FC<ErrorContentProps> = ({ error, retry }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className='w-full h-full flex justify-center items-center flex-col'>
|
||||||
|
<span className='inline-flex items-center'>
|
||||||
|
<AiOutlineWarning className='inline-block scale-[2] mr-4 justify-end flex-none' />
|
||||||
|
<div className='max-w-full break-all'>
|
||||||
|
<div>{t('发生了错误')}{retry && <button className='link link-info ml-1'>{t('重试')}</button>}</div>
|
||||||
|
{String(error)}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { AiOutlineLeft } from 'react-icons/ai';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
type HeaderProps = {
|
||||||
|
title?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Header: React.FC<HeaderProps> = ({ title }) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return <>
|
||||||
|
<h2 className="card-title" data-tauri-drag-region><button onClick={() => navigate(-1)}><AiOutlineLeft /></button>{title}</h2>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
|
||||||
|
export const Layout: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className='w-full h-full flex flex-col'>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { AiOutlineLoading3Quarters } from 'react-icons/ai';
|
||||||
|
|
||||||
|
type LoadingProps = {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Loading: React.FC<LoadingProps> = ({ className }) => {
|
||||||
|
return <AiOutlineLoading3Quarters className={`animate-spin my-2 mx-auto ${className}`} />
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { usePromise } from 'hooks/usePromise';
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { getConfig, getProfile, setProfile } from 'services/config';
|
||||||
|
import { ensureTokenValid } from 'services/s3si';
|
||||||
|
import { composeLoadable } from 'utils/composeLoadable';
|
||||||
|
import { ErrorContent } from './ErrorContent';
|
||||||
|
|
||||||
|
type OpenSplatnetProps = {
|
||||||
|
children?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OpenSplatnet: React.FC<OpenSplatnetProps> = ({ children }) => {
|
||||||
|
let { loading, error, retry, result } = composeLoadable({
|
||||||
|
config: usePromise(getConfig),
|
||||||
|
profile: usePromise(() => getProfile(0)),
|
||||||
|
});
|
||||||
|
const [doing, setDoing] = useState(false);
|
||||||
|
const [err, setError] = useState<any>();
|
||||||
|
|
||||||
|
const onClick = async () => {
|
||||||
|
setDoing(true);
|
||||||
|
try {
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const state = result.profile.state;
|
||||||
|
const newState = await ensureTokenValid(state);
|
||||||
|
await setProfile(0, {
|
||||||
|
...result.profile,
|
||||||
|
state: newState,
|
||||||
|
});
|
||||||
|
retry?.();
|
||||||
|
const gtoken = newState.loginState?.gToken;
|
||||||
|
await invoke('open_splatnet', {
|
||||||
|
gtoken,
|
||||||
|
lang: result.profile.state.userLang,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setError(e);
|
||||||
|
} finally {
|
||||||
|
setDoing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (error || err) {
|
||||||
|
return <>
|
||||||
|
<ErrorContent error={error || err} retry={retry} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<button className={classNames('btn', {
|
||||||
|
'btn-disabled': !result?.profile.state.loginState?.sessionToken,
|
||||||
|
'loading': loading || doing,
|
||||||
|
})} onClick={onClick}>{children}</button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { usePromise } from 'hooks/usePromise';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { canExport, getProfile, setProfile } from 'services/config';
|
||||||
|
import { addLog, run, useLog } from 'services/s3si';
|
||||||
|
import { Checkbox } from './Checkbox';
|
||||||
|
import { Loading } from './Loading';
|
||||||
|
|
||||||
|
type RunPanelProps = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RunPanel: React.FC<RunPanelProps> = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { result } = usePromise(() => getProfile(0));
|
||||||
|
const [exportBattle, setExportBattle] = useState(true);
|
||||||
|
const [exportCoop, setExportCoop] = useState(true);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return <Loading />
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
addLog({
|
||||||
|
level: 'log',
|
||||||
|
msg: ['Export started at', new Date().toLocaleString()],
|
||||||
|
})
|
||||||
|
const { state } = result;
|
||||||
|
const newState = await run(state, {
|
||||||
|
exporter: "stat.ink",
|
||||||
|
monitor: false,
|
||||||
|
withSummary: false,
|
||||||
|
skipMode: exportBattle === false ? 'vs' : exportCoop === false ? 'coop' : undefined,
|
||||||
|
});
|
||||||
|
await setProfile(0, {
|
||||||
|
...result,
|
||||||
|
state: newState,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
addLog({
|
||||||
|
level: 'error',
|
||||||
|
msg: [e],
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
addLog({
|
||||||
|
level: 'log',
|
||||||
|
msg: ['Export ended at', new Date().toLocaleString()],
|
||||||
|
})
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const disabled = !canExport(result);
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className="tooltip" data-tip={disabled ? t('请先在设置中完成Nintendo Account登录和stat.ink的API密钥') : undefined}>
|
||||||
|
<Checkbox disabled={disabled || loading} value={exportBattle} onChange={setExportBattle}>{t('导出对战数据')}</Checkbox>
|
||||||
|
<Checkbox disabled={disabled || loading} value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={classNames('btn w-full', {
|
||||||
|
'btn-disabled': disabled || (!exportBattle && !exportCoop),
|
||||||
|
'loading': loading,
|
||||||
|
})}
|
||||||
|
>{t('导出')}</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogPanelProps = {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogPanel: React.FC<LogPanelProps> = ({ className }) => {
|
||||||
|
const { renderedLogs } = useLog();
|
||||||
|
const div = useRef<HTMLDivElement>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (div.current) {
|
||||||
|
div.current.scrollTop = div.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [renderedLogs])
|
||||||
|
|
||||||
|
return <div ref={div} className={`bg-neutral text-neutral-content overflow-auto rounded p-4 ${className}`}>
|
||||||
|
{renderedLogs.length === 0 && <pre><code>{t('欢迎! 请点击"导出"按钮开始使用.')}</code></pre>}
|
||||||
|
{renderedLogs.map((line, i) => <pre key={i}><code>{line}</code></pre>)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
/**
|
||||||
|
* https://stat.ink
|
||||||
|
*/
|
||||||
|
export const STAT_INK = 'https://stat.ink'
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that returns a promise and its state.
|
||||||
|
*
|
||||||
|
* @param factory A function that returns a promise.
|
||||||
|
* @returns An object containing the promise's state and result.
|
||||||
|
* @example
|
||||||
|
* const { loading, result, error } = usePromise(() => fetch('https://example.com')
|
||||||
|
* .then(response => response.text())
|
||||||
|
* );
|
||||||
|
* if (loading) {
|
||||||
|
* return <p>Loading...</p>;
|
||||||
|
* }
|
||||||
|
* if (error) {
|
||||||
|
* return <p>Error: {error.message}</p>;
|
||||||
|
* }
|
||||||
|
* return <p>Result: {result}</p>;
|
||||||
|
*/
|
||||||
|
export function usePromise<T>(factory: () => Promise<T>) {
|
||||||
|
const init = () => {
|
||||||
|
const promise = factory();
|
||||||
|
if (!promise || typeof promise.then !== "function") {
|
||||||
|
throw new Error("The factory function must return a promise.");
|
||||||
|
}
|
||||||
|
return promise
|
||||||
|
.then(r => {
|
||||||
|
setResult(r);
|
||||||
|
setLoading(false);
|
||||||
|
return r;
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
setError(e);
|
||||||
|
setLoading(false);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [result, setResult] = useState<T | undefined>(undefined);
|
||||||
|
const [error, setError] = useState<any | undefined>(undefined);
|
||||||
|
const [promise, setPromise] = useState(init);
|
||||||
|
const retry = () => {
|
||||||
|
setLoading(true);
|
||||||
|
setResult(undefined);
|
||||||
|
setError(undefined);
|
||||||
|
setPromise(init);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { loading, result, error, promise, retry };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that returns a promise and its state.
|
||||||
|
*/
|
||||||
|
export function usePromiseLazy<T, Args extends any[]>(factory: (...args: Args) => Promise<T>) {
|
||||||
|
const init = (promise: Promise<T>) => {
|
||||||
|
if (!promise || typeof promise.then !== "function") {
|
||||||
|
throw new Error("The factory function must return a promise.");
|
||||||
|
}
|
||||||
|
return promise
|
||||||
|
.then(r => {
|
||||||
|
setResult(r);
|
||||||
|
setLoading(false);
|
||||||
|
return r;
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
setError(e);
|
||||||
|
setLoading(false);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [result, setResult] = useState<T | undefined>(undefined);
|
||||||
|
const [error, setError] = useState<any | undefined>(undefined);
|
||||||
|
const [promise, setPromise] = useState<Promise<T> | undefined>(undefined);
|
||||||
|
const execute = (...args: Args) => {
|
||||||
|
setLoading(true);
|
||||||
|
setResult(undefined);
|
||||||
|
setError(undefined);
|
||||||
|
setPromise(init(factory(...args)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [execute, { loading, result, error, promise }] as const;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { useCallback, useEffect } from "react";
|
||||||
|
import { getCurrent } from "@tauri-apps/api/window";
|
||||||
|
|
||||||
|
export const useShowWindow = () => {
|
||||||
|
const show = useCallback(() => {
|
||||||
|
if (window.location.pathname === '/hide') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
getCurrent().show().catch(e => console.error(e))
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
useEffect(() => {
|
||||||
|
show();
|
||||||
|
}, [show])
|
||||||
|
|
||||||
|
return show;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
type Maybe<T> = T | null | undefined;
|
||||||
|
type KeyOf<T extends Record<string, any>, K = keyof T> = K extends string ? (T[K] extends Function ? never : K) : never;
|
||||||
|
type DotField<T extends Maybe<Record<string, any>>, K = KeyOf<NonNullable<T>>> = K extends string
|
||||||
|
? K | `${K}.${DotField<NonNullable<T>[K]>}`
|
||||||
|
: never;
|
||||||
|
type ValueOf<T extends Record<string, any>, K> = K extends `${infer I}.${infer R}`
|
||||||
|
? ValueOf<NonNullable<T>[I], R>
|
||||||
|
: K extends string
|
||||||
|
? NonNullable<T>[K]
|
||||||
|
: never;
|
||||||
|
export type FormProps<T> = {
|
||||||
|
value: T;
|
||||||
|
onChange: (value: T) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pick = <T extends Record<string, any>, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> => {
|
||||||
|
const ret = {} as Pick<T, K>;
|
||||||
|
keys.forEach((key) => {
|
||||||
|
ret[key] = obj[key];
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapFormProps = <T, U>(
|
||||||
|
formProps: FormProps<T>,
|
||||||
|
{ mapValue, mapOnChange }: {
|
||||||
|
mapValue: (v: T) => U;
|
||||||
|
mapOnChange: (v: U) => T;
|
||||||
|
},
|
||||||
|
): FormProps<U> => {
|
||||||
|
const { value, onChange } = formProps;
|
||||||
|
return {
|
||||||
|
value: mapValue(value),
|
||||||
|
onChange: (value: U) => onChange(mapOnChange(value)),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSubField = <T extends Record<string, any>>({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: T;
|
||||||
|
onChange?: (cb: (value: T) => T) => void;
|
||||||
|
}) => {
|
||||||
|
const subField = <K extends DotField<T>>(key: K): FormProps<ValueOf<T, K>> => {
|
||||||
|
const v = key.split('.').reduce((o, x) => (o ?? {})[x], value) as ValueOf<T, K>;
|
||||||
|
return {
|
||||||
|
value: v,
|
||||||
|
onChange: (v: ValueOf<T, K>) => {
|
||||||
|
const setInner = <O extends Record<string, any>>(o: O, k: string[], v: any): O => {
|
||||||
|
const [head, ...tail] = k;
|
||||||
|
let out;
|
||||||
|
if (tail.length === 0) {
|
||||||
|
out = {
|
||||||
|
...o,
|
||||||
|
[head]: v,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
out = {
|
||||||
|
...o,
|
||||||
|
[head]: setInner(o[head], tail, v),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
onChange?.((old) => setInner(old, key.split('.'), v));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const subKeys = <K extends keyof T>(keys: K[]) => {
|
||||||
|
return {
|
||||||
|
value: pick(value, keys),
|
||||||
|
onChange: (v: Pick<T, K>) => {
|
||||||
|
onChange?.((old) => ({
|
||||||
|
...old,
|
||||||
|
v,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
subField,
|
||||||
|
subKeys,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { getCurrent, LogicalSize, appWindow } from '@tauri-apps/api/window'
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the window size, and disable resizable, and restores it on unmount.
|
||||||
|
*/
|
||||||
|
export const useWindowSize = ({ w, h }: { w: number, h: number }) => {
|
||||||
|
const oldSize = useRef<{ w: number, h: number }>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const run = async () => {
|
||||||
|
const factor = await appWindow.scaleFactor();
|
||||||
|
const outerSize = (await getCurrent().outerSize()).toLogical(factor);
|
||||||
|
oldSize.current = {
|
||||||
|
w: outerSize.width,
|
||||||
|
h: outerSize.height
|
||||||
|
};
|
||||||
|
|
||||||
|
await getCurrent().setResizable(false);
|
||||||
|
await getCurrent().setSize(new LogicalSize(w, h));
|
||||||
|
}
|
||||||
|
run();
|
||||||
|
return () => {
|
||||||
|
const size = oldSize.current;
|
||||||
|
if (size) {
|
||||||
|
getCurrent().setSize(new LogicalSize(size.w, size.h));
|
||||||
|
getCurrent().setResizable(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [oldSize, w, h]);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import i18next from 'i18next';
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import en from './translation/en.json';
|
||||||
|
import zhCN from './translation/zh-CN.json';
|
||||||
|
import ja from './translation/ja.json';
|
||||||
|
import HttpBackend from 'i18next-http-backend'
|
||||||
|
|
||||||
|
export const resources = {
|
||||||
|
en: {
|
||||||
|
translation: en,
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
|
translation: zhCN,
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
translation: ja,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const instance = i18next
|
||||||
|
.use(initReactI18next)
|
||||||
|
.use(LanguageDetector);
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
instance.use(HttpBackend);
|
||||||
|
}
|
||||||
|
|
||||||
|
instance.init({
|
||||||
|
debug: import.meta.env.DEV,
|
||||||
|
resources,
|
||||||
|
fallbackLng: 'en',
|
||||||
|
// saveMissing: true,
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"保存": "Save",
|
||||||
|
"查看API密钥": "View API Key",
|
||||||
|
"打开 stat.ink": "Open stat.ink",
|
||||||
|
"打开鱿鱼圈3": "Open Splatnet3",
|
||||||
|
"导出": "Export",
|
||||||
|
"导出打工数据": "Export Salmon Run",
|
||||||
|
"导出对战数据": "Export VS",
|
||||||
|
"欢迎! 请点击\"导出\"按钮开始使用.": "Welcome! Please click \"Export\" button to start using.",
|
||||||
|
"界面语言": "Interface Language",
|
||||||
|
"没有更改": "No changes made",
|
||||||
|
"密钥的长度应该为{{length}}, 请检查": "The length of the key should be {{length}}, please check",
|
||||||
|
"前往 stat.ink": "Go to stat.ink",
|
||||||
|
"请从stat.ink中获取API密钥": "Please obtain the API key from stat.ink",
|
||||||
|
"请点击右上角的登录填入": "Please click the top right corner to log in",
|
||||||
|
"请先在设置中完成Nintendo Account登录和stat.ink的API密钥": "Please complete Nintendo Account login and stat.ink API key setup in settings first",
|
||||||
|
"设置": "Settings",
|
||||||
|
"网页登录": "Web Login",
|
||||||
|
"鱿鱼圈3语言偏好": "Splatnet3 Language",
|
||||||
|
"重置": "Reset",
|
||||||
|
"Nintendo Account 会话令牌": "Nintendo Account Session Token",
|
||||||
|
"stat.ink API密钥": "stat.ink API Key"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"保存": "保存",
|
||||||
|
"查看API密钥": "APIキーを表示する",
|
||||||
|
"打开 stat.ink": "stat.inkを開く",
|
||||||
|
"打开鱿鱼圈3": "イカリング3を開く",
|
||||||
|
"导出": "エクスポートする",
|
||||||
|
"导出打工数据": "サーモンランデータをエクスポートする",
|
||||||
|
"导出对战数据": "バトルデータをエクスポートする",
|
||||||
|
"欢迎! 请点击\"导出\"按钮开始使用.": "ようこそ!「エクスポート」ボタンをクリックして使い始めてください。",
|
||||||
|
"界面语言": "インターフェース言語",
|
||||||
|
"没有更改": "変更はありません",
|
||||||
|
"密钥的长度应该为{{length}}, 请检查": "キーの長さは{{length}}でなければなりません。確認してください。",
|
||||||
|
"前往 stat.ink": "stat.inkに移動する",
|
||||||
|
"请从stat.ink中获取API密钥": "stat.inkからAPIキーを取得してください",
|
||||||
|
"请点击右上角的登录填入": "右上隅のログインをクリックして入力してください",
|
||||||
|
"请先在设置中完成Nintendo Account登录和stat.ink的API密钥": "Nintendoアカウントのログインとstat.inkのAPIキー設定を先に設定してください",
|
||||||
|
"设置": "設定",
|
||||||
|
"网页登录": "ウェブサイトにログインする",
|
||||||
|
"鱿鱼圈3语言偏好": "イカリング3言語設定",
|
||||||
|
"重置": "リセット",
|
||||||
|
"Nintendo Account 会话令牌": "Nintendo Accountセッショントークン",
|
||||||
|
"stat.ink API密钥": "stat.ink APIキー"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"保存": "保存",
|
||||||
|
"查看API密钥": "查看API密钥",
|
||||||
|
"打开 stat.ink": "打开 stat.ink",
|
||||||
|
"打开鱿鱼圈3": "打开鱿鱼圈3",
|
||||||
|
"导出": "导出",
|
||||||
|
"导出打工数据": "导出打工数据",
|
||||||
|
"导出对战数据": "导出对战数据",
|
||||||
|
"欢迎! 请点击\"导出\"按钮开始使用.": "欢迎! 请点击\"导出\"按钮开始使用.",
|
||||||
|
"界面语言": "界面语言",
|
||||||
|
"没有更改": "没有更改",
|
||||||
|
"密钥的长度应该为{{length}}, 请检查": "密钥的长度应该为{{length}}, 请检查",
|
||||||
|
"前往 stat.ink": "前往 stat.ink",
|
||||||
|
"请从stat.ink中获取API密钥": "请从stat.ink中获取API密钥",
|
||||||
|
"请点击右上角的登录填入": "请点击右上角的登录填入",
|
||||||
|
"请先在设置中完成Nintendo Account登录和stat.ink的API密钥": "请先在设置中完成Nintendo Account登录和stat.ink的API密钥",
|
||||||
|
"设置": "设置",
|
||||||
|
"网页登录": "网页登录",
|
||||||
|
"鱿鱼圈3语言偏好": "鱿鱼圈3语言偏好",
|
||||||
|
"重置": "重置",
|
||||||
|
"Nintendo Account 会话令牌": "Nintendo Account 会话令牌",
|
||||||
|
"stat.ink API密钥": "stat.ink API密钥"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
// A copy of `../../../src/jsonrpc/client.ts`
|
||||||
|
// deno-lint-ignore-file no-explicit-any
|
||||||
|
import {
|
||||||
|
ID,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
ResponseError,
|
||||||
|
RPCResult,
|
||||||
|
Service,
|
||||||
|
Transport,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export class JSONRPCError extends Error {
|
||||||
|
constructor(public rpcError: ResponseError) {
|
||||||
|
super(rpcError.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class JSONRPCClient<S extends Service> {
|
||||||
|
protected nextId = 1;
|
||||||
|
protected transport: Transport;
|
||||||
|
protected requestMap: Map<
|
||||||
|
ID,
|
||||||
|
(result: RPCResult<any, ResponseError>) => void
|
||||||
|
> = new Map();
|
||||||
|
protected fatal: unknown = undefined;
|
||||||
|
protected task: Promise<void>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{ transport }: { transport: Transport },
|
||||||
|
) {
|
||||||
|
this.transport = transport;
|
||||||
|
this.task = this.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setFatal(e: unknown) {
|
||||||
|
if (!this.fatal) {
|
||||||
|
this.fatal = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleResponse(
|
||||||
|
resp: Response<unknown, ResponseError>,
|
||||||
|
) {
|
||||||
|
const { id } = resp;
|
||||||
|
const callback = this.requestMap.get(id);
|
||||||
|
if (callback) {
|
||||||
|
this.requestMap.delete(id);
|
||||||
|
callback(resp);
|
||||||
|
} else {
|
||||||
|
this.setFatal(new Error("invalid response id: " + String(id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive response from server
|
||||||
|
protected async run() {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const data = await this.transport.recv();
|
||||||
|
if (data === undefined) {
|
||||||
|
this.setFatal(new Error("transport closed"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const result = JSON.parse(data);
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
for (const resp of result) {
|
||||||
|
this.handleResponse(resp);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.handleResponse(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.setFatal(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeRequest<
|
||||||
|
K extends keyof S & string,
|
||||||
|
P extends Parameters<S[K]>,
|
||||||
|
>(
|
||||||
|
method: K,
|
||||||
|
params: P,
|
||||||
|
): Request<K, P> {
|
||||||
|
const req = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: this.nextId,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
} as const;
|
||||||
|
this.nextId += 1;
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
async call<
|
||||||
|
K extends keyof S & string,
|
||||||
|
P extends Parameters<S[K]>,
|
||||||
|
R extends ReturnType<S[K]>,
|
||||||
|
>(
|
||||||
|
method: K,
|
||||||
|
...params: P
|
||||||
|
): Promise<R> {
|
||||||
|
if (this.fatal) {
|
||||||
|
throw this.fatal;
|
||||||
|
}
|
||||||
|
const req = this.makeRequest(method, params);
|
||||||
|
await this.transport.send(JSON.stringify(req));
|
||||||
|
|
||||||
|
return new Promise<R>((res, rej) => {
|
||||||
|
this.requestMap.set(req.id, (result) => {
|
||||||
|
if (result.error) {
|
||||||
|
rej(new JSONRPCError(result.error));
|
||||||
|
} else {
|
||||||
|
res(result.result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getProxy(): S {
|
||||||
|
const proxy = new Proxy({}, {
|
||||||
|
get: (_, method: string) => {
|
||||||
|
return (...params: unknown[]) => this.call(method, ...params as any);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return proxy as S;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
await this.transport.close();
|
||||||
|
await this.task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export type { S3SIService } from './types'
|
||||||
|
export { JSONRPCClient } from './client'
|
||||||
|
export { StdioTransport } from './stdio'
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Command, Child } from '@tauri-apps/api/shell'
|
||||||
|
|
||||||
|
export class StdioTransport {
|
||||||
|
queue: string[] = [];
|
||||||
|
waiting: ((value: string | undefined) => void)[] = [];
|
||||||
|
callback = (data: string) => {
|
||||||
|
const waiting = this.waiting.shift();
|
||||||
|
if (waiting) {
|
||||||
|
waiting(data);
|
||||||
|
} else {
|
||||||
|
this.queue.push(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
child: Promise<Child>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const command = import.meta.env.DEV
|
||||||
|
? new Command("deno", ["run", "-A", "../../src/daemon.ts"])
|
||||||
|
: Command.sidecar('../binaries/s3si');
|
||||||
|
command.stdout.on('data', line => {
|
||||||
|
this.callback(line)
|
||||||
|
})
|
||||||
|
command.stderr.on('data', line => {
|
||||||
|
console.error('daemon stderr', line)
|
||||||
|
})
|
||||||
|
this.child = command.spawn()
|
||||||
|
}
|
||||||
|
|
||||||
|
async recv(): Promise<string | undefined> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const data = this.queue.shift();
|
||||||
|
if (data) {
|
||||||
|
resolve(data);
|
||||||
|
} else {
|
||||||
|
this.waiting.push(resolve);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async send(data: string) {
|
||||||
|
const child = await this.child;
|
||||||
|
await child.write(data + "\n")
|
||||||
|
}
|
||||||
|
async close() {
|
||||||
|
const child = await this.child;
|
||||||
|
await child.kill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export type * from '../../../src/jsonrpc/types';
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color: #0f0f0f;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
color: #f6f6f6;
|
||||||
|
background-color: #2f2f2f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* custom classes */
|
||||||
|
|
||||||
|
.flex-auto-all > * {
|
||||||
|
@apply flex-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-card {
|
||||||
|
@apply card p-2 h-full;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
import { LogProvider } from "services/s3si";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<LogProvider limit={100}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</LogProvider>
|
||||||
|
</React.StrictMode >
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Header } from 'components/Header';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
type StepState = {
|
||||||
|
next: boolean,
|
||||||
|
prev: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Step = {
|
||||||
|
title: string,
|
||||||
|
element: React.FC<{ onChange: (v: StepState) => void }>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Steps: React.FC<{ steps: Step[], className?: string }> = ({ className, steps }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
const [state, setState] = useState({ next: true, prev: true });
|
||||||
|
const hasPrev = step > 0;
|
||||||
|
const hasNext = step < steps.length - 1;
|
||||||
|
|
||||||
|
const Content = steps[step].element;
|
||||||
|
return <div className={`flex flex-col items-center ${className}`}>
|
||||||
|
{/* <ul className="steps w-full mb-4">
|
||||||
|
{steps.map(({ title }, i) => <li key={i} className={classNames("step", {
|
||||||
|
'step-primary': i <= step,
|
||||||
|
})}>{title}</li>)}
|
||||||
|
</ul> */}
|
||||||
|
{Content && <Content onChange={setState} />}
|
||||||
|
<div className='mt-4 flex gap-2'>
|
||||||
|
<button
|
||||||
|
onClick={() => setStep(s => s - 1)}
|
||||||
|
className={classNames('btn', {
|
||||||
|
'btn-disabled': !hasPrev || !state.prev,
|
||||||
|
})}
|
||||||
|
>{t('上一步')}</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setStep(s => s + 1)}
|
||||||
|
className={classNames('btn', {
|
||||||
|
'btn-disabled': !hasNext || !state.next,
|
||||||
|
})}
|
||||||
|
>{t('下一步')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoginNintendoAccount: React.FC<{ onChange: (v: StepState) => void }> = ({ onChange }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return <div className='my-3'>
|
||||||
|
<button className='btn' onClick={() => onChange({ next: true, prev: true })}>{t('点击登录')}</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Guide: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
||||||
|
const steps: Step[] = [{
|
||||||
|
title: t('登录Nintendo Account'),
|
||||||
|
element: LoginNintendoAccount,
|
||||||
|
}, {
|
||||||
|
title: t('填写stat.ink API密钥'),
|
||||||
|
element: () => <></>,
|
||||||
|
}, {
|
||||||
|
title: t('完成'),
|
||||||
|
element: () => <></>,
|
||||||
|
}]
|
||||||
|
|
||||||
|
return <div className="full-card">
|
||||||
|
<Header title={t('设置向导')} />
|
||||||
|
<Steps className='mt-4' steps={steps} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { OpenSplatnet } from 'components/OpenSplatnet';
|
||||||
|
import { LogPanel, RunPanel } from 'components/RunPanel';
|
||||||
|
import { STAT_INK } from 'constant';
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
export const Home: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return <div className='flex p-2 w-full h-full gap-2'>
|
||||||
|
<div className='max-w-full h-full md:max-w-sm flex-auto'>
|
||||||
|
<div className='flex flex-col gap-2 h-full'>
|
||||||
|
<LogPanel className='sm:hidden flex-auto' />
|
||||||
|
<RunPanel />
|
||||||
|
<Link to='/settings' className='btn'>{t('设置')}</Link>
|
||||||
|
<div className='flex gap-2 flex-auto-all'>
|
||||||
|
<OpenSplatnet>{t('打开鱿鱼圈3')}</OpenSplatnet>
|
||||||
|
<a className='btn' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<LogPanel className='hidden sm:block flex-1' />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Loading } from 'components/Loading';
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useLocation } from 'react-use';
|
||||||
|
|
||||||
|
export const RedirectLogin: React.FC = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const state = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const search = state.search ?? '';
|
||||||
|
|
||||||
|
const index = search.indexOf('url=');
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = decodeURIComponent(search.substring(index + 4));
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
return <div className='h-full flex justify-center items-center'>
|
||||||
|
<span className='flex justify-center items-center gap-1'><Loading className='align-middle' />{t('正在跳转到登录页面...')}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
import { ErrorContent } from 'components/ErrorContent';
|
||||||
|
import { Loading } from 'components/Loading';
|
||||||
|
import { usePromise, usePromiseLazy } from 'hooks/usePromise';
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Config, getConfig, getProfile, Profile, setConfig, setProfile } from 'services/config';
|
||||||
|
import { composeLoadable } from 'utils/composeLoadable';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useLogin } from 'services/s3si';
|
||||||
|
import { STAT_INK } from 'constant';
|
||||||
|
import { Header } from 'components/Header';
|
||||||
|
import { useSubField } from 'hooks/useSubField';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
const STAT_INK_KEY_LENGTH = 43;
|
||||||
|
|
||||||
|
const Page: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return <div className='full-card'>
|
||||||
|
<Header title={t('设置')} />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
type FormData = {
|
||||||
|
config: Config,
|
||||||
|
profile: Profile,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SPLATNET3_LANGS = {
|
||||||
|
"de-DE": "German",
|
||||||
|
"en-GB": "English (UK/Australia)",
|
||||||
|
"en-US": "English (US)",
|
||||||
|
"es-ES": "Spanish (Spain)",
|
||||||
|
"es-MX": "Spanish (Latin America)",
|
||||||
|
"fr-CA": "French (Canada)",
|
||||||
|
"fr-FR": "French (France)",
|
||||||
|
"it-IT": "Italian",
|
||||||
|
"ja-JP": "Japanese",
|
||||||
|
"ko-KR": "Korean",
|
||||||
|
"nl-NL": "Dutch",
|
||||||
|
"ru-RU": "Russian",
|
||||||
|
"zh-CN": "Chinese (China)",
|
||||||
|
"zh-TW": "Chinese (Taiwan)"
|
||||||
|
}
|
||||||
|
const UI_LANGS = {
|
||||||
|
"en": "English",
|
||||||
|
"zh-CN": "简体中文",
|
||||||
|
"ja": "日本語",
|
||||||
|
};
|
||||||
|
|
||||||
|
const Form: React.FC<{
|
||||||
|
oldValue: FormData,
|
||||||
|
onSaved?: () => void,
|
||||||
|
}> = ({ oldValue, onSaved }) => {
|
||||||
|
const { login } = useLogin();
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
const [value, setValue] = useState(oldValue);
|
||||||
|
const { subField } = useSubField({ value, onChange: setValue });
|
||||||
|
|
||||||
|
const changed = JSON.stringify(value) !== JSON.stringify(oldValue);
|
||||||
|
|
||||||
|
const sessionToken = subField('profile.state.loginState.sessionToken')
|
||||||
|
const statInkApiKey = subField('profile.state.statInkApiKey')
|
||||||
|
const splatnet3Lang = subField('profile.state.userLang')
|
||||||
|
|
||||||
|
const [onSave, { loading, error }] = usePromiseLazy(async () => {
|
||||||
|
await setProfile(0, value.profile);
|
||||||
|
await setConfig(value.config);
|
||||||
|
onSaved?.();
|
||||||
|
})
|
||||||
|
const [onLogin, loginState] = usePromiseLazy(async () => {
|
||||||
|
const result = await login();
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionToken.onChange(result.sessionToken);
|
||||||
|
})
|
||||||
|
|
||||||
|
const statInkKeyError = (statInkApiKey.value?.length ?? STAT_INK_KEY_LENGTH) !== STAT_INK_KEY_LENGTH;
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<div className='card'>
|
||||||
|
<div className="form-control w-full max-w-md mb-4">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">{t('Nintendo Account 会话令牌')}</span>
|
||||||
|
<span className="label-text-alt"><button
|
||||||
|
className={classNames('link', {
|
||||||
|
loading: loginState.loading,
|
||||||
|
})}
|
||||||
|
onClick={onLogin}
|
||||||
|
disabled={loginState.loading}
|
||||||
|
>{t('网页登录')}</button></span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="input input-bordered w-full"
|
||||||
|
type="text"
|
||||||
|
placeholder={t('请点击右上角的登录填入') ?? undefined}
|
||||||
|
value={sessionToken.value ?? ''}
|
||||||
|
onChange={e => sessionToken.onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-control w-full max-w-md mb-4">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">{t('stat.ink API密钥')}</span>
|
||||||
|
<span className="label-text-alt"><a
|
||||||
|
className='underline'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
href={`${STAT_INK}/profile`}
|
||||||
|
title={t('打开 stat.ink') ?? undefined}
|
||||||
|
>{t('查看API密钥')}</a></span>
|
||||||
|
</label>
|
||||||
|
<div className='tooltip' data-tip={statInkKeyError ? t('密钥的长度应该为{{length}}, 请检查', { length: STAT_INK_KEY_LENGTH }) : null}>
|
||||||
|
<input
|
||||||
|
className={classNames("input input-bordered w-full", {
|
||||||
|
'input-error': statInkKeyError,
|
||||||
|
})}
|
||||||
|
type="text"
|
||||||
|
placeholder={t('请从stat.ink中获取API密钥') ?? undefined}
|
||||||
|
value={statInkApiKey.value ?? ''}
|
||||||
|
onChange={e => statInkApiKey.onChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-control w-full max-w-md mb-4">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">{t('鱿鱼圈3语言偏好')}</span>
|
||||||
|
</label>
|
||||||
|
<select className="select w-full" value={splatnet3Lang.value} onChange={
|
||||||
|
e => splatnet3Lang.onChange(e.target.value)
|
||||||
|
}>
|
||||||
|
{Object.entries(SPLATNET3_LANGS).map(([key, value]) => <option key={key} value={key}>{value} ({key})</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="form-control w-full max-w-md mb-4">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">{t('界面语言')}</span>
|
||||||
|
</label>
|
||||||
|
<select className="select w-full" value={i18n.language} onChange={
|
||||||
|
e => {
|
||||||
|
i18n.changeLanguage(e.target.value);
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
{Object.entries(UI_LANGS).map(([key, value]) => <option key={key} value={key}>{value} ({key})</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ErrorContent error={error} />
|
||||||
|
<div className='flex gap-4 max-w-md justify-between flex-auto-all'>
|
||||||
|
<div className="tooltip" data-tip={changed ? undefined : t('没有更改')}>
|
||||||
|
<button className={classNames('btn btn-primary w-full', {
|
||||||
|
loading,
|
||||||
|
})} onClick={onSave} disabled={!changed || statInkKeyError}>{t('保存')}</button>
|
||||||
|
</div>
|
||||||
|
<button className={classNames('btn', {
|
||||||
|
loading,
|
||||||
|
})} onClick={() => setValue(oldValue)}>{t('重置')}</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Settings: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
let { loading, error, retry, result } = composeLoadable({
|
||||||
|
config: usePromise(getConfig),
|
||||||
|
profile: usePromise(() => getProfile(0)),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Page>
|
||||||
|
<div className='h-full flex items-center justify-center'><Loading /></div>
|
||||||
|
</Page>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Page>
|
||||||
|
<ErrorContent error={error} retry={retry} />
|
||||||
|
</Page>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Page>
|
||||||
|
{result && <Form oldValue={result} onSaved={() => navigate(-1)} />}
|
||||||
|
</Page>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { fs } from "@tauri-apps/api"
|
||||||
|
import { appConfigDir, join } from '@tauri-apps/api/path'
|
||||||
|
import { State } from '../../../src/state';
|
||||||
|
|
||||||
|
const configFile = appConfigDir().then(c => join(c, 'config.json'));
|
||||||
|
const profileDir = appConfigDir().then(c => join(c, 'profile'));
|
||||||
|
|
||||||
|
export type Profile = {
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: import from state.ts.
|
||||||
|
const DEFAULT_STATE: State = {
|
||||||
|
cacheDir: "./cache",
|
||||||
|
fGen: "https://api.imink.app/f",
|
||||||
|
fileExportPath: "./export",
|
||||||
|
monitorInterval: 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProfile: Profile = {
|
||||||
|
state: DEFAULT_STATE,
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultConfig: Config = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initFiles() {
|
||||||
|
await fs.createDir(await profileDir, { recursive: true });
|
||||||
|
await configFile;
|
||||||
|
}
|
||||||
|
initFiles().catch(console.error);
|
||||||
|
|
||||||
|
export async function getConfig(): Promise<Config> {
|
||||||
|
try {
|
||||||
|
const config = await fs.readTextFile(await configFile);
|
||||||
|
return JSON.parse(config);
|
||||||
|
} catch (e) {
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setConfig(config: Config) {
|
||||||
|
await fs.writeTextFile(await configFile, JSON.stringify(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProfile(index: number): Promise<Profile> {
|
||||||
|
try {
|
||||||
|
const profile = await fs.readTextFile(await profileDir.then(c => join(c, `${index}.json`)));
|
||||||
|
return JSON.parse(profile);
|
||||||
|
} catch (e) {
|
||||||
|
return defaultProfile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setProfile(index: number, profile: Profile) {
|
||||||
|
await fs.writeTextFile(await profileDir.then(c => join(c, `${index}.json`)), JSON.stringify(profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canExport(profile: Profile): boolean {
|
||||||
|
return !!(profile.state.loginState?.sessionToken && profile.state.statInkApiKey)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
import { invoke } from "@tauri-apps/api";
|
||||||
|
import { JSONRPCClient, S3SIService, StdioTransport } from "jsonrpc";
|
||||||
|
import { ExportOpts, Log, LoggerLevel, State } from "jsonrpc/types";
|
||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
const client = new JSONRPCClient<S3SIService>({
|
||||||
|
transport: new StdioTransport()
|
||||||
|
}).getProxy();
|
||||||
|
const LOG_SUB = new Set<(logs: Log[]) => void>();
|
||||||
|
|
||||||
|
async function getLogs() {
|
||||||
|
while (true) {
|
||||||
|
const r = await client.getLogs()
|
||||||
|
|
||||||
|
if (r.error) {
|
||||||
|
throw new Error(r.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { level, msg } of r.result) {
|
||||||
|
switch (level) {
|
||||||
|
case 'debug':
|
||||||
|
console.debug(...msg);
|
||||||
|
break;
|
||||||
|
case 'log':
|
||||||
|
console.log(...msg);
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
console.warn(...msg);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
console.error(...msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const cb of LOG_SUB) {
|
||||||
|
cb(r.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getLogs()
|
||||||
|
|
||||||
|
export function addLog(...log: Log[]) {
|
||||||
|
for (const cb of LOG_SUB) {
|
||||||
|
cb(log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_CONTEXT = createContext<{
|
||||||
|
logs: Log[],
|
||||||
|
renderedLogs: React.ReactNode[]
|
||||||
|
}>({
|
||||||
|
logs: [],
|
||||||
|
renderedLogs: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useLog = () => {
|
||||||
|
return useContext(LOG_CONTEXT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMsg(i: any) {
|
||||||
|
if (i instanceof Error) {
|
||||||
|
return i.message
|
||||||
|
}
|
||||||
|
return String(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DISPLAY_MAP: Record<LoggerLevel, string> = {
|
||||||
|
debug: 'DEBUG',
|
||||||
|
log: 'INFO',
|
||||||
|
warn: 'WARN',
|
||||||
|
error: 'ERROR',
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLevel(log: Log) {
|
||||||
|
return `[${DISPLAY_MAP[log.level]}]`.padEnd(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLog(log: Log) {
|
||||||
|
return `${renderLevel(log)} ${log.msg.map(renderMsg).join(' ')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogProvider: React.FC<{ limit?: number, children?: React.ReactNode }> = ({ children, limit = 10 }) => {
|
||||||
|
const [logs, setLogs] = useState<Log[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cb = (logs: Log[]) => {
|
||||||
|
setLogs(old => [...old, ...logs].slice(-limit));
|
||||||
|
}
|
||||||
|
LOG_SUB.add(cb);
|
||||||
|
return () => {
|
||||||
|
LOG_SUB.delete(cb);
|
||||||
|
}
|
||||||
|
}, [limit])
|
||||||
|
|
||||||
|
|
||||||
|
const renderedLogs = useMemo(() => logs.map(renderLog), [logs])
|
||||||
|
|
||||||
|
return <LOG_CONTEXT.Provider value={{
|
||||||
|
logs,
|
||||||
|
renderedLogs,
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</LOG_CONTEXT.Provider>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLogin = () => {
|
||||||
|
const login = useCallback(async () => {
|
||||||
|
const result = await client.loginSteps();
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(result.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const login: string | null = await invoke('open_login_window', {
|
||||||
|
url: result.result.url
|
||||||
|
})
|
||||||
|
if (login === null || login === '') {
|
||||||
|
console.log('user cancel login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const loginResult: { url: string } = JSON.parse(login);
|
||||||
|
const sessionToken = await client.loginSteps({
|
||||||
|
authCodeVerifier: result.result.authCodeVerifier,
|
||||||
|
login: loginResult.url,
|
||||||
|
})
|
||||||
|
if (sessionToken.error) {
|
||||||
|
throw new Error(sessionToken.error.message);
|
||||||
|
}
|
||||||
|
return sessionToken.result;
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function run(state: State, opts: ExportOpts) {
|
||||||
|
const r = await client.run(state, opts);
|
||||||
|
if (r.error) {
|
||||||
|
throw new Error(r.error.message);
|
||||||
|
}
|
||||||
|
return r.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureTokenValid(state: State) {
|
||||||
|
const r = await client.ensureTokenValid(state);
|
||||||
|
if (r.error) {
|
||||||
|
throw new Error(r.error.message);
|
||||||
|
}
|
||||||
|
return r.result;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
export type Loadable<T> = {
|
||||||
|
loading: boolean;
|
||||||
|
result?: T;
|
||||||
|
error?: any;
|
||||||
|
retry?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function composeLoadable<T extends Record<string, Loadable<any>>>(map: T): Loadable<{
|
||||||
|
[P in keyof T]: T[P] extends Loadable<infer R> ? R : never
|
||||||
|
}> {
|
||||||
|
const values = Object.values(map)
|
||||||
|
|
||||||
|
const loading = values.some(v => v.loading);
|
||||||
|
const error = values.find(v => v.error)?.error;
|
||||||
|
const result = loading || error ? undefined : Object.fromEntries(Object.entries(map).map(([k, v]) => [k, v.result])) as any;
|
||||||
|
const retry = values.some(i => !!i.retry) ? () => Object.values(map).forEach(v => v.retry?.()) : undefined;
|
||||||
|
|
||||||
|
return { loading, result, error, retry };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function sleep(ms: number) {
|
||||||
|
return new Promise<void>(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [require("daisyui")],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"baseUrl": "src",
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||||
|
import eslint from 'vite-plugin-eslint';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tsconfigPaths(),
|
||||||
|
eslint(),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
|
// prevent vite from obscuring rust errors
|
||||||
|
clearScreen: false,
|
||||||
|
// tauri expects a fixed port, fail if that port is not available
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
proxy: {
|
||||||
|
'/locales/': 'http://127.0.0.1:1421'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// to make use of `TAURI_DEBUG` and other env variables
|
||||||
|
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||||
|
envPrefix: ["VITE_", "TAURI_"],
|
||||||
|
build: {
|
||||||
|
// Tauri supports es2021
|
||||||
|
target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13",
|
||||||
|
// don't minify for debug builds
|
||||||
|
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
|
||||||
|
// produce sourcemaps for debug builds
|
||||||
|
sourcemap: !!process.env.TAURI_DEBUG,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import * as path from "https://deno.land/std@0.178.0/path/mod.ts";
|
||||||
|
import { assertEquals } from "../dev_deps.ts";
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
const __dirname = path.dirname(path.fromFileUrl(import.meta.url));
|
||||||
|
const TARGETS = [
|
||||||
|
"x86_64-unknown-linux-gnu",
|
||||||
|
"x86_64-pc-windows-msvc",
|
||||||
|
"x86_64-apple-darwin",
|
||||||
|
"aarch64-apple-darwin",
|
||||||
|
];
|
||||||
|
const rustInfo = await (new Deno.Command("rustc", {
|
||||||
|
args: ["-Vv"],
|
||||||
|
})).output();
|
||||||
|
const target =
|
||||||
|
/host: (\S+)/g.exec(new TextDecoder().decode(rustInfo.stdout))?.[1] ?? "?";
|
||||||
|
|
||||||
|
if (!TARGETS.includes(target)) {
|
||||||
|
console.error(`Unsupported target: ${target}`);
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = new Deno.Command("deno", {
|
||||||
|
args: [
|
||||||
|
"compile",
|
||||||
|
"--target",
|
||||||
|
target,
|
||||||
|
"-o",
|
||||||
|
`../gui/binaries/s3si-${target}`,
|
||||||
|
"-A",
|
||||||
|
"../src/daemon.ts",
|
||||||
|
],
|
||||||
|
cwd: __dirname,
|
||||||
|
});
|
||||||
|
const status = await p.output();
|
||||||
|
if (!status.success) {
|
||||||
|
console.error(
|
||||||
|
"Failed to run deno compile for target",
|
||||||
|
target,
|
||||||
|
"code:",
|
||||||
|
status.code,
|
||||||
|
);
|
||||||
|
Deno.exit(status.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
const binPath = `${__dirname}/../gui/binaries/s3si-${target}${
|
||||||
|
Deno.build.os === "windows" ? ".exe" : ""
|
||||||
|
}`;
|
||||||
|
console.log("Test the binary");
|
||||||
|
const s3si = new Deno.Command(binPath, {
|
||||||
|
stdin: "piped",
|
||||||
|
stdout: "piped",
|
||||||
|
}).spawn();
|
||||||
|
const s3siWriter = s3si.stdin.getWriter();
|
||||||
|
await s3siWriter.write(
|
||||||
|
new TextEncoder().encode(
|
||||||
|
'{"jsonrpc":"2.0","method":"hello","params":[],"id":1}\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = new TextDecoder().decode(
|
||||||
|
(await s3si.stdout.getReader().read()).value,
|
||||||
|
);
|
||||||
|
await s3siWriter.close();
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
output,
|
||||||
|
'{"jsonrpc":"2.0","id":1,"result":{"result":"world"}}\n',
|
||||||
|
);
|
||||||
|
console.log("Test passed");
|
||||||
|
|
||||||
|
const hashBuffer = await crypto.subtle.digest(
|
||||||
|
"SHA-256",
|
||||||
|
await Deno.readFile(binPath),
|
||||||
|
);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
|
||||||
|
// 将 Uint8Array 转换为十六进制字符串形式的散列值
|
||||||
|
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Hash:", hashHex);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{}
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
{
|
||||||
|
"version": "2",
|
||||||
|
"remote": {
|
||||||
|
"https://deno.land/std@0.141.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
|
||||||
|
"https://deno.land/std@0.141.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d",
|
||||||
|
"https://deno.land/std@0.141.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9",
|
||||||
|
"https://deno.land/std@0.141.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf",
|
||||||
|
"https://deno.land/std@0.141.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37",
|
||||||
|
"https://deno.land/std@0.141.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b",
|
||||||
|
"https://deno.land/std@0.141.0/io/types.d.ts": "01f60ae7ec02675b5dbed150d258fc184a78dfe5c209ef53ba4422b46b58822c",
|
||||||
|
"https://deno.land/std@0.141.0/streams/conversion.ts": "8268f3f1a43324953dd8e9e4e31adb42e3caddb4502433bde03c279e43d70a3b",
|
||||||
|
"https://deno.land/std@0.160.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
|
||||||
|
"https://deno.land/std@0.160.0/_util/os.ts": "8a33345f74990e627b9dfe2de9b040004b08ea5146c7c9e8fe9a29070d193934",
|
||||||
|
"https://deno.land/std@0.160.0/bytes/bytes_list.ts": "aba5e2369e77d426b10af1de0dcc4531acecec27f9b9056f4f7bfbf8ac147ab4",
|
||||||
|
"https://deno.land/std@0.160.0/bytes/equals.ts": "3c3558c3ae85526f84510aa2b48ab2ad7bdd899e2e0f5b7a8ffc85acb3a6043a",
|
||||||
|
"https://deno.land/std@0.160.0/bytes/mod.ts": "b2e342fd3669176a27a4e15061e9d588b89c1aaf5008ab71766e23669565d179",
|
||||||
|
"https://deno.land/std@0.160.0/encoding/base64.ts": "c57868ca7fa2fbe919f57f88a623ad34e3d970d675bdc1ff3a9d02bba7409db2",
|
||||||
|
"https://deno.land/std@0.160.0/flags/mod.ts": "686b6b36e14b00f11c9e26cecf439021158436a6e34f60eeb0d927f0b169ae20",
|
||||||
|
"https://deno.land/std@0.160.0/fmt/colors.ts": "9e36a716611dcd2e4865adea9c4bec916b5c60caad4cdcdc630d4974e6bb8bd4",
|
||||||
|
"https://deno.land/std@0.160.0/io/buffer.ts": "fae02290f52301c4e0188670e730cd902f9307fb732d79c4aa14ebdc82497289",
|
||||||
|
"https://deno.land/std@0.160.0/io/mod.ts": "6e781ebafd5cdccf9ab4afa1f499b08c513602d023cb08ceebc58758501f78bd",
|
||||||
|
"https://deno.land/std@0.160.0/io/readers.ts": "45847ad404afd2f605eae1cff193f223462bc55eeb9ae313c2f3db28aada0fd6",
|
||||||
|
"https://deno.land/std@0.160.0/io/types.d.ts": "107e1e64834c5ba917c783f446b407d33432c5d612c4b3430df64fc2b4ecf091",
|
||||||
|
"https://deno.land/std@0.160.0/io/util.ts": "23e706b4b6a3ebb34af00ad74d7549d906f3211fc98c1fba1185a36e017fb727",
|
||||||
|
"https://deno.land/std@0.160.0/io/writers.ts": "2e1c63ffd0cfba411b1fd8374609abff9ea86187c9d4d885d42e6fc20325ef0e",
|
||||||
|
"https://deno.land/std@0.160.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3",
|
||||||
|
"https://deno.land/std@0.160.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09",
|
||||||
|
"https://deno.land/std@0.160.0/path/_util.ts": "d16be2a16e1204b65f9d0dfc54a9bc472cafe5f4a190b3c8471ec2016ccd1677",
|
||||||
|
"https://deno.land/std@0.160.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633",
|
||||||
|
"https://deno.land/std@0.160.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee",
|
||||||
|
"https://deno.land/std@0.160.0/path/mod.ts": "56fec03ad0ebd61b6ab39ddb9b0ddb4c4a5c9f2f4f632e09dd37ec9ebfd722ac",
|
||||||
|
"https://deno.land/std@0.160.0/path/posix.ts": "6b63de7097e68c8663c84ccedc0fd977656eb134432d818ecd3a4e122638ac24",
|
||||||
|
"https://deno.land/std@0.160.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9",
|
||||||
|
"https://deno.land/std@0.160.0/path/win32.ts": "ee8826dce087d31c5c81cd414714e677eb68febc40308de87a2ce4b40e10fb8d",
|
||||||
|
"https://deno.land/std@0.160.0/streams/conversion.ts": "328afbedee0a7e0c330ac4c7b4c1af569ee53974f970230f6a78f545b93abb9b",
|
||||||
|
"https://deno.land/std@0.160.0/testing/_diff.ts": "a23e7fc2b4d8daa3e158fa06856bedf5334ce2a2831e8bf9e509717f455adb2c",
|
||||||
|
"https://deno.land/std@0.160.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832",
|
||||||
|
"https://deno.land/std@0.160.0/testing/asserts.ts": "1e340c589853e82e0807629ba31a43c84ebdcdeca910c4a9705715dfdb0f5ce8",
|
||||||
|
"https://deno.land/std@0.160.0/uuid/_common.ts": "76e1fdfb03aecf733f7b3a5edc900f5734f2433b359fdb1535f8de72873bdb3f",
|
||||||
|
"https://deno.land/std@0.160.0/uuid/mod.ts": "e57ba10200d75f2b17570f13eba19faa6734b1be2da5091e2c01039df41274a5",
|
||||||
|
"https://deno.land/std@0.160.0/uuid/v1.ts": "7123410ef9ce980a4f2e54a586ccde5ed7063f6f119a70d86eebd92f8e100295",
|
||||||
|
"https://deno.land/std@0.160.0/uuid/v4.ts": "3e983c6ac895ea2a7ba03da927a2438fe1c26ac43fb38dc44f2f8aa50c23cb53",
|
||||||
|
"https://deno.land/std@0.160.0/uuid/v5.ts": "43973aeda44ad212f2ec9b8d6c042b74d5cef4ce583d6aa6fc4cdb339344c74c",
|
||||||
|
"https://deno.land/std@0.178.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
|
||||||
|
"https://deno.land/std@0.178.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
|
||||||
|
"https://deno.land/std@0.178.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
|
||||||
|
"https://deno.land/std@0.178.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
|
||||||
|
"https://deno.land/std@0.178.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
|
||||||
|
"https://deno.land/std@0.178.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
|
||||||
|
"https://deno.land/std@0.178.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
|
||||||
|
"https://deno.land/std@0.178.0/path/mod.ts": "4b83694ac500d7d31b0cdafc927080a53dc0c3027eb2895790fb155082b0d232",
|
||||||
|
"https://deno.land/std@0.178.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
|
||||||
|
"https://deno.land/std@0.178.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
|
||||||
|
"https://deno.land/std@0.178.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
|
||||||
|
"https://deno.land/x/another_cookiejar@v4.1.4/cookie.ts": "72d6a6633ea13dd2f13b53d9726735b194996353a958024072c4d6b077c97baf",
|
||||||
|
"https://deno.land/x/another_cookiejar@v4.1.4/cookie_jar.ts": "9accd36e76929f2f06fa710d2165fb544703617245fa36ac63560b9fa2a22a25",
|
||||||
|
"https://deno.land/x/another_cookiejar@v4.1.4/fetch_wrapper.ts": "d8918c0776413b2d4a675415727973390b4401a026f6dfdcffedce3296b5e0dc",
|
||||||
|
"https://deno.land/x/another_cookiejar@v4.1.4/mod.ts": "eff949014965771f2cd447fe78625a1ad28b59333afa40640f02c0922534d89a",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/CachedKeyDecoder.ts": "c39b6f1572902ae08c0e4971f639e81031ac59403957fc43c6fb3c7fe69d99a1",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/Decoder.ts": "bdb68309cd51da2b9a897f269784c6d636796258838a97f25b0e1b399c6f369b",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/Encoder.ts": "4852bbacb30cd66eb2bd61a9e20476802458b991e13aacb5eb984d0348247ffe",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/ExtData.ts": "8d97fe43568e119a1eeb93e1ef1c431e0a24e392fb0c6ffed775aac1e579f244",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/ExtensionCodec.ts": "e8a24eb1786156239f589cc3058c8ff3d79ed393f420c40fdf7a93df943c91f2",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/context.ts": "6228de10854dbadf6aef096960af0115214078ec3784eca4565587769fde3d1c",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/decode.ts": "c808aeec46f6d0e5b28d0bbacd40e78d0a3614b229368c70db2e53c03f7555ca",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/decodeAsync.ts": "19e4f33ba0cc8d200b857deb9721bace863c0e89f7bff73e2b04379e4ee85bad",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/encode.ts": "c5598f8eec9efcbd0ef07f246ade049a8f4906703cdb601baf03b2774b293916",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/mod.ts": "c28290db26b1ea027e1798085fd6c8055685ea086f1418d54a33542b285633c9",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/timestamp.ts": "5169949efe39bc24f58cd5dcaae682cdf5353c762a54abf9ae6e18c8d9feb648",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/utils/int.ts": "b08743982f954d2dd7f4f11d868019576b63cb8147d8acc1bce3843f39398188",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/utils/prettyByte.ts": "35c8104d57ba2a727056beaf1063bbe941d512cdd23ce6b04d7c5b44dafcd46e",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/utils/stream.ts": "1315e29af5c1a40d97bfa6f1c4f7f73d26067b912236f56851981f2f049500b8",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/utils/typedArrays.ts": "bb819c2f28cf7f85ed50b2e57f108462715555cc61ce315e8134cf1eef2ae662",
|
||||||
|
"https://deno.land/x/msgpack@v1.4/utils/utf8.ts": "93183055a7a41986080eeb711e83d553e7c8b121642da4379a5adf253b7beefd",
|
||||||
|
"https://deno.land/x/murmurhash@v1.0.0/mod.ts": "13fd2c5534dfd22ffbfcd4255ea13e47a2f2b99e9c90a83dc43e814a0e278829",
|
||||||
|
"https://deno.land/x/progress@v1.2.8/deps.ts": "e0abdc972a0c152508b28ced5ae9c4be26a5773f0aa4a3caa72371c84d2e28a2",
|
||||||
|
"https://deno.land/x/progress@v1.2.8/mod.ts": "5ef7c7ff079d71effed5055666af81cc58a566bc98e2df8473526bd6457976c5",
|
||||||
|
"https://deno.land/x/progress@v1.2.8/multi.ts": "392553552243204539d83ee53cadda990db20b1b421520411318ff9bd0320646",
|
||||||
|
"https://deno.land/x/semaphore@v1.1.1/mod.ts": "431abb51927a16c537cec1cfb05bf2de6a8f3916331f1ec3f9f13ad7ad6a4ea5",
|
||||||
|
"https://deno.land/x/semaphore@v1.1.1/mutex.ts": "2cc6490481f0fdfe97c6b326a2073819d76b76eac3877864a8ada6a2127492f2",
|
||||||
|
"https://deno.land/x/semaphore@v1.1.1/semaphore.ts": "0acf1159d635fa3b9198a4ad4acac9e877d79196601aa80544ac0db5a71c646d",
|
||||||
|
"https://deno.land/x/ts_essentials@v9.1.2/lib/functions.ts": "20681c98ce82d503dba56f5ef9313c196f18a2317ce7c0c331cc3fdea0d56688",
|
||||||
|
"https://deno.land/x/ts_essentials@v9.1.2/lib/literal-types/mod.ts": "c1b9e16a7e49814e9509bed8a5dec25b717761a37d0ef1589d411bd6130dd2e5",
|
||||||
|
"https://deno.land/x/ts_essentials@v9.1.2/lib/mod.ts": "d7e44a25aa621425ffd118a0210a492c5c354411018e2db648a68614d5901f5b",
|
||||||
|
"https://deno.land/x/ts_essentials@v9.1.2/lib/types.ts": "7ee99797a880948c07020e90d569ca3c5d465c378949262110283aa7856f5603",
|
||||||
|
"https://deno.land/x/ts_essentials@v9.1.2/mod.ts": "ffae461c16d4a1bf24c2179582ab8d5c81ad0df61e4ae2fba51ef5e5bdf90345"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"specifiers": {
|
||||||
|
"mongodb": "mongodb@5.1.0",
|
||||||
|
"splatnet3-types": "splatnet3-types@0.2.20230227204004"
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/node@18.14.2": {
|
||||||
|
"integrity": "sha512-1uEQxww3DaghA0RxqHx0O0ppVlo43pJhepY51OxuQIKHpjbnYLA7vcdwioNPzIqmC2u3I/dmylcqjlh0e7AyUA==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"@types/webidl-conversions@7.0.0": {
|
||||||
|
"integrity": "sha512-xTE1E+YF4aWPJJeUzaZI5DRntlkY3+BCVJi0axFptnjGmAoWxkyREIh/XMrfxVLejwQxMCfDXdICo0VLxThrog==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"@types/whatwg-url@8.2.2": {
|
||||||
|
"integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "@types/node@18.14.2",
|
||||||
|
"@types/webidl-conversions": "@types/webidl-conversions@7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bson@5.0.1": {
|
||||||
|
"integrity": "sha512-y09gBGusgHtinMon/GVbv1J6FrXhnr/+6hqLlSmEFzkz6PodqF6TxjyvfvY3AfO+oG1mgUtbC86xSbOlwvM62Q==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"ip@2.0.0": {
|
||||||
|
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"memory-pager@1.5.0": {
|
||||||
|
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"mongodb-connection-string-url@2.6.0": {
|
||||||
|
"integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/whatwg-url": "@types/whatwg-url@8.2.2",
|
||||||
|
"whatwg-url": "whatwg-url@11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mongodb@5.1.0": {
|
||||||
|
"integrity": "sha512-qgKb7y+EI90y4weY3z5+lIgm8wmexbonz0GalHkSElQXVKtRuwqXuhXKccyvIjXCJVy9qPV82zsinY0W1FBnJw==",
|
||||||
|
"dependencies": {
|
||||||
|
"bson": "bson@5.0.1",
|
||||||
|
"mongodb-connection-string-url": "mongodb-connection-string-url@2.6.0",
|
||||||
|
"saslprep": "saslprep@1.0.3",
|
||||||
|
"socks": "socks@2.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"punycode@2.3.0": {
|
||||||
|
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"saslprep@1.0.3": {
|
||||||
|
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
|
||||||
|
"dependencies": {
|
||||||
|
"sparse-bitfield": "sparse-bitfield@3.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"smart-buffer@4.2.0": {
|
||||||
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"socks@2.7.1": {
|
||||||
|
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"ip": "ip@2.0.0",
|
||||||
|
"smart-buffer": "smart-buffer@4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sparse-bitfield@3.0.3": {
|
||||||
|
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"memory-pager": "memory-pager@1.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"splatnet3-types@0.2.20230227204004": {
|
||||||
|
"integrity": "sha512-FAY6pbUcrp5O8c49BNXSKxoyM3UlCrRx2AtA9Y3qlvqOLdHqwxtzcdzbk1b1hRam8ZcrxRzE/ii6ESRiPIAnZw==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"tr46@3.0.0": {
|
||||||
|
"integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "punycode@2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webidl-conversions@7.0.0": {
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"dependencies": {}
|
||||||
|
},
|
||||||
|
"whatwg-url@11.0.0": {
|
||||||
|
"integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "tr46@3.0.0",
|
||||||
|
"webidl-conversions": "webidl-conversions@7.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { MongoDB } from "../deps.ts";
|
||||||
|
import { DEFAULT_ENV } from "../src/env.ts";
|
||||||
|
import { MongoDBExporter } from "../src/exporters/mongodb.ts";
|
||||||
|
import { FileStateBackend, Profile } from "../src/state.ts";
|
||||||
|
|
||||||
|
const env = DEFAULT_ENV;
|
||||||
|
const stateBackend = new FileStateBackend("./profile.json");
|
||||||
|
const profile = new Profile({ stateBackend, env });
|
||||||
|
await profile.readState();
|
||||||
|
|
||||||
|
if (!profile.state.mongoDbUri) {
|
||||||
|
console.error("MongoDB URI not set");
|
||||||
|
Deno.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mongoDbClient = new MongoDB.MongoClient(profile.state.mongoDbUri);
|
||||||
|
const battlesCollection = mongoDbClient.db("splashcat").collection("battles");
|
||||||
|
|
||||||
|
const cursor = battlesCollection.find();
|
||||||
|
|
||||||
|
const oldDocuments = await battlesCollection.countDocuments();
|
||||||
|
|
||||||
|
console.log(`Found ${oldDocuments} old battles to upload...`);
|
||||||
|
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
const erroredBattles = [];
|
||||||
|
|
||||||
|
for await (const doc of cursor) {
|
||||||
|
const { splatNetData, _id } = doc;
|
||||||
|
|
||||||
|
// start time for performance tracking, needs to be very accurate
|
||||||
|
const startTime = new Date();
|
||||||
|
|
||||||
|
splatNetData.playedTime = splatNetData.playedTime.toISOString();
|
||||||
|
|
||||||
|
const response = await fetch("http://127.0.0.1:8000/battles/api/upload/", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${profile.state.splashcatApiKey}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"data_type": "splatnet3",
|
||||||
|
"battle": splatNetData,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Failed to upload ${splatNetData.id}`);
|
||||||
|
erroredBattles.push({
|
||||||
|
id: doc.gameId,
|
||||||
|
error: await response.text(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// end time for performance tracking, needs to be very accurate
|
||||||
|
const endTime = new Date();
|
||||||
|
const timeTaken = endTime.getTime() - startTime.getTime();
|
||||||
|
|
||||||
|
console.log(`Uploaded ${splatNetData.id} (${timeTaken}ms)`);
|
||||||
|
count++;
|
||||||
|
console.log(`Uploaded ${count}/${oldDocuments} battles`)
|
||||||
|
|
||||||
|
if (count % 100 === 0) {
|
||||||
|
console.log("Updating error logs...");
|
||||||
|
if (erroredBattles.length > 0) {
|
||||||
|
await Deno.writeFile("./errored-battles.json", new TextEncoder().encode(JSON.stringify(erroredBattles, null, "\t")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Done!");
|
||||||
|
|
||||||
|
if (erroredBattles.length > 0) {
|
||||||
|
await Deno.writeFile("./errored-battles.json", new TextEncoder().encode(JSON.stringify(erroredBattles, null, 2)));
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,62 @@ function getConst(content: string, name: string): string {
|
||||||
return JSON.parse(match[1]);
|
return JSON.parse(match[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replaceEnum(
|
||||||
|
content: string,
|
||||||
|
name: string,
|
||||||
|
pairs: Record<string, string>,
|
||||||
|
): string {
|
||||||
|
const regex = new RegExp(`export enum ${name} {([\\s\\S^}]+?)}`);
|
||||||
|
|
||||||
|
const body = Object.entries(pairs).map(([key, value]) =>
|
||||||
|
` ${key} = "${value}"`
|
||||||
|
).join(",\n");
|
||||||
|
|
||||||
|
return content.replace(regex, `export enum ${name} {\n${body}\n}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnumKeys(content: string, name: string): string[] {
|
||||||
|
const regex = new RegExp(`export enum ${name} {([\\s\\S^}]+?)}`);
|
||||||
|
|
||||||
|
const match = regex.exec(content);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Cannot find ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = match[1];
|
||||||
|
|
||||||
|
// extract keys from `key = "value"`
|
||||||
|
const keys: string[] = [];
|
||||||
|
const keyRE = /\s*(\w+)\s*=/g;
|
||||||
|
while (true) {
|
||||||
|
const match = keyRE.exec(body);
|
||||||
|
if (!match) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
keys.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQueryHash(js: string, query: string): string {
|
||||||
|
const regex = new RegExp(
|
||||||
|
`params:\\{id:"([^"]*?)",metadata:{},name:"${query}"`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const match = regex.exec(js);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Cannot find ${query}`);
|
||||||
|
}
|
||||||
|
if (match[0].length > 500) {
|
||||||
|
throw new Error(`Match too large ${match[0].length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
async function printError<T>(p: Promise<T>): Promise<T | undefined> {
|
async function printError<T>(p: Promise<T>): Promise<T | undefined> {
|
||||||
try {
|
try {
|
||||||
return await p;
|
return await p;
|
||||||
|
|
@ -39,7 +95,7 @@ async function printError<T>(p: Promise<T>): Promise<T | undefined> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWebViewVer(): Promise<string> {
|
async function getMainJSBody(): Promise<string> {
|
||||||
const splatnet3Home = await (await fetch(SPLATNET3_URL)).text();
|
const splatnet3Home = await (await fetch(SPLATNET3_URL)).text();
|
||||||
|
|
||||||
const mainJS = /src="(\/.*?\.js)"/.exec(splatnet3Home)?.[1];
|
const mainJS = /src="(\/.*?\.js)"/.exec(splatnet3Home)?.[1];
|
||||||
|
|
@ -50,9 +106,16 @@ async function getWebViewVer(): Promise<string> {
|
||||||
|
|
||||||
const mainJSBody = await (await fetch(SPLATNET3_URL + mainJS)).text();
|
const mainJSBody = await (await fetch(SPLATNET3_URL + mainJS)).text();
|
||||||
|
|
||||||
const revision = /"([0-9a-f]{40})"/.exec(mainJSBody)?.[1];
|
return mainJSBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainJSBody = await getMainJSBody();
|
||||||
|
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
async function getWebViewVer(js: string): Promise<string> {
|
||||||
|
const revision = /"([0-9a-f]{40})"/.exec(js)?.[1];
|
||||||
const version = /revision_info_not_set.*?=("|`)(\d+\.\d+\.\d+)-/.exec(
|
const version = /revision_info_not_set.*?=("|`)(\d+\.\d+\.\d+)-/.exec(
|
||||||
mainJSBody,
|
js,
|
||||||
)
|
)
|
||||||
?.[2];
|
?.[2];
|
||||||
|
|
||||||
|
|
@ -83,7 +146,7 @@ const oldValues = {
|
||||||
};
|
};
|
||||||
const newValues: Record<string, string | undefined> = {};
|
const newValues: Record<string, string | undefined> = {};
|
||||||
|
|
||||||
newValues.WEB_VIEW_VERSION = await printError(getWebViewVer());
|
newValues.WEB_VIEW_VERSION = await printError(getWebViewVer(mainJSBody));
|
||||||
newValues.NSOAPP_VERSION = await printError(getNSOVer());
|
newValues.NSOAPP_VERSION = await printError(getNSOVer());
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(newValues)) {
|
for (const [key, value] of Object.entries(newValues)) {
|
||||||
|
|
@ -91,8 +154,27 @@ for (const [key, value] of Object.entries(newValues)) {
|
||||||
content = replaceConst(content, key, value);
|
content = replaceConst(content, key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Deno.writeTextFile(CONSTANT_PATH, content);
|
|
||||||
|
|
||||||
console.log("Done");
|
console.log("const updated");
|
||||||
console.log("Old:", oldValues);
|
console.log("Old:", oldValues);
|
||||||
console.log("New:", newValues);
|
console.log("New:", newValues);
|
||||||
|
|
||||||
|
const keys = getEnumKeys(content, "Queries");
|
||||||
|
const pairs = Object.fromEntries(
|
||||||
|
keys.map((key) => [key, getQueryHash(mainJSBody, key)]),
|
||||||
|
);
|
||||||
|
content = replaceEnum(content, "Queries", pairs);
|
||||||
|
console.log("query updated");
|
||||||
|
|
||||||
|
await Deno.writeTextFile(CONSTANT_PATH, content);
|
||||||
|
|
||||||
|
const command = new Deno.Command(Deno.execPath(), {
|
||||||
|
args: ["fmt", "./src/constant.ts"],
|
||||||
|
cwd: ROOT_DIR,
|
||||||
|
stdin: "inherit",
|
||||||
|
stdout: "inherit",
|
||||||
|
});
|
||||||
|
const { code } = command.outputSync();
|
||||||
|
if (code !== 0) {
|
||||||
|
Deno.exit(code);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ Deno.test("getSeason", () => {
|
||||||
|
|
||||||
assertEquals(season3?.id, "season202303");
|
assertEquals(season3?.id, "season202303");
|
||||||
|
|
||||||
|
const season4 = getSeason(new Date("2023-06-01T00:00:00+00:00"));
|
||||||
|
|
||||||
|
assertEquals(season4?.id, "season202306");
|
||||||
|
|
||||||
const nonExist = getSeason(new Date("2022-06-09T00:00:00+00:00"));
|
const nonExist = getSeason(new Date("2022-06-09T00:00:00+00:00"));
|
||||||
|
|
||||||
assertEquals(nonExist, undefined);
|
assertEquals(nonExist, undefined);
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ export const SEASONS: Season[] = [
|
||||||
start: new Date("2023-03-01T00:00:00+00:00"),
|
start: new Date("2023-03-01T00:00:00+00:00"),
|
||||||
end: new Date("2023-06-01T00:00:00+00:00"),
|
end: new Date("2023-06-01T00:00:00+00:00"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "season202306",
|
||||||
|
name: "Sizzle Season 2023",
|
||||||
|
start: new Date("2023-06-01T00:00:00+00:00"),
|
||||||
|
end: new Date("2023-09-01T00:00:00+00:00"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getSeason = (date: Date): Season | undefined => {
|
export const getSeason = (date: Date): Season | undefined => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,522 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"key": "ink_saver_main",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Hauptverbrauch",
|
||||||
|
"en-GB": "Ink Saver (Main)",
|
||||||
|
"en-US": "Ink Saver (Main)",
|
||||||
|
"es-ES": "Tintahorro (ppal.)",
|
||||||
|
"es-MX": "Ahorro tinta (ppal.)",
|
||||||
|
"fr-CA": "Encrémenteur (pr.)",
|
||||||
|
"fr-FR": "Encrémenteur (pr.)",
|
||||||
|
"it-IT": "Eco-colore princ.",
|
||||||
|
"ja-JP": "インク効率アップ(メイン)",
|
||||||
|
"ko-KR": "잉크 효율 업(메인)",
|
||||||
|
"nl-NL": "Hoofdspaarder",
|
||||||
|
"ru-RU": "Основной баллон X",
|
||||||
|
"zh-CN": "提升墨汁效率(主要武器)",
|
||||||
|
"zh-TW": "提升墨汁效率(主要武器)"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ink_saver_sub",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Sekundärverbrauch",
|
||||||
|
"en-GB": "Ink Saver (Sub)",
|
||||||
|
"en-US": "Ink Saver (Sub)",
|
||||||
|
"es-ES": "Tintahorro (sec.)",
|
||||||
|
"es-MX": "Ahorro tinta (sec.)",
|
||||||
|
"fr-CA": "Encrémenteur (sec.)",
|
||||||
|
"fr-FR": "Encrémenteur (sec.)",
|
||||||
|
"it-IT": "Eco-colore second.",
|
||||||
|
"ja-JP": "インク効率アップ(サブ)",
|
||||||
|
"ko-KR": "잉크 효율 업(서브)",
|
||||||
|
"nl-NL": "Subspaarder",
|
||||||
|
"ru-RU": "Запасной баллон X",
|
||||||
|
"zh-CN": "提升墨汁效率(次要武器)",
|
||||||
|
"zh-TW": "提升墨汁效率(次要武器)"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ink_recovery_up",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Regeneration +",
|
||||||
|
"en-GB": "Ink Recovery Up",
|
||||||
|
"en-US": "Ink Recovery Up",
|
||||||
|
"es-ES": "Recarga rápida",
|
||||||
|
"es-MX": "Mejor recarga tinta",
|
||||||
|
"fr-CA": "Levée d'encre",
|
||||||
|
"fr-FR": "Levée d'encre",
|
||||||
|
"it-IT": "Recupero colore +",
|
||||||
|
"ja-JP": "インク回復力アップ",
|
||||||
|
"ko-KR": "잉크 회복력 업",
|
||||||
|
"nl-NL": "Inktvulling",
|
||||||
|
"ru-RU": "Быстрый баллон",
|
||||||
|
"zh-CN": "提升墨汁回复力",
|
||||||
|
"zh-TW": "提升墨汁回復力"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "run_speed_up",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Lauftempo +",
|
||||||
|
"en-GB": "Run Speed Up",
|
||||||
|
"en-US": "Run Speed Up",
|
||||||
|
"es-ES": "Supercarrera",
|
||||||
|
"es-MX": "Carrera acelerada",
|
||||||
|
"fr-CA": "Course à pied",
|
||||||
|
"fr-FR": "Course à pied",
|
||||||
|
"it-IT": "Velocità +",
|
||||||
|
"ja-JP": "ヒト移動速度アップ",
|
||||||
|
"ko-KR": "인간 이동 속도 업",
|
||||||
|
"nl-NL": "Hardloper",
|
||||||
|
"ru-RU": "Спринтер",
|
||||||
|
"zh-CN": "提升人类移动速度",
|
||||||
|
"zh-TW": "提升人類移動速度"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "swim_speed_up",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Schwimmtempo +",
|
||||||
|
"en-GB": "Swim Speed Up",
|
||||||
|
"en-US": "Swim Speed Up",
|
||||||
|
"es-ES": "Superbuceo",
|
||||||
|
"es-MX": "Nado acelerado",
|
||||||
|
"fr-CA": "Turbo-calmar",
|
||||||
|
"fr-FR": "Turbo-calamar",
|
||||||
|
"it-IT": "Velocità nuoto +",
|
||||||
|
"ja-JP": "イカダッシュ速度アップ",
|
||||||
|
"ko-KR": "징어대시 속도 업",
|
||||||
|
"nl-NL": "Zwemdiploma",
|
||||||
|
"ru-RU": "Плавунец",
|
||||||
|
"zh-CN": "提升鱿鱼冲刺速度",
|
||||||
|
"zh-TW": "提升魷魚衝刺速度"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "special_charge_up",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Spezialladezeit +",
|
||||||
|
"en-GB": "Special Charge Up",
|
||||||
|
"en-US": "Special Charge Up",
|
||||||
|
"es-ES": "Recarga especial",
|
||||||
|
"es-MX": "Recarga especial",
|
||||||
|
"fr-CA": "Jauge spéciale +",
|
||||||
|
"fr-FR": "Jauge spéciale +",
|
||||||
|
"it-IT": "Ricarica speciale +",
|
||||||
|
"ja-JP": "スペシャル増加量アップ",
|
||||||
|
"ko-KR": "스페셜 증가량 업",
|
||||||
|
"nl-NL": "Speciaallader",
|
||||||
|
"ru-RU": "Особый насос",
|
||||||
|
"zh-CN": "提升特殊武器增加量",
|
||||||
|
"zh-TW": "提升特殊武器增加量"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "special_saver",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Spezialabzug -",
|
||||||
|
"en-GB": "Special Saver",
|
||||||
|
"en-US": "Special Saver",
|
||||||
|
"es-ES": "Reducción especial",
|
||||||
|
"es-MX": "Ahorro especial",
|
||||||
|
"fr-CA": "Baisse spéciale -",
|
||||||
|
"fr-FR": "Baisse spéciale -",
|
||||||
|
"it-IT": "Riduzione speciale -",
|
||||||
|
"ja-JP": "スペシャル減少量ダウン",
|
||||||
|
"ko-KR": "스페셜 감소량 다운",
|
||||||
|
"nl-NL": "Speciaalspaarder",
|
||||||
|
"ru-RU": "Особый резерв",
|
||||||
|
"zh-CN": "降低特殊武器减少量",
|
||||||
|
"zh-TW": "降低特殊武器減少量"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "special_power_up",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Spezialstärke +",
|
||||||
|
"en-GB": "Special Power Up",
|
||||||
|
"en-US": "Special Power Up",
|
||||||
|
"es-ES": "Superarma especial",
|
||||||
|
"es-MX": "Mejora especial",
|
||||||
|
"fr-CA": "Arme spéciale +",
|
||||||
|
"fr-FR": "Arme spéciale +",
|
||||||
|
"it-IT": "Arma speciale +",
|
||||||
|
"ja-JP": "スペシャル性能アップ",
|
||||||
|
"ko-KR": "스페셜 성능 업",
|
||||||
|
"nl-NL": "Specialist",
|
||||||
|
"ru-RU": "Особый подход",
|
||||||
|
"zh-CN": "提升特殊武器性能",
|
||||||
|
"zh-TW": "提升特殊武器性能"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "quick_respawn",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Schnelle Rückkehr",
|
||||||
|
"en-GB": "Quick Respawn",
|
||||||
|
"en-US": "Quick Respawn",
|
||||||
|
"es-ES": "Retorno exprés",
|
||||||
|
"es-MX": "Regeneración rápida",
|
||||||
|
"fr-CA": "Sans temps mort",
|
||||||
|
"fr-FR": "Sans temps morts",
|
||||||
|
"it-IT": "Il tempo è colore",
|
||||||
|
"ja-JP": "復活時間短縮",
|
||||||
|
"ko-KR": "부활 시간 단축",
|
||||||
|
"nl-NL": "Comeback",
|
||||||
|
"ru-RU": "Феникс",
|
||||||
|
"zh-CN": "缩短复活时间",
|
||||||
|
"zh-TW": "縮短復活時間"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "quick_super_jump",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Supersprung +",
|
||||||
|
"en-GB": "Quick Super Jump",
|
||||||
|
"en-US": "Quick Super Jump",
|
||||||
|
"es-ES": "Supersalto rápido",
|
||||||
|
"es-MX": "Supersalto rápido",
|
||||||
|
"fr-CA": "Aérodynamisme",
|
||||||
|
"fr-FR": "Aérodynamisme",
|
||||||
|
"it-IT": "Salti super e veloci",
|
||||||
|
"ja-JP": "スーパージャンプ時間短縮",
|
||||||
|
"ko-KR": "슈퍼 점프 시간 단축",
|
||||||
|
"nl-NL": "Turbosprong",
|
||||||
|
"ru-RU": "Суперпрыгун",
|
||||||
|
"zh-CN": "缩短超级跳跃时间",
|
||||||
|
"zh-TW": "縮短超級跳躍時間"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "sub_power_up",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Sekundärstärke +",
|
||||||
|
"en-GB": "Sub Power Up",
|
||||||
|
"en-US": "Sub Power Up",
|
||||||
|
"es-ES": "Superarma secundaria",
|
||||||
|
"es-MX": "Mejora secundaria",
|
||||||
|
"fr-CA": "Arme secondaire +",
|
||||||
|
"fr-FR": "Arme secondaire +",
|
||||||
|
"it-IT": "Arma secondaria +",
|
||||||
|
"ja-JP": "サブ性能アップ",
|
||||||
|
"ko-KR": "서브 성능 업",
|
||||||
|
"nl-NL": "Subtopper",
|
||||||
|
"ru-RU": "Про-Запас",
|
||||||
|
"zh-CN": "提升次要武器性能",
|
||||||
|
"zh-TW": "提升次要武器性能"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ink_resistance_up",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Tintentoleranz +",
|
||||||
|
"en-GB": "Ink Resistance Up",
|
||||||
|
"en-US": "Ink Resistance Up",
|
||||||
|
"es-ES": "Impermeabilidad",
|
||||||
|
"es-MX": "Impermeabilidad",
|
||||||
|
"fr-CA": "Imperméabilité",
|
||||||
|
"fr-FR": "Pieds au sec",
|
||||||
|
"it-IT": "Scarpe impermeabili",
|
||||||
|
"ja-JP": "相手インク影響軽減",
|
||||||
|
"ko-KR": "상대 잉크 영향 감소",
|
||||||
|
"nl-NL": "Inkttolerantie",
|
||||||
|
"ru-RU": "Краскостойкость",
|
||||||
|
"zh-CN": "减轻对手墨汁影响",
|
||||||
|
"zh-TW": "減輕對手墨汁影響"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "sub_resistance_up",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Sekundärschutz +",
|
||||||
|
"en-GB": "Sub Resistance Up",
|
||||||
|
"en-US": "Sub Resistance Up",
|
||||||
|
"es-ES": "Resistencia secundaria",
|
||||||
|
"es-MX": "Resistencia secundaria",
|
||||||
|
"fr-CA": "Filtre à secondaires",
|
||||||
|
"fr-FR": "Filtre à secondaires",
|
||||||
|
"it-IT": "Arma sec. impermeabile",
|
||||||
|
"ja-JP": "サブ影響軽減",
|
||||||
|
"ko-KR": "서브 영향 감소",
|
||||||
|
"nl-NL": "Subdemper",
|
||||||
|
"ru-RU": "Стойкость запаса",
|
||||||
|
"zh-CN": "减轻次要武器影响",
|
||||||
|
"zh-TW": "減輕次要武器影響"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "intensify_action",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Action +",
|
||||||
|
"en-GB": "Intensify Action",
|
||||||
|
"en-US": "Intensify Action",
|
||||||
|
"es-ES": "Agilidad extra",
|
||||||
|
"es-MX": "Agilidad extra",
|
||||||
|
"fr-CA": "Feu de l'action",
|
||||||
|
"fr-FR": "Feu de l'action",
|
||||||
|
"it-IT": "Intensificazione",
|
||||||
|
"ja-JP": "アクション強化",
|
||||||
|
"ko-KR": "액션 강화",
|
||||||
|
"nl-NL": "Actie-assistentie",
|
||||||
|
"ru-RU": "Ультраудар",
|
||||||
|
"zh-CN": "行动强化",
|
||||||
|
"zh-TW": "行動強化"
|
||||||
|
},
|
||||||
|
"primary_only": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "opening_gambit",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Startvorteil",
|
||||||
|
"en-GB": "Opening Gambit",
|
||||||
|
"en-US": "Opening Gambit",
|
||||||
|
"es-ES": "Acelerón de salida",
|
||||||
|
"es-MX": "Acelerón de salida",
|
||||||
|
"fr-CA": "Départ toute allure",
|
||||||
|
"fr-FR": "Chapeaux de roue",
|
||||||
|
"it-IT": "Partenza a razzo",
|
||||||
|
"ja-JP": "スタートダッシュ",
|
||||||
|
"ko-KR": "스타트 대시",
|
||||||
|
"nl-NL": "Vliegende start",
|
||||||
|
"ru-RU": "Стартовый спурт",
|
||||||
|
"zh-CN": "最初冲刺",
|
||||||
|
"zh-TW": "最初衝刺"
|
||||||
|
},
|
||||||
|
"primary_only": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "last_ditch_effort",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Endspurt",
|
||||||
|
"en-GB": "Last-Ditch Effort",
|
||||||
|
"en-US": "Last-Ditch Effort",
|
||||||
|
"es-ES": "Sprint final",
|
||||||
|
"es-MX": "Último recurso",
|
||||||
|
"fr-CA": "Ultime sursaut",
|
||||||
|
"fr-FR": "Ultime sursaut",
|
||||||
|
"it-IT": "Splash finale",
|
||||||
|
"ja-JP": "ラストスパート",
|
||||||
|
"ko-KR": "라스트 스퍼트",
|
||||||
|
"nl-NL": "Eindsprint",
|
||||||
|
"ru-RU": "Финишный спурт",
|
||||||
|
"zh-CN": "最后冲刺",
|
||||||
|
"zh-TW": "最後衝刺"
|
||||||
|
},
|
||||||
|
"primary_only": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "tenacity",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Zähigkeit",
|
||||||
|
"en-GB": "Tenacity",
|
||||||
|
"en-US": "Tenacity",
|
||||||
|
"es-ES": "Ventaja",
|
||||||
|
"es-MX": "Tenacidad",
|
||||||
|
"fr-CA": "Ténacité",
|
||||||
|
"fr-FR": "Justice",
|
||||||
|
"it-IT": "Tenacia",
|
||||||
|
"ja-JP": "逆境強化",
|
||||||
|
"ko-KR": "역경 강화",
|
||||||
|
"nl-NL": "Volharding",
|
||||||
|
"ru-RU": "Компенсатор",
|
||||||
|
"zh-CN": "逆境强化",
|
||||||
|
"zh-TW": "逆境強化"
|
||||||
|
},
|
||||||
|
"primary_only": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "comeback",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Rückkehr",
|
||||||
|
"en-GB": "Comeback",
|
||||||
|
"en-US": "Comeback",
|
||||||
|
"es-ES": "Remontada",
|
||||||
|
"es-MX": "Remonte",
|
||||||
|
"fr-CA": "Retour",
|
||||||
|
"fr-FR": "Come-back",
|
||||||
|
"it-IT": "Gran ritorno",
|
||||||
|
"ja-JP": "カムバック",
|
||||||
|
"ko-KR": "컴백",
|
||||||
|
"nl-NL": "Opfrisser",
|
||||||
|
"ru-RU": "Ответный удар",
|
||||||
|
"zh-CN": "回归",
|
||||||
|
"zh-TW": "回歸"
|
||||||
|
},
|
||||||
|
"primary_only": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ninja_squid",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Tintenfisch-Ninja",
|
||||||
|
"en-GB": "Ninja Squid",
|
||||||
|
"en-US": "Ninja Squid",
|
||||||
|
"es-ES": "Ninjalamar",
|
||||||
|
"es-MX": "Ninjalamar",
|
||||||
|
"fr-CA": "Ninjalmar",
|
||||||
|
"fr-FR": "Ninjalamar",
|
||||||
|
"it-IT": "Calamaro ninja",
|
||||||
|
"ja-JP": "イカニンジャ",
|
||||||
|
"ko-KR": "징어닌자",
|
||||||
|
"nl-NL": "Ninja-inktvis",
|
||||||
|
"ru-RU": "Мимикрия",
|
||||||
|
"zh-CN": "鱿鱼忍者",
|
||||||
|
"zh-TW": "魷魚忍者"
|
||||||
|
},
|
||||||
|
"primary_only": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "haunt",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Vergeltung",
|
||||||
|
"en-GB": "Haunt",
|
||||||
|
"en-US": "Haunt",
|
||||||
|
"es-ES": "Represalia",
|
||||||
|
"es-MX": "Resentimiento",
|
||||||
|
"fr-CA": "Vengeance",
|
||||||
|
"fr-FR": "Revanche",
|
||||||
|
"it-IT": "Rappresaglia",
|
||||||
|
"ja-JP": "リベンジ",
|
||||||
|
"ko-KR": "리벤지",
|
||||||
|
"nl-NL": "Revanche",
|
||||||
|
"ru-RU": "Вендетта",
|
||||||
|
"zh-CN": "复仇",
|
||||||
|
"zh-TW": "復仇"
|
||||||
|
},
|
||||||
|
"primary_only": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "thermal_ink",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Markierfarbe",
|
||||||
|
"en-GB": "Thermal Ink",
|
||||||
|
"en-US": "Thermal Ink",
|
||||||
|
"es-ES": "Señuelo",
|
||||||
|
"es-MX": "Tinta rastreadora",
|
||||||
|
"fr-CA": "Encre thermique",
|
||||||
|
"fr-FR": "Encre thermique",
|
||||||
|
"it-IT": "Inchiostro termico",
|
||||||
|
"ja-JP": "サーマルインク",
|
||||||
|
"ko-KR": "서멀 잉크",
|
||||||
|
"nl-NL": "Markeerstift",
|
||||||
|
"ru-RU": "Клеймо",
|
||||||
|
"zh-CN": "热力墨汁",
|
||||||
|
"zh-TW": "熱力墨汁"
|
||||||
|
},
|
||||||
|
"primary_only": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "respawn_punisher",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Heimsuchung",
|
||||||
|
"en-GB": "Respawn Punisher",
|
||||||
|
"en-US": "Respawn Punisher",
|
||||||
|
"es-ES": "Castigo póstumo",
|
||||||
|
"es-MX": "Castigo póstumo",
|
||||||
|
"fr-CA": "Retour perdant",
|
||||||
|
"fr-FR": "Retour perdant",
|
||||||
|
"it-IT": "Castigo",
|
||||||
|
"ja-JP": "復活ペナルティアップ",
|
||||||
|
"ko-KR": "부활 페널티 업",
|
||||||
|
"nl-NL": "Repercussie",
|
||||||
|
"ru-RU": "Кара",
|
||||||
|
"zh-CN": "提升复活惩罚",
|
||||||
|
"zh-TW": "提升復活懲罰"
|
||||||
|
},
|
||||||
|
"primary_only": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ability_doubler",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Effektdoppelung",
|
||||||
|
"en-GB": "Ability Doubler",
|
||||||
|
"en-US": "Ability Doubler",
|
||||||
|
"es-ES": "Duplicador",
|
||||||
|
"es-MX": "Duplicador",
|
||||||
|
"fr-CA": "Bonus ×2",
|
||||||
|
"fr-FR": "Bonus ×2",
|
||||||
|
"it-IT": "Raddoppiatore",
|
||||||
|
"ja-JP": "追加ギアパワー倍化",
|
||||||
|
"ko-KR": "추가 기어 파워 2배",
|
||||||
|
"nl-NL": "Verdubbelaar",
|
||||||
|
"ru-RU": "Дупликатор",
|
||||||
|
"zh-CN": "追加装备能力增倍",
|
||||||
|
"zh-TW": "追加裝備能力增倍"
|
||||||
|
},
|
||||||
|
"primary_only": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "stealth_jump",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Sprunginfiltration",
|
||||||
|
"en-GB": "Stealth Jump",
|
||||||
|
"en-US": "Stealth Jump",
|
||||||
|
"es-ES": "Supersalto invisible",
|
||||||
|
"es-MX": "Supersalto invisible",
|
||||||
|
"fr-CA": "Super saut invisible",
|
||||||
|
"fr-FR": "Réception réussie",
|
||||||
|
"it-IT": "Salto al buio",
|
||||||
|
"ja-JP": "ステルスジャンプ",
|
||||||
|
"ko-KR": "스텔스 점프",
|
||||||
|
"nl-NL": "Sluipsprong",
|
||||||
|
"ru-RU": "Десант",
|
||||||
|
"zh-CN": "隐身跳跃",
|
||||||
|
"zh-TW": "隱身跳躍"
|
||||||
|
},
|
||||||
|
"primary_only": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "object_shredder",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Zerstörer",
|
||||||
|
"en-GB": "Object Shredder",
|
||||||
|
"en-US": "Object Shredder",
|
||||||
|
"es-ES": "Demolición",
|
||||||
|
"es-MX": "Demolición",
|
||||||
|
"fr-CA": "Démolition",
|
||||||
|
"fr-FR": "Démolition",
|
||||||
|
"it-IT": "Demolitore",
|
||||||
|
"ja-JP": "対物攻撃力アップ",
|
||||||
|
"ko-KR": "오브젝트 공격력 업",
|
||||||
|
"nl-NL": "Sloper",
|
||||||
|
"ru-RU": "Демонтажник",
|
||||||
|
"zh-CN": "提升对物体攻击力",
|
||||||
|
"zh-TW": "提升對物體攻擊力"
|
||||||
|
},
|
||||||
|
"primary_only": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "drop_roller",
|
||||||
|
"name": {
|
||||||
|
"de-DE": "Tricklandung",
|
||||||
|
"en-GB": "Drop Roller",
|
||||||
|
"en-US": "Drop Roller",
|
||||||
|
"es-ES": "Amortiguador",
|
||||||
|
"es-MX": "Aterrizaje rodante",
|
||||||
|
"fr-CA": "Super roulade",
|
||||||
|
"fr-FR": "Super roulade",
|
||||||
|
"it-IT": "Atterraggio stiloso",
|
||||||
|
"ja-JP": "受け身術",
|
||||||
|
"ko-KR": "낙법",
|
||||||
|
"nl-NL": "Rolmodel",
|
||||||
|
"ru-RU": "Акробат",
|
||||||
|
"zh-CN": "受身术",
|
||||||
|
"zh-TW": "受身術"
|
||||||
|
},
|
||||||
|
"primary_only": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -1,11 +1,28 @@
|
||||||
import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
|
import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
|
||||||
|
|
||||||
export const AGENT_NAME = "splashcat / s3si.ts";
|
export const AGENT_NAME = "splashcat / s3si.ts";
|
||||||
export const AGENT_VERSION = "1.1.0";
|
export const AGENT_VERSION = "1.1.1";
|
||||||
export const S3SI_VERSION = "0.3.1";
|
export const S3SI_VERSION = "0.4.1";
|
||||||
export const COMBINED_VERSION = `${AGENT_VERSION}/${S3SI_VERSION}`;
|
export const COMBINED_VERSION = `${AGENT_VERSION}/${S3SI_VERSION}`;
|
||||||
export const NSOAPP_VERSION = "2.5.0";
|
export const NSOAPP_VERSION = "2.5.1";
|
||||||
export const WEB_VIEW_VERSION = "3.0.0-2857bc50";
|
export const WEB_VIEW_VERSION = "4.0.0-d5178440";
|
||||||
|
export enum Queries {
|
||||||
|
HomeQuery = "7dcc64ea27a08e70919893a0d3f70871",
|
||||||
|
LatestBattleHistoriesQuery = "0d90c7576f1916469b2ae69f64292c02",
|
||||||
|
RegularBattleHistoriesQuery = "3baef04b095ad8975ea679d722bc17de",
|
||||||
|
BankaraBattleHistoriesQuery = "0438ea6978ae8bd77c5d1250f4f84803",
|
||||||
|
XBattleHistoriesQuery = "6796e3cd5dc3ebd51864dc709d899fc5",
|
||||||
|
PrivateBattleHistoriesQuery = "8e5ae78b194264a6c230e262d069bd28",
|
||||||
|
VsHistoryDetailQuery = "9ee0099fbe3d8db2a838a75cf42856dd",
|
||||||
|
CoopHistoryQuery = "91b917becd2fa415890f5b47e15ffb15",
|
||||||
|
CoopHistoryDetailQuery = "379f0d9b78b531be53044bcac031b34b",
|
||||||
|
myOutfitCommonDataFilteringConditionQuery =
|
||||||
|
"d02ab22c9dccc440076055c8baa0fa7a",
|
||||||
|
myOutfitCommonDataEquipmentsQuery = "d29cd0c2b5e6bac90dd5b817914832f8",
|
||||||
|
HistoryRecordQuery = "d9246baf077b2a29b5f7aac321810a77",
|
||||||
|
ConfigureAnalyticsQuery = "f8ae00773cc412a50dd41a6d9a159ddd",
|
||||||
|
StageRecordQuery = "f08a932d533845dde86e674e03bbb7d3",
|
||||||
|
}
|
||||||
export const S3SI_LINK = "https://forgejo.catgirlin.space/catgirl/s3si.ts";
|
export const S3SI_LINK = "https://forgejo.catgirlin.space/catgirl/s3si.ts";
|
||||||
|
|
||||||
export const USERAGENT = `${AGENT_NAME}/(${COMBINED_VERSION}) (${S3SI_LINK})`;
|
export const USERAGENT = `${AGENT_NAME}/(${COMBINED_VERSION}) (${S3SI_LINK})`;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
import {
|
||||||
|
JSONRPCServer,
|
||||||
|
RPCResult,
|
||||||
|
S3SIService,
|
||||||
|
Service,
|
||||||
|
} from "./jsonrpc/mod.ts";
|
||||||
|
import { DenoIO } from "./jsonrpc/deno.ts";
|
||||||
|
import { loginSteps } from "./iksm.ts";
|
||||||
|
import { DEFAULT_ENV, Env } from "./env.ts";
|
||||||
|
import { Queue } from "./jsonrpc/channel.ts";
|
||||||
|
import { ExportOpts, Log } from "./jsonrpc/types.ts";
|
||||||
|
import { App } from "./app.ts";
|
||||||
|
import { InMemoryStateBackend, Profile, State } from "./state.ts";
|
||||||
|
import { MemoryCache } from "./cache.ts";
|
||||||
|
import { Splatnet3 } from "./splatnet3.ts";
|
||||||
|
|
||||||
|
class S3SIServiceImplement implements S3SIService, Service {
|
||||||
|
loginMap: Map<string, {
|
||||||
|
step1: (url: string) => void;
|
||||||
|
promise: Promise<string>;
|
||||||
|
}> = new Map();
|
||||||
|
loggerQueue: Queue<Log> = new Queue();
|
||||||
|
env: Env = {
|
||||||
|
prompts: {
|
||||||
|
promptLogin: () => {
|
||||||
|
return Promise.reject("Not implemented");
|
||||||
|
},
|
||||||
|
prompt: () => {
|
||||||
|
return Promise.reject("Not implemented");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logger: {
|
||||||
|
debug: (...msg) => this.loggerQueue.push({ level: "debug", msg }),
|
||||||
|
log: (...msg) => this.loggerQueue.push({ level: "log", msg }),
|
||||||
|
warn: (...msg) => this.loggerQueue.push({ level: "warn", msg }),
|
||||||
|
error: (...msg) => this.loggerQueue.push({ level: "error", msg }),
|
||||||
|
},
|
||||||
|
newFetcher: DEFAULT_ENV.newFetcher,
|
||||||
|
};
|
||||||
|
|
||||||
|
hello(): Promise<RPCResult<string>> {
|
||||||
|
return Promise.resolve({
|
||||||
|
result: "world",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loginSteps(): Promise<
|
||||||
|
RPCResult<{
|
||||||
|
authCodeVerifier: string;
|
||||||
|
url: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
loginSteps(step2: {
|
||||||
|
authCodeVerifier: string;
|
||||||
|
login: string;
|
||||||
|
}): Promise<
|
||||||
|
RPCResult<{ sessionToken: string }>
|
||||||
|
>;
|
||||||
|
async loginSteps(step2?: {
|
||||||
|
authCodeVerifier: string;
|
||||||
|
login: string;
|
||||||
|
}): Promise<
|
||||||
|
RPCResult<
|
||||||
|
{
|
||||||
|
authCodeVerifier: string;
|
||||||
|
url: string;
|
||||||
|
} | {
|
||||||
|
sessionToken: string;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
if (!step2) {
|
||||||
|
return {
|
||||||
|
result: await loginSteps(this.env),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
result: await loginSteps(this.env, step2),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async ensureTokenValid(state: State): Promise<
|
||||||
|
RPCResult<State>
|
||||||
|
> {
|
||||||
|
const stateBackend = new InMemoryStateBackend(state);
|
||||||
|
const profile = new Profile({ stateBackend, env: this.env });
|
||||||
|
await profile.readState();
|
||||||
|
const splatnet3 = new Splatnet3({ profile, env: this.env });
|
||||||
|
if (!await splatnet3.checkToken()) {
|
||||||
|
return {
|
||||||
|
error: {
|
||||||
|
code: 101,
|
||||||
|
message: "SessionToken is invalid",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
result: stateBackend.state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async getLogs(): Promise<RPCResult<Log[]>> {
|
||||||
|
const log = await this.loggerQueue.pop();
|
||||||
|
return {
|
||||||
|
result: log ? [log] : [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async run(state: State, opts: ExportOpts): Promise<RPCResult<State>> {
|
||||||
|
const stateBackend = new InMemoryStateBackend(state);
|
||||||
|
const app = new App({
|
||||||
|
...opts,
|
||||||
|
noProgress: true,
|
||||||
|
env: this.env,
|
||||||
|
profilePath: "",
|
||||||
|
stateBackend,
|
||||||
|
cache: new MemoryCache(),
|
||||||
|
});
|
||||||
|
await app.run();
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: stateBackend.state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
const service = new S3SIServiceImplement();
|
||||||
|
const server = new JSONRPCServer({
|
||||||
|
transport: new DenoIO({
|
||||||
|
reader: Deno.stdin,
|
||||||
|
writer: Deno.stdout,
|
||||||
|
}),
|
||||||
|
service,
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.serve();
|
||||||
|
}
|
||||||
|
|
@ -113,8 +113,9 @@ export class MongoDBExporter implements GameExporter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async exportStages(stages: RespMap[Queries.StageRecordQuery]["stageRecords"]["nodes"]): Promise<ExportResult> {
|
async exportStages(
|
||||||
|
stages: RespMap[Queries.StageRecordQuery]["stageRecords"]["nodes"],
|
||||||
|
): Promise<ExportResult> {
|
||||||
for (const stage of stages) {
|
for (const stage of stages) {
|
||||||
await this.mongoDb.collection("stages").updateOne({
|
await this.mongoDb.collection("stages").updateOne({
|
||||||
"stage.id": stage.id,
|
"stage.id": stage.id,
|
||||||
|
|
@ -127,6 +128,6 @@ export class MongoDBExporter implements GameExporter {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "success",
|
status: "success",
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import {
|
||||||
GameExporter,
|
GameExporter,
|
||||||
Image,
|
Image,
|
||||||
PlayerGear,
|
PlayerGear,
|
||||||
StatInkAbility,
|
|
||||||
StatInkCoopPlayer,
|
StatInkCoopPlayer,
|
||||||
StatInkCoopPostBody,
|
StatInkCoopPostBody,
|
||||||
StatInkCoopWave,
|
StatInkCoopWave,
|
||||||
|
|
@ -42,6 +41,7 @@ import {
|
||||||
urlSimplify,
|
urlSimplify,
|
||||||
} from "../utils.ts";
|
} from "../utils.ts";
|
||||||
import { Env } from "../env.ts";
|
import { Env } from "../env.ts";
|
||||||
|
import GEAR_MAP from "../assets/gear-map.json" assert { type: "json" };
|
||||||
|
|
||||||
const COOP_POINT_MAP: Record<number, number | undefined> = {
|
const COOP_POINT_MAP: Record<number, number | undefined> = {
|
||||||
0: -20,
|
0: -20,
|
||||||
|
|
@ -229,8 +229,7 @@ class StatInkAPI {
|
||||||
);
|
);
|
||||||
getWeapon = () =>
|
getWeapon = () =>
|
||||||
this._getCached<StatInkWeapon>(`${this.statInk}/api/v3/weapon?full=1`);
|
this._getCached<StatInkWeapon>(`${this.statInk}/api/v3/weapon?full=1`);
|
||||||
getAbility = () =>
|
getAbility = () => GEAR_MAP;
|
||||||
this._getCached<StatInkAbility>(`${this.statInk}/api/v3/ability?full=1`);
|
|
||||||
getStage = () =>
|
getStage = () =>
|
||||||
this._getCached<StatInkStage>(`${this.statInk}/api/v3/stage`);
|
this._getCached<StatInkStage>(`${this.statInk}/api/v3/stage`);
|
||||||
}
|
}
|
||||||
|
|
@ -331,6 +330,8 @@ export class StatInkExporter implements GameExporter {
|
||||||
}
|
}
|
||||||
} else if (vsMode === "X_MATCH") {
|
} else if (vsMode === "X_MATCH") {
|
||||||
return "xmatch";
|
return "xmatch";
|
||||||
|
} else if (vsMode === "LEAGUE") {
|
||||||
|
return "event";
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new TypeError(`Unknown vsMode ${vsMode}`);
|
throw new TypeError(`Unknown vsMode ${vsMode}`);
|
||||||
|
|
@ -422,6 +423,7 @@ export class StatInkExporter implements GameExporter {
|
||||||
myTeam,
|
myTeam,
|
||||||
otherTeams,
|
otherTeams,
|
||||||
bankaraMatch,
|
bankaraMatch,
|
||||||
|
leagueMatch,
|
||||||
festMatch,
|
festMatch,
|
||||||
playedTime,
|
playedTime,
|
||||||
} = vsDetail;
|
} = vsDetail;
|
||||||
|
|
@ -564,6 +566,10 @@ export class StatInkExporter implements GameExporter {
|
||||||
result.rank_after_s_plus = result.rank_before_s_plus;
|
result.rank_after_s_plus = result.rank_before_s_plus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (leagueMatch) {
|
||||||
|
result.event = leagueMatch.leagueMatchEvent?.id;
|
||||||
|
result.event_power = leagueMatch.myLeaguePower;
|
||||||
|
}
|
||||||
|
|
||||||
if (challengeProgress) {
|
if (challengeProgress) {
|
||||||
result.challenge_win = challengeProgress.winCount;
|
result.challenge_win = challengeProgress.winCount;
|
||||||
|
|
@ -724,6 +730,8 @@ export class StatInkExporter implements GameExporter {
|
||||||
golden_appearances: wave.goldenPopCount,
|
golden_appearances: wave.goldenPopCount,
|
||||||
golden_delivered: wave.teamDeliverCount,
|
golden_delivered: wave.teamDeliverCount,
|
||||||
special_uses,
|
special_uses,
|
||||||
|
// fill it later
|
||||||
|
danger_rate: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
async mapCoop(
|
async mapCoop(
|
||||||
|
|
@ -767,10 +775,13 @@ export class StatInkExporter implements GameExporter {
|
||||||
: undefined;
|
: undefined;
|
||||||
const title_exp_after = detail.afterGradePoint;
|
const title_exp_after = detail.afterGradePoint;
|
||||||
|
|
||||||
|
const maxWaves = detail.rule === "TEAM_CONTEST" ? 5 : 3;
|
||||||
let clear_waves: number;
|
let clear_waves: number;
|
||||||
if (waveResults.length > 0) {
|
if (waveResults.length > 0) {
|
||||||
// when cleared, resultWave === 0, so we need to add 1.
|
// when cleared, resultWave === 0, so we need to add 1.
|
||||||
clear_waves = waveResults.filter((i) => i.waveNumber < 4).length -
|
clear_waves = waveResults.filter((i) =>
|
||||||
|
i.waveNumber < maxWaves + 1
|
||||||
|
).length -
|
||||||
1 + (resultWave === 0 ? 1 : 0);
|
1 + (resultWave === 0 ? 1 : 0);
|
||||||
} else {
|
} else {
|
||||||
clear_waves = 0;
|
clear_waves = 0;
|
||||||
|
|
@ -811,7 +822,7 @@ export class StatInkExporter implements GameExporter {
|
||||||
|
|
||||||
let fail_reason: StatInkCoopPostBody["fail_reason"] = null;
|
let fail_reason: StatInkCoopPostBody["fail_reason"] = null;
|
||||||
// failed
|
// failed
|
||||||
if (clear_waves !== 3 && waveResults.length > 0) {
|
if (clear_waves !== maxWaves && waveResults.length > 0) {
|
||||||
const lastWave = waveResults[waveResults.length - 1];
|
const lastWave = waveResults[waveResults.length - 1];
|
||||||
if (lastWave.teamDeliverCount >= lastWave.deliverNorm) {
|
if (lastWave.teamDeliverCount >= lastWave.deliverNorm) {
|
||||||
fail_reason = "wipe_out";
|
fail_reason = "wipe_out";
|
||||||
|
|
@ -822,8 +833,9 @@ export class StatInkExporter implements GameExporter {
|
||||||
uuid: await gameId(detail.id),
|
uuid: await gameId(detail.id),
|
||||||
private: groupInfo?.mode === "PRIVATE_CUSTOM" ? "yes" : "no",
|
private: groupInfo?.mode === "PRIVATE_CUSTOM" ? "yes" : "no",
|
||||||
big_run: detail.rule === "BIG_RUN" ? "yes" : "no",
|
big_run: detail.rule === "BIG_RUN" ? "yes" : "no",
|
||||||
|
eggstra_work: detail.rule === "TEAM_CONTEST" ? "yes" : "no",
|
||||||
stage: b64Number(detail.coopStage.id).toString(),
|
stage: b64Number(detail.coopStage.id).toString(),
|
||||||
danger_rate: dangerRate * 100,
|
danger_rate: detail.rule === "TEAM_CONTEST" ? null : dangerRate * 100,
|
||||||
clear_waves,
|
clear_waves,
|
||||||
fail_reason,
|
fail_reason,
|
||||||
king_smell: smellMeter,
|
king_smell: smellMeter,
|
||||||
|
|
@ -856,6 +868,57 @@ export class StatInkExporter implements GameExporter {
|
||||||
automated: "yes",
|
automated: "yes",
|
||||||
start_at: startedAt,
|
start_at: startedAt,
|
||||||
};
|
};
|
||||||
|
// caculate wave danger_rate.
|
||||||
|
// translated from here: https://github.com/frozenpandaman/s3s/commit/d46ece00e5a7706688eaf025f18c5a8ea1c54c0f#diff-819571ec7b067d2398cd1f9dbc737160312efc4128ba4a2f0e165c70225dea0eR1050
|
||||||
|
if (detail.rule === "TEAM_CONTEST") {
|
||||||
|
let lastWave: StatInkCoopWave | undefined;
|
||||||
|
for (
|
||||||
|
const [wave] of result.waves
|
||||||
|
.map((p, i) => [p, i] as const)
|
||||||
|
) {
|
||||||
|
let haz_level: number;
|
||||||
|
if (!lastWave) {
|
||||||
|
haz_level = 60;
|
||||||
|
} else {
|
||||||
|
const num_players = result.players.length;
|
||||||
|
const quota = lastWave.golden_quota; // last wave, most recent one added to the list
|
||||||
|
const delivered = lastWave.golden_delivered;
|
||||||
|
let added_percent = 0; // default, no increase if less than 1.5x quota delivered
|
||||||
|
if (num_players == 4) {
|
||||||
|
if (delivered >= quota * 2) {
|
||||||
|
added_percent = 60;
|
||||||
|
} else if (delivered >= quota * 1.5) {
|
||||||
|
added_percent = 30;
|
||||||
|
}
|
||||||
|
} else if (num_players == 3) {
|
||||||
|
if (delivered >= quota * 2) {
|
||||||
|
added_percent = 40;
|
||||||
|
} else if (delivered >= quota * 1.5) {
|
||||||
|
added_percent = 20;
|
||||||
|
}
|
||||||
|
} else if (num_players == 2) {
|
||||||
|
if (delivered >= quota * 2) {
|
||||||
|
added_percent = 20;
|
||||||
|
} else if (delivered >= quota * 1.5) {
|
||||||
|
added_percent = 10;
|
||||||
|
added_percent = 5;
|
||||||
|
}
|
||||||
|
} else if (num_players == 1) {
|
||||||
|
if (delivered >= quota * 2) {
|
||||||
|
added_percent = 10;
|
||||||
|
} else if (delivered >= quota * 1.5) {
|
||||||
|
added_percent = 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev_percent = lastWave.danger_rate!;
|
||||||
|
|
||||||
|
haz_level = prev_percent + added_percent;
|
||||||
|
}
|
||||||
|
wave.danger_rate = haz_level;
|
||||||
|
lastWave = wave;
|
||||||
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
98
src/iksm.ts
|
|
@ -8,11 +8,42 @@ import {
|
||||||
import { APIError } from "./APIError.ts";
|
import { APIError } from "./APIError.ts";
|
||||||
import { Env, Fetcher } from "./env.ts";
|
import { Env, Fetcher } from "./env.ts";
|
||||||
|
|
||||||
export async function loginManually(
|
export async function loginSteps(
|
||||||
{ newFetcher, prompts: { promptLogin } }: Env,
|
env: Env,
|
||||||
): Promise<string> {
|
): Promise<
|
||||||
|
{
|
||||||
|
authCodeVerifier: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
export async function loginSteps(
|
||||||
|
env: Env,
|
||||||
|
step2: {
|
||||||
|
authCodeVerifier: string;
|
||||||
|
login: string;
|
||||||
|
},
|
||||||
|
): Promise<
|
||||||
|
{
|
||||||
|
sessionToken: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
export async function loginSteps(
|
||||||
|
{ newFetcher }: Env,
|
||||||
|
step2?: {
|
||||||
|
authCodeVerifier: string;
|
||||||
|
login: string;
|
||||||
|
},
|
||||||
|
): Promise<
|
||||||
|
{
|
||||||
|
authCodeVerifier: string;
|
||||||
|
url: string;
|
||||||
|
} | {
|
||||||
|
sessionToken: string;
|
||||||
|
}
|
||||||
|
> {
|
||||||
const fetch = newFetcher();
|
const fetch = newFetcher();
|
||||||
|
|
||||||
|
if (!step2) {
|
||||||
const state = urlBase64Encode(random(36));
|
const state = urlBase64Encode(random(36));
|
||||||
const authCodeVerifier = urlBase64Encode(random(32));
|
const authCodeVerifier = urlBase64Encode(random(32));
|
||||||
const authCvHash = await crypto.subtle.digest(
|
const authCvHash = await crypto.subtle.digest(
|
||||||
|
|
@ -51,10 +82,12 @@ export async function loginManually(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const login = (await promptLogin(res.url)).trim();
|
return {
|
||||||
if (!login) {
|
authCodeVerifier,
|
||||||
throw new Error("No login URL provided");
|
url: res.url,
|
||||||
}
|
};
|
||||||
|
} else {
|
||||||
|
const { login, authCodeVerifier } = step2;
|
||||||
const loginURL = new URL(login);
|
const loginURL = new URL(login);
|
||||||
const params = new URLSearchParams(loginURL.hash.substring(1));
|
const params = new URLSearchParams(loginURL.hash.substring(1));
|
||||||
const sessionTokenCode = params.get("session_token_code");
|
const sessionTokenCode = params.get("session_token_code");
|
||||||
|
|
@ -71,7 +104,27 @@ export async function loginManually(
|
||||||
throw new Error("No session token found");
|
throw new Error("No session token found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessionToken;
|
return { sessionToken };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginManually(
|
||||||
|
env: Env,
|
||||||
|
): Promise<string> {
|
||||||
|
const { prompts: { promptLogin } } = env;
|
||||||
|
|
||||||
|
const step1 = await loginSteps(env);
|
||||||
|
|
||||||
|
const { url, authCodeVerifier } = step1;
|
||||||
|
|
||||||
|
const login = (await promptLogin(url)).trim();
|
||||||
|
if (!login) {
|
||||||
|
throw new Error("No login URL provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
const step2 = await loginSteps(env, { authCodeVerifier, login });
|
||||||
|
|
||||||
|
return step2.sessionToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGToken(
|
export async function getGToken(
|
||||||
|
|
@ -122,13 +175,14 @@ export async function getGToken(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const uiRespJson = await uiResp.json();
|
const uiRespJson = await uiResp.json();
|
||||||
const { nickname, birthday, language, country } = uiRespJson;
|
const { nickname, birthday, language, country, id: userId } = uiRespJson;
|
||||||
|
|
||||||
const getIdToken2 = async (idToken: string) => {
|
const getIdToken2 = async (idToken: string) => {
|
||||||
const { f, request_id: requestId, timestamp } = await callImink({
|
const { f, request_id: requestId, timestamp } = await callImink({
|
||||||
fApi,
|
fApi,
|
||||||
step: 1,
|
step: 1,
|
||||||
idToken,
|
idToken,
|
||||||
|
userId,
|
||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
const resp = await fetch.post(
|
const resp = await fetch.post(
|
||||||
|
|
@ -157,23 +211,28 @@ export async function getGToken(
|
||||||
);
|
);
|
||||||
const respJson = await resp.json();
|
const respJson = await resp.json();
|
||||||
|
|
||||||
const idToken2 = respJson?.result?.webApiServerCredential?.accessToken;
|
const idToken2: string = respJson?.result?.webApiServerCredential
|
||||||
|
?.accessToken;
|
||||||
|
const coralUserId: number = respJson?.result?.user?.id;
|
||||||
|
|
||||||
if (!idToken2) {
|
if (!idToken2 || !coralUserId) {
|
||||||
throw new APIError({
|
throw new APIError({
|
||||||
response: resp,
|
response: resp,
|
||||||
json: respJson,
|
json: respJson,
|
||||||
message: "No idToken2 found",
|
message:
|
||||||
|
`No idToken2 or coralUserId found. Please try again later. ('${idToken2}', '${coralUserId}')`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return idToken2 as string;
|
return [idToken2, coralUserId] as const;
|
||||||
};
|
};
|
||||||
const getGToken = async (idToken: string) => {
|
const getGToken = async (idToken: string, coralUserId: number) => {
|
||||||
const { f, request_id: requestId, timestamp } = await callImink({
|
const { f, request_id: requestId, timestamp } = await callImink({
|
||||||
step: 2,
|
step: 2,
|
||||||
idToken,
|
idToken,
|
||||||
fApi,
|
fApi,
|
||||||
|
userId,
|
||||||
|
coralUserId,
|
||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
const resp = await fetch.post(
|
const resp = await fetch.post(
|
||||||
|
|
@ -213,8 +272,8 @@ export async function getGToken(
|
||||||
return webServiceToken as string;
|
return webServiceToken as string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const idToken2 = await retry(() => getIdToken2(idToken));
|
const [idToken2, coralUserId] = await retry(() => getIdToken2(idToken));
|
||||||
const webServiceToken = await retry(() => getGToken(idToken2));
|
const webServiceToken = await retry(() => getGToken(idToken2, coralUserId));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
webServiceToken,
|
webServiceToken,
|
||||||
|
|
@ -350,13 +409,16 @@ type IminkResponse = {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
};
|
};
|
||||||
async function callImink(
|
async function callImink(
|
||||||
{ fApi, step, idToken, env }: {
|
params: {
|
||||||
fApi: string;
|
fApi: string;
|
||||||
step: number;
|
step: number;
|
||||||
idToken: string;
|
idToken: string;
|
||||||
|
userId: string;
|
||||||
|
coralUserId?: number;
|
||||||
env: Env;
|
env: Env;
|
||||||
},
|
},
|
||||||
): Promise<IminkResponse> {
|
): Promise<IminkResponse> {
|
||||||
|
const { fApi, step, idToken, userId, coralUserId, env } = params;
|
||||||
const { post } = env.newFetcher();
|
const { post } = env.newFetcher();
|
||||||
const resp = await post({
|
const resp = await post({
|
||||||
url: fApi,
|
url: fApi,
|
||||||
|
|
@ -367,6 +429,8 @@ async function callImink(
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
"token": idToken,
|
"token": idToken,
|
||||||
"hash_method": step,
|
"hash_method": step,
|
||||||
|
"na_id": userId,
|
||||||
|
"coral_user_id": coralUserId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { IPC } from "./stdio.ts";
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
/// <reference lib="deno.ns" />
|
||||||
|
|
||||||
|
import { io, writeAll } from "../../deps.ts";
|
||||||
|
import type { ExtractType } from "./types.ts";
|
||||||
|
|
||||||
|
export class IPC<T extends { type: string }> {
|
||||||
|
lines: AsyncIterableIterator<string>;
|
||||||
|
writer: Deno.Writer;
|
||||||
|
constructor({ reader, writer }: {
|
||||||
|
reader: Deno.Reader;
|
||||||
|
writer: Deno.Writer;
|
||||||
|
}) {
|
||||||
|
this.lines = io.readLines(reader);
|
||||||
|
this.writer = writer;
|
||||||
|
}
|
||||||
|
async recvType<K extends T["type"]>(
|
||||||
|
type: K,
|
||||||
|
): Promise<ExtractType<T, K>> {
|
||||||
|
const data = await this.recv();
|
||||||
|
if (data.type !== type) {
|
||||||
|
throw new Error(`Unexpected type: ${data.type}`);
|
||||||
|
}
|
||||||
|
return data as ExtractType<T, K>;
|
||||||
|
}
|
||||||
|
async recv(): Promise<T> {
|
||||||
|
const result = await this.lines.next();
|
||||||
|
|
||||||
|
if (!result.done) {
|
||||||
|
return JSON.parse(result.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("EOF");
|
||||||
|
}
|
||||||
|
async send(data: T) {
|
||||||
|
await writeAll(
|
||||||
|
this.writer,
|
||||||
|
new TextEncoder().encode(JSON.stringify(data) + "\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
export type Command = {
|
||||||
|
type: "hello";
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtractType<T extends { type: string }, K extends T["type"]> =
|
||||||
|
Extract<
|
||||||
|
T,
|
||||||
|
{ type: K }
|
||||||
|
>;
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
export class Queue<T> {
|
||||||
|
private queue: T[] = [];
|
||||||
|
private waiting: ((value: T | undefined) => void)[] = [];
|
||||||
|
|
||||||
|
pop = (): Promise<T | undefined> => {
|
||||||
|
return new Promise<T | undefined>((resolve) => {
|
||||||
|
const data = this.queue.shift();
|
||||||
|
if (data) {
|
||||||
|
resolve(data);
|
||||||
|
} else {
|
||||||
|
this.waiting.push(resolve);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// TODO: wait until the data is queued if queue has limit
|
||||||
|
push = (data: T): Promise<void> => {
|
||||||
|
const waiting = this.waiting.shift();
|
||||||
|
if (waiting) {
|
||||||
|
waiting(data);
|
||||||
|
} else {
|
||||||
|
this.queue.push(data);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
close = (): Promise<void> => {
|
||||||
|
for (const resolve of this.waiting) {
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function channel<T>() {
|
||||||
|
const q1 = new Queue<T>();
|
||||||
|
const q2 = new Queue<T>();
|
||||||
|
const close = async () => {
|
||||||
|
await q1.close();
|
||||||
|
await q2.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return [{
|
||||||
|
send: q1.push,
|
||||||
|
recv: q2.pop,
|
||||||
|
close,
|
||||||
|
}, {
|
||||||
|
send: q2.push,
|
||||||
|
recv: q1.pop,
|
||||||
|
close,
|
||||||
|
}] as const;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
// deno-lint-ignore-file no-explicit-any
|
||||||
|
import {
|
||||||
|
ID,
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
ResponseError,
|
||||||
|
RPCResult,
|
||||||
|
Service,
|
||||||
|
Transport,
|
||||||
|
} from "./types.ts";
|
||||||
|
|
||||||
|
export class JSONRPCClient<S extends Service> {
|
||||||
|
protected nextId = 1;
|
||||||
|
protected transport: Transport;
|
||||||
|
protected requestMap: Map<
|
||||||
|
ID,
|
||||||
|
(result: RPCResult<any, ResponseError>) => void
|
||||||
|
> = new Map();
|
||||||
|
protected fatal: unknown = undefined;
|
||||||
|
protected task: Promise<void>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{ transport }: { transport: Transport },
|
||||||
|
) {
|
||||||
|
this.transport = transport;
|
||||||
|
this.task = this.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected setFatal(e: unknown) {
|
||||||
|
if (!this.fatal) {
|
||||||
|
this.fatal = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleResponse(
|
||||||
|
resp: Response<unknown, ResponseError>,
|
||||||
|
) {
|
||||||
|
const { id } = resp;
|
||||||
|
const callback = this.requestMap.get(id);
|
||||||
|
if (callback) {
|
||||||
|
this.requestMap.delete(id);
|
||||||
|
callback(resp);
|
||||||
|
} else {
|
||||||
|
this.setFatal(new Error("invalid response id: " + String(id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive response from server
|
||||||
|
protected async run() {
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const data = await this.transport.recv();
|
||||||
|
if (data === undefined) {
|
||||||
|
this.setFatal(new Error("transport closed"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const result = JSON.parse(data);
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
for (const resp of result) {
|
||||||
|
this.handleResponse(resp);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.handleResponse(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.setFatal(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeRequest<
|
||||||
|
K extends keyof S & string,
|
||||||
|
P extends Parameters<S[K]>,
|
||||||
|
>(
|
||||||
|
method: K,
|
||||||
|
params: P,
|
||||||
|
): Request<K, P> {
|
||||||
|
const req = {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: this.nextId,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
} as const;
|
||||||
|
this.nextId += 1;
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
async call<
|
||||||
|
K extends keyof S & string,
|
||||||
|
P extends Parameters<S[K]>,
|
||||||
|
R extends ReturnType<S[K]>,
|
||||||
|
>(
|
||||||
|
method: K,
|
||||||
|
...params: P
|
||||||
|
): Promise<R> {
|
||||||
|
if (this.fatal) {
|
||||||
|
throw this.fatal;
|
||||||
|
}
|
||||||
|
const req = this.makeRequest(method, params);
|
||||||
|
await this.transport.send(JSON.stringify(req));
|
||||||
|
|
||||||
|
return new Promise<R>((res, rej) => {
|
||||||
|
this.requestMap.set(req.id, (result) => {
|
||||||
|
if (result.error) {
|
||||||
|
rej(result.error);
|
||||||
|
} else {
|
||||||
|
res(result.result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getProxy(): S {
|
||||||
|
const proxy = new Proxy({}, {
|
||||||
|
get: (_, method: string) => {
|
||||||
|
return (...params: unknown[]) => {
|
||||||
|
return this.call(method, ...params as any);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return proxy as S;
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
await this.transport.close();
|
||||||
|
await this.task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { io, writeAll } from "../../deps.ts";
|
||||||
|
import { Transport } from "./types.ts";
|
||||||
|
|
||||||
|
export class DenoIO implements Transport {
|
||||||
|
lines: AsyncIterableIterator<string>;
|
||||||
|
writer: Deno.Writer & Deno.Closer;
|
||||||
|
constructor({ reader, writer }: {
|
||||||
|
reader: Deno.Reader;
|
||||||
|
writer: Deno.Writer & Deno.Closer;
|
||||||
|
}) {
|
||||||
|
this.lines = io.readLines(reader);
|
||||||
|
this.writer = writer;
|
||||||
|
}
|
||||||
|
async recv(): Promise<string | undefined> {
|
||||||
|
const result = await this.lines.next();
|
||||||
|
|
||||||
|
if (!result.done) {
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
async send(data: string) {
|
||||||
|
await writeAll(
|
||||||
|
this.writer,
|
||||||
|
new TextEncoder().encode(data + "\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async close() {
|
||||||
|
await this.writer.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { channel } from "./channel.ts";
|
||||||
|
import { JSONRPCClient } from "./client.ts";
|
||||||
|
import { JSONRPCServer } from "./server.ts";
|
||||||
|
import { RPCResult, Service } from "./types.ts";
|
||||||
|
import { assertEquals } from "../../dev_deps.ts";
|
||||||
|
|
||||||
|
export interface SimpleService {
|
||||||
|
add(a: number, b: number): Promise<
|
||||||
|
RPCResult<number>
|
||||||
|
>;
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SimpleServiceImplement implements SimpleService, Service {
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
async add(a: number, b: number): Promise<RPCResult<number>> {
|
||||||
|
return {
|
||||||
|
result: a + b,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test("jsonrpc", async () => {
|
||||||
|
const [c1, c2] = channel<string>();
|
||||||
|
|
||||||
|
const service = new SimpleServiceImplement();
|
||||||
|
const server = new JSONRPCServer({
|
||||||
|
transport: c1,
|
||||||
|
service,
|
||||||
|
});
|
||||||
|
const serverTask = server.serve().catch((e) => console.error(e));
|
||||||
|
const client = new JSONRPCClient<SimpleService>({
|
||||||
|
transport: c2,
|
||||||
|
});
|
||||||
|
const p = client.getProxy();
|
||||||
|
assertEquals((await p.add(1, 2)).result, 3);
|
||||||
|
|
||||||
|
await client.close();
|
||||||
|
await server.close();
|
||||||
|
await serverTask;
|
||||||
|
});
|
||||||