feat: automatic keychip installation, improved onboarding ux, tweak styles, bomb israel

This commit is contained in:
raymonable
2026-02-14 23:41:22 -05:00
parent 6ddb72e28e
commit ca5d33a1cc
14 changed files with 526 additions and 248 deletions

Binary file not shown.

View File

@@ -13,6 +13,9 @@
import { t } from "./libs/i18n";
import Transfer from "./pages/Transfer/Transfer.svelte";
import { link } from "d3";
import Communities from "./pages/Home/Communities.svelte";
import LinkCard from "./pages/Home/LinkCard.svelte";
import SetupInstructions from "./pages/Home/SetupInstructions.svelte";
console.log(`%c
┏━┓ ┳━┓━┓┏━
@@ -82,7 +85,9 @@
<Route path="/verify" component={Welcome} /> <!-- For email verification only, backwards compatibility with AquaNet2 in the future -->
<Route path="/reset-password" component={Welcome} />
<Route path="/home" component={Home} />
<Route path="/ranking" component={Ranking} />
<Route path="/support" component={Communities} />
<Route path="/cards" component={LinkCard} />
<Route path="/setup" component={SetupInstructions} />
<Route path="/ranking/:game" component={Ranking} />
<Route path="/u/:username" component={UserHome} />
<Route path="/u/:username/:game" component={UserHome} />

View File

@@ -55,6 +55,29 @@ blockquote
border-left: solid #ff7c7c 3px
border-radius: vars.$border-radius
&::before
content: ""
margin-right: 1em
&.success
$c1: rgba(156, 255, 149, 0.05)
$c2: rgba(152, 255, 174, 0.02)
background: repeating-linear-gradient(45deg, $c1, $c1 10px, $c2 10px, $c2 20px)
border-left: solid #97fa8c 3px
&::before
content: ""
margin-right: 1em
&.info
$c1: rgba(149, 183, 255, 0.05)
$c2: rgba(152, 186, 255, 0.02)
background: repeating-linear-gradient(45deg, $c1, $c1 10px, $c2 10px, $c2 20px)
border-left: solid #6e8bff 3px
&::before
content: ""
margin-right: 1em
#app
width: 100%

View File

@@ -5,6 +5,8 @@
export let color: string = '179, 198, 255'
export let icon: string
export let href: string | undefined
export let isSmall: boolean = false
// Manually positioned icons
const iconPos = [
@@ -19,7 +21,7 @@
]
</script>
<div class="action-card" style="--card-color: {color}" on:click role="button" tabindex="0" on:keydown>
<a class="action-card" class:small={isSmall} style="--card-color: {color}" on:click {href} role="button" tabindex="0" on:keydown>
<slot/>
<div class="icons">
@@ -27,13 +29,14 @@
<Icon icon={icon} style={`top: ${y}rem; right: ${x}rem; font-size: ${size || 1}em`} />
{/each}
</div>
</div>
</a>
<style lang="sass">
@use '../vars'
.action-card
overflow: hidden
display: block
padding: 1rem
border-radius: vars.$border-radius
box-shadow: 0 5px 5px 1px vars.$c-shadow
@@ -43,6 +46,7 @@
background: linear-gradient(45deg, transparent 20%, rgba(var(--card-color), 0.5) 100%)
outline: 1px solid transparent
filter: drop-shadow(0 0 12px rgba(var(--card-color), 0))
color: rgba(255, 255, 255, 0.78)
&:hover
box-shadow: 0 0 0.5rem 0.2rem vars.$c-shadow
@@ -52,10 +56,11 @@
filter: drop-shadow(0 0 12px rgba(var(--card-color), 0.5))
outline-color: rgba(var(--card-color), 0.5)
:global(span)
:global(span), &.small
font-size: 1.2rem
display: block
margin-bottom: 0.5rem
color: color-mix(in oklab, rgb(var(--card-color)) 25%, rgba(255, 255, 255, 0.75))
.icons
position: absolute

View File

@@ -5,22 +5,23 @@
export let color: string = '179, 198, 255'
export let icon: string
export let href: string
</script>
<div class="action-card" style="--card-color: {color}" on:click role="button" tabindex="0" on:keydown>
<a class="action-card" style="--card-color: {color}" {href} role="button" tabindex="0" on:keydown>
<slot/>
<div class="icon">
<Icon icon={icon} />
</div>
</div>
</a>
<style lang="sass">
@use '../vars'
.action-card
overflow: hidden
display: block
padding: 1rem
border-radius: vars.$border-radius
box-shadow: 0 5px 5px 1px vars.$c-shadow
@@ -30,6 +31,7 @@
background: linear-gradient(45deg, transparent 20%, rgba(var(--card-color), 0.5) 100%)
outline: 1px solid transparent
filter: drop-shadow(0 0 12px rgba(var(--card-color), 0))
color: rgba(255, 255, 255, 0.78)
&:hover
box-shadow: 0 0 0.5rem 0.2rem vars.$c-shadow

View File

@@ -9,7 +9,7 @@
</script>
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
<blockquote>
<blockquote class="info">
{ts("settings.siteNotice")}
</blockquote>
<div class="field">
@@ -22,7 +22,7 @@
</div>
</div>
<div class="divider"></div>
<blockquote>
<blockquote class="info">
{ts("settings.regionNotice")}
</blockquote>
<RegionSelector/>

View File

@@ -36,7 +36,7 @@ export function ts(key: string, variables?: { [index: string]: any }) {
* @param key
* @param variables
*/
export function t(key: keyof LocalizedMessages, variables?: { [index: string]: any }) {
export function t(key: keyof LocalizedMessages, variables?: { [index: string]: any }): string {
// Check if the key exists
let msg = msgs[lang][key]
if (!msg) {

View File

@@ -94,26 +94,28 @@ export const EN_REF_HOME = {
'home.nav.portal': 'Portal',
'home.nav.link-card': 'Link Card',
'home.nav.game-setup': 'Game Setup',
'home.nav.support': 'Support',
'home.user-profile': 'Profile',
'home.rankings': 'Rankings',
'home.settings': 'Settings',
'home.manage-cards': 'Manage Cards',
'home.manage-cards-description': 'Link, unlink, and manage your cards.',
'home.link-card': 'Link Card',
'home.link-cards-description': 'Link your Amusement IC / Aime card to play games.',
'home.join-community': 'Join Community',
'home.join-community-description': 'Join our community to chat with other players and get help.',
'home.setup': 'Setup Connection',
'home.setup-description': 'If you own a cab or arcade setup, begin setting up the connection.',
'home.join-community-description': 'Join our community for support and chatting with other players.',
'home.setup': 'Setup Network Connection',
'home.setup-description': 'Configure a game to connect to our servers.',
'home.import': 'Import Player Data',
'home.import-description': 'If you are from another server, you can import your data here.',
'home.import-description': 'Add player data from a separate instance.',
'home.linkcard.cards': 'Your Cards',
'home.linkcard.description': 'Here are the cards you have linked to your account',
'home.linkcard.description': 'Access cards that are currently linked',
'home.linkcard.account-card': 'Account Card',
'home.linkcard.registered': 'Registered',
'home.linkcard.lastused': 'Last used',
'home.linkcard.enter-info': 'Please enter the following information, or drag and drop your aime.txt / felica.txt file here',
'home.linkcard.access-code': 'The 20-digit access code on the back of your card. (If it doesn\'t work, please try scanning your card in game and enter the access code shown on screen)',
'home.linkcard.enter-sn1': 'Download the NFC Tools app on your phone',
'home.linkcard.enter-sn2': 'and scan your card. Then, enter the Serial Number.',
'home.linkcard.kdx-notice': "If you're using KanadeDX, please enter the simulated card number (you can find it in settings > card).",
'home.linkcard.enter-info': 'Please enter either an Access Code or a Serial Number in the following boxes. Dragging and dropping aime.txt or felica.txt is also supported.',
'home.linkcard.access-code': 'If you have an Access Code, enter it below. Access Codes can be obtained from the back of your card or from the title screen of your game (press the Access Code button and card in). If you do not have an Access Code, you may generate one.',
'home.linkcard.enter-sn': 'If you have a Serial Number, enter it below. Serial Numbers can be obtained via most smartphones (NFC Tools: <a href="https://play.google.com/store/apps/details?id=com.wakdev.wdnfc">Android</a> / <a href="https://apps.apple.com/us/app/nfc-tools/id1252962749">Apple</a>) or a card reader.',
'home.linkcard.link': 'Link',
'home.linkcard.data-conflict': 'Data Conflict',
'home.linkcard.name': 'Name',
@@ -125,22 +127,34 @@ export const EN_REF_HOME = {
'home.linkcard.unlink': 'Unlink 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 00-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.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.edit': 'Please edit your segatools.ini file and modify the following lines',
'home.setup.test': 'Then, after you restart the game, you should be able to connect to AquaDX. Please verify that the network tests are all GOOD in the test menu.',
'home.setup.ask': 'If you have any questions, please ask in our',
'home.setup.support': 'server',
'home.setup.keychip-tips': 'This is your unique keychip, do not share it with anyone',
'home.linkcard.card-security-warning': 'Access Cards give full access to your games\' player data. Do not share your card information with anyone.',
'home.community.discord': 'Discord',
'home.community.telegram': 'Telegram (Chinese)',
'home.community.qq': 'QQ (Chinese)',
'home.import.unknown-game': 'Unknown game type. Currently only maimai and chunithm are supported for importing.',
'home.import.unknown-game': 'Unknown game type. Currently only Mai and Chuni are supported for importing.',
'home.import.new-data': 'Data to import',
'home.import.data-conflict': 'Proceed will override your current data',
}
export const EN_REF_SETUP = {
'loading': `Please wait...`,
'setup.welcome': `Welcome! If you have a game set up, please follow the instructions below to set up the connection with AquaDX.`,
'setup.keychip-warning': `Your keychip is linked to your account and should be kept secure. Do not give others your keychip.`,
'setup.steps.one': `Pick a method of setting up network communications. Some browsers may not be able to do automatic setup.`,
'setup.steps.two': `Ensure your game has encryption disabled as AquaDX does not support encryption. This may be via a patch or setting.`,
'setup.steps.three': `Link your Aime card to your AquaDX account using the <a href="/cards">Cards</a> page via it's access code or serial number.`,
'setup.steps.four': `Start the game. Upon reaching the title screen, the network icon in the corner should now show green instead of grey.`,
'setup.support-info': `If you need assistance, feel free to make an inquiry in a <a href="/support">support channel</a>.`,
'setup.reveal-keychip': `Reveal keychip`,
'setup.type.automatic': `Automatic Setup`,
'setup.type.manual': `Manual Setup`,
'setup.manual': `Please modify your segatools.ini with the following information.`,
'setup.automatic': `Select your segatools.ini below to autofill the network information.`,
'setup.automatic.success': `Success, data has been updated`,
'setup.automatic.failure': `Failed to update information, please ensure access is not blocked and try again.`,
'setup.automatic.select': `Pick file`
}
export const EN_REF_SETTINGS = {
'settings.title': 'Settings',
'settings.tabs.profile': 'Profile',
@@ -301,12 +315,12 @@ export const EN_REF_AQUATRANS = {
'trans.btn.test': 'Test Connection',
'trans.btn.export': 'Export Data',
'trans.btn.import': 'Import Data',
'trans.blacklist': "Your server's rules doesn't allow using this tool. You might get banned if you try (idk, ask them if you want to know why)",
'trans.blacklist': "This server may have rules against tools such as AquaTrans, please use caution.",
}
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,
...EN_REF_LEADERBOARD, ...EN_REF_HOME, ...EN_REF_SETTINGS, ...EN_REF_USERBOX,
...EN_REF_MAI_PHOTO, ...EN_REF_AQUATRANS
...EN_REF_MAI_PHOTO, ...EN_REF_AQUATRANS, ...EN_REF_SETUP
}
export type LocalizedMessages = typeof EN_REF

83
AquaNet/src/libs/setup.ts Normal file
View File

@@ -0,0 +1,83 @@
// backported from AquaNet2
export interface NetworkData {
keychip?: string,
dns?: string
}
function isSection(section: string): boolean {
return section.substring(0, 1) === "["
&& section.substring(section.length - 1, section.length) === "]"
}
function addOrUpdateItem(ini: string[], section: string, key: string, value: string) {
let sections: Record<string, {
idx: number,
keys: Record<string, number>
}> = {};
let activeSection: string | undefined;
for (let [idx, item] of ini.entries()) {
if (isSection(item)) {
activeSection = item.substring(1, item.length - 1)
sections[activeSection] = {
idx, keys: {}
};
} else {
let key = item.split("=")[0];
let value = item.split("=")[1];
if (!key || !value)
continue;
if (activeSection && sections[activeSection])
sections[activeSection].keys[key] = idx;
}
}
if (sections[section] && sections[section].keys[key]) {
ini[
sections[section].keys[key]
] = `${key}=${value}`;
} else if (sections[section]) {
ini.splice(sections[section].idx + 1, 0, `${key}=${value}`);
} else {
ini.push(`[${section}]`)
ini.push(`${key}=${value}`)
}
}
export function injectNetworkData(baseIni: string, networkData: NetworkData): string {
let ini: string[] = baseIni.split("\n").map(i => i.replaceAll("\r", ""));
if (networkData.dns)
addOrUpdateItem(ini, "dns", "default", networkData.dns);
addOrUpdateItem(ini, "keychip", "enable", "1");
if (networkData.keychip)
addOrUpdateItem(ini, "keychip", "id", networkData.keychip);
return ini.join("\n");
}
export async function patchUserSegatools(networkData: NetworkData): Promise<boolean> {
try {
let [file] = await window.showOpenFilePicker({
multiple: false,
types: [{
description: "segatools.ini",
accept: {"text/ini": ".ini"}
}]
});
let writable = await file.createWritable();
await writable.write(
injectNetworkData(
await (await file.getFile()).text(),
networkData
)
);
await writable.close();
return true;
} catch(err) {
return false;
}
}

View File

@@ -1,43 +1,39 @@
<script lang="ts">
import { fade } from "svelte/transition";
import LinkCard from "./Home/LinkCard.svelte";
import SetupInstructions from "./Home/SetupInstructions.svelte";
import { DISCORD_INVITE, FADE_IN, FADE_OUT } from "../libs/config";
import { USER } from "../libs/sdk.js";
import type { AquaNetUser } from "../libs/generalTypes";
import StatusOverlays from "../components/StatusOverlays.svelte";
import ActionCard from "../components/ActionCard.svelte";
import { t } from "../libs/i18n";
import ImportDataAction from "./Home/ImportDataAction.svelte";
import Communities from "./Home/Communities.svelte";
import DashboardTabs from "./Home/DashboardTabs.svelte";
USER.ensureLoggedIn();
let me: AquaNetUser
let error = ""
let tab = 0;
let tabs = [t('home.nav.portal'), t('home.nav.link-card'), t('home.nav.game-setup')]
let me: AquaNetUser;
let error = "";
USER.me().then((m) => me = m).catch(e => error = e.message)
</script>
<main class="content">
<!-- <h2 class="outer-title">&nbsp;</h2>-->
<nav class="tabs">
{#each tabs as t, i}
<div class="clickable"
class:active={tab === i}
on:click={() => tab = i}
on:keydown={(e) => e.key === "Enter" && (tab = i)}
role="button" tabindex={i}>{t}
<DashboardTabs />
{#if me}
<div class="action-cards">
<div class="quick-action-cards">
<ActionCard isSmall={true} color="201, 135, 174" icon="fluent:games-16-filled" href={`/u/${me.username}`}>
<h3>{t('home.user-profile')}</h3>
</ActionCard>
<ActionCard isSmall={true} color="136, 99, 150" icon="fluent:text-bullet-list-square-16-filled" href={`/rankings`}>
<h3>{t('home.rankings')}</h3>
</ActionCard>
<ActionCard isSmall={true} color="133, 199, 201" icon="fluent:settings-16-filled" href={`/settings`}>
<h3>{t('home.settings')}</h3>
</ActionCard>
</div>
{/each}
</nav>
{#if tab === 0}
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="action-cards">
<ActionCard color="255, 192, 203" icon="solar:card-bold-duotone" on:click={() => tab = 1}>
<div class="separator"></div>
<ActionCard color="255, 192, 203" icon="solar:card-bold-duotone" href="/cards">
{#if me && me.cards.length > 1}
<h3>{t('home.manage-cards')}</h3>
<span>{t('home.manage-cards-description')}</span>
@@ -47,29 +43,17 @@
{/if}
</ActionCard>
<ActionCard color="82, 93, 233" icon="fluent:chat-12-filled" on:click={() => tab = 3}>
<h3>{t('home.join-community')}</h3>
<span>{t('home.join-community-description')}</span>
</ActionCard>
<ActionCard on:click={() => tab = 2} icon="uil:link-alt">
<ImportDataAction/>
<ActionCard icon="uil:link-alt" href="/setup">
<h3>{t('home.setup')}</h3>
<span>{t('home.setup-description')}</span>
</ActionCard>
<ImportDataAction/>
</div>
{:else if tab === 1}
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
<LinkCard/>
</div>
{:else if tab === 2}
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
<SetupInstructions/>
</div>
{:else if tab === 3}
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
<Communities/>
<ActionCard color="82, 93, 233" icon="fluent:chat-12-filled" href="/support">
<h3>{t('home.join-community')}</h3>
<span>{t('home.join-community-description')}</span>
</ActionCard>
</div>
{/if}
</main>
@@ -95,4 +79,24 @@
display: flex
flex-direction: column
gap: 1rem
.quick-action-cards
display: flex
flex-direction: row
gap: 1rem
:global(.action-card)
flex: 1
height: 2rem
display: flex
align-content: center
flex-wrap: wrap
.separator
position: relative
left: 50%
transform: translate(-50%, 0)
width: 75%
height: 1px
background: vars.$ov-light
</style>

View File

@@ -4,30 +4,34 @@
import { t } from "../../libs/i18n";
import CommunityCard from "../../components/CommunityCard.svelte";
import { DISCORD_INVITE, QQ_INVITE, TELEGRAM_INVITE } from "../../libs/config";
import DashboardTabs from "./DashboardTabs.svelte";
</script>
<div class="setup-instructions">
<h2>{t('home.join-community')}</h2>
<div class="grid cols-3 gap-4">
{#if DISCORD_INVITE}
<CommunityCard color="82, 93, 233" icon="ic:baseline-discord" on:click={() => window.location.href = DISCORD_INVITE}>
<h3>{t('home.community.discord')}</h3>
</CommunityCard>
{/if}
<main class="content">
<DashboardTabs />
<div class="setup-instructions">
<h2>{t('home.join-community')}</h2>
<div class="grid cols-3 gap-4">
{#if DISCORD_INVITE}
<CommunityCard color="82, 93, 233" icon="ic:baseline-discord" href={DISCORD_INVITE}>
<h3>{t('home.community.discord')}</h3>
</CommunityCard>
{/if}
{#if TELEGRAM_INVITE}
<CommunityCard color="46, 163, 224" icon="mingcute:telegram-fill" on:click={() => window.location.href = TELEGRAM_INVITE}>
<h3>{t('home.community.telegram')}</h3>
</CommunityCard>
{/if}
{#if TELEGRAM_INVITE}
<CommunityCard color="46, 163, 224" icon="mingcute:telegram-fill" href={TELEGRAM_INVITE}>
<h3>{t('home.community.telegram')}</h3>
</CommunityCard>
{/if}
{#if QQ_INVITE}
<CommunityCard color="226, 60, 68" icon="ri:qq-fill" on:click={() => window.location.href = QQ_INVITE}>
<h3>{t('home.community.qq')}</h3>
</CommunityCard>
{/if}
{#if QQ_INVITE}
<CommunityCard color="226, 60, 68" icon="ri:qq-fill" href={QQ_INVITE}>
<h3>{t('home.community.qq')}</h3>
</CommunityCard>
{/if}
</div>
</div>
</div>
</main>
<style lang="sass">

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import { t } from "../../libs/i18n";
const tabs: Record<string, string> = {
[t('home.nav.portal')]: `/home`,
[t('home.nav.link-card')]: `/cards`,
[t('home.nav.game-setup')]: `/setup`,
[t('home.nav.support')]: `/support`
}
</script>
<nav class="tabs">
{#each Object.entries(tabs) as t, i}
<a class="clickable"
href={t[1]}
class:active={
new URL(location.href).pathname === t[1]
}>
{t[0]}
</a>
{/each}
</nav>
<style lang="sass">
@use "../../vars"
.tabs
display: flex
gap: 1rem
div
&.active
color: vars.$c-main
</style>

View File

@@ -8,6 +8,7 @@
import Icon from "@iconify/svelte"
import StatusOverlays from "../../components/StatusOverlays.svelte"
import { t } from "../../libs/i18n"
import DashboardTabs from "./DashboardTabs.svelte";
// State
let state: 'ready' | 'linking-AC' | 'linking-SN' | 'loading' = "loading"
@@ -262,125 +263,147 @@
break
}
}
function generateRandom() {
inputAC = "";
while (inputAC.length < 20) {
let digit = Math.floor(Math.random() * 10);
if (!((digit == 5 || digit == 3) && inputAC.length == 0))
inputAC += digit;
};
inputACChange();
}
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="link-card" on:drop={dropFile} on:dragover={(e) => e.preventDefault()}>
<h2>{t('home.linkcard.cards')}</h2>
<p>{t('home.linkcard.description')}:</p>
<main class="content">
<DashboardTabs />
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="link-card" on:drop={dropFile} on:dragover={(e) => e.preventDefault()}>
{#if me}
<div class="existing-cards" transition:slide>
{#each me.cards as card (card.luid)}
<div class:ghost={card.isGhost} class='existing card' transition:fade|global>
<span class="type">{card.isGhost ? t('home.linkcard.account-card') : cardType(card.luid)}</span>
<span class="register">{t('home.linkcard.registered')}: {moment(card.registerTime).format("YYYY MMM DD")}</span>
<span class="last">{t('home.linkcard.lastused')}: {moment(card.accessTime).format("YYYY MMM DD")}</span>
<div></div>
<span class="id">{formatLUID(card.luid, card.isGhost)}</span>
{#if me && me.cards && me.cards.find(card => !card.isGhost)}
<h2>{t('home.linkcard.cards')}</h2>
<p>{t('home.linkcard.description')}:</p>
<div class="existing-cards" transition:slide>
{#each me.cards as card (card.luid)}
<!-- Hide account cards, they only cause confusion to a large majority of users -->
{#if !card.isGhost}
<button class="icon error" on:click={() => unlink(card)}><Icon icon="tabler:trash-x-filled"/></button>
<div class:ghost={card.isGhost} class={`existing card ${cardType(card.luid) == "FeliCa SN" ? "sn" : "ac"}`} transition:fade|global>
<span class="type">{card.isGhost ? t('home.linkcard.account-card') : cardType(card.luid)}</span>
<span class="register">{t('home.linkcard.registered')}: {moment(card.registerTime).format("YYYY MMM DD")}</span>
<span class="last">{t('home.linkcard.lastused')}: {moment(card.accessTime).format("YYYY MMM DD")}</span>
<div></div>
<!-- Sorry, it's kind of ugly to do it like this, but it improves copiablity -->
<span class="id">{@html formatLUID(card.luid, card.isGhost)
.split(" ").map(v => `<i>${v}</i>`).join("")
.split(":").map((v, i, a) => `<i>${v}${i < a.length - 1 ? ":" : ""}</i>`).join("")}</span>
{#if !card.isGhost}
<button class="icon error" on:click={() => unlink(card)}><Icon icon="tabler:trash-x-filled"/></button>
{/if}
</div>
{/if}
</div>
{/each}
</div>
{/if}
<h2>{t('home.link-card')}</h2>
<p>{t('home.linkcard.enter-info')}:</p>
{#if !inputSN}
<div out:slide={{ duration: 250 }}>
<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 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
if (isInput(e) && !/[\d ]/.test(e.key)) e.preventDefault()
}}
bind:value={inputAC}
on:input={inputACChange}
class:error={inputAC && (!inputACRegex.test(inputAC) || errorAC)}
class:warning={inputAC && warningAC}>
{#if inputAC.length > 0}
<button transition:slide={{axis: 'x'}} on:click={() => link('AC')}>{t('home.linkcard.link')}</button>
{/if}
</label>
<blockquote>{t('home.linkcard.kdx-notice')}</blockquote>
{#if errorAC}
<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}
{#if !inputAC}
<div out:slide={{ duration: 250 }}>
<p>{t('home.linkcard.enter-sn1')}
(<a href="https://play.google.com/store/apps/details?id=com.wakdev.wdnfc">Android</a> /
<a href="https://apps.apple.com/us/app/nfc-tools/id1252962749">Apple</a>)
{t('home.linkcard.enter-sn2')}
</p>
<label>
<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
if (isInput(e) && !/[0-9A-Fa-f:]/.test(e.key)) e.preventDefault()
}}
bind:value={inputSN}
on:input={inputSNChange}
class:error={inputSN && (!inputSNRegex.test(inputSN) || errorSN)}>
{#if inputSN.length > 0}
<button transition:slide={{axis: 'x'}} on:click={() => link('SN')}>{t('home.linkcard.link')}</button>
{/if}
</label>
{#if errorSN}
<p class="error" transition:slide>{errorSN}</p>
{/if}
</div>
{/if}
{#if conflictOld && conflictNew && me}
<div class="overlay" transition:fade>
<div>
<h2>{t('home.linkcard.data-conflict')}</h2>
<p></p>
<div class="conflict-cards">
<div class="old card clickable" on:click={() => linkConflictContinue('old')}
role="button" tabindex="0" on:keydown={e => e.key === "Enter" && linkConflictContinue('old')}>
<span class="type">{t('home.linkcard.account-card')}</span>
<span>{t('home.linkcard.name')}: {conflictOld.name}</span>
<span>{t('home.linkcard.rating')}: {conflictOld.rating}</span>
<span>{t('home.linkcard.last-login')}: {moment(conflictOld.lastLogin).format("YYYY MMM DD")}</span>
<span class="id">{formatLUID(me.ghostCard.luid, true)}</span>
</div>
<div class="new card clickable" on:click={() => linkConflictContinue('new')}
role="button" tabindex="0" on:keydown={e => e.key === "Enter" && linkConflictContinue('new')}>
<span class="type">{cardType(conflictCardID)}</span>
<span>{t('home.linkcard.name')}: {conflictNew.name}</span>
<span>{t('home.linkcard.rating')}: {conflictNew.rating}</span>
<span>{t('home.linkcard.last-login')}: {moment(conflictNew.lastLogin).format("YYYY MMM DD")}</span>
<span class="id">{conflictCardID}</span>
</div>
</div>
<button class="error" on:click={linkConflictCancel}>{t('action.cancel')}</button>
{/each}
</div>
</div>
{/if}
<blockquote class="info">
{t('home.linkcard.card-security-warning')}
</blockquote>
{/if}
<StatusOverlays bind:confirm={showConfirm} bind:error={error} loading={!me} />
</div>
<h2>{t('home.link-card')}</h2>
<p>{t('home.linkcard.enter-info')}</p>
{#if !inputSN}
<div out:slide={{ duration: 250 }}>
<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 bind:this={elemInputAC}
placeholder="Access Code (e.g. 0008 1234 5678 8765 4321)"
on:keydown={(e) => {
e.key === "Enter" && link('AC')
// Ensure key is numeric
if (isInput(e) && !/[\d ]/.test(e.key)) e.preventDefault()
}}
bind:value={inputAC}
on:input={inputACChange}
class:error={inputAC && (!inputACRegex.test(inputAC) || errorAC)}
class:warning={inputAC && warningAC}>
{#if inputAC.length > 0}
<button transition:slide={{axis: 'x'}} on:click={() => link('AC')}>{t('home.linkcard.link')}</button>
{:else}
<button on:click={() => generateRandom()}>Generate</button>
{/if}
</label>
{#if errorAC}
<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}
{#if !inputAC}
<div out:slide={{ duration: 250 }}>
<p>{@html t('home.linkcard.enter-sn')}
</p>
<label>
<input bind:this={inputElemSN}
placeholder="Serial Number (e.g. 01:2E:1A:2B:3C:4D:5E:6F)"
on:keydown={(e) => {
e.key === "Enter" && link('SN')
// Ensure key is hex or colon
if (isInput(e) && !/[0-9A-Fa-f:]/.test(e.key)) e.preventDefault()
}}
bind:value={inputSN}
on:input={inputSNChange}
class:error={inputSN && (!inputSNRegex.test(inputSN) || errorSN)}>
{#if inputSN.length > 0}
<button transition:slide={{axis: 'x'}} on:click={() => link('SN')}>{t('home.linkcard.link')}</button>
{/if}
</label>
{#if errorSN}
<p class="error" transition:slide>{errorSN}</p>
{/if}
</div>
{/if}
{#if conflictOld && conflictNew && me}
<div class="overlay" transition:fade>
<div>
<h2>{t('home.linkcard.data-conflict')}</h2>
<p></p>
<div class="conflict-cards">
<div class="old card clickable" on:click={() => linkConflictContinue('old')}
role="button" tabindex="0" on:keydown={e => e.key === "Enter" && linkConflictContinue('old')}>
<span class="type">{t('home.linkcard.account-card')}</span>
<span>{t('home.linkcard.name')}: {conflictOld.name}</span>
<span>{t('home.linkcard.rating')}: {conflictOld.rating}</span>
<span>{t('home.linkcard.last-login')}: {moment(conflictOld.lastLogin).format("YYYY MMM DD")}</span>
<span class="id">{formatLUID(me.ghostCard.luid, true)}</span>
</div>
<div class="new card clickable" on:click={() => linkConflictContinue('new')}
role="button" tabindex="0" on:keydown={e => e.key === "Enter" && linkConflictContinue('new')}>
<span class="type">{cardType(conflictCardID)}</span>
<span>{t('home.linkcard.name')}: {conflictNew.name}</span>
<span>{t('home.linkcard.rating')}: {conflictNew.rating}</span>
<span>{t('home.linkcard.last-login')}: {moment(conflictNew.lastLogin).format("YYYY MMM DD")}</span>
<span class="id">{conflictCardID}</span>
</div>
</div>
<button class="error" on:click={linkConflictCancel}>{t('action.cancel')}</button>
</div>
</div>
{/if}
</div>
</main>
<StatusOverlays bind:confirm={showConfirm} bind:error={error} loading={!me} />
<style lang="sass">
@use "../../vars"
@@ -425,6 +448,23 @@
right: 10px
bottom: 10px
.id
overflow: hidden
:global(i)
display: inline-block
font-style: normal
transition: 350ms filter
&.ac
:global(i)
margin-right: 0.25em
&:nth-child(n+3)
filter: blur(8px)
&.sn
:global(i):nth-child(n+5)
filter: blur(8px)
&:hover :global(i)
filter: none !important
.conflict-cards
.card
transition: vars.$transition
@@ -437,5 +477,4 @@
.id
opacity: 0.7
</style>

View File

@@ -7,80 +7,105 @@
import { codeToHtml } from 'shiki'
import { AQUA_CONNECTION, DISCORD_INVITE, FADE_IN, FADE_OUT } from "../../libs/config";
import { t } from "../../libs/i18n";
import DashboardTabs from "./DashboardTabs.svelte";
import { patchUserSegatools } from "../../libs/setup";
let user: AquaNetUser
let keychip: string;
let keychipCode: string;
let getStartedRequesting = false;
let exposeKeychip = false;
let automaticSetupStatus: "none" | "success" | "failure" = "none";
USER.me().then((u) => {
user = u;
});
function getStarted() {
if (getStartedRequesting) return;
getStartedRequesting = true;
USER.keychip().then(k => {
getStartedRequesting = false;
keychip = k;
keychip = `${k.slice(0, 4)}-${k.slice(4)}1337`;
codeToHtml(`
[dns]
default=${AQUA_CONNECTION}
[keychip]
enable=1
; ${t('home.setup.keychip-tips')}
id=${keychip.slice(0, 4)}-${keychip.slice(4)}1337`.trim(), {
id=${keychip}`.trim(), {
lang: 'ini',
theme: 'rose-pine',
transformers: []
}).then((html) => {
keychipCode = html;
});
});
});
async function patchSegatools() {
automaticSetupStatus = await patchUserSegatools({ keychip, dns: AQUA_CONNECTION }) ? "success" : "failure";
}
</script>
<div class="setup-instructions">
<h2>{t('home.setup')}</h2>
<p>
{t('home.setup.welcome')}
</p>
<blockquote>
{t('home.setup.blockquote')}
</blockquote>
<main class="content">
<DashboardTabs />
<div class="setup-instructions">
<h2>{t('home.setup')}</h2>
{#if user}
<div transition:slide>
{#if !keychip && !keychipCode}
<div class="no-margin" out:fade={FADE_OUT}>
<button class="emp" on:click={getStarted}>{t('home.setup.get')}</button>
{#if keychip}
<div class="setup-step">
1. <div>{@html t('setup.steps.one')}</div>
</div>
<blockquote class="info">
{t('setup.keychip-warning')}
</blockquote>
<details>
<summary>{t('setup.type.automatic')}</summary>
{@html t('setup.automatic')}
{#if automaticSetupStatus != "none"}
<blockquote class={`keychip-status ${automaticSetupStatus}`}>
{t(`setup.automatic.${automaticSetupStatus}`)}
</blockquote>
{/if}
<div class="setup-btn">
<button on:click={patchSegatools}>{t('setup.automatic.select')}</button>
</div>
{:else}
<div class="no-margin" in:fade={FADE_IN}>
<p>
{t('home.setup.edit')}:
</p>
</details>
<div class="code">
<details>
<summary>{t('setup.type.manual')}</summary>
{@html t('setup.manual')}
<div class="code-container">
<div class="code" class:revealed={exposeKeychip}>
{@html keychipCode}
</div>
<p>
{t('home.setup.test')}
</p>
<p>
{t('home.setup.ask')} <a href={DISCORD_INVITE}>Discord</a> {t('home.setup.support')}.
</p>
{#if !exposeKeychip}
<button class="reveal-btn" on:click={() => exposeKeychip = true}>
{t('setup.reveal-keychip')}
</button>
{/if}
</div>
{/if}
</div>
{:else}
<p>Loading...</p>
{/if}
</div>
</details>
<br>
<div class="setup-step">
2. <div>{@html t('setup.steps.two')}</div>
</div>
<div class="setup-step">
3. <div>{@html t('setup.steps.three')}</div>
</div>
<div class="setup-step">
4. <div>{@html t('setup.steps.four')}</div>
</div>
<p>
{@html t('setup.support-info')}
</p>
{:else}
<p>{t('loading')}</p>
{/if}
</div>
</main>
<style lang="sass">
@use "../../vars"
.code
overflow-x: auto
@@ -100,4 +125,45 @@ id=${keychip.slice(0, 4)}-${keychip.slice(4)}1337`.trim(), {
text-align: right
color: rgba(115,138,148,.4)
.setup-step
display: flex
div
margin-left: 1em
.setup-btn
margin: 0.5em
details
summary
cursor: pointer
font-weight: bold
padding: 0.25em 0
&:open
summary
margin: 0 0 1em 0
.code-container
padding: 10px
position: relative
margin: 1em
overflow: hidden
background: vars.$c-shadow
.code
filter: blur(4px)
transition: 250ms filter
&.revealed
filter: none
:global(.copy)
position: absolute
right: 2em
top: 2em
.reveal-btn
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
</style>