feat: add basic export
parent
16a4546710
commit
77c621b499
|
|
@ -85,9 +85,9 @@
|
|||
"resizable": true,
|
||||
"title": "s3si.ts",
|
||||
"width": 400,
|
||||
"height": 300,
|
||||
"height": 400,
|
||||
"minWidth": 320,
|
||||
"minHeight": 300,
|
||||
"minHeight": 400,
|
||||
"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 = () => {
|
||||
return <></>
|
||||
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('登录任天堂帐号'),
|
||||
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 { Loading } from 'components/Loading';
|
||||
import { RunPanel } from 'components/RunPanel';
|
||||
import { STAT_INK } from 'constant';
|
||||
import { usePromise } from 'hooks/usePromise';
|
||||
import React from 'react'
|
||||
|
|
@ -32,9 +32,9 @@ export const Home: React.FC = () => {
|
|||
<div className='full-card'>
|
||||
<h1 className='mb-4'>{t('欢迎!')}</h1>
|
||||
<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>
|
||||
<CheckUpdate className='btn'>{t('检查更新')}</CheckUpdate>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -58,3 +58,7 @@ export async function getProfile(index: number): Promise<Profile> {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,40 @@
|
|||
import { invoke } from "@tauri-apps/api";
|
||||
import { JSONRPCClient, S3SIService, StdioTransport } from "jsonrpc";
|
||||
import { ExportOpts, State } from "jsonrpc/types";
|
||||
import { useCallback } from "react";
|
||||
|
||||
const client = new JSONRPCClient<S3SIService>({
|
||||
transport: new StdioTransport()
|
||||
}).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 = () => {
|
||||
const login = useCallback(async () => {
|
||||
const result = await client.loginSteps();
|
||||
|
|
@ -35,3 +64,11 @@ export const useLogin = () => {
|
|||
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 { DEFAULT_ENV, Env } from "./env.ts";
|
||||
import { Queue } from "./jsonrpc/channel.ts";
|
||||
|
||||
enum LoggerLevel {
|
||||
Debug = "debug",
|
||||
Log = "log",
|
||||
Warn = "warn",
|
||||
Error = "error",
|
||||
}
|
||||
import { ExportOpts, Log } from "./jsonrpc/types.ts";
|
||||
import { App } from "./app.ts";
|
||||
import { InMemoryStateBackend, State } from "./state.ts";
|
||||
import { MemoryCache } from "./cache.ts";
|
||||
|
||||
class S3SIServiceImplement implements S3SIService, Service {
|
||||
loginMap: Map<string, {
|
||||
step1: (url: string) => void;
|
||||
promise: Promise<string>;
|
||||
}> = new Map();
|
||||
loggerQueue: Queue<{ level: LoggerLevel; msg: unknown[] }> = new Queue();
|
||||
loggerQueue: Queue<Log> = new Queue();
|
||||
env: Env = {
|
||||
prompts: {
|
||||
promptLogin: () => {
|
||||
|
|
@ -32,12 +29,10 @@ class S3SIServiceImplement implements S3SIService, Service {
|
|||
},
|
||||
},
|
||||
logger: {
|
||||
debug: (...msg) =>
|
||||
this.loggerQueue.push({ level: LoggerLevel.Debug, msg }),
|
||||
log: (...msg) => this.loggerQueue.push({ level: LoggerLevel.Log, msg }),
|
||||
warn: (...msg) => this.loggerQueue.push({ level: LoggerLevel.Warn, msg }),
|
||||
error: (...msg) =>
|
||||
this.loggerQueue.push({ level: LoggerLevel.Error, msg }),
|
||||
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,
|
||||
};
|
||||
|
|
@ -76,6 +71,28 @@ class S3SIServiceImplement implements S3SIService, Service {
|
|||
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
|
||||
[key: string]: any;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export class Queue<T> {
|
||||
queue: T[] = [];
|
||||
waiting: ((value: T | undefined) => void)[] = [];
|
||||
private queue: T[] = [];
|
||||
private waiting: ((value: T | undefined) => void)[] = [];
|
||||
|
||||
pop = (): Promise<T | undefined> => {
|
||||
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;
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
|
|
@ -68,6 +99,20 @@ export const ERROR_INTERNAL_ERROR: ResponseError<-32603> = {
|
|||
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 {
|
||||
loginSteps(): Promise<
|
||||
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
|
||||
[key: string]: any;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue