[+] i18n rest
This commit is contained in:
+114
-1
@@ -1,4 +1,3 @@
|
||||
|
||||
export default {
|
||||
home: {
|
||||
titles: {
|
||||
@@ -13,5 +12,119 @@ export default {
|
||||
text: {
|
||||
playlistCreatedBy: 'From {u}'
|
||||
}
|
||||
},
|
||||
admin: {
|
||||
neteaseLogin: {
|
||||
title: 'NetEase Login',
|
||||
scanTitle: 'Scan to Login',
|
||||
scanTip: 'Please use NetEase Music App to scan',
|
||||
generating: 'Generating QR Code...',
|
||||
scanned: 'Scanned',
|
||||
confirm: 'Please confirm login on your phone',
|
||||
success: 'Login Successful',
|
||||
errorPrefix: 'Error: '
|
||||
}
|
||||
},
|
||||
import: {
|
||||
netease: {
|
||||
title: 'Import from NetEase',
|
||||
status: {
|
||||
importing: 'Importing',
|
||||
success: 'Import Complete',
|
||||
error: 'Import Failed'
|
||||
},
|
||||
songs: 'songs',
|
||||
tip: 'Go to NetEase Music App, find a Japanese playlist you like, click share, copy link, and paste it here to start importing!',
|
||||
inputLabel: 'NetEase Playlist Link / ID',
|
||||
btnStart: 'Start Import',
|
||||
btnView: 'View Playlist'
|
||||
}
|
||||
},
|
||||
playlist: {
|
||||
detail: {
|
||||
title: 'Playlist Details',
|
||||
creator: 'Creator: ',
|
||||
count: 'Songs: ',
|
||||
startPractice: 'Start Practice',
|
||||
songList: 'Song List',
|
||||
songs: 'songs'
|
||||
},
|
||||
list: {
|
||||
mine: 'My Playlists',
|
||||
rec: 'Recommended Playlists',
|
||||
import: 'Import from NetEase',
|
||||
created: 'Created by {u}',
|
||||
count: '{n} songs'
|
||||
}
|
||||
},
|
||||
results: {
|
||||
title: 'Practice Results',
|
||||
fields: {
|
||||
speed: 'Speed',
|
||||
accuracy: 'Accuracy',
|
||||
realtime: 'Realtime Rate',
|
||||
count: 'Count',
|
||||
time: 'Time',
|
||||
duration: 'Duration'
|
||||
},
|
||||
units: {
|
||||
cpm: 'CPM',
|
||||
percent: '%',
|
||||
x: 'x',
|
||||
char: 'chars'
|
||||
},
|
||||
chart: {
|
||||
speed: 'Speed (CPM)',
|
||||
accuracy: 'Accuracy (%)'
|
||||
},
|
||||
btn: {
|
||||
next: 'Next Song',
|
||||
back: 'Back to Playlist',
|
||||
retry: 'Retry'
|
||||
}
|
||||
},
|
||||
song: {
|
||||
mode: {
|
||||
typing: 'Typing Mode',
|
||||
music: 'Music Mode'
|
||||
},
|
||||
karaoke: {
|
||||
noVocals: 'No vocal separation track detected, cannot adjust vocal volume. Please process in song details page first.'
|
||||
},
|
||||
play: {
|
||||
speed: 'Speed: ',
|
||||
accuracy: 'Accuracy: ',
|
||||
stats: {
|
||||
right: 'Right: ',
|
||||
fuzzy: 'Fuzzy: ',
|
||||
wrong: 'Wrong: ',
|
||||
remaining: 'Remaining: '
|
||||
}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
title: 'Account Management',
|
||||
loginSuccess: {
|
||||
title: 'Login Successful',
|
||||
content: 'Login Successful!',
|
||||
jump: 'Jump'
|
||||
},
|
||||
generateCode: {
|
||||
title: 'Generate Sync Code',
|
||||
copy: 'Copy',
|
||||
success: 'Sync Code Generated! Code is: {code}',
|
||||
expiry: 'This code will expire after use or in 7 days if unused.'
|
||||
},
|
||||
desc: {
|
||||
intro: 'This App uses a sync code system like Japanese mobile games, no email/password registration needed.',
|
||||
instruction: 'To login on another device, click "Generate Sync Code" then click "Login" on the target device.',
|
||||
loginMode: 'You are in "Sync Login" page. Get a code from another device and enter it below to login.'
|
||||
},
|
||||
input: 'Enter Sync Code',
|
||||
btn: {
|
||||
generate: 'Generate Sync Code',
|
||||
loginWithCode: 'Login with Sync Code',
|
||||
login: 'Login'
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
-1
@@ -1,4 +1,3 @@
|
||||
|
||||
export default {
|
||||
home: {
|
||||
titles: {
|
||||
@@ -13,5 +12,119 @@ export default {
|
||||
text: {
|
||||
playlistCreatedBy: '{u} 创建'
|
||||
}
|
||||
},
|
||||
admin: {
|
||||
neteaseLogin: {
|
||||
title: '网易云登录',
|
||||
scanTitle: '扫码登录',
|
||||
scanTip: '请使用网易云音乐 APP 扫码',
|
||||
generating: '正在生成二维码...',
|
||||
scanned: '已扫描',
|
||||
confirm: '请在手机上确认登录',
|
||||
success: '登录成功',
|
||||
errorPrefix: '错误: '
|
||||
}
|
||||
},
|
||||
import: {
|
||||
netease: {
|
||||
title: '从网易云导入',
|
||||
status: {
|
||||
importing: '正在导入',
|
||||
success: '导入完成',
|
||||
error: '导入出错'
|
||||
},
|
||||
songs: '首歌曲',
|
||||
tip: '去网易云 APP 找一个你喜欢的日本语歌单,点击分享,再点击复制链接,然后把链接粘贴到这里就可以开始导入了!',
|
||||
inputLabel: '网易云歌单链接 / ID',
|
||||
btnStart: '开始导入',
|
||||
btnView: '查看歌单'
|
||||
}
|
||||
},
|
||||
playlist: {
|
||||
detail: {
|
||||
title: '歌单详情',
|
||||
creator: '创建者: ',
|
||||
count: '歌曲数: ',
|
||||
startPractice: '开始练习',
|
||||
songList: '歌曲列表',
|
||||
songs: '首歌曲'
|
||||
},
|
||||
list: {
|
||||
mine: '我的歌单',
|
||||
rec: '推荐歌单',
|
||||
import: '从网易云导入',
|
||||
created: '{u} 创建',
|
||||
count: '{n} 首歌'
|
||||
}
|
||||
},
|
||||
results: {
|
||||
title: '练习结果',
|
||||
fields: {
|
||||
speed: '速度',
|
||||
accuracy: '准确率',
|
||||
realtime: '实时率',
|
||||
count: '字数',
|
||||
time: '用时',
|
||||
duration: '歌曲时长'
|
||||
},
|
||||
units: {
|
||||
cpm: 'CPM',
|
||||
percent: '%',
|
||||
x: 'x',
|
||||
char: '字'
|
||||
},
|
||||
chart: {
|
||||
speed: '速度 (CPM)',
|
||||
accuracy: '准确率 (%)'
|
||||
},
|
||||
btn: {
|
||||
next: '下一首',
|
||||
back: '返回歌单',
|
||||
retry: '再来一次'
|
||||
}
|
||||
},
|
||||
song: {
|
||||
mode: {
|
||||
typing: '打字模式',
|
||||
music: '音乐模式'
|
||||
},
|
||||
karaoke: {
|
||||
noVocals: '未检测到人声分离音轨,无法调节人声音量。请先在歌曲详情页进行处理。'
|
||||
},
|
||||
play: {
|
||||
speed: '速度: ',
|
||||
accuracy: '正确率: ',
|
||||
stats: {
|
||||
right: '正确:',
|
||||
fuzzy: '模糊:',
|
||||
wrong: '错误:',
|
||||
remaining: '剩余:'
|
||||
}
|
||||
}
|
||||
},
|
||||
user: {
|
||||
title: '账号管理',
|
||||
loginSuccess: {
|
||||
title: '登录成功',
|
||||
content: '登录成功!',
|
||||
jump: '跳转'
|
||||
},
|
||||
generateCode: {
|
||||
title: '生成引继码',
|
||||
copy: '复制',
|
||||
success: '引继码生成成功!生成的引继码是:{code}',
|
||||
expiry: '这个引继码将会在使用之后、或者未使用的 7 天后会失效'
|
||||
},
|
||||
desc: {
|
||||
intro: '这个 App 像日本的手机游戏一样采用引继码系统管理账号,并不需要用邮箱密码注册帐号。',
|
||||
instruction: '如果想要在另一个设备上登录账号,请点击「生成引继码」然后在想要登录的设备上点击「登录」',
|
||||
loginMode: '您当前在「引继登录」页面,请在另一个设备上获取引继码之后输入到下面的输入框内点击「登录」'
|
||||
},
|
||||
input: '输入引继码',
|
||||
btn: {
|
||||
generate: '生成引继码',
|
||||
loginWithCode: '用引继码登录',
|
||||
login: '登录'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,9 @@ This page is vibe-coded. It's not a part of the regular UI intended for users an
|
||||
import { API } from '$lib/client';
|
||||
import { fade, scale } from 'svelte/transition';
|
||||
import AppBar from "$lib/ui/appbar/AppBar.svelte";
|
||||
import { Layer } from "m3-svelte";
|
||||
import { getI18n } from "$lib/i18n";
|
||||
|
||||
const t = getI18n().admin.neteaseLogin;
|
||||
|
||||
let status = $state<'loading' | 'waiting_scan' | 'waiting_confirm' | 'success' | 'error'>('loading');
|
||||
let qrImg = $state('');
|
||||
@@ -53,20 +55,20 @@ This page is vibe-coded. It's not a part of the regular UI intended for users an
|
||||
});
|
||||
</script>
|
||||
|
||||
<AppBar title="网易云登录" />
|
||||
<AppBar title={t.title} />
|
||||
|
||||
<div class="vbox flex-1 cbox p-content">
|
||||
<div class="vbox items-center gap-24px p-32px rounded-24px relative overflow-hidden w-full max-w-400px" in:scale={{ duration: 400, start: 0.95 }}>
|
||||
<div class="vbox items-center gap-8px z-1">
|
||||
<h1 class="m3-font-headline-medium mfg-on-surface m-0">扫码登录</h1>
|
||||
<p class="m3-font-body-medium mfg-on-surface-variant m-0">请使用网易云音乐 APP 扫码</p>
|
||||
<h1 class="m3-font-headline-medium mfg-on-surface m-0">{t.scanTitle}</h1>
|
||||
<p class="m3-font-body-medium mfg-on-surface-variant m-0">{t.scanTip}</p>
|
||||
</div>
|
||||
|
||||
<div class="cbox min-h-200px w-full z-1">
|
||||
{#if status === 'loading'}
|
||||
<div class="vbox items-center gap-16px" in:fade>
|
||||
<div class="i-material-symbols:sync animate-spin text-40px mfg-primary"></div>
|
||||
<p class="m3-font-body-medium mfg-on-surface-variant">正在生成二维码...</p>
|
||||
<p class="m3-font-body-medium mfg-on-surface-variant">{t.generating}</p>
|
||||
</div>
|
||||
{:else if status === 'waiting_scan'}
|
||||
<div class="p-16px bg-white rounded-16px shadow-sm" in:fade>
|
||||
@@ -78,8 +80,8 @@ This page is vibe-coded. It's not a part of the regular UI intended for users an
|
||||
<div class="i-material-symbols:check-circle-outline text-48px"></div>
|
||||
</div>
|
||||
<div class="vbox items-center">
|
||||
<p class="m3-font-title-large mfg-on-surface font-bold">已扫描</p>
|
||||
<p class="m3-font-body-medium mfg-on-surface-variant">请在手机上确认登录</p>
|
||||
<p class="m3-font-title-large mfg-on-surface font-bold">{t.scanned}</p>
|
||||
<p class="m3-font-body-medium mfg-on-surface-variant">{t.confirm}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if status === 'success'}
|
||||
@@ -88,7 +90,7 @@ This page is vibe-coded. It's not a part of the regular UI intended for users an
|
||||
<div class="i-material-symbols:check text-48px"></div>
|
||||
</div>
|
||||
<div class="vbox items-center">
|
||||
<p class="m3-font-title-large mfg-on-surface font-bold">登录成功</p>
|
||||
<p class="m3-font-title-large mfg-on-surface font-bold">{t.success}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if status === 'error'}
|
||||
@@ -96,7 +98,7 @@ This page is vibe-coded. It's not a part of the regular UI intended for users an
|
||||
<div class="size-80px rounded-full cbox mbg-error-container mfg-error">
|
||||
<div class="i-material-symbols:error-outline text-48px"></div>
|
||||
</div>
|
||||
<p class="m3-font-body-medium mfg-error text-center">错误: {errorMessage}</p>
|
||||
<p class="m3-font-body-medium mfg-error text-center">{t.errorPrefix}{errorMessage}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import { API } from "$lib/client"
|
||||
import ErrorDialog from "$lib/ui/status/ErrorDialog.svelte"
|
||||
import ProgressList from "$lib/ui/ProgressList.svelte"
|
||||
import { getI18n } from "$lib/i18n"
|
||||
|
||||
const t = getI18n().import.netease
|
||||
|
||||
let link = $state('')
|
||||
|
||||
@@ -34,8 +37,8 @@
|
||||
return ''
|
||||
}
|
||||
|
||||
let listTitle = $derived(status === 'idle' ? '' : (status === 'importing' ? '正在导入' : (status === 'success' ? '导入完成' : '导入出错')))
|
||||
let listSubtitle = $derived(`${progress.done} / ${progress.total} 首歌曲`)
|
||||
let listTitle = $derived(status === 'idle' ? '' : (status === 'importing' ? t.status.importing : (status === 'success' ? t.status.success : t.status.error)))
|
||||
let listSubtitle = $derived(`${progress.done} / ${progress.total} ${t.songs}`)
|
||||
let listPercent = $derived(progress.total ? progress.done / progress.total * 100 : 0)
|
||||
let listItems = $derived(songs.map(song => ({
|
||||
title: song.song.name,
|
||||
@@ -72,26 +75,26 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppBar title="从网易云导入"/>
|
||||
<AppBar title={t.title}/>
|
||||
|
||||
<ErrorDialog error={error} />
|
||||
|
||||
<div class="vbox gap-16px flex-1 min-h-0">
|
||||
<div class="m3-font-body-medium mfg-on-surface-variant py-12px p-content">
|
||||
去网易云 APP 找一个你喜欢的日本语歌单,点击分享,再点击复制链接,然后把链接粘贴到这里就可以开始导入了!
|
||||
{t.tip}
|
||||
</div>
|
||||
<div class="vbox p-content">
|
||||
<TextFieldOutlined label="网易云歌单链接 / ID" bind:value={link} />
|
||||
<TextFieldOutlined label={t.inputLabel} bind:value={link} />
|
||||
</div>
|
||||
|
||||
<ProgressList title={listTitle} subtitle={listSubtitle} percentage={listPercent} items={listItems} />
|
||||
|
||||
<div class="py-16px p-content">
|
||||
{#if status === 'idle'}
|
||||
<Button big icon="i-material-symbols:download" onclick={startImport}>开始导入</Button>
|
||||
<Button big icon="i-material-symbols:download" onclick={startImport}>{t.btnStart}</Button>
|
||||
{:else if status === 'success'}
|
||||
<a href="/playlist/{id}">
|
||||
<Button big icon="i-material-symbols:right-arrow">查看歌单</Button>
|
||||
<Button big icon="i-material-symbols:right-arrow">{t.btnView}</Button>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
import Button from "$lib/ui/Button.svelte"
|
||||
import SongInfo from "$lib/ui/listitem/SongInfo.svelte"
|
||||
import { API } from "$lib/client"
|
||||
import { getI18n } from "$lib/i18n"
|
||||
|
||||
const t = getI18n().playlist.detail
|
||||
|
||||
let { data }: PageProps = $props()
|
||||
|
||||
@@ -42,7 +45,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppBar title="歌单详情" right={[
|
||||
<AppBar title={t.title} right={[
|
||||
{
|
||||
icon: isFavorite ? "i-material-symbols:bookmark-rounded" : "i-material-symbols:bookmark-add-outline-rounded",
|
||||
onclick: toggleFavorite
|
||||
@@ -55,18 +58,18 @@
|
||||
<div class="vbox flex-1 py-8px self-stretch min-h-full">
|
||||
<div class="m3-font-headline-small font-bold">{meta.name}</div>
|
||||
<div class="flex-1">
|
||||
<div class="m3-font-body-small text-surface-variant">创建者: {meta.creator.nickname}</div>
|
||||
<div class="m3-font-body-small text-surface-variant">歌曲数: {meta.trackCount}</div>
|
||||
<div class="m3-font-body-small text-surface-variant">{t.creator}{meta.creator.nickname}</div>
|
||||
<div class="m3-font-body-small text-surface-variant">{t.count}{meta.trackCount}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button onclick={startPractice}>开始练习</Button>
|
||||
<Button onclick={startPractice}>{t.startPractice}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hbox gap-12px items-end! h-48px p-content">
|
||||
<div class="m3-font-headline-small">歌曲列表</div>
|
||||
<div class="m3-font-label-small pb-3px">{songs.length} 首歌曲</div>
|
||||
<div class="m3-font-headline-small">{t.songList}</div>
|
||||
<div class="m3-font-label-small pb-3px">{songs.length} {t.songs}</div>
|
||||
</div>
|
||||
<div class="vbox gap-12px mt-12px pb-12px scroll-here">
|
||||
{#each songs as song, index}
|
||||
|
||||
@@ -4,18 +4,21 @@
|
||||
import ImageListItem from "$lib/ui/listitem/ImageListItem.svelte"
|
||||
import MenuItem from "$lib/ui/material3/MenuItem.svelte"
|
||||
import type { PageProps } from "./$types"
|
||||
import { getI18n } from "$lib/i18n"
|
||||
|
||||
const t = getI18n().playlist.list
|
||||
|
||||
let { data }: PageProps = $props()
|
||||
</script>
|
||||
|
||||
<AppBar title={data.isMine ? '我的歌单' : '推荐歌单'}>
|
||||
<MenuItem icon="i-material-symbols:download" onclick={() => goto("/import/netease")}>从网易云导入</MenuItem>
|
||||
<AppBar title={data.isMine ? t.mine : t.rec}>
|
||||
<MenuItem icon="i-material-symbols:download" onclick={() => goto("/import/netease")}>{t.import}</MenuItem>
|
||||
</AppBar>
|
||||
|
||||
<div class="vbox p-content gap-12px scroll-here">
|
||||
{#each data.playlists as pl}
|
||||
<a href="/playlist/{pl.id}">
|
||||
<ImageListItem photoUrl={pl.coverImgUrl} title={pl.name} text={`${pl.creator.nickname} 创建`} right={`${pl.trackCount} 首歌`} />
|
||||
<ImageListItem photoUrl={pl.coverImgUrl} title={pl.name} text={t.created.sed({u: pl.creator.nickname})} right={t.count.sed({n: pl.trackCount})} />
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
import Chart from "chart.js/auto"
|
||||
import { API } from "$lib/client"
|
||||
import { getI18n } from "$lib/i18n"
|
||||
|
||||
const t = getI18n().results
|
||||
|
||||
import type { NeteaseSong, UserData } from "../../../lib/types"
|
||||
|
||||
@@ -19,12 +22,12 @@
|
||||
let duration = endTime - startTime
|
||||
|
||||
let fields = [
|
||||
{ label: "速度", value: Math.round(totalTyped / (Math.max(1, duration) / 60000)), unit: "CPM" },
|
||||
{ label: "准确率", value: totalTyped === 0 ? 100 : Math.round((totalRight / totalTyped) * 10000) / 100, unit: "%" },
|
||||
{ label: "实时率", value: data.result.realTimeFactor.toFixed(2), unit: "x" },
|
||||
{ label: "字数", value: totalTyped, unit: "字" },
|
||||
{ label: "用时", value: new Date(duration).toISOString().slice(14, 19), unit: "" },
|
||||
{ label: "歌曲时长", value: new Date(data.song.dt).toISOString().slice(14, 19), unit: "" }
|
||||
{ label: t.fields.speed, value: Math.round(totalTyped / (Math.max(1, duration) / 60000)), unit: t.units.cpm },
|
||||
{ label: t.fields.accuracy, value: totalTyped === 0 ? 100 : Math.round((totalRight / totalTyped) * 10000) / 100, unit: t.units.percent },
|
||||
{ label: t.fields.realtime, value: data.result.realTimeFactor.toFixed(2), unit: t.units.x },
|
||||
{ label: t.fields.count, value: totalTyped, unit: t.units.char },
|
||||
{ label: t.fields.time, value: new Date(duration).toISOString().slice(14, 19), unit: "" },
|
||||
{ label: t.fields.duration, value: new Date(data.song.dt).toISOString().slice(14, 19), unit: "" }
|
||||
]
|
||||
|
||||
let chartCanvas: HTMLCanvasElement
|
||||
@@ -38,7 +41,7 @@
|
||||
labels: statsHistory.map((_: any, i: number) => i),
|
||||
datasets: [
|
||||
{
|
||||
label: "速度 (CPM)",
|
||||
label: t.chart.speed,
|
||||
data: statsHistory.map((h: { cpm: number }) => h.cpm),
|
||||
tension: 0.4,
|
||||
pointRadius: 0,
|
||||
@@ -47,7 +50,7 @@
|
||||
backgroundColor: "rgba(123, 120, 194, 0.1)",
|
||||
},
|
||||
{
|
||||
label: "准确率 (%)",
|
||||
label: t.chart.accuracy,
|
||||
data: statsHistory.map((h: { acc: number }) => h.acc),
|
||||
tension: 0.4,
|
||||
yAxisID: "y1",
|
||||
@@ -146,9 +149,9 @@
|
||||
}
|
||||
|
||||
let buttonText = $derived(
|
||||
nextSongId !== null ? "下一首" :
|
||||
isPlaylistFinished ? "返回歌单" :
|
||||
"再来一次"
|
||||
nextSongId !== null ? t.btn.next :
|
||||
isPlaylistFinished ? t.btn.back :
|
||||
t.btn.retry
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -156,7 +159,7 @@
|
||||
|
||||
<div class="vbox gap-16px p-content scroll-here">
|
||||
<div class="hbox gap-12px items-end! h-48px">
|
||||
<div class="m3-font-headline-small">练习结果</div>
|
||||
<div class="m3-font-headline-small">{t.title}</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-16px">
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import ProgressList from "$lib/ui/ProgressList.svelte"
|
||||
import { goto } from "$app/navigation"
|
||||
import { artistAndAlbum } from "$lib/utils"
|
||||
import { getI18n } from "$lib/i18n"
|
||||
|
||||
const t = getI18n().song.mode
|
||||
|
||||
let { data } = $props()
|
||||
let loadStatus = $state<"idle" | "loading" | "done">("idle")
|
||||
@@ -52,7 +55,7 @@
|
||||
|
||||
{#if loadStatus === "done"}
|
||||
<div class="hbox gap-4 p-16px">
|
||||
<Button big icon="i-material-symbols:keyboard-rounded" onclick={() => goto(`/song/${data.song.id}/play`)}>打字模式</Button>
|
||||
<Button big icon="i-material-symbols:music-note-rounded" onclick={() => goto(`/song/${data.song.id}/play?music=true`)}>音乐模式</Button>
|
||||
<Button big icon="i-material-symbols:keyboard-rounded" onclick={() => goto(`/song/${data.song.id}/play`)}>{t.typing}</Button>
|
||||
<Button big icon="i-material-symbols:music-note-rounded" onclick={() => goto(`/song/${data.song.id}/play?music=true`)}>{t.music}</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
import { MusicControl } from "$lib/ui/player/MusicControl"
|
||||
import Lyrics from "$lib/ui/player/Lyrics.svelte"
|
||||
import PlayerAppBar from "$lib/ui/player/PlayerAppBar.svelte"
|
||||
import { getI18n } from "$lib/i18n"
|
||||
|
||||
const t = getI18n().song.karaoke
|
||||
|
||||
let { data }: PageProps = $props()
|
||||
|
||||
@@ -88,7 +91,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center text-sm opacity-70">
|
||||
未检测到人声分离音轨,无法调节人声音量。请先在歌曲详情页进行处理。
|
||||
{t.noVocals}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
import { MusicControl } from "$lib/ui/player/MusicControl.ts"
|
||||
import Lyrics from "$lib/ui/player/Lyrics.svelte"
|
||||
import PlayerAppBar from "$lib/ui/player/PlayerAppBar.svelte"
|
||||
import { getI18n } from "$lib/i18n"
|
||||
|
||||
const t = getI18n().song.play
|
||||
|
||||
let { data }: PageProps = $props()
|
||||
|
||||
@@ -167,14 +170,14 @@
|
||||
<!-- Stats -->
|
||||
<div class="vbox p-content py-12px mfg-on-surface-variant m3-font-body-medium">
|
||||
<div class="hbox justify-between">
|
||||
<div>速度: {startTime ? Math.round(totalTyped / (Math.max(1, (now - startTime)) / 60000)) : '-'} cpm</div>
|
||||
<div>正確率: {totalTyped === 0 ? 100 : Math.round((totalRight / totalTyped) * 100)}%</div>
|
||||
<div>{t.speed}{startTime ? Math.round(totalTyped / (Math.max(1, (now - startTime)) / 60000)) : '-'} cpm</div>
|
||||
<div>{t.accuracy}{totalTyped === 0 ? 100 : Math.round((totalRight / totalTyped) * 100)}%</div>
|
||||
</div>
|
||||
<div class="hbox justify-between">
|
||||
<div>正确:{flat.filter(s => s === 'right').length}</div>
|
||||
<div>模糊:{flat.filter(s => s === 'fuzzy').length}</div>
|
||||
<div>错误:{flat.filter(s => s === 'wrong').length}</div>
|
||||
<div>剩余:{flat.filter(s => s === 'unseen').length}</div>
|
||||
<div>{t.stats.right}{flat.filter(s => s === 'right').length}</div>
|
||||
<div>{t.stats.fuzzy}{flat.filter(s => s === 'fuzzy').length}</div>
|
||||
<div>{t.stats.wrong}{flat.filter(s => s === 'wrong').length}</div>
|
||||
<div>{t.stats.remaining}{flat.filter(s => s === 'unseen').length}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
import Dialog from "$lib/ui/status/Dialog.svelte"
|
||||
import { API } from "$lib/client"
|
||||
import ErrorDialog from "$lib/ui/status/ErrorDialog.svelte"
|
||||
import { getI18n } from "$lib/i18n"
|
||||
|
||||
const t = getI18n().user
|
||||
|
||||
let showCodeOpen = $state(false)
|
||||
let loginSuccessOpen = $state(false)
|
||||
@@ -25,34 +28,34 @@
|
||||
|
||||
<ErrorDialog error={error}/>
|
||||
|
||||
<Dialog title="登录成功" bind:open={loginSuccessOpen} buttons={[{
|
||||
text: '跳转', onclick: () => location.href = '/'
|
||||
<Dialog title={t.loginSuccess.title} bind:open={loginSuccessOpen} buttons={[{
|
||||
text: t.loginSuccess.jump, onclick: () => location.href = '/'
|
||||
}]}>
|
||||
登录成功!
|
||||
{t.loginSuccess.content}
|
||||
</Dialog>
|
||||
|
||||
<Dialog title="生成引继码" bind:open={showCodeOpen} buttons={[{
|
||||
text: '复制', onclick: () => navigator.clipboard.writeText(generatedCode)
|
||||
<Dialog title={t.generateCode.title} bind:open={showCodeOpen} buttons={[{
|
||||
text: t.generateCode.copy, onclick: () => navigator.clipboard.writeText(generatedCode)
|
||||
}]}>
|
||||
引继码生成成功!生成的引继码是:{generatedCode}
|
||||
{t.generateCode.success.sed({code: generatedCode})}
|
||||
<br><br>
|
||||
这个引继码将会在使用之后、或者未使用的 7 天后会失效
|
||||
{t.generateCode.expiry}
|
||||
</Dialog>
|
||||
|
||||
<AppBar title="账号管理"/>
|
||||
<AppBar title={t.title}/>
|
||||
|
||||
<div class="m3-font-body-medium mfg-on-surface-variant py-12px p-content">
|
||||
这个 App 像日本的手机游戏一样采用引继码系统管理账号,并不需要用邮箱密码注册帐号。
|
||||
{t.desc.intro}
|
||||
<br><br>
|
||||
如果想要在另一个设备上登录账号,请点击「生成引继码」然后在想要登录的设备上点击「登录」
|
||||
{t.desc.instruction}
|
||||
{#if loginMode}
|
||||
<br><br>您当前在「引继登录」页面,请在另一个设备上获取引继码之后输入到下面的输入框内点击「登录」
|
||||
<br><br>{t.desc.loginMode}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if loginMode}
|
||||
<div class="vbox px-16px py-10px">
|
||||
<TextFieldOutlined label="输入引继码" bind:value={loginCode}/>
|
||||
<TextFieldOutlined label={t.input} bind:value={loginCode}/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -60,9 +63,9 @@
|
||||
|
||||
<div class="hbox p-16px gap-16px">
|
||||
{#if !loginMode}
|
||||
<Button big secondary icon="i-material-symbols:add" disabled={showCodeOpen} onclick={generateCode}>生成引继码</Button>
|
||||
<Button big secondary icon="i-material-symbols:login" disabled={showCodeOpen} onclick={() => loginMode = true}>用引继码登录</Button>
|
||||
<Button big secondary icon="i-material-symbols:add" disabled={showCodeOpen} onclick={generateCode}>{t.btn.generate}</Button>
|
||||
<Button big secondary icon="i-material-symbols:login" disabled={showCodeOpen} onclick={() => loginMode = true}>{t.btn.loginWithCode}</Button>
|
||||
{:else}
|
||||
<Button big secondary icon="i-material-symbols:login" disabled={showCodeOpen} onclick={doLogin}>登录</Button>
|
||||
<Button big secondary icon="i-material-symbols:login" disabled={showCodeOpen} onclick={doLogin}>{t.btn.login}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user