Files
ppc_amnet/amnet-native/app/ServerCardIn.tsx
T
2024-08-21 21:00:28 +01:00

237 lines
9.6 KiB
TypeScript

import {BSON} from "realm";
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useState} from "react";
import {Alert, RefreshControl, ScrollView, Share, StyleSheet, TouchableOpacity, View} from "react-native";
import {ActivityIndicator, Button, Card, Paragraph, Title, useTheme} from "react-native-paper";
import {CircleCheck, CircleDot, CreditCard, Nfc, Plus, SmartphoneNfcIcon, WifiOff, Share as ShareIcon} from "lucide-react-native";
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
import {AimeCard} from "../components/AimeCard";
import RealmContext from "../models/RealmContext";
import {AMServer} from "../models/AMServer";
import {AMCard} from "../models/AMCard";
import ObjectId = BSON.ObjectId;
const {useRealm, useQuery, useObject} = RealmContext;
export default function ServerCardIn({route, navigation}) {
const realm = useRealm();
const theme = useTheme();
// card listing state
const query = useQuery(AMCard);
const cards = useMemo(() => query.sorted("lastUsed", true), [query]);
const [selectedCard, setSelectedCard] = useState<AMCard>(null);
const [cardInCompleted, setCardInCompleted] = useState<boolean>(null);
const unselectedCards = useMemo(() => {
// protect against selected card being removed
switch (selectedCard?.isValid()) {
case true:
return cards.filter(x => !x._id.equals(selectedCard._id));
// clear out invalid
case false:
setSelectedCard(null);
return cards;
default:
return cards;
}
}, [cards, selectedCard]);
// server state
const [serverRefreshing, setServerRefreshing] = useState<boolean>(false);
const [serverReloadCounter, setServerReloadCounter] = useState<number>(0);
const [connectionTestResult, setConnectionTestResult] = useState<boolean | null>(null);
let {serverId} = route.params;
const server = useObject(AMServer, ObjectId.createFromHexString(serverId));
useLayoutEffect(() => {
if (!server) {
return;
}
navigation.setOptions({
title: server.serverName,
headerRight: () => (
<TouchableOpacity onPress={() => Share.share({url: `http://card.ppc.moe/#/servers/add?serverAddress=${encodeURIComponent(server.serverAddress)}`})}>
<ShareIcon/>
</TouchableOpacity>
)
});
}, [navigation, server]);
// set selected card (once)
useEffect(() => {
if (selectedCard?.isValid() === true) {
return;
}
setSelectedCard(cards.length ? cards[0] : null);
}, [realm]);
// refresh server info (server change or reload request)
useEffect(() => {
if (server?.isValid() !== true) {
navigation.goBack();
// don't show error on invalid server, just means it was deleted
if (!server) {
Alert.alert("Server not found", "The server you are looking for does not exist.");
}
return;
}
const abortController = new AbortController();
server.getInfo(abortController.signal).then(r => {
if (r) {
setConnectionTestResult(true);
realm.write(() => {
server.lastUsed = new Date();
Object.keys(r).forEach(x => server[x] = r[x]);
});
} else {
setConnectionTestResult(false);
}
}).catch(() => {
setConnectionTestResult(false);
}).finally(() => {
setServerRefreshing(false);
});
setTimeout(() => abortController.abort(), 5000);
return () => abortController.abort();
}, [serverId, serverReloadCounter, realm]);
// card in callback
const performCardIn = useCallback(() => {
setCardInCompleted(false);
const abortController = new AbortController();
server.submitCard(selectedCard.code, abortController.signal).then(async r => {
const now = new Date();
setCardInCompleted(r);
realm.write(() => {
server.lastUsed = now;
selectedCard.lastUsed = now;
});
setSelectedCard(selectedCard);
}).catch(() => {
setCardInCompleted(false);
}).finally(() => {
setTimeout(() => setCardInCompleted(null), 3000);
});
setTimeout(() => abortController.abort(), 5000);
return () => abortController.abort();
}, [server, selectedCard]);
const disableCardIn = useMemo(() => cardInCompleted !== null || !connectionTestResult, [cardInCompleted, connectionTestResult]);
if (connectionTestResult === null) {
return (
<View style={{flex: 1, justifyContent: "center", alignItems: "center", gap: 10}}>
<ActivityIndicator animating/>
<Paragraph>Connecting to server...</Paragraph>
</View>
)
}
return (
<ScrollView contentInsetAdjustmentBehavior="automatic"
contentContainerStyle={{...styles.container, paddingTop: connectionTestResult ? 0 : 10}}
refreshControl={<RefreshControl refreshing={serverRefreshing}
tintColor={theme.colors.onSurface}
onRefresh={() => {
setServerRefreshing(true);
setServerReloadCounter(serverReloadCounter + 1);
}}/>}>
{!connectionTestResult && (
<Card mode="outlined" style={{borderColor: theme.colors.secondary}}>
<Card.Content>
<View style={{alignItems: "center", flexDirection: "row", gap: 10}}>
<WifiOff color={theme.colors.secondary} size={24}/>
<Title style={{color: theme.colors.secondary}}>Connection Failed</Title>
</View>
<Paragraph>
There was an issue connecting to <Paragraph
style={{fontFamily: "JetBrainsMono_400Regular"}}>{server.serverAddress}</Paragraph>.
Check the network connection and try again.
</Paragraph>
</Card.Content>
</Card>
)}
<View style={styles.section}>
<Title style={{fontWeight: "bold"}}>Selected Card</Title>
{selectedCard ? <>
<AimeCard actionDisabled
card={selectedCard}
actionIcon={<CircleCheck color="green" size={24}/>}/>
<Button textColor="white"
onPress={performCardIn}
disabled={disableCardIn}
contentStyle={{flexDirection: 'row-reverse'}}
mode={connectionTestResult ? "contained" : "outlined"}
buttonColor={cardInCompleted === false ? "red" : "green"}
icon={() => <Nfc color={disableCardIn ? "gray" : "white"} size={18}/>}>
{cardInCompleted === null ? "Card In" : cardInCompleted ? "Card In Success" : "Failed"}
</Button>
</>
: <View style={styles.emptyPlaceholderView}>
<EmptyStatePlaceholder message="No card selected" icon={<CreditCard color="gray"/>}/>
<Button mode="text"
icon={() => <Plus color={theme.colors.primary} size={18}/>}
onPress={() => navigation.navigate('add-card')}>
Add card
</Button>
</View>}
</View>
{cards.length > 0 && (
<View style={styles.section}>
<Title style={{fontWeight: "bold"}}>Additional Cards</Title>
<Button mode="contained"
textColor="white"
buttonColor={theme.colors.tertiary}
icon={() => <Plus color="white" size={18}/>}
contentStyle={{flexDirection: 'row-reverse'}}
onPress={() => navigation.navigate('add-card')}>
Add card
</Button>
{unselectedCards.length
? unselectedCards.map(card => (
<AimeCard card={card}
key={card._id.toHexString()}
actionIcon={<CircleDot color={theme.colors.tertiary} size={24}/>}
onPress={() => setSelectedCard(card)}/>))
: <View style={styles.emptyPlaceholderView}>
<EmptyStatePlaceholder message="No additional cards"
icon={<SmartphoneNfcIcon color="gray"/>}/>
</View>}
</View>)}
</ScrollView>
)
}
const styles = StyleSheet.create({
section: {
gap: 10
},
emptyPlaceholderView: {
marginVertical: 30,
gap: 15
},
container: {
paddingHorizontal: 16,
paddingBottom: 25,
gap: 20
}
})