import {BSON} from "realm"; import {ActivityIndicator, Button, Text, useTheme} from "react-native-paper"; import React, {useCallback, useEffect, useLayoutEffect, useMemo, useState} from "react"; import {Alert, Platform, RefreshControl, ScrollView, Share, StyleSheet, View} from "react-native"; import { CircleCheck, CircleDot, CircleEllipsis, CreditCard, Plus, SmartphoneNfcIcon, WifiOff, TvMinimalPlay, Star, SquareArrowOutUpRight } from "lucide-react-native"; import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder"; import {AMUIStatusAlert} from "../components/AMUIStatusAlert"; import {AMUICardDetail} from "../components/AMUICard"; import {ICCard} from "../components/ICCard"; import RealmContext from "../models/RealmContext"; import {AMServer} from "../models/AMServer"; import {AMCard} from "../models/AMCard"; import { AMCardInRequest, AMServerConnectionInfo, AMServerInstanceMetrics, fetchServerInfo, getGameName, performCardIn } from "amnet-shared"; import ObjectId = BSON.ObjectId; import {AMCardRecentUse} from "../models/AMCardRecentUse"; import {MenuAction, MenuView, NativeActionEvent} from "@react-native-menu/menu"; const {useRealm, useQuery, useObject} = RealmContext; export default function ServerCardIn({route, navigation}) { const realm = useRealm(); const theme = useTheme() const {serverId} = route.params; const server = useObject(AMServer, ObjectId.createFromHexString(serverId)); const cards = useQuery({ type: AMCard, query: q => q.sorted("lastUsed", true) }); // header actions const menuCallback = useCallback(async (e: NativeActionEvent) => { switch (e.nativeEvent.event) { case "share": Share.share({url: server.shareUrl}); break; case "delete": Alert.alert("Delete Server", "Are you sure you want to delete this server?", [ { text: "Cancel", style: "cancel" }, { text: "Delete", style: "destructive", onPress: () => { realm.write(() => { realm.delete(server); navigation.goBack(); }); } } ]); break; } }, [realm, server, navigation]); // server state const [serverRefreshing, setServerRefreshing] = useState(false); const [serverReloadCounter, setServerReloadCounter] = useState(0); const [serverConnectionSuccess, setServerConnectionSuccess] = useState(null); const [userSelectedCard, setUserSelectedCard] = useState(null); const [cardInCompleted, setCardInCompleted] = useState(null); const disableCardIn = useMemo(() => cardInCompleted !== null || !serverConnectionSuccess, [cardInCompleted, serverConnectionSuccess]); const serverDefaultCard = useMemo(() => { if (server?.isValid() !== true || !server.gameId) { return null; } return realm.objects(AMCardRecentUse.schema.name).find(x => x.gameId === server.gameId)?.card; }, [realm, cards, server?.gameId]); const selectedCard = useMemo(() => [userSelectedCard, serverDefaultCard, cards.at(0)].find(x => x?.isValid() == true) ?? null, [userSelectedCard, serverDefaultCard, cards]); const unselectedCards = useMemo(() => cards.filter(x => x.isValid() === true && x._id.toHexString() !== selectedCard?._id.toHexString()), [cards, selectedCard]); useLayoutEffect(() => { if (server?.isValid() !== true) { return; } navigation.setOptions({ title: server.serverName, headerRight: () => ( ) }); }, [navigation, server]); useEffect(() => { if (server?.isValid() !== true) { navigation.goBack(); return; } if (!serverConnectionSuccess) { return; } const metrics = server as AMServerInstanceMetrics; if (metrics.sessionUptime > 0 && metrics.timeSinceLastPoll === null) { const timeout = setTimeout(() => setServerReloadCounter(serverReloadCounter + 1), 5000); return () => clearTimeout(timeout); } }, [serverConnectionSuccess, server]); // invalid card/server selection useEffect(() => { if (userSelectedCard?.isValid() === false) { setUserSelectedCard(null); } }, [userSelectedCard]); // server info update useEffect(() => { const abortController = new AbortController(); fetchServerInfo(server.serverAddress, abortController.signal).then(r => { if (!r) { setServerConnectionSuccess(false); return; } setServerConnectionSuccess(true); realm.write(() => { server.lastUsed = new Date(); Object.keys(r).forEach(x => server[x] = r[x]); }); }).catch(() => { setServerConnectionSuccess(false); }).finally(() => { setServerRefreshing(false); }); const timeout = setTimeout(() => abortController.abort(), 5000); return () => { clearTimeout(timeout); abortController.abort(); } }, [serverId, serverReloadCounter, realm]); const cardInCallback = useCallback((card: AMCard) => { const abortController = new AbortController(); const request: AMCardInRequest = { cardId: card.code, physicalCardIDm: card.physicalCardData?.cardIDm ?? undefined }; performCardIn(server, request, abortController.signal).then(r => { const now = new Date(); realm.write(() => { server.lastUsed = now; card.lastUsed = now; if (server.gameId) { // replace the "most recent card" record for the current game realm.delete(realm.objects(AMCardRecentUse.schema.name).filtered("gameId = $0", server.gameId)); realm.create(AMCardRecentUse.schema.name, AMCardRecentUse.generate(server.gameId, card)); } }); setCardInCompleted(r); }).catch(() => { setCardInCompleted(false); }).finally(() => { setTimeout(() => setCardInCompleted(null), 3000); }); setTimeout(() => abortController.abort(), 5000); return () => abortController.abort(); }, [server]); const performRefresh = () => { setServerRefreshing(true); setServerReloadCounter(serverReloadCounter + 1); }; if (server?.isValid() !== true) { return null; } if (serverConnectionSuccess === null) { return ( Connecting to server... ) } return ( }> {server.gameId ? `Play ${getGameName(server.gameId)}` : "Selected Card"} {selectedCard ? <> }> {serverDefaultCard?.code == selectedCard.code && } : }/> } {cards.length > 0 && ( Additional Cards {unselectedCards.length ? unselectedCards.map((card: AMCard) => ( } onPress={() => setUserSelectedCard(card)}> {serverDefaultCard?.code == card.code && } )) : }/> } )} ) } function StatusAlert(props: { server: AMServerConnectionInfo & Partial, connectionEstablished: boolean }) { if (!props.connectionEstablished) { return ( There was an issue connecting to {props.server.serverAddress}. Check the network connection and try again. ) } // don't show if undefined (some older server versions) if (props.server.timeSinceLastPoll === null) { return ( Scanning a card below may not have any effect until the service has fully started up. ) } return null; } function DefaultCardDetail() { return ( <> {/* spacer */} Previously used with this game ) } 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 }, sectionTitle: { fontWeight: "bold" }, emptyPlaceholderView: { marginVertical: 30, gap: 15 }, container: { paddingTop: Platform.select({android: 120, default: 0}), paddingHorizontal: 16, paddingBottom: 80, gap: 20 } });