-
+
+
}
diff --git a/gui/src/pages/Settings.tsx b/gui/src/pages/Settings.tsx
index 725849b..7665d37 100644
--- a/gui/src/pages/Settings.tsx
+++ b/gui/src/pages/Settings.tsx
@@ -1,16 +1,17 @@
-import { ErrorContent } from 'components/ErrorContent';
+import { ErrorContent, FallbackComponent } from 'components/ErrorContent';
import { Loading } from 'components/Loading';
-import { usePromise, usePromiseLazy } from 'hooks/usePromise';
-import React, { useState } from 'react'
+import React, { Suspense, 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 { Config, Profile } from 'services/config';
+import clsx from 'clsx';
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';
+import useSWRMutation from 'swr/mutation'
+import { useService, useServiceMutation } from 'services/useService';
+import { ErrorBoundary } from 'react-error-boundary';
const STAT_INK_KEY_LENGTH = 43;
@@ -57,6 +58,8 @@ const Form: React.FC<{
const { t, i18n } = useTranslation();
const [value, setValue] = useState(oldValue);
const { subField } = useSubField({ value, onChange: setValue });
+ const { trigger: setProfile } = useServiceMutation('profile', 0)
+ const { trigger: setConfig } = useServiceMutation('config')
const changed = JSON.stringify(value) !== JSON.stringify(oldValue);
@@ -64,12 +67,12 @@ const Form: React.FC<{
const statInkApiKey = subField('profile.state.statInkApiKey')
const splatnet3Lang = subField('profile.state.userLang')
- const [onSave, { loading, error }] = usePromiseLazy(async () => {
- await setProfile(0, value.profile);
+ const { trigger: onSave, isMutating: loading, error } = useSWRMutation('saveSettings', async () => {
+ await setProfile(value.profile);
await setConfig(value.config);
onSaved?.();
})
- const [onLogin, loginState] = usePromiseLazy(async () => {
+ const loginState = useSWRMutation('login', async () => {
const result = await login();
if (!result) {
return;
@@ -86,11 +89,11 @@ const Form: React.FC<{
{t('Nintendo Account 会话令牌')}
-
+
}>
+
+
+
}
diff --git a/gui/src/services/config.ts b/gui/src/services/config.ts
index 0022ab9..039f16d 100644
--- a/gui/src/services/config.ts
+++ b/gui/src/services/config.ts
@@ -1,6 +1,6 @@
import { fs } from "@tauri-apps/api"
import { appConfigDir, join } from '@tauri-apps/api/path'
-import { State } from '../../../src/state';
+import type { State } from '../../../src/state';
const configFile = appConfigDir().then(c => join(c, 'config.json'));
const profileDir = appConfigDir().then(c => join(c, 'profile'));
@@ -9,8 +9,7 @@ export type Profile = {
state: State,
}
-export type Config = {
-}
+export type Config = Record
// TODO: import from state.ts.
const DEFAULT_STATE: State = {
diff --git a/gui/src/services/s3si.tsx b/gui/src/services/s3si.tsx
index 70e4176..9ae5175 100644
--- a/gui/src/services/s3si.tsx
+++ b/gui/src/services/s3si.tsx
@@ -9,6 +9,7 @@ const client = new JSONRPCClient({
const LOG_SUB = new Set<(logs: Log[]) => void>();
async function getLogs() {
+ // eslint-disable-next-line no-constant-condition
while (true) {
const r = await client.getLogs()
@@ -57,7 +58,7 @@ export const useLog = () => {
return useContext(LOG_CONTEXT);
}
-function renderMsg(i: any) {
+function renderMsg(i: unknown) {
if (i instanceof Error) {
return i.message
}
diff --git a/gui/src/services/useService.ts b/gui/src/services/useService.ts
new file mode 100644
index 0000000..aaab455
--- /dev/null
+++ b/gui/src/services/useService.ts
@@ -0,0 +1,37 @@
+import useSWR, { Key, SWRResponse } from 'swr'
+import useSWRMutation, { SWRMutationResponse } from 'swr/mutation'
+import { getConfig, getProfile, setConfig, setProfile } from './config'
+
+const SERVICES = {
+ profile: {
+ fetcher: getProfile,
+ updater: setProfile,
+ },
+ config: {
+ fetcher: getConfig,
+ updater: setConfig,
+ },
+} as const
+
+export type Services = keyof typeof SERVICES
+
+export const useService = (service: S, ...args: Parameters<(typeof SERVICES)[S]['fetcher']>): SWRResponse<
+ Awaited>
+> => {
+ // @ts-expect-error TypeScript can not infer type here
+ return useSWR(['service', service, ...args], () => SERVICES[service].fetcher(...args), { suspense: true })
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type RemoveLastParamters any> = T extends (...args: [...infer P, any]) => any ? P : never;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type LastParamter any> = T extends (...args: [...infer _, infer P]) => any ? P : never;
+export const useServiceMutation = (service: S, ...args: RemoveLastParamters<(typeof SERVICES)[S]['updater']>): SWRMutationResponse<
+ Awaited>,
+ Error,
+ Key,
+ LastParamter<(typeof SERVICES)[S]['updater']>
+> => {
+ // @ts-expect-error TypeScript can not infer type here
+ return useSWRMutation(['service', service, ...args], (_, { arg }) => SERVICES[service].updater(...args, arg))
+}
diff --git a/gui/src/utils/composeLoadable.ts b/gui/src/utils/composeLoadable.ts
deleted file mode 100644
index 40e0a04..0000000
--- a/gui/src/utils/composeLoadable.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export type Loadable = {
- loading: boolean;
- result?: T;
- error?: any;
- retry?: () => void;
-}
-
-export function composeLoadable>>(map: T): Loadable<{
- [P in keyof T]: T[P] extends Loadable ? 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 };
-}
diff --git a/src/ipc/mod.ts b/src/ipc/mod.ts
deleted file mode 100644
index f0cfe10..0000000
--- a/src/ipc/mod.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { IPC } from "./stdio.ts";
diff --git a/src/ipc/stdio.ts b/src/ipc/stdio.ts
deleted file mode 100644
index eaabbd2..0000000
--- a/src/ipc/stdio.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-///
-
-import { io, writeAll } from "../../deps.ts";
-import type { ExtractType } from "./types.ts";
-
-export class IPC {
- lines: AsyncIterableIterator;
- writer: Deno.Writer;
- constructor({ reader, writer }: {
- reader: Deno.Reader;
- writer: Deno.Writer;
- }) {
- this.lines = io.readLines(reader);
- this.writer = writer;
- }
- async recvType(
- type: K,
- ): Promise> {
- const data = await this.recv();
- if (data.type !== type) {
- throw new Error(`Unexpected type: ${data.type}`);
- }
- return data as ExtractType;
- }
- async recv(): Promise {
- 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"),
- );
- }
-}
diff --git a/src/ipc/types.ts b/src/ipc/types.ts
deleted file mode 100644
index c19944d..0000000
--- a/src/ipc/types.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export type Command = {
- type: "hello";
- data: string;
-};
-
-export type ExtractType =
- Extract<
- T,
- { type: K }
- >;
diff --git a/src/jsonrpc/deno.ts b/src/jsonrpc/deno.ts
index 3d3d235..80f9e83 100644
--- a/src/jsonrpc/deno.ts
+++ b/src/jsonrpc/deno.ts
@@ -3,13 +3,13 @@ import { Transport } from "./types.ts";
export class DenoIO implements Transport {
lines: AsyncIterableIterator;
- writer: WritableStream;
+ writer: WritableStreamDefaultWriter;
constructor({ reader, writer }: {
reader: ReadableStream;
writer: WritableStream;
}) {
this.lines = readLines(reader);
- this.writer = writer;
+ this.writer = writer.getWriter();
}
async recv(): Promise {
const result = await this.lines.next();
@@ -21,13 +21,8 @@ export class DenoIO implements Transport {
return undefined;
}
async send(data: string) {
- const writer = this.writer.getWriter();
- try {
- await writer.ready;
- await writer.write(new TextEncoder().encode(data + "\n"));
- } finally {
- writer.releaseLock();
- }
+ await this.writer.ready;
+ await this.writer.write(new TextEncoder().encode(data + "\n"));
}
async close() {
await this.writer.close();