[O] LinkCard UX improvements and i18n fixes

pull/113/head
Menci 2025-01-28 04:54:10 +08:00
parent 99e1d130f0
commit db7be134c7
6 changed files with 1547 additions and 1204 deletions

File diff suppressed because it is too large Load Diff

View File

@ -78,6 +78,7 @@ button
opacity: 0.9 opacity: 0.9
cursor: pointer cursor: pointer
transition: vars.$transition transition: vars.$transition
white-space: nowrap
button:hover button:hover
border-color: vars.$c-main border-color: vars.$c-main
@ -126,6 +127,8 @@ button.icon
// --lv-color: 239, 242, 225 // --lv-color: 239, 242, 225
--lv-text-clip: linear-gradient(110deg, #5ac42c, #5ccc22, #959f26, #cc7c23, #c93143, #8f4876, #4c3eb1, #3c3397) --lv-text-clip: linear-gradient(110deg, #5ac42c, #5ccc22, #959f26, #cc7c23, #c93143, #8f4876, #4c3eb1, #3c3397)
.warning
color: vars.$c-warning
.error .error
color: vars.$c-error color: vars.$c-error
@ -182,6 +185,9 @@ input:focus, input:focus-visible
border: 1px solid vars.$c-main border: 1px solid vars.$c-main
outline: none outline: none
input.warning
border: 1px solid vars.$c-warning
input.error input.error
border: 1px solid vars.$c-error border: 1px solid vars.$c-error

View File

@ -107,6 +107,7 @@ export const EN_REF_HOME = {
'home.linkcard.notfound': 'Card not found', 'home.linkcard.notfound': 'Card not found',
'home.linkcard.unlink': 'Unlink Card', 'home.linkcard.unlink': 'Unlink Card',
'home.linkcard.unlink-notice': 'Are you sure you want to unlink this card?', 'home.linkcard.unlink-notice': 'Are you sure you want to unlink this card?',
'home.linkcard.felica-ac-warning': 'This Access Code is of a Felica AIC card.\nIf you are logging in with a physical card (not aime.txt emulation), unlike the official server, you need to bind the Felica SN of the card (or the 0-prefixed card number shown in the game) instead of this code.\nIf you are logging in with aime.txt emulation, please ignore this warning and proceed.',
'home.setup.welcome': 'Welcome! If you own an arcade cabinet or game setup, please follow the instructions below to set up the connection with AquaDX.', 'home.setup.welcome': 'Welcome! If you own an arcade cabinet or game setup, please follow the instructions below to set up the connection with AquaDX.',
'home.setup.blockquote': 'We assume that you already have the required files and can run the game (e.g. ROM and segatools) that come with the cabinet or game setup. If not, please contact the seller of your device for the required files, as we will not provide them for copyright reasons.', 'home.setup.blockquote': 'We assume that you already have the required files and can run the game (e.g. ROM and segatools) that come with the cabinet or game setup. If not, please contact the seller of your device for the required files, as we will not provide them for copyright reasons.',
'home.setup.get': 'Get started', 'home.setup.get': 'Get started',
@ -134,7 +135,7 @@ export const EN_REF_SETTINGS = {
'settings.fields.unlockCollectables.name': 'Unlock All Collectables', 'settings.fields.unlockCollectables.name': 'Unlock All Collectables',
'settings.fields.unlockCollectables.desc': 'Unlock all collectables (nameplate, title, icon, frame) in game.', 'settings.fields.unlockCollectables.desc': 'Unlock all collectables (nameplate, title, icon, frame) in game.',
'settings.fields.unlockTickets.name': 'Unlock All Tickets', 'settings.fields.unlockTickets.name': 'Unlock All Tickets',
'settings.fields.unlockTickets.desc': 'Infinite map/ex tickets (note: maimai still limits which tickets can be used).', 'settings.fields.unlockTickets.desc': 'Infinite map/ex tickets (Note: maimai still limits which tickets can be used).',
'settings.fields.waccaInfiniteWp.name': 'Wacca: Infinite WP', 'settings.fields.waccaInfiniteWp.name': 'Wacca: Infinite WP',
'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999', 'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999',
'settings.fields.waccaAlwaysVip.name': 'Wacca: Always VIP', 'settings.fields.waccaAlwaysVip.name': 'Wacca: Always VIP',

View File

@ -27,7 +27,7 @@ const zhUser: typeof EN_REF_USER = {
'UserHome.Version': '游戏版本', 'UserHome.Version': '游戏版本',
'UserHome.RecentScores': '成绩', 'UserHome.RecentScores': '成绩',
'UserHome.NoData': '过去 ${days} 天内没有玩过', 'UserHome.NoData': '过去 ${days} 天内没有玩过',
'UserHome.UnknownSong': "(未知曲目)", 'UserHome.UnknownSong': "(未知曲目)",
'UserHome.Settings': '设置', 'UserHome.Settings': '设置',
'UserHome.NoValidGame': "用户还没有玩过游戏", 'UserHome.NoValidGame': "用户还没有玩过游戏",
'UserHome.ShowRanksDetails': "点击显示评分详细", 'UserHome.ShowRanksDetails': "点击显示评分详细",
@ -36,7 +36,7 @@ const zhUser: typeof EN_REF_USER = {
'UserHome.B50': "B50", 'UserHome.B50': "B50",
'UserHome.AddRival': "添加劲敌", 'UserHome.AddRival': "添加劲敌",
'UserHome.RemoveRival': "移除劲敌", 'UserHome.RemoveRival': "移除劲敌",
'UserHome.InvalidGame': "游戏 ${game} 还不支持网页端查看。我们目前只支持舞萌、中二、Wacca 和音击。", 'UserHome.InvalidGame': "游戏 ${game} 还不支持网页端查看。我们目前只支持舞萌、中二、华卡和音击。",
'UserHome.ShowMoreRecent': "显示更多", 'UserHome.ShowMoreRecent': "显示更多",
} }
@ -49,14 +49,14 @@ const zhWelcome: typeof EN_REF_Welcome = {
'welcome.btn-signup': '注册', 'welcome.btn-signup': '注册',
'welcome.email-password-missing': '邮箱和密码必须填哦', 'welcome.email-password-missing': '邮箱和密码必须填哦',
'welcome.username-missing': '用户名/邮箱必须填哦', 'welcome.username-missing': '用户名/邮箱必须填哦',
'welcome.waiting-turnstile': '正在验证网络环境...', 'welcome.waiting-turnstile': '正在验证网络环境',
'welcome.turnstile-error': '验证网络环境出错了,请关闭VPN后重试', 'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试',
'welcome.turnstile-timeout': '验证网络环境超时了,请重试', 'welcome.turnstile-timeout': '验证网络环境超时了,请重试',
'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱', 'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱',
'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱', 'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱',
'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的24小时内已经发送了3封验证邮件所以我们不会再发送了请翻翻收件箱', 'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱',
'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱', 'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱',
'welcome.verifying': '正在验证邮箱...请稍等', 'welcome.verifying': '正在验证邮箱请稍等',
'welcome.verified': '您的邮箱已经验证成功!您现在可以登录了', 'welcome.verified': '您的邮箱已经验证成功!您现在可以登录了',
'welcome.verification-failed': '验证失败:${message}。请重试', 'welcome.verification-failed': '验证失败:${message}。请重试',
} }
@ -74,7 +74,7 @@ const zhGeneral: typeof EN_REF_GENERAL = {
'game.mai2': "舞萌", 'game.mai2': "舞萌",
'game.chu3': "中二", 'game.chu3': "中二",
'game.ongeki': "音击", 'game.ongeki': "音击",
'game.wacca': "Wacca", 'game.wacca': "华卡",
"status.error": "发生错误", "status.error": "发生错误",
"status.error.hint": "出了一些问题,请稍后刷新重试或者", "status.error.hint": "出了一些问题,请稍后刷新重试或者",
"status.error.hint.link": "加我们的 Discord 群问一问", "status.error.hint.link": "加我们的 Discord 群问一问",
@ -91,7 +91,7 @@ const zhHome: typeof EN_REF_HOME = {
'home.manage-cards': '管理游戏卡', 'home.manage-cards': '管理游戏卡',
'home.manage-cards-description': '绑定、解绑、管理游戏数据卡', 'home.manage-cards-description': '绑定、解绑、管理游戏数据卡',
'home.link-card': '绑定游戏卡', 'home.link-card': '绑定游戏卡',
'home.link-cards-description':'绑定游戏数据卡 (Amusement IC 或 Aime 卡) 后才可以访问游戏存档哦', 'home.link-cards-description':'绑定游戏数据卡Amusement IC 或 Aime 卡)后才可以访问游戏存档哦',
'home.join-community': '加入群组', 'home.join-community': '加入群组',
'home.join-community-description': '加入我们的聊天群组,与其他玩家聊天、获取帮助', 'home.join-community-description': '加入我们的聊天群组,与其他玩家聊天、获取帮助',
'home.setup': '连接到 AquaDX', 'home.setup': '连接到 AquaDX',
@ -104,7 +104,7 @@ const zhHome: typeof EN_REF_HOME = {
'home.linkcard.registered': "注册于", 'home.linkcard.registered': "注册于",
'home.linkcard.lastused': "上次使用", 'home.linkcard.lastused': "上次使用",
'home.linkcard.enter-info': "请输入以下信息,或将 aime.txt / felica.txt 文件拖放到此区域", 'home.linkcard.enter-info': "请输入以下信息,或将 aime.txt / felica.txt 文件拖放到此区域",
'home.linkcard.access-code': "卡背面的20位卡号 (如果没有, 请尝试在游戏中扫描您的卡, 并输入屏幕上显示的卡号)", 'home.linkcard.access-code': "卡背面的 20 位卡号(如果提示找不到卡,请尝试使用游戏内置的显示卡号功能,输入游戏读取到的卡号)",
'home.linkcard.enter-sn1': "在您的手机", 'home.linkcard.enter-sn1': "在您的手机",
'home.linkcard.enter-sn2': "上下载 NFC Tools 并扫描您的卡。然后输入显示的 SN 号。", 'home.linkcard.enter-sn2': "上下载 NFC Tools 并扫描您的卡。然后输入显示的 SN 号。",
'home.linkcard.link': "绑定", 'home.linkcard.link': "绑定",
@ -116,15 +116,16 @@ const zhHome: typeof EN_REF_HOME = {
'home.linkcard.linked-another': "此卡已链接到其他用户", 'home.linkcard.linked-another': "此卡已链接到其他用户",
'home.linkcard.notfound': "找不到卡", 'home.linkcard.notfound': "找不到卡",
'home.linkcard.unlink': "取消链接", 'home.linkcard.unlink': "取消链接",
'home.linkcard.unlink-notice': "你确定要取消此卡的链接吗?", 'home.linkcard.unlink-notice': "你确定要取消此卡的链接吗?",
'home.setup.welcome': "欢迎! 如果您有街机框体或者手台, 请按照以下说明设置以连接到 AquaDX.", 'home.linkcard.felica-ac-warning': "该 Access Code 是一张 Felica AIC 卡。\n如果你使用实体卡而非 aime.txt 模拟)刷卡登录游戏,与官方服务器不同,你需要绑定该卡的 Felica SN或与之对应的游戏界面中查看得到的 0 开头的卡号)而非此号码。\n如果你使用 aime.txt 模拟登录,请忽略本警告继续绑定。",
'home.setup.blockquote': "我们假设您已经拥有所需的文件, 并且可以启动机台或手台附带的游戏 (例如 ROM 和 segatools )。如果没有, 请联系您设备的卖家以获取所需的文件, 因为出于版权原因, 我们不会提供这些文件。", 'home.setup.welcome': "欢迎! 如果您有街机框体或者手台,请按照以下说明设置以连接到 AquaDX。",
'home.setup.blockquote': "我们假设您已经拥有所需的文件,并且可以启动机台或手台附带的游戏(例如 ROM 和 segatools。如果没有请联系您设备的卖家以获取所需的文件因为出于版权原因我们不会提供这些文件。",
'home.setup.get': "开始", 'home.setup.get': "开始",
'home.setup.edit': "请打开您的 segatools.ini 文件并修改以下行", 'home.setup.edit': "请打开您的 segatools.ini 文件并修改以下行",
'home.setup.test': "在您重新启动游戏后, 应该能够连接到 AquaDX。可以验证测试菜单中的网络测试测试连接是否全部良好。", 'home.setup.test': "在您重新启动游戏后应该能够连接到 AquaDX。可以验证测试菜单中的网络测试测试连接是否全部良好。",
'home.setup.ask': "如果您有任何问题, 请加入我们的", 'home.setup.ask': "如果您有任何问题请加入我们的",
'home.setup.support': "以获取支持", 'home.setup.support': "以获取支持",
'home.setup.keychip-tips': "这是你的狗号, 不要与任何人分享", 'home.setup.keychip-tips': "这是你的狗号不要与任何人分享",
'home.import.unknown-game': '未知游戏类型 (目前导入只支持舞萌和中二)', 'home.import.unknown-game': '未知游戏类型 (目前导入只支持舞萌和中二)',
'home.import.new-data': '要导入的数据', 'home.import.new-data': '要导入的数据',
'home.import.data-conflict': '继续导入将覆盖现有数据', 'home.import.data-conflict': '继续导入将覆盖现有数据',
@ -136,7 +137,7 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.tabs.game': '游戏设置', 'settings.tabs.game': '游戏设置',
'settings.tabs.chu3': '中二', 'settings.tabs.chu3': '中二',
'settings.tabs.mai2': '舞萌', 'settings.tabs.mai2': '舞萌',
'settings.tabs.wacca': 'Wacca', 'settings.tabs.wacca': '华卡',
'settings.fields.unlockMusic.name': '解锁谱面', 'settings.fields.unlockMusic.name': '解锁谱面',
'settings.fields.unlockMusic.desc': '在游戏中解锁所有曲目和大师难度谱面。', 'settings.fields.unlockMusic.desc': '在游戏中解锁所有曲目和大师难度谱面。',
'settings.fields.unlockChara.name': '解锁角色', 'settings.fields.unlockChara.name': '解锁角色',
@ -145,9 +146,9 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.fields.unlockCollectables.desc': '在游戏中解锁所有收藏品(名牌、称号、图标、背景图)。', 'settings.fields.unlockCollectables.desc': '在游戏中解锁所有收藏品(名牌、称号、图标、背景图)。',
'settings.fields.unlockTickets.name': '解锁游戏券', 'settings.fields.unlockTickets.name': '解锁游戏券',
'settings.fields.unlockTickets.desc': '无限跑图券/解锁券maimai 客户端仍限制一些券不能使用)。', 'settings.fields.unlockTickets.desc': '无限跑图券/解锁券maimai 客户端仍限制一些券不能使用)。',
'settings.fields.waccaInfiniteWp.name': 'Wacca: 无限 WP', 'settings.fields.waccaInfiniteWp.name': '华卡:无限 WP',
'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999', 'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999',
'settings.fields.waccaAlwaysVip.name': 'Wacca: 永久会员', 'settings.fields.waccaAlwaysVip.name': '华卡:永久会员',
'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01', 'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01',
'settings.fields.chusanTeamName.name': '队伍名称', 'settings.fields.chusanTeamName.name': '队伍名称',
'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。', 'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。',
@ -163,11 +164,12 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.fields.gameUsername.desc': '在游戏中显示的用户名', 'settings.fields.gameUsername.desc': '在游戏中显示的用户名',
'settings.fields.optOutOfLeaderboard.name': '不参与排行榜', 'settings.fields.optOutOfLeaderboard.name': '不参与排行榜',
'settings.fields.optOutOfLeaderboard.desc': '登录之后还是可以在排行榜上看到自己', 'settings.fields.optOutOfLeaderboard.desc': '登录之后还是可以在排行榜上看到自己',
'settings.fields.enableMusicRank.name': '在你的机台上启用“推荐乐曲排行榜”', 'settings.fields.enableMusicRank.name': '在你的机台上启用「推荐乐曲排行榜」',
'settings.fields.enableMusicRank.desc': '如果你自己设计了排行榜的话,可以关闭这个。只会影响你自己的机器', 'settings.fields.enableMusicRank.desc': '如果你自己设计了排行榜的话,可以关闭这个(会影响你自己的机器)。',
'settings.mai2.name': '玩家名字', 'settings.mai2.name': '玩家名字',
'settings.profile.picture': '头像', 'settings.profile.picture': '头像',
'settings.profile.upload-new': '上传', 'settings.profile.upload-new': '上传',
'settings.profile.bad-format': '无效的图片格式,支持的格式有 PNG、JPG、JPEG、WEBP 和 GIF。',
'settings.profile.save': '保存', 'settings.profile.save': '保存',
'settings.profile.name': '昵称', 'settings.profile.name': '昵称',
'settings.profile.username': '用户名', 'settings.profile.username': '用户名',
@ -177,7 +179,8 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.profile.unset': '未设置', 'settings.profile.unset': '未设置',
'settings.profile.unchanged': '未更改', 'settings.profile.unchanged': '未更改',
'settings.export': '导出玩家数据', 'settings.export': '导出玩家数据',
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置' 'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
'settings.gameNotice': "这些设置仅对舞萌和华卡生效。",
} }
export const zhUserbox: typeof EN_REF_USERBOX = { export const zhUserbox: typeof EN_REF_USERBOX = {
@ -209,7 +212,7 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.matching.custom.sub': '输入其他的匹配 URL', 'userbox.matching.custom.sub': '输入其他的匹配 URL',
'userbox.new.name': 'AquaBox', 'userbox.new.name': 'AquaBox',
'userbox.new.setup': '将 ChuniLumi 或更高版本)的游戏文件夹拖放到下方区域,以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。', 'userbox.new.setup': '将中二Lumi 或更高版本)的游戏文件夹拖放到下方区域,以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。',
'userbox.new.setup.notice': '我们支持的目录结构是把 opt 放进 "bin/option" 并且把 "A000" 放在 "data" 里面。', 'userbox.new.setup.notice': '我们支持的目录结构是把 opt 放进 "bin/option" 并且把 "A000" 放在 "data" 里面。',
'userbox.new.setup.processing_file': '正在处理文件', 'userbox.new.setup.processing_file': '正在处理文件',
'userbox.new.setup.finalizing': '正在保存到内部存储', 'userbox.new.setup.finalizing': '正在保存到内部存储',

View File

@ -2,12 +2,12 @@
<script lang="ts"> <script lang="ts">
import { fade, slide } from "svelte/transition" import { fade, slide } from "svelte/transition"
import type { Card, CardSummary, CardSummaryGame, ConfirmProps, AquaNetUser } from "../../libs/generalTypes"; import type { Card, CardSummary, CardSummaryGame, ConfirmProps, AquaNetUser } from "../../libs/generalTypes"
import { CARD, USER } from "../../libs/sdk"; import { CARD, USER } from "../../libs/sdk"
import moment from "moment" import moment from "moment"
import Icon from "@iconify/svelte"; import Icon from "@iconify/svelte"
import StatusOverlays from "../../components/StatusOverlays.svelte"; import StatusOverlays from "../../components/StatusOverlays.svelte"
import { t } from "../../libs/i18n"; import { t } from "../../libs/i18n"
// State // State
let state: 'ready' | 'linking-AC' | 'linking-SN' | 'loading' = "loading" let state: 'ready' | 'linking-AC' | 'linking-SN' | 'loading' = "loading"
@ -42,14 +42,22 @@
} }
async function doLink(id: string, migrate: string) { async function doLink(id: string, migrate: string) {
await CARD.link({cardId: id, migrate}) try {
await updateMe() await CARD.link({cardId: id, migrate})
await updateMe()
if (linkingType === 'AC') inputAC = ""
if (linkingType === 'SN') inputSN = ""
} catch (e) {
setError(e.message, linkingType)
}
state = "ready" state = "ready"
} }
let linkingType: 'AC' | 'SN' = null
async function link(type: 'AC' | 'SN') { async function link(type: 'AC' | 'SN') {
if (state !== 'ready' || accountCardSummary === null) return if (state !== 'ready' || accountCardSummary === null) return
state = "linking-" + type state = "linking-" + type
linkingType = type
const id = type === 'AC' ? inputAC : inputSN const id = type === 'AC' ? inputAC : inputSN
console.log("linking card", id) console.log("linking card", id)
@ -64,7 +72,7 @@
// First, lookup the card summary // First, lookup the card summary
const card = (await CARD.summary(id).catch(e => { const card = (await CARD.summary(id).catch(e => {
// If card is not found, create a card and link it // 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, "") doLink(id, "")
return 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 // Access code input
const inputACRegex = /^(\d{4} ){0,4}\d{0,4}$/ const inputACRegex = /^(\d{4} ){0,4}\d{0,4}$/
let elemInputAC: HTMLInputElement
let inputOldAC = ""
let inputAC = "" let inputAC = ""
let errorAC = "" let errorAC = ""
let warningAC = ""
function inputACChange() { function inputACChange() {
// Add spaces to the input // 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(/ $/, '') 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 // 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}$/
let inputElemSN: HTMLInputElement
let inputOldSN = ""
let inputSN = "" let inputSN = ""
let errorSN = "" let errorSN = ""
function inputSNChange() { function inputSNChange() {
// Add colons to the input // Add colons to the input
const old = inputSN inputSN = inputSN.toUpperCase()
inputSN = inputSN.toUpperCase().replace(/[^0-9A-F]/g, '').replace(/(.{2})/g, '$1:').replace(/:$/, '') const cursorIndex = cursorPositionToCursorIndex(inputSN, inputElemSN.selectionStart, /[0-9A-F]/)
if (inputSN !== old) errorSN = "" 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) { function formatLUID(luid: string, ghost: boolean = false) {
@ -253,7 +293,8 @@
<p>{t('home.linkcard.access-code')}</p> <p>{t('home.linkcard.access-code')}</p>
<label> <label>
<!-- 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 bind:this={elemInputAC}
placeholder="e.g. 2408 1234 5678 9012 3456 / 0008 1234 5678 8765 4321"
on:keydown={(e) => { on:keydown={(e) => {
e.key === "Enter" && link('AC') e.key === "Enter" && link('AC')
// Ensure key is numeric // Ensure key is numeric
@ -261,13 +302,22 @@
}} }}
bind:value={inputAC} bind:value={inputAC}
on:input={inputACChange} 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} {#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} {/if}
</label> </label>
{#if errorAC} {#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} {/if}
</div> </div>
{/if} {/if}
@ -280,7 +330,8 @@
{t('home.linkcard.enter-sn2')} {t('home.linkcard.enter-sn2')}
</p> </p>
<label> <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) => { on:keydown={(e) => {
e.key === "Enter" && link('SN') e.key === "Enter" && link('SN')
// Ensure key is hex or colon // Ensure key is hex or colon
@ -290,7 +341,7 @@
on:input={inputSNChange} on:input={inputSNChange}
class:error={inputSN && (!inputSNRegex.test(inputSN) || errorSN)}> class:error={inputSN && (!inputSNRegex.test(inputSN) || errorSN)}>
{#if inputSN.length > 0} {#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} {/if}
</label> </label>
{#if errorSN} {#if errorSN}

View File

@ -4,6 +4,7 @@ $c-sub: rgba(0, 0, 0, 0.77)
$c-good: #b3ffb9 $c-good: #b3ffb9
$c-darker: #646cff $c-darker: #646cff
$c-bg: #242424 $c-bg: #242424
$c-warning:hsl(40 100% 71% / 1)
$c-error: #ff6b6b $c-error: #ff6b6b
$c-shadow: rgba(0, 0, 0, 0.1) $c-shadow: rgba(0, 0, 0, 0.1)