feat: add github support, remove easter egg and declutter

This commit is contained in:
raymonable 2026-02-16 20:10:19 -05:00
parent 1b5601db45
commit c2ac01d940
No known key found for this signature in database
25 changed files with 338 additions and 496 deletions

View File

@ -7,3 +7,4 @@ VITE_TURNSTILE_SITE_KEY=0x4AAAAAAASGA2KQEIelo9P9
VITE_DISCORD_INVITE=https://discord.gg/FNgveqFF7s
VITE_TELEGRAM_INVITE=https://t.me/+zBL4RZdyfvUzZGU1
VITE_QQ_INVITE=https://qm.qq.com/q/dpYmGoVHnG
VITE_GITHUB_REPOSITORY=https://github.com/MewoLab/AquaDX

View File

@ -1,163 +0,0 @@
/*
Happy April Fools!
This theme will stay here.
Note that I made it with Stylish in mind, it's quite jank.
*/
* {
font-family: "ヒラギノ角ゴ Pro W3", "メイリオ", Meiryo, " Pゴシック",
"MS P Gothic", sans-serif;
}
nav > a,
nav > *.active,
.setting-icon path {
color: unset !important;
}
.aqua-tooltip {
background: black;
}
.fw-block {
background: none !important;
box-shadow: none !important;
}
#app {
background: url(/assets/theme/cn/logo.bin),
#f9f9db;
background-repeat: no-repeat;
background-position: 50% 4px;
max-width: 528px !important;
margin: 0 auto;
padding: 100px 0 0 0 !important;
height: unset !important;
box-shadow: -8px 0 0 0 #fdd500, -12px 0 0 0 #f9f9db, 8px 0 0 0 #fdd500,
12px 0 0 0 #f9f9db;
}
nav:has(.logo) {
position: absolute !important;
top: 0;
left: 0;
width: calc(100% - 96px);
}
nav {
color: black;
}
.user-pfp {
margin-top: -56px !important;
}
.outer-title-options,
.outer-title-options *,
nav.tabs {
color: white !important;
}
.outer-title-options {
margin-top: 0 !important;
display: unset !important;
}
.outer-title-options h2 {
width: 460px;
position: relative;
right: 20px;
display: flex;
justify-content: center;
margin: 0 0 10px 0 !important;
background: url(/assets/theme/cn/header.bin);
}
.chuni-userbox-row {
flex-wrap: wrap;
}
.chuni-userbox button {
width: calc(100% / 4) !important;
font-size: 0px;
}
.chuni-userbox-row button {
width: unset !important;
flex: 0 1 calc(100% / 3) !important;
}
.chuni-userbox-row button img {
overflow: hidden;
font-size: 10px;
}
.chuni-nameplate {
background: none !important;
position: relative !important;
left: 20px;
}
.chuni-userbox {
background: none !important;
}
main {
max-width: calc(460px - 40px) !important;
margin: 16px auto 0 auto !important;
background: #2c4056 !important;
border-radius: unset !important;
padding: 10px 20px !important;
}
main:has(.user-pfp) {
margin: 64px auto 0 auto !important;
}
.rating-composition {
display: flex !important;
flex-wrap: wrap;
gap: 0 !important;
}
.rating-composition > div {
width: 47.5%;
margin: 1.25%;
}
.map-detail-container {
background: none !important;
border-radius: 0 !important;
}
.lv {
border-radius: 0 !important;
display: flex;
align-items: center;
justify-content: center;
padding: 0 !important;
width: 50px !important;
}
.rank-text {
min-width: 20px !important;
}
.chuni-userbox-container {
flex-wrap: wrap;
}
.profile-bio-text {
white-space: unset !important;
}
.chuni-penguin-container {
padding: 64px 0;
width: 100%;
background: linear-gradient(
180deg,
rgba(249, 249, 219, 1) 0%,
rgba(249, 249, 219, 1) 69%,
rgba(231, 231, 202, 1) 70%,
rgba(231, 231, 202, 1) 100%
);
}
body {
background: #fdd500 !important;
color: white;
}
@media (max-width: 1200px) {
#app {
background-position: 50% 60px !important;
padding-top: 150px !important;
}
}
@media (max-width: 1028px) {
#app {
background-size: 90%;
}
.user-pfp {
margin-top: -36px !important;
}
.user-pfp nav {
top: -10px !important;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

@ -16,6 +16,7 @@
import Communities from "./pages/Home/Communities.svelte";
import LinkCard from "./pages/Home/LinkCard.svelte";
import SetupInstructions from "./pages/Home/SetupInstructions.svelte";
import PageNotFound from "./pages/PageNotFound.svelte";
console.log(`%c
┏━┓ ┳━┓━┓┏━
@ -41,15 +42,6 @@
playedMai = !!game.mai2
})
}).catch(e => console.error(e))
const themeStyle = document.createElement("link");
themeStyle.rel = "stylesheet";
switch (localStorage.getItem("theme")) {
case "cn":
themeStyle.href = "/assets/theme/cn.css";
};
if (themeStyle.href)
document.head.appendChild(themeStyle);
}
let path = window.location.pathname;
</script>
@ -93,8 +85,10 @@
<Route path="/u/:username" component={UserHome} />
<Route path="/u/:username/:game" component={UserHome} />
<Route path="/settings" component={Settings} />
<Route path="/settings/:page" component={Settings} />
<Route path="/pictures" component={MaiPhoto} />
<Route path="/transfer" component={Transfer} />
<Route component={PageNotFound} />
</Router>
<style lang="sass">

View File

@ -381,6 +381,4 @@ nav
position: absolute
padding: 4px 8px
background: vars.$ov-lighter
backdrop-filter: blur(5px)
backdrop-filter: blur(5px)

View File

@ -5,7 +5,7 @@
export let color: string = '179, 198, 255'
export let icon: string
export let href: string | undefined
export let href: string | undefined = undefined
export let isSmall: boolean = false
// Manually positioned icons

View File

@ -45,6 +45,7 @@
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))
.icon
position: absolute

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { t } from "../../libs/i18n";
import { t } from "../libs/i18n";
const tabs: Record<string, string> = {
[t('home.nav.portal')]: `/home`,
@ -22,7 +22,7 @@
</nav>
<style lang="sass">
@use "../../vars"
@use "../vars"
.tabs
display: flex
gap: 1rem

View File

@ -242,16 +242,6 @@
link.click();
}
function g(v: string) {
if (v != ("\x63\x68\x75\x6E\x69\x74\x68\x6D ").repeat(3).trim()) return;
const t = v.substring(5, 6) + v.substring(1, 2) + "eme";
if (!localStorage.getItem(t)) {
localStorage.setItem(t, v.substring(0, 1) + "\x6E");
} else
localStorage.removeItem(t);
setTimeout(location.reload, 1000); // ?
}
let DDSreader: DDS | undefined;
let USERBOX_PROGRESS = 0;
@ -316,8 +306,7 @@
<StatusOverlays {error} loading={loading || !!submitting} />
{#if !loading && !error}
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
<h2>{t("userbox.header.general")}</h2>
<div>
<div class="general-options">
<GameSettingFields game="chu3"/>
@ -477,10 +466,6 @@
input
width: 100%
h2
margin-bottom: 0.5rem
.general-options
display: flex
flex-direction: column

View File

@ -8,7 +8,7 @@
const rounding = useLocalStorage("rounding", true);
</script>
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
<div class="fields">
<blockquote class="info">
{ts("settings.siteNotice")}
</blockquote>

View File

@ -4,7 +4,7 @@
import { t } from "../../libs/i18n.js";
import Icon from "@iconify/svelte";
import StatusOverlays from "../StatusOverlays.svelte";
import { GAME } from "../../libs/sdk";
import { GAME, USER } from "../../libs/sdk";
import GameSettingFields from "./GameSettingFields.svelte";
import { download } from "../../libs/ui";
@ -12,15 +12,15 @@
['name', t('settings.mai2.name')],
]
export let username: string;
let error: string
let submitting = ""
let values = Array(profileFields.length).fill('')
let changed: string[] = []
GAME.userSummary(username, 'mai2').then(({name}) => {
values = [name]
}).catch(e => error = e.message)
USER.me().then(me =>
GAME.userSummary(me.username, 'mai2').then(({name}) => {
values = [name]
}).catch(e => error = e.message));
function submit(field: string, value: string) {
if (submitting) return
@ -112,14 +112,14 @@
}
try {
musicData = await fetch(`${DATA_HOST}/d/mai2/00/all-music.json`).then(res => res.json())
} catch (e) {
} catch (e: any) {
error = e.message;
submitting = ""
return;
}
try {
data = await GAME.export('mai2');
} catch (e) {
} catch (e: any) {
error = e.message;
submitting = ""
return;
@ -208,7 +208,7 @@
}
</script>
<div class="fields" out:fade={FADE_OUT} in:fade={FADE_IN}>
<div class="fields">
{#each profileFields as [field, name], i (field)}
<div class="field">
<label for={field}>{name}</label>

View File

@ -4,6 +4,6 @@
import GameSettingFields from "./GameSettingFields.svelte";
</script>
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
<div>
<GameSettingFields game="ongeki"/>
</div>

View File

@ -0,0 +1,233 @@
<script lang="ts">
import { slide, fade } from "svelte/transition";
import type { AquaNetUser } from "../../libs/generalTypes";
import { CARD, USER } from "../../libs/sdk";
import StatusOverlays from "../../components/StatusOverlays.svelte";
import Icon from "@iconify/svelte";
import { pfp } from "../../libs/ui";
import { t, ts } from "../../libs/i18n";
import Cropper from "svelte-easy-crop";
let me: AquaNetUser;
let error: string;
let submitting = ""
let loading = false;
const profileFields = [
[ 'displayName', t('settings.profile.name') ],
[ 'username', t('settings.profile.username') ],
[ 'password', t('settings.profile.password') ],
[ 'profileBio', t('settings.profile.bio') ],
] as const
// Fetch user data
const getMe = () => {
loading = true;
USER.me().then((m) => {
if (pfpCropURL != null) {
URL.revokeObjectURL(pfpCropURL);
pfpField.value = "";
pfpCropURL = null;
}; me = m;
loading = false;
}).catch(e => error = e.message)
}
getMe();
let changed: string[] = []
let pfpField: HTMLInputElement
let pfpCropURL: string | null = null;
let pfpCrop = { width: 0, height: 0, x: 0, y: 0 };
function submit(field: string, value: string) {
if (submitting) return
submitting = field
USER.setting(field, value).then(() => {
changed = changed.filter(c => c !== field)
}).catch(e => error = e.message).finally(() => submitting = "")
}
function uploadPfp() {
if (submitting) return
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
const size = Math.round(Math.min(pfpCrop.width, pfpCrop.height, 1024));
canvas.width = size;
canvas.height = size;
let img = document.createElement("img");
img.onload = () => {
ctx?.drawImage(img, pfpCrop.x, pfpCrop.y, pfpCrop.width, pfpCrop.height, 0, 0, size, size);
canvas.toBlob(blob => {
if (!blob) return;
submitting = 'profilePicture'
USER.uploadPfp(blob as File).then(() => {
me.profilePicture = me.username
setTimeout(getMe, 200);
}).catch(e => error = e.message).finally(() => submitting = "")
});
}
img.src = pfpCropURL ?? "";
}
function handlePfpUpload(e: Event & { currentTarget: HTMLInputElement }) {
if (!e.target) return;
let files = e?.currentTarget?.files;
if (!files || files.length <= 0) return;
let file = files[0];
console.log(me.username, me);
switch (file.type) {
case "image/gif":
USER.uploadPfp(file).then(() => {
me.profilePicture = me.username
// reload
setTimeout(getMe, 200);
}).catch(e => error = e.message).finally(() => submitting = "")
break;
case "image/png":
case "image/jpeg":
case "image/webp":
pfpCropURL = URL.createObjectURL(file);
break;
default:
error = t("settings.profile.bad-format");
}
};
function logOut() {
localStorage.removeItem("token");
location.href = "/";
}
const passwordAction = (node: HTMLInputElement, whether: boolean) => {
if (whether) node.type = 'password'
}
</script>
<StatusOverlays {error} loading={!!submitting || loading} />
{#if !submitting && !error && me}
<div>
<div class="fields">
<div class="field">
<label for="profile-upload">{t('settings.profile.picture')}</label>
<div>
{#if me && me.profilePicture}
<div on:click={() => pfpField.click()} on:keydown={e => e.key === 'Enter' && pfpField.click()}
role="button" tabindex="0" class="clickable">
<img use:pfp={me} alt="Profile" />
</div>
{:else}
<button on:click={() => pfpField.click()}>
{t('settings.profile.upload-new')}
</button>
{/if}
</div>
<input id="profile-upload" type="file" accept="image/gif,image/png,image/jpeg,image/webp" hidden bind:this={pfpField}
on:change={handlePfpUpload} />
</div>
{#each profileFields as [field, name], i (field)}
<div class="field">
<label for={field}>{name}</label>
<div>
{#if field == "profileBio"}
<textarea id={field} bind:value={me[field]} on:input={() => changed = [...changed, field]} maxlength=255 placeholder={t('settings.profile.unset')}></textarea>
{:else}
<input id={field} type="text" use:passwordAction={field === 'password'}
bind:value={me[field]} on:input={() => changed = [...changed, field]}
placeholder={field === 'password' ? t('settings.profile.unchanged') : t('settings.profile.unset')}/>
{/if}
{#if changed.includes(field) && me[field]}
<button transition:slide={{axis: 'x'}} on:click={() => submit(field, me[field])}>
{#if submitting === field}
<Icon icon="line-md:loading-twotone-loop" />
{:else}
{t('settings.profile.save')}
{/if}
</button>
{/if}
</div>
</div>
{/each}
<div class="field m-t">
<div class="bool">
<input id="optOutOfLeaderboard" type="checkbox" bind:checked={me.optOutOfLeaderboard}
on:change={() => submit('optOutOfLeaderboard', me.optOutOfLeaderboard.toString())}/>
<label for="optOutOfLeaderboard">
<span class="name">{ts(`settings.fields.optOutOfLeaderboard.name`)}</span>
<span class="desc">{ts(`settings.fields.optOutOfLeaderboard.desc`)}</span>
</label>
</div>
</div>
<div class="field m-t">
<div>
<button on:click={logOut}>{ts(`settings.profile.logout`)}</button>
</div>
</div>
</div>
</div>
{/if}
{#if pfpCropURL != null}
<div class="overlay" transition:fade>
<div>
<div class="cropper-container">
<Cropper maxZoom={1e9} oncropcomplete={(e) => pfpCrop = e.pixels} image={pfpCropURL ?? "assets/imgs/no_profile.png"} aspect={1} cropShape="round"></Cropper>
</div>
<button on:click={uploadPfp}>
{t("settings.profile.save")}
</button>
<button on:click={getMe}>
{t("back")}
</button>
</div>
</div>
{/if}
<style lang="sass">
@use "../../vars"
.fields
display: flex
flex-direction: column
gap: 12px
.bool
display: flex
align-items: center
gap: 1rem
label
display: flex
flex-direction: column
.desc
opacity: 0.6
.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
> input, > textarea
flex: 1
img
max-width: 100px
max-height: 100px
border-radius: vars.$border-radius
object-fit: cover
aspect-ratio: 1
.cropper-container
position: relative
width: 400px
aspect-ratio: 1
</style>

View File

@ -4,6 +4,6 @@
import GameSettingFields from "./GameSettingFields.svelte";
</script>
<div out:fade={FADE_OUT} in:fade={FADE_IN}>
<div>
<GameSettingFields game="wacca"/>
</div>

View File

@ -10,15 +10,15 @@
<div class="overlay" transition:fade>
<div>
<h2 class="error">{t('status.error')}</h2>
{#if !expected}
<span>{t('status.error.hint')}<a href={DISCORD_INVITE}>{t('status.error.hint.link')}</a></span>
{/if}
<span class="detail">{error}</span>
<a class="hint" href="/support">{t("status.error.hint")}</a>
<div class="actions">
<button on:click={() => location.reload()} class="error">
{t('action.refresh')}
</button>
<button on:click={() => location.href = "/home"} class="error">
{t('action.home')}
</button>
</div>
</div>
</div>
@ -26,14 +26,19 @@
<style lang="sass">
.actions
display: flex
gap: 16px
gap: 0.5em
button
width: 100%
.detail
white-space: pre-line
font-size: 0.9em
line-height: 1.2
opacity: 0.8
.hint
font-size: 0.875em
.overlay > div
min-width: 15rem
max-width: 25rem
</style>

View File

@ -10,6 +10,7 @@ export const TURNSTILE_SITE_KEY = import.meta.env.VITE_TURNSTILE_SITE_KEY
export const DISCORD_INVITE = import.meta.env.VITE_DISCORD_INVITE
export const TELEGRAM_INVITE = import.meta.env.VITE_TELEGRAM_INVITE
export const QQ_INVITE = import.meta.env.VITE_QQ_INVITE
export const GITHUB_REPOSITORY = import.meta.env.VITE_GITHUB_REPOSITORY
// UI
export const FADE_OUT = { duration: 200 }

View File

@ -77,17 +77,19 @@ export const EN_REF_GENERAL = {
'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.error.hint': 'Support',
'status.detail': 'Detail: ${detail}',
'action.refresh': 'Refresh',
'action.refresh': 'Retry',
'action.cancel': 'Cancel',
'action.confirm': 'Confirm',
'action.home': 'Home',
'navigation.profile': 'Profile',
'navigation.maps': 'Maps',
'navigation.home': 'Home',
'navigation.rankings': 'Rankings',
'navigation.notice': 'Notice'
'navigation.notice': 'Notice',
'loading': `Please wait...`,
'404': 'Page not found (${pathname}). If this is in error, please report it via an appropriate support channel.'
}
export const EN_REF_HOME = {
@ -102,7 +104,7 @@ export const EN_REF_HOME = {
'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': 'Community & Support',
'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.',
@ -131,13 +133,13 @@ export const EN_REF_HOME = {
'home.community.discord': 'Discord',
'home.community.telegram': 'Telegram (Chinese)',
'home.community.qq': 'QQ (Chinese)',
'home.community.github': 'GitHub Repository',
'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.`,
@ -157,6 +159,7 @@ export const EN_REF_SETUP = {
export const EN_REF_SETTINGS = {
'settings.title': 'Settings',
'settings.page-title': '${page} Settings',
'settings.tabs.profile': 'Profile',
'settings.tabs.global': 'Global',
'settings.tabs.chu3': 'Chuni',

View File

@ -5,7 +5,7 @@
import ActionCard from "../components/ActionCard.svelte";
import { t } from "../libs/i18n";
import ImportDataAction from "./Home/ImportDataAction.svelte";
import DashboardTabs from "./Home/DashboardTabs.svelte";
import DashboardTabs from "../components/DashboardTabs.svelte";
USER.ensureLoggedIn();

View File

@ -3,30 +3,36 @@
<script lang="ts">
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";
import { DISCORD_INVITE, GITHUB_REPOSITORY, QQ_INVITE, TELEGRAM_INVITE } from "../../libs/config";
import DashboardTabs from "../../components/DashboardTabs.svelte";
</script>
<main class="content">
<DashboardTabs />
<div class="setup-instructions">
<h2>{t('home.join-community')}</h2>
<div class="grid cols-3 gap-4">
<div class="communities grid cols-2 gap-4">
{#if DISCORD_INVITE}
<CommunityCard color="82, 93, 233" icon="ic:baseline-discord" href={DISCORD_INVITE}>
<h3>{t('home.community.discord')}</h3>
<span>{t('home.community.discord')}</span>
</CommunityCard>
{/if}
{#if TELEGRAM_INVITE}
<CommunityCard color="46, 163, 224" icon="mingcute:telegram-fill" href={TELEGRAM_INVITE}>
<h3>{t('home.community.telegram')}</h3>
<span>{t('home.community.telegram')}</span>
</CommunityCard>
{/if}
{#if QQ_INVITE}
<CommunityCard color="226, 60, 68" icon="ri:qq-fill" href={QQ_INVITE}>
<h3>{t('home.community.qq')}</h3>
<span>{t('home.community.qq')}</span>
</CommunityCard>
{/if}
{#if GITHUB_REPOSITORY}
<CommunityCard color="245, 245, 249" icon="ri:github-fill" href={GITHUB_REPOSITORY}>
<span>{t('home.community.github')}</span>
</CommunityCard>
{/if}
</div>
@ -34,5 +40,6 @@
</main>
<style lang="sass">
.communities
margin: 0 1rem
</style>

View File

@ -8,7 +8,7 @@
import Icon from "@iconify/svelte"
import StatusOverlays from "../../components/StatusOverlays.svelte"
import { t } from "../../libs/i18n"
import DashboardTabs from "./DashboardTabs.svelte";
import DashboardTabs from "../../components/DashboardTabs.svelte";
// State
let state: 'ready' | 'linking-AC' | 'linking-SN' | 'loading' = "loading"

View File

@ -7,7 +7,7 @@
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 DashboardTabs from "../../components/DashboardTabs.svelte";
import { patchUserSegatools } from "../../libs/setup";
let user: AquaNetUser

View File

@ -0,0 +1,5 @@
<script>
import StatusOverlays from "../components/StatusOverlays.svelte";
import { t } from "../libs/i18n";
</script>
<StatusOverlays error={t("404", {pathname: new URL(location.href).pathname})} />

View File

@ -5,281 +5,58 @@
import type { AquaNetUser } from "../../libs/generalTypes";
import { CARD, USER } from "../../libs/sdk";
import StatusOverlays from "../../components/StatusOverlays.svelte";
import Icon from "@iconify/svelte";
import { pfp } from "../../libs/ui";
import { t, ts } from "../../libs/i18n";
import { FADE_IN, FADE_OUT } from "../../libs/config";
import Cropper from "svelte-easy-crop";
import UserBox from "../../components/settings/ChuniSettings.svelte";
import ChuniSettings from "../../components/settings/ChuniSettings.svelte";
import Mai2Settings from "../../components/settings/Mai2Settings.svelte";
import WaccaSettings from "../../components/settings/WaccaSettings.svelte";
import GeneralGameSettings from "../../components/settings/GeneralGameSettings.svelte";
import OngekiSettings from "../../components/settings/OngekiSettings.svelte";
import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte";
import UserSettings from "../../components/settings/UserSettings.svelte";
import type { Component } from "svelte";
import { EN_REF } from "../../libs/i18n/en_ref";
USER.ensureLoggedIn()
let me: AquaNetUser;
let error: string;
let submitting = ""
let tab = 0
let tabs = ['profile']
export let page: string = "profile";
const profileFields = [
[ 'displayName', t('settings.profile.name') ],
[ 'username', t('settings.profile.username') ],
[ 'password', t('settings.profile.password') ],
/* Neither of these did anything of importance
[ 'country', t('settings.profile.country') ],
[ 'profileLocation', t('settings.profile.location') ],*/
[ 'profileBio', t('settings.profile.bio') ],
] as const
// Fetch user data
const getMe = () => USER.me().then((m) => {
if (pfpCropURL != null) {
URL.revokeObjectURL(pfpCropURL);
pfpField.value = "";
pfpCropURL = null;
}
me = m
CARD.userGames(m.username).then(games => {
tabs = [
...tabs,
...['chu3', 'mai2','wacca', 'ongeki'].filter(v => games[v as keyof typeof games]), // :xdx:
'global'
]
})
}).catch(e => error = e.message)
getMe()
let changed: string[] = []
let pfpField: HTMLInputElement
let pfpCropURL: string | null = null;
let pfpCrop = { width: 0, height: 0, x: 0, y: 0 };
function submit(field: string, value: string) {
if (submitting) return
submitting = field
USER.setting(field, value).then(() => {
changed = changed.filter(c => c !== field)
}).catch(e => error = e.message).finally(() => submitting = "")
const pages: Record<string, Component> = {
"profile": UserSettings, "chu3": ChuniSettings,
"mai2": Mai2Settings, "wacca": WaccaSettings,
"ongeki": OngekiSettings, "global": GeneralGameSettings
}
function uploadPfp() {
if (submitting) return
// Don't know why this isn't just a part of the cropper module. Have to do this myself.. What a shame
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
const size = Math.round(Math.min(pfpCrop.width, pfpCrop.height, 1024));
canvas.width = size;
canvas.height = size;
let img = document.createElement("img");
img.onload = () => {
ctx?.drawImage(img, pfpCrop.x, pfpCrop.y, pfpCrop.width, pfpCrop.height, 0, 0, size, size);
canvas.toBlob(blob => {
if (!blob) return;
submitting = 'profilePicture'
USER.uploadPfp(blob as File).then(() => {
me.profilePicture = me.username
// reload
// this doesn't work btw
setTimeout(getMe, 200);
}).catch(e => error = e.message).finally(() => submitting = "")
});
}
img.src = pfpCropURL ?? "";
}
function handlePfpUpload(e: Event & { target: HTMLInputElement }) {
if (!e.target) return;
let files = e?.target?.files;
if (!files || files.length <= 0) return;
let file = files[0];
console.log(me.username, me);
switch (file.type) {
case "image/gif":
USER.uploadPfp(file).then(() => {
me.profilePicture = me.username
// reload
setTimeout(getMe, 200);
}).catch(e => error = e.message).finally(() => submitting = "")
break;
case "image/png":
case "image/jpeg":
case "image/webp":
pfpCropURL = URL.createObjectURL(file);
break;
default:
error = t("settings.profile.bad-format");
}
};
function logOut() {
localStorage.removeItem("token");
location.href = "/";
}
if (!pages[page] && page)
error = t("404", {pathname: new URL(location.href).pathname});
const passwordAction = (node: HTMLInputElement, whether: boolean) => {
if (whether) node.type = 'password'
}
USER.me().then(m => me = m)
.catch(e => error = e.message)
</script>
<main class="content">
<div class="outer-title-options">
<h2>{t('settings.title')}</h2>
<nav>
{#each tabs as tabName, i}
<div transition:slide={{axis: 'x'}} class:active={tab === i}
on:click={() => tab = i} on:keydown={e => e.key === 'Enter' && (tab = i)}
role="button" tabindex="0">
{ts(`settings.tabs.${tabName}`)}
</div>
{#each Object.entries(pages) as tab}
<a href={`/settings/${tab[0] != "profile" ? tab[0] : ""}`} transition:slide={{axis: 'x'}}
class:active={tab[0] == page || (tab[0] == "profile" && !page)} role="button" tabindex="0">
{ts(`settings.tabs.${tab[0]}`)}
</a>
{/each}
</nav>
</div>
{#if tab === 0 && me}
<!-- Tab 0: Profile settings -->
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
<div class="field">
<label for="profile-upload">{t('settings.profile.picture')}</label>
<div>
{#if me && me.profilePicture}
<div on:click={() => pfpField.click()} on:keydown={e => e.key === 'Enter' && pfpField.click()}
role="button" tabindex="0" class="clickable">
<img use:pfp={me} alt="Profile" />
</div>
{:else}
<button on:click={() => pfpField.click()}>
{t('settings.profile.upload-new')}
</button>
{/if}
</div>
<!-- Genuinely don't know why this is giving me an intellisense error. Works fine. -->
<input id="profile-upload" type="file" accept="image/gif,image/png,image/jpeg,image/webp" style="display: none" bind:this={pfpField}
on:change={handlePfpUpload} />
</div>
<h2 class="header">
{t('settings.page-title', {page: ts(`settings.tabs.${page}`)})}
</h2>
{#each profileFields as [field, name], i (field)}
<div class="field">
<label for={field}>{name}</label>
<div>
{#if field == "profileBio"}
<textarea id={field} bind:value={me[field]} on:input={() => changed = [...changed, field]} maxlength=255 placeholder={t('settings.profile.unset')}></textarea>
{:else}
<input id={field} type="text" use:passwordAction={field === 'password'}
bind:value={me[field]} on:input={() => changed = [...changed, field]}
placeholder={field === 'password' ? t('settings.profile.unchanged') : t('settings.profile.unset')}/>
{/if}
{#if changed.includes(field) && me[field]}
<button transition:slide={{axis: 'x'}} on:click={() => submit(field, me[field])}>
{#if submitting === field}
<Icon icon="line-md:loading-twotone-loop" />
{:else}
{t('settings.profile.save')}
{/if}
</button>
{/if}
</div>
</div>
{/each}
<div class="field m-t">
<div class="bool">
<input id="optOutOfLeaderboard" type="checkbox" bind:checked={me.optOutOfLeaderboard}
on:change={() => submit('optOutOfLeaderboard', me.optOutOfLeaderboard.toString())}/>
<label for="optOutOfLeaderboard">
<span class="name">{ts(`settings.fields.optOutOfLeaderboard.name`)}</span>
<span class="desc">{ts(`settings.fields.optOutOfLeaderboard.desc`)}</span>
</label>
</div>
</div>
<div class="field m-t">
<div>
<button on:click={logOut}>{ts(`settings.profile.logout`)}</button>
</div>
</div>
</div>
{:else if tabs[tab] === 'chu3'}
<!-- Userbox settings -->
<UserBox />
{:else if tabs[tab] === 'mai2'}
<Mai2Settings username={me.username} />
{:else if tabs[tab] === 'wacca'}
<WaccaSettings />
{:else if tabs[tab] === 'ongeki'}
<OngekiSettings />
{:else if tabs[tab] === 'global'}
<GeneralGameSettings />
{#if pages[page]}
<svelte:component this={pages[page]} />
{/if}
<StatusOverlays {error} loading={!me || !!submitting} />
</main>
{#if pfpCropURL != null}
<div class="overlay" transition:fade>
<div>
<div class="cropper-container">
<Cropper maxZoom={1e9} oncropcomplete={(e) => pfpCrop = e.pixels} image={pfpCropURL ?? "assets/imgs/no_profile.png"} aspect={1} cropShape="round"></Cropper>
</div>
<button on:click={uploadPfp}>
{t("settings.profile.save")}
</button>
<button on:click={getMe}>
{t("back")}
</button>
</div>
</div>
{/if}
<StatusOverlays {error} />
<style lang="sass">
@use "../../vars"
.fields
display: flex
flex-direction: column
gap: 12px
.bool
display: flex
align-items: center
gap: 1rem
label
display: flex
flex-direction: column
.desc
opacity: 0.6
.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
> input, > textarea
flex: 1
img
max-width: 100px
max-height: 100px
border-radius: vars.$border-radius
object-fit: cover
aspect-ratio: 1
.cropper-container
position: relative
width: 400px
aspect-ratio: 1
</style>
h2.header
margin: 0 0 0.5rem 0
</style>

View File

@ -30,15 +30,13 @@
registerChart()
export let username: string;
export let game: GameName | "auto" = "auto"
export let game: GameName;
let calElement: HTMLElement
let error: string;
let me: AquaNetUser
title(`User ${username}`)
const rounding = useLocalStorage("rounding", true);
const titleText = game != "auto" ? GAME_TITLE[game] : "?"
interface MusicAndPlay extends MusicMeta, GenericGamePlaylog {}
let d: {
@ -57,7 +55,7 @@
USER.isLoggedIn() && USER.me().then(u => me = u)
CARD.userGames(username).then(games => {
if (game == "auto") {
if (!game) {
let targetGames = Object.entries(games)
.map(d => {
if (d[1])
@ -66,9 +64,7 @@
}).sort((a,b) => {
return b[1]?.lastLogin - a[1]?.lastLogin;
});
if (targetGames[0])
window.location.href = `/u/${username}/${targetGames[0][0]}`
return;
game = targetGames[0][0] as GameName;
}
if (!games[game]) {
// Find a valid game
@ -118,11 +114,10 @@
}).catch((e) => { error = e.message; console.error(e) } );
}
if (Object.keys(GAME_TITLE).includes(game) || game == "auto") init()
if (Object.keys(GAME_TITLE).includes(game) || !game) init()
else error = t("UserHome.InvalidGame", {game})
const setRival = (isAdd: boolean) => {
if (game == "auto") return;
isLoading = true
GAME.setRival(game, username, isAdd).then(() => {
d!.user.rival = isAdd
@ -167,7 +162,7 @@
{/each}
{#if me && me.username === username}
<a class="setting-icon clickable" use:tooltip={t("UserHome.Settings")} href="/settings">
<a class="setting-icon clickable" use:tooltip={t("UserHome.Settings")} href={`/settings/${game}`}>
<Icon icon="eos-icons:rotating-gear"/>
</a>
{/if}
@ -188,7 +183,7 @@
<ChuniUserboxDisplay {game} {username} bind:error={error} />
<div>
<h2>{titleText} {t('UserHome.Statistics')}</h2>
<h2>{GAME_TITLE[game] ?? "?"} {t('UserHome.Statistics')}</h2>
<div class="scoring-info">
<div class="chart">
<div class="info-top">
@ -313,16 +308,16 @@
<!-- I don't like doing this but it may be preferable to gaslighting the types -->
<RatingComposition title="B30" comp={d.user.ratingComposition.best30} {allMusics} game={game != "auto" ? game : "mai2"}/>
<RatingComposition title="B35" comp={d.user.ratingComposition.best35} {allMusics} game={game != "auto" ? game : "mai2"}/>
<RatingComposition title="B15" comp={d.user.ratingComposition.best15} {allMusics} game={game != "auto" ? game : "mai2"}/>
<RatingComposition title="B30" comp={d.user.ratingComposition.best30} {allMusics} {game}/>
<RatingComposition title="B35" comp={d.user.ratingComposition.best35} {allMusics} {game}/>
<RatingComposition title="B15" comp={d.user.ratingComposition.best15} {allMusics} {game}/>
<!-- <RatingComposition title="Hot 10" comp={d.user.ratingComposition.hot10} {allMusics} {game}/> -->
<!-- <RatingComposition title="N10" comp={d.user.ratingComposition.next10} {allMusics} {game}/> -->
<!-- Chuni -->
{#if d.user.ratingComposition.new}
<RatingComposition title="New 20" comp={d.user.ratingComposition.new} {allMusics} game="chu3"/>
{:else}
<RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} game={game != "auto" ? game : "mai2"} top={10}/>
<RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} {game} top={10}/>
{/if}
<div class="recent">
@ -344,12 +339,12 @@
{r.notes?.[r.level === 10 ? 0 : r.level]?.lv?.toFixed(1) ?? r.worldsEndTag ?? '-'}
</span>
</span>
<span class={`rank-${getMult(r.achievement, game != "auto" ? game : "mai2")[2].toString()[0]}`}>
<span class="rank-text">{("" + getMult(r.achievement, game != "auto" ? game : "mai2")[2]).replace("p", "+")}</span>
<span class={`rank-${getMult(r.achievement, game)[2].toString()[0]}`}>
<span class="rank-text">{("" + getMult(r.achievement, game)[2]).replace("p", "+")}</span>
<span class="rank-num" use:tooltip={(r.achievement / 10000).toFixed(4)}>
{
rounding.value ?
roundFloor(r.achievement, game != "auto" ? game : "mai2", 1) :
roundFloor(r.achievement, game, 1) :
(r.achievement / 10000).toFixed(4)
}%
</span>