mirror of https://github.com/hykilpikonna/AquaDX
Added UserBox page
parent
c5d81afdf6
commit
f4bb1101bf
|
@ -133,6 +133,17 @@ input
|
|||
transition: $transition
|
||||
box-sizing: border-box
|
||||
|
||||
select
|
||||
border-radius: $border-radius
|
||||
border: 1px solid transparent
|
||||
padding: 0.6em 1.2em
|
||||
font-size: 1em
|
||||
font-weight: 500
|
||||
font-family: inherit
|
||||
background-color: $ov-lighter
|
||||
transition: $transition
|
||||
box-sizing: border-box
|
||||
|
||||
input[type="checkbox"]
|
||||
width: 1.2em
|
||||
height: 1.2em
|
||||
|
|
|
@ -0,0 +1,674 @@
|
|||
<!-- Svelte 4.2.11 -->
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
UserBoxItemKind,
|
||||
type AquaNetUser,
|
||||
type ChangeUserBoxReq,
|
||||
} from "../libs/generalTypes";
|
||||
import { USER, USERBOX } from "../libs/sdk";
|
||||
import { t, ts } from "../libs/i18n";
|
||||
import { DATA_HOST, HAS_USERBOX_ASSETS } from "../libs/config";
|
||||
import { FADE_IN, FADE_OUT } from "../libs/config";
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import StatusOverlays from "./StatusOverlays.svelte";
|
||||
import Icon from "@iconify/svelte";
|
||||
|
||||
let user: AquaNetUser;
|
||||
let aimeId = "";
|
||||
let loading = true;
|
||||
let error = "";
|
||||
let submitting = "";
|
||||
let changed: string[] = [];
|
||||
|
||||
let tab = 0;
|
||||
const tabs = ["chusan", "ongeki", "maimai"];
|
||||
|
||||
// Things that can be changed in the userbox
|
||||
const userBoxFields = [
|
||||
{
|
||||
key: "frame",
|
||||
label: t("userbox.frame"),
|
||||
userBoxKey: "frameId",
|
||||
kind: UserBoxItemKind.frame,
|
||||
},
|
||||
{
|
||||
key: "nameplate",
|
||||
label: t("userbox.nameplate"),
|
||||
userBoxKey: "nameplateId",
|
||||
kind: UserBoxItemKind.nameplate,
|
||||
},
|
||||
{
|
||||
key: "trophy",
|
||||
label: t("userbox.trophy"),
|
||||
userBoxKey: "trophyId",
|
||||
kind: UserBoxItemKind.trophy,
|
||||
},
|
||||
{
|
||||
key: "mapicon",
|
||||
label: t("userbox.mapicon"),
|
||||
userBoxKey: "mapIconId",
|
||||
kind: UserBoxItemKind.mapicon,
|
||||
},
|
||||
{
|
||||
key: "voice",
|
||||
label: t("userbox.voice"),
|
||||
userBoxKey: "voiceId",
|
||||
kind: UserBoxItemKind.sysvoice,
|
||||
},
|
||||
{
|
||||
key: "wear",
|
||||
label: t("userbox.wear"),
|
||||
userBoxKey: "avatarWear",
|
||||
kind: UserBoxItemKind.avatar,
|
||||
},
|
||||
{
|
||||
key: "head",
|
||||
label: t("userbox.head"),
|
||||
userBoxKey: "avatarHead",
|
||||
kind: UserBoxItemKind.avatar,
|
||||
},
|
||||
{
|
||||
key: "face",
|
||||
label: t("userbox.face"),
|
||||
userBoxKey: "avatarHead",
|
||||
kind: UserBoxItemKind.avatar,
|
||||
},
|
||||
{
|
||||
key: "skin",
|
||||
label: t("userbox.skin"),
|
||||
userBoxKey: "avatarSkin",
|
||||
kind: UserBoxItemKind.avatar,
|
||||
},
|
||||
{
|
||||
key: "item",
|
||||
label: t("userbox.item"),
|
||||
userBoxKey: "avatarItem",
|
||||
kind: UserBoxItemKind.avatar,
|
||||
},
|
||||
{
|
||||
key: "front",
|
||||
label: t("userbox.front"),
|
||||
userBoxKey: "avatarFront",
|
||||
kind: UserBoxItemKind.avatar,
|
||||
},
|
||||
{
|
||||
key: "back",
|
||||
label: t("userbox.back"),
|
||||
userBoxKey: "avatarBack",
|
||||
kind: UserBoxItemKind.avatar,
|
||||
},
|
||||
] as const;
|
||||
|
||||
// The different types available (avatar is a special case, as it has multiple categories)
|
||||
const userBoxItems = userBoxFields
|
||||
.map((f) => f.kind)
|
||||
.filter((v, i, a) => a.indexOf(v) === i);
|
||||
|
||||
// Available options for the current user on each field
|
||||
let availableOptions = {
|
||||
nameplate: [],
|
||||
frame: [],
|
||||
trophy: [],
|
||||
mapicon: [],
|
||||
voice: [],
|
||||
wear: [],
|
||||
head: [],
|
||||
face: [],
|
||||
skin: [],
|
||||
item: [],
|
||||
front: [],
|
||||
back: [],
|
||||
} as Record<string, { id: number; label: string }[]>;
|
||||
|
||||
// Current values of the userbox
|
||||
let values = {
|
||||
nameplate: undefined,
|
||||
frame: undefined,
|
||||
trophy: undefined,
|
||||
mapicon: undefined,
|
||||
voice: undefined,
|
||||
wear: undefined,
|
||||
head: undefined,
|
||||
face: undefined,
|
||||
skin: undefined,
|
||||
item: undefined,
|
||||
front: undefined,
|
||||
back: undefined,
|
||||
} as Record<string, number | undefined>;
|
||||
|
||||
function submit(body: ChangeUserBoxReq, field: string) {
|
||||
if (submitting) return;
|
||||
submitting = body.kind;
|
||||
|
||||
USERBOX.setUserBox(body)
|
||||
.then(() => {
|
||||
changed = changed.filter((c) => c !== field);
|
||||
})
|
||||
.catch((e) => {
|
||||
error = e.message;
|
||||
submitting = "";
|
||||
})
|
||||
.finally(() => {
|
||||
submitting = "";
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchData(card: string) {
|
||||
const userId = await USERBOX.getAimeId(card);
|
||||
|
||||
if (!userId) return;
|
||||
|
||||
aimeId = userId.luid;
|
||||
|
||||
const currentValues = await USERBOX.getProfile(userId.luid).catch((e) => {
|
||||
loading = false;
|
||||
error = t("userbox.error.noprofile")
|
||||
return;
|
||||
});
|
||||
|
||||
if(!currentValues) return;
|
||||
|
||||
values = {
|
||||
nameplate: currentValues.nameplateId,
|
||||
frame: currentValues.frameId,
|
||||
trophy: currentValues.trophyId,
|
||||
mapicon: currentValues.mapIconId,
|
||||
voice: currentValues.voiceId,
|
||||
wear: currentValues.avatarWear,
|
||||
head: currentValues.avatarHead,
|
||||
face: currentValues.avatarFace,
|
||||
skin: currentValues.avatarSkin,
|
||||
item: currentValues.avatarItem,
|
||||
front: currentValues.avatarFront,
|
||||
back: currentValues.avatarBack,
|
||||
};
|
||||
|
||||
const itemLabels = await USERBOX.getItemLabels().catch((e) => {
|
||||
loading = false;
|
||||
error = t("userbox.error.nodata")
|
||||
return
|
||||
});
|
||||
|
||||
if(!itemLabels) return;
|
||||
|
||||
await Promise.all(
|
||||
userBoxItems.map(async (kind) => {
|
||||
// Populate info about the items
|
||||
return USERBOX.getUnlockedItems(userId.luid, kind).then((items) => {
|
||||
switch (kind) {
|
||||
case UserBoxItemKind.nameplate:
|
||||
// Add the item id and the label to the available options
|
||||
availableOptions.nameplate = items.map((i) => {
|
||||
return {
|
||||
id: i.itemId,
|
||||
label: itemLabels.nameplate[i.itemId],
|
||||
};
|
||||
});
|
||||
break;
|
||||
case UserBoxItemKind.frame:
|
||||
availableOptions.frame = items.map((i) => {
|
||||
return {
|
||||
id: i.itemId,
|
||||
label: itemLabels.frame[i.itemId],
|
||||
};
|
||||
});
|
||||
break;
|
||||
case UserBoxItemKind.trophy:
|
||||
availableOptions.trophy = items.map((i) => {
|
||||
return {
|
||||
id: i.itemId,
|
||||
label: itemLabels.trophy[i.itemId],
|
||||
};
|
||||
});
|
||||
break;
|
||||
case UserBoxItemKind.mapicon:
|
||||
availableOptions.mapicon = items.map((i) => {
|
||||
return {
|
||||
id: i.itemId,
|
||||
label: itemLabels.mapicon[i.itemId],
|
||||
};
|
||||
});
|
||||
break;
|
||||
case UserBoxItemKind.sysvoice:
|
||||
availableOptions.voice = items.map((i) => {
|
||||
return {
|
||||
id: i.itemId,
|
||||
label: itemLabels.sysvoice[i.itemId],
|
||||
};
|
||||
});
|
||||
break;
|
||||
case UserBoxItemKind.avatar:
|
||||
// Depending of the second number of the item id, we can know the kind of item
|
||||
|
||||
items.forEach((i) => {
|
||||
const kind = i.itemId.toString().split("")[1];
|
||||
switch (kind) {
|
||||
case "1":
|
||||
availableOptions.wear.push({
|
||||
id: i.itemId,
|
||||
label: itemLabels.avatar[i.itemId],
|
||||
});
|
||||
break;
|
||||
case "2":
|
||||
availableOptions.head.push({
|
||||
id: i.itemId,
|
||||
label: itemLabels.avatar[i.itemId],
|
||||
});
|
||||
break;
|
||||
case "3":
|
||||
availableOptions.face.push({
|
||||
id: i.itemId,
|
||||
label: itemLabels.avatar[i.itemId],
|
||||
});
|
||||
break;
|
||||
case "4":
|
||||
availableOptions.skin.push({
|
||||
id: i.itemId,
|
||||
label: itemLabels.avatar[i.itemId],
|
||||
});
|
||||
break;
|
||||
case "5":
|
||||
availableOptions.item.push({
|
||||
id: i.itemId,
|
||||
label: itemLabels.avatar[i.itemId],
|
||||
});
|
||||
break;
|
||||
case "6":
|
||||
availableOptions.front.push({
|
||||
id: i.itemId,
|
||||
label: itemLabels.avatar[i.itemId],
|
||||
});
|
||||
break;
|
||||
case "7":
|
||||
availableOptions.back.push({
|
||||
id: i.itemId,
|
||||
label: itemLabels.avatar[i.itemId],
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
loading = false;
|
||||
}
|
||||
|
||||
function generateBodyFromKind(
|
||||
kind:
|
||||
| "frame"
|
||||
| "nameplate"
|
||||
| "trophy"
|
||||
| "mapicon"
|
||||
| "voice"
|
||||
| "wear"
|
||||
| "head"
|
||||
| "face"
|
||||
| "skin"
|
||||
| "item"
|
||||
| "front"
|
||||
| "back",
|
||||
value: number,
|
||||
aimeId: string,
|
||||
): ChangeUserBoxReq {
|
||||
switch (kind) {
|
||||
case "frame":
|
||||
return { kind: "frame", frameId: value, aimeId };
|
||||
case "nameplate":
|
||||
return { kind: "plate", nameplateId: value, aimeId };
|
||||
case "trophy":
|
||||
return { kind: "trophy", trophyId: value, aimeId };
|
||||
case "mapicon":
|
||||
return { kind: "mapicon", mapiconid: value, aimeId };
|
||||
case "voice":
|
||||
return { kind: "sysvoice", voiceId: value, aimeId };
|
||||
case "wear":
|
||||
return { kind: "avatar", accId: value, category: 1, aimeId };
|
||||
case "head":
|
||||
return { kind: "avatar", accId: value, category: 2, aimeId };
|
||||
case "face":
|
||||
return { kind: "avatar", accId: value, category: 3, aimeId };
|
||||
case "skin":
|
||||
return { kind: "avatar", accId: value, category: 4, aimeId };
|
||||
case "item":
|
||||
return { kind: "avatar", accId: value, category: 5, aimeId };
|
||||
case "front":
|
||||
return { kind: "avatar", accId: value, category: 6, aimeId };
|
||||
case "back":
|
||||
return { kind: "avatar", accId: value, category: 7, aimeId };
|
||||
}
|
||||
}
|
||||
|
||||
USER.me().then((u) => {
|
||||
if (u) {
|
||||
user = u;
|
||||
const card = user.cards.length > 0 ? user.cards[0].luid : "";
|
||||
|
||||
if (card) {
|
||||
fetchData(card);
|
||||
} else {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !loading && !error}
|
||||
<div class="outer-container">
|
||||
<nav>
|
||||
{#each tabs as tabName, i}
|
||||
<div
|
||||
transition:slide={{ axis: "x" }}
|
||||
class:active={tab === i}
|
||||
on:click={() => {
|
||||
tab = i;
|
||||
|
||||
// Set url params
|
||||
window.history.pushState({}, "", `/settings?tab=${tab}`);
|
||||
}}
|
||||
on:keydown={(e) => e.key === "Enter" && (tab = i)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
{ts(`userbox.tabs.${tabName}`)}
|
||||
</div>
|
||||
{/each}
|
||||
</nav>
|
||||
{#if tab === 0}
|
||||
<div class="container" out:fade={FADE_OUT} in:fade={FADE_IN}>
|
||||
<div class="fields">
|
||||
{#each userBoxFields as { key, label, kind }, i (key)}
|
||||
<div class="field">
|
||||
<label for={key}>{label}</label>
|
||||
<div>
|
||||
<select
|
||||
bind:value={values[key]}
|
||||
id={key}
|
||||
on:change={() => {
|
||||
changed = [...changed, key];
|
||||
}}
|
||||
>
|
||||
{#each availableOptions[key] as option (option)}
|
||||
<option value={option.id}
|
||||
>{option.label ||
|
||||
`${key} ${option.id.toString()}`}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
{#if changed.includes(key)}
|
||||
<button
|
||||
transition:slide={{ axis: "x" }}
|
||||
on:click={() => {
|
||||
const newValue = values[key];
|
||||
|
||||
if (newValue === undefined) return;
|
||||
|
||||
submit(generateBodyFromKind(key, newValue, aimeId), key);
|
||||
}}
|
||||
>
|
||||
{#if submitting === key}
|
||||
<Icon icon="line-md:loading-twotone-loop" />
|
||||
{:else}
|
||||
{t("settings.profile.save")}
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if HAS_USERBOX_ASSETS}
|
||||
<div class="preview">
|
||||
<h2>{t("userbox.preview.ui")}</h2>
|
||||
<!-- Frame -->
|
||||
{#if values.frame}
|
||||
<img
|
||||
src={`${DATA_HOST}/d/chu3/frame/${values.frame}.png`}
|
||||
alt="Preview"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="secondrow">
|
||||
<!-- Map Icon -->
|
||||
{#if values.mapicon}
|
||||
<div class="mapicon">
|
||||
<img
|
||||
src={`${DATA_HOST}/d/chu3/mapicon/${values.mapicon}.png`}
|
||||
alt="Preview"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- System voice -->
|
||||
{#if values.voice}
|
||||
<div>
|
||||
<img
|
||||
src={`${DATA_HOST}/d/chu3/systemVoice/${values.voice}.png`}
|
||||
alt="Preview"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<h2>{t("userbox.preview.nameplate")}</h2>
|
||||
<!-- Nameplate -->
|
||||
{#if values.nameplate}
|
||||
<div class="nameplate">
|
||||
<img
|
||||
src={`${DATA_HOST}/d/chu3/nameplate/${values.nameplate}.png`}
|
||||
alt="Preview"
|
||||
/>
|
||||
<p class="trophy">
|
||||
{availableOptions.trophy.find((x) => x.id === values.trophy)
|
||||
?.label}
|
||||
</p>
|
||||
<div class="username">
|
||||
<p>
|
||||
{user.displayName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h2>{t("userbox.preview.avatar")}</h2>
|
||||
<div class="avatar">
|
||||
<div>
|
||||
<img
|
||||
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.wear}.png`}
|
||||
alt="Preview"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.head}.png`}
|
||||
alt="Preview"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.face}.png`}
|
||||
alt="Preview"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.skin}.png`}
|
||||
alt="Preview"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.item}.png`}
|
||||
alt="Preview"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.front}.png`}
|
||||
alt="Preview"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src={`${DATA_HOST}/d/chu3/avatarAccessory/${values.back}.png`}
|
||||
alt="Preview"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<p>WIP</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<StatusOverlays {error} {loading} />
|
||||
|
||||
<style lang="sass">
|
||||
@import "../vars"
|
||||
|
||||
.outer-container
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
|
||||
nav
|
||||
display: flex
|
||||
gap: 1rem
|
||||
|
||||
div
|
||||
padding: 0.5rem 1rem
|
||||
border-radius: 0.4rem
|
||||
cursor: pointer
|
||||
transition: background-color 0.2s
|
||||
font-weight: 500
|
||||
|
||||
&.active
|
||||
color: $c-main
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: auto
|
||||
|
||||
.container
|
||||
display: flex
|
||||
flex-direction: row
|
||||
gap: 3rem
|
||||
|
||||
@media (max-width: $w-max)
|
||||
flex-direction: column
|
||||
|
||||
.preview
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1rem
|
||||
width: 50%
|
||||
|
||||
@media (max-width: $w-max)
|
||||
width: 100%
|
||||
|
||||
.secondrow
|
||||
display: flex
|
||||
gap: 1rem
|
||||
justify-content: space-between
|
||||
|
||||
div
|
||||
width: 40%
|
||||
flex-grow:0
|
||||
|
||||
.avatar
|
||||
display: grid
|
||||
grid-template-columns: repeat(3, 1fr)
|
||||
grid-template-rows: repeat(3, 1fr)
|
||||
gap: 1rem
|
||||
|
||||
div
|
||||
border: 1px solid white
|
||||
aspect-ratio:1
|
||||
overflow: hidden
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
border-radius: 0.4rem
|
||||
|
||||
img
|
||||
width: auto
|
||||
height: 100%
|
||||
|
||||
|
||||
.nameplate
|
||||
position: relative
|
||||
width: 400px
|
||||
|
||||
> .trophy
|
||||
position: absolute
|
||||
top: 10px
|
||||
left: 150px
|
||||
width: 220px
|
||||
background-color: #fff
|
||||
color: black
|
||||
border-radius: 0.4rem
|
||||
text-align: center
|
||||
font-weight:500
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5)
|
||||
font-size: .8rem
|
||||
|
||||
> .username
|
||||
position: absolute
|
||||
top: 50px
|
||||
left: 150px
|
||||
width: 220px
|
||||
height: 50px
|
||||
background-color: #fff
|
||||
color: black
|
||||
border-radius: 0.2rem
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5)
|
||||
display: flex
|
||||
|
||||
> p
|
||||
margin: .2rem .5rem
|
||||
font-size: 1.2rem
|
||||
|
||||
.mapicon
|
||||
height: 100px
|
||||
display: flex
|
||||
justify-content: center
|
||||
|
||||
img
|
||||
width: auto
|
||||
height: 100%
|
||||
|
||||
.fields
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 12px
|
||||
width: 100%
|
||||
flex-grow: 0
|
||||
|
||||
label
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
.field
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
label
|
||||
max-width: max-content
|
||||
|
||||
> div:not(.bool)
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 1rem
|
||||
margin-top: 0.5rem
|
||||
|
||||
> select
|
||||
flex: 1
|
||||
|
||||
</style>
|
|
@ -11,5 +11,7 @@ export const DISCORD_INVITE = 'https://discord.gg/FNgveqFF7s'
|
|||
// UI
|
||||
export const FADE_OUT = { duration: 200 }
|
||||
export const FADE_IN = { delay: 400 }
|
||||
export const DEFAULT_PFP = "/assets/imgs/no_profile.png"
|
||||
export const DEFAULT_PFP = '/assets/imgs/no_profile.png'
|
||||
|
||||
// USERBOX_ASSETS
|
||||
export const HAS_USERBOX_ASSETS = true
|
|
@ -118,5 +118,68 @@ export type AllMusic = { [key: string]: MusicMeta }
|
|||
export interface GameOption {
|
||||
key: string
|
||||
value: any
|
||||
type: "Boolean"
|
||||
type: 'Boolean'
|
||||
}
|
||||
|
||||
export interface UserBox {
|
||||
userName:string,
|
||||
level:number,
|
||||
exp:string,
|
||||
point:number,
|
||||
totalPoint:number,
|
||||
playerRating:number,
|
||||
highestRating:number,
|
||||
nameplateId:number,
|
||||
frameId:number,
|
||||
characterId:number,
|
||||
trophyId:number,
|
||||
totalMapNum:number,
|
||||
totalHiScore: number,
|
||||
totalBasicHighScore:number,
|
||||
totalAdvancedHighScore:number,
|
||||
totalExpertHighScore:number,
|
||||
totalMasterHighScore:number,
|
||||
totalUltimaHighScore:number,
|
||||
friendCount:number,
|
||||
firstPlayDate:Date,
|
||||
lastPlayDate:Date,
|
||||
courseClass:number,
|
||||
overPowerPoint:number,
|
||||
overPowerRate:number,
|
||||
mapIconId:number,
|
||||
voiceId:number,
|
||||
avatarWear: number,
|
||||
avatarHead: number,
|
||||
avatarFace: number,
|
||||
avatarSkin: number,
|
||||
avatarItem: number,
|
||||
avatarFront: number,
|
||||
avatarBack: number,
|
||||
}
|
||||
|
||||
// Assign a number to each kind of user box item with an enum
|
||||
export enum UserBoxItemKind {
|
||||
nameplate = 1,
|
||||
frame = 2,
|
||||
trophy = 3,
|
||||
mapicon = 8,
|
||||
sysvoice = 9,
|
||||
avatar = 11,
|
||||
}
|
||||
|
||||
// Define type only with the keys
|
||||
export type UserBoxItemKindStr = keyof typeof UserBoxItemKind;
|
||||
|
||||
type ChangePlateReq = {kind:'plate', nameplateId:number}
|
||||
type ChangeFrameReq = {kind:'frame', frameId:number}
|
||||
type ChangeTrophyReq = {kind:'trophy',trophyId:number}
|
||||
type ChangeMapIconReq = {kind:'mapicon',mapiconid:number}
|
||||
type ChangeVoiceReq = {kind:'sysvoice',voiceId:number}
|
||||
type ChangeAvatarReq = {
|
||||
kind:'avatar',
|
||||
accId:number,
|
||||
category:number
|
||||
}
|
||||
|
||||
export type ChangeUserBoxReq = {aimeId:string} & (ChangePlateReq | ChangeFrameReq | ChangeTrophyReq | ChangeMapIconReq | ChangeVoiceReq | ChangeAvatarReq);
|
||||
|
||||
|
|
|
@ -17,13 +17,13 @@ export const EN_REF_USER = {
|
|||
'UserHome.Version': 'Last Version',
|
||||
'UserHome.RecentScores': 'Recent Scores',
|
||||
'UserHome.NoData': 'No data in the past ${days} days',
|
||||
'UserHome.UnknownSong': "(unknown song)",
|
||||
'UserHome.UnknownSong': '(unknown song)',
|
||||
'UserHome.Settings': 'Settings',
|
||||
'UserHome.NoValidGame': "The user hasn't played any game yet.",
|
||||
'UserHome.ShowRanksDetails': "Click to show details",
|
||||
'UserHome.NoValidGame': 'The user hasn\'t played any game yet.',
|
||||
'UserHome.ShowRanksDetails': 'Click to show details',
|
||||
'UserHome.RankDetail.Title': 'Achievement Details',
|
||||
'UserHome.RankDetail.Level': "Level",
|
||||
'UserHome.B50': "B50",
|
||||
'UserHome.RankDetail.Level': 'Level',
|
||||
'UserHome.B50': 'B50',
|
||||
}
|
||||
|
||||
export const EN_REF_Welcome = {
|
||||
|
@ -57,11 +57,11 @@ export const EN_REF_LEADERBOARD = {
|
|||
}
|
||||
|
||||
export const EN_REF_GENERAL = {
|
||||
'game.mai2': "Mai",
|
||||
'game.chu3': "Chuni",
|
||||
'game.ongeki': "Ongeki",
|
||||
'game.wacca': "Wacca",
|
||||
'status.error': "Error",
|
||||
'game.mai2': 'Mai',
|
||||
'game.chu3': 'Chuni',
|
||||
'game.ongeki': 'Ongeki',
|
||||
'game.wacca': 'Wacca',
|
||||
'status.error': 'Error',
|
||||
'status.error.hint': 'Something went wrong, please try again later or ',
|
||||
'status.error.hint.link': 'join our discord for support.',
|
||||
'status.detail': 'Detail: ${detail}',
|
||||
|
@ -82,39 +82,40 @@ 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.linkcard.cards': "Your Cards",
|
||||
'home.linkcard.description': "Here are the cards you have linked to your account",
|
||||
'home.linkcard.account-card': "Account Card",
|
||||
'home.linkcard.registered': "Registered",
|
||||
'home.linkcard.lastused': "Last used",
|
||||
'home.linkcard.enter-info': "Please enter the following information",
|
||||
'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.link': "Link",
|
||||
'home.linkcard.data-conflict': "Data Conflict",
|
||||
'home.linkcard.name': "Name",
|
||||
'home.linkcard.rating': "Rating",
|
||||
'home.linkcard.last-login': "Last Login",
|
||||
'home.linkcard.linked-own': "This card is already linked to your account",
|
||||
'home.linkcard.linked-another': "This card is already linked to another account",
|
||||
'home.linkcard.notfound': "Card not found",
|
||||
'home.linkcard.unlink': "Unlink Card",
|
||||
'home.linkcard.unlink-notice': "Are you sure you want to unlink this card?",
|
||||
'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.cards': 'Your Cards',
|
||||
'home.linkcard.description': 'Here are the cards you have linked to your account',
|
||||
'home.linkcard.account-card': 'Account Card',
|
||||
'home.linkcard.registered': 'Registered',
|
||||
'home.linkcard.lastused': 'Last used',
|
||||
'home.linkcard.enter-info': 'Please enter the following information',
|
||||
'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.link': 'Link',
|
||||
'home.linkcard.data-conflict': 'Data Conflict',
|
||||
'home.linkcard.name': 'Name',
|
||||
'home.linkcard.rating': 'Rating',
|
||||
'home.linkcard.last-login': 'Last Login',
|
||||
'home.linkcard.linked-own': 'This card is already linked to your account',
|
||||
'home.linkcard.linked-another': 'This card is already linked to another account',
|
||||
'home.linkcard.notfound': 'Card not found',
|
||||
'home.linkcard.unlink': 'Unlink Card',
|
||||
'home.linkcard.unlink-notice': 'Are you sure you want to unlink this card?',
|
||||
'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',
|
||||
}
|
||||
|
||||
export const EN_REF_SETTINGS = {
|
||||
'settings.title': 'Settings',
|
||||
'settings.tabs.profile': 'Profile',
|
||||
'settings.tabs.game': 'Game',
|
||||
'settings.tabs.userbox': 'Userbox',
|
||||
'settings.fields.unlockMusic.name': 'Unlock All Music',
|
||||
'settings.fields.unlockMusic.desc': 'Unlock all music and master difficulty in game.',
|
||||
'settings.fields.unlockChara.name': 'Unlock All Characters',
|
||||
|
@ -140,7 +141,30 @@ export const EN_REF_SETTINGS = {
|
|||
'settings.profile.unchanged': 'Unchanged',
|
||||
}
|
||||
|
||||
export const EN_REF_USERBOX = {
|
||||
'userbox.tabs.chusan':'Chuni',
|
||||
'userbox.tabs.maimai':'Mai (WIP)',
|
||||
'userbox.tabs.ongeki':'Ongeki (WIP)',
|
||||
'userbox.nameplate': 'Nameplate',
|
||||
'userbox.frame': 'Frame',
|
||||
'userbox.trophy': 'Trophy (Title)',
|
||||
'userbox.mapicon': 'Map Icon',
|
||||
'userbox.voice':'System Voice',
|
||||
'userbox.wear':'Avatar Wear',
|
||||
'userbox.head':'Avatar Head',
|
||||
'userbox.face':'Avatar Face',
|
||||
'userbox.skin':'Avatar Skin',
|
||||
'userbox.item':'Avatar Item',
|
||||
'userbox.front':'Avatar Front',
|
||||
'userbox.back':'Avatar Back',
|
||||
'userbox.preview.avatar':'Avatar Preview',
|
||||
'userbox.preview.nameplate':'Nameplate Preview',
|
||||
'userbox.preview.ui':'Interface Preview',
|
||||
'userbox.error.noprofile':'No profile was found for this game',
|
||||
'userbox.error.nodata':'No data was found for this game',
|
||||
}
|
||||
|
||||
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,
|
||||
...EN_REF_LEADERBOARD, ...EN_REF_HOME, ...EN_REF_SETTINGS }
|
||||
...EN_REF_LEADERBOARD, ...EN_REF_HOME, ...EN_REF_SETTINGS, ...EN_REF_USERBOX }
|
||||
|
||||
export type LocalizedMessages = typeof EN_REF
|
||||
|
|
|
@ -1,162 +1,315 @@
|
|||
import { AQUA_HOST, DATA_HOST } from "./config";
|
||||
import type {
|
||||
AllMusic,
|
||||
Card,
|
||||
CardSummary,
|
||||
GenericGameSummary,
|
||||
GenericRanking,
|
||||
TrendEntry,
|
||||
AquaNetUser, GameOption
|
||||
} from "./generalTypes";
|
||||
import type { GameName } from "./scoring";
|
||||
|
||||
interface RequestInitWithParams extends RequestInit {
|
||||
params?: { [index: string]: string }
|
||||
localCache?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a fetch url
|
||||
*
|
||||
* @param input Fetch url input
|
||||
* @param callback Callback for modification
|
||||
*/
|
||||
export function reconstructUrl(input: URL | RequestInfo, callback: (url: URL) => URL | void): RequestInfo | URL {
|
||||
let u = new URL((input instanceof Request) ? input.url : input);
|
||||
const result = callback(u)
|
||||
if (result) u = result
|
||||
if (input instanceof Request) {
|
||||
// @ts-ignore
|
||||
return { url: u, ...input }
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with url parameters
|
||||
*/
|
||||
export function fetchWithParams(input: URL | RequestInfo, init?: RequestInitWithParams): Promise<Response> {
|
||||
return fetch(reconstructUrl(input, u => {
|
||||
u.search = new URLSearchParams(init?.params ?? {}).toString()
|
||||
}), init)
|
||||
}
|
||||
|
||||
let cache: { [index: string]: any } = {}
|
||||
|
||||
export async function post(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
|
||||
// Add token if exists
|
||||
const token = localStorage.getItem('token')
|
||||
if (token && !('token' in params)) params = { ...(params ?? {}), token }
|
||||
|
||||
if (init?.localCache) {
|
||||
const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)]
|
||||
if (cached) return cached
|
||||
}
|
||||
|
||||
let res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'POST',
|
||||
params,
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
const ret = res.json()
|
||||
cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* aqua.net.UserRegistrar
|
||||
*
|
||||
* @param user
|
||||
*/
|
||||
async function register(user: { username: string, email: string, password: string, turnstile: string }) {
|
||||
return await post('/api/v2/user/register', user)
|
||||
}
|
||||
async function login(user: { email: string, password: string, turnstile: string }) {
|
||||
const data = await post('/api/v2/user/login', user)
|
||||
|
||||
// Put token into local storage
|
||||
localStorage.setItem('token', data.token)
|
||||
}
|
||||
|
||||
const isLoggedIn = () => !!localStorage.getItem('token')
|
||||
const ensureLoggedIn = () => !isLoggedIn() && (window.location.href = '/')
|
||||
|
||||
export const USER = {
|
||||
register,
|
||||
login,
|
||||
confirmEmail: (token: string) =>
|
||||
post('/api/v2/user/confirm-email', { token }),
|
||||
me: (): Promise<AquaNetUser> => {
|
||||
ensureLoggedIn()
|
||||
return post('/api/v2/user/me', {})
|
||||
},
|
||||
keychip: (): Promise<string> =>
|
||||
post('/api/v2/user/keychip', {}).then(it => it.keychip),
|
||||
setting: (key: string, value: string) =>
|
||||
post('/api/v2/user/setting', { key: key === 'password' ? 'pwHash' : key, value }),
|
||||
uploadPfp: (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return post('/api/v2/user/upload-pfp', { }, { method: 'POST', body: formData })
|
||||
},
|
||||
isLoggedIn,
|
||||
ensureLoggedIn,
|
||||
}
|
||||
|
||||
export const CARD = {
|
||||
summary: (cardId: string): Promise<{card: Card, summary: CardSummary}> =>
|
||||
post('/api/v2/card/summary', { cardId }),
|
||||
link: (props: { cardId: string, migrate: string }) =>
|
||||
post('/api/v2/card/link', props),
|
||||
unlink: (cardId: string) =>
|
||||
post('/api/v2/card/unlink', { cardId }),
|
||||
userGames: (username: string): Promise<CardSummary> =>
|
||||
post('/api/v2/card/user-games', { username }),
|
||||
}
|
||||
|
||||
export const GAME = {
|
||||
trend: (username: string, game: GameName): Promise<TrendEntry[]> =>
|
||||
post(`/api/v2/game/${game}/trend`, { username }),
|
||||
userSummary: (username: string, game: GameName): Promise<GenericGameSummary> =>
|
||||
post(`/api/v2/game/${game}/user-summary`, { username }),
|
||||
ranking: (game: GameName): Promise<GenericRanking[]> =>
|
||||
post(`/api/v2/game/${game}/ranking`, { }),
|
||||
|
||||
}
|
||||
|
||||
export const DATA = {
|
||||
allMusic: (game: GameName): Promise<AllMusic> =>
|
||||
fetch(`${DATA_HOST}/d/${game}/00/all-music.json`).then(it => it.json())
|
||||
}
|
||||
|
||||
export const SETTING = {
|
||||
get: (): Promise<GameOption[]> =>
|
||||
post('/api/v2/settings/get', {}),
|
||||
set: (key: string, value: any) =>
|
||||
post('/api/v2/settings/set', { key, value: `${value}` }),
|
||||
}
|
||||
import { AQUA_HOST, DATA_HOST } from './config'
|
||||
import type {
|
||||
AllMusic,
|
||||
Card,
|
||||
CardSummary,
|
||||
GenericGameSummary,
|
||||
GenericRanking,
|
||||
TrendEntry,
|
||||
AquaNetUser, GameOption,
|
||||
UserBox,
|
||||
ChangeUserBoxReq,
|
||||
UserBoxItemKind
|
||||
} from './generalTypes'
|
||||
import type { GameName } from './scoring'
|
||||
|
||||
interface RequestInitWithParams extends RequestInit {
|
||||
params?: { [index: string]: string }
|
||||
localCache?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a fetch url
|
||||
*
|
||||
* @param input Fetch url input
|
||||
* @param callback Callback for modification
|
||||
*/
|
||||
export function reconstructUrl(input: URL | RequestInfo, callback: (url: URL) => URL | void): RequestInfo | URL {
|
||||
let u = new URL((input instanceof Request) ? input.url : input)
|
||||
const result = callback(u)
|
||||
if (result) u = result
|
||||
if (input instanceof Request) {
|
||||
// @ts-ignore
|
||||
return { url: u, ...input }
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with url parameters
|
||||
*/
|
||||
export function fetchWithParams(input: URL | RequestInfo, init?: RequestInitWithParams): Promise<Response> {
|
||||
return fetch(reconstructUrl(input, u => {
|
||||
u.search = new URLSearchParams(init?.params ?? {}).toString()
|
||||
}), init)
|
||||
}
|
||||
|
||||
const cache: { [index: string]: any } = {}
|
||||
|
||||
export async function post(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
|
||||
// Add token if exists
|
||||
const token = localStorage.getItem('token')
|
||||
if (token && !('token' in params)) params = { ...(params ?? {}), token }
|
||||
|
||||
if (init?.localCache) {
|
||||
const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)]
|
||||
if (cached) return cached
|
||||
}
|
||||
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'POST',
|
||||
params,
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
const ret = res.json()
|
||||
cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export async function get(endpoint: string, params:any,init?: RequestInitWithParams): Promise<any> {
|
||||
// Add token if exists
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (init?.localCache) {
|
||||
const cached = cache[endpoint + JSON.stringify(init)]
|
||||
if (cached) return cached
|
||||
}
|
||||
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'GET',
|
||||
params,
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
const ret = res.json()
|
||||
cache[endpoint + JSON.stringify(init)] = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export async function put(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
|
||||
// Add token if exists
|
||||
const token = localStorage.getItem('token')
|
||||
if (token && !('token' in params)) params = { ...(params ?? {}), token }
|
||||
|
||||
if (init?.localCache) {
|
||||
const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)]
|
||||
if (cached) return cached
|
||||
}
|
||||
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(params),
|
||||
headers:{
|
||||
'Content-Type':'application/json',
|
||||
...init?.headers
|
||||
},
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
const ret = res.json()
|
||||
cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
export async function realPost(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
|
||||
const res = await fetchWithParams(AQUA_HOST + endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
headers:{
|
||||
'Content-Type':'application/json',
|
||||
...init?.headers
|
||||
},
|
||||
...init
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
throw new Error('Network error')
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
console.error(`${res.status}: ${text}`)
|
||||
|
||||
// If 400 invalid token is caught, should invalidate the token and redirect to signin
|
||||
if (text === 'Invalid token') {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/'
|
||||
}
|
||||
|
||||
// Try to parse as json
|
||||
let json
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch (e) {
|
||||
throw new Error(text)
|
||||
}
|
||||
if (json.error) throw new Error(json.error)
|
||||
}
|
||||
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* aqua.net.UserRegistrar
|
||||
*
|
||||
* @param user
|
||||
*/
|
||||
async function register(user: { username: string, email: string, password: string, turnstile: string }) {
|
||||
return await post('/api/v2/user/register', user)
|
||||
}
|
||||
async function login(user: { email: string, password: string, turnstile: string }) {
|
||||
const data = await post('/api/v2/user/login', user)
|
||||
|
||||
// Put token into local storage
|
||||
localStorage.setItem('token', data.token)
|
||||
}
|
||||
|
||||
const isLoggedIn = () => !!localStorage.getItem('token')
|
||||
const ensureLoggedIn = () => !isLoggedIn() && (window.location.href = '/')
|
||||
|
||||
export const USER = {
|
||||
register,
|
||||
login,
|
||||
confirmEmail: (token: string) =>
|
||||
post('/api/v2/user/confirm-email', { token }),
|
||||
me: (): Promise<AquaNetUser> => {
|
||||
ensureLoggedIn()
|
||||
return post('/api/v2/user/me', {})
|
||||
},
|
||||
keychip: (): Promise<string> =>
|
||||
post('/api/v2/user/keychip', {}).then(it => it.keychip),
|
||||
setting: (key: string, value: string) =>
|
||||
post('/api/v2/user/setting', { key: key === 'password' ? 'pwHash' : key, value }),
|
||||
uploadPfp: (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return post('/api/v2/user/upload-pfp', { }, { method: 'POST', body: formData })
|
||||
},
|
||||
isLoggedIn,
|
||||
ensureLoggedIn,
|
||||
}
|
||||
|
||||
export const USERBOX = {
|
||||
getAimeId:(cardId:string):Promise<{luid:string}|null> =>realPost('/api/sega/aime/getByAccessCode',{ accessCode:cardId }),
|
||||
getProfile:(aimeId:string):Promise<UserBox> =>get('/api/game/chuni/v2/profile',{ aimeId }),
|
||||
getUnlockedItems:(aimeId:string, itemId: UserBoxItemKind):Promise<{itemKind:number, itemId:number,stock:number,isValid:boolean}[]> =>
|
||||
get(`/api/game/chuni/v2/item/${itemId}`,{ aimeId }),
|
||||
getItemLabels:() => {
|
||||
const kinds = [ 'nameplate', 'frame', 'trophy', 'mapicon', 'sysvoice', 'avatar' ]
|
||||
|
||||
return Promise.all(kinds.map(it =>
|
||||
get(`/api/game/chuni/v2/data/${it}`,{}).then((res:{id:number,name:string}[]) =>
|
||||
// Use the id as the key
|
||||
res.reduce((acc, cur) => ({ ...acc, [cur.id]: cur.name }), {}) as { [index: number]: string }
|
||||
))).then(([ nameplate, frame, trophy, mapicon, sysvoice, avatar ]) => ({
|
||||
nameplate, frame, trophy, mapicon, sysvoice, avatar
|
||||
}))
|
||||
},
|
||||
setUserBox:({ kind,...body }:ChangeUserBoxReq) =>
|
||||
put(`/api/game/chuni/v2/profile/${kind}`, body),
|
||||
}
|
||||
|
||||
export const CARD = {
|
||||
summary: (cardId: string): Promise<{card: Card, summary: CardSummary}> =>
|
||||
post('/api/v2/card/summary', { cardId }),
|
||||
link: (props: { cardId: string, migrate: string }) =>
|
||||
post('/api/v2/card/link', props),
|
||||
unlink: (cardId: string) =>
|
||||
post('/api/v2/card/unlink', { cardId }),
|
||||
userGames: (username: string): Promise<CardSummary> =>
|
||||
post('/api/v2/card/user-games', { username }),
|
||||
}
|
||||
|
||||
export const GAME = {
|
||||
trend: (username: string, game: GameName): Promise<TrendEntry[]> =>
|
||||
post(`/api/v2/game/${game}/trend`, { username }),
|
||||
userSummary: (username: string, game: GameName): Promise<GenericGameSummary> =>
|
||||
post(`/api/v2/game/${game}/user-summary`, { username }),
|
||||
ranking: (game: GameName): Promise<GenericRanking[]> =>
|
||||
post(`/api/v2/game/${game}/ranking`, { }),
|
||||
|
||||
}
|
||||
|
||||
export const DATA = {
|
||||
allMusic: (game: GameName): Promise<AllMusic> =>
|
||||
fetch(`${DATA_HOST}/d/${game}/00/all-music.json`).then(it => it.json())
|
||||
}
|
||||
|
||||
export const SETTING = {
|
||||
get: (): Promise<GameOption[]> =>
|
||||
post('/api/v2/settings/get', {}),
|
||||
set: (key: string, value: any) =>
|
||||
post('/api/v2/settings/set', { key, value: `${value}` }),
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { pfp } from "../../libs/ui";
|
||||
import { t, ts } from "../../libs/i18n";
|
||||
import { FADE_IN, FADE_OUT } from "../../libs/config";
|
||||
import UserBox from "../../components/UserBox.svelte";
|
||||
|
||||
USER.ensureLoggedIn()
|
||||
|
||||
|
@ -16,7 +17,7 @@
|
|||
let error: string;
|
||||
let submitting = ""
|
||||
let tab = 0
|
||||
const tabs = [ 'profile', 'game' ]
|
||||
const tabs = [ 'profile', 'game', 'userbox']
|
||||
|
||||
const profileFields = [
|
||||
[ 'displayName', t('settings.profile.name') ],
|
||||
|
@ -145,6 +146,9 @@
|
|||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if tab === 2}
|
||||
<!-- Tab 2: Userbox settings -->
|
||||
<UserBox />
|
||||
{/if}
|
||||
|
||||
<StatusOverlays {error} loading={!me || !!submitting} />
|
||||
|
|
Loading…
Reference in New Issue