33 Commits
Author SHA1 Message Date
ppc 2d3b129126 bump android package version 2024-12-13 20:46:08 +00:00
ppc b729b4e31b more package updates 2024-12-13 20:22:34 +00:00
ppc 377fe93645 Merge remote-tracking branch 'origin/master' 2024-12-13 19:52:42 +00:00
ppc cbd2825475 update to expo 52 2024-11-28 14:52:31 +00:00
ppc b9bbcc9228 update workflow to .net 9 2024-11-28 14:00:33 +00:00
ppc 61fb81c75c add android links to readme 2024-11-28 13:52:09 +00:00
ppc d3b96d3017 don't render invisible icons on android 2024-11-22 16:22:51 +00:00
ppc 9b136006a0 add build-properties plugin 2024-11-21 22:52:48 +00:00
ppc 2eb61d74a7 assetlinks.json fix 2024-11-18 22:13:28 +00:00
ppc 89101c5d5f more fun 2024-11-18 22:00:54 +00:00
ppc 2fe9238d02 update dll metadata 2024-11-18 22:00:54 +00:00
ppc d1b626ffac add android asset link validation file 2024-11-18 22:00:53 +00:00
ppc fe7ab1c50f prepare android release bundling 2024-11-18 22:00:53 +00:00
ppc 5a3a7be906 add android intent filters 2024-11-18 22:00:53 +00:00
ppc 703067e6d6 whitespace removal 2024-11-18 22:00:53 +00:00
ppc 67f90397b0 upgrade server to .net9 2024-11-18 22:00:53 +00:00
ppc 8ba439721f android-specific adjustments 2024-11-18 22:00:53 +00:00
ppc 35a8c28c0e more variable naming 2024-11-09 20:21:33 +00:00
ppc a0b44c165b improve variable naming 2024-11-09 20:14:25 +00:00
ppc 489a93adfe update card selection, automatically check for server startup to end 2024-11-09 20:11:00 +00:00
ppc 6172f2bbd7 fix site on ios 12 2024-11-09 16:47:07 +00:00
ppc 414a199552 target es2019 (safari 12+) 2024-11-09 15:53:27 +00:00
ppc a583b72798 update expo sdk version 2024-11-06 16:04:15 +00:00
ppc 33cd321325 improve card selection when changing games 2024-11-06 16:02:59 +00:00
ppc 85b900ea30 fix haptic feedback not happening on iOS (see https://github.com/facebook/react-native/issues/43388) 2024-10-26 18:08:31 +01:00
ppc 4038de583d fix card unselection on reload 2024-10-26 18:07:57 +01:00
ppc b08a7cd125 show oldest cards at the top 2024-10-14 09:41:15 +01:00
ppc 4b6037fe24 fix bad selection logic 2024-09-28 09:22:43 +01:00
ppc 4702052cc4 prevent invalid array access 2024-09-26 12:13:46 +01:00
ppc 969edd3675 add game-default card indicator 2024-09-26 12:09:38 +01:00
ppc 0d200a22fc update vulnerable packages (CVE-2024-47068, CVE-2024-45812 and CVE-2024-45811) 2024-09-24 12:44:26 +01:00
ppc c30abfe482 auto-select card last used with specific game 2024-09-20 11:57:06 +01:00
ppc 6628071f5b fix dark mode inconsistency 2024-09-20 11:55:27 +01:00
31 changed files with 5883 additions and 5409 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
dotnet-version: '9.0.x'
- name: Set version
run: |
+5 -5
View File
@@ -4,18 +4,18 @@
<PublishAot>true</PublishAot>
<OutputType>Library</OutputType>
<EventSourceSupport>false</EventSourceSupport>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net9.0-windows</TargetFramework>
<OptimizationPreference>Size</OptimizationPreference>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
</PropertyGroup>
<PropertyGroup>
<AssemblyName>amnet</AssemblyName>
<ProductName>AMNet</ProductName>
<Product>AMNet</Product>
<CompanyName>ppc</CompanyName>
<Title>AMNet Server</Title>
<Description>Server component for the AMNet IC Card switcher</Description>
<AssemblyName>amnet</AssemblyName>
<AssemblyTitle>AMNet Server</AssemblyTitle>
<Copyright>Copyright 2024 (c) ppc</Copyright>
<Description>Server component for the AMNet IC Card switcher</Description>
</PropertyGroup>
<PropertyGroup>
+4 -3
View File
@@ -1,6 +1,7 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading;
namespace AMNet.Server;
@@ -9,9 +10,9 @@ namespace AMNet.Server;
/// </summary>
internal class CardPresenter
{
private readonly object _cardLock = new();
private readonly Lock _cardLock = new();
private StoredCard _currentCard;
public record StoredCard(byte[] Value, string OriginalValue, long ExpiresAt)
{
public bool Expired => Environment.TickCount64 >= ExpiresAt;
@@ -65,7 +66,7 @@ internal class CardPresenter
return null;
}
}
var card = new StoredCard(bytes, matrixCode, Environment.TickCount64 + validFor);
lock (_cardLock)
+4 -4
View File
@@ -14,17 +14,17 @@ internal static class Config
{
GameId = ReadKey(IOSection, "gameId", 4);
ServerName = ReadKey(IOSection, "serverName", 26, Environment.MachineName);
EnableAimeTxt = PInvoke.GetPrivateProfileInt(IOSection, "enableKeyboardMode", 1, ConfigFileName) > 0;
AimeTxtPath = ReadKey("aime", "aimePath", PInvoke.MAX_PATH, @"DEVICE\aime.txt");
}
public static string GameId { get; }
public static string ServerName { get; }
public static bool EnableAimeTxt { get; }
public static string AimeTxtPath { get; }
internal static unsafe string ReadKey(string section, string key, uint maxLength, string @default = null)
{
// +1 for null terminator
+2 -2
View File
@@ -63,7 +63,7 @@ internal static partial class WebServer
builder.Services.AddSingleton<CardPresenter>();
var app = builder.Build();
app.UseCors();
app.MapGet("/amnet/info", ServerInfo);
@@ -83,7 +83,7 @@ internal static partial class WebServer
Config.ServerName,
Environment.TickCount64 - startedAt,
lastPollAt < 0 ? null : Environment.TickCount64 - lastPollAt);
return Results.Ok(state);
}
+57 -3
View File
@@ -1,7 +1,7 @@
{
"name": "AMNet",
"scheme": "amnet",
"version": "1.1.0",
"version": "1.2.0",
"slug": "amnet-native",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
@@ -19,7 +19,61 @@
}
},
"android": {
"package": "moe.ppc.amnet"
"package": "moe.ppc.amnet",
"versionCode": 3,
"adaptiveIcon": {
"foregroundImage": "./assets/icon-android.png",
"backgroundColor": "#DED8FD"
},
"splash": {
"resizeMode": "cover",
"image": "./assets/icon-android.png",
"backgroundColor": "#DED8FD",
"dark": {
"backgroundColor": "#3B5ACC"
}
},
"intentFilters": [
{
"autoVerify": true,
"action": "VIEW",
"data": [
{
"scheme": "http",
"host": "card.ppc.moe",
"pathPrefix": "/#/servers"
},
{
"scheme": "http",
"host": "card.ppc.moe",
"pathPrefix": "/#/cards"
},
{
"scheme": "https",
"host": "card.ppc.moe",
"pathPrefix": "/#/servers"
},
{
"scheme": "https",
"host": "card.ppc.moe",
"pathPrefix": "/#/cards"
}
],
"category": [
"BROWSABLE",
"DEFAULT"
]
}
]
},
"plugins": []
"plugins": [
[
"expo-build-properties",
{
"android": {
"usesCleartextTraffic": true
}
}
]
]
}
+11 -9
View File
@@ -1,7 +1,7 @@
import {generateCardNumber} from "amnet-shared";
import {useModalStyles} from "../utils/ModalStyles";
import {useCallback, useLayoutEffect, useState} from "react";
import {TextInput, TouchableOpacity, View} from "react-native";
import {Platform, TextInput, TouchableOpacity, View} from "react-native";
import {Button, Card, Divider, Paragraph, useTheme} from "react-native-paper";
import {AMCard} from "../models/AMCard";
@@ -36,13 +36,15 @@ export default function CardAddModal({navigation, route}) {
}, [realm]);
// header effects
useLayoutEffect(() => {
navigation.setOptions({
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
</TouchableOpacity>,
});
}, [navigation, modalStyles]);
if (Platform.OS === 'ios') {
useLayoutEffect(() => {
navigation.setOptions({
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
</TouchableOpacity>,
});
}, [navigation, modalStyles]);
}
useLayoutEffect(() => {
const canSave = cardName.length && cardNumber.length === 20;
@@ -103,7 +105,7 @@ export default function CardAddModal({navigation, route}) {
placeholderTextColor={theme.colors.outline}
value={cardNumber.match(/.{1,4}/g)?.join(" ")}
onChangeText={val => setCardNumber(val?.replaceAll(/\D/g, '').substring(0, 20) ?? '')}
style={{...modalStyles.formItemInput, color: cardNumberReadOnly ? theme.colors.onSurfaceDisabled : undefined}}/>
style={{...modalStyles.formItemInput, color: cardNumberReadOnly ? theme.colors.onSurfaceDisabled : theme.colors.onSurface}}/>
</View>
</Card.Content>
</Card>
+17 -9
View File
@@ -1,15 +1,16 @@
import * as Clipboard from "expo-clipboard";
import {FlatList, View} from "react-native";
import {FlatList, Platform, View} from "react-native";
import React, {useCallback, useMemo} from "react";
import {Button, useTheme} from "react-native-paper";
import {CreditCard, Plus} from "lucide-react-native";
import ContextMenu from "react-native-context-menu-view";
import {AMCard} from "../models/AMCard";
import {AimeCard} from "../components/AimeCard";
import RealmContext from "../models/RealmContext";
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
import {AMCard} from "../models/AMCard";
import RealmContext from "../models/RealmContext";
const {useQuery, useRealm} = RealmContext;
function CardEntry(props: { card: AMCard, navigation: any }) {
@@ -43,7 +44,7 @@ function CardEntry(props: { card: AMCard, navigation: any }) {
actions={[
{title: "Copy Card Number", systemIcon: "doc.on.doc"},
{
title: "",
title: Platform.OS === 'ios' ? "" : "Additional Actions",
inlineChildren: true,
actions: [
{title: "Edit Card", systemIcon: "square.and.pencil"},
@@ -55,16 +56,16 @@ function CardEntry(props: { card: AMCard, navigation: any }) {
}
export default function CardManager({navigation}) {
const query = useQuery(AMCard);
const cards = useMemo(() => query.sorted("added", true), [query]);
const theme = useTheme();
const query = useQuery(AMCard);
const cards = useMemo(() => query.sorted("added", false), [query]);
if (!cards.length) {
return (
<View style={{flex: 1, gap: 10, justifyContent: "center", alignItems: "center"}}>
<EmptyStatePlaceholder message="No cards added yet" icon={<CreditCard color="gray"/>}/>
<Button mode="text" onPress={() => navigation.navigate('add-card')}
<Button mode="text"
onPress={() => navigation.navigate('add-card')}
icon={() => <Plus size={18} color={theme.colors.primary}/>}>Add Card</Button>
</View>
)
@@ -76,7 +77,14 @@ export default function CardManager({navigation}) {
data={cards}
keyExtractor={i => i._id.toString()}
contentContainerStyle={{paddingBottom: 120}}
renderItem={i => <CardEntry card={i.item} navigation={navigation}/>}
ItemSeparatorComponent={() => <View style={{height: 15}}/>}
renderItem={i => <CardEntry card={i.item} navigation={navigation}/>}/>
ListFooterComponent={() => Platform.OS === "ios" ? null : (
<Button mode="text"
onPress={() => navigation.navigate('add-card')}
style={{marginTop: 15, marginBottom: 25}}
icon={() => <Plus size={18} color={theme.colors.primary}/>}>Add Card</Button>
)}
/>
)
}
+2 -2
View File
@@ -1,6 +1,6 @@
import {Plus} from "lucide-react-native";
import {TouchableOpacity} from "react-native";
import {useTheme} from "@react-navigation/native";
import {Platform, TouchableOpacity} from "react-native";
import {createNativeStackNavigator} from "@react-navigation/native-stack";
import CardManager from "./CardManager";
@@ -22,7 +22,7 @@ export default function CardManagerStack({navigation}) {
component={CardManager}
options={{
title: "Cards",
headerRight: () => <TouchableOpacity onPress={() => navigation.navigate('add-card')}><Plus/></TouchableOpacity>
headerRight: () => Platform.OS === 'android' ? null : <TouchableOpacity onPress={() => navigation.navigate('add-card')}><Plus/></TouchableOpacity>
}}/>
</Stack.Navigator>
);
+10 -8
View File
@@ -1,7 +1,7 @@
import {useModalStyles} from "../utils/ModalStyles";
import {Card, Paragraph, useTheme} from "react-native-paper";
import {TextInput, TouchableOpacity, View} from "react-native";
import {useCallback, useEffect, useLayoutEffect, useState} from "react";
import {Platform, TextInput, TouchableOpacity, View} from "react-native";
import RealmContext from "../models/RealmContext";
import {AMServer} from "../models/AMServer";
@@ -20,13 +20,15 @@ export default function ServerAddModal({route, navigation}) {
handleDeepLink(realm, navigation, route, setServerAddress);
}, []);
useLayoutEffect(() => {
navigation.setOptions({
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
</TouchableOpacity>,
});
}, [navigation, modalStyles]);
if (Platform.OS === 'ios') {
useLayoutEffect(() => {
navigation.setOptions({
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
</TouchableOpacity>,
});
}, [navigation, modalStyles]);
}
useLayoutEffect(() => {
let urlValid = false;
+153 -105
View File
@@ -1,15 +1,33 @@
import {BSON} from "realm";
import {useSafeAreaInsets} from "react-native-safe-area-context";
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useState} from "react";
import {ActivityIndicator, Button, Paragraph, Title, useTheme} from "react-native-paper";
import {Alert, RefreshControl, ScrollView, Share, StyleSheet, TouchableOpacity, View} from "react-native";
import {
CircleCheck, CircleDot, CreditCard, Nfc, Plus, SmartphoneNfcIcon,
Share as ShareIcon, WifiOff, TvMinimalPlay
Alert,
Platform,
RefreshControl,
ScrollView,
Share,
StyleSheet,
Text,
TouchableOpacity,
View
} from "react-native";
import {
CircleCheck,
CircleDot,
CreditCard,
Nfc,
Plus,
SmartphoneNfcIcon,
Share as ShareIcon,
WifiOff,
TvMinimalPlay,
Star
} from "lucide-react-native";
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
import {AMUIStatusAlert} from "../components/AMUIStatusAlert";
import {AMUICardDetail} from "../components/AMUICard";
import {AimeCard} from "../components/AimeCard";
import RealmContext from "../models/RealmContext";
@@ -29,38 +47,33 @@ const {useRealm, useQuery, useObject} = RealmContext;
export default function ServerCardIn({route, navigation}) {
const realm = useRealm();
const theme = useTheme();
const safeArea = useSafeAreaInsets();
// 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]);
let {serverId} = route.params;
const server = useObject(AMServer, ObjectId.createFromHexString(serverId));
const cards = useQuery({
type: AMCard,
query: q => q.sorted("lastUsed", true)
});
// server state
const [serverRefreshing, setServerRefreshing] = useState<boolean>(false);
const [serverReloadCounter, setServerReloadCounter] = useState<number>(0);
const [connectionTestResult, setConnectionTestResult] = useState<boolean | null>(null);
const [serverConnectionSuccess, setServerConnectionSuccess] = useState<boolean | null>(null);
let {serverId} = route.params;
const server = useObject(AMServer, ObjectId.createFromHexString(serverId));
const [userSelectedCard, setUserSelectedCard] = useState<AMCard>(null);
const [cardInCompleted, setCardInCompleted] = useState<boolean>(null);
const disableCardIn = useMemo(() => cardInCompleted !== null || !serverConnectionSuccess, [cardInCompleted, serverConnectionSuccess]);
const serverDefaultCard = useMemo(() => {
if (server?.isValid() !== true || !server.gameId) {
return null;
}
return cards.find(x => x.lastUsedGameId === server.gameId);
}, [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) {
@@ -71,65 +84,80 @@ export default function ServerCardIn({route, navigation}) {
title: server.serverName,
headerRight: () => (
<TouchableOpacity onPress={() => Share.share({url: server.shareUrl})}>
<ShareIcon/>
<ShareIcon color={theme.colors.primary}/>
</TouchableOpacity>
)
});
}, [navigation, server]);
// card selection, server connection checks
// invalid card/server selection
useEffect(() => {
if (server?.isValid() !== true) {
navigation.goBack();
return;
}
}, [server]);
// 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.");
}
useEffect(() => {
if (userSelectedCard?.isValid() === false) {
setUserSelectedCard(null);
}
}, [userSelectedCard]);
// handle auto-refresh
useEffect(() => {
if (!serverConnectionSuccess || server?.isValid() !== true) {
return;
}
if (selectedCard?.isValid() !== true) {
setSelectedCard(cards.length ? cards[0] : null);
const metrics = server as AMServerInstanceMetrics;
if (metrics.sessionUptime > 0 && metrics.timeSinceLastPoll === null) {
const timeout = setTimeout(() => setServerReloadCounter(serverReloadCounter + 1), 5000);
return () => clearTimeout(timeout);
}
}, [serverConnectionSuccess, server]);
// server info update
useEffect(() => {
const abortController = new AbortController();
fetchServerInfo(server.serverAddress, 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);
if (!r) {
setServerConnectionSuccess(false);
return;
}
setServerConnectionSuccess(true);
realm.write(() => {
server.lastUsed = new Date();
Object.keys(r).forEach(x => server[x] = r[x]);
});
}).catch(() => {
setConnectionTestResult(false);
setServerConnectionSuccess(false);
}).finally(() => {
setServerRefreshing(false);
});
setTimeout(() => abortController.abort(), 5000);
return () => abortController.abort();
const timeout = setTimeout(() => abortController.abort(), 5000);
return () => {
clearTimeout(timeout);
abortController.abort();
}
}, [serverId, serverReloadCounter, realm]);
// card in callback
const cardInCallback = useCallback(() => {
setCardInCompleted(false);
const abortController = new AbortController();
performCardIn(server, {cardId: selectedCard.code}, abortController.signal).then(async r => {
const now = new Date();
setCardInCompleted(r);
realm.write(() => {
server.lastUsed = now;
selectedCard.lastUsed = now;
selectedCard.lastUsedGameId = server.gameId;
});
setSelectedCard(selectedCard);
setCardInCompleted(r);
}).catch(() => {
setCardInCompleted(false);
}).finally(() => {
@@ -143,12 +171,13 @@ export default function ServerCardIn({route, navigation}) {
const performRefresh = () => {
setServerRefreshing(true);
setServerReloadCounter(serverReloadCounter + 1);
};
if (server?.isValid() !== true) {
return null;
}
const gameName = useMemo(() => getGameName(server.gameId), [server.gameId]);
const disableCardIn = useMemo(() => cardInCompleted !== null || !connectionTestResult, [cardInCompleted, connectionTestResult]);
if (connectionTestResult === null) {
if (serverConnectionSuccess === null) {
return (
<View style={{flex: 1, justifyContent: "center", alignItems: "center", gap: 10}}>
<ActivityIndicator animating/>
@@ -158,59 +187,63 @@ export default function ServerCardIn({route, navigation}) {
}
return (
<ScrollView contentInsetAdjustmentBehavior="automatic"
contentContainerStyle={{...styles.container, paddingTop: connectionTestResult ? 0 : 10}}
refreshControl={<RefreshControl refreshing={serverRefreshing} tintColor={theme.colors.onSurface} onRefresh={performRefresh}/>}>
<StatusAlert server={server} connectionEstablished={connectionTestResult}/>
<View style={styles.section}>
<Title style={styles.sectionTitle}>{gameName ? `Play ${gameName}` : "Selected Card"}</Title>
{selectedCard ? <>
<AimeCard actionDisabled
card={selectedCard}
actionIcon={<CircleCheck color="green" size={24}/>}/>
<Button textColor="white"
onPress={cardInCallback}
disabled={disableCardIn}
<ScrollView contentInsetAdjustmentBehavior="automatic" refreshControl={<RefreshControl refreshing={serverRefreshing} onRefresh={performRefresh}/>}>
<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>
{selectedCard ? <>
<AimeCard actionDisabled
card={selectedCard}
actionIcon={<CircleCheck color="green" size={24}/>}>
{serverDefaultCard?.code == selectedCard.code && <DefaultCardDetail/>}
</AimeCard>
<Button textColor="white"
onPress={cardInCallback}
disabled={disableCardIn}
contentStyle={{flexDirection: 'row-reverse'}}
mode={serverConnectionSuccess ? "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={styles.sectionTitle}>Additional Cards</Title>
<Button mode="contained"
textColor="white"
buttonColor={theme.colors.tertiary}
icon={() => <Plus color="white" size={18}/>}
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>}
{unselectedCards.length
? unselectedCards.map((card: AMCard) => (
<AimeCard card={card}
key={card._id.toHexString()}
actionIcon={<CircleDot color={theme.colors.tertiary} size={24}/>}
onPress={() => setUserSelectedCard(card)}>
{serverDefaultCard?.code == card.code && <DefaultCardDetail/>}
</AimeCard>))
: <View style={styles.emptyPlaceholderView}>
<EmptyStatePlaceholder message="No additional cards"
icon={<SmartphoneNfcIcon color="gray"/>}/>
</View>}
</View>)}
</View>
{cards.length > 0 && (
<View style={styles.section}>
<Title style={styles.sectionTitle}>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: AMCard) => (
<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>
)
}
@@ -245,6 +278,20 @@ function StatusAlert(props: {
return null;
}
function DefaultCardDetail() {
return (
<>
{/* spacer */}
<View></View>
<AMUICardDetail>
<Star size={18} color="goldenrod"/>
<Text style={{color: "goldenrod"}}>Previously used with this game</Text>
</AMUICardDetail>
</>
)
}
const styles = StyleSheet.create({
section: {
gap: 10
@@ -257,6 +304,7 @@ const styles = StyleSheet.create({
gap: 15
},
container: {
paddingTop: Platform.OS === 'android' ? 120 : 0,
paddingHorizontal: 16,
paddingBottom: 80,
gap: 20
+12 -4
View File
@@ -1,7 +1,7 @@
import {Button, Paragraph, useTheme} from "react-native-paper";
import {FlatList, NativeSyntheticEvent, Share, View} from "react-native";
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";
@@ -16,7 +16,7 @@ const {useRealm, 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()
@@ -43,7 +43,7 @@ function ServerCard(props: { server: AMServer, navigation: any }) {
<ContextMenu onPress={pageContextMenuCallback} actions={[
{title: `Select ${props.server.serverName}`, systemIcon: "chevron.right"},
{
title: "",
title: Platform.OS === "ios" ? "" : "Additional Actions",
inlineChildren: true,
actions: [
{title: "Share Server", systemIcon: "square.and.arrow.up"},
@@ -98,6 +98,8 @@ export default function ServerManager({navigation}) {
value: searchQuery,
autoCorrect: "none",
autoCapitalize: "none",
textColor: theme.colors.onSurface,
headerIconColor: theme.colors.primary,
onChangeText: e => setSearchQuery(e.nativeEvent.text)
},
})
@@ -121,7 +123,13 @@ export default function ServerManager({navigation}) {
keyExtractor={i => i._id.toString()}
style={{padding: 16, paddingTop: 24}}
contentContainerStyle={{paddingBottom: 80}}
renderItem={i => <ServerCard navigation={navigation} server={i.item}/>}
ItemSeparatorComponent={() => <View style={{height: 15}}/>}
renderItem={i => <ServerCard navigation={navigation} server={i.item}/>}/>
ListFooterComponent={() => Platform.OS === "ios" ? null : (
<Button mode="text"
onPress={() => navigation.navigate('add-server')}
style={{marginTop: 15, marginBottom: 25}}
icon={() => <Plus size={18} color={theme.colors.primary}/>}>Add Server</Button>
)}/>
)
}
+2 -2
View File
@@ -1,6 +1,6 @@
import {Plus} from "lucide-react-native";
import {TouchableOpacity} from "react-native";
import {useTheme} from "@react-navigation/native";
import {Platform, TouchableOpacity} from "react-native";
import {createNativeStackNavigator} from "@react-navigation/native-stack";
import ServerCardIn from "./ServerCardIn";
@@ -24,7 +24,7 @@ export default function ServerManagerStack({navigation}) {
component={ServerManager}
options={{
title: "Servers",
headerRight: () => <TouchableOpacity
headerRight: () => Platform.OS === 'android' ? null : <TouchableOpacity
onPress={() => navigation.navigate('add-server')}>
<Plus/>
</TouchableOpacity>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

+3 -1
View File
@@ -1,4 +1,4 @@
import {useMemo} from "react";
import {ReactNode, useMemo} from "react";
import {Text} from "react-native";
import {CreditCard, History} from "lucide-react-native";
@@ -8,6 +8,7 @@ import {AMUICard, AMUICardActionProps, AMUICardDetail} from "./AMUICard";
export interface AimeCardProps extends AMUICardActionProps {
card: AMCardData,
children?: ReactNode,
showFullCardNumber?: boolean
}
@@ -29,6 +30,7 @@ export function AimeCard(props: AimeCardProps) {
<TimeAgo style={{color: "gray"}} dateTo={props.card.lastUsed}/>
</AMUICardDetail>
)}
{props.children}
</AMUICard>
);
}
+3 -1
View File
@@ -16,6 +16,7 @@ export class AMCard extends Realm.Object<AMCard> implements AMCardData {
added: Date;
lastUsed?: Date;
lastUsedGameId?: string | null;
static generate(code: string, name: string) {
return {
@@ -35,7 +36,8 @@ export class AMCard extends Realm.Object<AMCard> implements AMCardData {
code: "string",
name: {type: "string", default: "New Card"},
added: {type: "date", default: new Date()},
lastUsed: {type: "date", optional: true}
lastUsed: {type: "date", optional: true},
lastUsedGameId: {type: "string", optional: true}
}
};
}
+1 -1
View File
@@ -4,7 +4,7 @@ import {AMServer} from "./AMServer";
// @ts-ignore
export default createRealmContext({
schemaVersion: 2,
schemaVersion: 3,
schema: [AMCard, AMServer],
deleteRealmIfMigrationNeeded: false
});
+4667 -4454
View File
File diff suppressed because it is too large Load Diff
+19 -18
View File
@@ -13,32 +13,33 @@
"@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0",
"@realm/react": "^0.9.0",
"@types/react": "~18.2.79",
"@realm/react": "^0.11.0",
"@types/react": "~18.3.12",
"amnet-shared": "file:../amnet-shared",
"date-fns": "^3.6.0",
"expo": "^51.0.0",
"expo-blur": "~13.0.2",
"expo-clipboard": "~6.0.3",
"expo-constants": "~16.0.2",
"expo-dev-client": "~4.0.10",
"expo-linking": "~6.3.1",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.7",
"expo": "^52.0.0",
"expo-blur": "~14.0.1",
"expo-build-properties": "^0.13.1",
"expo-clipboard": "~7.0.0",
"expo-constants": "~17.0.3",
"expo-dev-client": "~5.0.4",
"expo-linking": "~7.0.3",
"expo-status-bar": "~2.0.0",
"expo-system-ui": "~4.0.4",
"lucide-react-native": "^0.427.0",
"patch-package": "^8.0.0",
"react": "18.2.0",
"react-native": "0.74.1",
"react": "18.3.1",
"react-native": "0.76.5",
"react-native-context-menu-view": "^1.16.0",
"react-native-gesture-handler": "^2.18.1",
"react-native-gesture-handler": "~2.20.2",
"react-native-get-random-values": "^1.11.0",
"react-native-paper": "^5.12.5",
"react-native-reanimated": "^3.15.0",
"react-native-safe-area-context": "4.10.5",
"react-native-screens": "3.31.1",
"react-native-svg": "^15.5.0",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.1.0",
"react-native-svg": "15.8.0",
"react-native-url-polyfill": "^2.0.0",
"realm": "^12.12.1",
"realm": "^12.14.0",
"typescript": "~5.3.3"
}
}
+70 -70
View File
@@ -541,9 +541,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz",
"integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
"cpu": [
"arm"
],
@@ -555,9 +555,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz",
"integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
"cpu": [
"arm64"
],
@@ -569,9 +569,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz",
"integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
"cpu": [
"arm64"
],
@@ -583,9 +583,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz",
"integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
"cpu": [
"x64"
],
@@ -597,9 +597,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz",
"integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
"cpu": [
"arm"
],
@@ -611,9 +611,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz",
"integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
"cpu": [
"arm"
],
@@ -625,9 +625,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz",
"integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
"cpu": [
"arm64"
],
@@ -639,9 +639,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz",
"integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
"cpu": [
"arm64"
],
@@ -653,9 +653,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz",
"integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
"cpu": [
"ppc64"
],
@@ -667,9 +667,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz",
"integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
"cpu": [
"riscv64"
],
@@ -681,9 +681,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz",
"integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
"cpu": [
"s390x"
],
@@ -695,9 +695,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz",
"integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
"cpu": [
"x64"
],
@@ -709,9 +709,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz",
"integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
"cpu": [
"x64"
],
@@ -723,9 +723,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz",
"integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
"cpu": [
"arm64"
],
@@ -737,9 +737,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz",
"integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
"cpu": [
"ia32"
],
@@ -751,9 +751,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz",
"integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
"cpu": [
"x64"
],
@@ -1423,9 +1423,9 @@
}
},
"node_modules/micromatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1723,9 +1723,9 @@
}
},
"node_modules/rollup": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz",
"integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==",
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1739,22 +1739,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.21.0",
"@rollup/rollup-android-arm64": "4.21.0",
"@rollup/rollup-darwin-arm64": "4.21.0",
"@rollup/rollup-darwin-x64": "4.21.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.21.0",
"@rollup/rollup-linux-arm-musleabihf": "4.21.0",
"@rollup/rollup-linux-arm64-gnu": "4.21.0",
"@rollup/rollup-linux-arm64-musl": "4.21.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.0",
"@rollup/rollup-linux-riscv64-gnu": "4.21.0",
"@rollup/rollup-linux-s390x-gnu": "4.21.0",
"@rollup/rollup-linux-x64-gnu": "4.21.0",
"@rollup/rollup-linux-x64-musl": "4.21.0",
"@rollup/rollup-win32-arm64-msvc": "4.21.0",
"@rollup/rollup-win32-ia32-msvc": "4.21.0",
"@rollup/rollup-win32-x64-msvc": "4.21.0",
"@rollup/rollup-android-arm-eabi": "4.22.4",
"@rollup/rollup-android-arm64": "4.22.4",
"@rollup/rollup-darwin-arm64": "4.22.4",
"@rollup/rollup-darwin-x64": "4.22.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
"@rollup/rollup-linux-arm-musleabihf": "4.22.4",
"@rollup/rollup-linux-arm64-gnu": "4.22.4",
"@rollup/rollup-linux-arm64-musl": "4.22.4",
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
"@rollup/rollup-linux-riscv64-gnu": "4.22.4",
"@rollup/rollup-linux-s390x-gnu": "4.22.4",
"@rollup/rollup-linux-x64-gnu": "4.22.4",
"@rollup/rollup-linux-x64-musl": "4.22.4",
"@rollup/rollup-win32-arm64-msvc": "4.22.4",
"@rollup/rollup-win32-ia32-msvc": "4.22.4",
"@rollup/rollup-win32-x64-msvc": "4.22.4",
"fsevents": "~2.3.2"
}
},
+666 -661
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -22,6 +22,7 @@
"amnet-shared": "file:../amnet-shared",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"core-js": "^3.39.0",
"detectincognitojs": "^1.3.5",
"dexie": "^4.0.8",
"dexie-react-hooks": "^1.1.7",
@@ -52,6 +53,6 @@
"tailwindcss": "^3.4.9",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.0"
"vite": "^5.4.6"
}
}
@@ -0,0 +1,10 @@
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "moe.ppc.amnet",
"sha256_cert_fingerprints": ["9E:4C:42:6F:9E:27:85:62:E5:DB:75:23:60:4A:B9:8D:B5:39:C8:1D:AD:FE:8E:D3:DC:BA:6E:55:A9:C6:59:05"]
}
}
]
+21 -9
View File
@@ -1,19 +1,31 @@
import * as React from "react"
import * as React from "react";
export function useMediaQuery(query: string) {
const [value, setValue] = React.useState(false)
const [value, setValue] = React.useState(false);
React.useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches)
setValue(event.matches);
}
const result = matchMedia(query)
result.addEventListener("change", onChange)
setValue(result.matches)
const result = matchMedia(query);
return () => result.removeEventListener("change", onChange)
}, [query])
// addEventListener isn't available on older versions of safari
if (result.addEventListener) {
result.addEventListener("change", onChange);
} else {
result.addListener(onChange);
}
setValue(result.matches);
return value
return () => {
if (result.removeEventListener) {
result.removeEventListener("change", onChange);
} else {
result.removeListener(onChange);
}
};
}, [query]);
return value;
}
+11 -1
View File
@@ -1,5 +1,10 @@
import './index.css'
// import polyfills from core-js for older browsers
import 'core-js/actual/promise';
import 'core-js/actual/symbol';
import 'core-js/actual/object/assign';
import {StrictMode} from "react";
import {createRoot} from "react-dom/client";
import {createHashRouter, Navigate, RouterProvider} from "react-router-dom";
@@ -10,6 +15,7 @@ import {ThemeProvider} from "@/components/theme-provider.tsx";
import CardManager from "@/pages/card-manager.tsx";
import ServerCardIn from "@/pages/card-in-screen.tsx";
import ServerListing from "@/pages/server-listing.tsx";
import Privacy from "@/pages/privacy.tsx";
// iOS PWAs show the navigation bar when changing routes
// HashRouter doesn't have this issue as the URL never changes
@@ -33,7 +39,11 @@ const router = createHashRouter([
path: x,
element: <CardManager/>
};
})
}),
{
path: "/privacy",
element: <Privacy/>
}
])
createRoot(document.getElementById('root')!).render(
+39
View File
@@ -0,0 +1,39 @@
// Google Play forces a Privacy Policy even for closed tests...
import {Server} from "lucide-react";
import {useNavigate} from "react-router-dom";
import {Button} from "@/components/ui/button.tsx";
import {AMFooter} from "@/components/am-footer.tsx";
import {AMHeader} from "@/components/am-header.tsx";
function PrivacyPolicy() {
const navigate = useNavigate();
return (
<main className="flex flex-col gap-5 container min-h-screen py-10">
<AMHeader title="AMNet">
<Button variant="ghost" size={"default"} onClick={() => navigate('/')}>
<Server className="h-4 w-4 sm:mr-2"/>
<span className="hidden sm:block">Manage Servers</span>
</Button>
</AMHeader>
<div className="flex flex-col gap-4 w-full flex-grow">
<h4 className="font-semibold text-2xl">Privacy</h4>
<p>The AMNet card switcher collects and stores the following information locally (on-device):</p>
<ul className="list-disc">
<li>Server Addresses</li>
<li>Server Information (including machine name, identifiers and metrics that are cached and updated on each load)</li>
<li>Card Names and Access Codes</li>
</ul>
<p>The card switcher does <strong>NOT</strong> send any data to external servers or services, except to perform the card-in action where only the card number is transmitted.</p>
</div>
<AMFooter/>
</main>
)
}
export default PrivacyPolicy;
+4 -2
View File
@@ -1,5 +1,7 @@
import * as animate from "tailwindcss-animate";
/** @type {import('tailwindcss').Config} */
module.exports = {
export default {
darkMode: ["class"],
content: [
'./src/**/*.{ts,tsx}',
@@ -70,5 +72,5 @@ module.exports = {
},
},
},
plugins: [require("tailwindcss-animate")],
plugins: [animate],
}
+1 -1
View File
@@ -7,7 +7,7 @@
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"module": "ES2020",
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
+5
View File
@@ -11,6 +11,7 @@ export default defineConfig({
},
},
build: {
target: "ES2018",
rollupOptions: {
output: {
manualChunks(id: string) {
@@ -21,6 +22,10 @@ export default defineConfig({
if (id.includes('react')) {
return 'react';
}
if (id.includes('core-js')) {
return "polyfills";
}
}
}
}
+29
View File
@@ -0,0 +1,29 @@
diff --git a/amnet-native/android/app/build.gradle b/amnet-native/android/app/build.gradle
index 6ee4a34..eb8c5f0 100644
--- a/amnet-native/android/app/build.gradle
+++ b/amnet-native/android/app/build.gradle
@@ -124,15 +124,21 @@ android {
keyAlias 'androiddebugkey'
keyPassword 'android'
}
+ release {
+ if (project.hasProperty('AMNET_UPLOAD_STORE_FILE')) {
+ storeFile file(AMNET_UPLOAD_STORE_FILE)
+ storePassword AMNET_UPLOAD_STORE_PASSWORD
+ keyAlias AMNET_UPLOAD_KEY_ALIAS
+ keyPassword AMNET_UPLOAD_KEY_PASSWORD
+ }
+ }
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
- // Caution! In production, you need to generate your own keystore file.
- // see https://reactnative.dev/docs/signed-apk-android.
- signingConfig signingConfigs.debug
+ signingConfig signingConfigs.release
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
+52 -32
View File
@@ -1,7 +1,7 @@
<div align="center">
# AMNet
[TestFlight](https://testflight.apple.com/join/kHbeJTsQ) • [Website](http://card.ppc.moe) • [Server DLLs](https://gitea.tendokyu.moe/ppc/amnet/releases)
[TestFlight](https://testflight.apple.com/join/kHbeJTsQ) • [Firebase](https://appdistribution.firebase.dev/i/6acc3151357deaa8) • [Website](http://card.ppc.moe) • [Server DLLs](https://gitea.tendokyu.moe/ppc/amnet/releases)
An Aime Card Switcher for segatools-based games.
@@ -9,42 +9,62 @@ An Aime Card Switcher for segatools-based games.
## Overview
AMNet is designed to simulate the `DEVICE/aime.txt` file, allowing multiple users to log into a game using their phone.
This was originally designed for use with maimai/Chunithm, but should be compatible with any segatools-based game with Aime support.
This was originally intended for use with maimai and Chunithm, but should be compatible with any segatools-based game with Aime support.
### Setup
## Setup
1. Download the latest `amnet.dll` from the releases page
2. Move `amnet.dll` to the game directory
3. Edit `segatools.ini` to use the new DLL:
```ini
[aimeio]
; To use a custom card reader IO DLL enter its path here.
; Leave empty if you want to use Segatools built-in keyboard input.
path=amnet.dll
; AMNet-specific options (all optional)
; gameId - the 4 character game ID (will show the game icon in the UI)
; serverName - the name of the server to show to users (defaults to the computer name if not set)
; serverAddress - the address to listen on, including the protocol (defaults to http://+:6070, where + is a wildcard)
; enableKeyboardMode - whether to enable hold-enter-to-login mode. Reads the file set at aimePath for card number (or DEVICE/aime.txt if not set).
gameId=SDEZ
;serverName=CHUNI-PENGUIN
;serverAddress=http://+:6070
;enableKeyboardMode=1
```
```ini
[aimeio]
; To use a custom card reader IO DLL enter its path here.
; Leave empty if you want to use Segatools built-in keyboard input.
path=amnet.dll
; AMNet-specific options (all optional)
; gameId - the 4 character game ID (will show the game icon in the UI)
; serverName - the name of the server to show to users (defaults to the computer name if not set)
; serverAddress - the address to listen on, including the protocol (defaults to http://+:6070, where + is a wildcard)
; enableKeyboardMode - whether to enable hold-enter-to-login mode. Reads the file set at aimePath for card number (or DEVICE/aime.txt if not set).
gameId=SDEZ
;serverName=CHUNI-PENGUIN
;serverAddress=http://+:6070
;enableKeyboardMode=1
```
4. Launch the game, and check the amdaemon console for server startup messages
5. Access the using one of the interfaces (see below), connect to the running server, add a card and start playing!
5. Access the using one of the interfaces (see below), add a new server `http://<your-ip-address>:6070`, add a card and start playing!
### Web Interface
The web interface can be accessed by navigating to http://card.ppc.moe/ on a device on the same network as the server.
This website is also installable as a PWA, which will allow it to be registered as an app on your device.
## Interfaces
AMNet can be used on most platforms, see below for options available for each platform:
> Because the server can only listen on HTTP, the website needs to be visited over HTTP as well. This may cause issues with some devices, as they may block HTTP requests on HTTPS websites.
[![](./res/web.jpeg)](http://card.ppc.moe)
### Native Application
The AMNet application is available (and fully supported) for iOS devices, and can be downloaded by joining the TestFlight (see links at the top of the file) or built manually.
Android is technically supported but hasn't been tested; as such APKs aren't provided at this moment.
<table>
<tbody>
<tr>
<th>Platform</th>
<th>Method</th>
<th>Notes</th>
</tr>
<tr>
<td>iOS</td>
<td><a href="https://testflight.apple.com/join/kHbeJTsQ" target="_blank">TestFlight</a></td>
<td></td>
</tr>
<tr>
<td rowspan="2">Android</td>
<td><a href="https://appdistribution.firebase.dev/i/6acc3151357deaa8" target="_blank">Firebase</a></td>
<td>Recommended for reduced download/install size and easier upgrading</td>
</tr>
<tr>
<td>.apk</td>
<td>Larger download, manual installation required.</td>
</tr>
<tr>
<td>All</td>
<td><a href="http://card.ppc.moe" target="_blank">Web</a></td>
<td>Works on all platforms and is installable as a PWA. Ensure it's used on <code>http</code> to avoid CORS-related errors</td>
</tr>
</tbody>
</table>
<div align="center">
<img src="./res/native.png" height="600"/>
@@ -62,7 +82,7 @@ Building the server without publish mode will result in a DLL that cannot be run
dotnet publish -c Release -r win-x64 -p:Version=1.0.0 AMNet.Server/AMNet.Server.csproj
```
The dll will be available at `.\AMNet.Server\bin\Release\net8.0-windows\win-x64\publish\amnet.dll`.
The dll will be available at `.\AMNet.Server\bin\Release\net9.0-windows\win-x64\publish\amnet.dll`.
### Website (amnet-web)
The web interface is built using Vite and React. Building the web interface requires Node.js (or alternative runtime).
@@ -79,7 +99,7 @@ npm run build # build the website, outputs to ./dist
The mobile app is built using Expo and React Native. The app requires Node.js and npm to build, along with either the Android SDK or Xcode for iOS.
After cloning the repository, install dependencies then run the app, or prebuild for the platform of your choice then complete the build process for a standalone app:
> amnet-native relies on one or more native libraries, so Expo Go cannot be used. The app must be built and run on a physical device or simulator.
> amnet-native relies on multiple native libraries not included in Expo Go. The app must be built from scratch and run on a physical device or simulator.
```shell
cd amnet-native