diff --git a/package.json b/package.json
index 51048e59..8065e642 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,9 @@
"release": "tauri build -t i686-pc-windows-msvc"
},
"dependencies": {
+ "@dnd-kit/core": "^6.3.1",
+ "@dnd-kit/sortable": "^10.0.0",
+ "@dnd-kit/utilities": "^3.2.2",
"@react-native-clipboard/clipboard": "^1.14.1",
"@tauri-apps/api": "^1.6.0",
"axios": "^1.7.2",
@@ -44,4 +47,4 @@
"typescript": "^5.3.3",
"vite": "^5.3.3"
}
-}
\ No newline at end of file
+}
diff --git a/src/containers/MainBody/ServerList/Item.tsx b/src/containers/MainBody/ServerList/Item.tsx
index f2333ff9..f08a4d33 100644
--- a/src/containers/MainBody/ServerList/Item.tsx
+++ b/src/containers/MainBody/ServerList/Item.tsx
@@ -1,9 +1,10 @@
import { t } from "i18next";
-import { memo, useRef, useState } from "react";
-import { Pressable, StyleSheet, View, PanResponder } from "react-native";
+import { memo, useRef } from "react";
+import { Pressable, StyleSheet, View } from "react-native";
import Icon from "../../../components/Icon";
import Text from "../../../components/Text";
import { images } from "../../../constants/images";
+import { useDraggableItem } from "../../../hooks/draggable";
import { useContextMenu } from "../../../states/contextMenu";
import { useJoinServerPrompt } from "../../../states/joinServerPrompt";
import { useTheme } from "../../../states/theme";
@@ -16,102 +17,38 @@ interface IProps {
isSelected?: boolean;
onSelect?: (server: Server) => void;
isDraggable?: boolean;
- onDragStart?: (server: Server, index: number) => void;
- onDragEnd?: () => void;
- onDragMove?: (index: number, y: number) => void;
- isDraggedOver?: boolean;
isBeingDragged?: boolean;
}
const ServerItem = memo((props: IProps) => {
- const {
- server,
- index,
- isDraggable = false,
- isDraggedOver = false,
- isBeingDragged = false,
- } = props;
+ const { server, index, isDraggable = false, isBeingDragged = false } = props;
const { theme, themeType } = useTheme();
const lastPressTime = useRef(0);
const { showPrompt, setServer } = useJoinServerPrompt();
const { show: showContextMenu } = useContextMenu();
- const [isDragging, setIsDragging] = useState(false);
- const dragStartTime = useRef(0);
- const hasStartedDragging = useRef(false);
-
- const panResponder = PanResponder.create({
- onStartShouldSetPanResponder: () => isDraggable,
-
- onMoveShouldSetPanResponder: (_, gestureState) => {
- if (!isDraggable) return false;
-
- const distance = Math.sqrt(
- gestureState.dx * gestureState.dx + gestureState.dy * gestureState.dy
- );
-
- if (hasStartedDragging.current) return true;
-
- if (distance > 2) {
- const isMainlyVertical =
- Math.abs(gestureState.dy) > Math.abs(gestureState.dx) * 0.5;
- return isMainlyVertical;
- }
-
- return false;
- },
-
- onPanResponderGrant: (evt) => {
- if (isDraggable && props.onDragStart) {
- hasStartedDragging.current = true;
- setIsDragging(true);
- document.body.style.cursor = "grabbing";
- dragStartTime.current = Date.now();
- props.onDragStart(server, index);
-
- if (props.onDragMove) {
- props.onDragMove(index, evt.nativeEvent.pageY);
- }
- }
- },
-
- onPanResponderMove: (evt) => {
- if (isDraggable && hasStartedDragging.current && props.onDragMove) {
- props.onDragMove(index, evt.nativeEvent.pageY);
- }
- },
-
- onPanResponderRelease: () => {
- if (isDraggable && hasStartedDragging.current) {
- hasStartedDragging.current = false;
- setIsDragging(false);
- document.body.style.cursor = ""; // Add this
- if (props.onDragEnd) {
- props.onDragEnd();
- }
- }
- },
-
- onPanResponderTerminate: () => {
- if (isDraggable && hasStartedDragging.current) {
- hasStartedDragging.current = false;
- setIsDragging(false);
- document.body.style.cursor = ""; // Add this
- if (props.onDragEnd) {
- props.onDragEnd();
- }
- }
- },
-
- onShouldBlockNativeResponder: () =>
- isDraggable && hasStartedDragging.current,
- });
+ const { attributes, listeners, setNodeRef, isDragging, style } =
+ useDraggableItem(`${server.ip}:${server.port}`, isDraggable);
const onDoublePress = () => {
setServer(server);
showPrompt(true);
};
+ const onPress = () => {
+ if (isDragging) return;
+
+ const delta = Date.now() - lastPressTime.current;
+
+ if (delta < 500) {
+ lastPressTime.current = 0;
+ onDoublePress();
+ } else {
+ lastPressTime.current = Date.now();
+ props.onSelect?.(server);
+ }
+ };
+
const getServerStatusIconColor = () => {
if (server.hasPassword) {
return "#eb4034";
@@ -142,182 +79,169 @@ const ServerItem = memo((props: IProps) => {
}
};
- const onPress = () => {
- if (hasStartedDragging.current) return;
-
- var delta = new Date().getTime() - lastPressTime.current;
-
- if (delta < 500) {
- lastPressTime.current = 0;
- onDoublePress();
- } else {
- lastPressTime.current = new Date().getTime();
- if (props.onSelect) {
- props.onSelect(server);
- }
- }
- };
-
return (
-
- {/* Drop indicator */}
- {isDraggedOver && (
-
- )}
-
- onPress()}
- // @ts-ignore
- onContextMenu={(e) => {
- e.preventDefault();
- showContextMenu({ x: e.clientX, y: e.clientY }, server);
- return e;
- }}
>
-
-
-
-
-
- {server.usingOmp && (
-
-
-
-
-
- )}
+ onPress()}
+ // @ts-ignore
+ onContextMenu={(e) => {
+ e.preventDefault();
+ showContextMenu({ x: e.clientX, y: e.clientY }, server);
+ return e;
+ }}
+ >
+
-
- {server.hostname}
-
+
+ {server.usingOmp && (
+
+
+
+
+
+ )}
-
- {server.ping === 9999 ? "-" : server.ping}
-
-
-
- {server.gameMode}
+ {server.hostname}
-
- {server.playerCount}
+
- /{server.maxPlayers}
+ {server.ping === 9999 ? "-" : server.ping}
-
+
+
+
+ {server.gameMode}
+
+
+
+
+ {server.playerCount}
+
+ /{server.maxPlayers}
+
+
+
-
-
-
+
+
+
);
});
diff --git a/src/containers/MainBody/ServerList/Tabs/Favorites.tsx b/src/containers/MainBody/ServerList/Tabs/Favorites.tsx
index 6372fdc8..b2e7bcd7 100644
--- a/src/containers/MainBody/ServerList/Tabs/Favorites.tsx
+++ b/src/containers/MainBody/ServerList/Tabs/Favorites.tsx
@@ -1,4 +1,21 @@
-import { useEffect, useMemo, useState, useRef } from "react";
+import { useEffect, useMemo, useState } from "react";
+
+import {
+ DndContext,
+ closestCenter,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+ DragEndEvent,
+} from '@dnd-kit/core';
+
+import {
+ SortableContext,
+ sortableKeyboardCoordinates,
+ verticalListSortingStrategy,
+} from '@dnd-kit/sortable';
+
import { useQuery } from "../../../../hooks/query";
import { useGenericTempState } from "../../../../states/genericStates";
import { usePersistentServers, useServers } from "../../../../states/servers";
@@ -6,19 +23,14 @@ import { sortAndSearchInServerList } from "../../../../utils/helpers";
import { Server } from "../../../../utils/types";
import List from "../List";
import ServerItem from "./../Item";
-import { sc } from "../../../../utils/sizeScaler";
const Favorites = () => {
const { startQuery, stopQuery } = useQuery();
const { favorites, updateInFavoritesList, reorderFavorites } = usePersistentServers();
const { selected, setSelected } = useServers();
const { searchData } = useGenericTempState();
-
- // Drag functionality state
- const [draggedItem, setDraggedItem] = useState<{server: Server, index: number} | null>(null);
- const [dragOverIndex, setDragOverIndex] = useState(null);
- const itemHeight = sc(39);
- const listRef = useRef(null);
+
+ const [activeId, setActiveId] = useState(null);
useEffect(() => {
return () => {
@@ -48,88 +60,107 @@ const Favorites = () => {
favorites,
]);
+ const isDraggable = useMemo(() => {
+ return searchData.query === "" &&
+ searchData.languages.length === 0 &&
+ !searchData.ompOnly &&
+ !searchData.nonEmpty &&
+ !searchData.unpassworded &&
+ searchData.sortPing === "none" &&
+ searchData.sortPlayer === "none" &&
+ searchData.sortName === "none" &&
+ searchData.sortMode === "none";
+ }, [searchData]);
+
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 8,
+ },
+ }),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates,
+ })
+ );
+
const onSelect = (server: Server) => {
stopQuery();
setSelected(server);
startQuery(server, "favorites");
};
- const handleDragStart = (server: Server, index: number) => {
- setDraggedItem({ server, index });
- setDragOverIndex(index);
- };
+ function handleDragStart(event: any) {
+ const { active } = event;
+ setActiveId(active.id);
+ }
- const handleDragEnd = () => {
- if (draggedItem && dragOverIndex !== null && draggedItem.index !== dragOverIndex) {
- const draggedServer = draggedItem.server;
- const targetServer = list[dragOverIndex];
-
- const originalDraggedIndex = favorites.findIndex(
- (fav) => fav.ip === draggedServer.ip && fav.port === draggedServer.port
- );
- const originalTargetIndex = favorites.findIndex(
- (fav) => fav.ip === targetServer.ip && fav.port === targetServer.port
- );
-
- if (originalDraggedIndex !== -1 && originalTargetIndex !== -1) {
- reorderFavorites(originalDraggedIndex, originalTargetIndex);
- }
- }
- setDraggedItem(null);
- setDragOverIndex(null);
- };
+ function handleDragEnd(event: DragEndEvent) {
+ const { active, over } = event;
+
+ if (active.id !== over?.id) {
+ const oldIndex = list.findIndex((server) => `${server.ip}:${server.port}` === active.id);
+ const newIndex = list.findIndex((server) => `${server.ip}:${server.port}` === over?.id);
+
+ if (oldIndex !== -1 && newIndex !== -1) {
+ const draggedServer = list[oldIndex];
+ const targetServer = list[newIndex];
- const handleDragMove = (draggedIndex: number, y: number) => {
- if (listRef.current && draggedItem) {
- const listElement = listRef.current;
- const listRect = listElement.getBoundingClientRect();
- const relativeY = y - listRect.top;
- const headerHeight = 26;
- const adjustedY = relativeY - headerHeight;
- let newHoverIndex = Math.floor(adjustedY / itemHeight);
- newHoverIndex = Math.max(0, Math.min(newHoverIndex, list.length - 1));
-
- if (newHoverIndex !== draggedIndex && newHoverIndex !== dragOverIndex) {
- setDragOverIndex(newHoverIndex);
+ const originalDraggedIndex = favorites.findIndex(
+ (fav) => fav.ip === draggedServer.ip && fav.port === draggedServer.port
+ );
+ const originalTargetIndex = favorites.findIndex(
+ (fav) => fav.ip === targetServer.ip && fav.port === targetServer.port
+ );
+
+ if (originalDraggedIndex !== -1 && originalTargetIndex !== -1) {
+ reorderFavorites(originalDraggedIndex, originalTargetIndex);
+ }
}
}
- };
- const isDraggable = useMemo(() => {
- return searchData.query === "" &&
- searchData.languages.length === 0 &&
- !searchData.ompOnly &&
- !searchData.nonEmpty &&
- !searchData.unpassworded &&
- searchData.sortPing === "none" &&
- searchData.sortPlayer === "none" &&
- searchData.sortName === "none" &&
- searchData.sortMode === "none";
- }, [searchData]);
+ setActiveId(null);
+ }
+
+ const serverIds = list.map(server => `${server.ip}:${server.port}`);
return (
- (
- onSelect(server)}
- isDraggable={isDraggable}
- onDragStart={handleDragStart}
- onDragEnd={handleDragEnd}
- onDragMove={handleDragMove}
- isDraggedOver={dragOverIndex === index}
- isBeingDragged={draggedItem?.index === index}
+ ({
+ ...args.transform,
+ x: 0,
+ })
+ ]}
+ >
+
+ (
+ onSelect(server)}
+ isDraggable={isDraggable}
+ isBeingDragged={activeId === `${item.ip}:${item.port}`}
+ />
+ )}
/>
- )}
- />
+
+
);
};
diff --git a/src/hooks/draggable.ts b/src/hooks/draggable.ts
new file mode 100644
index 00000000..8c37e856
--- /dev/null
+++ b/src/hooks/draggable.ts
@@ -0,0 +1,28 @@
+import { useSortable } from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+
+export const useDraggableItem = (id: string, isDraggable: boolean) => {
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({
+ id,
+ disabled: !isDraggable,
+ animateLayoutChanges: () => false,
+ });
+
+ return {
+ attributes: isDraggable ? attributes : {},
+ listeners: isDraggable ? listeners : {},
+ setNodeRef,
+ isDragging,
+ style: {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ },
+ };
+};
diff --git a/yarn.lock b/yarn.lock
index b5e3d922..be2fb9b9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -841,6 +841,37 @@
"@babel/helper-validator-identifier" "^7.24.7"
to-fast-properties "^2.0.0"
+"@dnd-kit/accessibility@^3.1.1":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz#3b4202bd6bb370a0730f6734867785919beac6af"
+ integrity sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==
+ dependencies:
+ tslib "^2.0.0"
+
+"@dnd-kit/core@^6.3.1":
+ version "6.3.1"
+ resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.3.1.tgz#4c36406a62c7baac499726f899935f93f0e6d003"
+ integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==
+ dependencies:
+ "@dnd-kit/accessibility" "^3.1.1"
+ "@dnd-kit/utilities" "^3.2.2"
+ tslib "^2.0.0"
+
+"@dnd-kit/sortable@^10.0.0":
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz#1f9382b90d835cd5c65d92824fa9dafb78c4c3e8"
+ integrity sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==
+ dependencies:
+ "@dnd-kit/utilities" "^3.2.2"
+ tslib "^2.0.0"
+
+"@dnd-kit/utilities@^3.2.2":
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b"
+ integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==
+ dependencies:
+ tslib "^2.0.0"
+
"@esbuild/aix-ppc64@0.21.5":
version "0.21.5"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f"
@@ -4582,6 +4613,11 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+tslib@^2.0.0:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
tslib@^2.0.1:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"