mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-27 06:42:10 +08:00
refactor: migrate to @dnd-kit/react and simplify sortable components (#7819)
* refactor: migrate to @dnd-kit/react and simplify sortable components * chore: enable css formatting in biome.json * refactor(profile): simplify icon rendering logic and add alt text * style(profiles): add overflow hidden to profile grid container * refactor(profile): add radix for parseInt and improve type safety in profiles * docs: update changelog with drag-and-drop styling improvements * refactor(profile): move hasValidName function out of ProxiesEditorViewer * refactor(proxy): refactor proxy chain into sortable components * feat(proxy): add drop state styling and arrow animations to proxy chain * style(profiles): reduce horizontal padding in profile grid * feat(profile): add vertical margin to editor viewer items * chore: refresh pnpm lockfile * fix: preserve drag and drop behavior --------- Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
This commit is contained in:
@@ -53,5 +53,6 @@
|
||||
- 配置变更后统一广播刷新,界面开关与托盘状态保持同步
|
||||
- 重构命令层错误返回为结构化格式,保留完整诊断链
|
||||
- 优化首页界面中当前节点固定状态显示
|
||||
- 优化拖动排序样式及动画效果
|
||||
|
||||
</details>
|
||||
|
||||
+3
-4
@@ -41,9 +41,8 @@
|
||||
"knip:report": "knip --no-exit-code"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@dnd-kit/helpers": "^0.5.0",
|
||||
"@dnd-kit/react": "^0.5.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
@@ -132,7 +131,7 @@
|
||||
"eslint --fix --max-warnings=0",
|
||||
"biome format --write"
|
||||
],
|
||||
"*.{css,scss,json}": [
|
||||
"*.{css,json}": [
|
||||
"biome format --write"
|
||||
]
|
||||
},
|
||||
|
||||
Generated
+63
-40
@@ -109,15 +109,12 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@dnd-kit/core':
|
||||
specifier: ^6.3.1
|
||||
version: 6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@dnd-kit/sortable':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)
|
||||
'@dnd-kit/utilities':
|
||||
specifier: ^3.2.2
|
||||
version: 3.2.2(react@19.2.8)
|
||||
'@dnd-kit/helpers':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
'@dnd-kit/react':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@emotion/react':
|
||||
specifier: ^11.14.0
|
||||
version: 11.14.0(@types/react@19.2.18)(react@19.2.8)
|
||||
@@ -933,27 +930,29 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1':
|
||||
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
'@dnd-kit/abstract@0.5.0':
|
||||
resolution: {integrity: sha512-hi13iMJgjPX/KDYVKg5VeDIhmYiV6buc9bAX+tCLYf4QdyYjPbsXjn2sPo6m7fQ6SGJBEFgHJ2PemeKDUbwBaA==}
|
||||
|
||||
'@dnd-kit/core@6.3.1':
|
||||
resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
'@dnd-kit/collision@0.5.0':
|
||||
resolution: {integrity: sha512-xUqRn3lS7oqLkT0AnnHS/STh/Czvwe1UapZFYiLbsUGxopMsQd4teaPCzPouOThoMdGEe+dHWjfqJl6t9iG4mQ==}
|
||||
|
||||
'@dnd-kit/sortable@10.0.0':
|
||||
resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==}
|
||||
peerDependencies:
|
||||
'@dnd-kit/core': ^6.3.0
|
||||
react: '>=16.8.0'
|
||||
'@dnd-kit/dom@0.5.0':
|
||||
resolution: {integrity: sha512-f2xFJp5SYQ8EW/Fbtaa8iBb66hpkWc7qa8vU826KW11/tb44sH+AisZnGtwOOTWTQ0GraqBDr5ixTErww+eKXw==}
|
||||
|
||||
'@dnd-kit/utilities@3.2.2':
|
||||
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
|
||||
'@dnd-kit/geometry@0.5.0':
|
||||
resolution: {integrity: sha512-ubHQS1CiSDH8ssYH2xG5BnpwPSFP1tStXXjug7/Ba6qnQdu/EUH47l6QXKIksQnnanfVfDf0aGeevRxgZlj28A==}
|
||||
|
||||
'@dnd-kit/helpers@0.5.0':
|
||||
resolution: {integrity: sha512-i4y+51/icSw+OHMr/su19qhnmNhAzh8PnBwXvapFYTd+64oodIyJRiRkB+hhfxAfnur7RYSW8qacDTrXjg2XOg==}
|
||||
|
||||
'@dnd-kit/react@0.5.0':
|
||||
resolution: {integrity: sha512-abQPLI8lmfVE+v/n+pqy5WFxrw6T2Yg0UQZsL78dp5DKci7dKTVDjvLWqvass+XTFtzJmsZEjk1NdqE6xG8Jiw==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
|
||||
'@dnd-kit/state@0.5.0':
|
||||
resolution: {integrity: sha512-y7XbabQqjF58Lk8YmDQuR8l6QjN+Kh4qlGEjUvHuIeasLk1QP+9L5diXS98VMxQIivyMmUtX2//f+3N7qPJX4w==}
|
||||
|
||||
'@emnapi/core@1.10.0':
|
||||
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
|
||||
@@ -1641,6 +1640,9 @@ packages:
|
||||
'@popperjs/core@2.11.8':
|
||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||
|
||||
'@preact/signals-core@1.14.4':
|
||||
resolution: {integrity: sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==}
|
||||
|
||||
'@rolldown/binding-android-arm-eabi@1.2.6':
|
||||
resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -4775,29 +4777,48 @@ snapshots:
|
||||
'@biomejs/cli-win32-x64@2.5.10':
|
||||
optional: true
|
||||
|
||||
'@dnd-kit/accessibility@3.1.1(react@19.2.8)':
|
||||
'@dnd-kit/abstract@0.5.0':
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
'@dnd-kit/geometry': 0.5.0
|
||||
'@dnd-kit/state': 0.5.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
|
||||
'@dnd-kit/collision@0.5.0':
|
||||
dependencies:
|
||||
'@dnd-kit/accessibility': 3.1.1(react@19.2.8)
|
||||
'@dnd-kit/utilities': 3.2.2(react@19.2.8)
|
||||
'@dnd-kit/abstract': 0.5.0
|
||||
'@dnd-kit/geometry': 0.5.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/dom@0.5.0':
|
||||
dependencies:
|
||||
'@dnd-kit/abstract': 0.5.0
|
||||
'@dnd-kit/collision': 0.5.0
|
||||
'@dnd-kit/geometry': 0.5.0
|
||||
'@dnd-kit/state': 0.5.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/geometry@0.5.0':
|
||||
dependencies:
|
||||
'@dnd-kit/state': 0.5.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/helpers@0.5.0':
|
||||
dependencies:
|
||||
'@dnd-kit/abstract': 0.5.0
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/react@0.5.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
|
||||
dependencies:
|
||||
'@dnd-kit/abstract': 0.5.0
|
||||
'@dnd-kit/dom': 0.5.0
|
||||
'@dnd-kit/state': 0.5.0
|
||||
react: 19.2.8
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)':
|
||||
'@dnd-kit/state@0.5.0':
|
||||
dependencies:
|
||||
'@dnd-kit/core': 6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@dnd-kit/utilities': 3.2.2(react@19.2.8)
|
||||
react: 19.2.8
|
||||
tslib: 2.8.1
|
||||
|
||||
'@dnd-kit/utilities@3.2.2(react@19.2.8)':
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
'@preact/signals-core': 1.14.4
|
||||
tslib: 2.8.1
|
||||
|
||||
'@emnapi/core@1.10.0':
|
||||
@@ -5441,6 +5462,8 @@ snapshots:
|
||||
|
||||
'@popperjs/core@2.11.8': {}
|
||||
|
||||
'@preact/signals-core@1.14.4': {}
|
||||
|
||||
'@rolldown/binding-android-arm-eabi@1.2.6':
|
||||
optional: true
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
@use './layout.scss';
|
||||
@use './page.scss';
|
||||
@use './font.scss';
|
||||
@use "./layout.scss";
|
||||
@use "./page.scss";
|
||||
@use "./font.scss";
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
|
||||
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
||||
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
user-select: none;
|
||||
@@ -59,3 +59,7 @@ body {
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
[data-dnd-dragging] {
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.32) !important;
|
||||
}
|
||||
|
||||
@@ -17,3 +17,4 @@ export {
|
||||
StickyVirtualList,
|
||||
type StickyVirtualListHandle,
|
||||
} from './sticky-virtual-list'
|
||||
export { SortableItem } from './sortable-item'
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useSortable } from '@dnd-kit/react/sortable'
|
||||
import { useTheme } from '@mui/material'
|
||||
import { type CSSProperties, type ReactNode, useCallback } from 'react'
|
||||
|
||||
export interface SortableItemRenderProps {
|
||||
ref: (element: HTMLElement | null) => void
|
||||
handleRef: (element: HTMLElement | null) => void
|
||||
style: CSSProperties
|
||||
}
|
||||
|
||||
interface SortableItemProps {
|
||||
id: string
|
||||
index: number
|
||||
group?: string
|
||||
disabled?: boolean
|
||||
style?: CSSProperties
|
||||
children: ReactNode | ((props: SortableItemRenderProps) => ReactNode)
|
||||
}
|
||||
|
||||
export const SortableItem = (props: SortableItemProps) => {
|
||||
const { id, index, group, disabled, children, style } = props
|
||||
const { ref, handleRef, sortable, isDragging } = useSortable({
|
||||
id,
|
||||
index,
|
||||
group,
|
||||
disabled,
|
||||
})
|
||||
const theme = useTheme()
|
||||
const setRef = useCallback(
|
||||
(element: HTMLDivElement | null) => {
|
||||
handleRef(element?.querySelector('[data-sortable-handle]') ?? null)
|
||||
ref(element)
|
||||
},
|
||||
[handleRef, ref],
|
||||
)
|
||||
|
||||
const mergedStyle: CSSProperties = {
|
||||
position: 'relative',
|
||||
borderRadius: 8,
|
||||
...style,
|
||||
...(sortable.transition && {
|
||||
transition: `${sortable.transition.duration ?? 300}ms ${sortable.transition.easing ?? 'ease'} transform`,
|
||||
}),
|
||||
...(isDragging && {
|
||||
zIndex: 100,
|
||||
// 拖拽中的元素置为不透明背景,避免半透明卡片透出下层内容。 投影由全局 [data-dnd-dragging] 规则统一施加。
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
}),
|
||||
}
|
||||
|
||||
if (typeof children === 'function') {
|
||||
return children({ ref, handleRef, style: mergedStyle })
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={setRef} style={mergedStyle}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragDropProvider,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/core'
|
||||
import { arrayMove, SortableContext, useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, useSortable } from '@dnd-kit/react/sortable'
|
||||
import { DragIndicatorRounded } from '@mui/icons-material'
|
||||
import {
|
||||
Button,
|
||||
@@ -21,9 +18,13 @@ import {
|
||||
ListItem,
|
||||
ListItemText,
|
||||
} from '@mui/material'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const columnPointerSensor = PointerSensor.configure({
|
||||
activationConstraints: () => undefined,
|
||||
})
|
||||
|
||||
export interface ConnectionColumnOption {
|
||||
id: string
|
||||
label: string
|
||||
@@ -46,14 +47,8 @@ export const ConnectionColumnManager = ({
|
||||
onOrderChange,
|
||||
onReset,
|
||||
}: Props) => {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 6 },
|
||||
}),
|
||||
)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const items = useMemo(() => columns.map((column) => column.id), [columns])
|
||||
const visibleCount = useMemo(
|
||||
() => columns.filter((column) => column.visible).length,
|
||||
[columns],
|
||||
@@ -61,13 +56,21 @@ export const ConnectionColumnManager = ({
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (!over || active.id === over.id) return
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const order = columns.map((column) => column.id)
|
||||
const oldIndex = order.indexOf(active.id as string)
|
||||
const newIndex = order.indexOf(over.id as string)
|
||||
if (oldIndex === -1 || newIndex === -1) return
|
||||
const { index: newIndex, initialIndex: oldIndex } = source.sortable
|
||||
if (
|
||||
oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= order.length ||
|
||||
newIndex >= order.length ||
|
||||
oldIndex === newIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
onOrderChange(arrayMove(order, oldIndex, newIndex))
|
||||
},
|
||||
@@ -80,30 +83,29 @@ export const ConnectionColumnManager = ({
|
||||
{t('connections.components.columnManager.title')}
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ pt: 1 }}>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
<DragDropProvider
|
||||
sensors={[columnPointerSensor]}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={items}>
|
||||
<List
|
||||
dense
|
||||
disablePadding
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<SortableColumnItem
|
||||
key={column.id}
|
||||
column={column}
|
||||
dragHandleLabel={t(
|
||||
'connections.components.columnManager.dragHandle',
|
||||
)}
|
||||
disableToggle={column.visible && visibleCount <= 1}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<List
|
||||
dense
|
||||
disablePadding
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}
|
||||
>
|
||||
{columns.map((column, index) => (
|
||||
<SortableColumnItem
|
||||
key={column.id}
|
||||
id={column.id}
|
||||
index={index}
|
||||
column={column}
|
||||
dragHandleLabel={t(
|
||||
'connections.components.columnManager.dragHandle',
|
||||
)}
|
||||
disableToggle={column.visible && visibleCount <= 1}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</DragDropProvider>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button variant="text" onClick={onReset}>
|
||||
@@ -118,48 +120,44 @@ export const ConnectionColumnManager = ({
|
||||
}
|
||||
|
||||
interface SortableColumnItemProps {
|
||||
id: string
|
||||
index: number
|
||||
column: ConnectionColumnOption
|
||||
dragHandleLabel: string
|
||||
disableToggle?: boolean
|
||||
}
|
||||
|
||||
const SortableColumnItem = ({
|
||||
id,
|
||||
index,
|
||||
column,
|
||||
dragHandleLabel,
|
||||
disableToggle = false,
|
||||
}: SortableColumnItemProps) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: column.id })
|
||||
|
||||
const style = useMemo(
|
||||
() => ({
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
}),
|
||||
[transform, transition],
|
||||
)
|
||||
const [element, setElement] = useState<Element | null>(null)
|
||||
const handleRef = useRef<HTMLButtonElement | null>(null)
|
||||
const { isDragging } = useSortable({
|
||||
id,
|
||||
index,
|
||||
element,
|
||||
handle: handleRef,
|
||||
})
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
ref={setNodeRef}
|
||||
ref={setElement}
|
||||
disableGutters
|
||||
sx={{
|
||||
px: 1,
|
||||
py: 0.5,
|
||||
borderRadius: 1,
|
||||
border: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
backgroundColor: isDragging ? 'action.hover' : 'transparent',
|
||||
// 拖拽中的行置为不透明背景,避免透出下方内容;投影由全局 [data-dnd-dragging] 规则统一施加。
|
||||
backgroundColor: isDragging ? 'background.paper' : 'transparent',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
style={style}
|
||||
>
|
||||
<Checkbox
|
||||
edge="start"
|
||||
@@ -175,10 +173,9 @@ const SortableColumnItem = ({
|
||||
<IconButton
|
||||
edge="end"
|
||||
size="small"
|
||||
ref={handleRef}
|
||||
sx={{ cursor: isDragging ? 'grabbing' : 'grab' }}
|
||||
aria-label={dragHandleLabel}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<DragIndicatorRounded fontSize="small" />
|
||||
</IconButton>
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
DragDropProvider,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
} from '@dnd-kit/core'
|
||||
import { SortableContext } from '@dnd-kit/sortable'
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable } from '@dnd-kit/react/sortable'
|
||||
import { Add, NetworkCheck } from '@mui/icons-material'
|
||||
import { Box, IconButton, Tooltip, alpha, styled, Grid } from '@mui/material'
|
||||
import { Box, IconButton, Tooltip, alpha, styled } from '@mui/material'
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { useEffect, useRef, useMemo, useCallback } from 'react'
|
||||
@@ -20,8 +16,9 @@ import apple from '@/assets/image/test/apple.svg?raw'
|
||||
import github from '@/assets/image/test/github.svg?raw'
|
||||
import google from '@/assets/image/test/google.svg?raw'
|
||||
import youtube from '@/assets/image/test/youtube.svg?raw'
|
||||
import { SortableItem } from '@/components/base'
|
||||
import { TestItem } from '@/components/test/test-item'
|
||||
import { TestViewer, TestViewerRef } from '@/components/test/test-viewer'
|
||||
import { TestViewer, type TestViewerRef } from '@/components/test/test-viewer'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
|
||||
import { EnhancedCard } from './enhanced-card'
|
||||
@@ -70,11 +67,6 @@ const DEFAULT_TEST_LIST = [
|
||||
|
||||
export const TestCard = () => {
|
||||
const { t } = useTranslation()
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 8 },
|
||||
}),
|
||||
)
|
||||
const { verge, mutateVerge, patchVerge } = useVerge()
|
||||
const viewerRef = useRef<TestViewerRef>(null)
|
||||
|
||||
@@ -111,29 +103,42 @@ export const TestCard = () => {
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (!over || active.id === over.id) return
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
|
||||
const old_index = testList.findIndex((x) => x.uid === active.id)
|
||||
const new_index = testList.findIndex((x) => x.uid === over.id)
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
if (old_index >= 0 && new_index >= 0) {
|
||||
const newList = [...testList]
|
||||
const [removed] = newList.splice(old_index, 1)
|
||||
newList.splice(new_index, 0, removed)
|
||||
const { index: newIndex, initialIndex: oldIndex } = source.sortable
|
||||
if (
|
||||
oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= testList.length ||
|
||||
newIndex >= testList.length ||
|
||||
oldIndex === newIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// 优化:先本地更新,再异步 patch,避免UI卡死
|
||||
mutateVerge({ ...verge, test_list: newList }, false)
|
||||
const patchFn = () => {
|
||||
try {
|
||||
patchVerge({ test_list: newList })
|
||||
} catch {}
|
||||
}
|
||||
if (window.requestIdleCallback) {
|
||||
window.requestIdleCallback(patchFn)
|
||||
} else {
|
||||
setTimeout(patchFn, 0)
|
||||
}
|
||||
const activeId = testList[oldIndex].uid
|
||||
const overId = testList[newIndex].uid
|
||||
|
||||
if (activeId == null || overId == null || activeId === overId) return
|
||||
|
||||
const newList = [...testList]
|
||||
const [removed] = newList.splice(oldIndex, 1)
|
||||
newList.splice(newIndex, 0, removed)
|
||||
|
||||
// 优化:先本地更新,再异步 patch,避免UI卡死
|
||||
mutateVerge({ ...verge, test_list: newList }, false)
|
||||
const patchFn = () => {
|
||||
try {
|
||||
patchVerge({ test_list: newList })
|
||||
} catch {}
|
||||
}
|
||||
if (window.requestIdleCallback) {
|
||||
window.requestIdleCallback(patchFn)
|
||||
} else {
|
||||
setTimeout(patchFn, 0)
|
||||
}
|
||||
},
|
||||
[testList, verge, mutateVerge, patchVerge],
|
||||
@@ -149,20 +154,25 @@ export const TestCard = () => {
|
||||
// 使用useMemo优化UI内容,减少渲染计算
|
||||
const renderTestItems = useMemo(
|
||||
() => (
|
||||
<Grid container spacing={1} columns={12}>
|
||||
<SortableContext items={testList.map((x) => x.uid)}>
|
||||
{testList.map((item) => (
|
||||
<Grid key={item.uid} size={3}>
|
||||
<TestItem
|
||||
id={item.uid}
|
||||
itemData={item}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onDelete={onDeleteTestListItem}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</SortableContext>
|
||||
</Grid>
|
||||
<Box
|
||||
sx={{
|
||||
mb: 1.5,
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(80px, 1fr))',
|
||||
gap: 1,
|
||||
px: 1,
|
||||
}}
|
||||
>
|
||||
{testList.map((item, index) => (
|
||||
<SortableItem key={item.uid} id={item.uid} index={index}>
|
||||
<TestItem
|
||||
itemData={item}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onDelete={onDeleteTestListItem}
|
||||
/>
|
||||
</SortableItem>
|
||||
))}
|
||||
</Box>
|
||||
),
|
||||
[testList, onDeleteTestListItem],
|
||||
)
|
||||
@@ -195,14 +205,9 @@ export const TestCard = () => {
|
||||
}
|
||||
>
|
||||
<ScrollBox>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<DragDropProvider sensors={[PointerSensor]} onDragEnd={onDragEnd}>
|
||||
{renderTestItems}
|
||||
<DragOverlay />
|
||||
</DndContext>
|
||||
</DragDropProvider>
|
||||
</ScrollBox>
|
||||
|
||||
<TestViewer ref={viewerRef} onChange={onTestListItemChange} />
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import type {
|
||||
DraggableAttributes,
|
||||
DraggableSyntheticListeners,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
alpha,
|
||||
ListItem,
|
||||
@@ -9,26 +5,17 @@ import {
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from '@mui/material'
|
||||
import type { CSSProperties, PointerEvent, ReactNode } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import { useMatch, useNavigate, useResolvedPath } from 'react-router'
|
||||
|
||||
import type { SortableItemRenderProps } from '@/components/base/sortable-item'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
|
||||
interface SortableProps {
|
||||
setNodeRef?: (element: HTMLElement | null) => void
|
||||
attributes?: DraggableAttributes
|
||||
listeners?: DraggableSyntheticListeners
|
||||
style?: CSSProperties
|
||||
isDragging?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
to: string
|
||||
children: string
|
||||
icon: ReactNode[]
|
||||
sortable?: SortableProps
|
||||
sortable?: SortableItemRenderProps
|
||||
}
|
||||
export const LayoutItem = (props: Props) => {
|
||||
const { to, children, icon, sortable } = props
|
||||
@@ -42,34 +29,15 @@ export const LayoutItem = (props: Props) => {
|
||||
const effectiveMenuIcon =
|
||||
navCollapsed && menu_icon === 'disable' ? 'monochrome' : menu_icon
|
||||
|
||||
const { setNodeRef, attributes, listeners, style, isDragging, disabled } =
|
||||
sortable ?? {}
|
||||
|
||||
const draggable = Boolean(sortable) && !disabled
|
||||
const { onPointerDown, ...otherListeners } = draggable
|
||||
? (listeners ?? {})
|
||||
: {}
|
||||
|
||||
const handlePointerDown = useCallback(
|
||||
(event: PointerEvent<HTMLDivElement>) => {
|
||||
onPointerDown?.(event)
|
||||
},
|
||||
[onPointerDown],
|
||||
)
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
sx={[
|
||||
{ py: 0.5, maxWidth: 250, mx: 'auto', padding: '4px 0px' },
|
||||
isDragging ? { opacity: 0.78 } : {},
|
||||
]}
|
||||
ref={sortable?.ref}
|
||||
style={sortable?.style}
|
||||
sx={{ py: 0.5, maxWidth: 250, mx: 'auto', padding: '4px 0px' }}
|
||||
>
|
||||
<ListItemButton
|
||||
ref={sortable?.handleRef}
|
||||
selected={!!match}
|
||||
{...(draggable ? (attributes ?? {}) : {})}
|
||||
{...(draggable ? otherListeners : {})}
|
||||
sx={[
|
||||
{
|
||||
borderRadius: 2,
|
||||
@@ -77,8 +45,7 @@ export const LayoutItem = (props: Props) => {
|
||||
paddingLeft: 1,
|
||||
paddingRight: 1,
|
||||
marginRight: 1.25,
|
||||
cursor: draggable ? 'grab' : 'pointer',
|
||||
'&:active': draggable ? { cursor: 'grabbing' } : {},
|
||||
cursor: 'pointer',
|
||||
'& .MuiListItemText-primary': {
|
||||
color: 'text.primary',
|
||||
fontWeight: '700',
|
||||
@@ -99,7 +66,6 @@ export const LayoutItem = (props: Props) => {
|
||||
]}
|
||||
title={navCollapsed ? children : undefined}
|
||||
aria-label={navCollapsed ? children : undefined}
|
||||
onPointerDown={handlePointerDown}
|
||||
onClick={() => navigate(to)}
|
||||
>
|
||||
{(effectiveMenuIcon === 'monochrome' || !effectiveMenuIcon) && (
|
||||
@@ -107,16 +73,14 @@ export const LayoutItem = (props: Props) => {
|
||||
sx={{
|
||||
color: 'text.primary',
|
||||
marginLeft: '6px',
|
||||
cursor: draggable ? 'grab' : 'inherit',
|
||||
cursor: 'inherit',
|
||||
}}
|
||||
>
|
||||
{icon[0]}
|
||||
</ListItemIcon>
|
||||
)}
|
||||
{effectiveMenuIcon === 'colorful' && (
|
||||
<ListItemIcon sx={{ cursor: draggable ? 'grab' : 'inherit' }}>
|
||||
{icon[1]}
|
||||
</ListItemIcon>
|
||||
<ListItemIcon sx={{ cursor: 'inherit' }}>{icon[1]}</ListItemIcon>
|
||||
)}
|
||||
<ListItemText
|
||||
sx={{
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { DeleteForeverRounded, UndoRounded } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
@@ -19,22 +17,7 @@ interface Props {
|
||||
|
||||
export const GroupItem = (props: Props) => {
|
||||
const { type, group, onDelete } = props
|
||||
const sortable = type === 'prepend' || type === 'append'
|
||||
|
||||
const {
|
||||
attributes: sortableAttributes,
|
||||
listeners: sortableListeners,
|
||||
setNodeRef: sortableSetNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: group.name,
|
||||
disabled: !sortable,
|
||||
})
|
||||
const dragAttributes = sortable ? sortableAttributes : undefined
|
||||
const dragListeners = sortable ? sortableListeners : undefined
|
||||
const dragNodeRef = sortable ? sortableSetNodeRef : undefined
|
||||
const isSortable = type === 'prepend' || type === 'append'
|
||||
|
||||
const iconCachePath = useIconCache({
|
||||
icon: group.icon,
|
||||
@@ -55,15 +38,12 @@ export const GroupItem = (props: Props) => {
|
||||
? alpha(palette.error.main, 0.3)
|
||||
: alpha(palette.success.main, 0.3),
|
||||
height: '100%',
|
||||
margin: '8px 0',
|
||||
borderRadius: '8px',
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 'calc(infinity)' : undefined,
|
||||
})}
|
||||
>
|
||||
{group.icon && group.icon?.trim().startsWith('http') && (
|
||||
{group?.icon?.trim().startsWith('http') && (
|
||||
<img
|
||||
alt="group icon"
|
||||
src={iconCachePath === '' ? group.icon : iconCachePath}
|
||||
width="32px"
|
||||
style={{
|
||||
@@ -72,8 +52,9 @@ export const GroupItem = (props: Props) => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{group.icon && group.icon?.trim().startsWith('data') && (
|
||||
{group?.icon?.trim().startsWith('data') && (
|
||||
<img
|
||||
alt="group icon"
|
||||
src={group.icon}
|
||||
width="32px"
|
||||
style={{
|
||||
@@ -82,8 +63,9 @@ export const GroupItem = (props: Props) => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{group.icon && group.icon?.trim().startsWith('<svg') && (
|
||||
{group?.icon?.trim().startsWith('<svg') && (
|
||||
<img
|
||||
alt="group icon"
|
||||
src={`data:image/svg+xml;base64,${btoa(group.icon ?? '')}`}
|
||||
width="32px"
|
||||
style={{
|
||||
@@ -93,10 +75,8 @@ export const GroupItem = (props: Props) => {
|
||||
/>
|
||||
)}
|
||||
<ListItemText
|
||||
{...(dragAttributes ?? {})}
|
||||
{...(dragListeners ?? {})}
|
||||
ref={dragNodeRef}
|
||||
sx={{ cursor: sortable ? 'move' : '' }}
|
||||
data-sortable-handle
|
||||
sx={{ cursor: isSortable ? 'move' : undefined }}
|
||||
primary={
|
||||
<StyledPrimary
|
||||
sx={{ textDecoration: type === 'delete' ? 'line-through' : '' }}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragDropProvider,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
} from '@dnd-kit/sortable'
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -51,6 +46,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
SortableItem,
|
||||
Switch,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
@@ -141,6 +137,16 @@ const buildGroupsYaml = (
|
||||
)
|
||||
}
|
||||
|
||||
const findRealIndex = (
|
||||
list: IProxyGroupConfig[],
|
||||
filtered: IProxyGroupConfig[],
|
||||
filteredIndex: number,
|
||||
): number => {
|
||||
const item = filtered[filteredIndex]
|
||||
if (!item) return -1
|
||||
return list.findIndex((group) => group.name === item.name)
|
||||
}
|
||||
|
||||
export const GroupsEditorViewer = (props: Props) => {
|
||||
const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
|
||||
props
|
||||
@@ -201,20 +207,17 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onPrependDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredPrependSeq.map((x) => {
|
||||
return x.name
|
||||
})}
|
||||
>
|
||||
{filteredPrependSeq.map((item) => {
|
||||
return (
|
||||
<>
|
||||
{filteredPrependSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`prepend:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="prepend"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<GroupItem
|
||||
key={item.name}
|
||||
type="prepend"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
@@ -223,109 +226,138 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else if (index < filteredGroupList.length + shift) {
|
||||
const newIndex = index - shift
|
||||
return (
|
||||
<GroupItem
|
||||
key={filteredGroupList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredGroupList[newIndex].name)
|
||||
? 'delete'
|
||||
: 'original'
|
||||
}
|
||||
group={filteredGroupList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredGroupList[newIndex].name)) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter((v) => v !== filteredGroupList[newIndex].name),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredGroupList[newIndex].name,
|
||||
])
|
||||
<Box sx={{ margin: '8px 0' }}>
|
||||
<GroupItem
|
||||
key={filteredGroupList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredGroupList[newIndex].name)
|
||||
? 'delete'
|
||||
: 'original'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
group={filteredGroupList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredGroupList[newIndex].name)) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter(
|
||||
(v) => v !== filteredGroupList[newIndex].name,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredGroupList[newIndex].name,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onAppendDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredAppendSeq.map((x) => {
|
||||
return x.name
|
||||
})}
|
||||
>
|
||||
{filteredAppendSeq.map((item) => {
|
||||
return (
|
||||
<>
|
||||
{filteredAppendSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`append:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="append"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<GroupItem
|
||||
key={item.name}
|
||||
type="append"
|
||||
group={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 8 },
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
)
|
||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
let activeIndex = 0
|
||||
let overIndex = 0
|
||||
prependSeq.forEach((item, index) => {
|
||||
if (item.name === active.id) {
|
||||
activeIndex = index
|
||||
}
|
||||
if (item.name === over.id) {
|
||||
overIndex = index
|
||||
}
|
||||
})
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
setPrependSeq(arrayMove(prependSeq, activeIndex, overIndex))
|
||||
}
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
overIndex,
|
||||
)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(arrayMove(prependSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
let activeIndex = 0
|
||||
let overIndex = 0
|
||||
appendSeq.forEach((item, index) => {
|
||||
if (item.name === active.id) {
|
||||
activeIndex = index
|
||||
}
|
||||
if (item.name === over.id) {
|
||||
overIndex = index
|
||||
}
|
||||
})
|
||||
setAppendSeq(arrayMove(appendSeq, activeIndex, overIndex))
|
||||
}
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
appendSeq,
|
||||
filteredAppendSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(appendSeq, filteredAppendSeq, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppendSeq(arrayMove(appendSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
|
||||
// prevent drag over if source and target are in different groups
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (source?.group !== target?.group) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const { initialGroup, group } = source.sortable
|
||||
if (initialGroup !== group) return
|
||||
|
||||
if (group === 'prepend') {
|
||||
await onPrependDragEnd(event)
|
||||
} else if (group === 'append') {
|
||||
await onAppendDragEnd(event)
|
||||
}
|
||||
}
|
||||
const fetchContent = useCallback(async () => {
|
||||
@@ -756,7 +788,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
size="small"
|
||||
sx={{ width: 'calc(100% - 150px)' }}
|
||||
onChange={(e) => {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
field.onChange(parseInt(e.target.value, 10))
|
||||
}}
|
||||
/>
|
||||
</Item>
|
||||
@@ -779,7 +811,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
size="small"
|
||||
sx={{ width: 'calc(100% - 150px)' }}
|
||||
onChange={(e) => {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
field.onChange(parseInt(e.target.value, 10))
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
@@ -807,7 +839,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
size="small"
|
||||
sx={{ width: 'calc(100% - 150px)' }}
|
||||
onChange={(e) => {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
field.onChange(parseInt(e.target.value, 10))
|
||||
}}
|
||||
slotProps={{
|
||||
input: {
|
||||
@@ -839,7 +871,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
size="small"
|
||||
sx={{ width: 'calc(100% - 150px)' }}
|
||||
onChange={(e) => {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
field.onChange(parseInt(e.target.value, 10))
|
||||
}}
|
||||
/>
|
||||
</Item>
|
||||
@@ -882,7 +914,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
size="small"
|
||||
sx={{ width: 'calc(100% - 150px)' }}
|
||||
onChange={(e) => {
|
||||
field.onChange(parseInt(e.target.value))
|
||||
field.onChange(parseInt(e.target.value, 10))
|
||||
}}
|
||||
/>
|
||||
</Item>
|
||||
@@ -1120,16 +1152,22 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<VirtualList
|
||||
count={
|
||||
filteredGroupList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<VirtualList
|
||||
count={
|
||||
filteredGroupList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import type {
|
||||
DraggableAttributes,
|
||||
DraggableSyntheticListeners,
|
||||
} from '@dnd-kit/core'
|
||||
import { useSortable } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
CheckBoxOutlineBlankRounded,
|
||||
CheckBoxRounded,
|
||||
@@ -58,6 +55,8 @@ const round = keyframes`
|
||||
`
|
||||
|
||||
export interface ProfileItemProps {
|
||||
id: string
|
||||
index: number
|
||||
selected: boolean
|
||||
activating: boolean
|
||||
itemData: IProfileItem
|
||||
@@ -71,13 +70,12 @@ export interface ProfileItemProps {
|
||||
onSelectionChange?: () => void
|
||||
timerUpdateRevision: number
|
||||
completedUpdateRevision: number
|
||||
dragHandleRef: (node: HTMLElement | null) => void
|
||||
dragHandleAttributes: DraggableAttributes
|
||||
dragHandleListeners: DraggableSyntheticListeners
|
||||
}
|
||||
|
||||
const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
const {
|
||||
id,
|
||||
index,
|
||||
selected,
|
||||
activating,
|
||||
itemData,
|
||||
@@ -91,11 +89,17 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
onSelectionChange,
|
||||
timerUpdateRevision,
|
||||
completedUpdateRevision,
|
||||
dragHandleRef,
|
||||
dragHandleAttributes,
|
||||
dragHandleListeners,
|
||||
} = props
|
||||
|
||||
const [element, setElement] = useState<Element | null>(null)
|
||||
const handleRef = useRef<HTMLButtonElement | null>(null)
|
||||
useSortable({
|
||||
id,
|
||||
index,
|
||||
element,
|
||||
handle: handleRef,
|
||||
})
|
||||
|
||||
const { t } = useTranslation()
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 })
|
||||
@@ -616,7 +620,7 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
})
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Box ref={setElement} sx={{ position: 'relative', borderRadius: '8px' }}>
|
||||
<ProfileBox
|
||||
aria-selected={selected}
|
||||
onClick={(e) => {
|
||||
@@ -678,14 +682,12 @@ const ProfileItemBase = (props: ProfileItemProps) => {
|
||||
</IconButton>
|
||||
)}
|
||||
<Box
|
||||
ref={dragHandleRef}
|
||||
ref={handleRef}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
margin: 'auto 0',
|
||||
...(batchMode && { marginLeft: '-4px' }),
|
||||
}}
|
||||
{...dragHandleAttributes}
|
||||
{...dragHandleListeners}
|
||||
>
|
||||
<DragIndicatorRounded
|
||||
sx={[
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragDropProvider,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
} from '@dnd-kit/sortable'
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -40,7 +35,12 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseSearchBox, MonacoEditor, VirtualList } from '@/components/base'
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
SortableItem,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
import { ProxyItem } from '@/components/profile/proxy-item'
|
||||
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
@@ -58,6 +58,24 @@ interface Props {
|
||||
onSave?: (prev?: string, curr?: string) => void
|
||||
}
|
||||
|
||||
const findRealIndex = (
|
||||
list: IProxyConfig[],
|
||||
filtered: IProxyConfig[],
|
||||
filteredIndex: number,
|
||||
): number => {
|
||||
const item = filtered[filteredIndex]
|
||||
if (!item) return -1
|
||||
return list.findIndex((proxy) => proxy.name === item.name)
|
||||
}
|
||||
|
||||
// 节点的 name 会被用作 sortable item id、React key 以及拖拽排序的
|
||||
// 依据。当 name 为空/null(例如高级模式下粘贴了缺少 name 的节点)时,
|
||||
// 无效 name 不能作为 sortable item id,否则会导致拖拽注册失败。
|
||||
// 这里统一过滤掉没有有效 name 的节点,避免可视化编辑页崩溃;原始 YAML
|
||||
// 数据仍然保留,用户可在高级(文本)模式中查看并修正这些节点。
|
||||
const hasValidName = (proxy: IProxyConfig) =>
|
||||
typeof proxy?.name === 'string' && proxy.name.length > 0
|
||||
|
||||
export const ProxiesEditorViewer = (props: Props) => {
|
||||
const { profileUid, property, open, onClose, onSave } = props
|
||||
const { t } = useTranslation()
|
||||
@@ -75,14 +93,6 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
const [deleteSeq, setDeleteSeq] = useState<string[]>([])
|
||||
const hasLoadedSeqConfigRef = useRef(false)
|
||||
|
||||
// 节点的 name 会被用作 SortableContext 的 item id、React key 以及拖拽排序的
|
||||
// 依据。当 name 为空/null(例如高级模式下粘贴了缺少 name 的节点)时,
|
||||
// @dnd-kit 的 SortableContext 会对 null 执行 `'id' in item` 从而崩溃
|
||||
// (Cannot use 'in' operator to search for 'id' in null)。
|
||||
// 这里统一过滤掉没有有效 name 的节点,避免可视化编辑页崩溃;原始 YAML
|
||||
// 数据仍然保留,用户可在高级(文本)模式中查看并修正这些节点。
|
||||
const hasValidName = (proxy: IProxyConfig) =>
|
||||
typeof proxy?.name === 'string' && proxy.name.length > 0
|
||||
const filteredPrependSeq = useMemo(
|
||||
() =>
|
||||
prependSeq.filter((proxy) => hasValidName(proxy) && match(proxy.name)),
|
||||
@@ -101,20 +111,17 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onPrependDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredPrependSeq.map((x) => {
|
||||
return x.name
|
||||
})}
|
||||
>
|
||||
{filteredPrependSeq.map((item) => {
|
||||
return (
|
||||
<>
|
||||
{filteredPrependSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`prepend:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="prepend"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<ProxyItem
|
||||
key={item.name}
|
||||
type="prepend"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
@@ -123,109 +130,135 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else if (index < filteredProxyList.length + shift) {
|
||||
const newIndex = index - shift
|
||||
return (
|
||||
<ProxyItem
|
||||
key={filteredProxyList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredProxyList[newIndex].name)
|
||||
? 'delete'
|
||||
: 'original'
|
||||
}
|
||||
proxy={filteredProxyList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredProxyList[newIndex].name)) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter((v) => v !== filteredProxyList[newIndex].name),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredProxyList[newIndex].name,
|
||||
])
|
||||
<Box sx={{ margin: '8px 0' }}>
|
||||
<ProxyItem
|
||||
key={filteredProxyList[newIndex].name}
|
||||
type={
|
||||
deleteSeq.includes(filteredProxyList[newIndex].name)
|
||||
? 'delete'
|
||||
: 'original'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
proxy={filteredProxyList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredProxyList[newIndex].name)) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter(
|
||||
(v) => v !== filteredProxyList[newIndex].name,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [
|
||||
...prev,
|
||||
filteredProxyList[newIndex].name,
|
||||
])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onAppendDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredAppendSeq.map((x) => {
|
||||
return x.name
|
||||
})}
|
||||
>
|
||||
{filteredAppendSeq.map((item) => {
|
||||
return (
|
||||
<>
|
||||
{filteredAppendSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item.name}
|
||||
id={`append:${item.name}`}
|
||||
index={itemIndex}
|
||||
group="append"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<ProxyItem
|
||||
key={item.name}
|
||||
type="append"
|
||||
proxy={item}
|
||||
onDelete={() => {
|
||||
setAppendSeq(appendSeq.filter((v) => v.name !== item.name))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 8 },
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
)
|
||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
let activeIndex = 0
|
||||
let overIndex = 0
|
||||
prependSeq.forEach((item, index) => {
|
||||
if (item.name === active.id) {
|
||||
activeIndex = index
|
||||
}
|
||||
if (item.name === over.id) {
|
||||
overIndex = index
|
||||
}
|
||||
})
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
setPrependSeq(arrayMove(prependSeq, activeIndex, overIndex))
|
||||
}
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
overIndex,
|
||||
)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(arrayMove(prependSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
let activeIndex = 0
|
||||
let overIndex = 0
|
||||
appendSeq.forEach((item, index) => {
|
||||
if (item.name === active.id) {
|
||||
activeIndex = index
|
||||
}
|
||||
if (item.name === over.id) {
|
||||
overIndex = index
|
||||
}
|
||||
})
|
||||
setAppendSeq(arrayMove(appendSeq, activeIndex, overIndex))
|
||||
}
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
appendSeq,
|
||||
filteredAppendSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(appendSeq, filteredAppendSeq, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppendSeq(arrayMove(appendSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (source?.group !== target?.group) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const { initialGroup, group } = source.sortable
|
||||
if (initialGroup !== group) return
|
||||
|
||||
if (group === 'prepend') {
|
||||
await onPrependDragEnd(event)
|
||||
} else if (group === 'append') {
|
||||
await onAppendDragEnd(event)
|
||||
}
|
||||
}
|
||||
// 优化:异步分片解析,避免主线程阻塞,解析完成后批量setState
|
||||
@@ -489,16 +522,22 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<VirtualList
|
||||
count={
|
||||
filteredProxyList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<VirtualList
|
||||
count={
|
||||
filteredProxyList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { DeleteForeverRounded, UndoRounded } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
@@ -18,22 +16,7 @@ interface Props {
|
||||
|
||||
export const ProxyItem = (props: Props) => {
|
||||
const { type, proxy, onDelete } = props
|
||||
const sortable = type === 'prepend' || type === 'append'
|
||||
|
||||
const {
|
||||
attributes: sortableAttributes,
|
||||
listeners: sortableListeners,
|
||||
setNodeRef: sortableSetNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: proxy.name,
|
||||
disabled: !sortable,
|
||||
})
|
||||
const dragAttributes = sortable ? sortableAttributes : undefined
|
||||
const dragListeners = sortable ? sortableListeners : undefined
|
||||
const dragNodeRef = sortable ? sortableSetNodeRef : undefined
|
||||
const isSortable = type === 'prepend' || type === 'append'
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
@@ -49,18 +32,12 @@ export const ProxyItem = (props: Props) => {
|
||||
? alpha(palette.error.main, 0.3)
|
||||
: alpha(palette.success.main, 0.3),
|
||||
height: '100%',
|
||||
margin: '8px 0',
|
||||
borderRadius: '8px',
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 'calc(infinity)' : undefined,
|
||||
})}
|
||||
>
|
||||
<ListItemText
|
||||
{...(dragAttributes ?? {})}
|
||||
{...(dragListeners ?? {})}
|
||||
ref={dragNodeRef}
|
||||
sx={{ cursor: sortable ? 'move' : '' }}
|
||||
data-sortable-handle
|
||||
sx={{ cursor: isSortable ? 'move' : undefined }}
|
||||
primary={
|
||||
<StyledPrimary
|
||||
title={proxy.name}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import {
|
||||
DeleteForeverRounded,
|
||||
UndoRounded,
|
||||
@@ -14,6 +12,7 @@ import {
|
||||
alpha,
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
|
||||
interface Props {
|
||||
type: 'prepend' | 'original' | 'delete' | 'append'
|
||||
ruleRaw: string
|
||||
@@ -24,32 +23,13 @@ interface Props {
|
||||
|
||||
export const RuleItem = (props: Props) => {
|
||||
const { type, ruleRaw, onDelete, onPrepend, onAppend } = props
|
||||
const sortable = type === 'prepend' || type === 'append'
|
||||
const isSortable = type === 'prepend' || type === 'append'
|
||||
const rule = ruleRaw.replace(',no-resolve', '')
|
||||
|
||||
const ruleType = rule.match(/^[^,]+/)?.[0] ?? ''
|
||||
const proxyPolicy = rule.match(/[^,]+$/)?.[0] ?? ''
|
||||
const ruleContent = rule.slice(ruleType.length + 1, -proxyPolicy.length - 1)
|
||||
|
||||
const $sortable = useSortable({ id: ruleRaw })
|
||||
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = sortable
|
||||
? $sortable
|
||||
: {
|
||||
attributes: {},
|
||||
listeners: {},
|
||||
setNodeRef: null,
|
||||
transform: null,
|
||||
transition: null,
|
||||
isDragging: false,
|
||||
}
|
||||
return (
|
||||
<ListItem
|
||||
dense
|
||||
@@ -64,18 +44,12 @@ export const RuleItem = (props: Props) => {
|
||||
? alpha(palette.error.main, 0.3)
|
||||
: alpha(palette.success.main, 0.3),
|
||||
height: '100%',
|
||||
margin: '8px 0',
|
||||
borderRadius: '8px',
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 'calc(infinity)' : undefined,
|
||||
})}
|
||||
>
|
||||
<ListItemText
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
ref={setNodeRef}
|
||||
sx={{ cursor: sortable ? 'move' : '' }}
|
||||
data-sortable-handle
|
||||
sx={{ cursor: isSortable ? 'move' : undefined }}
|
||||
primary={
|
||||
<StyledPrimary
|
||||
title={ruleContent || '-'}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
DragDropProvider,
|
||||
type DragOverEvent,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import { SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, isSortableOperation } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -41,6 +39,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
SortableItem,
|
||||
Switch,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
@@ -252,6 +251,16 @@ const PROXY_POLICY_LABEL_KEYS: Record<string, TranslationKey> =
|
||||
{} as Record<string, TranslationKey>,
|
||||
)
|
||||
|
||||
const findRealIndex = (
|
||||
list: string[],
|
||||
filtered: string[],
|
||||
filteredIndex: number,
|
||||
): number => {
|
||||
const item = filtered[filteredIndex]
|
||||
if (item === undefined) return -1
|
||||
return list.indexOf(item)
|
||||
}
|
||||
|
||||
export const RulesEditorViewer = (props: Props) => {
|
||||
const { groupsUid, mergeUid, profileUid, property, open, onClose, onSave } =
|
||||
props
|
||||
@@ -296,20 +305,17 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
const shift = filteredPrependSeq.length > 0 ? 1 : 0
|
||||
if (filteredPrependSeq.length > 0 && index === 0) {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onPrependDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredPrependSeq.map((x) => {
|
||||
return x
|
||||
})}
|
||||
>
|
||||
{filteredPrependSeq.map((item) => {
|
||||
return (
|
||||
<>
|
||||
{filteredPrependSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item}
|
||||
id={`prepend:${item}`}
|
||||
index={itemIndex}
|
||||
group="prepend"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<RuleItem
|
||||
key={item}
|
||||
type="prepend"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
@@ -322,49 +328,48 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
setPrependSeq(prependSeq.filter((v) => v !== item))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else if (index < filteredRuleList.length + shift) {
|
||||
const newIndex = index - shift
|
||||
return (
|
||||
<RuleItem
|
||||
key={filteredRuleList[newIndex]}
|
||||
type={
|
||||
deleteSeq.includes(filteredRuleList[newIndex])
|
||||
? 'delete'
|
||||
: 'original'
|
||||
}
|
||||
ruleRaw={filteredRuleList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredRuleList[newIndex])) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter((v) => v !== filteredRuleList[newIndex]),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, filteredRuleList[newIndex]])
|
||||
<Box sx={{ margin: '8px 0' }}>
|
||||
<RuleItem
|
||||
key={filteredRuleList[newIndex]}
|
||||
type={
|
||||
deleteSeq.includes(filteredRuleList[newIndex])
|
||||
? 'delete'
|
||||
: 'original'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
ruleRaw={filteredRuleList[newIndex]}
|
||||
onDelete={() => {
|
||||
if (deleteSeq.includes(filteredRuleList[newIndex])) {
|
||||
setDeleteSeq(
|
||||
deleteSeq.filter((v) => v !== filteredRuleList[newIndex]),
|
||||
)
|
||||
} else {
|
||||
setDeleteSeq((prev) => [...prev, filteredRuleList[newIndex]])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onAppendDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={filteredAppendSeq.map((x) => {
|
||||
return x
|
||||
})}
|
||||
>
|
||||
{filteredAppendSeq.map((item) => {
|
||||
return (
|
||||
<>
|
||||
{filteredAppendSeq.map((item, itemIndex) => {
|
||||
return (
|
||||
<SortableItem
|
||||
key={item}
|
||||
id={`append:${item}`}
|
||||
index={itemIndex}
|
||||
group="append"
|
||||
style={{ margin: '8px 0' }}
|
||||
>
|
||||
<RuleItem
|
||||
key={item}
|
||||
type="append"
|
||||
ruleRaw={item}
|
||||
onDelete={() => {
|
||||
@@ -377,22 +382,14 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
setAppendSeq(appendSeq.filter((v) => v !== item))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</SortableItem>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 8 },
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
)
|
||||
const reorder = (list: string[], startIndex: number, endIndex: number) => {
|
||||
const result = Array.from(list)
|
||||
const [removed] = result.splice(startIndex, 1)
|
||||
@@ -400,23 +397,73 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
return result
|
||||
}
|
||||
const onPrependDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
const activeIndex = prependSeq.indexOf(active.id.toString())
|
||||
const overIndex = prependSeq.indexOf(over.id.toString())
|
||||
setPrependSeq(reorder(prependSeq, activeIndex, overIndex))
|
||||
}
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(
|
||||
prependSeq,
|
||||
filteredPrependSeq,
|
||||
overIndex,
|
||||
)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setPrependSeq(reorder(prependSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onAppendDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
const activeIndex = appendSeq.indexOf(active.id.toString())
|
||||
const overIndex = appendSeq.indexOf(over.id.toString())
|
||||
setAppendSeq(reorder(appendSeq, activeIndex, overIndex))
|
||||
}
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: overIndex, initialIndex: activeIndex } = source.sortable
|
||||
const activeRealIndex = findRealIndex(
|
||||
appendSeq,
|
||||
filteredAppendSeq,
|
||||
activeIndex,
|
||||
)
|
||||
const overRealIndex = findRealIndex(appendSeq, filteredAppendSeq, overIndex)
|
||||
if (
|
||||
activeRealIndex < 0 ||
|
||||
overRealIndex < 0 ||
|
||||
activeRealIndex === overRealIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setAppendSeq(reorder(appendSeq, activeRealIndex, overRealIndex))
|
||||
}
|
||||
const onDragOver = (event: DragOverEvent) => {
|
||||
const { operation } = event
|
||||
if (!isSortableOperation(operation)) return
|
||||
|
||||
const { source, target } = operation
|
||||
if (source?.group !== target?.group) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const source = event.operation.source
|
||||
if (!isSortable(source)) return
|
||||
|
||||
const { initialGroup, group } = source.sortable
|
||||
if (initialGroup !== group) return
|
||||
|
||||
if (group === 'prepend') {
|
||||
await onPrependDragEnd(event)
|
||||
} else if (group === 'append') {
|
||||
await onAppendDragEnd(event)
|
||||
}
|
||||
}
|
||||
const fetchContent = useCallback(async () => {
|
||||
@@ -806,16 +853,22 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
}}
|
||||
>
|
||||
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
|
||||
<VirtualList
|
||||
count={
|
||||
filteredRuleList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragOver={onDragOver}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<VirtualList
|
||||
count={
|
||||
filteredRuleList.length +
|
||||
(filteredPrependSeq.length > 0 ? 1 : 0) +
|
||||
(filteredAppendSeq.length > 0 ? 1 : 0)
|
||||
}
|
||||
estimateSize={56}
|
||||
renderItem={renderItem}
|
||||
style={{ height: 'calc(100% - 24px)', marginTop: '8px' }}
|
||||
/>
|
||||
</DragDropProvider>
|
||||
</List>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { Box } from '@mui/material'
|
||||
|
||||
import { ProfileItem, type ProfileItemProps } from './profile-item'
|
||||
|
||||
const SORT_TRANSITION = 'transform 160ms cubic-bezier(0.2, 0, 0, 1)'
|
||||
|
||||
type SortableProfileItemProps = Omit<
|
||||
ProfileItemProps,
|
||||
'dragHandleRef' | 'dragHandleAttributes' | 'dragHandleListeners'
|
||||
> & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const SortableProfileItem = ({
|
||||
id,
|
||||
...profileItemProps
|
||||
}: SortableProfileItemProps) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setActivatorNodeRef,
|
||||
setNodeRef,
|
||||
transform,
|
||||
isSorting,
|
||||
isDragging,
|
||||
isOver,
|
||||
} = useSortable({ id, transition: null })
|
||||
|
||||
const isDiagonalMove =
|
||||
transform !== null && transform.x !== 0 && transform.y !== 0
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={setNodeRef}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition:
|
||||
isSorting && !isDragging && isOver && !isDiagonalMove
|
||||
? SORT_TRANSITION
|
||||
: undefined,
|
||||
zIndex: isDragging ? 'calc(infinity)' : undefined,
|
||||
}}
|
||||
>
|
||||
<ProfileItem
|
||||
{...profileItemProps}
|
||||
dragHandleRef={setActivatorNodeRef}
|
||||
dragHandleAttributes={attributes}
|
||||
dragHandleListeners={listeners}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,11 @@
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
type DragEndEvent,
|
||||
DragDropProvider,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable, useSortable } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
ArrowDownward,
|
||||
Delete as DeleteIcon,
|
||||
@@ -31,10 +22,18 @@ import {
|
||||
IconButton,
|
||||
Paper,
|
||||
Typography,
|
||||
keyframes,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import * as yaml from 'js-yaml'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
type Ref,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
closeAllConnections,
|
||||
@@ -54,6 +53,10 @@ import { debugLog } from '@/utils/debug'
|
||||
|
||||
import { rebindProxyChainItems, type ProxyChainItem } from './proxy-chain-model'
|
||||
|
||||
const chainPointerSensor = PointerSensor.configure({
|
||||
activationConstraints: () => undefined,
|
||||
})
|
||||
|
||||
type RuntimeConfigWithProxySequence = IConfigData & { proxies?: unknown }
|
||||
|
||||
interface ParsedChainConfig {
|
||||
@@ -73,7 +76,8 @@ interface ProxyChainProps {
|
||||
selectedGroup?: string | null
|
||||
}
|
||||
|
||||
interface SortableItemProps {
|
||||
interface ProxyChainItemProps {
|
||||
id: string
|
||||
proxy: ProxyChainItem
|
||||
index: number
|
||||
isFirst: boolean
|
||||
@@ -81,6 +85,17 @@ interface SortableItemProps {
|
||||
onRemove: (id: string) => void
|
||||
}
|
||||
|
||||
const arrowFadeIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
`
|
||||
|
||||
const toChainItems = (
|
||||
parsedConfig: ParsedChainConfig | null | undefined,
|
||||
): ProxyChainItem[] => {
|
||||
@@ -96,29 +111,29 @@ const toChainItems = (
|
||||
)
|
||||
}
|
||||
|
||||
const SortableItem = ({
|
||||
interface ChainCardProps {
|
||||
proxy: ProxyChainItem
|
||||
index: number
|
||||
isFirst: boolean
|
||||
isLast: boolean
|
||||
isDragging?: boolean
|
||||
isDropping?: boolean
|
||||
handleRef?: Ref<HTMLElement> | null
|
||||
onRemove?: (id: string) => void
|
||||
}
|
||||
|
||||
const ChainCard = ({
|
||||
proxy,
|
||||
index,
|
||||
isFirst,
|
||||
isLast,
|
||||
isDragging,
|
||||
isDropping,
|
||||
handleRef,
|
||||
onRemove,
|
||||
}: SortableItemProps) => {
|
||||
}: ChainCardProps) => {
|
||||
const theme = useTheme()
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: proxy.id })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}
|
||||
|
||||
const roleLabel = isFirst
|
||||
? t('proxies.page.chain.entryNode')
|
||||
@@ -134,37 +149,31 @@ const SortableItem = ({
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
sx={{
|
||||
mb: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
p: 1,
|
||||
backgroundColor: isDragging
|
||||
? theme.palette.action.selected
|
||||
: theme.palette.background.default,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderRadius: 1,
|
||||
border: roleColor
|
||||
? `1.5px solid ${roleColor}`
|
||||
: `1px solid ${theme.palette.divider}`,
|
||||
boxShadow: isDragging ? theme.shadows[4] : theme.shadows[1],
|
||||
transition: 'box-shadow 0.2s, background-color 0.2s',
|
||||
opacity: proxy.recordId === undefined ? 0.55 : undefined,
|
||||
transition: 'box-shadow 0.2s, background-color 0.2s',
|
||||
boxShadow: isDropping
|
||||
? `0 0 0 2px ${theme.palette.primary.main}66`
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
ref={handleRef}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
mr: 1,
|
||||
color: theme.palette.text.secondary,
|
||||
cursor: 'grab',
|
||||
'&:active': {
|
||||
cursor: 'grabbing',
|
||||
},
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
}}
|
||||
>
|
||||
<DragIndicator />
|
||||
@@ -229,18 +238,87 @@ const SortableItem = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onRemove(proxy.id)}
|
||||
sx={{
|
||||
color: theme.palette.error.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.error.light + '20',
|
||||
{onRemove && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onRemove(proxy.id)}
|
||||
sx={{
|
||||
color: theme.palette.error.main,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.error.light + '20',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const SortableProxyChainItem = ({
|
||||
id,
|
||||
proxy,
|
||||
index,
|
||||
isFirst,
|
||||
isLast,
|
||||
onRemove,
|
||||
}: ProxyChainItemProps) => {
|
||||
const theme = useTheme()
|
||||
const [element, setElement] = useState<Element | null>(null)
|
||||
const handleRef = useRef<HTMLElement | null>(null)
|
||||
const { isDragging, isDropping } = useSortable({
|
||||
id,
|
||||
index,
|
||||
element,
|
||||
handle: handleRef,
|
||||
})
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={setElement}
|
||||
sx={{
|
||||
width: '100%',
|
||||
'&[data-dnd-dragging]': {
|
||||
height: 'auto',
|
||||
boxShadow: 'none !important',
|
||||
'.proxy-chain-arrow': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
},
|
||||
'& .proxy-chain-arrow': {
|
||||
animation: `${arrowFadeIn} 0.25s ease`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ChainCard
|
||||
proxy={proxy}
|
||||
index={index}
|
||||
isFirst={isFirst}
|
||||
isLast={isLast}
|
||||
isDragging={isDragging}
|
||||
isDropping={isDropping}
|
||||
handleRef={handleRef}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
{!isLast && (
|
||||
<Box
|
||||
className="proxy-chain-arrow"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
py: 0.25,
|
||||
}}
|
||||
>
|
||||
<ArrowDownward
|
||||
sx={{
|
||||
fontSize: 20,
|
||||
color: theme.palette.primary.main,
|
||||
opacity: 0.7,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -330,30 +408,25 @@ export const ProxyChain = ({
|
||||
chainLengthRef.current = currentProxyChain.length
|
||||
}, [currentProxyChain.length, markUnsavedChanges])
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 8 },
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
)
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
if (active.id !== over?.id) {
|
||||
const oldIndex = currentProxyChain.findIndex(
|
||||
(item) => item.id === active.id,
|
||||
)
|
||||
const newIndex = currentProxyChain.findIndex(
|
||||
(item) => item.id === over?.id,
|
||||
)
|
||||
|
||||
onUpdateChain(arrayMove(currentProxyChain, oldIndex, newIndex))
|
||||
markUnsavedChanges()
|
||||
const { index: newIndex, initialIndex: oldIndex } = source.sortable
|
||||
if (
|
||||
oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= currentProxyChain.length ||
|
||||
newIndex >= currentProxyChain.length ||
|
||||
oldIndex === newIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
onUpdateChain(arrayMove(currentProxyChain, oldIndex, newIndex))
|
||||
markUnsavedChanges()
|
||||
},
|
||||
[currentProxyChain, onUpdateChain, markUnsavedChanges],
|
||||
)
|
||||
@@ -594,56 +667,27 @@ export const ProxyChain = ({
|
||||
<Typography>{t('proxies.page.chain.empty')}</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
<DragDropProvider
|
||||
sensors={[chainPointerSensor, KeyboardSensor]}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={currentProxyChain.map((proxy) => proxy.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
borderRadius: 1,
|
||||
minHeight: 60,
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
{currentProxyChain.map((proxy, index) => (
|
||||
<Box key={proxy.id}>
|
||||
<SortableItem
|
||||
proxy={proxy}
|
||||
index={index}
|
||||
isFirst={index === 0}
|
||||
isLast={
|
||||
index === currentProxyChain.length - 1 &&
|
||||
currentProxyChain.length > 1
|
||||
}
|
||||
onRemove={handleRemoveProxy}
|
||||
/>
|
||||
{index < currentProxyChain.length - 1 && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
py: 0.25,
|
||||
}}
|
||||
>
|
||||
<ArrowDownward
|
||||
sx={{
|
||||
fontSize: 20,
|
||||
color: theme.palette.primary.main,
|
||||
opacity: 0.7,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<Box sx={{ borderRadius: 1, minHeight: 60, p: 1 }}>
|
||||
{currentProxyChain.map((proxy, index) => (
|
||||
<SortableProxyChainItem
|
||||
key={proxy.id}
|
||||
id={proxy.id}
|
||||
proxy={proxy}
|
||||
index={index}
|
||||
isFirst={index === 0}
|
||||
isLast={
|
||||
index === currentProxyChain.length - 1 &&
|
||||
currentProxyChain.length > 1
|
||||
}
|
||||
onRemove={handleRemoveProxy}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</DragDropProvider>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { LanguageRounded } from '@mui/icons-material'
|
||||
import { Box, Divider, MenuItem, Menu, styled, alpha } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
@@ -16,29 +14,12 @@ import { showNotice } from '@/services/notice-service'
|
||||
import { TestBox } from './test-box'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
itemData: IVergeTestItem
|
||||
onEdit: () => void
|
||||
onDelete: (uid: string) => void
|
||||
}
|
||||
|
||||
export const TestItem = ({
|
||||
id,
|
||||
itemData,
|
||||
onEdit,
|
||||
onDelete: removeTest,
|
||||
}: Props) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id,
|
||||
})
|
||||
|
||||
export const TestItem = ({ itemData, onEdit, onDelete: removeTest }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [anchorEl, setAnchorEl] = useState<any>(null)
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 })
|
||||
@@ -77,14 +58,7 @@ export const TestItem = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 'calc(infinity)' : undefined,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<TestBox
|
||||
onContextMenu={(event) => {
|
||||
const { clientX, clientY } = event
|
||||
@@ -93,12 +67,7 @@ export const TestItem = ({
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ position: 'relative', cursor: 'move' }}
|
||||
ref={setNodeRef}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<Box data-sortable-handle sx={{ position: 'relative', cursor: 'move' }}>
|
||||
{icon && icon.trim() !== '' ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{icon.trim().startsWith('http') && (
|
||||
|
||||
+36
-87
@@ -1,17 +1,4 @@
|
||||
import {
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
} from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { DragDropProvider, KeyboardSensor, PointerSensor } from '@dnd-kit/react'
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
@@ -23,7 +10,6 @@ import {
|
||||
} from '@mui/material'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import type { CSSProperties } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Outlet, useNavigate } from 'react-router'
|
||||
@@ -31,7 +17,7 @@ import { Outlet, useNavigate } from 'react-router'
|
||||
import iconDark from '@/assets/image/icon_dark.svg?react'
|
||||
import iconLight from '@/assets/image/icon_light.svg?react'
|
||||
import LogoSvg from '@/assets/image/logo.svg?react'
|
||||
import { BaseErrorBoundary } from '@/components/base'
|
||||
import { BaseErrorBoundary, SortableItem } from '@/components/base'
|
||||
import { LayoutItem } from '@/components/layout/layout-item'
|
||||
import { LayoutTraffic } from '@/components/layout/layout-traffic'
|
||||
import { NoticeManager } from '@/components/layout/notice-manager'
|
||||
@@ -68,43 +54,22 @@ type MenuContextPosition = { top: number; left: number }
|
||||
interface SortableNavMenuItemProps {
|
||||
item: NavItem
|
||||
label: string
|
||||
index: number
|
||||
}
|
||||
|
||||
const SortableNavMenuItem = ({ item, label }: SortableNavMenuItemProps) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({
|
||||
id: item.path,
|
||||
})
|
||||
|
||||
const style: CSSProperties = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
style.zIndex = 100
|
||||
}
|
||||
|
||||
const SortableNavMenuItem = ({
|
||||
item,
|
||||
label,
|
||||
index,
|
||||
}: SortableNavMenuItemProps) => {
|
||||
return (
|
||||
<LayoutItem
|
||||
to={item.path}
|
||||
icon={item.icon}
|
||||
sortable={{
|
||||
setNodeRef,
|
||||
attributes,
|
||||
listeners,
|
||||
style,
|
||||
isDragging,
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</LayoutItem>
|
||||
<SortableItem id={item.path} index={index}>
|
||||
{(sortable) => (
|
||||
<LayoutItem to={item.path} icon={item.icon} sortable={sortable}>
|
||||
{label}
|
||||
</LayoutItem>
|
||||
)}
|
||||
</SortableItem>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,17 +96,6 @@ const Layout = () => {
|
||||
const windowControlsRef = useRef<any>(null)
|
||||
const { decorated } = useWindowDecorations()
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 6,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
)
|
||||
|
||||
const handleMenuOrderOptimisticUpdate = useCallback(
|
||||
(order: string[]) => {
|
||||
mutateVerge(
|
||||
@@ -365,32 +319,27 @@ const Layout = () => {
|
||||
)}
|
||||
|
||||
{menuUnlocked ? (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleMenuDragEnd}
|
||||
>
|
||||
<SortableContext items={menuOrder}>
|
||||
<List
|
||||
className="the-menu"
|
||||
onContextMenu={handleMenuContextMenu}
|
||||
>
|
||||
{menuOrder.map((path) => {
|
||||
const item = navItemMap.get(path)
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<SortableNavMenuItem
|
||||
key={item.path}
|
||||
item={item}
|
||||
label={t(item.label)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<List className="the-menu" onContextMenu={handleMenuContextMenu}>
|
||||
<DragDropProvider
|
||||
sensors={[PointerSensor, KeyboardSensor]}
|
||||
onDragEnd={handleMenuDragEnd}
|
||||
>
|
||||
{menuOrder.map((path) => {
|
||||
const item = navItemMap.get(path)
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<SortableNavMenuItem
|
||||
key={item.path}
|
||||
item={item}
|
||||
label={t(item.label)}
|
||||
index={menuOrder.indexOf(path)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</DragDropProvider>
|
||||
</List>
|
||||
) : (
|
||||
<List className="the-menu" onContextMenu={handleMenuContextMenu}>
|
||||
{menuOrder.map((path) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { DragEndEvent } from '@dnd-kit/core'
|
||||
import { arrayMove } from '@dnd-kit/sortable'
|
||||
import { arrayMove } from '@dnd-kit/helpers'
|
||||
import type { DragEndEvent } from '@dnd-kit/react'
|
||||
import { isSortable } from '@dnd-kit/react/sortable'
|
||||
import { useCallback, useEffect, useMemo, useReducer } from 'react'
|
||||
|
||||
type MenuOrderAction = { type: 'sync'; payload: string[] }
|
||||
@@ -92,21 +93,26 @@ export const useNavMenuOrder = <T extends { path: string }>({
|
||||
if (!enabled) {
|
||||
return
|
||||
}
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
|
||||
const { active, over } = event
|
||||
if (!over || active.id === over.id) {
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: newIndex, initialIndex: oldIndex } = source.sortable
|
||||
if (
|
||||
oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= menuOrder.length ||
|
||||
newIndex >= menuOrder.length ||
|
||||
oldIndex === newIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const activeId = String(active.id)
|
||||
const overId = String(over.id)
|
||||
const activeId = menuOrder[oldIndex]
|
||||
const overId = menuOrder[newIndex]
|
||||
|
||||
const oldIndex = menuOrder.indexOf(activeId)
|
||||
const newIndex = menuOrder.indexOf(overId)
|
||||
|
||||
if (oldIndex === -1 || newIndex === -1) {
|
||||
return
|
||||
}
|
||||
if (activeId == null || overId == null || activeId === overId) return
|
||||
|
||||
const previousOrder = [...menuOrder]
|
||||
const nextOrder = arrayMove(menuOrder, oldIndex, newIndex)
|
||||
|
||||
+121
-161
@@ -1,20 +1,10 @@
|
||||
import {
|
||||
closestCenter,
|
||||
type CollisionDetection,
|
||||
DndContext,
|
||||
type DragEndEvent,
|
||||
DragOverlay,
|
||||
DragDropProvider,
|
||||
KeyboardSensor,
|
||||
pointerWithin,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
type SortingStrategy,
|
||||
} from '@dnd-kit/sortable'
|
||||
type DragEndEvent,
|
||||
} from '@dnd-kit/react'
|
||||
import { isSortable } from '@dnd-kit/react/sortable'
|
||||
import {
|
||||
CheckBoxOutlineBlankRounded,
|
||||
CheckBoxRounded,
|
||||
@@ -42,12 +32,12 @@ import {
|
||||
BaseStyledTextField,
|
||||
type DialogRef,
|
||||
} from '@/components/base'
|
||||
import { ProfileItem } from '@/components/profile/profile-item'
|
||||
import { ProfileMore } from '@/components/profile/profile-more'
|
||||
import {
|
||||
ProfileViewer,
|
||||
type ProfileViewerRef,
|
||||
} from '@/components/profile/profile-viewer'
|
||||
import { SortableProfileItem } from '@/components/profile/sortable-profile-item'
|
||||
import { ConfigViewer } from '@/components/setting/mods/config-viewer'
|
||||
import { useListen } from '@/hooks/use-listen'
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
@@ -78,48 +68,9 @@ import { debugLog } from '@/utils/debug'
|
||||
// 与 src-tauri/src/main.rs 的 worker_limit 上限(8)保持一致,避免前后端更新风暴不对齐
|
||||
const PROFILE_UPDATE_WORKER_LIMIT = 8
|
||||
const PROFILE_SWITCH_LOADING_DELAY = 400
|
||||
|
||||
const profileCollisionDetection: CollisionDetection = (args) => {
|
||||
const pointerCollisions = pointerWithin(args)
|
||||
return pointerCollisions.length > 0 ? pointerCollisions : closestCenter(args)
|
||||
}
|
||||
|
||||
// Equivalent to rectSortingStrategy without copying the full rect array for every item.
|
||||
const profileRectSortingStrategy: SortingStrategy = ({
|
||||
rects,
|
||||
activeIndex,
|
||||
overIndex,
|
||||
index,
|
||||
}) => {
|
||||
let newIndex = index
|
||||
|
||||
if (index === activeIndex) {
|
||||
newIndex = overIndex
|
||||
} else if (
|
||||
activeIndex < overIndex &&
|
||||
index > activeIndex &&
|
||||
index <= overIndex
|
||||
) {
|
||||
newIndex = index - 1
|
||||
} else if (
|
||||
activeIndex > overIndex &&
|
||||
index >= overIndex &&
|
||||
index < activeIndex
|
||||
) {
|
||||
newIndex = index + 1
|
||||
}
|
||||
|
||||
const oldRect = rects[index]
|
||||
const newRect = rects[newIndex]
|
||||
if (!oldRect || !newRect) return null
|
||||
|
||||
return {
|
||||
x: newRect.left - oldRect.left,
|
||||
y: newRect.top - oldRect.top,
|
||||
scaleX: newRect.width / oldRect.width,
|
||||
scaleY: newRect.height / oldRect.height,
|
||||
}
|
||||
}
|
||||
const profilePointerSensor = PointerSensor.configure({
|
||||
activationConstraints: () => undefined,
|
||||
})
|
||||
|
||||
interface ProfileSwitchRequest {
|
||||
profile: string
|
||||
@@ -138,6 +89,7 @@ const ProfilePage = () => {
|
||||
const { addListener } = useListen()
|
||||
const [url, setUrl] = useState('')
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
const [profileDndRevision, setProfileDndRevision] = useState(0)
|
||||
const [activatings, setActivatings] = useState<string[]>([])
|
||||
const [switchTarget, setSwitchTarget] = useState<string | null>(null)
|
||||
const [visibleSwitchingProfile, setVisibleSwitchingProfile] = useState<
|
||||
@@ -165,14 +117,6 @@ const ProfilePage = () => {
|
||||
)
|
||||
const currentProfileRef = useRef<string | undefined>(undefined)
|
||||
const profilePageMountedRef = useRef(true)
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 8 },
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
)
|
||||
const { current } = location.state || {}
|
||||
|
||||
const {
|
||||
@@ -268,7 +212,7 @@ const ProfilePage = () => {
|
||||
|
||||
const type1 = ['local', 'remote']
|
||||
|
||||
return items.filter((i) => i && type1.includes(i.type!))
|
||||
return items.filter((i) => i?.type && type1.includes(i.type))
|
||||
}, [profiles])
|
||||
|
||||
const currentActivatings = () => {
|
||||
@@ -366,12 +310,31 @@ const ProfilePage = () => {
|
||||
}
|
||||
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
await reorderProfile(active.id.toString(), over.id.toString())
|
||||
mutateProfiles()
|
||||
}
|
||||
const { operation, canceled } = event
|
||||
const { source, target } = operation
|
||||
if (canceled || !target || !isSortable(source)) return
|
||||
|
||||
const { index: newIndex, initialIndex: oldIndex } = source.sortable
|
||||
if (
|
||||
oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= profileItems.length ||
|
||||
newIndex >= profileItems.length ||
|
||||
oldIndex === newIndex
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const activeId = profileItems[oldIndex]?.uid
|
||||
const overId = profileItems[newIndex]?.uid
|
||||
if (activeId == null || overId == null || activeId === overId) return
|
||||
|
||||
try {
|
||||
await reorderProfile(activeId, overId)
|
||||
mutateProfiles()
|
||||
} catch (error) {
|
||||
setProfileDndRevision((revision) => revision + 1)
|
||||
showNotice.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -933,100 +896,97 @@ const ProfilePage = () => {
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={profileCollisionDetection}
|
||||
onDragEnd={onDragEnd}
|
||||
<Box
|
||||
sx={{
|
||||
pl: '10px',
|
||||
pr: '10px',
|
||||
height: 'calc(100% - 48px)',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
pl: '10px',
|
||||
pr: '10px',
|
||||
height: 'calc(100% - 48px)',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
<DragDropProvider
|
||||
key={profileDndRevision}
|
||||
sensors={[profilePointerSensor, KeyboardSensor]}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<Box sx={{ mb: 1.5 }}>
|
||||
<Grid container spacing={{ xs: 1, lg: 1 }}>
|
||||
<SortableContext
|
||||
strategy={profileRectSortingStrategy}
|
||||
items={profileItems.map((x) => {
|
||||
return x.uid
|
||||
})}
|
||||
>
|
||||
{profileItems.map((item) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={item.file}>
|
||||
<SortableProfileItem
|
||||
id={item.uid}
|
||||
selected={(switchTarget ?? profiles.current) === item.uid}
|
||||
activating={
|
||||
activatings.includes(item.uid) ||
|
||||
visibleSwitchingProfile === item.uid
|
||||
}
|
||||
itemData={item}
|
||||
timerUpdateRevision={
|
||||
timerUpdateRevisions.get(item.uid) ?? 0
|
||||
}
|
||||
completedUpdateRevision={
|
||||
completedUpdateRevisions.get(item.uid) ?? 0
|
||||
}
|
||||
mutateProfiles={mutateProfiles}
|
||||
onSelect={(f) => onSelect(item.uid, f)}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr && profiles.current === item.uid) {
|
||||
await onEnhance(false)
|
||||
}
|
||||
}}
|
||||
onDelete={() => {
|
||||
if (batchMode) {
|
||||
toggleProfileSelection(item.uid)
|
||||
} else {
|
||||
onDelete(item.uid)
|
||||
}
|
||||
}}
|
||||
batchMode={batchMode}
|
||||
isSelected={selectedProfiles.has(item.uid)}
|
||||
onSelectionChange={() => toggleProfileSelection(item.uid)}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</SortableContext>
|
||||
</Grid>
|
||||
<Box
|
||||
sx={{
|
||||
mb: 1.5,
|
||||
display: 'grid',
|
||||
overflow: 'hidden',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))',
|
||||
gap: 1,
|
||||
px: 0.5,
|
||||
}}
|
||||
>
|
||||
{profileItems.map((item, index) => (
|
||||
<ProfileItem
|
||||
key={item.uid}
|
||||
id={item.uid}
|
||||
index={index}
|
||||
selected={(switchTarget ?? profiles.current) === item.uid}
|
||||
activating={
|
||||
activatings.includes(item.uid) ||
|
||||
visibleSwitchingProfile === item.uid
|
||||
}
|
||||
itemData={item}
|
||||
timerUpdateRevision={timerUpdateRevisions.get(item.uid) ?? 0}
|
||||
completedUpdateRevision={
|
||||
completedUpdateRevisions.get(item.uid) ?? 0
|
||||
}
|
||||
mutateProfiles={mutateProfiles}
|
||||
onSelect={(f) => onSelect(item.uid, f)}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr && profiles.current === item.uid) {
|
||||
await onEnhance(false)
|
||||
}
|
||||
}}
|
||||
onDelete={() => {
|
||||
if (batchMode) {
|
||||
toggleProfileSelection(item.uid)
|
||||
} else {
|
||||
onDelete(item.uid)
|
||||
}
|
||||
}}
|
||||
batchMode={batchMode}
|
||||
isSelected={selectedProfiles.has(item.uid)}
|
||||
onSelectionChange={() => toggleProfileSelection(item.uid)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Divider
|
||||
variant="middle"
|
||||
flexItem
|
||||
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
|
||||
></Divider>
|
||||
<Box sx={{ mt: 1.5, mb: '10px' }}>
|
||||
<Grid container spacing={{ xs: 1, lg: 1 }}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
||||
<ProfileMore
|
||||
id="Merge"
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr) {
|
||||
await onEnhance(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
||||
<ProfileMore
|
||||
id="Script"
|
||||
logInfo={chainLogs['Script']}
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr) {
|
||||
await onEnhance(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</DragDropProvider>
|
||||
<Divider
|
||||
variant="middle"
|
||||
flexItem
|
||||
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
|
||||
></Divider>
|
||||
<Box sx={{ mt: 1.5, mb: '10px' }}>
|
||||
<Grid container spacing={{ xs: 1, lg: 1 }}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
||||
<ProfileMore
|
||||
id="Merge"
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr) {
|
||||
await onEnhance(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Box>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
|
||||
<ProfileMore
|
||||
id="Script"
|
||||
logInfo={chainLogs['Script']}
|
||||
onSave={async (prev, curr) => {
|
||||
if (prev !== curr) {
|
||||
await onEnhance(false)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
<DragOverlay />
|
||||
</DndContext>
|
||||
</Box>
|
||||
|
||||
<ProfileViewer
|
||||
ref={viewerRef}
|
||||
|
||||
Reference in New Issue
Block a user