Merge branch 'v1-dev' into pr/99

This commit is contained in:
Azalea
2025-01-05 06:54:15 -05:00
122 changed files with 1592 additions and 3401 deletions

View File

@@ -92,7 +92,7 @@
user = u
return fetchData()
}).catch((e) => { loading = false; error = e.message });
let DDSreader: DDS | undefined;
let USERBOX_PROGRESS = 0;
@@ -150,7 +150,7 @@
if (databaseExists || USERBOX_URL_STATE.value) {
DDSreader = new DDS(ddsDB);
USERBOX_INSTALLED = databaseExists || USERBOX_URL_STATE.value != "";
}
}
})
</script>
@@ -183,10 +183,10 @@
</div>
{:else}
<div class="chuni-userbox-container">
<ChuniUserplateComponent chuniIsUserbox={true} on:click={() => userboxSelected = "nameplateId"} chuniCharacter={userbox.characterId} chuniLevel={userbox.level.toString()} chuniRating={userbox.playerRating / 100}
<ChuniUserplateComponent chuniIsUserbox={true} on:click={() => userboxSelected = "nameplateId"} chuniCharacter={userbox.characterId} chuniLevel={userbox.level.toString()} chuniRating={userbox.playerRating / 100}
chuniNameplate={userbox.nameplateId} chuniName={userbox.userName} chuniTrophyName={allItems.trophy[userbox.trophyId].name}></ChuniUserplateComponent>
<ChuniPenguinComponent chuniWear={userbox.avatarWear} chuniHead={userbox.avatarHead} chuniBack={userbox.avatarBack}
chuniFront={userbox.avatarFront} chuniFace={userbox.avatarFace} chuniItem={userbox.avatarItem}
<ChuniPenguinComponent chuniWear={userbox.avatarWear} chuniHead={userbox.avatarHead} chuniBack={userbox.avatarBack}
chuniFront={userbox.avatarFront} chuniFace={userbox.avatarFace} chuniItem={userbox.avatarItem}
chuniSkin={userbox.avatarSkin}></ChuniPenguinComponent>
</div>
<div class="chuni-userbox-row">
@@ -258,26 +258,11 @@
<p>
<button on:click={() => USERBOX_SETUP_RUN = !USERBOX_SETUP_RUN}>{t(!USERBOX_INSTALLED ? `userbox.new.activate_first` : `userbox.new.activate_update`)}</button>
</p>
{/if}
{/if}
<ChuniMatchingSettings/>
<!--{#if !USERBOX_SUPPORT || !USERBOX_INSTALLED || !USERBOX_ENABLED.value}
<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}
{#if USERBOX_SETUP_RUN && !error}
<div class="overlay" transition:fade>
<div>
@@ -328,7 +313,7 @@ p.notice
opacity: 0.6
margin-top: 0
.progress
.progress
width: 100%
height: 10px
box-shadow: 0 0 1px 1px vars.$ov-lighter
@@ -463,10 +448,10 @@ p.notice
&.focused
filter: brightness(75%)
.chuni-userbox
.chuni-userbox
width: calc(100% - 20px)
height: 350px
display: flex
flex-direction: row
flex-wrap: wrap

View File

@@ -161,4 +161,4 @@ export interface ChusanMatchingOption {
matching: string
reflector: string
coop: string[]
}
}

View File

@@ -102,7 +102,7 @@ const zhHome: typeof EN_REF_HOME = {
'home.linkcard.account-card': "账户卡",
'home.linkcard.registered': "注册于",
'home.linkcard.lastused': "上次使用",
'home.linkcard.enter-info': "请输入以下信息",
'home.linkcard.enter-info': "请输入以下信息,或将 aime.txt / felica.txt 文件拖放到此区域",
'home.linkcard.access-code': "卡背面的20位卡号 (如果没有, 请尝试在游戏中扫描您的卡, 并输入屏幕上显示的卡号)",
'home.linkcard.enter-sn1': "在您的手机",
'home.linkcard.enter-sn2': "上下载 NFC Tools 并扫描您的卡。然后输入显示的 SN 号。",
@@ -148,10 +148,14 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999',
'settings.fields.waccaAlwaysVip.name': 'Wacca: 永久会员',
'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01',
'settings.fields.chusanTeamName.name': '中二: 队伍名称',
'settings.fields.chusanTeamName.name': '队伍名称',
'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。',
'settings.fields.chusanInfinitePenguins.name': '中二: 无限企鹅',
'settings.fields.chusanInfinitePenguins.name': '我是桐谷遥',
'settings.fields.chusanInfinitePenguins.desc': '将角色界限突破的企鹅雕像数量设置为 999。',
'settings.fields.chusanMatchingReflector.name': '全国对战 Reflector',
'settings.fields.chusanMatchingReflector.desc': '全国对战服务器的 UDP 反射服务器的 URL',
'settings.fields.chusanMatchingServer.name': '全国对战服务器',
'settings.fields.chusanMatchingServer.desc': '全国对战服务器的 URL',
'settings.fields.rounding.name': '分数舍入',
'settings.fields.rounding.desc': '把分数四舍五入到一位小数',
'settings.fields.optOutOfLeaderboard.name': '不参与排行榜',
@@ -168,10 +172,12 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.profile.unset': '未设置',
'settings.profile.unchanged': '未更改',
'settings.export': '导出玩家数据',
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置'
}
export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.header.general': '游戏设置',
'userbox.header.matching': '全国对战',
'userbox.header.userbox': 'UserBox 设置',
'userbox.header.preview': 'UserBox 预览',
'userbox.nameplateId': '名牌',
@@ -189,7 +195,15 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.preview.notice': '「生存战略」:为了尊重版权,我们不会提供游戏内物品的图片。但是如果你认识其他愿意提供图床的人,在这里输入 URL 就可以显示出预览。',
'userbox.preview.url': '图床 URL',
'userbox.error.nodata': '未找到中二数据',
'userbox.matching.select': '选择对战服务器',
'userbox.matching.select.sub': '选择你想加入的跨服全国对战服务器',
'userbox.matching.option.ui': '房间列表',
'userbox.matching.option.guide': '教程',
'userbox.matching.option.collab': '合作伙伴',
'userbox.matching.custom.name': '自定义',
'userbox.matching.custom.sub': '输入其他的匹配 URL',
'userbox.new.name': 'AquaBox',
'userbox.new.setup': '将 ChuniLumi 或更高版本)的游戏文件夹拖放到下方区域,以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。',
'userbox.new.setup.processing_file': '正在处理文件',

View File

@@ -312,4 +312,6 @@ export const SETTING = {
post('/api/v2/settings/get', {}),
set: (key: string, value: any) =>
post('/api/v2/settings/set', { key, value: `${value}` }),
detailSet: (game: string, field: string, value: any) =>
post(`/api/v2/game/${game}/user-detail-set`, { field, value }),
}

View File

@@ -25,7 +25,7 @@ const validateDirectories = async (base: FileSystemDirectoryEntry, path: string)
let newDirectory = await getDirectory(directory, part).catch(_ => null);
if (newDirectory && isDirectory(newDirectory)) {
directory = newDirectory;
} else
} else
return false;
};
return true
@@ -38,7 +38,7 @@ const getDirectoryFromPath = async (base: FileSystemDirectoryEntry, path: string
let newDirectory = await getDirectory(directory, part).catch(_ => null);
if (newDirectory && isDirectory(newDirectory)) {
directory = newDirectory;
} else
} else
return null;
};
return directory;
@@ -81,7 +81,7 @@ const DIRECTORY_PATHS = ([
processName: "Surfboard Textures",
useFileName: true,
path: "surfboard",
filter: (name: string) =>
filter: (name: string) =>
([
"CHU_UI_Common_Avatar_body_00.dds",
"CHU_UI_Common_Avatar_face_00.dds",
@@ -134,7 +134,7 @@ export const scanOptionFolder = async (optionFolder: FileSystemDirectoryEntry, p
let objectStore = transaction.objectStore('dds');
for (let object of data)
objectStore.put(object)
// await transaction completion
await new Promise(r => transaction.addEventListener("complete", r, {once: true}))
};
@@ -163,7 +163,7 @@ export function initializeDb() : Promise<void> {
export async function userboxFileProcess(folder: FileSystemEntry, progressUpdate: (progress: number, progressString: string) => void): Promise<string | null> {
if (!isDirectory(folder))
return t("userbox.new.error.invalidFolder")
if (!(await validateDirectories(folder, "bin/option")) || !(await validateDirectories(folder, "data/A000")))
if (!(await validateDirectories(folder, "bin/option")) && !(await validateDirectories(folder, "data/A000")))
return t("userbox.new.error.invalidFolder");
initializeDb();
@@ -179,4 +179,4 @@ export async function userboxFileProcess(folder: FileSystemEntry, progressUpdate
location.reload();
return null
}
}

View File

@@ -161,13 +161,10 @@
let inputAC = ""
let errorAC = ""
function inputACChange(e: any) {
e = e as InputEvent
function inputACChange() {
// Add spaces to the input
const old = inputAC
if (e.inputType === "insertText" && inputAC.length % 5 === 4 && inputAC.length < 24)
inputAC += " "
inputAC = inputAC.slice(0, 24)
inputAC = inputAC.replace(/\D/g, '').replace(/(.{4})/g, '$1 ').replace(/ $/, '')
if (inputAC !== old) errorAC = ""
}
@@ -176,13 +173,10 @@
let inputSN = ""
let errorSN = ""
function inputSNChange(e: any) {
e = e as InputEvent
function inputSNChange() {
// Add colons to the input
const old = inputSN
if (e.inputType === "insertText" && inputSN.length % 3 === 2 && inputSN.length < 23)
inputSN += ":"
inputSN = inputSN.toUpperCase().slice(0, 23)
inputSN = inputSN.toUpperCase().replace(/[^0-9A-F]/g, '').replace(/(.{2})/g, '$1:').replace(/:$/, '')
if (inputSN !== old) errorSN = ""
}
@@ -209,9 +203,29 @@
function isInput(e: KeyboardEvent) {
return e.key.length === 1 && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey
}
async function dropFile(e: DragEvent) {
e.preventDefault()
e.stopPropagation()
const file = e.dataTransfer?.files[0]
if (!file) return
switch (file.name.toLowerCase()) {
case "aime.txt":
inputSN = ""
inputAC = await file.text()
inputACChange()
break
case "felica.txt":
inputAC = ""
inputSN = await file.text()
inputSNChange()
break
}
}
</script>
<div class="link-card">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="link-card" on:drop={dropFile} on:dragover={(e) => e.preventDefault()}>
<h2>{t('home.linkcard.cards')}</h2>
<p>{t('home.linkcard.description')}:</p>

View File

@@ -195,7 +195,7 @@
<div class="rank">
<span>{t('UserHome.ServerRank')}</span>
<span>#{+d.user.serverRank.toLocaleString() + 1}</span>
<span>#{(d.user.serverRank + 1).toLocaleString()}</span>
</div>
</div>

View File

@@ -1,251 +1,255 @@
<script lang="ts">
import { Turnstile } from "svelte-turnstile";
import { slide } from 'svelte/transition';
import { TURNSTILE_SITE_KEY } from "../libs/config";
import Icon from "@iconify/svelte";
import { USER } from "../libs/sdk";
import { t } from "../libs/i18n"
let params = new URLSearchParams(window.location.search)
let state = "home"
$: isSignup = state === "signup"
let submitting = false
let email = ""
let password = ""
let username = ""
let turnstile = ""
let turnstileReset: () => void | undefined;
let error = ""
let verifyMsg = ""
if (params.get('confirm-email')) {
state = 'verify'
verifyMsg = t("welcome.verifying")
submitting = true
// Send request to server
USER.confirmEmail(params.get('confirm-email')!)
.then(() => {
verifyMsg = t('welcome.verified')
submitting = false
// Clear the query param
window.history.replaceState({}, document.title, window.location.pathname)
})
.catch(e => verifyMsg = t('welcome.verification-failed', { message: e.message }))
}
async function submit(): Promise<any> {
submitting = true
// Check if username and password are valid
if (email === "" || password === "") {
error = t("welcome.email-password-missing")
return submitting = false
}
if (turnstile === "") {
// Sleep for 100ms to allow Turnstile to finish
error = t("welcome.waiting-turnstile")
return setTimeout(submit, 100)
}
// Signup
if (isSignup) {
if (username === "") {
error = t("welcome.username-missing")
return submitting = false
}
// Send request to server
await USER.register({ username, email, password, turnstile })
.then(() => {
// Show verify email message
state = 'verify'
verifyMsg = t("welcome.verification-sent", { email })
})
.catch(e => {
error = e.message
submitting = false
turnstileReset()
})
}
else {
// Send request to server
await USER.login({ email, password, turnstile })
.then(() => window.location.href = "/home")
.catch(e => {
if (e.message === 'Email not verified - STATE_0') {
state = 'verify'
verifyMsg = t("welcome.verify-state-0")
}
else if (e.message === 'Email not verified - STATE_1') {
state = 'verify'
verifyMsg = t("welcome.verify-state-1")
}
else if (e.message === 'Email not verified - STATE_2') {
state = 'verify'
verifyMsg = t("welcome.verify-state-2")
}
else {
error = e.message
submitting = false
turnstileReset()
}
})
}
submitting = false
}
</script>
<main id="home" class="no-margin">
<div>
<h1 id="title">AquaNet</h1>
{#if state === "home"}
<div class="btn-group" transition:slide>
<button on:click={() => state = 'login'}>{t('welcome.btn-login')}</button>
<button on:click={() => state = 'signup'}>{t('welcome.btn-signup')}</button>
</div>
{:else if state === "login" || state === "signup"}
<div class="login-form" transition:slide>
{#if error}
<span class="error">{error}</span>
{/if}
<div on:click={() => state = 'home'} on:keypress={() => state = 'home'}
role="button" tabindex="0" class="clickable">
<Icon icon="line-md:chevron-small-left" />
<span>{t('back')}</span>
</div>
{#if isSignup}
<input type="text" placeholder={t('username')} bind:value={username}>
{/if}
<input type="email" placeholder={t('email')} bind:value={email}>
<input type="password" placeholder={t('password')} bind:value={password}>
<button on:click={submit}>
{#if submitting}
<Icon icon="line-md:loading-twotone-loop"/>
{:else}
{isSignup ? t('welcome.btn-signup') : t('welcome.btn-login')}
{/if}
</button>
<Turnstile siteKey={TURNSTILE_SITE_KEY} bind:reset={turnstileReset}
on:turnstile-callback={e => console.log(turnstile = e.detail.token)}
on:turnstile-error={_ => console.log(error = t("welcome.turnstile-error"))}
on:turnstile-expired={_ => window.location.reload()}
on:turnstile-timeout={_ => console.log(error = t('welcome.turnstile-timeout'))} />
</div>
{:else if state === "verify"}
<div class="login-form" transition:slide>
<span>{verifyMsg}</span>
{#if !submitting}
<button on:click={() => state = 'home'} transition:slide>{t('back')}</button>
{/if}
</div>
{/if}
</div>
<div class="light-pollution">
<div class="l1"></div>
<div class="l2"></div>
<div class="l3"></div>
</div>
</main>
<style lang="sass">
@use "../vars"
.login-form
display: flex
flex-direction: column
gap: 8px
width: calc(100% - 12px)
max-width: 300px
div.clickable
display: flex
align-items: center
#home
color: vars.$c-main
position: relative
width: 100%
height: 100%
padding-left: 100px
overflow: hidden
background-color: black
box-sizing: border-box
display: flex
flex-direction: column
justify-content: center
margin-top: -(vars.$nav-height)
// Content container
> div
display: flex
flex-direction: column
align-items: flex-start
width: max-content
// Switching state container
> div
transition: vars.$transition
#title
font-family: Quicksand, vars.$font
user-select: none
// Gap between text characters
letter-spacing: 0.2em
margin-top: 0
margin-bottom: 32px
opacity: 0.9
.btn-group
display: flex
gap: 8px
.light-pollution
pointer-events: none
opacity: 0.8
> div
position: absolute
z-index: 1
.l1
left: -560px
top: 90px
height: 1130px
width: 1500px
$color: rgb(158, 110, 230)
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba(0,0,0,0) 100%)
.l2
left: -200px
top: 560px
height: 1200px
width: 1500px
$color: rgb(92, 195, 250)
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba(0,0,0,0) 100%)
.l3
left: -600px
opacity: 0.7
top: -630px
width: 1500px
height: 1000px
$color: rgb(230, 110, 156)
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba(0,0,0,0) 100%)
@media (max-width: 500px)
align-items: center
padding-left: 0
</style>
<script lang="ts">
import { Turnstile } from "svelte-turnstile";
import { slide } from 'svelte/transition';
import { TURNSTILE_SITE_KEY } from "../libs/config";
import Icon from "@iconify/svelte";
import { USER } from "../libs/sdk";
import { t } from "../libs/i18n"
let params = new URLSearchParams(window.location.search)
let state = "home"
$: isSignup = state === "signup"
let submitting = false
let email = ""
let password = ""
let username = ""
let turnstile = ""
let turnstileReset: () => void | undefined;
let error = ""
let verifyMsg = ""
if (USER.isLoggedIn()) {
window.location.href = "/home"
}
if (params.get('confirm-email')) {
state = 'verify'
verifyMsg = t("welcome.verifying")
submitting = true
// Send request to server
USER.confirmEmail(params.get('confirm-email')!)
.then(() => {
verifyMsg = t('welcome.verified')
submitting = false
// Clear the query param
window.history.replaceState({}, document.title, window.location.pathname)
})
.catch(e => verifyMsg = t('welcome.verification-failed', { message: e.message }))
}
async function submit(): Promise<any> {
submitting = true
// Check if username and password are valid
if (email === "" || password === "") {
error = t("welcome.email-password-missing")
return submitting = false
}
if (turnstile === "") {
// Sleep for 100ms to allow Turnstile to finish
error = t("welcome.waiting-turnstile")
return setTimeout(submit, 100)
}
// Signup
if (isSignup) {
if (username === "") {
error = t("welcome.username-missing")
return submitting = false
}
// Send request to server
await USER.register({ username, email, password, turnstile })
.then(() => {
// Show verify email message
state = 'verify'
verifyMsg = t("welcome.verification-sent", { email })
})
.catch(e => {
error = e.message
submitting = false
turnstileReset()
})
}
else {
// Send request to server
await USER.login({ email, password, turnstile })
.then(() => window.location.href = "/home")
.catch(e => {
if (e.message === 'Email not verified - STATE_0') {
state = 'verify'
verifyMsg = t("welcome.verify-state-0")
}
else if (e.message === 'Email not verified - STATE_1') {
state = 'verify'
verifyMsg = t("welcome.verify-state-1")
}
else if (e.message === 'Email not verified - STATE_2') {
state = 'verify'
verifyMsg = t("welcome.verify-state-2")
}
else {
error = e.message
submitting = false
turnstileReset()
}
})
}
submitting = false
}
</script>
<main id="home" class="no-margin">
<div>
<h1 id="title">AquaNet</h1>
{#if state === "home"}
<div class="btn-group" transition:slide>
<button on:click={() => state = 'login'}>{t('welcome.btn-login')}</button>
<button on:click={() => state = 'signup'}>{t('welcome.btn-signup')}</button>
</div>
{:else if state === "login" || state === "signup"}
<div class="login-form" transition:slide>
{#if error}
<span class="error">{error}</span>
{/if}
<div on:click={() => state = 'home'} on:keypress={() => state = 'home'}
role="button" tabindex="0" class="clickable">
<Icon icon="line-md:chevron-small-left" />
<span>{t('back')}</span>
</div>
{#if isSignup}
<input type="text" placeholder={t('username')} bind:value={username}>
{/if}
<input type="email" placeholder={t('email')} bind:value={email}>
<input type="password" placeholder={t('password')} bind:value={password}>
<button on:click={submit}>
{#if submitting}
<Icon icon="line-md:loading-twotone-loop"/>
{:else}
{isSignup ? t('welcome.btn-signup') : t('welcome.btn-login')}
{/if}
</button>
<Turnstile siteKey={TURNSTILE_SITE_KEY} bind:reset={turnstileReset}
on:turnstile-callback={e => console.log(turnstile = e.detail.token)}
on:turnstile-error={_ => console.log(error = t("welcome.turnstile-error"))}
on:turnstile-expired={_ => window.location.reload()}
on:turnstile-timeout={_ => console.log(error = t('welcome.turnstile-timeout'))} />
</div>
{:else if state === "verify"}
<div class="login-form" transition:slide>
<span>{verifyMsg}</span>
{#if !submitting}
<button on:click={() => state = 'home'} transition:slide>{t('back')}</button>
{/if}
</div>
{/if}
</div>
<div class="light-pollution">
<div class="l1"></div>
<div class="l2"></div>
<div class="l3"></div>
</div>
</main>
<style lang="sass">
@use "../vars"
.login-form
display: flex
flex-direction: column
gap: 8px
width: calc(100% - 12px)
max-width: 300px
div.clickable
display: flex
align-items: center
#home
color: vars.$c-main
position: relative
width: 100%
height: 100%
padding-left: 100px
overflow: hidden
background-color: black
box-sizing: border-box
display: flex
flex-direction: column
justify-content: center
margin-top: -(vars.$nav-height)
// Content container
> div
display: flex
flex-direction: column
align-items: flex-start
width: max-content
// Switching state container
> div
transition: vars.$transition
#title
font-family: Quicksand, vars.$font
user-select: none
// Gap between text characters
letter-spacing: 0.2em
margin-top: 0
margin-bottom: 32px
opacity: 0.9
.btn-group
display: flex
gap: 8px
.light-pollution
pointer-events: none
opacity: 0.8
> div
position: absolute
z-index: 1
.l1
left: -560px
top: 90px
height: 1130px
width: 1500px
$color: rgb(158, 110, 230)
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba(0,0,0,0) 100%)
.l2
left: -200px
top: 560px
height: 1200px
width: 1500px
$color: rgb(92, 195, 250)
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba(0,0,0,0) 100%)
.l3
left: -600px
opacity: 0.7
top: -630px
width: 1500px
height: 1000px
$color: rgb(230, 110, 156)
background: radial-gradient(50% 50% at 50% 50%, rgba($color, 0.28) 0%, rgba(0,0,0,0) 100%)
@media (max-width: 500px)
align-items: center
padding-left: 0
</style>