mirror of https://github.com/hykilpikonna/AquaDX
Merge branch 'v1-dev' into pr/119
commit
338819416f
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
|
||||
.tooltip
|
||||
position: absolute
|
||||
z-index: 1000
|
||||
z-index: 900
|
||||
background: white
|
||||
padding: 10px 16px
|
||||
border-radius: vars.$border-radius
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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([]));
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue