diff --git a/gui/src-tauri/tauri.conf.json b/gui/src-tauri/tauri.conf.json index a45fc1c..a36a35f 100644 --- a/gui/src-tauri/tauri.conf.json +++ b/gui/src-tauri/tauri.conf.json @@ -85,11 +85,11 @@ "resizable": true, "title": "s3si.ts", "width": 400, - "height": 300, + "height": 400, "minWidth": 320, - "minHeight": 300, + "minHeight": 400, "visible": false } ] } -} +} \ No newline at end of file diff --git a/gui/src/components/Checkbox.tsx b/gui/src/components/Checkbox.tsx new file mode 100644 index 0000000..ee99e70 --- /dev/null +++ b/gui/src/components/Checkbox.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +type CheckboxProps = { + children?: React.ReactNode + value?: boolean + onChange?: (value: boolean) => void +} + +export const Checkbox: React.FC = ({ value, onChange, children }) => { + return
+ +
+} diff --git a/gui/src/components/RunPanel.tsx b/gui/src/components/RunPanel.tsx new file mode 100644 index 0000000..ac991b8 --- /dev/null +++ b/gui/src/components/RunPanel.tsx @@ -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 = () => { + 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 + } + + 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 <> + {t('导出对战数据')} + {t('导出打工数据')} + + +} diff --git a/gui/src/jsonrpc/types.ts b/gui/src/jsonrpc/types.ts index bbd45ba..7388171 100644 --- a/gui/src/jsonrpc/types.ts +++ b/gui/src/jsonrpc/types.ts @@ -1 +1 @@ -export * from '../../../src/jsonrpc/types'; +export type * from '../../../src/jsonrpc/types'; diff --git a/gui/src/pages/Guide.tsx b/gui/src/pages/Guide.tsx index 8a61a62..78bfe31 100644 --- a/gui/src/pages/Guide.tsx +++ b/gui/src/pages/Guide.tsx @@ -1,3 +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
+ {/*
    + {steps.map(({ title }, i) =>
  • {title}
  • )} +
*/} + {Content && } +
+ + +
+
+} + +const LoginNintendoAccount: React.FC<{ onChange: (v: StepState) => void }> = ({ onChange }) => { + const { t } = useTranslation(); + + return
+ +
+} + export const Guide: React.FC = () => { - return <> -} \ No newline at end of file + const { t } = useTranslation(); + + + const steps: Step[] = [{ + title: t('登录任天堂帐号'), + element: LoginNintendoAccount, + }, { + title: t('填写stat.ink API密钥'), + element: () => <>, + }, { + title: t('完成'), + element: () => <>, + }] + + return
+
+ +
+} diff --git a/gui/src/pages/Home.tsx b/gui/src/pages/Home.tsx index 225f52a..f27dc69 100644 --- a/gui/src/pages/Home.tsx +++ b/gui/src/pages/Home.tsx @@ -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 = () => {

{t('欢迎!')}

- {t('配置')} + + {t('配置')} {t('前往 stat.ink')} - {t('检查更新')}
diff --git a/gui/src/services/config.ts b/gui/src/services/config.ts index 0ab006b..3d8ed76 100644 --- a/gui/src/services/config.ts +++ b/gui/src/services/config.ts @@ -58,3 +58,7 @@ export async function getProfile(index: number): Promise { 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 +} diff --git a/gui/src/services/s3si.ts b/gui/src/services/s3si.ts index eedee76..6520740 100644 --- a/gui/src/services/s3si.ts +++ b/gui/src/services/s3si.ts @@ -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({ 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; +} diff --git a/src/daemon.ts b/src/daemon.ts index 8df4af8..8b9e3d1 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -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 void; promise: Promise; }> = new Map(); - loggerQueue: Queue<{ level: LoggerLevel; msg: unknown[] }> = new Queue(); + loggerQueue: Queue = 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> { + const log = await this.loggerQueue.pop(); + return { + result: log ? [log] : [], + }; + } + async run(state: State, opts: ExportOpts): Promise> { + 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; } diff --git a/src/jsonrpc/channel.ts b/src/jsonrpc/channel.ts index d1ef783..63e3153 100644 --- a/src/jsonrpc/channel.ts +++ b/src/jsonrpc/channel.ts @@ -1,6 +1,6 @@ export class Queue { - queue: T[] = []; - waiting: ((value: T | undefined) => void)[] = []; + private queue: T[] = []; + private waiting: ((value: T | undefined) => void)[] = []; pop = (): Promise => { return new Promise((resolve) => { diff --git a/src/jsonrpc/types.ts b/src/jsonrpc/types.ts index e6c310b..c47cded 100644 --- a/src/jsonrpc/types.ts +++ b/src/jsonrpc/types.ts @@ -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>; + run(state: State, opts: ExportOpts): Promise>; // deno-lint-ignore no-explicit-any [key: string]: any; }