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 };