mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-04-13 05:20:28 +08:00
refactor: replace @mui/x-data-grid with @tanstack/react-table (#5544)
This commit is contained in:
parent
7e373ff39c
commit
bf06cbc87d
@ -44,7 +44,8 @@
|
||||
"@mui/icons-material": "^7.3.5",
|
||||
"@mui/lab": "7.0.0-beta.17",
|
||||
"@mui/material": "^7.3.5",
|
||||
"@mui/x-data-grid": "^8.18.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@tauri-apps/api": "2.9.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||
|
||||
80
pnpm-lock.yaml
generated
80
pnpm-lock.yaml
generated
@ -38,9 +38,12 @@ importers:
|
||||
'@mui/material':
|
||||
specifier: ^7.3.5
|
||||
version: 7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@mui/x-data-grid':
|
||||
specifier: ^8.18.0
|
||||
version: 8.18.0(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@tanstack/react-table':
|
||||
specifier: ^8.21.3
|
||||
version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@tanstack/react-virtual':
|
||||
specifier: ^3.13.12
|
||||
version: 3.13.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@tauri-apps/api':
|
||||
specifier: 2.9.0
|
||||
version: 2.9.0
|
||||
@ -1245,22 +1248,6 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@mui/x-data-grid@8.18.0':
|
||||
resolution: {integrity: sha512-g8y5EI3TNqrimHpH/Hv6u6i04cbvsqh39Tg4bZEhGq+SDxWp42iABlUvB7p+gtXfyd+IbmpfzUQ1hOCsHlTMZw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.9.0
|
||||
'@emotion/styled': ^11.8.1
|
||||
'@mui/material': ^5.15.14 || ^6.0.0 || ^7.0.0
|
||||
'@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/react':
|
||||
optional: true
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
|
||||
'@mui/x-internals@8.18.0':
|
||||
resolution: {integrity: sha512-iM2SJALLo4kNqxTel8lfjIymYV9MgTa6021/rAlfdh/vwPMglaKyXQHrxkkWs2Eu/JFKkCKr5Fd34Gsdp63wIg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -1687,6 +1674,26 @@ packages:
|
||||
'@swc/types@0.1.25':
|
||||
resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
|
||||
|
||||
'@tanstack/react-table@8.21.3':
|
||||
resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
react: '>=16.8'
|
||||
react-dom: '>=16.8'
|
||||
|
||||
'@tanstack/react-virtual@3.13.12':
|
||||
resolution: {integrity: sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@tanstack/table-core@8.21.3':
|
||||
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@tanstack/virtual-core@3.13.12':
|
||||
resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
|
||||
|
||||
'@tauri-apps/api@2.9.0':
|
||||
resolution: {integrity: sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==}
|
||||
|
||||
@ -5436,25 +5443,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.6
|
||||
|
||||
'@mui/x-data-grid@8.18.0(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@mui/material@7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@mui/system@7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
'@mui/material': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@mui/system': 7.3.5(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0)
|
||||
'@mui/utils': 7.3.5(@types/react@19.2.6)(react@19.2.0)
|
||||
'@mui/x-internals': 8.18.0(@types/react@19.2.6)(react@19.2.0)
|
||||
'@mui/x-virtualizer': 0.2.8(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
clsx: 2.1.1
|
||||
prop-types: 15.8.1
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
use-sync-external-store: 1.6.0(react@19.2.0)
|
||||
optionalDependencies:
|
||||
'@emotion/react': 11.14.0(@types/react@19.2.6)(react@19.2.0)
|
||||
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.6)(react@19.2.0))(@types/react@19.2.6)(react@19.2.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@mui/x-internals@8.18.0(@types/react@19.2.6)(react@19.2.0)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
@ -5814,6 +5802,22 @@ snapshots:
|
||||
dependencies:
|
||||
'@swc/counter': 0.1.3
|
||||
|
||||
'@tanstack/react-table@8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@tanstack/table-core': 8.21.3
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@tanstack/react-virtual@3.13.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||
dependencies:
|
||||
'@tanstack/virtual-core': 3.13.12
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
'@tanstack/table-core@8.21.3': {}
|
||||
|
||||
'@tanstack/virtual-core@3.13.12': {}
|
||||
|
||||
'@tauri-apps/api@2.9.0': {}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@2.9.4':
|
||||
|
||||
@ -1,26 +1,27 @@
|
||||
import { Box } from "@mui/material";
|
||||
import { ViewColumnRounded } from "@mui/icons-material";
|
||||
import { Box, IconButton, Tooltip } from "@mui/material";
|
||||
import {
|
||||
DataGrid,
|
||||
GridColDef,
|
||||
GridColumnOrderChangeParams,
|
||||
GridColumnResizeParams,
|
||||
GridColumnVisibilityModel,
|
||||
useGridApiRef,
|
||||
GridColumnMenuItemProps,
|
||||
GridColumnMenuHideItem,
|
||||
useGridRootProps,
|
||||
} from "@mui/x-data-grid";
|
||||
ColumnDef,
|
||||
ColumnSizingState,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
Updater,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import dayjs from "dayjs";
|
||||
import { useLocalStorage } from "foxact/use-local-storage";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
createContext,
|
||||
use,
|
||||
useRef,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import type { MouseEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
@ -28,7 +29,7 @@ import { truncateStr } from "@/utils/truncate-str";
|
||||
|
||||
import { ConnectionColumnManager } from "./connection-column-manager";
|
||||
|
||||
const ColumnManagerContext = createContext<() => void>(() => {});
|
||||
const ROW_HEIGHT = 40;
|
||||
|
||||
/**
|
||||
* Reconcile stored column order with base columns to handle added/removed fields
|
||||
@ -59,131 +60,6 @@ export const ConnectionTable = (props: Props) => {
|
||||
onCloseColumnManager,
|
||||
} = props;
|
||||
const { t } = useTranslation();
|
||||
const apiRef = useGridApiRef();
|
||||
useLayoutEffect(() => {
|
||||
const PATCH_FLAG_KEY = "__clashPatchedPublishEvent" as const;
|
||||
const ORIGINAL_KEY = "__clashOriginalPublishEvent" as const;
|
||||
let isUnmounted = false;
|
||||
let retryHandle: ReturnType<typeof setTimeout> | null = null;
|
||||
let cleanupOriginal: (() => void) | null = null;
|
||||
|
||||
const scheduleRetry = () => {
|
||||
if (isUnmounted || retryHandle !== null) return;
|
||||
retryHandle = setTimeout(() => {
|
||||
retryHandle = null;
|
||||
ensurePatched();
|
||||
}, 16);
|
||||
};
|
||||
|
||||
// Safari occasionally emits grid events without an event object,
|
||||
// and MUI expects `defaultMuiPrevented` to exist. Normalize here to avoid crashes.
|
||||
const createFallbackEvent = () => {
|
||||
const fallback = {
|
||||
defaultMuiPrevented: false,
|
||||
preventDefault() {
|
||||
fallback.defaultMuiPrevented = true;
|
||||
},
|
||||
};
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const ensureMuiEvent = (
|
||||
value: unknown,
|
||||
): {
|
||||
defaultMuiPrevented: boolean;
|
||||
preventDefault: () => void;
|
||||
[key: string]: unknown;
|
||||
} => {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return createFallbackEvent();
|
||||
}
|
||||
|
||||
const eventObject = value as {
|
||||
defaultMuiPrevented?: unknown;
|
||||
preventDefault?: () => void;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
if (typeof eventObject.defaultMuiPrevented !== "boolean") {
|
||||
eventObject.defaultMuiPrevented = false;
|
||||
}
|
||||
|
||||
if (typeof eventObject.preventDefault !== "function") {
|
||||
eventObject.preventDefault = () => {
|
||||
eventObject.defaultMuiPrevented = true;
|
||||
};
|
||||
}
|
||||
|
||||
return eventObject as {
|
||||
defaultMuiPrevented: boolean;
|
||||
preventDefault: () => void;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
const ensurePatched = () => {
|
||||
if (isUnmounted) return;
|
||||
const api = apiRef.current;
|
||||
|
||||
if (!api?.publishEvent) {
|
||||
scheduleRetry();
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataApi = api as unknown as typeof api &
|
||||
Record<string, unknown>;
|
||||
if (metadataApi[PATCH_FLAG_KEY] === true) return;
|
||||
|
||||
const originalPublishEvent = api.publishEvent;
|
||||
|
||||
// Use Proxy to create a more resilient wrapper that always normalizes events
|
||||
const patchedPublishEvent = new Proxy(originalPublishEvent, {
|
||||
apply(target, thisArg, rawArgs: unknown[]) {
|
||||
rawArgs[2] = ensureMuiEvent(rawArgs[2]);
|
||||
|
||||
return Reflect.apply(
|
||||
target as (...args: unknown[]) => unknown,
|
||||
thisArg,
|
||||
rawArgs,
|
||||
);
|
||||
},
|
||||
}) as typeof originalPublishEvent;
|
||||
|
||||
api.publishEvent = patchedPublishEvent;
|
||||
metadataApi[PATCH_FLAG_KEY] = true;
|
||||
metadataApi[ORIGINAL_KEY] = originalPublishEvent;
|
||||
|
||||
cleanupOriginal = () => {
|
||||
const storedOriginal = metadataApi[ORIGINAL_KEY] as
|
||||
| typeof originalPublishEvent
|
||||
| undefined;
|
||||
|
||||
api.publishEvent = (
|
||||
typeof storedOriginal === "function"
|
||||
? storedOriginal
|
||||
: originalPublishEvent
|
||||
) as typeof originalPublishEvent;
|
||||
|
||||
delete metadataApi[PATCH_FLAG_KEY];
|
||||
delete metadataApi[ORIGINAL_KEY];
|
||||
};
|
||||
};
|
||||
|
||||
ensurePatched();
|
||||
|
||||
return () => {
|
||||
isUnmounted = true;
|
||||
if (retryHandle !== null) {
|
||||
clearTimeout(retryHandle);
|
||||
retryHandle = null;
|
||||
}
|
||||
if (cleanupOriginal) {
|
||||
cleanupOriginal();
|
||||
cleanupOriginal = null;
|
||||
}
|
||||
};
|
||||
}, [apiRef]);
|
||||
|
||||
const [columnWidths, setColumnWidths] = useLocalStorage<
|
||||
Record<string, number>
|
||||
>(
|
||||
@ -229,95 +105,132 @@ export const ConnectionTable = (props: Props) => {
|
||||
},
|
||||
);
|
||||
|
||||
const baseColumns = useMemo<GridColDef[]>(() => {
|
||||
const createConnectionRow = (each: IConnectionsItem) => {
|
||||
const { metadata, rulePayload } = each;
|
||||
const chains = [...each.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
||||
const Destination = metadata.destinationIP
|
||||
? `${metadata.destinationIP}:${metadata.destinationPort}`
|
||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
||||
return {
|
||||
id: each.id,
|
||||
host: metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`,
|
||||
download: each.download,
|
||||
upload: each.upload,
|
||||
dlSpeed: each.curDownload,
|
||||
ulSpeed: each.curUpload,
|
||||
chains,
|
||||
rule,
|
||||
process: truncateStr(metadata.process || metadata.processPath),
|
||||
time: each.start,
|
||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
remoteDestination: Destination,
|
||||
type: `${metadata.type}(${metadata.network})`,
|
||||
connectionData: each,
|
||||
};
|
||||
};
|
||||
|
||||
type ConnectionRow = ReturnType<typeof createConnectionRow>;
|
||||
|
||||
type ColumnField = Exclude<keyof ConnectionRow, "connectionData">;
|
||||
|
||||
interface BaseColumn {
|
||||
field: ColumnField;
|
||||
headerName: string;
|
||||
width?: number;
|
||||
minWidth?: number;
|
||||
align?: "left" | "right";
|
||||
cell?: (row: ConnectionRow) => ReactNode;
|
||||
}
|
||||
|
||||
const baseColumns = useMemo<BaseColumn[]>(() => {
|
||||
return [
|
||||
{
|
||||
field: "host",
|
||||
headerName: t("connections.components.fields.host"),
|
||||
width: columnWidths["host"] || 220,
|
||||
minWidth: 180,
|
||||
width: 180,
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: "download",
|
||||
headerName: t("shared.labels.downloaded"),
|
||||
width: columnWidths["download"] || 88,
|
||||
width: 76,
|
||||
minWidth: 60,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
valueFormatter: (value: number) => parseTraffic(value).join(" "),
|
||||
cell: (row) => parseTraffic(row.download).join(" "),
|
||||
},
|
||||
{
|
||||
field: "upload",
|
||||
headerName: t("shared.labels.uploaded"),
|
||||
width: columnWidths["upload"] || 88,
|
||||
width: 76,
|
||||
minWidth: 60,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
valueFormatter: (value: number) => parseTraffic(value).join(" "),
|
||||
cell: (row) => parseTraffic(row.upload).join(" "),
|
||||
},
|
||||
{
|
||||
field: "dlSpeed",
|
||||
headerName: t("connections.components.fields.dlSpeed"),
|
||||
width: columnWidths["dlSpeed"] || 88,
|
||||
width: 76,
|
||||
minWidth: 60,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
|
||||
cell: (row) => `${parseTraffic(row.dlSpeed).join(" ")}/s`,
|
||||
},
|
||||
{
|
||||
field: "ulSpeed",
|
||||
headerName: t("connections.components.fields.ulSpeed"),
|
||||
width: columnWidths["ulSpeed"] || 88,
|
||||
width: 76,
|
||||
minWidth: 60,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
|
||||
cell: (row) => `${parseTraffic(row.ulSpeed).join(" ")}/s`,
|
||||
},
|
||||
{
|
||||
field: "chains",
|
||||
headerName: t("connections.components.fields.chains"),
|
||||
width: columnWidths["chains"] || 340,
|
||||
minWidth: 180,
|
||||
width: 280,
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: "rule",
|
||||
headerName: t("connections.components.fields.rule"),
|
||||
width: columnWidths["rule"] || 280,
|
||||
minWidth: 180,
|
||||
width: 220,
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
field: "process",
|
||||
headerName: t("connections.components.fields.process"),
|
||||
width: columnWidths["process"] || 220,
|
||||
minWidth: 180,
|
||||
width: 180,
|
||||
minWidth: 140,
|
||||
},
|
||||
{
|
||||
field: "time",
|
||||
headerName: t("connections.components.fields.time"),
|
||||
width: columnWidths["time"] || 120,
|
||||
minWidth: 100,
|
||||
width: 100,
|
||||
minWidth: 80,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
sortComparator: (v1: string, v2: string) =>
|
||||
new Date(v2).getTime() - new Date(v1).getTime(),
|
||||
valueFormatter: (value: number) => dayjs(value).fromNow(),
|
||||
cell: (row) => dayjs(row.time).fromNow(),
|
||||
},
|
||||
{
|
||||
field: "source",
|
||||
headerName: t("connections.components.fields.source"),
|
||||
width: columnWidths["source"] || 200,
|
||||
minWidth: 130,
|
||||
width: 160,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: "remoteDestination",
|
||||
headerName: t("connections.components.fields.destination"),
|
||||
width: columnWidths["remoteDestination"] || 200,
|
||||
minWidth: 130,
|
||||
width: 160,
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
field: "type",
|
||||
headerName: t("connections.components.fields.type"),
|
||||
width: columnWidths["type"] || 160,
|
||||
minWidth: 100,
|
||||
width: 120,
|
||||
minWidth: 80,
|
||||
},
|
||||
];
|
||||
}, [columnWidths, t]);
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
setColumnOrder((prevValue) => {
|
||||
@ -334,7 +247,7 @@ export const ConnectionTable = (props: Props) => {
|
||||
});
|
||||
}, [baseColumns, setColumnOrder]);
|
||||
|
||||
const columns = useMemo<GridColDef[]>(() => {
|
||||
const columns = useMemo<BaseColumn[]>(() => {
|
||||
const order = Array.isArray(columnOrder) ? columnOrder : [];
|
||||
const orderMap = new Map(order.map((field, index) => [field, index]));
|
||||
|
||||
@ -362,42 +275,6 @@ export const ConnectionTable = (props: Props) => {
|
||||
}, 0);
|
||||
}, [columns, columnVisibilityModel]);
|
||||
|
||||
const handleColumnResize = (params: GridColumnResizeParams) => {
|
||||
const { colDef, width } = params;
|
||||
setColumnWidths((prev) => ({
|
||||
...prev,
|
||||
[colDef.field]: width,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleColumnVisibilityChange = useCallback(
|
||||
(model: GridColumnVisibilityModel) => {
|
||||
const hiddenFields = new Set<string>();
|
||||
Object.entries(model).forEach(([field, value]) => {
|
||||
if (value === false) {
|
||||
hiddenFields.add(field);
|
||||
}
|
||||
});
|
||||
|
||||
const nextVisibleCount = columns.reduce((count, column) => {
|
||||
return hiddenFields.has(column.field) ? count : count + 1;
|
||||
}, 0);
|
||||
|
||||
if (nextVisibleCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setColumnVisibilityModel(() => {
|
||||
const sanitized: Partial<Record<string, boolean>> = {};
|
||||
hiddenFields.forEach((field) => {
|
||||
sanitized[field] = false;
|
||||
});
|
||||
return sanitized;
|
||||
});
|
||||
},
|
||||
[columns, setColumnVisibilityModel],
|
||||
);
|
||||
|
||||
const handleToggleColumn = useCallback(
|
||||
(field: string, visible: boolean) => {
|
||||
if (!visible && visibleColumnsCount <= 1) {
|
||||
@ -417,30 +294,6 @@ export const ConnectionTable = (props: Props) => {
|
||||
[setColumnVisibilityModel, visibleColumnsCount],
|
||||
);
|
||||
|
||||
const handleColumnOrderChange = useCallback(
|
||||
(params: GridColumnOrderChangeParams) => {
|
||||
setColumnOrder((prevValue) => {
|
||||
const baseFields = baseColumns.map((col) => col.field);
|
||||
const currentOrder = Array.isArray(prevValue)
|
||||
? [...prevValue]
|
||||
: [...baseFields];
|
||||
const field = params.column.field;
|
||||
const currentIndex = currentOrder.indexOf(field);
|
||||
if (currentIndex === -1) return currentOrder;
|
||||
|
||||
currentOrder.splice(currentIndex, 1);
|
||||
const targetIndex = Math.min(
|
||||
Math.max(params.targetIndex, 0),
|
||||
currentOrder.length,
|
||||
);
|
||||
currentOrder.splice(targetIndex, 0, field);
|
||||
|
||||
return currentOrder;
|
||||
});
|
||||
},
|
||||
[baseColumns, setColumnOrder],
|
||||
);
|
||||
|
||||
const handleManagerOrderChange = useCallback(
|
||||
(order: string[]) => {
|
||||
setColumnOrder(() => {
|
||||
@ -456,16 +309,54 @@ export const ConnectionTable = (props: Props) => {
|
||||
setColumnOrder(baseColumns.map((col) => col.field));
|
||||
}, [baseColumns, setColumnOrder, setColumnVisibilityModel]);
|
||||
|
||||
const gridVisibilityModel = useMemo(() => {
|
||||
const result: GridColumnVisibilityModel = {};
|
||||
if (!columnVisibilityModel) return result;
|
||||
Object.entries(columnVisibilityModel).forEach(([field, value]) => {
|
||||
if (typeof value === "boolean") {
|
||||
result[field] = value;
|
||||
}
|
||||
const handleColumnVisibilityChange = useCallback(
|
||||
(update: Updater<VisibilityState>) => {
|
||||
setColumnVisibilityModel((prev) => {
|
||||
const current = prev ?? {};
|
||||
const baseState: VisibilityState = {};
|
||||
columns.forEach((column) => {
|
||||
baseState[column.field] = (current[column.field] ?? true) !== false;
|
||||
});
|
||||
|
||||
const mergedState =
|
||||
typeof update === "function"
|
||||
? update(baseState)
|
||||
: { ...baseState, ...update };
|
||||
|
||||
const hiddenFields = columns
|
||||
.filter((column) => mergedState[column.field] === false)
|
||||
.map((column) => column.field);
|
||||
|
||||
if (columns.length - hiddenFields.length === 0) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const sanitized: Partial<Record<string, boolean>> = {};
|
||||
hiddenFields.forEach((field) => {
|
||||
sanitized[field] = false;
|
||||
});
|
||||
return sanitized;
|
||||
});
|
||||
},
|
||||
[columns, setColumnVisibilityModel],
|
||||
);
|
||||
|
||||
const columnVisibilityState = useMemo<VisibilityState>(() => {
|
||||
const result: VisibilityState = {};
|
||||
if (!columnVisibilityModel) {
|
||||
columns.forEach((column) => {
|
||||
result[column.field] = true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
columns.forEach((column) => {
|
||||
result[column.field] =
|
||||
(columnVisibilityModel?.[column.field] ?? true) !== false;
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [columnVisibilityModel]);
|
||||
}, [columnVisibilityModel, columns]);
|
||||
|
||||
const columnOptions = useMemo(() => {
|
||||
return columns.map((column) => ({
|
||||
@ -475,72 +366,293 @@ export const ConnectionTable = (props: Props) => {
|
||||
}));
|
||||
}, [columns, columnVisibilityModel]);
|
||||
|
||||
const connRows = useMemo(() => {
|
||||
return connections.map((each) => {
|
||||
const { metadata, rulePayload } = each;
|
||||
const chains = [...each.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
|
||||
const Destination = metadata.destinationIP
|
||||
? `${metadata.destinationIP}:${metadata.destinationPort}`
|
||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
|
||||
return {
|
||||
id: each.id,
|
||||
host: metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.remoteDestination}:${metadata.destinationPort}`,
|
||||
download: each.download,
|
||||
upload: each.upload,
|
||||
dlSpeed: each.curDownload,
|
||||
ulSpeed: each.curUpload,
|
||||
chains,
|
||||
rule,
|
||||
process: truncateStr(metadata.process || metadata.processPath),
|
||||
time: each.start,
|
||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
remoteDestination: Destination,
|
||||
type: `${metadata.type}(${metadata.network})`,
|
||||
connectionData: each,
|
||||
};
|
||||
});
|
||||
}, [connections]);
|
||||
const connRows = useMemo<ConnectionRow[]>(
|
||||
() => connections.map((each) => createConnectionRow(each)),
|
||||
[connections],
|
||||
);
|
||||
|
||||
const columnDefs = useMemo<ColumnDef<ConnectionRow>[]>(() => {
|
||||
return columns.map((column) => ({
|
||||
id: column.field,
|
||||
accessorKey: column.field,
|
||||
header: column.headerName,
|
||||
size: column.width,
|
||||
minSize: column.minWidth ?? 80,
|
||||
enableResizing: true,
|
||||
meta: {
|
||||
align: column.align ?? "left",
|
||||
field: column.field,
|
||||
},
|
||||
cell: column.cell
|
||||
? ({ row }) => column.cell?.(row.original)
|
||||
: (info) => info.getValue(),
|
||||
}));
|
||||
}, [columns]);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const handleColumnSizingChange = useCallback(
|
||||
(updater: Updater<ColumnSizingState>) => {
|
||||
setColumnWidths((prev) => {
|
||||
const prevState = prev ?? {};
|
||||
const nextState =
|
||||
typeof updater === "function" ? updater(prevState) : updater;
|
||||
const sanitized: Record<string, number> = {};
|
||||
Object.entries(nextState).forEach(([key, size]) => {
|
||||
if (typeof size === "number" && Number.isFinite(size)) {
|
||||
sanitized[key] = size;
|
||||
}
|
||||
});
|
||||
return sanitized;
|
||||
});
|
||||
},
|
||||
[setColumnWidths],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: connRows,
|
||||
columns: columnDefs,
|
||||
state: {
|
||||
columnVisibility: columnVisibilityState,
|
||||
columnSizing: columnWidths,
|
||||
sorting,
|
||||
},
|
||||
columnResizeMode: "onChange",
|
||||
enableSortingRemoval: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
onColumnSizingChange: handleColumnSizingChange,
|
||||
onColumnVisibilityChange: handleColumnVisibilityChange,
|
||||
});
|
||||
|
||||
const rows = table.getRowModel().rows;
|
||||
const tableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: rows.length,
|
||||
getScrollElement: () => tableContainerRef.current,
|
||||
estimateSize: () => ROW_HEIGHT,
|
||||
overscan: 8,
|
||||
});
|
||||
|
||||
const virtualRows = rowVirtualizer.getVirtualItems();
|
||||
const totalSize = rowVirtualizer.getTotalSize();
|
||||
const tableWidth = table.getTotalSize();
|
||||
|
||||
return (
|
||||
<ColumnManagerContext value={onOpenColumnManager}>
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
position: "relative",
|
||||
fontFamily: (theme) => theme.typography.fontFamily,
|
||||
}}
|
||||
>
|
||||
<DataGrid
|
||||
apiRef={apiRef}
|
||||
rows={connRows}
|
||||
columns={columns}
|
||||
onRowClick={(e) => onShowDetail(e.row.connectionData)}
|
||||
density="compact"
|
||||
<Tooltip title={t("connections.components.columnManager.title")}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onOpenColumnManager}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 4,
|
||||
right: 4,
|
||||
zIndex: 3,
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === "dark"
|
||||
? theme.palette.background.default
|
||||
: theme.palette.background.paper,
|
||||
"&:hover": {
|
||||
backgroundColor: (theme) => theme.palette.action.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ViewColumnRounded fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Box
|
||||
ref={tableContainerRef}
|
||||
sx={{
|
||||
flex: 1,
|
||||
border: "none",
|
||||
minHeight: 0,
|
||||
"div:focus": { outline: "none !important" },
|
||||
"& .MuiDataGrid-columnHeader": {
|
||||
userSelect: "none",
|
||||
overflow: "auto",
|
||||
borderRadius: 1,
|
||||
border: "none",
|
||||
"&::-webkit-scrollbar": {
|
||||
height: 8,
|
||||
},
|
||||
}}
|
||||
columnVisibilityModel={gridVisibilityModel}
|
||||
onColumnVisibilityModelChange={handleColumnVisibilityChange}
|
||||
onColumnResize={handleColumnResize}
|
||||
onColumnOrderChange={handleColumnOrderChange}
|
||||
slotProps={{
|
||||
columnMenu: {
|
||||
slots: {
|
||||
columnMenuColumnsItem: ConnectionColumnMenuColumnsItem,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
minWidth: "100%",
|
||||
width: tableWidth,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<Box
|
||||
key={headerGroup.id}
|
||||
sx={{
|
||||
display: "flex",
|
||||
borderBottom: (theme) =>
|
||||
`1px solid ${theme.palette.divider}`,
|
||||
backgroundColor: (theme) => theme.palette.background.paper,
|
||||
}}
|
||||
>
|
||||
{headerGroup.headers.map((header) => {
|
||||
if (header.isPlaceholder) {
|
||||
return null;
|
||||
}
|
||||
const meta = header.column.columnDef.meta as {
|
||||
align?: "left" | "right";
|
||||
field: string;
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
key={header.id}
|
||||
sx={{
|
||||
flex: `0 0 ${header.getSize()}px`,
|
||||
minWidth: header.column.columnDef.minSize || 80,
|
||||
maxWidth: header.column.columnDef.maxSize,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
px: 1,
|
||||
py: 1,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: "text.secondary",
|
||||
userSelect: "none",
|
||||
justifyContent:
|
||||
meta?.align === "right" ? "flex-end" : "flex-start",
|
||||
gap: 0.25,
|
||||
"&:hover": {
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.action.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
cursor: header.column.getCanSort()
|
||||
? "pointer"
|
||||
: "default",
|
||||
}}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
{{
|
||||
asc: "▲",
|
||||
desc: "▼",
|
||||
}[header.column.getIsSorted() as string] ?? null}
|
||||
</Box>
|
||||
{header.column.getCanResize() && (
|
||||
<Box
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
sx={{
|
||||
cursor: "col-resize",
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: 4,
|
||||
height: "100%",
|
||||
transform: "translateX(50%)",
|
||||
"&:hover": {
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.action.active,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
position: "relative",
|
||||
height: totalSize,
|
||||
}}
|
||||
>
|
||||
{virtualRows.map((virtualRow) => {
|
||||
const row = rows[virtualRow.index];
|
||||
if (!row) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={row.id}
|
||||
onClick={() => onShowDetail(row.original.connectionData)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: virtualRow.size,
|
||||
transform: `translateY(${virtualRow.start}px)`,
|
||||
borderBottom: (theme) =>
|
||||
`1px solid ${theme.palette.divider}`,
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
backgroundColor: (theme) => theme.palette.action.hover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const meta = cell.column.columnDef.meta as {
|
||||
align?: "left" | "right";
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
key={cell.id}
|
||||
sx={{
|
||||
flex: `0 0 ${cell.column.getSize()}px`,
|
||||
minWidth: cell.column.columnDef.minSize || 80,
|
||||
maxWidth: cell.column.columnDef.maxSize,
|
||||
px: 1,
|
||||
fontSize: 13,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent:
|
||||
meta?.align === "right"
|
||||
? "flex-end"
|
||||
: "flex-start",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<ConnectionColumnManager
|
||||
open={columnManagerOpen}
|
||||
@ -550,52 +662,6 @@ export const ConnectionTable = (props: Props) => {
|
||||
onOrderChange={handleManagerOrderChange}
|
||||
onReset={handleResetColumns}
|
||||
/>
|
||||
</ColumnManagerContext>
|
||||
);
|
||||
};
|
||||
|
||||
type ConnectionColumnMenuManageItemProps = GridColumnMenuItemProps & {
|
||||
onOpenColumnManager: () => void;
|
||||
};
|
||||
|
||||
const ConnectionColumnMenuManageItem = (
|
||||
props: ConnectionColumnMenuManageItemProps,
|
||||
) => {
|
||||
const { onClick, onOpenColumnManager } = props;
|
||||
const rootProps = useGridRootProps();
|
||||
const { t } = useTranslation();
|
||||
const handleClick = useCallback(
|
||||
(event: MouseEvent<HTMLElement>) => {
|
||||
onClick(event);
|
||||
onOpenColumnManager();
|
||||
},
|
||||
[onClick, onOpenColumnManager],
|
||||
);
|
||||
|
||||
if (rootProps.disableColumnSelector) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const MenuItem = rootProps.slots.baseMenuItem;
|
||||
const Icon = rootProps.slots.columnMenuManageColumnsIcon;
|
||||
|
||||
return (
|
||||
<MenuItem onClick={handleClick} iconStart={<Icon fontSize="small" />}>
|
||||
{t("connections.components.columnManager.title")}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
const ConnectionColumnMenuColumnsItem = (props: GridColumnMenuItemProps) => {
|
||||
const onOpenColumnManager = use(ColumnManagerContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GridColumnMenuHideItem {...props} />
|
||||
<ConnectionColumnMenuManageItem
|
||||
{...props}
|
||||
onOpenColumnManager={onOpenColumnManager}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,18 +1,10 @@
|
||||
import { alpha, createTheme, Theme as MuiTheme, Shadows } from "@mui/material";
|
||||
import {
|
||||
arSD as arXDataGrid,
|
||||
enUS as enXDataGrid,
|
||||
faIR as faXDataGrid,
|
||||
ruRU as ruXDataGrid,
|
||||
zhCN as zhXDataGrid,
|
||||
} from "@mui/x-data-grid/locales";
|
||||
import {
|
||||
getCurrentWebviewWindow,
|
||||
WebviewWindow,
|
||||
} from "@tauri-apps/api/webviewWindow";
|
||||
import { Theme as TauriOsTheme } from "@tauri-apps/api/window";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { defaultDarkTheme, defaultTheme } from "@/pages/_theme";
|
||||
@ -73,24 +65,12 @@ ${css}
|
||||
return scopedBlock;
|
||||
};
|
||||
|
||||
const languagePackMap: Record<string, any> = {
|
||||
zh: { ...zhXDataGrid },
|
||||
fa: { ...faXDataGrid },
|
||||
ru: { ...ruXDataGrid },
|
||||
ar: { ...arXDataGrid },
|
||||
en: { ...enXDataGrid },
|
||||
};
|
||||
|
||||
const getLanguagePackMap = (key: string) =>
|
||||
languagePackMap[key] || languagePackMap.en;
|
||||
|
||||
/**
|
||||
* custom theme
|
||||
*/
|
||||
export const useCustomTheme = () => {
|
||||
const appWindow: WebviewWindow = useMemo(() => getCurrentWebviewWindow(), []);
|
||||
const { verge } = useVerge();
|
||||
const { i18n } = useTranslation();
|
||||
const { theme_mode, theme_setting } = verge ?? {};
|
||||
const mode = useThemeMode();
|
||||
const setMode = useSetThemeMode();
|
||||
@ -237,37 +217,34 @@ export const useCustomTheme = () => {
|
||||
let muiTheme: MuiTheme;
|
||||
|
||||
try {
|
||||
muiTheme = createTheme(
|
||||
{
|
||||
breakpoints: {
|
||||
values: { xs: 0, sm: 650, md: 900, lg: 1200, xl: 1536 },
|
||||
muiTheme = createTheme({
|
||||
breakpoints: {
|
||||
values: { xs: 0, sm: 650, md: 900, lg: 1200, xl: 1536 },
|
||||
},
|
||||
palette: {
|
||||
mode,
|
||||
primary: { main: setting.primary_color || dt.primary_color },
|
||||
secondary: { main: setting.secondary_color || dt.secondary_color },
|
||||
info: { main: setting.info_color || dt.info_color },
|
||||
error: { main: setting.error_color || dt.error_color },
|
||||
warning: { main: setting.warning_color || dt.warning_color },
|
||||
success: { main: setting.success_color || dt.success_color },
|
||||
text: {
|
||||
primary: setting.primary_text || dt.primary_text,
|
||||
secondary: setting.secondary_text || dt.secondary_text,
|
||||
},
|
||||
palette: {
|
||||
mode,
|
||||
primary: { main: setting.primary_color || dt.primary_color },
|
||||
secondary: { main: setting.secondary_color || dt.secondary_color },
|
||||
info: { main: setting.info_color || dt.info_color },
|
||||
error: { main: setting.error_color || dt.error_color },
|
||||
warning: { main: setting.warning_color || dt.warning_color },
|
||||
success: { main: setting.success_color || dt.success_color },
|
||||
text: {
|
||||
primary: setting.primary_text || dt.primary_text,
|
||||
secondary: setting.secondary_text || dt.secondary_text,
|
||||
},
|
||||
background: {
|
||||
paper: dt.background_color,
|
||||
default: dt.background_color,
|
||||
},
|
||||
},
|
||||
shadows: Array(25).fill("none") as Shadows,
|
||||
typography: {
|
||||
fontFamily: setting.font_family
|
||||
? `${setting.font_family}, ${dt.font_family}`
|
||||
: dt.font_family,
|
||||
background: {
|
||||
paper: dt.background_color,
|
||||
default: dt.background_color,
|
||||
},
|
||||
},
|
||||
getLanguagePackMap(i18n.language),
|
||||
);
|
||||
shadows: Array(25).fill("none") as Shadows,
|
||||
typography: {
|
||||
fontFamily: setting.font_family
|
||||
? `${setting.font_family}, ${dt.font_family}`
|
||||
: dt.font_family,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error creating MUI theme, falling back to defaults:", e);
|
||||
muiTheme = createTheme({
|
||||
@ -419,13 +396,7 @@ export const useCustomTheme = () => {
|
||||
}, 0);
|
||||
|
||||
return muiTheme;
|
||||
}, [
|
||||
mode,
|
||||
theme_setting,
|
||||
i18n.language,
|
||||
userBackgroundImage,
|
||||
hasUserBackground,
|
||||
]);
|
||||
}, [mode, theme_setting, userBackgroundImage, hasUserBackground]);
|
||||
|
||||
return { theme };
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user