From 1b73b60efad3f707e226d0e5d85e15e611e5ddd4 Mon Sep 17 00:00:00 2001 From: ppc Date: Sat, 29 Mar 2025 10:02:56 +0000 Subject: [PATCH] move "most recent card used" tracking to separate table --- amnet-native/app/ServerCardIn.tsx | 28 ++++++++++------- amnet-native/components/ICCard.tsx | 4 +-- amnet-native/models/AMCard.ts | 27 +++++++--------- amnet-native/models/AMCardData.ts | 11 +++++++ amnet-native/models/AMCardRecentUse.ts | 37 ++++++++++++++++++++++ amnet-native/models/RealmContext.ts | 43 +++++++++++++++++++++++--- 6 files changed, 117 insertions(+), 33 deletions(-) create mode 100644 amnet-native/models/AMCardData.ts create mode 100644 amnet-native/models/AMCardRecentUse.ts diff --git a/amnet-native/app/ServerCardIn.tsx b/amnet-native/app/ServerCardIn.tsx index 3305fd9..c7c89e9 100644 --- a/amnet-native/app/ServerCardIn.tsx +++ b/amnet-native/app/ServerCardIn.tsx @@ -28,6 +28,7 @@ import {AMCard} from "../models/AMCard"; import {felicaCardReadAsync, icCard0008Number, ICType} from "../lib/iccard"; import { + AMCardInRequest, AMServerConnectionInfo, AMServerInstanceMetrics, fetchServerInfo, @@ -36,6 +37,7 @@ import { } from "amnet-shared"; import ObjectId = BSON.ObjectId; +import {AMCardRecentUse} from "../models/AMCardRecentUse"; const {useRealm, useQuery, useObject} = RealmContext; @@ -69,13 +71,9 @@ export default function ServerCardIn({route, navigation}) { const [cardInCompleted, setCardInCompleted] = useState(null); const disableCardIn = useMemo(() => cardInCompleted !== null || !serverConnectionSuccess, [cardInCompleted, serverConnectionSuccess]); - const serverDefaultCard = useMemo(() => { - if (server?.isValid() !== true || !server.gameId) { - return null; - } - return cards.find(x => x.lastUsedGameId === server.gameId); - }, [cards, server?.gameId]); + const serverDefaultCardQuery = useObject(AMCardRecentUse, server.gameId); + const serverDefaultCard = serverDefaultCardQuery?.card ?? null; 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]); @@ -154,17 +152,25 @@ export default function ServerCardIn({route, navigation}) { setCardInCompleted(false); const abortController = new AbortController(); - - performCardIn(server, { + const request: AMCardInRequest = { cardId: card.code, physicalCardIDm: card.physicalCardData?.cardIDm ?? undefined - }, abortController.signal).then(r => { + }; + + performCardIn(server, request, abortController.signal).then(r => { const now = new Date(); realm.write(() => { server.lastUsed = now; - card.lastUsed = now; - card.lastUsedGameId = server.gameId; + + // replace the "most recent card" record + if (server.gameId) { + if (serverDefaultCardQuery) { + realm.delete(serverDefaultCardQuery); + } + + realm.create(AMCardRecentUse.schema.name, AMCardRecentUse.generate(server.gameId, card)); + } }); setCardInCompleted(r); diff --git a/amnet-native/components/ICCard.tsx b/amnet-native/components/ICCard.tsx index d8507f8..08af301 100644 --- a/amnet-native/components/ICCard.tsx +++ b/amnet-native/components/ICCard.tsx @@ -5,8 +5,8 @@ import {CreditCard, Gem, History, Joystick, LucideIcon, RailSymbol} from "lucide import TimeAgo from "./TimeAgo"; import {AMUICard, AMUICardActionProps, AMUICardDetail} from "./AMUICard"; -import {AMCardData} from "../models/AMCard"; import {AmusementICVendor, ICType} from "../lib/iccard"; +import {AMCardData} from "../models/AMCardData"; export interface ICCardProps extends AMUICardActionProps { card: AMCardData, @@ -68,7 +68,7 @@ function PhysicalICInfo(props: { card: AMCardData | null }) { function ICVendorName(vendor: AmusementICVendor): string { switch (vendor) { case AmusementICVendor.Sega: - return "AiMe Card"; + return "Aime Card"; case AmusementICVendor.Konami: return "e-Amusement Pass"; diff --git a/amnet-native/models/AMCard.ts b/amnet-native/models/AMCard.ts index f0e074f..51e7bcf 100644 --- a/amnet-native/models/AMCard.ts +++ b/amnet-native/models/AMCard.ts @@ -1,15 +1,7 @@ import {Realm} from "realm"; import {AmusementICVendor, ICType} from "../lib/iccard"; - -export interface AMCardData { - code: string; - name: string; - - added: Date; - lastUsed?: Date | null; - isLimitedAimeCard: boolean; - physicalCardData?: AMPhysicalCardData | null; -} +import {AMCardRecentUse} from "./AMCardRecentUse"; +import {AMCardData} from "./AMCardData"; export class AMCard extends Realm.Object implements AMCardData { _id!: Realm.BSON.ObjectId; @@ -18,9 +10,9 @@ export class AMCard extends Realm.Object implements AMCardData { name: string; added: Date; - lastUsed?: Date; - lastUsedGameId?: string | null; + lastUsed: Date; + recentUses: Realm.Results; physicalCardData?: AMPhysicalCardData | null = null; get isLimitedAimeCard() { @@ -43,6 +35,8 @@ export class AMCard extends Realm.Object implements AMCardData { realm.delete(card.physicalCardData); } + // remove any usage history tied to the card + realm.delete(card.recentUses); realm.delete(card); }); } @@ -54,15 +48,18 @@ export class AMCard extends Realm.Object implements AMCardData { properties: { _id: "objectId", code: "string", - name: {type: "string", default: "New Card"}, - added: {type: "date", default: new Date()}, + name: {type: "string", default: "New Card", indexed: true}, + added: {type: "date", default: new Date(), indexed: true}, lastUsed: {type: "date", optional: true, indexed: true}, - lastUsedGameId: {type: "string", optional: true, indexed: true}, + usageHistory: {type: "linkingObjects", objectType: "cardUsageHistory", property: "_id"}, physicalCardData: {type: "object", objectType: "physicalCard", optional: true} } }; } +/** + * Holds physical card information when a card is imported via NFC/FeliCa + */ export class AMPhysicalCardData extends Realm.Object { cardIDm: string; diff --git a/amnet-native/models/AMCardData.ts b/amnet-native/models/AMCardData.ts new file mode 100644 index 0000000..32f5f4d --- /dev/null +++ b/amnet-native/models/AMCardData.ts @@ -0,0 +1,11 @@ +import {AMPhysicalCardData} from "./AMCard"; + +export interface AMCardData { + code: string; + name: string; + + added: Date; + lastUsed?: Date | null; + isLimitedAimeCard: boolean; + physicalCardData?: AMPhysicalCardData | null; +} \ No newline at end of file diff --git a/amnet-native/models/AMCardRecentUse.ts b/amnet-native/models/AMCardRecentUse.ts new file mode 100644 index 0000000..9b21aa0 --- /dev/null +++ b/amnet-native/models/AMCardRecentUse.ts @@ -0,0 +1,37 @@ +import {Realm} from "realm"; +import {AMCard} from "./AMCard"; + +/** + * Represents a usage of a card for determining which card was last used on a specific game. + */ +export class AMCardRecentUse extends Realm.Object { + + /** + * The game id that is being tracked + */ + gameId: string; + + /** + * The card that was last used on the game. + * If the card is removed, this record will also be removed. + */ + card: AMCard; + + static generate(gameId: string, card: AMCard) { + return { + gameId: gameId, + card: card + }; + } + + static schema: Realm.ObjectSchema = { + name: "cardUsageHistory", + primaryKey: "gameId", + + properties: { + gameId: "string", + card: {type: "object", objectType: "card", optional: false} + } + } + +} \ No newline at end of file diff --git a/amnet-native/models/RealmContext.ts b/amnet-native/models/RealmContext.ts index 6ecd846..1ba0fdc 100644 --- a/amnet-native/models/RealmContext.ts +++ b/amnet-native/models/RealmContext.ts @@ -1,10 +1,43 @@ +import {Realm} from "realm"; import {createRealmContext} from "@realm/react"; -import {AMCard, AMPhysicalCardData} from "./AMCard"; + import {AMServer} from "./AMServer"; +import {AMCardRecentUse} from "./AMCardRecentUse"; +import {AMCard, AMPhysicalCardData} from "./AMCard"; + +const migrationFunction = (oldRealm: Realm, newRealm: Realm) => { + if (oldRealm.schemaVersion < 5) { + const oldCards = oldRealm.objects("card"); + const gameIdMap: Map = new Map(); + + // get last used card for each game + oldCards.forEach((card: any) => { + if (card.lastUsedGameId && card.lastUsed) { + const existing = gameIdMap.get(card.lastUsedGameId); + if (!existing || card.lastUsed > existing.lastUsed) { + gameIdMap.set(card.lastUsedGameId, { card, lastUsed: card.lastUsed }); + } + } + }); + + // write new history records + const newCards = newRealm.objects("card"); + newRealm.write(() => { + gameIdMap.forEach((value, gameId) => { + const newCard = newCards.filtered(`_id == $0`, value.card._id)[0]; + newRealm.create("cardUsageHistory", { + gameId: gameId, + card: newCard + }); + }); + }); + } +}; // @ts-ignore export default createRealmContext({ - schemaVersion: 4, - schema: [AMPhysicalCardData, AMCard, AMServer], - deleteRealmIfMigrationNeeded: false -}); \ No newline at end of file + schemaVersion: 5, + schema: [AMPhysicalCardData, AMCardRecentUse, AMCard, AMServer], + onMigration: migrationFunction, + deleteRealmIfMigrationNeeded: false, +});