diff --git a/package.json b/package.json index 3544f9f..8b9c130 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dev": "cross-env NODE_ENV=development vite", "build": "cross-env NODE_ENV=production tsc && vite build", "lint": "prettier --check src/**/*.{ts,tsx} && eslint --ext .ts,.tsx --ignore-path .gitignore .", - "lint:fix": "prettier --fix src/**/*.{ts,tsx} && eslint --fix --ext .ts,.tsx --ignore-path .gitignore ." + "lint:fix": "prettier --write src/**/*.{ts,tsx} && eslint --fix --ext .ts,.tsx --ignore-path .gitignore ." }, "dependencies": { "@emotion/react": "^11.11.0", diff --git a/src/App.tsx b/src/App.tsx index ffde550..a139cb3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,22 @@ -import React from 'react' -import { RouterProvider } from 'react-router-dom' -import { QueryClientProvider } from '@tanstack/react-query' -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' -import queryClient from '@/query-client' -import router from '@/router' -import Theme from '@/theme' +import React from "react"; +import { RouterProvider } from "react-router-dom"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import queryClient from "@/query-client"; +import router from "@/router"; +import Theme from "@/theme"; -export default function App () { +export default function App() { // HACK: Disable context menu in production React.useEffect(() => { if (import.meta.env.PROD) { - const listener = (evt: Event) => evt.preventDefault() - document.addEventListener('contextmenu', listener) - return () => { document.removeEventListener('contextmenu', listener) } + const listener = (evt: Event) => evt.preventDefault(); + document.addEventListener("contextmenu", listener); + return () => { + document.removeEventListener("contextmenu", listener); + }; } - }, []) + }, []); return ( @@ -23,5 +25,5 @@ export default function App () { - ) + ); } diff --git a/src/ErrorPage.tsx b/src/ErrorPage.tsx index e96a498..6282f10 100644 --- a/src/ErrorPage.tsx +++ b/src/ErrorPage.tsx @@ -1,20 +1,22 @@ -import React from 'react' -import { useRouteError } from 'react-router-dom' -import Box from '@mui/material/Box' -import Typography from '@mui/material/Typography' +import React from "react"; +import { useRouteError } from "react-router-dom"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; -export default function ErrorPage () { - const error = useRouteError() as any - console.error(error) +export default function ErrorPage() { + const error = useRouteError(); + console.error(error); // TODO: Error page return ( Oops! - Sorry, an unexpected error has occurred. + + Sorry, an unexpected error has occurred. + {error.stack || error.statusText || error.message} - ) + ); } diff --git a/src/components/Content.tsx b/src/components/Content.tsx index c977639..23a8c1b 100644 --- a/src/components/Content.tsx +++ b/src/components/Content.tsx @@ -1,10 +1,10 @@ -import React from 'react' -import Box from '@mui/material/Box' +import React from "react"; +import Box from "@mui/material/Box"; -export default function Content (props: React.PropsWithChildren) { +export default function Content(props: React.PropsWithChildren) { return ( {props.children} - ) + ); } diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index a9b87c1..855d910 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,37 +1,47 @@ -import React from 'react' -import Stack from '@mui/material/Stack' -import AppBar from '@mui/material/AppBar' -import Toolbar from '@mui/material/Toolbar' -import Typography from '@mui/material/Typography' -import { SidebarWidth } from '@/components/Sidebar' +import React from "react"; +import Stack from "@mui/material/Stack"; +import AppBar from "@mui/material/AppBar"; +import Toolbar from "@mui/material/Toolbar"; +import Typography from "@mui/material/Typography"; +import { SidebarWidth } from "@/components/Sidebar"; export interface LayoutProps { - title?: React.ReactNode - navbar?: React.ReactNode + title?: React.ReactNode; + navbar?: React.ReactNode; } -export default function Layout (props: React.PropsWithChildren) { +export default function Layout(props: React.PropsWithChildren) { return ( - - {props.navbar} - + {props.navbar} {props.children} - ) + ); } -function Navbar (props: React.PropsWithChildren<{ title?: React.ReactNode }>) { +function Navbar(props: React.PropsWithChildren<{ title?: React.ReactNode }>) { return ( - theme.palette.divider, - paddingLeft: SidebarWidth - }}> - + theme.palette.divider, + paddingLeft: SidebarWidth, + }} + > + {props.title && ( {props.title} @@ -42,5 +52,5 @@ function Navbar (props: React.PropsWithChildren<{ title?: React.ReactNode }>) { - ) + ); } diff --git a/src/components/account/AccountAvatar.tsx b/src/components/account/AccountAvatar.tsx index 6f7340a..f65d637 100644 --- a/src/components/account/AccountAvatar.tsx +++ b/src/components/account/AccountAvatar.tsx @@ -1,28 +1,26 @@ -import React from 'react' -import Avatar, { AvatarProps } from '@mui/material/Avatar' -import { AccountFacet } from '@/interfaces/account' -import AvatarGenshinLumine from '@/assets/images/genshin/UI_AvatarIcon_PlayerGirl.png' -import AvatarGenshinAether from '@/assets/images/genshin/UI_AvatarIcon_PlayerBoy.png' -import AvatarStarRailTrailblazer from '@/assets/images/starrail/Trailblazer.png' +import React from "react"; +import Avatar, { AvatarProps } from "@mui/material/Avatar"; +import { AccountFacet } from "@/interfaces/account"; +import AvatarGenshinLumine from "@/assets/images/genshin/UI_AvatarIcon_PlayerGirl.png"; +// import AvatarGenshinAether from "@/assets/images/genshin/UI_AvatarIcon_PlayerBoy.png"; +import AvatarStarRailTrailblazer from "@/assets/images/starrail/Trailblazer.png"; -export interface AccountAvatarProps extends Omit { - facet: AccountFacet +export interface AccountAvatarProps extends Omit { + facet: AccountFacet; } -export default function AccountAvatar (props: AccountAvatarProps) { - const { facet, ...rest } = props +export default function AccountAvatar(props: AccountAvatarProps) { + const { facet, ...rest } = props; const src = React.useMemo(() => { switch (facet) { case AccountFacet.Genshin: - return AvatarGenshinLumine + return AvatarGenshinLumine; case AccountFacet.StarRail: - return AvatarStarRailTrailblazer + return AvatarStarRailTrailblazer; default: - return undefined + return undefined; } - }, [facet]) + }, [facet]); - return ( - - ) + return ; } diff --git a/src/components/common/Version.tsx b/src/components/common/Version.tsx index 7f4b9e7..cc942ac 100644 --- a/src/components/common/Version.tsx +++ b/src/components/common/Version.tsx @@ -10,7 +10,7 @@ export default function Version (props: TypographyProps) { {version.data ? formatVersion(version.data) : __APP_VERSION__} - ) + ); } function formatVersion (version: CurrentVersion): string { diff --git a/src/components/gacha/GachaLayoutContext.ts b/src/components/gacha/GachaLayoutContext.ts index 2d7b26a..5dfb59a 100644 --- a/src/components/gacha/GachaLayoutContext.ts +++ b/src/components/gacha/GachaLayoutContext.ts @@ -1,22 +1,28 @@ -import React from 'react' -import { AccountFacet, Account } from '@/interfaces/account' -import { GachaRecords } from '@/hooks/useGachaRecordsQuery' +import React from "react"; +import { AccountFacet, Account } from "@/interfaces/account"; +import { GachaRecords } from "@/hooks/useGachaRecordsQuery"; export interface GachaLayoutContextValue { - facet: AccountFacet - selectedAccount: Account - gachaRecords: GachaRecords - alert (error: Error | string | undefined | null | unknown, message?: string): void + facet: AccountFacet; + selectedAccount: Account; + gachaRecords: GachaRecords; + alert( + error: Error | string | undefined | null | unknown, + message?: string + ): void; } -export const GachaLayoutContext = - React.createContext(undefined) +export const GachaLayoutContext = React.createContext< + GachaLayoutContextValue | undefined +>(undefined); -export function useGachaLayoutContext () { - const context = React.useContext(GachaLayoutContext) +export function useGachaLayoutContext() { + const context = React.useContext(GachaLayoutContext); if (!context) { - throw new Error('useGachaLayoutContext must be used within a GachaLayoutContext.Provider') + throw new Error( + "useGachaLayoutContext must be used within a GachaLayoutContext.Provider" + ); } else { - return context + return context; } } diff --git a/src/components/gacha/analysis/index.tsx b/src/components/gacha/analysis/index.tsx index 08bda27..555ff88 100644 --- a/src/components/gacha/analysis/index.tsx +++ b/src/components/gacha/analysis/index.tsx @@ -1,13 +1,13 @@ -import React from 'react' -import GachaAnalysisSum from '@/components/gacha/analysis/GachaAnalysisSum' -import GachaAnalysisHistory from '@/components/gacha/analysis/GachaAnalysisHistory' -import Stack from '@mui/material/Stack' +import React from "react"; +import GachaAnalysisSum from "@/components/gacha/analysis/GachaAnalysisSum"; +import GachaAnalysisHistory from "@/components/gacha/analysis/GachaAnalysisHistory"; +import Stack from "@mui/material/Stack"; -export default function GachaAnalysis () { +export default function GachaAnalysis() { return ( - ) + ); } diff --git a/src/components/gacha/overview/index.tsx b/src/components/gacha/overview/index.tsx index 25e39bc..60c896a 100644 --- a/src/components/gacha/overview/index.tsx +++ b/src/components/gacha/overview/index.tsx @@ -1,15 +1,15 @@ -import React from 'react' -import GachaOverviewLastUpdated from '@/components/gacha/overview/GachaOverviewLastUpdated' -import GachaOverviewGrid from '@/components/gacha/overview/GachaOverviewGrid' -import GachaOverviewTooltips from '@/components/gacha/overview/GachaOverviewTooltips' -import Stack from '@mui/material/Stack' +import React from "react"; +import GachaOverviewLastUpdated from "@/components/gacha/overview/GachaOverviewLastUpdated"; +import GachaOverviewGrid from "@/components/gacha/overview/GachaOverviewGrid"; +import GachaOverviewTooltips from "@/components/gacha/overview/GachaOverviewTooltips"; +import Stack from "@mui/material/Stack"; -export default function GachaOverview () { +export default function GachaOverview() { return ( - ) + ); } diff --git a/src/components/gacha/toolbar/GachaActionTabs.tsx b/src/components/gacha/toolbar/GachaActionTabs.tsx index 8deaafe..17000d7 100644 --- a/src/components/gacha/toolbar/GachaActionTabs.tsx +++ b/src/components/gacha/toolbar/GachaActionTabs.tsx @@ -1,29 +1,33 @@ -import React from 'react' -import Box from '@mui/material/Box' -import Tabs, { TabsProps } from '@mui/material/Tabs' -import Tab from '@mui/material/Tab' -import { styled, alpha } from '@mui/material/styles' +import React from "react"; +import Box from "@mui/material/Box"; +import Tabs, { TabsProps } from "@mui/material/Tabs"; +import Tab from "@mui/material/Tab"; +import { styled, alpha } from "@mui/material/styles"; export interface GachaActionTabsProps { - tabs: string[] - value: number - onChange?: TabsProps['onChange'] + tabs: string[]; + value: number; + onChange?: TabsProps["onChange"]; } -export default function GachaActionTabs (props: GachaActionTabsProps) { +export default function GachaActionTabs(props: GachaActionTabsProps) { return ( - theme.palette.action.hover - }}> + theme.palette.action.hover, + }} + > {props.tabs.map((label, i) => ( ))} - ) + ); } const GachaActionTabsItem = styled((props: { label: string }) => ( @@ -33,15 +37,18 @@ const GachaActionTabsItem = styled((props: { label: string }) => ( minHeight: 0, height: 36, padding: theme.spacing(0, 2), - '&:first-of-type': { + "&:first-of-type": { borderTopLeftRadius: 2, - borderBottomLeftRadius: 2 + borderBottomLeftRadius: 2, }, - '&:last-of-type': { + "&:last-of-type": { borderTopRightRadius: 2, - borderBottomRightRadius: 2 + borderBottomRightRadius: 2, }, - '&.Mui-selected': { - background: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity + 0.05) - } -})) + "&.Mui-selected": { + background: alpha( + theme.palette.primary.main, + theme.palette.action.selectedOpacity + 0.05 + ), + }, +})); diff --git a/src/components/gacha/toolbar/index.tsx b/src/components/gacha/toolbar/index.tsx index 4e42c8e..b494c55 100644 --- a/src/components/gacha/toolbar/index.tsx +++ b/src/components/gacha/toolbar/index.tsx @@ -1,19 +1,21 @@ -import React from 'react' -import { AccountFacet } from '@/interfaces/account' -import GachaActionUrl from '@/components/gacha/toolbar/GachaActionUrl' -import GachaActionFetch from '@/components/gacha/toolbar/GachaActionFetch' -import GachaActionImport from '@/components/gacha/toolbar/GachaActionImport' -import GachaActionExport from '@/components/gacha/toolbar/GachaActionExport' -import GachaActionTabs, { GachaActionTabsProps } from '@/components/gacha/toolbar/GachaActionTabs' -import Stack from '@mui/material/Stack' +import React from "react"; +import { AccountFacet } from "@/interfaces/account"; +import GachaActionUrl from "@/components/gacha/toolbar/GachaActionUrl"; +import GachaActionFetch from "@/components/gacha/toolbar/GachaActionFetch"; +import GachaActionImport from "@/components/gacha/toolbar/GachaActionImport"; +import GachaActionExport from "@/components/gacha/toolbar/GachaActionExport"; +import GachaActionTabs, { + GachaActionTabsProps, +} from "@/components/gacha/toolbar/GachaActionTabs"; +import Stack from "@mui/material/Stack"; export interface GachaToolbarProps { - facet: AccountFacet - ActionTabsProps: GachaActionTabsProps + facet: AccountFacet; + ActionTabsProps: GachaActionTabsProps; } -export default function GachaToolbar (props: GachaToolbarProps) { - const { ActionTabsProps } = props +export default function GachaToolbar(props: GachaToolbarProps) { + const { ActionTabsProps } = props; return ( @@ -26,5 +28,5 @@ export default function GachaToolbar (props: GachaToolbarProps) { - ) + ); } diff --git a/src/hooks/useGachaRecordsFetcher.ts b/src/hooks/useGachaRecordsFetcher.ts index 234e48b..98e7069 100644 --- a/src/hooks/useGachaRecordsFetcher.ts +++ b/src/hooks/useGachaRecordsFetcher.ts @@ -1,56 +1,60 @@ -import React from 'react' -import { event } from '@tauri-apps/api' -import { useImmer } from 'use-immer' -import { GenshinGachaRecord, StarRailGachaRecord } from '@/interfaces/gacha' -import PluginGacha from '@/utilities/plugin-gacha' +import React from "react"; +import { event } from "@tauri-apps/api"; +import { useImmer } from "use-immer"; +import { GenshinGachaRecord, StarRailGachaRecord } from "@/interfaces/gacha"; +import PluginGacha from "@/utilities/plugin-gacha"; type Fragment = - 'sleeping' | - { ready: string } | - { pagination: number } | - { data: Array } | - 'finished' + | "sleeping" + | { ready: string } + | { pagination: number } + | { data: Array } + | "finished"; -export default function useGachaRecordsFetcher () { +export default function useGachaRecordsFetcher() { const [{ fragments, current }, produceState] = useImmer<{ - fragments: Fragment[] - current: 'idle' | Fragment + fragments: Fragment[]; + current: "idle" | Fragment; }>({ fragments: [], - current: 'idle' - }) + current: "idle", + }); - const pull = React.useCallback(async ( - ...args: Parameters - ) => { - // reset state - produceState((draft) => { - draft.fragments = [] - draft.current = 'idle' - }) - - const [,, { eventChannel }] = args - try { - const unlisten = await event.listen(eventChannel, ({ payload }) => { - produceState((draft) => { - draft.fragments.push(payload) - draft.current = payload - }) - }) + const pull = React.useCallback( + async (...args: Parameters) => { + // reset state + produceState((draft) => { + draft.fragments = []; + draft.current = "idle"; + }); + const [, , { eventChannel }] = args; try { - await PluginGacha.pullAllGachaRecords(...args) - } finally { - unlisten() + const unlisten = await event.listen( + eventChannel, + ({ payload }) => { + produceState((draft) => { + draft.fragments.push(payload); + draft.current = payload; + }); + } + ); + + try { + await PluginGacha.pullAllGachaRecords(...args); + } finally { + unlisten(); + } + } catch (error) { + return Promise.reject(error); } - } catch (error) { - return Promise.reject(error) - } - }, [produceState]) + }, + [produceState] + ); return { fragments, currentFragment: current, - pull - } + pull, + }; } diff --git a/src/hooks/useStatefulAccount.tsx b/src/hooks/useStatefulAccount.tsx index 7f5fd1e..a9d315f 100644 --- a/src/hooks/useStatefulAccount.tsx +++ b/src/hooks/useStatefulAccount.tsx @@ -1,81 +1,110 @@ -import React from 'react' -import { produce } from 'immer' -import { LoaderFunction } from 'react-router-dom' -import { QueryClient, QueryKey, FetchQueryOptions, useQuery, useQueryClient } from '@tanstack/react-query' -import { AccountFacet, Account } from '@/interfaces/account' -import PluginStorage, { AccountUid, CreateAccountPayload } from '@/utilities/plugin-storage' +import React from "react"; +import { produce } from "immer"; +import { LoaderFunction } from "react-router-dom"; +import { + QueryClient, + QueryKey, + FetchQueryOptions, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { AccountFacet, Account } from "@/interfaces/account"; +import PluginStorage, { + AccountUid, + CreateAccountPayload, +} from "@/utilities/plugin-storage"; export interface StatefulAccount { - readonly facet: AccountFacet - readonly accounts: Record - readonly selectedAccountUid: AccountUid | null + readonly facet: AccountFacet; + readonly accounts: Record; + readonly selectedAccountUid: AccountUid | null; } -export const StatefulAccountContext = - React.createContext(undefined) +export const StatefulAccountContext = React.createContext< + StatefulAccount | undefined +>(undefined); -StatefulAccountContext.displayName = 'StatefulAccountContext' +StatefulAccountContext.displayName = "StatefulAccountContext"; /// Query -const QueryPrefix = 'statefulAccount' +const QueryPrefix = "statefulAccount"; -const SelectedAccountUidKey = 'selectedAccountUid' +const SelectedAccountUidKey = "selectedAccountUid"; const LocalStorageSelectedAccountUid = Object.freeze({ - get (facet: AccountFacet) { return localStorage.getItem(`${QueryPrefix}:${facet}:${SelectedAccountUidKey}`) }, - set (facet: AccountFacet, uid: AccountUid) { localStorage.setItem(`${QueryPrefix}:${facet}:${SelectedAccountUidKey}`, uid) }, - remove (facet: AccountFacet) { localStorage.removeItem(`${QueryPrefix}:${facet}:${SelectedAccountUidKey}`) } -}) - -const statefulAccountQueryFn: FetchQueryOptions['queryFn'] = async (context) => { - const [, facet] = context.queryKey as [string, AccountFacet] - const accounts = (await PluginStorage - .findAccounts(facet)) - .reduce((acc, account) => { - acc[account.uid] = account - return acc - }, {} as StatefulAccount['accounts']) - - let selectedAccountUid = LocalStorageSelectedAccountUid.get(facet) - if (selectedAccountUid && !accounts[selectedAccountUid]) { - console.warn(`LocalStorage contains invalid ${SelectedAccountUidKey} ${selectedAccountUid} for ${facet} facet.`) - LocalStorageSelectedAccountUid.remove(facet as AccountFacet) - - // HACK: Auto-select first valid account - selectedAccountUid = Object.keys(accounts)[0] ?? null - if (selectedAccountUid) { - console.warn(`Auto-selecting first account ${selectedAccountUid} for ${facet} facet.`) - LocalStorageSelectedAccountUid.set(facet, selectedAccountUid) + get(facet: AccountFacet) { + return localStorage.getItem( + `${QueryPrefix}:${facet}:${SelectedAccountUidKey}` + ); + }, + set(facet: AccountFacet, uid: AccountUid) { + localStorage.setItem( + `${QueryPrefix}:${facet}:${SelectedAccountUidKey}`, + uid + ); + }, + remove(facet: AccountFacet) { + localStorage.removeItem(`${QueryPrefix}:${facet}:${SelectedAccountUidKey}`); + }, +}); + +const statefulAccountQueryFn: FetchQueryOptions["queryFn"] = + async (context) => { + const [, facet] = context.queryKey as [string, AccountFacet]; + const accounts = (await PluginStorage.findAccounts(facet)).reduce( + (acc, account) => { + acc[account.uid] = account; + return acc; + }, + {} as StatefulAccount["accounts"] + ); + + let selectedAccountUid = LocalStorageSelectedAccountUid.get(facet); + if (selectedAccountUid && !accounts[selectedAccountUid]) { + console.warn( + `LocalStorage contains invalid ${SelectedAccountUidKey} ${selectedAccountUid} for ${facet} facet.` + ); + LocalStorageSelectedAccountUid.remove(facet as AccountFacet); + + // HACK: Auto-select first valid account + selectedAccountUid = Object.keys(accounts)[0] ?? null; + if (selectedAccountUid) { + console.warn( + `Auto-selecting first account ${selectedAccountUid} for ${facet} facet.` + ); + LocalStorageSelectedAccountUid.set(facet, selectedAccountUid); + } } - } - return { - facet, - accounts, - selectedAccountUid - } as StatefulAccount -} + return { + facet, + accounts, + selectedAccountUid, + } as StatefulAccount; + }; -function createQuery (facet: AccountFacet): FetchQueryOptions & { queryKey: QueryKey } { +function createQuery( + facet: AccountFacet +): FetchQueryOptions & { queryKey: QueryKey } { return { queryKey: [QueryPrefix, facet], queryFn: statefulAccountQueryFn, - staleTime: Infinity - } + staleTime: Infinity, + }; } /// Loader -export function createStatefulAccountLoader ( +export function createStatefulAccountLoader( facet: AccountFacet ): (queryClient: QueryClient) => LoaderFunction { return (queryClient) => async () => { - const query = createQuery(facet) + const query = createQuery(facet); return ( queryClient.getQueryData(query.queryKey) ?? (await queryClient.fetchQuery(query)) - ) - } + ); + }; } /// Hook @@ -83,13 +112,13 @@ export function createStatefulAccountLoader ( export const withStatefulAccount = ( facet: AccountFacet, Wrapped: React.ComponentType<{ - facet: AccountFacet - accounts: StatefulAccount['accounts'] | null - selectedAccountUid: StatefulAccount['selectedAccountUid'] | null + facet: AccountFacet; + accounts: StatefulAccount["accounts"] | null; + selectedAccountUid: StatefulAccount["selectedAccountUid"] | null; }> ) => { - return function withStatefulAccountHOC () { - const statefulAccount = useStatefulAccountQuery(facet) + return function withStatefulAccountHOC() { + const statefulAccount = useStatefulAccountQuery(facet); return ( - ) - } -} + ); + }; +}; -export function useStatefulAccountQuery (facet: AccountFacet) { - const query = createQuery(facet) +export function useStatefulAccountQuery(facet: AccountFacet) { + const query = createQuery(facet); return useQuery({ ...query, - refetchOnWindowFocus: false - }) + refetchOnWindowFocus: false, + }); } -export function useStatefulAccountContext () { - const context = React.useContext(StatefulAccountContext) +export function useStatefulAccountContext() { + const context = React.useContext(StatefulAccountContext); if (!context) { - throw new Error('useStatefulAccountContext must be used within a StatefulAccountContext.Provider') + throw new Error( + "useStatefulAccountContext must be used within a StatefulAccountContext.Provider" + ); } else { - return context + return context; } } // Hook Fn -export function useSetSelectedAccountFn () { - const queryClient = useQueryClient() - const statefulAccount = useStatefulAccountContext() - - return React.useCallback((uid: AccountUid) => { - const account = statefulAccount.accounts[uid] - if (!account) throw new Error(`Account ${uid} not found.`) - - LocalStorageSelectedAccountUid.set(statefulAccount.facet, uid) - queryClient.setQueryData([QueryPrefix, statefulAccount.facet], (prev) => { - return prev && produce(prev, (draft) => { - draft.selectedAccountUid = account.uid - }) - }) - }, [queryClient, statefulAccount]) +export function useSetSelectedAccountFn() { + const queryClient = useQueryClient(); + const statefulAccount = useStatefulAccountContext(); + + return React.useCallback( + (uid: AccountUid) => { + const account = statefulAccount.accounts[uid]; + if (!account) throw new Error(`Account ${uid} not found.`); + + LocalStorageSelectedAccountUid.set(statefulAccount.facet, uid); + queryClient.setQueryData( + [QueryPrefix, statefulAccount.facet], + (prev) => { + return ( + prev && + produce(prev, (draft) => { + draft.selectedAccountUid = account.uid; + }) + ); + } + ); + }, + [queryClient, statefulAccount] + ); } -export function useCreateAccountFn () { - const queryClient = useQueryClient() - const statefulAccount = useStatefulAccountContext() - - return React.useCallback(async (payload: Omit) => { - const selectedAccountUid = statefulAccount.selectedAccountUid - const account = await PluginStorage.createAccount({ ...payload, facet: statefulAccount.facet }) - - if (!selectedAccountUid) LocalStorageSelectedAccountUid.set(statefulAccount.facet, account.uid) - queryClient.setQueryData([QueryPrefix, statefulAccount.facet], (prev) => { - return prev && produce(prev, (draft) => { - draft.accounts[account.uid] = account - draft.selectedAccountUid = selectedAccountUid ?? account.uid - }) - }) - }, [queryClient, statefulAccount]) +export function useCreateAccountFn() { + const queryClient = useQueryClient(); + const statefulAccount = useStatefulAccountContext(); + + return React.useCallback( + async (payload: Omit) => { + const selectedAccountUid = statefulAccount.selectedAccountUid; + const account = await PluginStorage.createAccount({ + ...payload, + facet: statefulAccount.facet, + }); + + if (!selectedAccountUid) + LocalStorageSelectedAccountUid.set(statefulAccount.facet, account.uid); + queryClient.setQueryData( + [QueryPrefix, statefulAccount.facet], + (prev) => { + return ( + prev && + produce(prev, (draft) => { + draft.accounts[account.uid] = account; + draft.selectedAccountUid = selectedAccountUid ?? account.uid; + }) + ); + } + ); + }, + [queryClient, statefulAccount] + ); } -function createUseUpdateAccountFn (caller: (...args: Args) => Promise) { +function createUseUpdateAccountFn( + caller: (...args: Args) => Promise +) { return function () { - const queryClient = useQueryClient() - const statefulAccount = useStatefulAccountContext() - - return React.useCallback(async (...args: Args) => { - const account = await caller(...args) - queryClient.setQueryData([QueryPrefix, statefulAccount.facet], (prev) => { - return prev && produce(prev, (draft) => { - draft.accounts[account.uid] = account - }) - }) - }, [queryClient, statefulAccount]) - } + const queryClient = useQueryClient(); + const statefulAccount = useStatefulAccountContext(); + + return React.useCallback( + async (...args: Args) => { + const account = await caller(...args); + queryClient.setQueryData( + [QueryPrefix, statefulAccount.facet], + (prev) => { + return ( + prev && + produce(prev, (draft) => { + draft.accounts[account.uid] = account; + }) + ); + } + ); + }, + [queryClient, statefulAccount] + ); + }; } -export const useUpdateAccountGameDataDirFn = createUseUpdateAccountFn(PluginStorage.updateAccountGameDataDir) -export const useUpdateAccountGachaUrlFn = createUseUpdateAccountFn(PluginStorage.updateAccountGachaUrl) -export const useUpdateAccountPropertiesFn = createUseUpdateAccountFn(PluginStorage.updateAccountProperties) - -export function useDeleteAccountFn () { - const queryClient = useQueryClient() - const statefulAccount = useStatefulAccountContext() - - return React.useCallback(async (uid: AccountUid) => { - await PluginStorage.deleteAccount(statefulAccount.facet, uid) - queryClient.setQueryData([QueryPrefix, statefulAccount.facet], (prev) => { - return prev && produce(prev, (draft) => { - delete draft.accounts[uid] - if (statefulAccount.selectedAccountUid === uid) { - draft.selectedAccountUid = Object.keys(draft.accounts)[0] ?? null - } - if (!draft.selectedAccountUid) { - LocalStorageSelectedAccountUid.remove(statefulAccount.facet) - } else { - LocalStorageSelectedAccountUid.set(statefulAccount.facet, draft.selectedAccountUid) +export const useUpdateAccountGameDataDirFn = createUseUpdateAccountFn( + PluginStorage.updateAccountGameDataDir +); +export const useUpdateAccountGachaUrlFn = createUseUpdateAccountFn( + PluginStorage.updateAccountGachaUrl +); +export const useUpdateAccountPropertiesFn = createUseUpdateAccountFn( + PluginStorage.updateAccountProperties +); + +export function useDeleteAccountFn() { + const queryClient = useQueryClient(); + const statefulAccount = useStatefulAccountContext(); + + return React.useCallback( + async (uid: AccountUid) => { + await PluginStorage.deleteAccount(statefulAccount.facet, uid); + queryClient.setQueryData( + [QueryPrefix, statefulAccount.facet], + (prev) => { + return ( + prev && + produce(prev, (draft) => { + delete draft.accounts[uid]; + if (statefulAccount.selectedAccountUid === uid) { + draft.selectedAccountUid = + Object.keys(draft.accounts)[0] ?? null; + } + if (!draft.selectedAccountUid) { + LocalStorageSelectedAccountUid.remove(statefulAccount.facet); + } else { + LocalStorageSelectedAccountUid.set( + statefulAccount.facet, + draft.selectedAccountUid + ); + } + }) + ); } - }) - }) - }, [queryClient, statefulAccount]) + ); + }, + [queryClient, statefulAccount] + ); } diff --git a/src/interfaces/gacha.ts b/src/interfaces/gacha.ts index 697fd59..98765dd 100644 --- a/src/interfaces/gacha.ts +++ b/src/interfaces/gacha.ts @@ -2,29 +2,29 @@ // See: src-tauri/src/gacha/impl_genshin.rs export interface GenshinGachaRecord { - id: string - uid: string - gacha_type: string // 100 | 200 | 301 | 302 | 400 - item_id: string // always empty - count: string // always 1 - time: string - name: string - lang: string // zh-cn - item_type: string // 角色 | 武器 - rank_type: string // 3 | 4 | 5 + id: string; + uid: string; + gacha_type: string; // 100 | 200 | 301 | 302 | 400 + item_id: string; // always empty + count: string; // always 1 + time: string; + name: string; + lang: string; // zh-cn + item_type: string; // 角色 | 武器 + rank_type: string; // 3 | 4 | 5 } // See: src-tauri/src/gacha/impl_starrail.rs export interface StarRailGachaRecord { - id: string - uid: string - gacha_id: string - gacha_type: string // 1 | 2 | 11 | 12 - item_id: string - count: string // always 1 - time: string - name: string - lang: string // zh-cn - item_type: string // 角色 | 光锥 - rank_type: string // 3 | 4 | 5 + id: string; + uid: string; + gacha_id: string; + gacha_type: string; // 1 | 2 | 11 | 12 + item_id: string; + count: string; // always 1 + time: string; + name: string; + lang: string; // zh-cn + item_type: string; // 角色 | 光锥 + rank_type: string; // 3 | 4 | 5 } diff --git a/src/main.tsx b/src/main.tsx index 5fbb5c8..f78a4df 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,9 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from '@/App' +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "@/App"; -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + -) +); diff --git a/src/query-client.ts b/src/query-client.ts index 2159d95..db2b63d 100644 --- a/src/query-client.ts +++ b/src/query-client.ts @@ -1,5 +1,5 @@ -import { QueryClient } from '@tanstack/react-query' +import { QueryClient } from "@tanstack/react-query"; -const queryClient = new QueryClient() +const queryClient = new QueryClient(); -export default queryClient +export default queryClient; diff --git a/src/router.tsx b/src/router.tsx index bf9c68d..fd0fac6 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,37 +1,37 @@ -import React from 'react' -import { createBrowserRouter } from 'react-router-dom' -import queryClient from './query-client' -import ErrorPage from '@/ErrorPage' -import Root from '@/routes/root' -import Index from '@/routes/index' -import Genshin, { loader as genshinLoader } from '@/routes/genshin' -import StarRail, { loader as starrailLoader } from '@/routes/starrail' -import Setting from '@/routes/setting' +import React from "react"; +import { createBrowserRouter } from "react-router-dom"; +import queryClient from "./query-client"; +import ErrorPage from "@/ErrorPage"; +import Root from "@/routes/root"; +import Index from "@/routes/index"; +import Genshin, { loader as genshinLoader } from "@/routes/genshin"; +import StarRail, { loader as starrailLoader } from "@/routes/starrail"; +import Setting from "@/routes/setting"; const router = createBrowserRouter([ { - path: '/', + path: "/", element: , errorElement: , children: [ { index: true, element: }, { - path: '/genshin', + path: "/genshin", element: , - loader: genshinLoader(queryClient) + loader: genshinLoader(queryClient), }, { - path: '/starrail', + path: "/starrail", element: , - loader: starrailLoader(queryClient) + loader: starrailLoader(queryClient), }, { - path: '/setting', - element: + path: "/setting", + element: , // TODO: loader: settingLoader(queryClient) - } - ] - } -]) + }, + ], + }, +]); -export default router +export default router; diff --git a/src/routes/root.tsx b/src/routes/root.tsx index f5abbc7..90f53c2 100644 --- a/src/routes/root.tsx +++ b/src/routes/root.tsx @@ -1,9 +1,9 @@ -import React from 'react' -import { Outlet } from 'react-router-dom' -import Sidebar from '@/components/Sidebar' -import Content from '@/components/Content' +import React from "react"; +import { Outlet } from "react-router-dom"; +import Sidebar from "@/components/Sidebar"; +import Content from "@/components/Content"; -export default function Root () { +export default function Root() { return ( @@ -11,5 +11,5 @@ export default function Root () { - ) + ); } diff --git a/src/theme.tsx b/src/theme.tsx index 5399334..f548530 100644 --- a/src/theme.tsx +++ b/src/theme.tsx @@ -1,17 +1,20 @@ -import React from 'react' -import { ThemeProvider, createTheme } from '@mui/material/styles' -import { zhCN } from '@mui/material/locale' -import CssBaseline from '@mui/material/CssBaseline' -import Box from '@mui/material/Box' -import '@/assets/global.css' +import React from "react"; +import { ThemeProvider, createTheme } from "@mui/material/styles"; +import { zhCN } from "@mui/material/locale"; +import CssBaseline from "@mui/material/CssBaseline"; +import Box from "@mui/material/Box"; +import "@/assets/global.css"; -const theme = createTheme({ - typography: { - fontFamily: '汉仪文黑-85W' - } -}, zhCN) +const theme = createTheme( + { + typography: { + fontFamily: "汉仪文黑-85W", + }, + }, + zhCN +); -export default function Theme (props: React.PropsWithChildren) { +export default function Theme(props: React.PropsWithChildren) { return ( @@ -19,5 +22,5 @@ export default function Theme (props: React.PropsWithChildren) { {props.children} - ) + ); } diff --git a/src/utilities/invoke.ts b/src/utilities/invoke.ts index dbf4b63..fb9e4fe 100644 --- a/src/utilities/invoke.ts +++ b/src/utilities/invoke.ts @@ -1,10 +1,10 @@ -import { invoke as _invoke } from '@tauri-apps/api' +import { invoke as _invoke } from "@tauri-apps/api"; const invoke: typeof _invoke = import.meta.env.DEV ? (...args) => { - console.debug('invoke', ...args) - return _invoke(...args) + console.debug("invoke", ...args); + return _invoke(...args); } - : _invoke + : _invoke; -export default invoke +export default invoke; diff --git a/src/utilities/plugin-gacha.ts b/src/utilities/plugin-gacha.ts index dbf0537..bd3fed9 100644 --- a/src/utilities/plugin-gacha.ts +++ b/src/utilities/plugin-gacha.ts @@ -1,53 +1,55 @@ -import { Account, AccountFacet } from '@/interfaces/account' -import { GenshinGachaRecord, StarRailGachaRecord } from '@/interfaces/gacha' -import invoke from '@/utilities/invoke' +import { Account, AccountFacet } from "@/interfaces/account"; +import { GenshinGachaRecord, StarRailGachaRecord } from "@/interfaces/gacha"; +import invoke from "@/utilities/invoke"; -export async function findGameDataDirectories (facet: AccountFacet): Promise { - return invoke('plugin:gacha|find_game_data_directories', { facet }) +export async function findGameDataDirectories( + facet: AccountFacet +): Promise { + return invoke("plugin:gacha|find_game_data_directories", { facet }); } -export async function findGachaUrl ( +export async function findGachaUrl( facet: AccountFacet, - uid: Account['uid'], + uid: Account["uid"], gameDataDir: string ): Promise { - return invoke('plugin:gacha|find_gacha_url', { facet, uid, gameDataDir }) + return invoke("plugin:gacha|find_gacha_url", { facet, uid, gameDataDir }); } -export async function pullAllGachaRecords ( +export async function pullAllGachaRecords( facet: AccountFacet, - uid: Account['uid'], + uid: Account["uid"], payload: { - gachaUrl: string + gachaUrl: string; gachaTypeAndLastEndIdMappings: Record< - GenshinGachaRecord['gacha_type'] | StarRailGachaRecord['gacha_type'], - GenshinGachaRecord['id'] | StarRailGachaRecord['id'] | null - > - eventChannel: string - saveToStorage?: boolean + GenshinGachaRecord["gacha_type"] | StarRailGachaRecord["gacha_type"], + GenshinGachaRecord["id"] | StarRailGachaRecord["id"] | null + >; + eventChannel: string; + saveToStorage?: boolean; } ): Promise { - return invoke('plugin:gacha|pull_all_gacha_records', { + return invoke("plugin:gacha|pull_all_gacha_records", { facet, uid, - ...payload - }) + ...payload, + }); } -export async function importGachaRecords ( +export async function importGachaRecords( facet: AccountFacet, - uid: Account['uid'], + uid: Account["uid"], file: string ): Promise { - return invoke('plugin:gacha|import_gacha_records', { facet, uid, file }) + return invoke("plugin:gacha|import_gacha_records", { facet, uid, file }); } -export async function exportGachaRecords ( +export async function exportGachaRecords( facet: AccountFacet, - uid: Account['uid'], + uid: Account["uid"], directory: string ): Promise { - return invoke('plugin:gacha|export_gacha_records', { facet, uid, directory }) + return invoke("plugin:gacha|export_gacha_records", { facet, uid, directory }); } const PluginGacha = Object.freeze({ @@ -55,7 +57,7 @@ const PluginGacha = Object.freeze({ findGachaUrl, pullAllGachaRecords, importGachaRecords, - exportGachaRecords -}) + exportGachaRecords, +}); -export default PluginGacha +export default PluginGacha; diff --git a/src/utilities/plugin-storage.ts b/src/utilities/plugin-storage.ts index 60979a2..4f6bc41 100644 --- a/src/utilities/plugin-storage.ts +++ b/src/utilities/plugin-storage.ts @@ -1,84 +1,108 @@ -import { AccountFacet, Account } from '@/interfaces/account' -import { GenshinGachaRecord, StarRailGachaRecord } from '@/interfaces/gacha' -import invoke from '@/utilities/invoke' +import { AccountFacet, Account } from "@/interfaces/account"; +import { GenshinGachaRecord, StarRailGachaRecord } from "@/interfaces/gacha"; +import invoke from "@/utilities/invoke"; -export type AccountUid = Account['uid'] -export type CreateAccountPayload = Omit +export type AccountUid = Account["uid"]; +export type CreateAccountPayload = Omit; export type FindGachaRecordsPayload = { - uid: AccountUid - gachaType?: GenshinGachaRecord['gacha_type'] | StarRailGachaRecord['gacha_type'] - limit?: number -} + uid: AccountUid; + gachaType?: + | GenshinGachaRecord["gacha_type"] + | StarRailGachaRecord["gacha_type"]; + limit?: number; +}; -export async function createAccount (payload: CreateAccountPayload): Promise { - return invoke('plugin:storage|create_account', payload) +export async function createAccount( + payload: CreateAccountPayload +): Promise { + return invoke("plugin:storage|create_account", payload); } -export async function findAccounts (facet?: AccountFacet): Promise> { - return invoke('plugin:storage|find_accounts', { facet }) +export async function findAccounts( + facet?: AccountFacet +): Promise> { + return invoke("plugin:storage|find_accounts", { facet }); } -export async function findAccount (facet: AccountFacet, uid: AccountUid): Promise { - return invoke('plugin:storage|find_account', { facet, uid }) +export async function findAccount( + facet: AccountFacet, + uid: AccountUid +): Promise { + return invoke("plugin:storage|find_account", { facet, uid }); } -export async function updateAccountGameDataDir ( +export async function updateAccountGameDataDir( facet: AccountFacet, uid: AccountUid, - gameDataDir: Account['gameDataDir'] + gameDataDir: Account["gameDataDir"] ): Promise { - return invoke('plugin:storage|update_account_game_data_dir', { facet, uid, gameDataDir }) + return invoke("plugin:storage|update_account_game_data_dir", { + facet, + uid, + gameDataDir, + }); } -export async function updateAccountGachaUrl ( +export async function updateAccountGachaUrl( facet: AccountFacet, uid: AccountUid, - gachaUrl: Account['gachaUrl'] + gachaUrl: Account["gachaUrl"] ): Promise { - return invoke('plugin:storage|update_account_gacha_url', { facet, uid, gachaUrl }) + return invoke("plugin:storage|update_account_gacha_url", { + facet, + uid, + gachaUrl, + }); } -export async function updateAccountProperties ( +export async function updateAccountProperties( facet: AccountFacet, uid: AccountUid, - properties: Account['properties'] + properties: Account["properties"] ): Promise { - return invoke('plugin:storage|update_account_properties', { facet, uid, properties }) + return invoke("plugin:storage|update_account_properties", { + facet, + uid, + properties, + }); } -export async function deleteAccount (facet: AccountFacet, uid: AccountUid): Promise { - return invoke('plugin:storage|delete_account', { facet, uid }) +export async function deleteAccount( + facet: AccountFacet, + uid: AccountUid +): Promise { + return invoke("plugin:storage|delete_account", { facet, uid }); } -export async function findGachaRecords ( +export async function findGachaRecords( facet: AccountFacet, payload: FindGachaRecordsPayload -): Promise> -export async function findGachaRecords ( +): Promise>; +export async function findGachaRecords( facet: AccountFacet, payload: FindGachaRecordsPayload -): Promise> -export async function findGachaRecords ( +): Promise>; +export async function findGachaRecords( facet: AccountFacet, payload: FindGachaRecordsPayload ): Promise> { - return invoke(`plugin:storage|find_${facet}_gacha_records`, payload) + return invoke(`plugin:storage|find_${facet}_gacha_records`, payload); } -export async function saveGachaRecords ( +export async function saveGachaRecords( facet: AccountFacet.Genshin, records: Array -): Promise -export async function saveGachaRecords ( +): Promise; +export async function saveGachaRecords( facet: AccountFacet.StarRail, records: Array -): Promise -export async function saveGachaRecords ( +): Promise; +export async function saveGachaRecords( facet: AccountFacet, records: Array ): Promise { - return invoke(`plugin:storage|save_${facet}_gacha_records`, { records }) + return invoke(`plugin:storage|save_${facet}_gacha_records`, { records }); } const PluginStorage = Object.freeze({ @@ -90,7 +114,7 @@ const PluginStorage = Object.freeze({ updateAccountProperties, deleteAccount, findGachaRecords, - saveGachaRecords -}) + saveGachaRecords, +}); -export default PluginStorage +export default PluginStorage; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 2d3d8fe..2dade66 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,7 +1,7 @@ /// -declare const __APP_VERSION__: string -declare const __APP_DESCRIPTION__: string -declare const __APP_AUTHOR__: string -declare const __APP_HOMEPAGE__: string -declare const __APP_REPOSITORY__: string +declare const __APP_VERSION__: string; +declare const __APP_DESCRIPTION__: string; +declare const __APP_AUTHOR__: string; +declare const __APP_HOMEPAGE__: string; +declare const __APP_REPOSITORY__: string;
{error.stack || error.statusText || error.message}