diff --git a/src/containers/MainBody/ServerList/Item.tsx b/src/containers/MainBody/ServerList/Item.tsx
index f82845d5..f2333ff9 100644
--- a/src/containers/MainBody/ServerList/Item.tsx
+++ b/src/containers/MainBody/ServerList/Item.tsx
@@ -1,6 +1,6 @@
import { t } from "i18next";
-import { memo, useRef } from "react";
-import { Pressable, StyleSheet, View } from "react-native";
+import { memo, useRef, useState } from "react";
+import { Pressable, StyleSheet, View, PanResponder } from "react-native";
import Icon from "../../../components/Icon";
import Text from "../../../components/Text";
import { images } from "../../../constants/images";
@@ -15,15 +15,98 @@ interface IProps {
index: number;
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 } = props;
+ const {
+ server,
+ index,
+ isDraggable = false,
+ isDraggedOver = 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 onDoublePress = () => {
setServer(server);
showPrompt(true);
@@ -60,6 +143,8 @@ const ServerItem = memo((props: IProps) => {
};
const onPress = () => {
+ if (hasStartedDragging.current) return;
+
var delta = new Date().getTime() - lastPressTime.current;
if (delta < 500) {
@@ -74,135 +159,170 @@ const ServerItem = memo((props: IProps) => {
};
return (
- onPress()}
- // @ts-ignore
- onContextMenu={(e) => {
- e.preventDefault();
- showContextMenu({ x: e.clientX, y: e.clientY }, server);
- return e;
- }}
+
-
+ {/* Drop indicator */}
+ {isDraggedOver && (
-
-
-
- {server.usingOmp && (
-
-
-
-
-
- )}
+ style={{
+ height: 2,
+ backgroundColor: theme.primary,
+ width: "100%",
+ marginBottom: 2,
+ }}
+ />
+ )}
+
+ 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}
+
+
+
-
-
+
+
);
});
const styles = StyleSheet.create({
pressableContainer: {
- // @ts-ignore
- cursor: "default",
marginTop: sc(7),
},
serverContainer: {
diff --git a/src/containers/MainBody/ServerList/List.tsx b/src/containers/MainBody/ServerList/List.tsx
index f0e9e705..4af21b5e 100644
--- a/src/containers/MainBody/ServerList/List.tsx
+++ b/src/containers/MainBody/ServerList/List.tsx
@@ -9,13 +9,14 @@ interface IProps {
data: Server[];
renderItem: (item: Server, index: number) => JSX.Element;
containerStyle?: StyleProp;
+ listRef?: React.RefObject;
}
const List = (props: IProps) => {
const { themeType } = useTheme();
return (
-
+
{
const { startQuery, stopQuery } = useQuery();
- const { favorites, updateInFavoritesList } = usePersistentServers();
+ 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);
useEffect(() => {
return () => {
@@ -47,9 +54,63 @@ const Favorites = () => {
startQuery(server, "favorites");
};
+ const handleDragStart = (server: Server, index: number) => {
+ setDraggedItem({ server, index });
+ setDragOverIndex(index);
+ };
+
+ 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);
+ };
+
+ 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 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]);
+
return (
(
{
server={item}
index={index}
onSelect={(server) => onSelect(server)}
+ isDraggable={isDraggable}
+ onDragStart={handleDragStart}
+ onDragEnd={handleDragEnd}
+ onDragMove={handleDragMove}
+ isDraggedOver={dragOverIndex === index}
+ isBeingDragged={draggedItem?.index === index}
/>
)}
/>
diff --git a/src/states/servers.ts b/src/states/servers.ts
index 24487043..6483d776 100644
--- a/src/states/servers.ts
+++ b/src/states/servers.ts
@@ -33,6 +33,7 @@ interface ServersPersistentState {
version: SAMPDLLVersions | undefined
) => void;
getServerSettings: (server: Server) => PerServerSettings | undefined;
+ reorderFavorites: (fromIndex: number, toIndex: number) => void;
}
const useServers = create()((set, get) => ({
@@ -208,6 +209,18 @@ const usePersistentServers = create()(
return undefined;
}
},
+ reorderFavorites: (fromIndex, toIndex) =>
+ set(() => {
+ const list = [...get().favorites];
+ const [movedItem] = list.splice(fromIndex, 1);
+ list.splice(toIndex, 0, movedItem);
+
+ setTimeout(() => {
+ emit("reorderFavorites", { fromIndex, toIndex });
+ }, 200);
+
+ return { favorites: list };
+ }),
}),
{
name: "favorites-and-recentlyjoined-storage",
@@ -246,4 +259,10 @@ listen("updateInRecentlyJoinedList", (ev) => {
}
});
+listen("reorderFavorites", (ev) => {
+ if (ev.windowLabel != appWindow.label) {
+ usePersistentServers.persist.rehydrate();
+ }
+});
+
export { usePersistentServers, useServers };