mirror of
https://gitea.tendokyu.moe/ppc/amnet.git
synced 2026-09-27 09:23:03 +03:00
amnet-native initial commit
This commit is contained in:
+130
@@ -145,3 +145,133 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Expo
|
||||
.expo
|
||||
__generated__
|
||||
web-build
|
||||
bare-apps
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Ruby
|
||||
.direnv
|
||||
|
||||
# Env
|
||||
.envrc.local
|
||||
|
||||
# Emacs
|
||||
*~
|
||||
|
||||
# Vim
|
||||
.*.swp
|
||||
.*.swo
|
||||
.*.swn
|
||||
.*.swm
|
||||
|
||||
# VS Code
|
||||
.vscode/launch.json
|
||||
|
||||
# Sublime Text
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# Xcode
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.xccheckout
|
||||
*.xcscmblueprint
|
||||
xcuserdata
|
||||
|
||||
# IDEA / Android Studio
|
||||
*.iml
|
||||
.gradle
|
||||
.idea
|
||||
|
||||
# Eclipse
|
||||
.project
|
||||
.settings
|
||||
|
||||
# VSCode
|
||||
.history/
|
||||
/vscode/launch.json
|
||||
|
||||
# Android
|
||||
*.apk
|
||||
*.hprof
|
||||
ReactAndroid-temp.aar
|
||||
|
||||
# Tools
|
||||
jarjar-rules.txt
|
||||
|
||||
# Dynamic Macros
|
||||
.kernel-ngrok-url
|
||||
|
||||
# Template files
|
||||
/apps/bare-expo/android/app/google-services.json
|
||||
/apps/bare-expo/ios/BareExpo/GoogleService-Info.plist
|
||||
|
||||
# Template projects
|
||||
templates/**/android/**/generated/*
|
||||
templates/**/android/app/build
|
||||
templates/**/Pods/**
|
||||
templates/**/Podfile.lock
|
||||
templates/**/yarn.lock
|
||||
|
||||
# Codemod
|
||||
.codemod.bookmark
|
||||
|
||||
# Fastlane
|
||||
/*.cer
|
||||
/fastlane/report.xml
|
||||
/fastlane/Preview.html
|
||||
/fastlane/Deployment
|
||||
/fastlane/test_output
|
||||
/Preview.html
|
||||
/gc_keys.json
|
||||
/fastlane/gc_keys.json
|
||||
|
||||
# CI
|
||||
/android/logcat.txt
|
||||
|
||||
# Shell apps
|
||||
android-shell-app
|
||||
shellAppBase-*
|
||||
shellAppIntermediates
|
||||
shellAppWorkspaces
|
||||
/artifacts/*
|
||||
|
||||
# Expo Client builds
|
||||
/client-builds
|
||||
|
||||
# Expo web env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
apps/bare-expo/deploy-url.txt
|
||||
|
||||
# Temporary files created by Metro to check the health of the file watcher
|
||||
.metro-health-check*
|
||||
|
||||
# Expo Doc merging
|
||||
docs/pages/versions/*/react-native/ADDED_*.md
|
||||
docs/pages/versions/*/react-native/REMOVED_*.md
|
||||
docs/pages/versions/*/react-native/*.diff
|
||||
|
||||
# Expo Go
|
||||
/apps/expo-go/src/dist
|
||||
|
||||
# Prebuilds
|
||||
/packages/**/*.xcframework
|
||||
/packages/**/*.spec.json
|
||||
/packages/**/Info-generated.plist
|
||||
!crsqlite.xcframework
|
||||
|
||||
# iOS
|
||||
**/ios/.xcode.env.local
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import RealmContext from "./models/RealmContext";
|
||||
import {AMUITabBar} from "./components/AMUITabBar";
|
||||
import {Server, WalletCards} from "lucide-react-native";
|
||||
import {NavigationContainer} from "@react-navigation/native";
|
||||
import {SafeAreaProvider} from "react-native-safe-area-context";
|
||||
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
|
||||
import {createNativeStackNavigator} from "@react-navigation/native-stack";
|
||||
import {DarkTheme as NavigationDarkTheme} from '@react-navigation/native';
|
||||
import {adaptNavigationTheme, MD3DarkTheme, MD3Theme, PaperProvider} from 'react-native-paper';
|
||||
|
||||
import CardAddModal from "./app/CardAddModal";
|
||||
import ServerManager from "./app/ServerManager";
|
||||
import CardManagerStack from "./app/CardManagerStack";
|
||||
|
||||
// blend themes to be a combination of react-native-paper and shadcn/ui
|
||||
const AMDarkTheme: MD3Theme = {
|
||||
...MD3DarkTheme,
|
||||
roundness: 2,
|
||||
colors: {
|
||||
...MD3DarkTheme.colors,
|
||||
primary: '#e11d48',
|
||||
secondary: "#dc2626",
|
||||
background: "#09090b",
|
||||
surface: "#1c1917",
|
||||
},
|
||||
};
|
||||
|
||||
const {DarkTheme} = adaptNavigationTheme({
|
||||
reactNavigationDark: NavigationDarkTheme,
|
||||
materialDark: AMDarkTheme
|
||||
});
|
||||
|
||||
const {RealmProvider} = RealmContext;
|
||||
const Tab = createBottomTabNavigator();
|
||||
const Stack = createNativeStackNavigator();
|
||||
|
||||
function AppContent() {
|
||||
return (<Tab.Navigator tabBar={p => <AMUITabBar {...p}/>} screenOptions={{headerShown: false}}>
|
||||
<Tab.Screen name="servers"
|
||||
component={ServerManager}
|
||||
options={{
|
||||
title: 'Servers',
|
||||
tabBarIcon: ({color, size}) => (<Server color={color} size={size}/>)
|
||||
}}/>
|
||||
|
||||
<Tab.Screen name="cards"
|
||||
component={CardManagerStack}
|
||||
options={{
|
||||
title: "Cards",
|
||||
tabBarIcon: ({color, size}) => (<WalletCards color={color} size={size}/>)
|
||||
}}/>
|
||||
</Tab.Navigator>)
|
||||
}
|
||||
|
||||
export default function AppRoot() {
|
||||
return (
|
||||
<RealmProvider>
|
||||
<SafeAreaProvider>
|
||||
<PaperProvider theme={AMDarkTheme}>
|
||||
<NavigationContainer theme={DarkTheme}>
|
||||
<Stack.Navigator initialRouteName="app-root">
|
||||
{/* main app */}
|
||||
<Stack.Screen name="app-root" component={AppContent} options={{headerShown: false}}/>
|
||||
|
||||
<Stack.Group screenOptions={{presentation: "modal"}}>
|
||||
<Stack.Screen name="add-card" component={CardAddModal} options={{title: "Add Card"}}/>
|
||||
</Stack.Group>
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
</PaperProvider>
|
||||
</SafeAreaProvider>
|
||||
</RealmProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "AimeNet",
|
||||
"slug": "amnet-native",
|
||||
"icon": "./assets/icon.png",
|
||||
"ios": {
|
||||
"bundleIdentifier": "moe.ppc.amnet",
|
||||
"infoPlist": {
|
||||
"UIViewControllerBasedStatusBarAppearance": "YES"
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import {useCallback, useEffect, useMemo, useState} from "react";
|
||||
import {Button, Card, Divider, Paragraph, useTheme} from "react-native-paper";
|
||||
import {StyleSheet, TextInput, TouchableOpacity, View} from "react-native";
|
||||
|
||||
import RealmContext from "../models/RealmContext";
|
||||
import {generateCardNumber} from "../models/AM-API";
|
||||
import {AMCard} from "../models/AMCard";
|
||||
|
||||
const {useRealm} = RealmContext;
|
||||
|
||||
export default function CardAddModal({navigation}) {
|
||||
const theme = useTheme();
|
||||
const themedStylesheet = useMemo(() => StyleSheet.create({
|
||||
...styles,
|
||||
formItemInput: {
|
||||
flex: 1,
|
||||
color: theme.colors.onSurface
|
||||
},
|
||||
formFootnote: {
|
||||
fontSize: 13,
|
||||
},
|
||||
headerText: {
|
||||
color: theme.colors.primary
|
||||
}
|
||||
}), [theme]);
|
||||
|
||||
// state values
|
||||
const realm = useRealm();
|
||||
const [cardName, setCardName] = useState('');
|
||||
const [cardNumber, setCardNumber] = useState('');
|
||||
|
||||
const cardAddCallback = useCallback((cardId: string, cardName: string) => {
|
||||
const duplicateCard = realm.objects<AMCard>("card").find(x => x.code == cardNumber);
|
||||
|
||||
if (duplicateCard) {
|
||||
realm.write(() => {
|
||||
duplicateCard.name = cardName;
|
||||
});
|
||||
} else {
|
||||
realm.write(() => {
|
||||
realm.create("card", AMCard.generate(cardId, cardName));
|
||||
});
|
||||
}
|
||||
|
||||
navigation.navigate('card-listing');
|
||||
}, [realm]);
|
||||
|
||||
// header effects
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => <TouchableOpacity onPress={() => navigation.goBack()}>
|
||||
<Paragraph style={themedStylesheet.headerText}>Cancel</Paragraph>
|
||||
</TouchableOpacity>,
|
||||
});
|
||||
}, [navigation, themedStylesheet]);
|
||||
|
||||
useEffect(() => {
|
||||
const canSave = cardName.length && cardNumber.length === 20;
|
||||
const themeOptions = canSave ? themedStylesheet.headerText : {color: theme.colors.outline};
|
||||
|
||||
navigation.setOptions({
|
||||
headerRight: () => <TouchableOpacity disabled={!canSave} onPress={() => cardAddCallback(cardNumber, cardName)}>
|
||||
<Paragraph style={{...themeOptions, fontWeight: "600"}}>Save</Paragraph>
|
||||
</TouchableOpacity>
|
||||
})
|
||||
}, [navigation, themedStylesheet, cardName, cardNumber]);
|
||||
|
||||
return (<View style={{display: "flex", gap: 10, justifyContent: "center", paddingHorizontal: 15, paddingTop: 25}}>
|
||||
<Card>
|
||||
<Card.Content>
|
||||
<View style={themedStylesheet.formItemRow}>
|
||||
<Paragraph style={themedStylesheet.formItemName}>
|
||||
Card Name
|
||||
</Paragraph>
|
||||
<TextInput style={themedStylesheet.formItemInput}
|
||||
numberOfLines={1} autoFocus
|
||||
value={cardName}
|
||||
onChangeText={setCardName}
|
||||
placeholder="Primary Card"
|
||||
placeholderTextColor={theme.colors.outline}/>
|
||||
</View>
|
||||
|
||||
<Divider style={{marginVertical: 15}}/>
|
||||
|
||||
<View style={themedStylesheet.formItemRow}>
|
||||
<Paragraph style={themedStylesheet.formItemName}>
|
||||
Matrix Code
|
||||
</Paragraph>
|
||||
<TextInput style={themedStylesheet.formItemInput}
|
||||
numberOfLines={1}
|
||||
placeholder="0000 0000 0000 0000 0000"
|
||||
placeholderTextColor={theme.colors.outline}
|
||||
value={cardNumber.match(/.{1,4}/g)?.join(" ")}
|
||||
onChangeText={val => setCardNumber(val?.replaceAll(/\D/g, '').substring(0, 20) ?? '')}/>
|
||||
</View>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Paragraph style={themedStylesheet.formFootnote}>
|
||||
If you have a physical card, you can enter the 20-digit code on the back.
|
||||
</Paragraph>
|
||||
|
||||
<Button mode="text" onPress={() => setCardNumber(generateCardNumber())}>Generate Matrix Code</Button>
|
||||
</View>)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
formItemName: {
|
||||
minWidth: 100,
|
||||
fontWeight: "400"
|
||||
},
|
||||
formItemRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
},
|
||||
formItemDivider: {
|
||||
marginVertical: 15
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import {useCallback, useMemo} from "react";
|
||||
import {FlatList, View} from "react-native";
|
||||
|
||||
import {AMCard} from "../models/AMCard";
|
||||
import {AimeCard} from "../components/AimeCard";
|
||||
import RealmContext from "../models/RealmContext";
|
||||
import ContextMenu from "react-native-context-menu-view";
|
||||
|
||||
const {useQuery, useRealm} = RealmContext;
|
||||
|
||||
function CardEntry(props: { card: AMCard }) {
|
||||
const realm = useRealm();
|
||||
const callback = useCallback(async (index: number) => {
|
||||
switch (index) {
|
||||
// copy card no
|
||||
case 0:
|
||||
await Clipboard.setStringAsync(props.card.code);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
realm.write(() => {
|
||||
realm.delete(props.card);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}, [realm, props.card]);
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
onPress={e => callback(e.nativeEvent?.index)}
|
||||
actions={[
|
||||
{title: "Copy Card Number", systemIcon: "doc.on.doc"},
|
||||
{title: "Delete Card", destructive: true, systemIcon: "trash"}]}>
|
||||
<AimeCard card={props.card} showFullCardNumber/>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CardManager() {
|
||||
const query = useQuery(AMCard);
|
||||
const cards = useMemo(() => query.sorted("added", true), [query]);
|
||||
|
||||
return (
|
||||
<FlatList contentInsetAdjustmentBehavior="always"
|
||||
style={{padding: 16, paddingTop: 24}}
|
||||
data={cards}
|
||||
keyExtractor={i => i._id.toString()}
|
||||
contentContainerStyle={{paddingBottom: 40}}
|
||||
ItemSeparatorComponent={() => <View style={{height: 15}}/>}
|
||||
renderItem={i => <CardEntry card={i.item}/>}/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {Plus} from "lucide-react-native";
|
||||
import {useTheme} from "@react-navigation/native";
|
||||
import {createNativeStackNavigator} from "@react-navigation/native-stack";
|
||||
|
||||
import CardManager from "./CardManager";
|
||||
import {TouchableOpacity} from "react-native";
|
||||
|
||||
const Stack = createNativeStackNavigator();
|
||||
|
||||
export default function CardManagerStack({navigation}) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Stack.Navigator screenOptions={{
|
||||
headerLargeTitle: true,
|
||||
headerBlurEffect: "regular",
|
||||
headerStyle: {
|
||||
backgroundColor: theme.colors.background,
|
||||
}
|
||||
}}>
|
||||
<Stack.Screen name="card-listing"
|
||||
component={CardManager}
|
||||
options={{
|
||||
title: "Cards",
|
||||
headerRight: () => <TouchableOpacity onPress={() => navigation.navigate('add-card')}><Plus/></TouchableOpacity>
|
||||
}}/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import {Text, View} from "react-native";
|
||||
|
||||
export default function ServerManager() {
|
||||
return (
|
||||
<View>
|
||||
<Text>Server Manager</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
@@ -0,0 +1,6 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from "react";
|
||||
import {TouchableOpacity, View} from "react-native";
|
||||
import {Card, Title, useTheme} from "react-native-paper";
|
||||
|
||||
export interface AMUICardActionProps {
|
||||
actionIcon?: Readonly<React.ReactNode>,
|
||||
actionDisabled?: boolean,
|
||||
|
||||
onPress?: () => void,
|
||||
onLongPress?: () => void,
|
||||
}
|
||||
|
||||
export interface AMUICardProps {
|
||||
title: string,
|
||||
children?: Readonly<React.ReactNode>
|
||||
}
|
||||
|
||||
export function AMUICard(props: AMUICardActionProps & AMUICardProps) {
|
||||
if (props.actionDisabled == undefined && !props.onPress && !props.onLongPress) {
|
||||
return (<CardComponent {...props}/>)
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity disabled={props.actionDisabled} onPress={props.onPress} onLongPress={props.onLongPress}>
|
||||
<CardComponent {...props}/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
export function AMUICardDetail(props: { children?: Readonly<React.ReactNode> }) {
|
||||
return (
|
||||
<View style={{display: "flex", flexDirection: "row", alignItems: "center", gap: 5}}>
|
||||
{props.children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function CardComponent(props: AMUICardProps & AMUICardActionProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (<Card mode="outlined" style={{backgroundColor: theme.colors.background, borderColor: theme.colors.surface}}>
|
||||
<Card.Content style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
gap: 5
|
||||
}}>
|
||||
<View>
|
||||
<Title style={{fontWeight: "bold"}}>{props.title}</Title>
|
||||
<View style={{display: "flex", flexDirection: "row", gap: 8, flexWrap: "wrap", marginTop: 10}}>
|
||||
{props.children}
|
||||
</View>
|
||||
</View>
|
||||
{props.actionIcon && (
|
||||
<View style={{flexShrink: 0}}>
|
||||
{props.actionIcon}
|
||||
</View>
|
||||
)}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import {useTheme} from 'react-native-paper';
|
||||
import {TouchableOpacity, View} from "react-native";
|
||||
|
||||
// tab bar inspired by https://stackoverflow.com/a/64028171
|
||||
export function AMUITabBar({state, descriptors, navigation}) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
height: 60,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-around",
|
||||
backgroundColor: theme.colors.surface,
|
||||
}}>
|
||||
{state.routes.map((route, index) => {
|
||||
const {options} = descriptors[route.key];
|
||||
const isFocused = state.index === index;
|
||||
const onPress = () => {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
});
|
||||
|
||||
if (!isFocused && !event.defaultPrevented) {
|
||||
navigation.navigate(route.name);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={route.key}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={options.tabBarAccessibilityLabel}
|
||||
testID={options.tabBarTestID}
|
||||
onPress={onPress}>
|
||||
{options.tabBarIcon({color: isFocused ? theme.colors.primary : theme.colors.outline, size: 24})}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {useMemo} from "react";
|
||||
import {Paragraph} from "react-native-paper";
|
||||
import {CreditCard, History} from "lucide-react-native";
|
||||
|
||||
import TimeAgo from "./TimeAgo";
|
||||
import {AMCardData} from "../models/AMCard";
|
||||
import {AMUICard, AMUICardActionProps, AMUICardDetail} from "./AMUICard";
|
||||
|
||||
export interface AimeCardProps extends AMUICardActionProps {
|
||||
card: AMCardData,
|
||||
showFullCardNumber?: boolean
|
||||
}
|
||||
|
||||
export function AimeCard(props: AimeCardProps) {
|
||||
const displayedCardNumber = useMemo(() => props.showFullCardNumber
|
||||
? props.card.code.match(/.{1,4}/g)?.join(" ")
|
||||
: `•••• ${props.card.code.substring(props.card.code.length - 4)}`,
|
||||
[props.showFullCardNumber, props.card.code]);
|
||||
|
||||
return (
|
||||
<AMUICard title={props.card.name} {...props}>
|
||||
<AMUICardDetail>
|
||||
<CreditCard size={18} color="gray"/>
|
||||
<Paragraph style={{color: "gray"}}>{displayedCardNumber}</Paragraph>
|
||||
</AMUICardDetail>
|
||||
{props.card.lastUsed && (
|
||||
<AMUICardDetail>
|
||||
<History size={18} color="gray"/>
|
||||
<TimeAgo style={{color: "gray"}} dateTo={props.card.lastUsed}/>
|
||||
</AMUICardDetail>
|
||||
)}
|
||||
</AMUICard>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {StyleProp, TextStyle} from 'react-native';
|
||||
import {formatDistance} from 'date-fns/formatDistance';
|
||||
import {Paragraph} from "react-native-paper";
|
||||
|
||||
interface Props {
|
||||
dateTo: Date;
|
||||
updateInterval?: number;
|
||||
dateFrom?: Date;
|
||||
hideAgo?: boolean;
|
||||
style?: StyleProp<TextStyle>;
|
||||
}
|
||||
|
||||
const TimeAgo: React.FC<Props> = ({
|
||||
dateTo,
|
||||
dateFrom,
|
||||
updateInterval = 60000,
|
||||
hideAgo = false,
|
||||
style,
|
||||
}) => {
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (!dateFrom) setCurrentDate(new Date());
|
||||
}, updateInterval);
|
||||
return () => clearInterval(interval);
|
||||
}, [dateFrom, updateInterval]);
|
||||
|
||||
return (
|
||||
<Paragraph {...{style}}>
|
||||
{formatDistance(dateTo, dateFrom || currentDate, {
|
||||
addSuffix: !hideAgo,
|
||||
})}
|
||||
</Paragraph>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeAgo;
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 0.42.4"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'expo-dev-client';
|
||||
import 'react-native-get-random-values';
|
||||
|
||||
import {registerRootComponent} from "expo";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||
// It also ensures that whether you load the app in the Expo client or in a native build,
|
||||
// the environment is set up appropriately
|
||||
registerRootComponent(App);
|
||||
@@ -0,0 +1,4 @@
|
||||
// Learn more https://docs.expo.dev/guides/customizing-metro
|
||||
const { getDefaultConfig } = require('expo/metro-config');
|
||||
|
||||
module.exports = getDefaultConfig(__dirname);
|
||||
@@ -0,0 +1,28 @@
|
||||
export interface AMServerInfoResponse {
|
||||
server_name: string;
|
||||
api_version: number;
|
||||
}
|
||||
|
||||
export interface AMCardInRequest {
|
||||
card_id: string;
|
||||
}
|
||||
|
||||
export function generateCardNumber(): string {
|
||||
let cardId = '';
|
||||
|
||||
while (true) {
|
||||
// don't start with a 3
|
||||
const firstDigit = Math.floor(Math.random() * 10);
|
||||
|
||||
if (firstDigit !== 3) {
|
||||
cardId = firstDigit.toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 19; i++) {
|
||||
cardId += Math.floor(Math.random() * 10).toString();
|
||||
}
|
||||
|
||||
return cardId;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {Realm} from "realm";
|
||||
|
||||
export interface AMCardData {
|
||||
code: string;
|
||||
name: string;
|
||||
|
||||
added: Date;
|
||||
lastUsed?: Date | null;
|
||||
}
|
||||
|
||||
export class AMCard extends Realm.Object<AMCard> implements AMCardData {
|
||||
_id!: Realm.BSON.ObjectId;
|
||||
|
||||
code: string;
|
||||
name: string;
|
||||
|
||||
added: Date;
|
||||
lastUsed?: Date | null;
|
||||
|
||||
static generate(code: string, name: string) {
|
||||
return {
|
||||
_id: new Realm.BSON.ObjectId(),
|
||||
code: code,
|
||||
name: name,
|
||||
added: new Date()
|
||||
};
|
||||
}
|
||||
|
||||
static schema: Realm.ObjectSchema = {
|
||||
name: "card",
|
||||
primaryKey: "_id",
|
||||
|
||||
properties: {
|
||||
_id: "objectId",
|
||||
code: "string",
|
||||
name: {type: "string", default: "New Card"},
|
||||
added: {type: "date", default: new Date()},
|
||||
lastUsed: {type: "date", optional: true}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import {Realm} from 'realm';
|
||||
import {AMServerInfoResponse} from "./AM-API";
|
||||
|
||||
export class AMServer extends Realm.Object<AMServer> implements AMServerInfoResponse {
|
||||
_id!: Realm.BSON.ObjectId;
|
||||
|
||||
server_name: string;
|
||||
server_address: string;
|
||||
|
||||
last_used: Date;
|
||||
created_at: Date;
|
||||
api_version: number;
|
||||
|
||||
async connect(abort?: AbortSignal | undefined): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(this.server_address, {
|
||||
signal: abort
|
||||
});
|
||||
|
||||
const data: AMServerInfoResponse = await response.json();
|
||||
|
||||
this.last_used = new Date();
|
||||
Object.apply(this, data);
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async submitCard(cardId: string, abort?: AbortSignal | undefined): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.server_address}/card`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
card_id: cardId
|
||||
}),
|
||||
signal: abort
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static schema: Realm.ObjectSchema = {
|
||||
name: "server",
|
||||
primaryKey: "_id",
|
||||
|
||||
properties: {
|
||||
_id: "objectId",
|
||||
server_address: "string",
|
||||
server_name: {type: "string", default: "New Server"},
|
||||
last_used: {type: "date", optional: true},
|
||||
created_at: {type: "date", default: new Date()},
|
||||
api_version: {type: "int", default: 0}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import {createRealmContext} from "@realm/react";
|
||||
import {AMCard} from "./AMCard";
|
||||
import {AMServer} from "./AMServer";
|
||||
|
||||
// @ts-ignore
|
||||
export default createRealmContext({
|
||||
inMemory: true,
|
||||
schemaVersion: 1,
|
||||
schema: [AMCard, AMServer],
|
||||
deleteRealmIfMigrationNeeded: true
|
||||
});
|
||||
Generated
+14424
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "amnet-native",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "expo start --dev-client",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"date-fns": "^3.6.0",
|
||||
"expo": "^51.0.0",
|
||||
"expo-blur": "~13.0.2",
|
||||
"expo-constants": "~16.0.2",
|
||||
"expo-dev-client": "~4.0.10",
|
||||
"expo-linking": "~6.3.1",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"lucide-react-native": "^0.427.0",
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.74.1",
|
||||
"react-native-context-menu-view": "^1.16.0",
|
||||
"react-native-gesture-handler": "^2.18.1",
|
||||
"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",
|
||||
"realm": "^12.12.1",
|
||||
"typescript": "~5.3.3",
|
||||
"expo-clipboard": "~6.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"compilerOptions": {},
|
||||
"extends": "expo/tsconfig.base"
|
||||
}
|
||||
Reference in New Issue
Block a user