mirror of https://github.com/hykilpikonna/AquaDX
[O] Rewrite userbox
parent
6cd18ba7f7
commit
b1de430f0b
|
@ -1,552 +0,0 @@
|
||||||
<!-- Svelte 4.2.11 -->
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
UserBoxItemKind,
|
|
||||||
type AquaNetUser,
|
|
||||||
} 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, slide } from "svelte/transition";
|
|
||||||
import StatusOverlays from "./StatusOverlays.svelte";
|
|
||||||
import Icon from "@iconify/svelte";
|
|
||||||
|
|
||||||
let user: AquaNetUser;
|
|
||||||
let loading = true;
|
|
||||||
let error = "";
|
|
||||||
let submitting = "";
|
|
||||||
let changed: string[] = [];
|
|
||||||
|
|
||||||
// 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(obj: { field: string; value: string }) {
|
|
||||||
if (submitting) return
|
|
||||||
submitting = obj.field
|
|
||||||
|
|
||||||
USERBOX.setUserBox(obj)
|
|
||||||
.then(() => changed = changed.filter((c) => c !== obj.field))
|
|
||||||
.catch(e => error = e.message)
|
|
||||||
.finally(() => submitting = "")
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchData() {
|
|
||||||
const currentValues = await USERBOX.getProfile().catch((e) => {
|
|
||||||
loading = false;
|
|
||||||
error = t("userbox.error.noprofile")
|
|
||||||
})
|
|
||||||
|
|
||||||
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(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const kindMap: Record<string, string> =
|
|
||||||
{ nameplate: "nameplateId", frame: "frameId", trophy: "trophyId", mapicon: "mapIconId", voice: "voiceId" }
|
|
||||||
const categories = ["wear", "head", "face", "skin", "item", "front", "back"]
|
|
||||||
function generateBodyFromKind(
|
|
||||||
kind: "frame" | "nameplate" | "trophy" | "mapicon" | "voice" | "wear" | "head" | "face" | "skin" | "item" | "front" | "back",
|
|
||||||
value: number,
|
|
||||||
) {
|
|
||||||
if (kind in kindMap) return { field: kindMap[kind], value: `${value}` }
|
|
||||||
return { field: "avatarItem", value: `${categories.indexOf(kind) + 1}:${value}` }
|
|
||||||
}
|
|
||||||
|
|
||||||
USER.me().then(u => {
|
|
||||||
if (!u) throw new Error(t("userbox.error.noprofile"))
|
|
||||||
user = u
|
|
||||||
fetchData()
|
|
||||||
}).catch((e) => { loading = false; error = e.message });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if !loading && !error}
|
|
||||||
<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 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));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#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}
|
|
||||||
{/if}
|
|
||||||
<StatusOverlays {error} {loading} />
|
|
||||||
|
|
||||||
<style lang="sass">
|
|
||||||
@use "../vars"
|
|
||||||
|
|
||||||
img
|
|
||||||
width: 100%
|
|
||||||
height: auto
|
|
||||||
|
|
||||||
.preview
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
gap: 1rem
|
|
||||||
width: 50%
|
|
||||||
|
|
||||||
@media (max-width: vars.$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
|
|
||||||
|
|
||||||
select
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
.field
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
width: 100%
|
|
||||||
|
|
||||||
label
|
|
||||||
max-width: max-content
|
|
||||||
|
|
||||||
> div:not(.bool)
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
gap: 1rem
|
|
||||||
margin-top: 0.5rem
|
|
||||||
|
|
||||||
> select
|
|
||||||
flex: 1
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -0,0 +1,205 @@
|
||||||
|
<!-- Svelte 4.2.11 -->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
type AquaNetUser,
|
||||||
|
type UserBox,
|
||||||
|
type UserItem,
|
||||||
|
} from "../../libs/generalTypes";
|
||||||
|
import { DATA, USER, USERBOX } from "../../libs/sdk";
|
||||||
|
import { t, ts } from "../../libs/i18n";
|
||||||
|
import { DATA_HOST, FADE_IN, FADE_OUT, HAS_USERBOX_ASSETS } from "../../libs/config";
|
||||||
|
import { fade, slide } from "svelte/transition";
|
||||||
|
import StatusOverlays from "../StatusOverlays.svelte";
|
||||||
|
import Icon from "@iconify/svelte";
|
||||||
|
import GameSettingFields from "./GameSettingFields.svelte";
|
||||||
|
import { filter } from "d3";
|
||||||
|
import { coverNotFound } from "../../libs/ui";
|
||||||
|
|
||||||
|
let user: AquaNetUser
|
||||||
|
let [loading, error, submitting, preview] = [true, "", "", ""]
|
||||||
|
let changed: string[] = [];
|
||||||
|
|
||||||
|
// Available (unlocked) options for each kind of item
|
||||||
|
// In allItems: 'namePlate', 'frame', 'trophy', 'mapIcon', 'systemVoice', 'avatarAccessory'
|
||||||
|
let allItems: Record<string, Record<string, { name: string }>> = {}
|
||||||
|
let iKinds = { namePlate: 1, frame: 2, trophy: 3, mapIcon: 8, systemVoice: 9, avatarAccessory: 11 }
|
||||||
|
// In userbox: 'nameplateId', 'frameId', 'trophyId', 'mapIconId', 'voiceId', 'avatar{Wear/Head/Face/Skin/Item/Front/Back}'
|
||||||
|
let userbox: UserBox
|
||||||
|
let avatarKinds = ['Wear', 'Head', 'Face', 'Skin', 'Item', 'Front', 'Back']
|
||||||
|
// iKey should match allItems keys, and ubKey should match userbox keys
|
||||||
|
let userItems: { iKey: string, ubKey: keyof UserBox, items: UserItem[] }[] = []
|
||||||
|
|
||||||
|
// Submit changes
|
||||||
|
function submit(field: keyof UserBox) {
|
||||||
|
let obj = { field, value: userbox[field] }
|
||||||
|
if (submitting) return
|
||||||
|
submitting = obj.field
|
||||||
|
|
||||||
|
USERBOX.setUserBox(obj)
|
||||||
|
.then(() => changed = changed.filter((c) => c !== obj.field))
|
||||||
|
.catch(e => error = e.message)
|
||||||
|
.finally(() => submitting = "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data from the server
|
||||||
|
async function fetchData() {
|
||||||
|
const profile = await USERBOX.getProfile().catch(_ => {
|
||||||
|
loading = false
|
||||||
|
error = t("userbox.error.nodata")
|
||||||
|
})
|
||||||
|
if (!profile) return
|
||||||
|
userbox = profile.user
|
||||||
|
userItems = Object.entries(iKinds).flatMap(([iKey, iKind]) => {
|
||||||
|
if (iKey != 'avatarAccessory') {
|
||||||
|
let ubKey = `${iKey}Id`
|
||||||
|
if (ubKey == 'namePlateId') ubKey = 'nameplateId'
|
||||||
|
if (ubKey == 'systemVoiceId') ubKey = 'voiceId'
|
||||||
|
return [{ iKey, ubKey: ubKey as keyof UserBox,
|
||||||
|
items: profile.items.filter(x => x.itemKind === iKind)
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return avatarKinds.map((aKind, i) => {
|
||||||
|
let items = profile.items.filter(x => x.itemKind === iKind && Math.floor(x.itemId / 100000) % 10 === i + 1)
|
||||||
|
return { iKey, ubKey: `avatar${aKind}` as keyof UserBox, items }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
allItems = await DATA.allItems('chu3').catch(_ => {
|
||||||
|
loading = false
|
||||||
|
error = t("userbox.error.nodata")
|
||||||
|
}) as typeof allItems
|
||||||
|
|
||||||
|
console.log("User Items", userItems)
|
||||||
|
console.log("All items", allItems)
|
||||||
|
console.log("Userbox", userbox)
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
USER.me().then(u => {
|
||||||
|
if (!u) throw new Error(t("userbox.error.nodata"))
|
||||||
|
user = u
|
||||||
|
return fetchData()
|
||||||
|
}).catch((e) => { loading = false; error = e.message });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<StatusOverlays {error} loading={loading || !!submitting} />
|
||||||
|
{#if !loading && !error}
|
||||||
|
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
|
||||||
|
<h2>{t("userbox.header.general")}</h2>
|
||||||
|
<GameSettingFields game="chu3"/>
|
||||||
|
<h2>{t("userbox.header.userbox")}</h2>
|
||||||
|
<div class="fields">
|
||||||
|
{#each userItems as { iKey, ubKey, items }, i}
|
||||||
|
<div class="field">
|
||||||
|
<label for={ubKey}>{ts(`userbox.${ubKey}`)}</label>
|
||||||
|
<div>
|
||||||
|
<select bind:value={userbox[ubKey]} id={ubKey} on:change={() => changed = [...changed, ubKey]}>
|
||||||
|
{#each items as option}
|
||||||
|
<option value={option.itemId}>{allItems[iKey][option.itemId]?.name || `(unknown ${option.itemId})`}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
{#if changed.includes(ubKey)}
|
||||||
|
<button transition:slide={{axis: "x"}} on:click={() => submit(ubKey)} disabled={!!submitting}>
|
||||||
|
{t("settings.profile.save")}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if HAS_USERBOX_ASSETS}
|
||||||
|
<h2>{t("userbox.header.preview")}</h2>
|
||||||
|
<p class="notice">{t("userbox.preview.notice")}</p>
|
||||||
|
<input bind:value={preview} placeholder={t("userbox.preview.url")}/>
|
||||||
|
{#if preview}
|
||||||
|
<div class="preview">
|
||||||
|
{#each userItems.filter(v => v.iKey != 'trophy' && v.iKey != 'systemVoice') as { iKey, ubKey, items }, i}
|
||||||
|
<div>
|
||||||
|
<span>{ts(`userbox.${ubKey}`)}</span>
|
||||||
|
<img src={`${preview}/${iKey}/${userbox[ubKey].toString().padStart(8, '0')}.png`} alt="" on:error={coverNotFound} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="sass">
|
||||||
|
@use "../../vars"
|
||||||
|
|
||||||
|
input
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
h2
|
||||||
|
margin-bottom: 0.5rem
|
||||||
|
|
||||||
|
p.notice
|
||||||
|
opacity: 0.6
|
||||||
|
margin-top: 0
|
||||||
|
|
||||||
|
.preview
|
||||||
|
margin-top: 32px
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
justify-content: space-between
|
||||||
|
gap: 32px
|
||||||
|
|
||||||
|
> div
|
||||||
|
position: relative
|
||||||
|
width: 100px
|
||||||
|
height: 100px
|
||||||
|
overflow: hidden
|
||||||
|
background: vars.$ov-lighter
|
||||||
|
border-radius: vars.$border-radius
|
||||||
|
|
||||||
|
span
|
||||||
|
position: absolute
|
||||||
|
bottom: 0
|
||||||
|
width: 100%
|
||||||
|
text-align: center
|
||||||
|
z-index: 10
|
||||||
|
background: rgba(0, 0, 0, 0.2)
|
||||||
|
backdrop-filter: blur(2px)
|
||||||
|
|
||||||
|
img
|
||||||
|
position: absolute
|
||||||
|
inset: 0
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
object-fit: contain
|
||||||
|
|
||||||
|
.fields
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
gap: 12px
|
||||||
|
width: 100%
|
||||||
|
flex-grow: 0
|
||||||
|
|
||||||
|
label
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
select
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
.field
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
label
|
||||||
|
max-width: max-content
|
||||||
|
|
||||||
|
> div:not(.bool)
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
gap: 1rem
|
||||||
|
margin-top: 0.5rem
|
||||||
|
|
||||||
|
> select
|
||||||
|
flex: 1
|
||||||
|
</style>
|
Loading…
Reference in New Issue