mirror of
https://gitea.tendokyu.moe/ppc/amnet.git
synced 2026-10-06 21:58:06 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d3b129126 | ||
|
|
b729b4e31b | ||
|
|
377fe93645 | ||
|
|
cbd2825475 | ||
|
|
b9bbcc9228 | ||
|
|
61fb81c75c | ||
|
|
d3b96d3017 | ||
|
|
9b136006a0 | ||
|
|
2eb61d74a7 | ||
|
|
89101c5d5f | ||
|
|
2fe9238d02 | ||
|
|
d1b626ffac | ||
|
|
fe7ab1c50f | ||
|
|
5a3a7be906 | ||
|
|
703067e6d6 | ||
|
|
67f90397b0 | ||
|
|
8ba439721f | ||
|
|
35a8c28c0e | ||
|
|
a0b44c165b | ||
|
|
489a93adfe | ||
|
|
6172f2bbd7 | ||
|
|
414a199552 | ||
|
|
a583b72798 | ||
|
|
33cd321325 | ||
|
|
85b900ea30 | ||
|
|
4038de583d | ||
|
|
b08a7cd125 | ||
|
|
4b6037fe24 | ||
|
|
4702052cc4 | ||
|
|
969edd3675 | ||
|
|
0d200a22fc | ||
|
|
c30abfe482 | ||
|
|
6628071f5b |
@@ -40,7 +40,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-dotnet@v4
|
- uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.x'
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
- name: Set version
|
- name: Set version
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -4,18 +4,18 @@
|
|||||||
<PublishAot>true</PublishAot>
|
<PublishAot>true</PublishAot>
|
||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<EventSourceSupport>false</EventSourceSupport>
|
<EventSourceSupport>false</EventSourceSupport>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
<OptimizationPreference>Size</OptimizationPreference>
|
<OptimizationPreference>Size</OptimizationPreference>
|
||||||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AssemblyName>amnet</AssemblyName>
|
<Product>AMNet</Product>
|
||||||
<ProductName>AMNet</ProductName>
|
|
||||||
<CompanyName>ppc</CompanyName>
|
<CompanyName>ppc</CompanyName>
|
||||||
<Title>AMNet Server</Title>
|
<AssemblyName>amnet</AssemblyName>
|
||||||
<Description>Server component for the AMNet IC Card switcher</Description>
|
<AssemblyTitle>AMNet Server</AssemblyTitle>
|
||||||
<Copyright>Copyright 2024 (c) ppc</Copyright>
|
<Copyright>Copyright 2024 (c) ppc</Copyright>
|
||||||
|
<Description>Server component for the AMNet IC Card switcher</Description>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace AMNet.Server;
|
namespace AMNet.Server;
|
||||||
|
|
||||||
@@ -9,9 +10,9 @@ namespace AMNet.Server;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal class CardPresenter
|
internal class CardPresenter
|
||||||
{
|
{
|
||||||
private readonly object _cardLock = new();
|
private readonly Lock _cardLock = new();
|
||||||
private StoredCard _currentCard;
|
private StoredCard _currentCard;
|
||||||
|
|
||||||
public record StoredCard(byte[] Value, string OriginalValue, long ExpiresAt)
|
public record StoredCard(byte[] Value, string OriginalValue, long ExpiresAt)
|
||||||
{
|
{
|
||||||
public bool Expired => Environment.TickCount64 >= ExpiresAt;
|
public bool Expired => Environment.TickCount64 >= ExpiresAt;
|
||||||
@@ -65,7 +66,7 @@ internal class CardPresenter
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var card = new StoredCard(bytes, matrixCode, Environment.TickCount64 + validFor);
|
var card = new StoredCard(bytes, matrixCode, Environment.TickCount64 + validFor);
|
||||||
|
|
||||||
lock (_cardLock)
|
lock (_cardLock)
|
||||||
|
|||||||
@@ -14,17 +14,17 @@ internal static class Config
|
|||||||
{
|
{
|
||||||
GameId = ReadKey(IOSection, "gameId", 4);
|
GameId = ReadKey(IOSection, "gameId", 4);
|
||||||
ServerName = ReadKey(IOSection, "serverName", 26, Environment.MachineName);
|
ServerName = ReadKey(IOSection, "serverName", 26, Environment.MachineName);
|
||||||
|
|
||||||
EnableAimeTxt = PInvoke.GetPrivateProfileInt(IOSection, "enableKeyboardMode", 1, ConfigFileName) > 0;
|
EnableAimeTxt = PInvoke.GetPrivateProfileInt(IOSection, "enableKeyboardMode", 1, ConfigFileName) > 0;
|
||||||
AimeTxtPath = ReadKey("aime", "aimePath", PInvoke.MAX_PATH, @"DEVICE\aime.txt");
|
AimeTxtPath = ReadKey("aime", "aimePath", PInvoke.MAX_PATH, @"DEVICE\aime.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GameId { get; }
|
public static string GameId { get; }
|
||||||
public static string ServerName { get; }
|
public static string ServerName { get; }
|
||||||
|
|
||||||
public static bool EnableAimeTxt { get; }
|
public static bool EnableAimeTxt { get; }
|
||||||
public static string AimeTxtPath { get; }
|
public static string AimeTxtPath { get; }
|
||||||
|
|
||||||
internal static unsafe string ReadKey(string section, string key, uint maxLength, string @default = null)
|
internal static unsafe string ReadKey(string section, string key, uint maxLength, string @default = null)
|
||||||
{
|
{
|
||||||
// +1 for null terminator
|
// +1 for null terminator
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ internal static partial class WebServer
|
|||||||
builder.Services.AddSingleton<CardPresenter>();
|
builder.Services.AddSingleton<CardPresenter>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseCors();
|
app.UseCors();
|
||||||
|
|
||||||
app.MapGet("/amnet/info", ServerInfo);
|
app.MapGet("/amnet/info", ServerInfo);
|
||||||
@@ -83,7 +83,7 @@ internal static partial class WebServer
|
|||||||
Config.ServerName,
|
Config.ServerName,
|
||||||
Environment.TickCount64 - startedAt,
|
Environment.TickCount64 - startedAt,
|
||||||
lastPollAt < 0 ? null : Environment.TickCount64 - lastPollAt);
|
lastPollAt < 0 ? null : Environment.TickCount64 - lastPollAt);
|
||||||
|
|
||||||
return Results.Ok(state);
|
return Results.Ok(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+57
-3
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "AMNet",
|
"name": "AMNet",
|
||||||
"scheme": "amnet",
|
"scheme": "amnet",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"slug": "amnet-native",
|
"slug": "amnet-native",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
@@ -19,7 +19,61 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {generateCardNumber} from "amnet-shared";
|
import {generateCardNumber} from "amnet-shared";
|
||||||
import {useModalStyles} from "../utils/ModalStyles";
|
import {useModalStyles} from "../utils/ModalStyles";
|
||||||
import {useCallback, useLayoutEffect, useState} from "react";
|
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 {Button, Card, Divider, Paragraph, useTheme} from "react-native-paper";
|
||||||
|
|
||||||
import {AMCard} from "../models/AMCard";
|
import {AMCard} from "../models/AMCard";
|
||||||
@@ -36,13 +36,15 @@ export default function CardAddModal({navigation, route}) {
|
|||||||
}, [realm]);
|
}, [realm]);
|
||||||
|
|
||||||
// header effects
|
// header effects
|
||||||
useLayoutEffect(() => {
|
if (Platform.OS === 'ios') {
|
||||||
navigation.setOptions({
|
useLayoutEffect(() => {
|
||||||
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
navigation.setOptions({
|
||||||
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
|
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||||
</TouchableOpacity>,
|
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
|
||||||
});
|
</TouchableOpacity>,
|
||||||
}, [navigation, modalStyles]);
|
});
|
||||||
|
}, [navigation, modalStyles]);
|
||||||
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const canSave = cardName.length && cardNumber.length === 20;
|
const canSave = cardName.length && cardNumber.length === 20;
|
||||||
@@ -103,7 +105,7 @@ export default function CardAddModal({navigation, route}) {
|
|||||||
placeholderTextColor={theme.colors.outline}
|
placeholderTextColor={theme.colors.outline}
|
||||||
value={cardNumber.match(/.{1,4}/g)?.join(" ")}
|
value={cardNumber.match(/.{1,4}/g)?.join(" ")}
|
||||||
onChangeText={val => setCardNumber(val?.replaceAll(/\D/g, '').substring(0, 20) ?? '')}
|
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>
|
</View>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import * as Clipboard from "expo-clipboard";
|
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 React, {useCallback, useMemo} from "react";
|
||||||
import {Button, useTheme} from "react-native-paper";
|
import {Button, useTheme} from "react-native-paper";
|
||||||
import {CreditCard, Plus} from "lucide-react-native";
|
import {CreditCard, Plus} from "lucide-react-native";
|
||||||
import ContextMenu from "react-native-context-menu-view";
|
import ContextMenu from "react-native-context-menu-view";
|
||||||
|
|
||||||
import {AMCard} from "../models/AMCard";
|
|
||||||
import {AimeCard} from "../components/AimeCard";
|
import {AimeCard} from "../components/AimeCard";
|
||||||
import RealmContext from "../models/RealmContext";
|
|
||||||
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
||||||
|
|
||||||
|
import {AMCard} from "../models/AMCard";
|
||||||
|
import RealmContext from "../models/RealmContext";
|
||||||
|
|
||||||
const {useQuery, useRealm} = RealmContext;
|
const {useQuery, useRealm} = RealmContext;
|
||||||
|
|
||||||
function CardEntry(props: { card: AMCard, navigation: any }) {
|
function CardEntry(props: { card: AMCard, navigation: any }) {
|
||||||
@@ -43,7 +44,7 @@ function CardEntry(props: { card: AMCard, navigation: any }) {
|
|||||||
actions={[
|
actions={[
|
||||||
{title: "Copy Card Number", systemIcon: "doc.on.doc"},
|
{title: "Copy Card Number", systemIcon: "doc.on.doc"},
|
||||||
{
|
{
|
||||||
title: "",
|
title: Platform.OS === 'ios' ? "" : "Additional Actions",
|
||||||
inlineChildren: true,
|
inlineChildren: true,
|
||||||
actions: [
|
actions: [
|
||||||
{title: "Edit Card", systemIcon: "square.and.pencil"},
|
{title: "Edit Card", systemIcon: "square.and.pencil"},
|
||||||
@@ -55,16 +56,16 @@ function CardEntry(props: { card: AMCard, navigation: any }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function CardManager({navigation}) {
|
export default function CardManager({navigation}) {
|
||||||
const query = useQuery(AMCard);
|
|
||||||
const cards = useMemo(() => query.sorted("added", true), [query]);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const query = useQuery(AMCard);
|
||||||
|
const cards = useMemo(() => query.sorted("added", false), [query]);
|
||||||
|
|
||||||
if (!cards.length) {
|
if (!cards.length) {
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1, gap: 10, justifyContent: "center", alignItems: "center"}}>
|
<View style={{flex: 1, gap: 10, justifyContent: "center", alignItems: "center"}}>
|
||||||
<EmptyStatePlaceholder message="No cards added yet" icon={<CreditCard color="gray"/>}/>
|
<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>
|
icon={() => <Plus size={18} color={theme.colors.primary}/>}>Add Card</Button>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -76,7 +77,14 @@ export default function CardManager({navigation}) {
|
|||||||
data={cards}
|
data={cards}
|
||||||
keyExtractor={i => i._id.toString()}
|
keyExtractor={i => i._id.toString()}
|
||||||
contentContainerStyle={{paddingBottom: 120}}
|
contentContainerStyle={{paddingBottom: 120}}
|
||||||
|
renderItem={i => <CardEntry card={i.item} navigation={navigation}/>}
|
||||||
ItemSeparatorComponent={() => <View style={{height: 15}}/>}
|
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>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Plus} from "lucide-react-native";
|
import {Plus} from "lucide-react-native";
|
||||||
import {TouchableOpacity} from "react-native";
|
|
||||||
import {useTheme} from "@react-navigation/native";
|
import {useTheme} from "@react-navigation/native";
|
||||||
|
import {Platform, TouchableOpacity} from "react-native";
|
||||||
import {createNativeStackNavigator} from "@react-navigation/native-stack";
|
import {createNativeStackNavigator} from "@react-navigation/native-stack";
|
||||||
|
|
||||||
import CardManager from "./CardManager";
|
import CardManager from "./CardManager";
|
||||||
@@ -22,7 +22,7 @@ export default function CardManagerStack({navigation}) {
|
|||||||
component={CardManager}
|
component={CardManager}
|
||||||
options={{
|
options={{
|
||||||
title: "Cards",
|
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>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {useModalStyles} from "../utils/ModalStyles";
|
import {useModalStyles} from "../utils/ModalStyles";
|
||||||
import {Card, Paragraph, useTheme} from "react-native-paper";
|
import {Card, Paragraph, useTheme} from "react-native-paper";
|
||||||
import {TextInput, TouchableOpacity, View} from "react-native";
|
|
||||||
import {useCallback, useEffect, useLayoutEffect, useState} from "react";
|
import {useCallback, useEffect, useLayoutEffect, useState} from "react";
|
||||||
|
import {Platform, TextInput, TouchableOpacity, View} from "react-native";
|
||||||
|
|
||||||
import RealmContext from "../models/RealmContext";
|
import RealmContext from "../models/RealmContext";
|
||||||
import {AMServer} from "../models/AMServer";
|
import {AMServer} from "../models/AMServer";
|
||||||
@@ -20,13 +20,15 @@ export default function ServerAddModal({route, navigation}) {
|
|||||||
handleDeepLink(realm, navigation, route, setServerAddress);
|
handleDeepLink(realm, navigation, route, setServerAddress);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
if (Platform.OS === 'ios') {
|
||||||
navigation.setOptions({
|
useLayoutEffect(() => {
|
||||||
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
navigation.setOptions({
|
||||||
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
|
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||||
</TouchableOpacity>,
|
<Paragraph style={modalStyles.headerText}>Cancel</Paragraph>
|
||||||
});
|
</TouchableOpacity>,
|
||||||
}, [navigation, modalStyles]);
|
});
|
||||||
|
}, [navigation, modalStyles]);
|
||||||
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
let urlValid = false;
|
let urlValid = false;
|
||||||
|
|||||||
+153
-105
@@ -1,15 +1,33 @@
|
|||||||
import {BSON} from "realm";
|
import {BSON} from "realm";
|
||||||
import {useSafeAreaInsets} from "react-native-safe-area-context";
|
|
||||||
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useState} from "react";
|
import React, {useCallback, useEffect, useLayoutEffect, useMemo, useState} from "react";
|
||||||
import {ActivityIndicator, Button, Paragraph, Title, useTheme} from "react-native-paper";
|
import {ActivityIndicator, Button, Paragraph, Title, useTheme} from "react-native-paper";
|
||||||
import {Alert, RefreshControl, ScrollView, Share, StyleSheet, TouchableOpacity, View} from "react-native";
|
|
||||||
import {
|
import {
|
||||||
CircleCheck, CircleDot, CreditCard, Nfc, Plus, SmartphoneNfcIcon,
|
Alert,
|
||||||
Share as ShareIcon, WifiOff, TvMinimalPlay
|
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";
|
} from "lucide-react-native";
|
||||||
|
|
||||||
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
||||||
import {AMUIStatusAlert} from "../components/AMUIStatusAlert";
|
import {AMUIStatusAlert} from "../components/AMUIStatusAlert";
|
||||||
|
import {AMUICardDetail} from "../components/AMUICard";
|
||||||
import {AimeCard} from "../components/AimeCard";
|
import {AimeCard} from "../components/AimeCard";
|
||||||
|
|
||||||
import RealmContext from "../models/RealmContext";
|
import RealmContext from "../models/RealmContext";
|
||||||
@@ -29,38 +47,33 @@ const {useRealm, useQuery, useObject} = RealmContext;
|
|||||||
export default function ServerCardIn({route, navigation}) {
|
export default function ServerCardIn({route, navigation}) {
|
||||||
const realm = useRealm();
|
const realm = useRealm();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const safeArea = useSafeAreaInsets();
|
|
||||||
|
|
||||||
// card listing state
|
let {serverId} = route.params;
|
||||||
const query = useQuery(AMCard);
|
const server = useObject(AMServer, ObjectId.createFromHexString(serverId));
|
||||||
const cards = useMemo(() => query.sorted("lastUsed", true), [query]);
|
const cards = useQuery({
|
||||||
|
type: AMCard,
|
||||||
const [selectedCard, setSelectedCard] = useState<AMCard>(null);
|
query: q => q.sorted("lastUsed", true)
|
||||||
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
|
// server state
|
||||||
const [serverRefreshing, setServerRefreshing] = useState<boolean>(false);
|
const [serverRefreshing, setServerRefreshing] = useState<boolean>(false);
|
||||||
const [serverReloadCounter, setServerReloadCounter] = useState<number>(0);
|
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 [userSelectedCard, setUserSelectedCard] = useState<AMCard>(null);
|
||||||
const server = useObject(AMServer, ObjectId.createFromHexString(serverId));
|
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(() => {
|
useLayoutEffect(() => {
|
||||||
if (server?.isValid() !== true) {
|
if (server?.isValid() !== true) {
|
||||||
@@ -71,65 +84,80 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
title: server.serverName,
|
title: server.serverName,
|
||||||
headerRight: () => (
|
headerRight: () => (
|
||||||
<TouchableOpacity onPress={() => Share.share({url: server.shareUrl})}>
|
<TouchableOpacity onPress={() => Share.share({url: server.shareUrl})}>
|
||||||
<ShareIcon/>
|
<ShareIcon color={theme.colors.primary}/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}, [navigation, server]);
|
}, [navigation, server]);
|
||||||
|
|
||||||
// card selection, server connection checks
|
// invalid card/server selection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (server?.isValid() !== true) {
|
if (server?.isValid() !== true) {
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [server]);
|
||||||
|
|
||||||
// don't show error on invalid server, just means it was deleted
|
useEffect(() => {
|
||||||
if (!server) {
|
if (userSelectedCard?.isValid() === false) {
|
||||||
Alert.alert("Server not found", "The server you are looking for does not exist.");
|
setUserSelectedCard(null);
|
||||||
}
|
}
|
||||||
|
}, [userSelectedCard]);
|
||||||
|
|
||||||
|
// handle auto-refresh
|
||||||
|
useEffect(() => {
|
||||||
|
if (!serverConnectionSuccess || server?.isValid() !== true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCard?.isValid() !== true) {
|
const metrics = server as AMServerInstanceMetrics;
|
||||||
setSelectedCard(cards.length ? cards[0] : null);
|
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();
|
const abortController = new AbortController();
|
||||||
fetchServerInfo(server.serverAddress, abortController.signal).then(r => {
|
fetchServerInfo(server.serverAddress, abortController.signal).then(r => {
|
||||||
if (r) {
|
if (!r) {
|
||||||
setConnectionTestResult(true);
|
setServerConnectionSuccess(false);
|
||||||
realm.write(() => {
|
return;
|
||||||
server.lastUsed = new Date();
|
|
||||||
Object.keys(r).forEach(x => server[x] = r[x]);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setConnectionTestResult(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setServerConnectionSuccess(true);
|
||||||
|
|
||||||
|
realm.write(() => {
|
||||||
|
server.lastUsed = new Date();
|
||||||
|
Object.keys(r).forEach(x => server[x] = r[x]);
|
||||||
|
});
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setConnectionTestResult(false);
|
setServerConnectionSuccess(false);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
setServerRefreshing(false);
|
setServerRefreshing(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => abortController.abort(), 5000);
|
const timeout = setTimeout(() => abortController.abort(), 5000);
|
||||||
return () => abortController.abort();
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
abortController.abort();
|
||||||
|
}
|
||||||
}, [serverId, serverReloadCounter, realm]);
|
}, [serverId, serverReloadCounter, realm]);
|
||||||
|
|
||||||
// card in callback
|
|
||||||
const cardInCallback = useCallback(() => {
|
const cardInCallback = useCallback(() => {
|
||||||
setCardInCompleted(false);
|
setCardInCompleted(false);
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
performCardIn(server, {cardId: selectedCard.code}, abortController.signal).then(async r => {
|
performCardIn(server, {cardId: selectedCard.code}, abortController.signal).then(async r => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
setCardInCompleted(r);
|
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
server.lastUsed = now;
|
server.lastUsed = now;
|
||||||
selectedCard.lastUsed = now;
|
selectedCard.lastUsed = now;
|
||||||
|
selectedCard.lastUsedGameId = server.gameId;
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelectedCard(selectedCard);
|
setCardInCompleted(r);
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setCardInCompleted(false);
|
setCardInCompleted(false);
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
@@ -143,12 +171,13 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
const performRefresh = () => {
|
const performRefresh = () => {
|
||||||
setServerRefreshing(true);
|
setServerRefreshing(true);
|
||||||
setServerReloadCounter(serverReloadCounter + 1);
|
setServerReloadCounter(serverReloadCounter + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (server?.isValid() !== true) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gameName = useMemo(() => getGameName(server.gameId), [server.gameId]);
|
if (serverConnectionSuccess === null) {
|
||||||
const disableCardIn = useMemo(() => cardInCompleted !== null || !connectionTestResult, [cardInCompleted, connectionTestResult]);
|
|
||||||
|
|
||||||
if (connectionTestResult === null) {
|
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1, justifyContent: "center", alignItems: "center", gap: 10}}>
|
<View style={{flex: 1, justifyContent: "center", alignItems: "center", gap: 10}}>
|
||||||
<ActivityIndicator animating/>
|
<ActivityIndicator animating/>
|
||||||
@@ -158,59 +187,63 @@ export default function ServerCardIn({route, navigation}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView contentInsetAdjustmentBehavior="automatic"
|
<ScrollView contentInsetAdjustmentBehavior="automatic" refreshControl={<RefreshControl refreshing={serverRefreshing} onRefresh={performRefresh}/>}>
|
||||||
contentContainerStyle={{...styles.container, paddingTop: connectionTestResult ? 0 : 10}}
|
<View style={styles.container}>
|
||||||
refreshControl={<RefreshControl refreshing={serverRefreshing} tintColor={theme.colors.onSurface} onRefresh={performRefresh}/>}>
|
<StatusAlert server={server} connectionEstablished={serverConnectionSuccess}/>
|
||||||
<StatusAlert server={server} connectionEstablished={connectionTestResult}/>
|
<View style={styles.section}>
|
||||||
<View style={styles.section}>
|
<Title style={styles.sectionTitle}>{server.gameId ? `Play ${getGameName(server.gameId)}` : "Selected Card"}</Title>
|
||||||
<Title style={styles.sectionTitle}>{gameName ? `Play ${gameName}` : "Selected Card"}</Title>
|
{selectedCard ? <>
|
||||||
{selectedCard ? <>
|
<AimeCard actionDisabled
|
||||||
<AimeCard actionDisabled
|
card={selectedCard}
|
||||||
card={selectedCard}
|
actionIcon={<CircleCheck color="green" size={24}/>}>
|
||||||
actionIcon={<CircleCheck color="green" size={24}/>}/>
|
{serverDefaultCard?.code == selectedCard.code && <DefaultCardDetail/>}
|
||||||
<Button textColor="white"
|
</AimeCard>
|
||||||
onPress={cardInCallback}
|
<Button textColor="white"
|
||||||
disabled={disableCardIn}
|
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'}}
|
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')}>
|
onPress={() => navigation.navigate('add-card')}>
|
||||||
Add card
|
Add card
|
||||||
</Button>
|
</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>
|
</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>
|
</ScrollView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -245,6 +278,20 @@ function StatusAlert(props: {
|
|||||||
return null;
|
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({
|
const styles = StyleSheet.create({
|
||||||
section: {
|
section: {
|
||||||
gap: 10
|
gap: 10
|
||||||
@@ -257,6 +304,7 @@ const styles = StyleSheet.create({
|
|||||||
gap: 15
|
gap: 15
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
|
paddingTop: Platform.OS === 'android' ? 120 : 0,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingBottom: 80,
|
paddingBottom: 80,
|
||||||
gap: 20
|
gap: 20
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {Button, Paragraph, useTheme} from "react-native-paper";
|
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 React, {useCallback, useLayoutEffect, useMemo, useState} from "react";
|
||||||
import {Globe, History, Joystick, Plus, ServerOff} from "lucide-react-native";
|
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 ContextMenu, {ContextMenuOnPressNativeEvent} from "react-native-context-menu-view";
|
||||||
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
import {EmptyStatePlaceholder} from "../components/EmptyStatePlaceholder";
|
||||||
import {AMUICard, AMUICardDetail} from "../components/AMUICard";
|
import {AMUICard, AMUICardDetail} from "../components/AMUICard";
|
||||||
@@ -16,7 +16,7 @@ const {useRealm, useQuery} = RealmContext;
|
|||||||
function ServerCard(props: { server: AMServer, navigation: any }) {
|
function ServerCard(props: { server: AMServer, navigation: any }) {
|
||||||
const realm = useRealm();
|
const realm = useRealm();
|
||||||
const gameName = useMemo(() => getGameName(props.server.gameId), [props.server.gameId]);
|
const gameName = useMemo(() => getGameName(props.server.gameId), [props.server.gameId]);
|
||||||
|
|
||||||
const pageCallback = useCallback(() => {
|
const pageCallback = useCallback(() => {
|
||||||
props.navigation.navigate('server', {
|
props.navigation.navigate('server', {
|
||||||
serverId: props.server._id.toHexString()
|
serverId: props.server._id.toHexString()
|
||||||
@@ -43,7 +43,7 @@ function ServerCard(props: { server: AMServer, navigation: any }) {
|
|||||||
<ContextMenu onPress={pageContextMenuCallback} actions={[
|
<ContextMenu onPress={pageContextMenuCallback} actions={[
|
||||||
{title: `Select ${props.server.serverName}`, systemIcon: "chevron.right"},
|
{title: `Select ${props.server.serverName}`, systemIcon: "chevron.right"},
|
||||||
{
|
{
|
||||||
title: "",
|
title: Platform.OS === "ios" ? "" : "Additional Actions",
|
||||||
inlineChildren: true,
|
inlineChildren: true,
|
||||||
actions: [
|
actions: [
|
||||||
{title: "Share Server", systemIcon: "square.and.arrow.up"},
|
{title: "Share Server", systemIcon: "square.and.arrow.up"},
|
||||||
@@ -98,6 +98,8 @@ export default function ServerManager({navigation}) {
|
|||||||
value: searchQuery,
|
value: searchQuery,
|
||||||
autoCorrect: "none",
|
autoCorrect: "none",
|
||||||
autoCapitalize: "none",
|
autoCapitalize: "none",
|
||||||
|
textColor: theme.colors.onSurface,
|
||||||
|
headerIconColor: theme.colors.primary,
|
||||||
onChangeText: e => setSearchQuery(e.nativeEvent.text)
|
onChangeText: e => setSearchQuery(e.nativeEvent.text)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -121,7 +123,13 @@ export default function ServerManager({navigation}) {
|
|||||||
keyExtractor={i => i._id.toString()}
|
keyExtractor={i => i._id.toString()}
|
||||||
style={{padding: 16, paddingTop: 24}}
|
style={{padding: 16, paddingTop: 24}}
|
||||||
contentContainerStyle={{paddingBottom: 80}}
|
contentContainerStyle={{paddingBottom: 80}}
|
||||||
|
renderItem={i => <ServerCard navigation={navigation} server={i.item}/>}
|
||||||
ItemSeparatorComponent={() => <View style={{height: 15}}/>}
|
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>
|
||||||
|
)}/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Plus} from "lucide-react-native";
|
import {Plus} from "lucide-react-native";
|
||||||
import {TouchableOpacity} from "react-native";
|
|
||||||
import {useTheme} from "@react-navigation/native";
|
import {useTheme} from "@react-navigation/native";
|
||||||
|
import {Platform, TouchableOpacity} from "react-native";
|
||||||
import {createNativeStackNavigator} from "@react-navigation/native-stack";
|
import {createNativeStackNavigator} from "@react-navigation/native-stack";
|
||||||
|
|
||||||
import ServerCardIn from "./ServerCardIn";
|
import ServerCardIn from "./ServerCardIn";
|
||||||
@@ -24,7 +24,7 @@ export default function ServerManagerStack({navigation}) {
|
|||||||
component={ServerManager}
|
component={ServerManager}
|
||||||
options={{
|
options={{
|
||||||
title: "Servers",
|
title: "Servers",
|
||||||
headerRight: () => <TouchableOpacity
|
headerRight: () => Platform.OS === 'android' ? null : <TouchableOpacity
|
||||||
onPress={() => navigation.navigate('add-server')}>
|
onPress={() => navigation.navigate('add-server')}>
|
||||||
<Plus/>
|
<Plus/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -1,4 +1,4 @@
|
|||||||
import {useMemo} from "react";
|
import {ReactNode, useMemo} from "react";
|
||||||
import {Text} from "react-native";
|
import {Text} from "react-native";
|
||||||
import {CreditCard, History} from "lucide-react-native";
|
import {CreditCard, History} from "lucide-react-native";
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import {AMUICard, AMUICardActionProps, AMUICardDetail} from "./AMUICard";
|
|||||||
|
|
||||||
export interface AimeCardProps extends AMUICardActionProps {
|
export interface AimeCardProps extends AMUICardActionProps {
|
||||||
card: AMCardData,
|
card: AMCardData,
|
||||||
|
children?: ReactNode,
|
||||||
showFullCardNumber?: boolean
|
showFullCardNumber?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ export function AimeCard(props: AimeCardProps) {
|
|||||||
<TimeAgo style={{color: "gray"}} dateTo={props.card.lastUsed}/>
|
<TimeAgo style={{color: "gray"}} dateTo={props.card.lastUsed}/>
|
||||||
</AMUICardDetail>
|
</AMUICardDetail>
|
||||||
)}
|
)}
|
||||||
|
{props.children}
|
||||||
</AMUICard>
|
</AMUICard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ export class AMCard extends Realm.Object<AMCard> implements AMCardData {
|
|||||||
|
|
||||||
added: Date;
|
added: Date;
|
||||||
lastUsed?: Date;
|
lastUsed?: Date;
|
||||||
|
lastUsedGameId?: string | null;
|
||||||
|
|
||||||
static generate(code: string, name: string) {
|
static generate(code: string, name: string) {
|
||||||
return {
|
return {
|
||||||
@@ -35,7 +36,8 @@ export class AMCard extends Realm.Object<AMCard> implements AMCardData {
|
|||||||
code: "string",
|
code: "string",
|
||||||
name: {type: "string", default: "New Card"},
|
name: {type: "string", default: "New Card"},
|
||||||
added: {type: "date", default: new Date()},
|
added: {type: "date", default: new Date()},
|
||||||
lastUsed: {type: "date", optional: true}
|
lastUsed: {type: "date", optional: true},
|
||||||
|
lastUsedGameId: {type: "string", optional: true}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import {AMServer} from "./AMServer";
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export default createRealmContext({
|
export default createRealmContext({
|
||||||
schemaVersion: 2,
|
schemaVersion: 3,
|
||||||
schema: [AMCard, AMServer],
|
schema: [AMCard, AMServer],
|
||||||
deleteRealmIfMigrationNeeded: false
|
deleteRealmIfMigrationNeeded: false
|
||||||
});
|
});
|
||||||
Generated
+4667
-4454
File diff suppressed because it is too large
Load Diff
+19
-18
@@ -13,32 +13,33 @@
|
|||||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||||
"@react-navigation/native": "^6.1.18",
|
"@react-navigation/native": "^6.1.18",
|
||||||
"@react-navigation/native-stack": "^6.11.0",
|
"@react-navigation/native-stack": "^6.11.0",
|
||||||
"@realm/react": "^0.9.0",
|
"@realm/react": "^0.11.0",
|
||||||
"@types/react": "~18.2.79",
|
"@types/react": "~18.3.12",
|
||||||
"amnet-shared": "file:../amnet-shared",
|
"amnet-shared": "file:../amnet-shared",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"expo": "^51.0.0",
|
"expo": "^52.0.0",
|
||||||
"expo-blur": "~13.0.2",
|
"expo-blur": "~14.0.1",
|
||||||
"expo-clipboard": "~6.0.3",
|
"expo-build-properties": "^0.13.1",
|
||||||
"expo-constants": "~16.0.2",
|
"expo-clipboard": "~7.0.0",
|
||||||
"expo-dev-client": "~4.0.10",
|
"expo-constants": "~17.0.3",
|
||||||
"expo-linking": "~6.3.1",
|
"expo-dev-client": "~5.0.4",
|
||||||
"expo-status-bar": "~1.12.1",
|
"expo-linking": "~7.0.3",
|
||||||
"expo-system-ui": "~3.0.7",
|
"expo-status-bar": "~2.0.0",
|
||||||
|
"expo-system-ui": "~4.0.4",
|
||||||
"lucide-react-native": "^0.427.0",
|
"lucide-react-native": "^0.427.0",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"react": "18.2.0",
|
"react": "18.3.1",
|
||||||
"react-native": "0.74.1",
|
"react-native": "0.76.5",
|
||||||
"react-native-context-menu-view": "^1.16.0",
|
"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-get-random-values": "^1.11.0",
|
||||||
"react-native-paper": "^5.12.5",
|
"react-native-paper": "^5.12.5",
|
||||||
"react-native-reanimated": "^3.15.0",
|
"react-native-reanimated": "~3.16.1",
|
||||||
"react-native-safe-area-context": "4.10.5",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "3.31.1",
|
"react-native-screens": "~4.1.0",
|
||||||
"react-native-svg": "^15.5.0",
|
"react-native-svg": "15.8.0",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
"realm": "^12.12.1",
|
"realm": "^12.14.0",
|
||||||
"typescript": "~5.3.3"
|
"typescript": "~5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+70
-70
@@ -541,9 +541,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
|
||||||
"integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==",
|
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -555,9 +555,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm64": {
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
|
||||||
"integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==",
|
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -569,9 +569,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
|
||||||
"integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==",
|
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -583,9 +583,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
|
||||||
"integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==",
|
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -597,9 +597,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
|
||||||
"integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==",
|
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -611,9 +611,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
|
||||||
"integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==",
|
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -625,9 +625,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==",
|
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -639,9 +639,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
|
||||||
"integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==",
|
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -653,9 +653,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==",
|
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -667,9 +667,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==",
|
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -681,9 +681,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==",
|
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -695,9 +695,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
|
||||||
"integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==",
|
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -709,9 +709,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
|
||||||
"integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==",
|
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -723,9 +723,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
|
||||||
"integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==",
|
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -737,9 +737,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
|
||||||
"integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==",
|
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -751,9 +751,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
|
||||||
"integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==",
|
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1423,9 +1423,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1723,9 +1723,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.21.0",
|
"version": "4.22.4",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
|
||||||
"integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==",
|
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1739,22 +1739,22 @@
|
|||||||
"npm": ">=8.0.0"
|
"npm": ">=8.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@rollup/rollup-android-arm-eabi": "4.21.0",
|
"@rollup/rollup-android-arm-eabi": "4.22.4",
|
||||||
"@rollup/rollup-android-arm64": "4.21.0",
|
"@rollup/rollup-android-arm64": "4.22.4",
|
||||||
"@rollup/rollup-darwin-arm64": "4.21.0",
|
"@rollup/rollup-darwin-arm64": "4.22.4",
|
||||||
"@rollup/rollup-darwin-x64": "4.21.0",
|
"@rollup/rollup-darwin-x64": "4.22.4",
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": "4.21.0",
|
"@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
|
||||||
"@rollup/rollup-linux-arm-musleabihf": "4.21.0",
|
"@rollup/rollup-linux-arm-musleabihf": "4.22.4",
|
||||||
"@rollup/rollup-linux-arm64-gnu": "4.21.0",
|
"@rollup/rollup-linux-arm64-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-arm64-musl": "4.21.0",
|
"@rollup/rollup-linux-arm64-musl": "4.22.4",
|
||||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.0",
|
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-riscv64-gnu": "4.21.0",
|
"@rollup/rollup-linux-riscv64-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-s390x-gnu": "4.21.0",
|
"@rollup/rollup-linux-s390x-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-x64-gnu": "4.21.0",
|
"@rollup/rollup-linux-x64-gnu": "4.22.4",
|
||||||
"@rollup/rollup-linux-x64-musl": "4.21.0",
|
"@rollup/rollup-linux-x64-musl": "4.22.4",
|
||||||
"@rollup/rollup-win32-arm64-msvc": "4.21.0",
|
"@rollup/rollup-win32-arm64-msvc": "4.22.4",
|
||||||
"@rollup/rollup-win32-ia32-msvc": "4.21.0",
|
"@rollup/rollup-win32-ia32-msvc": "4.22.4",
|
||||||
"@rollup/rollup-win32-x64-msvc": "4.21.0",
|
"@rollup/rollup-win32-x64-msvc": "4.22.4",
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+666
-661
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@
|
|||||||
"amnet-shared": "file:../amnet-shared",
|
"amnet-shared": "file:../amnet-shared",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"core-js": "^3.39.0",
|
||||||
"detectincognitojs": "^1.3.5",
|
"detectincognitojs": "^1.3.5",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
"dexie-react-hooks": "^1.1.7",
|
"dexie-react-hooks": "^1.1.7",
|
||||||
@@ -52,6 +53,6 @@
|
|||||||
"tailwindcss": "^3.4.9",
|
"tailwindcss": "^3.4.9",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^8.0.0",
|
"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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,19 +1,31 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
export function useMediaQuery(query: string) {
|
export function useMediaQuery(query: string) {
|
||||||
const [value, setValue] = React.useState(false)
|
const [value, setValue] = React.useState(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function onChange(event: MediaQueryListEvent) {
|
function onChange(event: MediaQueryListEvent) {
|
||||||
setValue(event.matches)
|
setValue(event.matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = matchMedia(query)
|
const result = matchMedia(query);
|
||||||
result.addEventListener("change", onChange)
|
|
||||||
setValue(result.matches)
|
|
||||||
|
|
||||||
return () => result.removeEventListener("change", onChange)
|
// addEventListener isn't available on older versions of safari
|
||||||
}, [query])
|
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
@@ -1,5 +1,10 @@
|
|||||||
import './index.css'
|
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 {StrictMode} from "react";
|
||||||
import {createRoot} from "react-dom/client";
|
import {createRoot} from "react-dom/client";
|
||||||
import {createHashRouter, Navigate, RouterProvider} from "react-router-dom";
|
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 CardManager from "@/pages/card-manager.tsx";
|
||||||
import ServerCardIn from "@/pages/card-in-screen.tsx";
|
import ServerCardIn from "@/pages/card-in-screen.tsx";
|
||||||
import ServerListing from "@/pages/server-listing.tsx";
|
import ServerListing from "@/pages/server-listing.tsx";
|
||||||
|
import Privacy from "@/pages/privacy.tsx";
|
||||||
|
|
||||||
// iOS PWAs show the navigation bar when changing routes
|
// iOS PWAs show the navigation bar when changing routes
|
||||||
// HashRouter doesn't have this issue as the URL never changes
|
// HashRouter doesn't have this issue as the URL never changes
|
||||||
@@ -33,7 +39,11 @@ const router = createHashRouter([
|
|||||||
path: x,
|
path: x,
|
||||||
element: <CardManager/>
|
element: <CardManager/>
|
||||||
};
|
};
|
||||||
})
|
}),
|
||||||
|
{
|
||||||
|
path: "/privacy",
|
||||||
|
element: <Privacy/>
|
||||||
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import * as animate from "tailwindcss-animate";
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
export default {
|
||||||
darkMode: ["class"],
|
darkMode: ["class"],
|
||||||
content: [
|
content: [
|
||||||
'./src/**/*.{ts,tsx}',
|
'./src/**/*.{ts,tsx}',
|
||||||
@@ -70,5 +72,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [animate],
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"DOM",
|
"DOM",
|
||||||
"DOM.Iterable"
|
"DOM.Iterable"
|
||||||
],
|
],
|
||||||
"module": "ESNext",
|
"module": "ES2020",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
target: "ES2018",
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks(id: string) {
|
manualChunks(id: string) {
|
||||||
@@ -21,6 +22,10 @@ export default defineConfig({
|
|||||||
if (id.includes('react')) {
|
if (id.includes('react')) {
|
||||||
return 'react';
|
return 'react';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (id.includes('core-js')) {
|
||||||
|
return "polyfills";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
# AMNet
|
# 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.
|
An Aime Card Switcher for segatools-based games.
|
||||||
|
|
||||||
@@ -9,42 +9,62 @@ An Aime Card Switcher for segatools-based games.
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
AMNet is designed to simulate the `DEVICE/aime.txt` file, allowing multiple users to log into a game using their phone.
|
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
|
1. Download the latest `amnet.dll` from the releases page
|
||||||
2. Move `amnet.dll` to the game directory
|
2. Move `amnet.dll` to the game directory
|
||||||
3. Edit `segatools.ini` to use the new DLL:
|
3. Edit `segatools.ini` to use the new DLL:
|
||||||
```ini
|
```ini
|
||||||
[aimeio]
|
[aimeio]
|
||||||
; To use a custom card reader IO DLL enter its path here.
|
; To use a custom card reader IO DLL enter its path here.
|
||||||
; Leave empty if you want to use Segatools built-in keyboard input.
|
; Leave empty if you want to use Segatools built-in keyboard input.
|
||||||
path=amnet.dll
|
path=amnet.dll
|
||||||
|
|
||||||
; AMNet-specific options (all optional)
|
; AMNet-specific options (all optional)
|
||||||
; gameId - the 4 character game ID (will show the game icon in the UI)
|
; 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)
|
; 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)
|
; 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).
|
; 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
|
gameId=SDEZ
|
||||||
;serverName=CHUNI-PENGUIN
|
;serverName=CHUNI-PENGUIN
|
||||||
;serverAddress=http://+:6070
|
;serverAddress=http://+:6070
|
||||||
;enableKeyboardMode=1
|
;enableKeyboardMode=1
|
||||||
```
|
```
|
||||||
4. Launch the game, and check the amdaemon console for server startup messages
|
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
|
## Interfaces
|
||||||
The web interface can be accessed by navigating to http://card.ppc.moe/ on a device on the same network as the server.
|
AMNet can be used on most platforms, see below for options available for each platform:
|
||||||
This website is also installable as a PWA, which will allow it to be registered as an app on your device.
|
|
||||||
|
|
||||||
> 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.
|
<table>
|
||||||
|
<tbody>
|
||||||
[](http://card.ppc.moe)
|
<tr>
|
||||||
|
<th>Platform</th>
|
||||||
### Native Application
|
<th>Method</th>
|
||||||
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.
|
<th>Notes</th>
|
||||||
Android is technically supported but hasn't been tested; as such APKs aren't provided at this moment.
|
</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">
|
<div align="center">
|
||||||
<img src="./res/native.png" height="600"/>
|
<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
|
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)
|
### Website (amnet-web)
|
||||||
The web interface is built using Vite and React. Building the web interface requires Node.js (or alternative runtime).
|
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.
|
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:
|
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
|
```shell
|
||||||
cd amnet-native
|
cd amnet-native
|
||||||
|
|||||||
Reference in New Issue
Block a user