move long-press server menu to actual page

This commit is contained in:
ppc
2025-05-04 19:08:57 +01:00
parent e39a117018
commit d917f8832a
4 changed files with 97 additions and 82 deletions
+57 -24
View File
@@ -1,15 +1,15 @@
import {BSON} from "realm";
import NfcManager from "react-native-nfc-manager";
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useState} from "react";
import {Platform, RefreshControl, ScrollView, Share, StyleSheet, Text, View} from "react-native";
import {ActivityIndicator, Button, IconButton, Paragraph, Title, useTheme} from "react-native-paper";
import {Platform, RefreshControl, ScrollView, Share, StyleSheet, Text, TouchableOpacity, View} from "react-native";
import {
CircleCheck,
CircleDot,
CircleEllipsis,
CreditCard,
Plus,
SmartphoneNfcIcon,
Share as ShareIcon,
WifiOff,
TvMinimalPlay,
Star,
@@ -38,6 +38,7 @@ import {
import ObjectId = BSON.ObjectId;
import {AMCardRecentUse} from "../models/AMCardRecentUse";
import {MenuAction, MenuView, NativeActionEvent} from "@react-native-menu/menu";
const {useRealm, useQuery, useObject} = RealmContext;
@@ -52,8 +53,24 @@ export default function ServerCardIn({route, navigation}) {
query: q => q.sorted("lastUsed", true)
});
// header actions
const menuCallback = useCallback((e: NativeActionEvent) => {
switch (e.nativeEvent.event) {
case "share":
Share.share({url: server.shareUrl});
break;
case "delete":
realm.write(() => {
realm.delete(server);
navigation.goBack();
});
break;
}
}, [realm, server, navigation]);
// nfc support
const [nfcSupported, setNfcSupported] = useState<boolean | null>(Platform.OS === 'android' ? false : null);
const [nfcSupported, setNfcSupported] = useState<boolean | null>(Platform.select({android: false, default: null}));
useEffect(() => {
if (nfcSupported !== null) {
return;
@@ -91,30 +108,20 @@ export default function ServerCardIn({route, navigation}) {
navigation.setOptions({
title: server.serverName,
headerRight: () => (
<TouchableOpacity onPress={() => Share.share({url: server.shareUrl})}>
<ShareIcon color={theme.colors.primary}/>
</TouchableOpacity>
<MenuView actions={menuActions} onPressAction={menuCallback}>
<CircleEllipsis color={theme.colors.primary}/>
</MenuView>
)
});
}, [navigation, server]);
// invalid card/server selection
useEffect(() => {
if (server?.isValid() !== true) {
navigation.goBack();
return;
}
}, [server]);
useEffect(() => {
if (userSelectedCard?.isValid() === false) {
setUserSelectedCard(null);
}
}, [userSelectedCard]);
// handle auto-refresh
useEffect(() => {
if (!serverConnectionSuccess || server?.isValid() !== true) {
if (!serverConnectionSuccess) {
return;
}
@@ -125,6 +132,13 @@ export default function ServerCardIn({route, navigation}) {
}
}, [serverConnectionSuccess, server]);
// invalid card/server selection
useEffect(() => {
if (userSelectedCard?.isValid() === false) {
setUserSelectedCard(null);
}
}, [userSelectedCard]);
// server info update
useEffect(() => {
const abortController = new AbortController();
@@ -239,8 +253,7 @@ export default function ServerCardIn({route, navigation}) {
<View style={styles.container}>
<StatusAlert server={server} connectionEstablished={serverConnectionSuccess}/>
<View style={styles.section}>
<Title
style={styles.sectionTitle}>{server.gameId ? `Play ${getGameName(server.gameId)}` : "Selected Card"}</Title>
<Title style={styles.sectionTitle}>{server.gameId ? `Play ${getGameName(server.gameId)}` : "Selected Card"}</Title>
{selectedCard ? <>
<ICCard actionDisabled
card={selectedCard}
@@ -255,8 +268,7 @@ export default function ServerCardIn({route, navigation}) {
mode={serverConnectionSuccess ? "contained" : "outlined"}
buttonColor={cardInCompleted === false ? "red" : "green"}
onPress={() => cardInCallback(selectedCard)}
icon={() => <SquareArrowOutUpRight color={disableCardIn ? "gray" : "white"}
size={18}/>}>
icon={() => <SquareArrowOutUpRight color={disableCardIn ? "gray" : "white"} size={18}/>}>
{cardInCompleted === null ? "Card In" : cardInCompleted ? "Card In Success" : "Failed"}
</Button>
{nfcSupported === true && (
@@ -265,8 +277,7 @@ export default function ServerCardIn({route, navigation}) {
disabled={disableCardIn}
onPress={performPhysicalCardIn}
style={{borderRadius: 10, margin: 0, height: "100%", flex: 0.2}}
icon={() => <SmartphoneNfc
color={disableCardIn ? "gray" : theme.colors.onBackground} size={18}/>}/>
icon={() => <SmartphoneNfc color={disableCardIn ? "gray" : theme.colors.onBackground} size={18}/>}/>
)}
</View>
</>
@@ -355,6 +366,28 @@ function DefaultCardDetail() {
)
}
const menuActions: MenuAction[] = [
{
id: "share",
title: "Share",
image: Platform.select({
ios: "square.and.arrow.up",
android: "share"
})
},
{
id: "delete",
title: "Delete Server",
attributes: {
destructive: true,
},
image: Platform.select({
ios: "trash",
android: "delete"
})
}
];
const styles = StyleSheet.create({
section: {
gap: 10
@@ -367,7 +400,7 @@ const styles = StyleSheet.create({
gap: 15
},
container: {
paddingTop: Platform.OS === 'android' ? 120 : 0,
paddingTop: Platform.select({android: 120, default: 0}),
paddingHorizontal: 16,
paddingBottom: 80,
gap: 20
+25 -55
View File
@@ -1,77 +1,47 @@
import {getGameName} from "amnet-shared";
import {FlatList, Platform, View} from "react-native";
import {Button, Paragraph, useTheme} from "react-native-paper";
import React, {useCallback, useLayoutEffect, useMemo, useState} from "react";
import {Globe, History, Joystick, Plus, ServerOff} from "lucide-react-native";
import {FlatList, NativeSyntheticEvent, Platform, ScrollView, Share, View} from "react-native";
import ContextMenu, {ContextMenuOnPressNativeEvent} from "react-native-context-menu-view";
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
import {AMUICard, AMUICardDetail} from "../components/AMUICard";
import TimeAgo from "../components/TimeAgo";
import {AMUICard, AMUICardDetail} from "../components/AMUICard";
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
import RealmContext from "../models/RealmContext";
import {AMServer} from "../models/AMServer";
import {getGameName} from "amnet-shared";
import RealmContext from "../models/RealmContext";
const {useRealm, useQuery} = RealmContext;
const {useQuery} = RealmContext;
function ServerCard(props: { server: AMServer, navigation: any }) {
const realm = useRealm();
const gameName = useMemo(() => getGameName(props.server.gameId), [props.server.gameId]);
const pageCallback = useCallback(() => {
props.navigation.navigate('server', {
serverId: props.server._id.toHexString()
});
}, [props.navigation, props.server]);
const pageContextMenuCallback = useCallback((action: NativeSyntheticEvent<ContextMenuOnPressNativeEvent>) => {
switch (action.nativeEvent.indexPath.toString()) {
case "0":
pageCallback();
return;
case "1,0":
Share.share({url: props.server.shareUrl});
return;
case "1,1":
realm.write(() => realm.delete(props.server));
return;
}
}, [realm, props.server]);
return (
<ContextMenu onPress={pageContextMenuCallback} actions={[
{title: `Select ${props.server.serverName}`, systemIcon: "chevron.right"},
{
title: Platform.OS === "ios" ? "" : "Additional Actions",
inlineChildren: true,
actions: [
{title: "Share Server", systemIcon: "square.and.arrow.up"},
{title: "Delete Server", systemIcon: "trash", destructive: true}
]
}
]}>
<AMUICard title={props.server.serverName} onPress={pageCallback} onLongPress={() => {}}>
<AMUICard title={props.server.serverName} onPress={pageCallback}>
<AMUICardDetail>
<Globe size={18} color="gray"/>
<Paragraph style={{color: "gray", fontFamily: "JetBrainsMono_400Regular"}}>
{props.server.serverAddress}
</Paragraph>
</AMUICardDetail>
{gameName && (
<AMUICardDetail>
<Globe size={18} color="gray"/>
<Paragraph style={{color: "gray", fontFamily: "JetBrainsMono_400Regular"}}>
{props.server.serverAddress}
</Paragraph>
<Joystick size={18} color="gray"/>
<Paragraph style={{color: "gray"}}>{gameName}</Paragraph>
</AMUICardDetail>
{gameName && (
<AMUICardDetail>
<Joystick size={18} color="gray"/>
<Paragraph style={{color: "gray"}}>{gameName}</Paragraph>
</AMUICardDetail>
)}
<AMUICardDetail>
<History size={18} color="gray"/>
{props.server.lastUsed
? (<TimeAgo style={{color: "gray"}} dateTo={props.server.lastUsed}/>)
: (<Paragraph style={{color: "gray"}}>never</Paragraph>)}
</AMUICardDetail>
</AMUICard>
</ContextMenu>
)}
<AMUICardDetail>
<History size={18} color="gray"/>
{props.server.lastUsed
? (<TimeAgo style={{color: "gray"}} dateTo={props.server.lastUsed}/>)
: (<Paragraph style={{color: "gray"}}>never</Paragraph>)}
</AMUICardDetail>
</AMUICard>
)
}
+14 -3
View File
@@ -10,6 +10,7 @@
"dependencies": {
"@babel/runtime": "^7.27.0",
"@expo-google-fonts/jetbrains-mono": "^0.2.3",
"@react-native-menu/menu": "^1.2.3",
"@react-navigation/bottom-tabs": "^7.3.3",
"@react-navigation/native": "^7.0.19",
"@react-navigation/native-stack": "^7.3.3",
@@ -3456,6 +3457,16 @@
"node": ">=14"
}
},
"node_modules/@react-native-menu/menu": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@react-native-menu/menu/-/menu-1.2.3.tgz",
"integrity": "sha512-sEfiVIivsa0lSelFm9Wbm/RAi+XoEHc75GGhjwvSrj9KSCVvNNXwr9F8l42e1t/lzYvVYzmkYxLG6VKxrDYJiw==",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.76.8",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.8.tgz",
@@ -7252,9 +7263,9 @@
}
},
"node_modules/image-size": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.0.tgz",
"integrity": "sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
"license": "MIT",
"dependencies": {
"queue": "6.0.2"
+1
View File
@@ -11,6 +11,7 @@
"dependencies": {
"@babel/runtime": "^7.27.0",
"@expo-google-fonts/jetbrains-mono": "^0.2.3",
"@react-native-menu/menu": "^1.2.3",
"@react-navigation/bottom-tabs": "^7.3.3",
"@react-navigation/native": "^7.0.19",
"@react-navigation/native-stack": "^7.3.3",