[+] Import player data, tested with maimai

pull/50/head
Clansty 2024-08-01 08:41:11 +08:00
parent 7c4f887ef4
commit a71c2bd8ec
No known key found for this signature in database
GPG Key ID: 3A6BE8BAF2EDE134
5 changed files with 177 additions and 0 deletions

View File

@ -82,6 +82,8 @@ export const EN_REF_HOME = {
'home.join-discord-description': 'Join our Discord server 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.import': 'Import Player Data',
'home.import-description': 'If you are from another server, you can import your data here.',
'home.linkcard.cards': 'Your Cards',
'home.linkcard.description': 'Here are the cards you have linked to your account',
'home.linkcard.account-card': 'Account Card',
@ -109,6 +111,9 @@ export const EN_REF_HOME = {
'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.import.unknown-game': 'Unknown game type',
'home.import.new-data': 'Data to import',
'home.import.data-conflict': 'Proceed will override your current data',
}
export const EN_REF_SETTINGS = {

View File

@ -91,6 +91,8 @@ const zhHome: typeof EN_REF_HOME = {
'home.join-discord-description': '加入我们的 Discord 群,与其他玩家聊天、获取帮助',
'home.setup': '连接到 AquaDX',
'home.setup-description': '如果您有街机框体或者手台,点击这里设置服务器的连接',
'home.import': '导入玩家数据',
'home.import-description': '如果你来自其他在线服,可以点击这里导入从其他服务器导出的数据',
'home.linkcard.cards': "已绑卡片",
'home.linkcard.description': "这些是您绑定到帐户的卡",
'home.linkcard.account-card': "账户卡",
@ -118,6 +120,9 @@ const zhHome: typeof EN_REF_HOME = {
'home.setup.ask': "如果您有任何问题, 请加入我们的",
'home.setup.support': "以获取支持",
'home.setup.keychip-tips': "这是你的狗号, 不要与任何人分享",
'home.import.unknown-game': '未知游戏类型',
'home.import.new-data': '要导入的数据',
'home.import.data-conflict': '继续导入将覆盖现有数据',
}
const zhSettings: typeof EN_REF_SETTINGS = {

View File

@ -303,6 +303,8 @@ export const GAME = {
post(`/api/v2/game/${game}/change-name`, { newName }),
export: (game: GameName): Promise<Record<string, any>> =>
post(`/api/v2/game/${game}/export`),
import: (game: GameName, data: any): Promise<Record<string, any>> =>
post(`/api/v2/game/${game}/import`, {}, { body: JSON.stringify(data) }),
}
export const DATA = {

View File

@ -8,6 +8,7 @@
import StatusOverlays from "../components/StatusOverlays.svelte";
import ActionCard from "../components/ActionCard.svelte";
import { t } from "../libs/i18n";
import ImportDataAction from "./Home/ImportDataAction.svelte";
USER.ensureLoggedIn();
@ -54,6 +55,8 @@
<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}>

View File

@ -0,0 +1,162 @@
<script lang="ts">
import { fade } from "svelte/transition"
import { t } from "../../libs/i18n";
import ActionCard from "../../components/ActionCard.svelte";
import StatusOverlays from "../../components/StatusOverlays.svelte";
import { CARD, GAME, USER } from "../../libs/sdk";
import Icon from "@iconify/svelte";
let load = false;
let error = "";
let conflict: {
oldName: string,
oldRating: number,
newName: string,
newRating: number
} | null;
let confirmAction: (override: boolean) => void;
const startImport = async () => {
const [fileHandle] = await window.showOpenFilePicker({
id: 'aquadx_import',
startIn: 'downloads',
types: [
{
description: "Aqua Player Data",
accept: {
"application/json": [".json"],
},
},
],
});
if (!fileHandle) return;
load = true;
try {
const file = await fileHandle.getFile();
const data = JSON.parse(await file.text()) as any;
const game = getGameByCode(data.gameId);
const me = await USER.me();
const userGames = await CARD.userGames(me.username);
const existed = userGames[game];
if (existed) {
conflict = {
oldName: existed.name,
oldRating: existed.rating,
newName: data.userData.userName,
newRating: data.userData.playerRating
};
if (!await new Promise(resolve => confirmAction = resolve)) {
return;
}
conflict = null;
}
await GAME.import(game, data);
location.href = `/u/${me.username}/${game}`;
} catch (e: any) {
error = e.message;
} finally {
conflict = null;
load = false;
}
}
const getGameByCode = (code: string) => {
switch (code.toUpperCase()) {
case 'SDEZ':
return 'mai2';
case 'SDHD':
return 'chu3';
default:
throw new Error(t('home.import.unknown-game'));
}
}
</script>
<ActionCard color="209, 124, 102" icon="bxs:file-import" on:click={startImport}>
<h3>{t('home.import')}</h3>
<span>{t('home.import-description')}</span>
</ActionCard>
<StatusOverlays {error} loading={load}/>
{#if conflict}
<div class="overlay" transition:fade>
<div>
<h2>{t('home.import.data-conflict')}</h2>
<p></p>
<div class="conflict-cards">
<div class="old card">
<span class="type">{t('home.linkcard.account-card')}</span>
<span>{t('home.linkcard.name')}: {conflict.oldName}</span>
<span>{t('home.linkcard.rating')}: {conflict.oldRating}</span>
<div class="trash">
<Icon icon="ph:trash-duotone"/>
</div>
</div>
<div class="icon">
<Icon icon="icon-park-outline:down"/>
</div>
<div class="new card">
<span class="type">{t('home.import.new-data')}</span>
<span>{t('home.linkcard.name')}: {conflict.newName}</span>
<span>{t('home.linkcard.rating')}: {conflict.newRating}</span>
</div>
</div>
<p></p>
<div class="buttons">
<button on:click={() => confirmAction(false)}>{t('action.cancel')}</button>
<button class="error" on:click={() => confirmAction(true)}>{t('action.confirm')}</button>
</div>
</div>
</div>
{/if}
<style lang="sass">
@import "../../vars"
h3
font-size: 1.3rem
margin: 0
.conflict-cards
display: grid
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr))
gap: 0.5rem
align-items: center
span:not(.type)
font-size: 0.8rem
.old
background: #ff6b6b20
border: 1px solid $c-error
color: #ffffff99
position: relative
.trash
display: flex
position: absolute
bottom: 0.5rem
right: 0.5rem
color: $c-error
opacity: 0.6
font-size: 2rem
.new
background: #646cff20
border: 1px solid $c-darker
.buttons
display: grid
grid-template-columns: 1fr 1fr
gap: 1rem
.icon
display: flex
justify-content: center
font-size: 2rem
</style>