Merge branch 'v1-dev' into pr/119

pull/119/head
Azalea 2025-03-01 00:33:30 -05:00
commit 338819416f
13 changed files with 210 additions and 40 deletions

Binary file not shown.

Binary file not shown.

View File

@ -7,8 +7,10 @@
import { USER } from "./libs/sdk";
import type { AquaNetUser } from "./libs/generalTypes";
import Settings from "./pages/User/Settings.svelte";
import { pfp } from "./libs/ui"
import MaiPhoto from "./pages/MaiPhoto.svelte";
import { pfp, tooltip } from "./libs/ui"
import { ANNOUNCEMENT } from "./libs/config";
import { t } from "./libs/i18n";
console.log(`%c
┏━┓ ┳━┓━┓┏━
@ -37,13 +39,18 @@
<span>AquaNet</span>
</a>
{/if}
<a href="/home">home</a>
<div on:click={() => alert("Coming soon™")} on:keydown={e => e.key === "Enter" && alert("Coming soon™")}
role="button" tabindex="0">maps</div>
<a href="/ranking">rankings</a>
{#if ANNOUNCEMENT}
<div class="announcement">
<strong>{t('navigation.notice')}</strong>: {ANNOUNCEMENT}
</div>
{/if}
<a href="/home">{t('navigation.home').toLowerCase()}</a>
<!-- <div on:click={() => alert("Coming soon™")} on:keydown={e => e.key === "Enter" && alert("Coming soon™")}
role="button" tabindex="0">{t('navigation.maps').toLowerCase()}</div> -->
<a href="/ranking">{t('navigation.rankings').toLowerCase()}</a>
<a href="/pictures">pictures</a>
{#if me}
<a href="/u/{me.username}">
<a href="/u/{me.username}" use:tooltip={t('navigation.profile')}>
<img alt="profile" class="pfp" use:pfp={me}/>
</a>
{/if}
@ -81,6 +88,22 @@
border-radius: vars.$border-radius
object-fit: cover
.announcement
position: absolute
left: 50%
transform: translate(-50%, 0)
top: 0
width: 50%
height: 100%
display: flex
justify-content: center
align-content: center
z-index: -1
background: linear-gradient(90deg, #6f0f0f00 0%, vars.$c-shadow 50%, #6f0f0f00 100%)
font-size: 1.125em
text-decoration: none !important
color: inherit !important
.pfp
width: 2rem
height: 2rem

View File

@ -134,7 +134,7 @@ button.icon
.error
color: vars.$c-error
input
input, textarea
border-radius: vars.$border-radius
border: 1px solid transparent
padding: 0.6em 1.2em
@ -144,6 +144,10 @@ input
background-color: vars.$ov-lighter
transition: vars.$transition
box-sizing: border-box
resize: none
textarea
height: 5em
// Dropdown
select
@ -314,6 +318,9 @@ main.content
max-width: 400px
.aqua-tooltip
z-index: 900
.no-margin
margin: 0

View File

@ -60,7 +60,7 @@
.tooltip
position: absolute
z-index: 1000
z-index: 900
background: white
padding: 10px 16px
border-radius: vars.$border-radius

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { DDS } from "../../../libs/userbox/dds"
import { DDS, type RGB } from "../../../libs/userbox/dds"
import { ddsDB } from "../../../libs/userbox/userbox"
const DDSreader = new DDS(ddsDB);
@ -15,6 +15,26 @@
let ratingToString = (rating: number) => {
return rating.toFixed(2)
}
interface RatingRange {
min: number,
offset: number,
color?: RGB
};
// https://en.wikipedia.org/wiki/Chunithm#Rating
const ratingColors: RatingRange[] = ([
{min: 0.00, offset: 4, color: {r: 0, g: 191, b: 64}},
{min: 4.00, offset: 4, color: {r: 255, g: 111, b: 0}},
{min: 7.00, offset: 4, color: {r: 255, g: 64, b: 64}},
{min: 10.00, offset: 4, color: {r: 147, g: 38, b: 255}},
{min: 12.00, offset: 3},
{min: 13.25, offset: 2},
{min: 14.50, offset: 1},
{min: 15.25, offset: 0},
{min: 16.00, offset: 5}
]).filter(f => f.min <= chuniRating);
const ratingDigitOrder = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."]
const ratingColorData = (ratingColors[ratingColors.length - 1] ?? ratingColors[0]);
</script>
{#await DDSreader?.getFile(`nameplate:${chuniNameplate.toString().padStart(8, "0")}`, `nameplate:00000001`) then nameplateURL}
<!-- svelte-ignore a11y_click_events_have_key_events -->
@ -41,17 +61,36 @@
{chuniName}
</span>
</div>
<div class="chuni-user-rating">
RATING
<span class="chuni-user-rating-number">
{ratingToString(chuniRating)}
</span>
<div class={`chuni-user-rating color-${ratingColorData.color}`}>
{#await DDSreader?.getFileFromSheet("surfboard:CHU_UI_Common_01_v11.dds", 485, 5 + (28 * ratingColorData.offset), 62, 15, undefined, ratingColorData.color) then url}
{#if url}
<img src={url} alt="Rating">
<span class="chuni-user-rating-number">
{#each ratingToString(chuniRating).split("") as digit}
{#await DDSreader?.getFileFromSheet("surfboard:CHU_UI_Common_01_v11.dds", 552 + (24 * (ratingDigitOrder.indexOf(digit) ?? 0)), 1 + (28 * ratingColorData.offset), 16, 20, undefined, ratingColorData.color) then url}
<img src={url} alt="Rating Digit">
{/await}
{/each}
</span>
{:else}
RATING
<span class="chuni-user-rating-number">
{ratingToString(chuniRating)}
</span>
{/if}
{/await}
</div>
</div>
</div>
{/await}
<style lang="sass">
@use "../../../vars"
@font-face
font-family: "Gothic A1"
src: url("/assets/fonts/GothicA1.woff2")
.chuni-nameplate
width: 576px
height: 228px
@ -78,7 +117,7 @@
top: 40px
font-size: 1.15em
font-family: sans-serif
font-family: "Gothic A1", sans-serif
font-weight: bold
overflow-x: hidden
@ -123,7 +162,7 @@
display: flex
align-items: center
color: black
font-family: sans-serif
font-family: "Gothic A1", sans-serif
font-weight: bold
.chuni-user-name
@ -144,7 +183,7 @@
flex: 1 0 35%
font-size: 0.875em
text-shadow: #333 1px 1px, #333 1px -1px, #333 -1px 1px, #333 -1px -1px
color: #ddf
color: #fff
.chuni-user-rating-number
font-size: 1.5em

View File

@ -16,6 +16,8 @@ export const FADE_OUT = { duration: 200 }
export const FADE_IN = { delay: 400 }
export const DEFAULT_PFP = '/assets/imgs/no_profile.png'
export const ANNOUNCEMENT = '' // If set, will add an announcement to the top bar. Keep it short.
// Documentation for Userbox mode can be found in `docs/aquabox-url-mode.md`
// Please note that if this is set, it must be manually unset by users in Chuni Settings -> Update Userbox -> Switch to URL mode -> (empty value) -> Enter key
export const USERBOX_DEFAULT_URL = ""

View File

@ -72,6 +72,11 @@ export const EN_REF_GENERAL = {
'action.refresh': 'Refresh',
'action.cancel': 'Cancel',
'action.confirm': 'Confirm',
'navigation.profile': 'Profile',
'navigation.maps': 'Maps',
'navigation.home': 'Home',
'navigation.rankings': 'Rankings',
'navigation.notice': 'Notice'
}
export const EN_REF_HOME = {

View File

@ -67,7 +67,7 @@ const multTable = {
[ 60.0, 0, 'B' ],
[ 1.0, 0, 'C' ],
[ 0.0, 0, 'D' ]
]
],
}
export function getMult(achievement: number, game: GameName) {

View File

@ -56,6 +56,12 @@ void main() {
gl_FragColor = texture2D(uTexture, vTextureCoord);
}`
export interface RGB {
r: number,
g: number,
b: number
}
export class DDS {
constructor(db: IDBDatabase | undefined) {
this.cache = new DDSCache(db);
@ -241,13 +247,27 @@ export class DDS {
* @param s Scale factor
* @returns An object URL which correlates to a Blob
*/
async getFileFromSheet(path: string, x: number, y: number, w: number, h: number, s?: number): Promise<string> {
async getFileFromSheet(path: string, x: number, y: number, w: number, h: number, s?: number, color?: RGB): Promise<string> {
if (!await this.loadFile(path))
return "";
this.canvas2D.width = w * (s ?? 1);
this.canvas2D.height = h * (s ?? 1);
this.ctx.drawImage(this.canvasGL, x, y, w, h, 0, 0, w * (s ?? 1), h * (s ?? 1));
if (color) {
let colorData = this.ctx.getImageData(0, 0, this.canvas2D.width, this.canvas2D.height);
for (let i = 0; colorData.data.length > i; i++)
switch (i % 4) {
case 0:
colorData.data[i] *= (color.r / 255); break;
case 1:
colorData.data[i] *= (color.g / 255); break;
case 2:
colorData.data[i] *= (color.b / 255); break;
}
this.ctx.putImageData(colorData, 0, 0);
}
/* We don't want to cache this, it's a spritesheet piece. */
return URL.createObjectURL(await this.get2DBlob("image/png") ?? new Blob([]));
};

View File

@ -85,6 +85,8 @@ const DIRECTORY_PATHS = ([
([
"CHU_UI_Common_Avatar_body_00.dds",
"CHU_UI_Common_Avatar_face_00.dds",
"CHU_UI_Common_01_v11.dds",
"CHU_UI_TeamEmblem_01_v14.dds",
"CHU_UI_title_rank_00_v10.dds"
]).includes(name),
id: (name: string) => name

View File

@ -28,8 +28,9 @@
[ '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') ],
[ 'profileLocation', t('settings.profile.location') ],*/
[ 'profileBio', t('settings.profile.bio') ],
] as const
@ -165,9 +166,14 @@
<div class="field">
<label for={field}>{name}</label>
<div>
<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 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}
@ -256,7 +262,7 @@
gap: 1rem
margin-top: 0.5rem
> input
> input, > textarea
flex: 1
img
@ -266,6 +272,8 @@
object-fit: cover
aspect-ratio: 1
.cropper-container
position: relative
width: 400px

View File

@ -30,14 +30,14 @@
registerChart()
export let username: string;
export let game: GameName = "mai2"
export let game: GameName | "auto" = "auto"
let calElement: HTMLElement
let error: string;
let me: AquaNetUser
title(`User ${username}`)
const rounding = useLocalStorage("rounding", true);
const titleText = GAME_TITLE[game]
const titleText = game != "auto" ? GAME_TITLE[game] : "?"
interface MusicAndPlay extends MusicMeta, GenericGamePlaylog {}
@ -57,6 +57,19 @@
USER.isLoggedIn() && USER.me().then(u => me = u)
CARD.userGames(username).then(games => {
if (game == "auto") {
let targetGames = Object.entries(games)
.map(d => {
if (d[1])
d[1].lastLogin = d[1].lastLogin ? new Date(d[1].lastLogin) : new Date(0);
return d;
}).sort((a,b) => {
return b[1]?.lastLogin - a[1]?.lastLogin;
});
if (targetGames[0])
window.location.href = `/u/${username}/${targetGames[0][0]}`
return;
}
if (!games[game]) {
// Find a valid game
const valid = Object.entries(games).filter(([g, valid]) => valid)
@ -105,10 +118,11 @@
}).catch((e) => { error = e.message; console.error(e) } );
}
if (Object.keys(GAME_TITLE).includes(game)) init()
if (Object.keys(GAME_TITLE).includes(game) || game == "auto") 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
@ -122,9 +136,22 @@
<img use:pfp={d.user.aquaUser} alt="" class="pfp" on:error={pfpNotFound}>
<div class="name-box">
<div class="name-left">
<h2>{d.user.name}</h2>
{#if d.user.aquaUser}
{#if d.user.aquaUser.displayName}
<h2>{d.user.aquaUser?.displayName}</h2>
{:else}
<h2>{d.user.name}</h2>
{/if}
<div class="game-name">
{#if d.user.aquaUser.displayName}
{d.user.name}
{/if}
(@{d.user.aquaUser.username})
</div>
<div class="country">{countryCodeToEmoji(d.user.aquaUser?.country)}</div>
{:else}
<h2>{d.user.name}</h2>
{/if}
</div>
{#if typeof d.user.rival === 'boolean' && game === 'mai2'}
@ -133,19 +160,31 @@
{d.user.rival ? t("UserHome.RemoveRival") : t("UserHome.AddRival")}
</span>
{/if}
{#if me && me.username === username}
<a class="setting-icon clickable" use:tooltip={t("UserHome.Settings")} href="/settings">
<Icon icon="eos-icons:rotating-gear"/>
</a>
{/if}
</div>
<nav>
{#each d.validGames as [g, name]}
<a href={`/u/${username}/${g}`} class:active={game === g}>{name}</a>
{/each}
{#if me && me.username === username}
<a class="setting-icon clickable" use:tooltip={t("UserHome.Settings")} href="/settings">
<Icon icon="eos-icons:rotating-gear"/>
</a>
{/if}
</nav>
</div>
{#if d.user.aquaUser?.profileBio}
<div class="activity-info">
<div class="info-bottom profile-bio-container">
<div class="profile-bio">
<span>{t("settings.profile.bio")}</span>
<span class="profile-bio-text">{d.user.aquaUser?.profileBio}</span>
</div>
</div>
</div>
{/if}
<ChuniUserboxDisplay {game} {username} bind:error={error} />
<div>
@ -272,12 +311,14 @@
</div>
</div>
<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}/>
<!-- 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="Hot 10" comp={d.user.ratingComposition.hot10} {allMusics} {game}/> -->
<!-- <RatingComposition title="N10" comp={d.user.ratingComposition.next10} {allMusics} {game}/> -->
<RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} {game} top={10}/>
<RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} game={game != "auto" ? game : "mai2"} top={10}/>
<div class="recent">
<h2>{t('UserHome.RecentScores')}</h2>
@ -298,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)[2].toString()[0]}`}>
<span class="rank-text">{("" + getMult(r.achievement, game)[2]).replace("p", "+")}</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-num" use:tooltip={(r.achievement / 10000).toFixed(4)}>
{
rounding.value ?
roundFloor(r.achievement, game, 1) :
roundFloor(r.achievement, game != "auto" ? game : "mai2", 1) :
(r.achievement / 10000).toFixed(4)
}%
</span>
@ -357,6 +398,9 @@
display: flex
align-items: center
position: relative
z-index: 20
.name-box
flex: 1
display: flex
@ -367,6 +411,16 @@
.name-left
display: flex
gap: 1em
position: relative
.game-name
position: absolute
left: 0.5em
bottom: 0
transform: translate(0, 75%)
opacity: 50%
white-space: nowrap
max-width: 50%
.pfp
width: 100px
@ -403,6 +457,16 @@
.info-bottom
width: max-content
&.profile-bio-container,
&.profile-bio-container div
width: 100%
.profile-bio-text
white-space: pre
max-height: 10em
overflow-y: auto
flex: 1
.info-top > div > span:last-child
font-size: 1.5rem