[O] LinkCard UX improvements and i18n fixes

This commit is contained in:
Menci
2025-01-28 04:54:10 +08:00
parent 99e1d130f0
commit db7be134c7
6 changed files with 1547 additions and 1204 deletions

View File

@@ -2,12 +2,12 @@
<script lang="ts">
import { fade, slide } from "svelte/transition"
import type { Card, CardSummary, CardSummaryGame, ConfirmProps, AquaNetUser } from "../../libs/generalTypes";
import { CARD, USER } from "../../libs/sdk";
import type { Card, CardSummary, CardSummaryGame, ConfirmProps, AquaNetUser } from "../../libs/generalTypes"
import { CARD, USER } from "../../libs/sdk"
import moment from "moment"
import Icon from "@iconify/svelte";
import StatusOverlays from "../../components/StatusOverlays.svelte";
import { t } from "../../libs/i18n";
import Icon from "@iconify/svelte"
import StatusOverlays from "../../components/StatusOverlays.svelte"
import { t } from "../../libs/i18n"
// State
let state: 'ready' | 'linking-AC' | 'linking-SN' | 'loading' = "loading"
@@ -42,14 +42,22 @@
}
async function doLink(id: string, migrate: string) {
await CARD.link({cardId: id, migrate})
await updateMe()
try {
await CARD.link({cardId: id, migrate})
await updateMe()
if (linkingType === 'AC') inputAC = ""
if (linkingType === 'SN') inputSN = ""
} catch (e) {
setError(e.message, linkingType)
}
state = "ready"
}
let linkingType: 'AC' | 'SN' = null
async function link(type: 'AC' | 'SN') {
if (state !== 'ready' || accountCardSummary === null) return
state = "linking-" + type
linkingType = type
const id = type === 'AC' ? inputAC : inputSN
console.log("linking card", id)
@@ -64,7 +72,7 @@
// First, lookup the card summary
const card = (await CARD.summary(id).catch(e => {
// If card is not found, create a card and link it
if (e.message === t('home.linkcard.notfound')) {
if (e.message === 'Card not found') {
doLink(id, "")
return
}
@@ -156,28 +164,60 @@
}
}
function cursorPositionToCursorIndex(text: string, cursorPosition: number, effectiveCharsRegex: RegExp) {
const textBeforeCursor = text.slice(0, cursorPosition)
const ignoredChars = textBeforeCursor.replace(new RegExp(effectiveCharsRegex, "g"), "")
return textBeforeCursor.length - ignoredChars.length
}
function cursorIndexToCursorPosition(text: string, cursorIndex: number, effectiveCharsRegex: RegExp) {
let i = 0
while (i < text.length) {
while (i < text.length && !effectiveCharsRegex.test(text[i])) i++
if (cursorIndex === 0) break
cursorIndex--
i++
}
return i
}
// Access code input
const inputACRegex = /^(\d{4} ){0,4}\d{0,4}$/
let elemInputAC: HTMLInputElement
let inputOldAC = ""
let inputAC = ""
let errorAC = ""
let warningAC = ""
function inputACChange() {
// Add spaces to the input
const old = inputAC
const cursorIndex = cursorPositionToCursorIndex(inputAC, elemInputAC.selectionStart, /\d/)
inputAC = inputAC.replace(/\D/g, '').replace(/(.{4})/g, '$1 ').replace(/ $/, '')
if (inputAC !== old) errorAC = ""
const cursorPosition = cursorIndexToCursorPosition(inputAC, cursorIndex, /\d/)
setTimeout(() => elemInputAC.selectionStart = elemInputAC.selectionEnd = cursorPosition, 0)
if (inputAC !== inputOldAC) errorAC = ""
warningAC = inputAC[0] === "5" ? t('home.linkcard.felica-ac-warning') : ""
inputOldAC = inputAC
}
// Serial number input
const inputSNRegex = /^([0-9A-Fa-f]{0,2}:){0,7}[0-9A-Fa-f]{0,2}$/
let inputElemSN: HTMLInputElement
let inputOldSN = ""
let inputSN = ""
let errorSN = ""
function inputSNChange() {
// Add colons to the input
const old = inputSN
inputSN = inputSN.toUpperCase().replace(/[^0-9A-F]/g, '').replace(/(.{2})/g, '$1:').replace(/:$/, '')
if (inputSN !== old) errorSN = ""
inputSN = inputSN.toUpperCase()
const cursorIndex = cursorPositionToCursorIndex(inputSN, inputElemSN.selectionStart, /[0-9A-F]/)
inputSN = inputSN.replace(/[^0-9A-F]/g, '').replace(/(.{2})/g, '$1:').replace(/:$/, '')
const cursorPosition = cursorIndexToCursorPosition(inputSN, cursorIndex, /[0-9A-F]/)
setTimeout(() => inputElemSN.selectionStart = inputElemSN.selectionEnd = cursorPosition, 0)
if (inputSN !== inputOldSN) errorSN = ""
inputOldSN = inputSN
}
function formatLUID(luid: string, ghost: boolean = false) {
@@ -253,7 +293,8 @@
<p>{t('home.linkcard.access-code')}</p>
<label>
<!-- 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 bind:this={elemInputAC}
placeholder="e.g. 2408 1234 5678 9012 3456 / 0008 1234 5678 8765 4321"
on:keydown={(e) => {
e.key === "Enter" && link('AC')
// Ensure key is numeric
@@ -261,13 +302,22 @@
}}
bind:value={inputAC}
on:input={inputACChange}
class:error={inputAC && (!inputACRegex.test(inputAC) || errorAC)}>
class:error={inputAC && (!inputACRegex.test(inputAC) || errorAC)}
class:warning={inputAC && warningAC}>
{#if inputAC.length > 0}
<button transition:slide={{axis: 'x'}} on:click={() => {link('AC');inputAC=''}}>{t('home.linkcard.link')}</button>
<button transition:slide={{axis: 'x'}} on:click={() => link('AC')}>{t('home.linkcard.link')}</button>
{/if}
</label>
{#if errorAC}
<p class="error" transition:slide>{errorAC}</p>
<p class="error" style={warningAC ? "margin-bottom: 0" : ""} transition:slide>{errorAC}</p>
{/if}
{#if warningAC}
<!-- Transition temporarily adds `overflow: hidden` which leads to BFC issue, breaking margin collapse -->
<div style="overflow: hidden" transition:slide>
{#each warningAC.trim().split("\n") as paragraph}
<p class="warning">{paragraph}</p>
{/each}
</div>
{/if}
</div>
{/if}
@@ -280,7 +330,8 @@
{t('home.linkcard.enter-sn2')}
</p>
<label>
<input placeholder="e.g. 01:2E:1A:2B:3C:4D:5E:6F"
<input bind:this={inputElemSN}
placeholder="e.g. 01:2E:1A:2B:3C:4D:5E:6F"
on:keydown={(e) => {
e.key === "Enter" && link('SN')
// Ensure key is hex or colon
@@ -290,7 +341,7 @@
on:input={inputSNChange}
class:error={inputSN && (!inputSNRegex.test(inputSN) || errorSN)}>
{#if inputSN.length > 0}
<button transition:slide={{axis: 'x'}} on:click={() => {link('SN'); inputSN = ''}}>{t('home.linkcard.link')}</button>
<button transition:slide={{axis: 'x'}} on:click={() => link('SN')}>{t('home.linkcard.link')}</button>
{/if}
</label>
{#if errorSN}