improve: SSR in database page, fix some UI bugs

extract: component SongCard used in database pages
adjust (UI): now lyrics container won't show scrollbar
This commit is contained in:
alikia2x 2024-07-20 19:18:35 +08:00
parent 73ad9036c2
commit aef964aa35
8 changed files with 155 additions and 79 deletions

View File

@ -4,11 +4,11 @@ AquaVox 是一个为中文虚拟歌手爱好者献上的产品。
> VOCALOID IS ALIVE.
这是一个**开源、本地优先、有B站良好支持、界面美观优雅**的**音乐播放器**。
这是一个 **开源、本地优先、有 B 站良好支持、界面美观优雅** **音乐播放器**
## 使用
当前 AquaVox 的公开预览版位于[aquavox.a2x.pub](https://aquavox.a2x.pub)上。
当前 AquaVox 的公开预览版位于 [aquavox.app](https://aquavox.app) 上。
[![wakatime](https://wakatime.com/badge/user/018f0628-909b-47e4-bcfd-0153235426d9/project/b67c03ef-ee0b-45f2-85ec-d9c60269cc55.svg)](https://wakatime.com/badge/user/018f0628-909b-47e4-bcfd-0153235426d9/project/b67c03ef-ee0b-45f2-85ec-d9c60269cc55)
@ -16,22 +16,22 @@ AquaVox 是一个为中文虚拟歌手爱好者献上的产品。
**开发者寒寒的话:**
AquaVox 这个播放器项目的灵感根本上来自于Apple Music以其优雅流畅的界面设计而著称。
AquaVox 这个播放器项目的灵感根本上来自于 Apple Music以其优雅流畅的界面设计而著称。
我的中V曲之前基本是在B站听的而中文虚拟歌手社区也基本上扎根于B站。但在B站听歌也有不少劣势——
首先自然是B站本身并不是一个音乐软件或播放器自然在相关的功能上并没有过多开发“听视频”的功能也仅在移动端可用
其次不少人听歌还是有看歌词的需求的而许多中V曲发布时附上的PV中的歌词为了美观歌词并不会以一个常规视频的字幕样式呈现而是采用了美术字等更为艺术化的表现形式这使得查看歌词并不方便更不用说在“听视频”模式下PV根本没有播放的情况。
我的中 V 曲之前基本是在 B 站听的,而中文虚拟歌手社区也基本上扎根于 B 站。但在 B 站听歌也有不少劣势——
首先自然是 B 站本身并不是一个音乐软件或播放器,自然在相关的功能上并没有过多开发,“听视频”的功能也仅在移动端可用;
其次,不少人听歌还是有看歌词的需求的,而许多中 V 曲发布时附上的 PV 中的歌词,为了美观,歌词并不会以一个常规视频的字幕样式呈现,而是采用了美术字等更为艺术化的表现形式,这使得查看歌词并不方便,更不用说在“听视频”模式下 PV 根本没有播放的情况。
而后来我选择了通过将B站的歌曲导入到Apple Music的方案而为什么选择AM呢尽管很多中V曲在网易云等平台上有更丰富的资源但我依然选择了AM。很大一个原因自然是我对AM播放器界面和交互设计及用户体验的喜爱但另一方面网易云的中V曲库其实也并不算全最终还要使用导入的方法才能将自己所有喜欢的歌囊括其中那么既然要导入何不直接将歌曲全部导入AM呢
而后来,我选择了通过将 B 站的歌曲导入到 Apple Music 的方案,而为什么选择 AM 呢?尽管很多中 V 曲在网易云等平台上有更丰富的资源,但我依然选择了 AM。很大一个原因自然是我对 AM 播放器界面和交互设计及用户体验的喜爱,但另一方面,网易云的中 V 曲库其实也并不算全,最终还要使用导入的方法才能将自己所有喜欢的歌囊括其中,那么既然要导入,何不直接将歌曲全部导入 AM 呢?
但很快我也发现了AM的一个致命问题自行导入的歌曲没有动态歌词的功能只能以一个静态的模式查看全部的歌词。而动态歌词的漂亮设计是我很大一部分喜欢Apple Music的原因但我自己导入的歌曲却无法享受这个功能不是很令人失望吗
但很快,我也发现了 AM 的一个致命问题:自行导入的歌曲没有动态歌词的功能,只能以一个静态的模式查看全部的歌词。而动态歌词的漂亮设计是我很大一部分喜欢 Apple Music 的原因,但我自己导入的歌曲却无法享受这个功能,不是很令人失望吗?
因此最后我还是最终决定自行开发一个播放器加上所有我喜欢的东西——Apple Music的页面设计和交互、从B站直接获取的曲库、通过网页、PWA和Electron使全平台有一致的体验。
因此最后我还是最终决定自行开发一个播放器加上所有我喜欢的东西——Apple Music 的页面设计和交互、从 B 站直接获取的曲库、通过网页、PWA Electron 使全平台有一致的体验。
## “赠品”
**开发者寒寒的话:**
在熟虑后,我决定让 AquaVox 不仅是一个播放器。更进一步我希望它是一个属于整个中文虚拟歌手社区的数据库。从音源、作者P主及staff、虚拟歌手、歌曲元信息、动态歌词在整个链路上成为一个中文虚拟歌手的终极“Archive”。
在熟虑后,我决定让 AquaVox 不仅是一个播放器。更进一步我希望它是一个属于整个中文虚拟歌手社区的数据库。从音源、作者P 主及 staff、虚拟歌手、歌曲元信息、动态歌词在整个链路上成为一个中文虚拟歌手的终极“Archive”。
因此,我们需要你的帮助。

View File

@ -1,6 +1,6 @@
{
"name": "aquavox",
"version": "1.12.6",
"version": "1.12.8",
"private": false,
"scripts": {
"dev": "vite dev",

View File

@ -0,0 +1,62 @@
<script lang="ts">
import formatDuration from "$lib/formatDuration";
import { formatViews } from "$lib/formatViews";
export let songData: MusicMetadata;
</script>
<div>
<div
class="relative w-56 h-56 bg-zinc-300 dark:bg-zinc-600 rounded-lg overflow-hidden
shadow-lg cursor-pointer justify-self-center"
>
<div
class="absolute top-0 left-0 w-full h-full duration-100
z-10 opacity-0 hover:opacity-100 bg-[rgba(0,0,0,0.15)]"
>
<a href={songData.url} class="absolute z-10 h-full w-full">
</a>
<a
class="brightness-125 absolute top-2 right-2 w-8 h-8 rounded-full
bg-[rgba(49,49,49,0.7)] backdrop-blur-lg z-30 hover:bg-red-500"
href={`/database/edit/${songData.id}`}
>
<img class="relative w-4 h-4 top-2 left-2 scale-90" src="/edit.svg" alt="编辑" />
</a>
</div>
<img src={songData.coverURL[0]} class="w-56 h-56" alt="" />
<div
class="absolute bottom-0 w-full h-28 backdrop-blur-xl"
style="mask-image: linear-gradient(to top, black 50%, transparent);"
>
<div class="absolute bottom-0 w-full h-16 pl-2">
<span
class="font-semibold text-2xl text-white"
style="text-shadow: 0px 0px 4px rgba(65, 65, 65, .6);">{songData.name}</span
>
<br />
<span
class="relative inline-block whitespace-nowrap text-white w-28
overflow-hidden text-ellipsis"
style="text-shadow: 0px 0px 4px rgba(65, 65, 65, .6);"
>
{songData.producer}
</span>
<div
class="absolute right-2 bottom-2 text-right text-white"
style="text-shadow: 0px 0px 4px rgba(65, 65, 65, .6);"
>
{#if songData.duration}
<span>{formatDuration(songData.duration)}</span>
{/if}
<br />
{#if songData.views}
<span>{formatViews(songData.views)}播放</span>
{/if}
</div>
</div>
</div>
</div>
</div>

View File

@ -68,7 +68,7 @@
for (let i = 0; i < scripts.length; i++) {
const offset = Math.abs(i - currentPositionIndex);
const blurRadius = Math.min(offset * 1.5, 16);
const blurRadius = Math.min(offset * 1, 16);
if (refs[i]) {
refs[i].style.filter = `blur(${blurRadius}px)`;
}
@ -278,10 +278,10 @@
);
}
.no-scrollbar {
scrollbar-width: 10px;
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
width: 10px;
width: 0px;
}
.current-lyric {
position: relative;

View File

@ -0,0 +1,17 @@
import { songData } from '$lib/server/cache.js';
import { loadData } from '$lib/server/database/loadData.js';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = () => {
loadData();
const songIDList = songData.keys().slice(0, 20);
const songDataList = [];
for (const songID of songIDList) {
songDataList.push(songData.get(songID)!);
}
return {
songDataList: songDataList
};
};
export const ssr = true;

View File

@ -1,20 +1,8 @@
<script lang="ts">
import formatDuration from '$lib/formatDuration';
import { formatViews } from '$lib/formatViews';
import { onMount } from 'svelte';
let songList: MusicMetadata[] = [];
onMount(async () => {
fetch('/api/database/songs')
.then((response) => response.json())
.then((data) => {
songList = data;
})
.catch((error) => {
console.log(error);
return [];
});
});
import SongCard from '$lib/components/database/songCard.svelte';
import type { PageServerData } from './$types';
export let data: PageServerData;
let songList: MusicMetadata[] = data.songDataList;
</script>
<svelte:head>
@ -33,57 +21,14 @@
>
</div>
<div class="grid mb-32" style="grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
<div
class="relative grid mb-32"
style="grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
justify-content: space-between;
gap: 2rem 1rem;">
gap: 2rem 1rem;"
>
{#each songList as song}
<a
class="relative w-56 h-56 bg-zinc-300 dark:bg-zinc-600 rounded-lg overflow-hidden
shadow-lg cursor-pointer justify-self-center"
href={song.url}
>
<div
class="absolute top-0 left-0 w-full h-full duration-100
z-10 opacity-0 hover:opacity-100 bg-[rgba(0,0,0,0.15)]"
>
<a
class="brightness-125 absolute top-2 right-2 w-8 h-8 rounded-full
bg-[rgba(49,49,49,0.7)] backdrop-blur-lg z-10 hover:bg-red-500"
href={`/database/edit/${song.id}`}
>
<img class="relative w-4 h-4 top-2 left-2 scale-90" src="/edit.svg" alt="编辑" />
</a>
</div>
<img src={song.coverURL[0]} class="w-56 h-56" alt="" />
<div
class="absolute bottom-0 w-full h-28 backdrop-blur-xl"
style="mask-image: linear-gradient(to top, black 50%, transparent);"
>
<div class="absolute bottom-0 w-full h-16 pl-2">
<span
class="font-semibold text-2xl text-white"
style="text-shadow: 0px 0px 4px rgba(65, 65, 65, .6);">{song.name}</span
>
<br />
<span
class="relative inline-block whitespace-nowrap text-white w-28
overflow-hidden text-ellipsis"
style="text-shadow: 0px 0px 4px rgba(65, 65, 65, .6);"
>
{song.producer}
</span>
<div class="absolute right-2 bottom-2 text-right text-white" style="text-shadow: 0px 0px 4px rgba(65, 65, 65, .6);">
{#if song.duration}
<span>{formatDuration(song.duration)}</span>
{/if}
<br />
{#if song.views}
<span>{formatViews(song.views)}播放</span>
{/if}
</div>
</div>
</div>
</a>
<SongCard songData={song}/>
{/each}
</div>
</div>

View File

@ -0,0 +1,18 @@
import { songData } from '$lib/server/cache.js';
import { loadData } from '$lib/server/database/loadData.js';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = ({ params }) => {
const offset = (parseInt(params.id) - 1) * 20;
loadData();
const songIDList = songData.keys().slice(offset, offset + 20);
const songDataList = [];
for (const songID of songIDList) {
songDataList.push(songData.get(songID)!);
}
return {
songDataList: songDataList
};
};
export const ssr = true;

View File

@ -0,0 +1,34 @@
<script lang="ts">
import SongCard from '$lib/components/database/songCard.svelte';
import type { PageServerData } from './$types';
export let data: PageServerData;
let songList: MusicMetadata[] = data.songDataList;
</script>
<svelte:head>
<title>AquaVox 音乐数据库</title>
</svelte:head>
<h1 class="text-3xl text-red-500"><a href="/">AquaVox</a></h1>
<div>
<div class="flex justify-between items-center h-20 mb-8">
<h1>AquaVox 音乐数据库</h1>
<a
href="/database/submit"
class="h-12 w-24 border-black dark:border-white border-2 flex items-center justify-center rounded-lg"
>提交新曲</a
>
</div>
<div
class="relative grid mb-32"
style="grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
justify-content: space-between;
gap: 2rem 1rem;"
>
{#each songList as song}
<SongCard songData={song}/>
{/each}
</div>
</div>