Skip to content

Commit f63e6ae

Browse files
authored
Ability to reorder servers in the fav tab (#320)
Ability to reorder servers in the favourites tab by dragging them around
1 parent e172808 commit f63e6ae

File tree

4 files changed

+317
-110
lines changed

4 files changed

+317
-110
lines changed

src/containers/MainBody/ServerList/Item.tsx

Lines changed: 227 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { t } from "i18next";
2-
import { memo, useRef } from "react";
3-
import { Pressable, StyleSheet, View } from "react-native";
2+
import { memo, useRef, useState } from "react";
3+
import { Pressable, StyleSheet, View, PanResponder } from "react-native";
44
import Icon from "../../../components/Icon";
55
import Text from "../../../components/Text";
66
import { images } from "../../../constants/images";
@@ -15,15 +15,98 @@ interface IProps {
1515
index: number;
1616
isSelected?: boolean;
1717
onSelect?: (server: Server) => void;
18+
isDraggable?: boolean;
19+
onDragStart?: (server: Server, index: number) => void;
20+
onDragEnd?: () => void;
21+
onDragMove?: (index: number, y: number) => void;
22+
isDraggedOver?: boolean;
23+
isBeingDragged?: boolean;
1824
}
1925

2026
const ServerItem = memo((props: IProps) => {
21-
const { server, index } = props;
27+
const {
28+
server,
29+
index,
30+
isDraggable = false,
31+
isDraggedOver = false,
32+
isBeingDragged = false,
33+
} = props;
2234
const { theme, themeType } = useTheme();
2335
const lastPressTime = useRef(0);
2436
const { showPrompt, setServer } = useJoinServerPrompt();
2537
const { show: showContextMenu } = useContextMenu();
2638

39+
const [isDragging, setIsDragging] = useState(false);
40+
const dragStartTime = useRef(0);
41+
const hasStartedDragging = useRef(false);
42+
43+
const panResponder = PanResponder.create({
44+
onStartShouldSetPanResponder: () => isDraggable,
45+
46+
onMoveShouldSetPanResponder: (_, gestureState) => {
47+
if (!isDraggable) return false;
48+
49+
const distance = Math.sqrt(
50+
gestureState.dx * gestureState.dx + gestureState.dy * gestureState.dy
51+
);
52+
53+
if (hasStartedDragging.current) return true;
54+
55+
if (distance > 2) {
56+
const isMainlyVertical =
57+
Math.abs(gestureState.dy) > Math.abs(gestureState.dx) * 0.5;
58+
return isMainlyVertical;
59+
}
60+
61+
return false;
62+
},
63+
64+
onPanResponderGrant: (evt) => {
65+
if (isDraggable && props.onDragStart) {
66+
hasStartedDragging.current = true;
67+
setIsDragging(true);
68+
document.body.style.cursor = "grabbing";
69+
dragStartTime.current = Date.now();
70+
props.onDragStart(server, index);
71+
72+
if (props.onDragMove) {
73+
props.onDragMove(index, evt.nativeEvent.pageY);
74+
}
75+
}
76+
},
77+
78+
onPanResponderMove: (evt) => {
79+
if (isDraggable && hasStartedDragging.current && props.onDragMove) {
80+
props.onDragMove(index, evt.nativeEvent.pageY);
81+
}
82+
},
83+
84+
onPanResponderRelease: () => {
85+
if (isDraggable && hasStartedDragging.current) {
86+
hasStartedDragging.current = false;
87+
setIsDragging(false);
88+
document.body.style.cursor = ""; // Add this
89+
if (props.onDragEnd) {
90+
props.onDragEnd();
91+
}
92+
}
93+
},
94+
95+
onPanResponderTerminate: () => {
96+
if (isDraggable && hasStartedDragging.current) {
97+
hasStartedDragging.current = false;
98+
setIsDragging(false);
99+
document.body.style.cursor = ""; // Add this
100+
if (props.onDragEnd) {
101+
props.onDragEnd();
102+
}
103+
}
104+
},
105+
106+
onShouldBlockNativeResponder: () =>
107+
isDraggable && hasStartedDragging.current,
108+
});
109+
27110
const onDoublePress = () => {
28111
setServer(server);
29112
showPrompt(true);
@@ -60,6 +143,8 @@ const ServerItem = memo((props: IProps) => {
60143
};
61144

62145
const onPress = () => {
146+
if (hasStartedDragging.current) return;
147+
63148
var delta = new Date().getTime() - lastPressTime.current;
64149

65150
if (delta < 500) {
@@ -74,135 +159,170 @@ const ServerItem = memo((props: IProps) => {
74159
};
75160

76161
return (
77-
<Pressable
78-
key={"server-item-" + index}
79-
style={styles.pressableContainer}
80-
onPress={() => onPress()}
81-
// @ts-ignore
82-
onContextMenu={(e) => {
83-
e.preventDefault();
84-
showContextMenu({ x: e.clientX, y: e.clientY }, server);
85-
return e;
86-
}}
162+
<View
163+
style={[
164+
styles.pressableContainer,
165+
{
166+
// // @ts-ignore
167+
// cursor: isDragging || isDraggedOver ? "grabbing" : "default",
168+
marginTop: 0,
169+
opacity: isDragging || isBeingDragged ? 0.7 : 1,
170+
transform: isDragging ? [{ translateY: -5 }] : [],
171+
zIndex: isDragging ? 1000 : 1,
172+
},
173+
]}
174+
{...(isDraggable ? panResponder.panHandlers : {})}
87175
>
88-
<View style={styles.serverContainer}>
176+
{/* Drop indicator */}
177+
{isDraggedOver && (
89178
<View
90-
style={[
91-
styles.iconContainer,
92-
{
93-
backgroundColor: getServerStatusIconViewBackgroundColor(),
94-
},
95-
]}
96-
>
97-
<Icon
98-
svg
99-
title={getServerStatusIconTitle()}
100-
image={
101-
server.hasPassword ? images.icons.locked : images.icons.unlocked
102-
}
103-
size={sc(20)}
104-
color={getServerStatusIconColor()}
105-
/>
106-
</View>
107-
<View
108-
id={
109-
!props.isSelected
110-
? themeType === "dark"
111-
? "server-list-item-dark"
112-
: "server-list-item-light"
113-
: undefined
114-
}
115-
style={[
116-
{
117-
flexDirection: "row",
118-
alignItems: "center",
119-
flex: 1,
120-
borderRadius: sc(5),
121-
},
122-
{
123-
// borderWidth: props.isSelected ? 1 : 0,
124-
borderColor: theme.primary,
125-
backgroundColor: props.isSelected
126-
? theme.primary + "7D"
127-
: undefined,
128-
},
129-
]}
130-
>
131-
{server.usingOmp && (
132-
<div
133-
style={{
134-
filter: `drop-shadow(0 0 8px ${theme.primary}CC)`,
135-
}}
136-
>
137-
<View style={[styles.iconContainer, { marginHorizontal: sc(3) }]}>
138-
<Icon
139-
title={t("openmp_server")}
140-
image={images.icons.omp}
141-
size={sc(23)}
142-
/>
143-
</View>
144-
</div>
145-
)}
179+
style={{
180+
height: 2,
181+
backgroundColor: theme.primary,
182+
width: "100%",
183+
marginBottom: 2,
184+
}}
185+
/>
186+
)}
187+
188+
<Pressable
189+
key={"server-item-" + index}
190+
style={[
191+
styles.pressableContainer,
192+
{
193+
// @ts-ignore
194+
cursor: isDragging || isDraggedOver ? "grabbing" : "default",
195+
},
196+
]}
197+
onPress={() => onPress()}
198+
// @ts-ignore
199+
onContextMenu={(e) => {
200+
e.preventDefault();
201+
showContextMenu({ x: e.clientX, y: e.clientY }, server);
202+
return e;
203+
}}
204+
>
205+
<View style={styles.serverContainer}>
146206
<View
147207
style={[
148-
styles.commonFieldContainer,
149-
styles.hostNameContainer,
150-
{ paddingLeft: server.usingOmp ? 0 : sc(10) },
208+
styles.iconContainer,
209+
{
210+
backgroundColor: getServerStatusIconViewBackgroundColor(),
211+
// @ts-ignore
212+
userSelect: "none",
213+
},
151214
]}
152215
>
153-
<Text style={{ fontSize: sc(17) }} color={theme.textPrimary}>
154-
{server.hostname}
155-
</Text>
216+
<Icon
217+
svg
218+
title={getServerStatusIconTitle()}
219+
image={
220+
server.hasPassword ? images.icons.locked : images.icons.unlocked
221+
}
222+
size={sc(20)}
223+
color={getServerStatusIconColor()}
224+
/>
156225
</View>
157226
<View
158-
style={{
159-
flex: 0.5,
160-
minWidth: 300,
161-
flexDirection: "row",
162-
marginLeft: server.usingOmp ? -26 : 0,
163-
}}
227+
id={
228+
!props.isSelected
229+
? themeType === "dark"
230+
? "server-list-item-dark"
231+
: "server-list-item-light"
232+
: undefined
233+
}
234+
style={[
235+
{
236+
flexDirection: "row",
237+
alignItems: "center",
238+
flex: 1,
239+
borderRadius: sc(5),
240+
},
241+
{
242+
// borderWidth: props.isSelected ? 1 : 0,
243+
borderColor: theme.primary,
244+
backgroundColor: props.isSelected
245+
? theme.primary + "7D"
246+
: undefined,
247+
},
248+
]}
164249
>
250+
{server.usingOmp && (
251+
<div
252+
style={{
253+
filter: `drop-shadow(0 0 8px ${theme.primary}CC)`,
254+
}}
255+
>
256+
<View
257+
style={[styles.iconContainer, { marginHorizontal: sc(3) }]}
258+
>
259+
<Icon
260+
title={t("openmp_server")}
261+
image={images.icons.omp}
262+
size={sc(23)}
263+
/>
264+
</View>
265+
</div>
266+
)}
165267
<View
166-
style={[styles.commonFieldContainer, styles.pingFieldContainer]}
167-
>
168-
<Text style={{ fontSize: sc(17) }} color={theme.textSecondary}>
169-
{server.ping === 9999 ? "-" : server.ping}
170-
</Text>
171-
</View>
172-
<View
173-
style={[styles.commonFieldContainer, styles.gameModeContainer]}
268+
style={[
269+
styles.commonFieldContainer,
270+
styles.hostNameContainer,
271+
{ paddingLeft: server.usingOmp ? 0 : sc(10) },
272+
]}
174273
>
175274
<Text style={{ fontSize: sc(17) }} color={theme.textPrimary}>
176-
{server.gameMode}
275+
{server.hostname}
177276
</Text>
178277
</View>
179278
<View
180-
style={[
181-
styles.commonFieldContainer,
182-
styles.playersFieldContainer,
183-
]}
279+
style={{
280+
flex: 0.5,
281+
minWidth: 300,
282+
flexDirection: "row",
283+
marginLeft: server.usingOmp ? -26 : 0,
284+
}}
184285
>
185-
<Text style={{ fontSize: sc(17) }} color={theme.textPrimary}>
186-
{server.playerCount}
187-
<Text
188-
style={{ fontSize: sc(17) }}
189-
color={theme.textPrimary + "AA"}
190-
>
191-
/{server.maxPlayers}
286+
<View
287+
style={[styles.commonFieldContainer, styles.pingFieldContainer]}
288+
>
289+
<Text style={{ fontSize: sc(17) }} color={theme.textSecondary}>
290+
{server.ping === 9999 ? "-" : server.ping}
192291
</Text>
193-
</Text>
292+
</View>
293+
<View
294+
style={[styles.commonFieldContainer, styles.gameModeContainer]}
295+
>
296+
<Text style={{ fontSize: sc(17) }} color={theme.textPrimary}>
297+
{server.gameMode}
298+
</Text>
299+
</View>
300+
<View
301+
style={[
302+
styles.commonFieldContainer,
303+
styles.playersFieldContainer,
304+
]}
305+
>
306+
<Text style={{ fontSize: sc(17) }} color={theme.textPrimary}>
307+
{server.playerCount}
308+
<Text
309+
style={{ fontSize: sc(17) }}
310+
color={theme.textPrimary + "AA"}
311+
>
312+
/{server.maxPlayers}
313+
</Text>
314+
</Text>
315+
</View>
194316
</View>
195317
</View>
196318
</View>
197-
</View>
198-
</Pressable>
319+
</Pressable>
320+
</View>
199321
);
200322
});
201323

202324
const styles = StyleSheet.create({
203325
pressableContainer: {
204-
// @ts-ignore
205-
cursor: "default",
206326
marginTop: sc(7),
207327
},
208328
serverContainer: {

0 commit comments

Comments
 (0)