feat: add basic export
parent
16a4546710
commit
77c621b499
|
|
@ -85,9 +85,9 @@
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "s3si.ts",
|
"title": "s3si.ts",
|
||||||
"width": 400,
|
"width": 400,
|
||||||
"height": 300,
|
"height": 400,
|
||||||
"minWidth": 320,
|
"minWidth": 320,
|
||||||
"minHeight": 300,
|
"minHeight": 400,
|
||||||
"visible": false
|
"visible": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
type CheckboxProps = {
|
||||||
|
children?: React.ReactNode
|
||||||
|
value?: boolean
|
||||||
|
onChange?: (value: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checkbox: React.FC<CheckboxProps> = ({ 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} onChange={() => onChange?.(!value)} className="checkbox" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { usePromise } from 'hooks/usePromise';
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { canExport, getProfile, setProfile } from 'services/config';
|
||||||
|
import { run } 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 {
|
||||||
|
const { state } = result;
|
||||||
|
const newState = await run(state, {
|
||||||
|
exporter: "stat.ink,file",
|
||||||
|
monitor: false,
|
||||||
|
withSummary: false,
|
||||||
|
skipMode: exportBattle === false ? 'vs' : exportCoop === false ? 'coop' : undefined,
|
||||||
|
});
|
||||||
|
await setProfile(0, {
|
||||||
|
...result,
|
||||||
|
state: newState,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<Checkbox value={exportBattle} onChange={setExportBattle}>{t('导出对战数据')}</Checkbox>
|
||||||
|
<Checkbox value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={classNames('btn', {
|
||||||
|
'btn-disabled': !canExport(result) || (!exportBattle && !exportCoop),
|
||||||
|
'loading': loading,
|
||||||
|
})}
|
||||||
|
>{t('导出')}</button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
export * from '../../../src/jsonrpc/types';
|
export type * from '../../../src/jsonrpc/types';
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,75 @@
|
||||||
export const Guide: React.FC = () => {
|
import classNames from 'classnames';
|
||||||
return <></>
|
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('登录任天堂帐号'),
|
||||||
|
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>
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { CheckUpdate } from 'components/CheckUpdate';
|
|
||||||
import { ErrorContent } from 'components/ErrorContent';
|
import { ErrorContent } from 'components/ErrorContent';
|
||||||
import { Loading } from 'components/Loading';
|
import { Loading } from 'components/Loading';
|
||||||
|
import { RunPanel } from 'components/RunPanel';
|
||||||
import { STAT_INK } from 'constant';
|
import { STAT_INK } from 'constant';
|
||||||
import { usePromise } from 'hooks/usePromise';
|
import { usePromise } from 'hooks/usePromise';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
@ -32,9 +32,9 @@ export const Home: React.FC = () => {
|
||||||
<div className='full-card'>
|
<div className='full-card'>
|
||||||
<h1 className='mb-4'>{t('欢迎!')}</h1>
|
<h1 className='mb-4'>{t('欢迎!')}</h1>
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
<Link to='/settings' className='btn btn-primary'>{t('配置')}</Link>
|
<RunPanel />
|
||||||
|
<Link to='/settings' className='btn'>{t('配置')}</Link>
|
||||||
<a className='btn' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
|
<a className='btn' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
|
||||||
<CheckUpdate className='btn'>{t('检查更新')}</CheckUpdate>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -58,3 +58,7 @@ export async function getProfile(index: number): Promise<Profile> {
|
||||||
export async function setProfile(index: number, profile: Profile) {
|
export async function setProfile(index: number, profile: Profile) {
|
||||||
await fs.writeTextFile(await profileDir.then(c => join(c, `${index}.json`)), JSON.stringify(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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,40 @@
|
||||||
import { invoke } from "@tauri-apps/api";
|
import { invoke } from "@tauri-apps/api";
|
||||||
import { JSONRPCClient, S3SIService, StdioTransport } from "jsonrpc";
|
import { JSONRPCClient, S3SIService, StdioTransport } from "jsonrpc";
|
||||||
|
import { ExportOpts, State } from "jsonrpc/types";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
const client = new JSONRPCClient<S3SIService>({
|
const client = new JSONRPCClient<S3SIService>({
|
||||||
transport: new StdioTransport()
|
transport: new StdioTransport()
|
||||||
}).getProxy();
|
}).getProxy();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getLogs()
|
||||||
|
|
||||||
export const useLogin = () => {
|
export const useLogin = () => {
|
||||||
const login = useCallback(async () => {
|
const login = useCallback(async () => {
|
||||||
const result = await client.loginSteps();
|
const result = await client.loginSteps();
|
||||||
|
|
@ -35,3 +64,11 @@ export const useLogin = () => {
|
||||||
login
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,17 @@ import { DenoIO } from "./jsonrpc/deno.ts";
|
||||||
import { loginSteps } from "./iksm.ts";
|
import { loginSteps } from "./iksm.ts";
|
||||||
import { DEFAULT_ENV, Env } from "./env.ts";
|
import { DEFAULT_ENV, Env } from "./env.ts";
|
||||||
import { Queue } from "./jsonrpc/channel.ts";
|
import { Queue } from "./jsonrpc/channel.ts";
|
||||||
|
import { ExportOpts, Log } from "./jsonrpc/types.ts";
|
||||||
enum LoggerLevel {
|
import { App } from "./app.ts";
|
||||||
Debug = "debug",
|
import { InMemoryStateBackend, State } from "./state.ts";
|
||||||
Log = "log",
|
import { MemoryCache } from "./cache.ts";
|
||||||
Warn = "warn",
|
|
||||||
Error = "error",
|
|
||||||
}
|
|
||||||
|
|
||||||
class S3SIServiceImplement implements S3SIService, Service {
|
class S3SIServiceImplement implements S3SIService, Service {
|
||||||
loginMap: Map<string, {
|
loginMap: Map<string, {
|
||||||
step1: (url: string) => void;
|
step1: (url: string) => void;
|
||||||
promise: Promise<string>;
|
promise: Promise<string>;
|
||||||
}> = new Map();
|
}> = new Map();
|
||||||
loggerQueue: Queue<{ level: LoggerLevel; msg: unknown[] }> = new Queue();
|
loggerQueue: Queue<Log> = new Queue();
|
||||||
env: Env = {
|
env: Env = {
|
||||||
prompts: {
|
prompts: {
|
||||||
promptLogin: () => {
|
promptLogin: () => {
|
||||||
|
|
@ -32,12 +29,10 @@ class S3SIServiceImplement implements S3SIService, Service {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
logger: {
|
logger: {
|
||||||
debug: (...msg) =>
|
debug: (...msg) => this.loggerQueue.push({ level: "debug", msg }),
|
||||||
this.loggerQueue.push({ level: LoggerLevel.Debug, msg }),
|
log: (...msg) => this.loggerQueue.push({ level: "log", msg }),
|
||||||
log: (...msg) => this.loggerQueue.push({ level: LoggerLevel.Log, msg }),
|
warn: (...msg) => this.loggerQueue.push({ level: "warn", msg }),
|
||||||
warn: (...msg) => this.loggerQueue.push({ level: LoggerLevel.Warn, msg }),
|
error: (...msg) => this.loggerQueue.push({ level: "error", msg }),
|
||||||
error: (...msg) =>
|
|
||||||
this.loggerQueue.push({ level: LoggerLevel.Error, msg }),
|
|
||||||
},
|
},
|
||||||
newFetcher: DEFAULT_ENV.newFetcher,
|
newFetcher: DEFAULT_ENV.newFetcher,
|
||||||
};
|
};
|
||||||
|
|
@ -76,6 +71,28 @@ class S3SIServiceImplement implements S3SIService, Service {
|
||||||
result: await loginSteps(this.env, step2),
|
result: await loginSteps(this.env, step2),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
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
|
// deno-lint-ignore no-explicit-any
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export class Queue<T> {
|
export class Queue<T> {
|
||||||
queue: T[] = [];
|
private queue: T[] = [];
|
||||||
waiting: ((value: T | undefined) => void)[] = [];
|
private waiting: ((value: T | undefined) => void)[] = [];
|
||||||
|
|
||||||
pop = (): Promise<T | undefined> => {
|
pop = (): Promise<T | undefined> => {
|
||||||
return new Promise<T | undefined>((resolve) => {
|
return new Promise<T | undefined>((resolve) => {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,34 @@
|
||||||
|
export type LoginState = {
|
||||||
|
sessionToken?: string;
|
||||||
|
gToken?: string;
|
||||||
|
bulletToken?: string;
|
||||||
|
};
|
||||||
|
export type RankState = {
|
||||||
|
// generated by gameId(battle.id)
|
||||||
|
gameId: string;
|
||||||
|
// extract from battle.id
|
||||||
|
timestamp?: number;
|
||||||
|
// C-, B, A+, S, S+0, S+12
|
||||||
|
rank: string;
|
||||||
|
rankPoint: number;
|
||||||
|
};
|
||||||
|
export type State = {
|
||||||
|
loginState?: LoginState;
|
||||||
|
fGen: string;
|
||||||
|
appUserAgent?: string;
|
||||||
|
userLang?: string;
|
||||||
|
userCountry?: string;
|
||||||
|
|
||||||
|
rankState?: RankState;
|
||||||
|
|
||||||
|
cacheDir: string;
|
||||||
|
|
||||||
|
// Exporter config
|
||||||
|
statInkApiKey?: string;
|
||||||
|
fileExportPath: string;
|
||||||
|
monitorInterval: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ID = string | number | null;
|
export type ID = string | number | null;
|
||||||
|
|
||||||
// deno-lint-ignore no-explicit-any
|
// deno-lint-ignore no-explicit-any
|
||||||
|
|
@ -68,6 +99,20 @@ export const ERROR_INTERNAL_ERROR: ResponseError<-32603> = {
|
||||||
message: "Internal error",
|
message: "Internal error",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LoggerLevel = "debug" | "log" | "warn" | "error";
|
||||||
|
|
||||||
|
export type Log = {
|
||||||
|
level: LoggerLevel;
|
||||||
|
msg: unknown[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExportOpts = {
|
||||||
|
exporter: string;
|
||||||
|
monitor: boolean;
|
||||||
|
withSummary: boolean;
|
||||||
|
skipMode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface S3SIService {
|
export interface S3SIService {
|
||||||
loginSteps(): Promise<
|
loginSteps(): Promise<
|
||||||
RPCResult<
|
RPCResult<
|
||||||
|
|
@ -87,6 +132,8 @@ export interface S3SIService {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
|
getLogs(): Promise<RPCResult<Log[]>>;
|
||||||
|
run(state: State, opts: ExportOpts): Promise<RPCResult<State>>;
|
||||||
// deno-lint-ignore no-explicit-any
|
// deno-lint-ignore no-explicit-any
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue