merge: branch main into dev
This commit is contained in:
commit
b4eab1b1ff
20
README.md
20
README.md
@ -4,11 +4,11 @@ AquaVox 是一个为中文虚拟歌手爱好者献上的产品。
|
||||
|
||||
> VOCALOID IS ALIVE.
|
||||
|
||||
这是一个**开源、本地优先、有B站良好支持、界面美观优雅**的**音乐播放器**。
|
||||
这是一个 **开源、本地优先、有 B 站良好支持、界面美观优雅** 的 **音乐播放器**。
|
||||
|
||||
## 使用
|
||||
|
||||
当前 AquaVox 的公开预览版位于[aquavox.a2x.pub](https://aquavox.a2x.pub)上。
|
||||
当前 AquaVox 的公开预览版位于 [aquavox.app](https://aquavox.app) 上。
|
||||
|
||||
[](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”。
|
||||
|
||||
因此,我们需要你的帮助。
|
||||
|
||||
|
@ -43,5 +43,6 @@
|
||||
"views": 502678,
|
||||
"publishTime": "2024-04-18 12:00:00",
|
||||
"updateTime": "2024-07-12 02:49:26",
|
||||
"netEaseID": 2145230796,
|
||||
"lyric": "[ti: 天山之外]\n[ar: 洛天依]\n[al: 游四方]\n[00:00.000]\n[00:20.890] 旷野是 自由的家\n[00:29.680] 顶冰花 雪里生芽\n[00:39.950] 光的延伸 让香弥漫夕阳下\n[00:49.310] 可妈妈 天山外是哪\n[00:59.130] 我是否如她挺拔 四季更迭洁白无暇\n[01:04.070] 你有绝世风华 芦苇悠悠 粼粼之下\n[01:08.910] 不必如四季伟大\n[01:11.260] 天山不曾 规定谁 像她 随心而去吧\n[01:17.020] 高空下\n[01:18.550] 自由它该乘骏马\n[01:20.880] 绿浪滚滚中散着发\n[01:23.360] 随风起舞落下\n[01:25.430] 也算生命别样优雅\n[01:28.110] 乘风随心而去吧\n[01:30.470] 向着无边的旷野 飞吧\n[01:35.890] 去浪迹天涯\n[01:57.190] 辽阔的 不止晚霞\n[02:06.430] 又盛开 天山梦话\n[02:16.430] 光的延伸 是你和我 放不下\n[02:25.930] 记忆里 天山下的家\n[02:35.280] 我是否如她挺拔 四季更迭洁白无暇\n[02:40.140] 你有绝世风华 芦苇悠悠粼粼之下\n[02:44.870] 不必如四季伟大\n[02:47.290] 天山不曾 规定谁像她随心而去吧\n[02:53.250] 高空下\n[02:54.520] 自由它该乘骏马\n[02:56.880] 绿浪滚滚中散着发\n[02:59.380] 随风起舞落下\n[03:01.720] 也算生命别样优雅\n[03:04.080] 乘风随心而去吧\n[03:06.500] 向着无边的旷野 飞吧\n[03:11.920] 去浪迹天涯\n[03:14.580]"
|
||||
}
|
42
data/song/BV1Au4y1a7cN.json
Normal file
42
data/song/BV1Au4y1a7cN.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "BV1Au4y1a7cN",
|
||||
"name": "姑苏画舫录",
|
||||
"url": "https://www.bilibili.com/video/BV1Au4y1a7cN",
|
||||
"singer": [
|
||||
"洛天依", "乐正绫"
|
||||
],
|
||||
"producer": "青天纤云-TsingClouds",
|
||||
"tuning": [
|
||||
"鬼面P"
|
||||
],
|
||||
"lyricist": [
|
||||
"青天纤云·纳兰清婧"
|
||||
],
|
||||
"composer": [
|
||||
"砖厂浪人"
|
||||
],
|
||||
"arranger": [
|
||||
"砖厂浪人"
|
||||
],
|
||||
"mixing": [
|
||||
"莫逆iMoNe"
|
||||
],
|
||||
"pv": [
|
||||
"結月林", "墨雨清泉"
|
||||
],
|
||||
"illustrator": [
|
||||
"小夏Elin", "绫也舒芙蕾", "景育"
|
||||
],
|
||||
"harmony": [
|
||||
"Vsinger ACE全员"
|
||||
],
|
||||
"instruments": [],
|
||||
"songURL": ["https://assets.aquavox.app/public/BV1Au4y1a7cN.flac"],
|
||||
"coverURL": ["https://assets.aquavox.app/public/BV1Au4y1a7cN.jpg"],
|
||||
"duration": 320.00,
|
||||
"views": 100002,
|
||||
"publishTime": "2023-11-04 18:00:00",
|
||||
"updateTime": "2024-07-16 15:14:30",
|
||||
"netEaseID": 2094903334,
|
||||
"lyric": ""
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -85,5 +85,6 @@
|
||||
"views": 2733456,
|
||||
"publishTime": "2024-02-09 20:31:23",
|
||||
"updateTime": "2024-07-12 00:23:22",
|
||||
"netEaseID": 2124462309,
|
||||
"lyric": "[ti: 大哉乾元]\n[ar: 洛天依]\n[al: 2024哔哩哔哩拜年纪]\n[tool: 歌词滚动姬 https://lrc-maker.github.io]\n[length: 04:19.536]\n[00:05.630] 经起幽明 悟处通玄\n[00:09.680] 首窥龙堑 见岳见渊\n[00:13.640] 道不善宣 义不善绻\n[00:17.270] 源流万世 大哉乾元!\n[00:21.837]\n[00:36.390] 不曾闻日月争辉\n[00:38.170] 坎离复往 立下恒规\n[00:40.330] 照东南 有坤徇乾 承西北\n[00:43.680] 天道自昆仑巍巍\n[00:45.590] 翻起华夏巽震艮兑\n[00:47.810] 万象予万灵得见 两相盈岁\n[00:51.210] 潜龙长生应紫微\n[00:53.000] 惟向四方五气寻遂\n[00:55.200] 燧火旁八卦百草揆经纬\n[00:58.220] 正位 纪天下一归\n[01:00.370] 不消祈天退水\n[01:02.590] 初难知一念一决生龙髓\n[01:05.930] 百家注龙慧 千军起龙威 砥淬\n[01:10.260] 妙笔生文穗 罡风抚长麾\n[01:13.030] 始见龙形汇 以天田冲腾直向九陲\n[01:20.490] 龙震于疆 万里宁壤 天地皆可往\n[01:24.140] 龙秀于象 引仙来访 诗蜀道河江\n[01:27.940] 龙明于章 执笔成鉴 映五千煌煌\n[01:31.820] 不独九州五岳 帝王将相见苍茫\n[01:35.340] 龙泽于汤 唤水筑乡 单舟见京杭\n[01:39.040] 龙健于常 百音同讲 道一种炎黄\n[01:42.740] 龙景于康 见之庙堂 亦显于曲坊\n[01:46.580] 不劳此间祥云瑞兽频频诰春长!\n[01:57.280] 干支移晷又几回\n[01:59.500] 揽尽天骄襄助一醉\n[02:01.790] 虽万言竟道不尽无字碑\n[02:04.680] 临渊乾乾 君子催\n[02:06.600] 或跃 无咎相随\n[02:09.130] 同为龙 却与往昔不连讳\n[02:12.390] 且待飞龙归 簸却沧溟水 如沸\n[02:16.700] 有龙掸风雷 见首不见尾\n[02:19.540] 苏苏万物蜕 证元亨利贞变易轮回\n[02:27.080] 龙华于旸 红旗漫卷 新水濯旧隍\n[02:30.740] 龙泰于霜 烽烟消长 更赳赳昂昂\n[02:34.430] 龙温于壮 留待潺潺 驰涌成泱泱\n[02:38.260] 好教流光紫极 鹊渡银潢伴流觞\n[02:41.660] 龙韧于刚 龙吟激荡 云止聆佳响\n[02:45.490] 龙德于昌 喜见船马 纵横间丰仓\n[02:49.150] 龙眷于邦 情习众广 仍化为一方\n[02:52.930] 其妙错综复杂 不孤兵车付一匡!\n[02:56.602]\n[03:40.980] 此去向东 瀚海游龙 滔滔几万重\n[03:44.620] 一跃破空 乘风逐虹 猎猎青云中\n[03:48.250] 天音入梦 扶摇上穹 矫矫游星宫\n[03:52.080] 犹念神州谷稻耕耘收藏守时无?\n[03:55.670] 一息一动 似异似同 无之以为用\n[03:59.240] 天地辰龙 龙生九种 但两爻合共\n[04:03.130] 假逢童蒙 欲解懵懂 何处有真龙\n[04:06.830] 只道「大哉乾元」秩秩幽幽必然中\n[04:10.350] 也道「大哉乾元」切切实实一言中!\n[04:14.693]"
|
||||
}
|
@ -37,5 +37,6 @@
|
||||
"views": 3021,
|
||||
"publishTime": "2024-07-12 00:00:00",
|
||||
"updateTime": "2024-07-12 01:49:30",
|
||||
"netEaseID": 2606318125,
|
||||
"lyric": "[ti: 唱给锦依卫]\n[ar: 洛天依]\n[tool: 歌词滚动姬 https://lrc-maker.github.io]\n[00:00.000]\n[00:11.400] 推开窗,又是一缕曙光轻拂你脸庞\n[00:17.100] 风轻扬,浮现初见时你青涩模样\n[00:22.830] 齐开唱,歌声驱散你眼神中的迷茫\n[00:28.770] 飘过秋叶与春花,流转几轮冬与夏\n[00:34.410] 乘着馨风去远航,有你长夜也明亮\n[00:40.230] 携手向梦的彼方,追寻心中那道光\n[00:46.800] 一起唱下去吧\n[00:50.880] 用你我梦想\n[00:53.610] 希望光芒为你点亮\n[00:58.230] 续写未来篇章\n[01:02.400] 纵岁月漫长\n[01:05.220] 会永远传唱\n[01:09.797]\n[01:32.370] 指尖淌,自由弦音随着风儿去流浪\n[01:38.040] 回头望,路上不时也会跌跌撞撞\n[01:44.010] 别彷徨,遗忘那过去的灰心和失望\n[01:49.740] 有你相伴的过往,每篇故事都珍藏\n[01:55.470] 载着梦想去远航,你我并肩去闯荡\n[02:01.260] 执笔最美的诗行,烟火为你而绽放\n[02:07.830] 一起唱下去吧\n[02:11.640] 星澜里荡漾\n[02:14.700] 留下最绚烂的光芒\n[02:19.350] 唱你我的梦想\n[02:23.370] 在舞台中央\n[02:26.190] 交织中回响\n[02:31.020] 一起唱下去吧\n[02:34.980] 用你我梦想\n[02:37.800] 将希望光芒再点亮\n[02:42.450] 续写未来篇章\n[02:46.500] 纵岁月沧桑\n[02:49.410] 永远在你身旁\n[02:53.364]"
|
||||
}
|
62
src/lib/components/database/songCard.svelte
Normal file
62
src/lib/components/database/songCard.svelte
Normal 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>
|
@ -11,6 +11,7 @@
|
||||
export let originalLyrics: LrcJsonData;
|
||||
export let progress: number;
|
||||
|
||||
// Local state and variables
|
||||
let getLyricIndex: Function;
|
||||
let debugMode = false;
|
||||
if (localStorage.getItem('debugMode') == null) {
|
||||
@ -31,11 +32,13 @@
|
||||
|
||||
let currentLyricTopMargin = 288;
|
||||
|
||||
// References to lyric elements
|
||||
let refs: HTMLParagraphElement[] = [];
|
||||
let _refs: any[] = [];
|
||||
$: refs = _refs.filter(Boolean);
|
||||
$: getLyricIndex = createLyricsSearcher(originalLyrics);
|
||||
|
||||
// Helper function to get CSS class for a lyric based on its index and progress
|
||||
function getClass(lyricIndex: number, progress: number) {
|
||||
if (!originalLyrics.scripts) return 'previous-lyric';
|
||||
if (currentLyricIndex === lyricIndex) return 'current-lyric';
|
||||
@ -76,8 +79,8 @@
|
||||
for (let i = processingLineIndex; i < refs.length; i++) {
|
||||
refs[i].style.transition =
|
||||
'transform 0s, filter 200ms ease, opacity 200ms ease, font-size 200ms ease, scale 250ms ease';
|
||||
const h = refs[i].getBoundingClientRect().height;
|
||||
refs[i].style.transform = `translateY(${-h}px)`;
|
||||
const height = refs[i].getBoundingClientRect().height;
|
||||
refs[i].style.transform = `translateY(${-height}px)`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,7 +104,8 @@
|
||||
scriptScrolling = false;
|
||||
}
|
||||
|
||||
async function b(currentLyric: HTMLParagraphElement) {
|
||||
// Scroll the lyrics container to the given lyric
|
||||
async function scrollToLyric(currentLyric: HTMLParagraphElement) {
|
||||
if (!originalLyrics || !originalLyrics.scripts || !lyricsContainer) return;
|
||||
scriptScrolling = true;
|
||||
lyricsContainer.scrollTop += currentLyric.getBoundingClientRect().top - currentLyricTopMargin;
|
||||
@ -133,18 +137,18 @@
|
||||
}
|
||||
});
|
||||
|
||||
// progressBarRaw is used to detect progress changes at system-level (not in AquaVox)
|
||||
// Handle progress changes at system level
|
||||
progressBarRaw.subscribe((progress: number) => {
|
||||
if ($userAdjustingProgress === false && getLyricIndex) {
|
||||
if (Math.abs(localProgress - progress) > 0.6) {
|
||||
const currentLyric = refs[getLyricIndex(progress)];
|
||||
b(currentLyric);
|
||||
scrollToLyric(currentLyric);
|
||||
}
|
||||
localProgress = progress;
|
||||
}
|
||||
});
|
||||
|
||||
// progressBarSlideValue is to detect progress bar sliding event
|
||||
// Handle progress bar sliding events
|
||||
progressBarSlideValue.subscribe((_) => {
|
||||
if ($userAdjustingProgress === false && getLyricIndex) {
|
||||
lastAdjustProgress = currentPositionIndex;
|
||||
@ -325,7 +329,7 @@
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
scrollbar-width: 10px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
|
5
src/lib/getVersion.ts
Normal file
5
src/lib/getVersion.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import * as pjson from "../../package.json";
|
||||
|
||||
export default function getVersion(){
|
||||
return pjson.version;
|
||||
}
|
1
src/lib/server/database/musicInfo.d.ts
vendored
1
src/lib/server/database/musicInfo.d.ts
vendored
@ -19,5 +19,6 @@ interface MusicMetadata {
|
||||
views: number | null;
|
||||
publishTime: string | null;
|
||||
updateTime: string | null;
|
||||
netEaseID: number | null;
|
||||
lyric: string | null;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import extractFileName from '$lib/extractFileName';
|
||||
import getVersion from '$lib/getVersion';
|
||||
import toHumanSize from '$lib/humanSize';
|
||||
import localforage from '$lib/storage';
|
||||
interface Song {
|
||||
@ -77,7 +78,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
AquaVox 1.10.1 · 早期公开预览 · 源代码参见
|
||||
AquaVox {getVersion()} · 早期公开预览 · 源代码参见
|
||||
<a href="https://github.com/alikia2x/aquavox">GitHub</a>
|
||||
</p>
|
||||
<a href="/import">导入音乐</a> <br />
|
||||
|
17
src/routes/database/+page.server.ts
Normal file
17
src/routes/database/+page.server.ts
Normal 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;
|
@ -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-40
|
||||
overflow-hidden text-ellipsis"
|
||||
style="text-shadow: 0px 0px 4px rgba(65, 65, 65, .6);"
|
||||
>
|
||||
{song.singer.join(', ')}
|
||||
</span>
|
||||
<div class="absolute right-2 bottom-2 text-right">
|
||||
{#if song.duration}
|
||||
<span style="text-shadow: 0px 0px 4px rgba(65, 65, 65, .6);">{formatDuration(song.duration)}</span>
|
||||
{/if}
|
||||
<br />
|
||||
{#if song.views}
|
||||
<span style="text-shadow: 0px 0px 4px rgba(65, 65, 65, .6);">{formatViews(song.views)}播放</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<SongCard songData={song}/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
18
src/routes/database/page/[id]/+page.server.ts
Normal file
18
src/routes/database/page/[id]/+page.server.ts
Normal 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;
|
34
src/routes/database/page/[id]/+page.svelte
Normal file
34
src/routes/database/page/[id]/+page.svelte
Normal 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>
|
@ -22,6 +22,7 @@
|
||||
views: null,
|
||||
publishTime: null,
|
||||
updateTime: getCurrentFormattedDateTime(),
|
||||
netEaseID: null,
|
||||
lyric: null
|
||||
};
|
||||
let editingData: string = JSON.stringify(templateSongData, null, 8);
|
||||
|
@ -24,5 +24,10 @@ export default defineConfig({
|
||||
rollupOptions: {
|
||||
plugins: [rollupNodePolyFill()]
|
||||
}
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
allow: ["./package.json"]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user