mirror of https://github.com/hykilpikonna/AquaDX
[+] Show card conflicts
parent
a620f02d57
commit
3e8395b0c6
|
@ -1,20 +1,98 @@
|
||||||
<!-- Svelte 4.2.11 -->
|
<!-- Svelte 4.2.11 -->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { slide } from "svelte/transition"
|
import { slide, fade } from "svelte/transition"
|
||||||
import { clz } from "../libs/ui";
|
import { clz } from "../libs/ui";
|
||||||
import type { UserMe } from "../libs/generalTypes";
|
import type { CardSummary, CardSummaryGame, UserMe } from "../libs/generalTypes";
|
||||||
import { USER } from "../libs/sdk";
|
import { CARD, USER } from "../libs/sdk";
|
||||||
import moment from "moment"
|
import moment from "moment"
|
||||||
|
|
||||||
|
// State
|
||||||
|
let state = "ready"
|
||||||
|
|
||||||
let error: string = ""
|
let error: string = ""
|
||||||
let me: UserMe | null = null
|
let me: UserMe | null = null
|
||||||
|
let accountCardSummary: CardSummary | null = null
|
||||||
|
|
||||||
// Fetch data for current user
|
// Fetch data for current user
|
||||||
USER.me().then(m => {
|
const updateMe = () => USER.me().then(m => {
|
||||||
me = m
|
me = m
|
||||||
m.cards.sort((a, b) => a.registerTime < b.registerTime ? 1 : -1)
|
m.cards.sort((a, b) => a.registerTime < b.registerTime ? 1 : -1)
|
||||||
|
CARD.summary(m.ghostCard.luid).then(s => accountCardSummary = s.summary)
|
||||||
}).catch(e => error = e.message)
|
}).catch(e => error = e.message)
|
||||||
|
updateMe()
|
||||||
|
|
||||||
|
// Data conflict overlay
|
||||||
|
let conflictCardID: string = ""
|
||||||
|
let conflictSummary: CardSummary | null = null
|
||||||
|
let conflictGame: string = ""
|
||||||
|
let conflictNew: CardSummaryGame | null = null
|
||||||
|
let conflictOld: CardSummaryGame | null = null
|
||||||
|
let conflictToMigrate: string[] = []
|
||||||
|
|
||||||
|
async function link(type: 'AC' | 'SN') {
|
||||||
|
if (state !== 'ready') return
|
||||||
|
state = "linking-AC"
|
||||||
|
const id = type === 'AC' ? inputAC : inputSN
|
||||||
|
|
||||||
|
console.log("linking card", id)
|
||||||
|
|
||||||
|
// First, lookup the card summary
|
||||||
|
const summary = (await CARD.summary(id)).summary
|
||||||
|
|
||||||
|
// If all games in summary are null or doesn't conflict with the ghost card,
|
||||||
|
// we can link the card directly
|
||||||
|
// @ts-ignore - TS doesn't understand that k is a key of CardSummary, so it says k cannot be used as index
|
||||||
|
if (Object.keys(summary).every(k => summary[k] === null || accountCardSummary[k] === null)) {
|
||||||
|
console.log("linking card directly")
|
||||||
|
// @ts-ignore
|
||||||
|
await CARD.link({cardId: id, migrate: Object.keys(summary).filter(k => summary[k] !== null).join(",")})
|
||||||
|
|
||||||
|
// Refresh the user data
|
||||||
|
await updateMe()
|
||||||
|
|
||||||
|
state = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each conflicting game, ask the user if they want to migrate the data
|
||||||
|
else {
|
||||||
|
conflictSummary = summary
|
||||||
|
conflictCardID = id
|
||||||
|
await linkConflictContinue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function linkConflictContinue() {
|
||||||
|
console.log("linking card with migration")
|
||||||
|
|
||||||
|
let isConflict = false
|
||||||
|
for (const k in conflictSummary) {
|
||||||
|
// @ts-ignore
|
||||||
|
conflictNew = conflictSummary[k]
|
||||||
|
// @ts-ignore
|
||||||
|
conflictOld = accountCardSummary[k]
|
||||||
|
conflictGame = k
|
||||||
|
if (!conflictNew || !conflictOld) continue
|
||||||
|
|
||||||
|
isConflict = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no longer conflicts, we can link the card
|
||||||
|
if (!isConflict) {
|
||||||
|
await CARD.link({cardId: conflictCardID, migrate: conflictToMigrate.join(",")})
|
||||||
|
await updateMe()
|
||||||
|
state = ""
|
||||||
|
|
||||||
|
// Reset conflict data
|
||||||
|
conflictSummary = null
|
||||||
|
conflictCardID = ""
|
||||||
|
conflictGame = ""
|
||||||
|
conflictNew = null
|
||||||
|
conflictOld = null
|
||||||
|
conflictToMigrate = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Access code input
|
// Access code input
|
||||||
const inputACRegex = /^(\d{4} ){0,4}\d{0,4}$/
|
const inputACRegex = /^(\d{4} ){0,4}\d{0,4}$/
|
||||||
|
@ -29,13 +107,6 @@
|
||||||
inputAC = inputAC.slice(0, 24)
|
inputAC = inputAC.slice(0, 24)
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindAC() {
|
|
||||||
if (inputACRegexFull.test(inputAC)) {
|
|
||||||
console.log("Binding access code", inputAC)
|
|
||||||
inputAC = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serial number input
|
// Serial number input
|
||||||
const inputSNRegex = /^([0-9A-Fa-f]{0,2}:){0,7}[0-9A-Fa-f]{0,2}$/
|
const inputSNRegex = /^([0-9A-Fa-f]{0,2}:){0,7}[0-9A-Fa-f]{0,2}$/
|
||||||
const inputSNRegexFull = /^([0-9A-Fa-f]{2}:){4,7}[0-9A-Fa-f]{2}$/
|
const inputSNRegexFull = /^([0-9A-Fa-f]{2}:){4,7}[0-9A-Fa-f]{2}$/
|
||||||
|
@ -48,13 +119,6 @@
|
||||||
inputSN += ":"
|
inputSN += ":"
|
||||||
inputSN = inputSN.toUpperCase().slice(0, 23)
|
inputSN = inputSN.toUpperCase().slice(0, 23)
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindSN() {
|
|
||||||
if (inputSNRegexFull.test(inputSN)) {
|
|
||||||
console.log("Binding serial number", inputSN)
|
|
||||||
inputSN = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatLUID(luid: string) {
|
function formatLUID(luid: string) {
|
||||||
switch (cardType(luid)) {
|
switch (cardType(luid)) {
|
||||||
|
@ -73,18 +137,24 @@
|
||||||
if (luid.startsWith("00")) return "Felica SN"
|
if (luid.startsWith("00")) return "Felica SN"
|
||||||
if (luid.length === 20) return "Access Code"
|
if (luid.length === 20) return "Access Code"
|
||||||
if (luid.length === 18) return "Account Card"
|
if (luid.length === 18) return "Account Card"
|
||||||
|
if (luid.includes(":")) return "Felica SN"
|
||||||
|
if (luid.includes(" ")) return "Access Code"
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isInput(e: KeyboardEvent) {
|
||||||
|
return e.key.length === 1 && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="bind-card">
|
<div class="link-card">
|
||||||
<h2>Your Cards</h2>
|
<h2>Your Cards</h2>
|
||||||
<p>Here are the cards you have linked to your account:</p>
|
<p>Here are the cards you have linked to your account:</p>
|
||||||
|
|
||||||
{#if me}
|
{#if me}
|
||||||
<div class="existing-cards">
|
<div class="existing-cards" transition:slide>
|
||||||
{#each me.cards as card}
|
{#each me.cards as card}
|
||||||
<div class={clz({ghost: cardType(card.luid) === 'Account Card'}, 'existing-card')}>
|
<div class={clz({ghost: cardType(card.luid) === 'Account Card'}, 'existing card')}>
|
||||||
<span class="type">{cardType(card.luid)}</span>
|
<span class="type">{cardType(card.luid)}</span>
|
||||||
<span class="register">Registered: {moment(card.registerTime).format("YYYY MMM DD")}</span>
|
<span class="register">Registered: {moment(card.registerTime).format("YYYY MMM DD")}</span>
|
||||||
<span class="last">Last used: {moment(card.accessTime).format("YYYY MMM DD")}</span>
|
<span class="last">Last used: {moment(card.accessTime).format("YYYY MMM DD")}</span>
|
||||||
|
@ -94,9 +164,9 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else if error}
|
{:else if error}
|
||||||
<p>{error}</p>
|
<span class="error">{error}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<p>Loading...</p>
|
<span>Loading...</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h2>Link Card</h2>
|
<h2>Link Card</h2>
|
||||||
|
@ -106,15 +176,15 @@
|
||||||
<!-- DO NOT change the order of bind:value and on:input. Their order determines the order of reactivity -->
|
<!-- DO NOT change the order of bind:value and on:input. Their order determines the order of reactivity -->
|
||||||
<input placeholder="e.g. 5200 1234 5678 9012 3456"
|
<input placeholder="e.g. 5200 1234 5678 9012 3456"
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
e.key === "Enter" && bindAC()
|
e.key === "Enter" && link('AC')
|
||||||
// Ensure key is numeric
|
// Ensure key is numeric
|
||||||
if (e.key.length === 1 && !/[\d ]/.test(e.key)) e.preventDefault()
|
if (isInput(e) && !/[\d ]/.test(e.key)) e.preventDefault()
|
||||||
}}
|
}}
|
||||||
bind:value={inputAC}
|
bind:value={inputAC}
|
||||||
on:input={inputACChange}
|
on:input={inputACChange}
|
||||||
class={clz({error: (inputAC && !inputACRegex.test(inputAC))})}>
|
class={clz({error: (inputAC && !inputACRegex.test(inputAC))})}>
|
||||||
{#if inputAC.length > 0}
|
{#if inputAC.length > 0}
|
||||||
<button transition:slide={{axis: 'x'}} on:click={bindAC}>Bind</button>
|
<button transition:slide={{axis: 'x'}} on:click={() => link('AC')}>Link</button>
|
||||||
{/if}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
<p>2. Download the NFC Tools app on your phone
|
<p>2. Download the NFC Tools app on your phone
|
||||||
|
@ -125,23 +195,49 @@
|
||||||
<label>
|
<label>
|
||||||
<input placeholder="e.g. 01:2E:1A:2B:3C:4D:5E:6F"
|
<input placeholder="e.g. 01:2E:1A:2B:3C:4D:5E:6F"
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
e.key === "Enter" && bindSN()
|
e.key === "Enter" && link('SN')
|
||||||
// Ensure key is hex or colon
|
// Ensure key is hex or colon
|
||||||
if (e.key.length === 1 && !/[0-9A-Fa-f:]/.test(e.key)) e.preventDefault()
|
if (isInput(e) && !/[0-9A-Fa-f:]/.test(e.key)) e.preventDefault()
|
||||||
}}
|
}}
|
||||||
bind:value={inputSN}
|
bind:value={inputSN}
|
||||||
on:input={inputSNChange}
|
on:input={inputSNChange}
|
||||||
class={clz({error: (inputSN && !inputSNRegex.test(inputSN))})}>
|
class={clz({error: (inputSN && !inputSNRegex.test(inputSN))})}>
|
||||||
{#if inputSN.length > 0}
|
{#if inputSN.length > 0}
|
||||||
<button transition:slide={{axis: 'x'}} on:click={bindSN}>Bind</button>
|
<button transition:slide={{axis: 'x'}} on:click={() => link('SN')}>Link</button>
|
||||||
{/if}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{#if conflictOld && conflictNew && me}
|
||||||
|
<div class="overlay" transition:fade>
|
||||||
|
<div>
|
||||||
|
<h2>Data Conflict</h2>
|
||||||
|
<p>The card contains data for {conflictGame}, which is already present on your account.
|
||||||
|
Please choose the data you would like to keep</p>
|
||||||
|
<div class="conflict-cards">
|
||||||
|
<div class="old card clickable">
|
||||||
|
<span class="type">Account Card</span>
|
||||||
|
<span>Name: {conflictOld.name}</span>
|
||||||
|
<span>Rating: {conflictOld.rating}</span>
|
||||||
|
<span>Last Login: {moment(conflictOld.lastLogin).format("YYYY MMM DD")}</span>
|
||||||
|
<span class="id">{formatLUID(me.ghostCard.luid)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="new card clickable">
|
||||||
|
<span class="type">{cardType(conflictCardID)}</span>
|
||||||
|
<span>Name: {conflictNew.name}</span>
|
||||||
|
<span>Rating: {conflictNew.rating}</span>
|
||||||
|
<span>Last Login: {moment(conflictNew.lastLogin).format("YYYY MMM DD")}</span>
|
||||||
|
<span class="id">{conflictCardID}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="sass">
|
<style lang="sass">
|
||||||
@import "../vars"
|
@import "../vars"
|
||||||
|
|
||||||
.bind-card
|
.link-card
|
||||||
input
|
input
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
|
@ -151,20 +247,14 @@
|
||||||
button
|
button
|
||||||
margin-left: 1rem
|
margin-left: 1rem
|
||||||
|
|
||||||
.existing-cards
|
.existing-cards, .conflict-cards
|
||||||
display: grid
|
display: grid
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr))
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr))
|
||||||
gap: 1rem
|
gap: 1rem
|
||||||
|
|
||||||
.existing-card
|
.existing.card
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
min-height: 90px
|
min-height: 90px
|
||||||
|
|
||||||
border-radius: 5px
|
|
||||||
padding: 12px 16px
|
|
||||||
background: $ov-light
|
|
||||||
|
|
||||||
&.ghost
|
&.ghost
|
||||||
background: rgba($c-darker, 0.8)
|
background: rgba($c-darker, 0.8)
|
||||||
|
|
||||||
|
@ -177,5 +267,17 @@
|
||||||
> div
|
> div
|
||||||
flex: 1
|
flex: 1
|
||||||
|
|
||||||
|
.conflict-cards
|
||||||
|
.card
|
||||||
|
transition: background 0.2s
|
||||||
|
|
||||||
|
.card:hover
|
||||||
|
background: $c-darker
|
||||||
|
|
||||||
|
span:not(.type)
|
||||||
|
font-size: 0.8rem
|
||||||
|
|
||||||
|
.id
|
||||||
|
opacity: 0.7
|
||||||
|
|
||||||
</style>
|
</style>
|
|
@ -94,6 +94,6 @@ export const USER = {
|
||||||
export const CARD = {
|
export const CARD = {
|
||||||
summary: (cardId: string): Promise<{card: Card, summary: CardSummary}> =>
|
summary: (cardId: string): Promise<{card: Card, summary: CardSummary}> =>
|
||||||
post('/api/v2/card/summary', { cardId }),
|
post('/api/v2/card/summary', { cardId }),
|
||||||
bind: (props: { cardId: string, migrate: string }) =>
|
link: (props: { cardId: string, migrate: string }) =>
|
||||||
post('/api/v2/card/bind', props),
|
post('/api/v2/card/bind', props),
|
||||||
}
|
}
|
Loading…
Reference in New Issue