implement memorial photo viewer (#119)

* commit current progress\

will prob work on my mac ltr

* more transferring to different device

* grammar

* [F] Fix warning inconsistency

* [O] Split status overlays

* [S] Better styling

* [+] i18n

* [+] Display photos tab conditionally

---------

Co-authored-by: Azalea <22280294+hykilpikonna@users.noreply.github.com>
v1-dev
alix 2025-03-01 01:35:49 -05:00 committed by GitHub
parent 6c21afaa57
commit eef40e39d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 210 additions and 106 deletions

Binary file not shown.

View File

@ -4,9 +4,10 @@
import UserHome from "./pages/UserHome.svelte"; import UserHome from "./pages/UserHome.svelte";
import Home from "./pages/Home.svelte"; import Home from "./pages/Home.svelte";
import Ranking from "./pages/Ranking.svelte"; import Ranking from "./pages/Ranking.svelte";
import { USER } from "./libs/sdk"; import { CARD, USER } from "./libs/sdk";
import type { AquaNetUser } from "./libs/generalTypes"; import type { AquaNetUser } from "./libs/generalTypes";
import Settings from "./pages/User/Settings.svelte"; import Settings from "./pages/User/Settings.svelte";
import MaiPhoto from "./pages/MaiPhoto.svelte";
import { pfp, tooltip } from "./libs/ui" import { pfp, tooltip } from "./libs/ui"
import { ANNOUNCEMENT } from "./libs/config"; import { ANNOUNCEMENT } from "./libs/config";
import { t } from "./libs/i18n"; import { t } from "./libs/i18n";
@ -25,9 +26,18 @@
export let url = ""; export let url = "";
let me: AquaNetUser let me: AquaNetUser
let playedMai = false
if (USER.isLoggedIn()) USER.me().then(m => me = m).catch(e => console.error(e)) if (USER.isLoggedIn())
{
USER.me().then(m => {
me = m
CARD.userGames(me.username).then(game => {
playedMai = !!game.mai2
})
}).catch(e => console.error(e))
}
let path = window.location.pathname; let path = window.location.pathname;
</script> </script>
@ -44,11 +54,14 @@
</div> </div>
{/if} {/if}
<a href="/home">{t('navigation.home').toLowerCase()}</a> <a href="/home">{t('navigation.home').toLowerCase()}</a>
<div on:click={() => alert("Coming soon™")} on:keydown={e => e.key === "Enter" && alert("Coming soon™")} <!-- <div on:click={() => alert("Coming soon™")} on:keydown={e => e.key === "Enter" && alert("Coming soon™")}
role="button" tabindex="0">{t('navigation.maps').toLowerCase()}</div> role="button" tabindex="0">{t('navigation.maps').toLowerCase()}</div> -->
<a href="/ranking">{t('navigation.rankings').toLowerCase()}</a> <a href="/ranking">{t('navigation.rankings').toLowerCase()}</a>
{#if playedMai}
<a href="/pictures">photo</a>
{/if}
{#if me} {#if me}
<a href="/u/{me.username}" use:tooltip={t('navigation.profile')}> <a href="/u/{me.username}" use:tooltip={t('navigation.profile')}>
<img alt="profile" class="pfp" use:pfp={me}/> <img alt="profile" class="pfp" use:pfp={me}/>
</a> </a>
{/if} {/if}
@ -62,6 +75,7 @@
<Route path="/u/:username" component={UserHome} /> <Route path="/u/:username" component={UserHome} />
<Route path="/u/:username/:game" component={UserHome} /> <Route path="/u/:username/:game" component={UserHome} />
<Route path="/settings" component={Settings} /> <Route path="/settings" component={Settings} />
<Route path="/pictures" component={MaiPhoto} />
</Router> </Router>
<style lang="sass"> <style lang="sass">

View File

@ -1,87 +1,54 @@
<!-- Svelte 4.2.11 --> <!-- Svelte 4.2.11 -->
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition' import { fade } from 'svelte/transition'
import type { ConfirmProps } from "../libs/generalTypes"; import type { ConfirmProps } from "../libs/generalTypes";
import { DISCORD_INVITE } from "../libs/config"; import { DISCORD_INVITE } from "../libs/config";
import Icon from "@iconify/svelte"; import { t } from "../libs/i18n"
import { t } from "../libs/i18n" import Loading from './ui/Loading.svelte';
import Error from './ui/Error.svelte';
// Props
export let confirm: ConfirmProps | null = null // Props
export let error: string | null export let confirm: ConfirmProps | null = null
export let loading: boolean = false export let error: string | null
</script> export let loading: boolean = false
</script>
{#if confirm}
<div class="overlay" transition:fade> {#if confirm}
<div> <div class="overlay" transition:fade>
<h2>{confirm.title}</h2> <div>
<span>{confirm.message}</span> <h2>{confirm.title}</h2>
<span>{confirm.message}</span>
<div class="actions">
{#if confirm.cancel} <div class="actions">
<!-- Svelte LSP is very annoying here --> {#if confirm.cancel}
<button on:click={() => { <!-- Svelte LSP is very annoying here -->
confirm && confirm.cancel && confirm.cancel() <button on:click={() => {
confirm && confirm.cancel && confirm.cancel()
// Set to null
confirm = null // Set to null
}}>{t('action.cancel')}</button> confirm = null
{/if} }}>{t('action.cancel')}</button>
<button on:click={() => confirm && confirm.confirm()} class:error={confirm.dangerous}>{t('action.confirm')}</button> {/if}
</div> <button on:click={() => confirm && confirm.confirm()} class:error={confirm.dangerous}>{t('action.confirm')}</button>
</div> </div>
</div> </div>
{/if} </div>
{/if}
{#if error}
<div class="overlay" transition:fade> {#if error}
<div> <Error {error}/>
<h2 class="error">{t('status.error')}</h2> {/if}
<span>{t('status.error.hint')}<a href={DISCORD_INVITE}>{t('status.error.hint.link')}</a></span>
<span>{t('status.detail', { detail: error })}</span> {#if loading && !error}
<Loading/>
<div class="actions"> {/if}
<button on:click={() => location.reload()} class="error">
{t('action.refresh')} <style lang="sass">
</button> .actions
</div> display: flex
</div> gap: 16px
</div>
{/if} button
width: 100%
{#if loading && !error} </style>
<div class="overlay loading" transition:fade>
<Icon class="icon" icon="svg-spinners:pulse-2"/>
<span><span>LOADING</span></span>
</div>
{/if}
<style lang="sass">
.actions
display: flex
gap: 16px
button
width: 100%
.loading.overlay
font-size: 28rem
:global(.icon)
opacity: 0.5
> span
position: absolute
inset: 0
display: flex
justify-content: center
align-items: center
background: transparent
letter-spacing: 20px
margin-left: 20px
font-size: 1.5rem
</style>

View File

@ -9,9 +9,9 @@
</script> </script>
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields"> <div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
<p class="warning"> <blockquote>
{ts("settings.gameNotice")} {ts("settings.gameNotice")}
</p> </blockquote>
<GameSettingFields game="general"/> <GameSettingFields game="general"/>
<div class="field"> <div class="field">
<div class="bool"> <div class="bool">
@ -59,14 +59,4 @@
> input > input
flex: 1 flex: 1
.warning
background: #aa555510
padding: 10px
border-left: solid 2px vars.$c-error
&::before
color: vars.$c-error
font-weight: bold
content: ""
</style> </style>

View File

@ -0,0 +1,30 @@
<script lang="ts">
import { fade } from "svelte/transition";
import { t } from "../../libs/i18n";
import { DISCORD_INVITE } from "../../libs/config";
export let error: string;
</script>
<div class="overlay" transition:fade>
<div>
<h2 class="error">{t('status.error')}</h2>
<span>{t('status.error.hint')}<a href={DISCORD_INVITE}>{t('status.error.hint.link')}</a></span>
<span>{t('status.detail', { detail: error })}</span>
<div class="actions">
<button on:click={() => location.reload()} class="error">
{t('action.refresh')}
</button>
</div>
</div>
</div>
<style lang="sass">
.actions
display: flex
gap: 16px
button
width: 100%
</style>

View File

@ -0,0 +1,30 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { fade } from 'svelte/transition'
</script>
<div class="overlay loading" transition:fade>
<Icon class="icon" icon="svg-spinners:pulse-2"/>
<span><span>LOADING</span></span>
</div>
<style lang="sass">
.loading.overlay
font-size: 28rem
:global(.icon)
opacity: 0.5
> span
position: absolute
inset: 0
display: flex
justify-content: center
align-items: center
background: transparent
letter-spacing: 20px
margin-left: 20px
font-size: 1.5rem
</style>

View File

@ -229,7 +229,15 @@ export const EN_REF_USERBOX = {
'userbox.new.error.invalidUrl': 'The URL you inputted is invalid.' 'userbox.new.error.invalidUrl': 'The URL you inputted is invalid.'
} }
export const EN_REF_MAI_PHOTO = {
'maiphoto.title': 'Mai Memorial Photo Gallery',
'maiphoto.url_warning': 'Note: If you want to share a photo with your friend, please save the photo. Do not copy image URL because the URL contains sensitive information.',
'maiphoto.none': 'No photo found. You can upload photo by clicking upload at the end of each game session.',
}
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL, export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,
...EN_REF_LEADERBOARD, ...EN_REF_HOME, ...EN_REF_SETTINGS, ...EN_REF_USERBOX } ...EN_REF_LEADERBOARD, ...EN_REF_HOME, ...EN_REF_SETTINGS, ...EN_REF_USERBOX,
...EN_REF_MAI_PHOTO
}
export type LocalizedMessages = typeof EN_REF export type LocalizedMessages = typeof EN_REF

View File

@ -2,6 +2,7 @@ import {
EN_REF_GENERAL, EN_REF_GENERAL,
EN_REF_HOME, EN_REF_HOME,
EN_REF_LEADERBOARD, EN_REF_LEADERBOARD,
EN_REF_MAI_PHOTO,
EN_REF_SETTINGS, EN_REF_SETTINGS,
EN_REF_USER, EN_REF_USER,
EN_REF_USERBOX, EN_REF_USERBOX,
@ -234,6 +235,11 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.new.error.invalidUrl': '输入的 URL 无效。' 'userbox.new.error.invalidUrl': '输入的 URL 无效。'
}; };
export const zhMaiPhoto: typeof EN_REF_MAI_PHOTO = {
'maiphoto.title': 'Mai 纪念照片库',
'maiphoto.url_warning': '注意:如果想与朋友分享图片的话,请先保存照片再发出去。不要复制图片 URL因为 URL 中包含 AquaDX 账号信息。',
'maiphoto.none': '还没有图片哦~ 可以在每次游戏结束的时候点击上传来上传照片。',
}
export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral, export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral,
...zhLeaderboard, ...zhHome, ...zhSettings, ...zhUserbox } ...zhLeaderboard, ...zhHome, ...zhSettings, ...zhUserbox, ...zhMaiPhoto }

View File

@ -284,6 +284,8 @@ export const CARD = {
export const GAME = { export const GAME = {
trend: (username: string, game: GameName): Promise<TrendEntry[]> => trend: (username: string, game: GameName): Promise<TrendEntry[]> =>
post(`/api/v2/game/${game}/trend`, { username }), post(`/api/v2/game/${game}/trend`, { username }),
photos: (): Promise<string[]> =>
post(`/api/v2/game/mai2/my-photo`, { }),
userSummary: (username: string, game: GameName): Promise<GenericGameSummary> => userSummary: (username: string, game: GameName): Promise<GenericGameSummary> =>
post(`/api/v2/game/${game}/user-summary`, { username }), post(`/api/v2/game/${game}/user-summary`, { username }),
ranking: (game: GameName): Promise<GenericRanking[]> => ranking: (game: GameName): Promise<GenericRanking[]> =>

View File

@ -0,0 +1,57 @@
<script lang="ts">
import {GAME} from "../libs/sdk";
import {AQUA_HOST} from "../libs/config";
import Loading from "../components/ui/Loading.svelte";
import Error from "../components/ui/Error.svelte";
import { t } from "../libs/i18n";
const token = localStorage.getItem("token")
</script>
<main class="content">
<div class="outer-title-options">
<h2>{t("maiphoto.title")}</h2>
</div>
{#await GAME.photos()}
<Loading/>
{:then photos}
{#if photos.length === 0}
<blockquote>{t('maiphoto.none')}</blockquote>
{:else}
<blockquote>{t('maiphoto.url_warning')}</blockquote>
{/if}
<div class="pictures">
{#each photos as photo}
<div class="photo-container">
<img class="rounded-2xl" src="{AQUA_HOST}/api/v2/game/mai2/my-photo/{photo}?token={token}" alt="Memorial" />
</div>
{/each}
</div>
{:catch error}
<Error {error}/>
{/await}
</main>
<style lang="sass">
@use "../vars"
.pictures
display: flex
flex-wrap: wrap
justify-content: center
row-gap: 1rem
gap: 1rem
.photo-container
flex: 1 1 300px
min-width: 280px
max-width: 100%
display: flex
justify-content: center
.photo-container img
width: 100%
height: auto
object-fit: contain
</style>