merge: ref/solid into main branch
This commit is contained in:
commit
4e08168ef4
6
.idea/compiler.xml
Normal file
6
.idea/compiler.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="TypeScriptCompiler">
|
||||
<option name="useTypesFromServer" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
@ -30,6 +30,12 @@
|
||||
<excludeFolder url="file://$MODULE_DIR$/redis" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/ml" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/crawler/.cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/solid/.vinxi" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.jj" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.jj/repo" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.jj/working_copy" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/packages/solid/.output" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
</option>
|
||||
<option name="myCustomValuesEnabled" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="HtmlUnknownTarget" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
|
||||
6
.idea/prettier.xml
Normal file
6
.idea/prettier.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PrettierConfiguration">
|
||||
<option name="myConfigurationMode" value="AUTOMATIC" />
|
||||
</component>
|
||||
</project>
|
||||
@ -9,3 +9,5 @@ MiSans.css
|
||||
*.yaml
|
||||
*.yml
|
||||
*.mdx
|
||||
packages/solid/src/drizzle/cred
|
||||
packages/solid/src/drizzle/main
|
||||
@ -2,9 +2,9 @@ import networkDelegate from "@core/net/delegate.ts";
|
||||
import type { VideoDetailsData, VideoDetailsResponse } from "@core/net/bilibili.d.ts";
|
||||
import logger from "@core/log/logger.ts";
|
||||
|
||||
export async function getVideoDetails(aid: number): Promise<VideoDetailsData | null> {
|
||||
export async function getVideoDetails(aid: number, archive: boolean = false): Promise<VideoDetailsData | null> {
|
||||
const url = `https://api.bilibili.com/x/web-interface/view/detail?aid=${aid}`;
|
||||
const data = await networkDelegate.request<VideoDetailsResponse>(url, "getVideoInfo");
|
||||
const data = await networkDelegate.request<VideoDetailsResponse>(url, archive ? "" : "getVideoInfo");
|
||||
const errMessage = `Error fetching metadata for ${aid}:`;
|
||||
if (data.code !== 0) {
|
||||
logger.error(errMessage + data.code + "-" + data.message, "net", "fn:getVideoInfo");
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
// @ts-nocheck -- skip type checking
|
||||
import { _runtime } from "fumadocs-mdx"
|
||||
import * as _source from "../source.config"
|
||||
@ -1,8 +0,0 @@
|
||||
// source.config.ts
|
||||
import { defineConfig } from "fumadocs-mdx/config";
|
||||
var source_config_default = defineConfig({
|
||||
mdxOptions: {}
|
||||
});
|
||||
export {
|
||||
source_config_default as default
|
||||
};
|
||||
11
packages/palette/.vercel/README.txt
Normal file
11
packages/palette/.vercel/README.txt
Normal file
@ -0,0 +1,11 @@
|
||||
> Why do I have a folder named ".vercel" in my project?
|
||||
The ".vercel" folder is created when you link a directory to a Vercel project.
|
||||
|
||||
> What does the "project.json" file contain?
|
||||
The "project.json" file contains:
|
||||
- The ID of the Vercel project that you linked ("projectId")
|
||||
- The ID of the user or team your Vercel project is owned by ("orgId")
|
||||
|
||||
> Should I commit the ".vercel" folder?
|
||||
No, you should not share the ".vercel" folder with anyone.
|
||||
Upon creation, it will be automatically added to your ".gitignore" file.
|
||||
1
packages/palette/.vercel/project.json
Normal file
1
packages/palette/.vercel/project.json
Normal file
@ -0,0 +1 @@
|
||||
{"projectId":"prj_a2fcj6ZRTyTlllCd2rFJm7kPLEOc","orgId":"team_DiIY95BaFppaGJqqgrXYNt5O","projectName":"cvsa-theme"}
|
||||
28
packages/solid/.gitignore
vendored
Normal file
28
packages/solid/.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
dist
|
||||
.wrangler
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.vinxi
|
||||
app.config.timestamp_*.js
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
*.launch
|
||||
.settings/
|
||||
|
||||
# Temp
|
||||
gitignore
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
8
packages/solid/.prettierrc
Normal file
8
packages/solid/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": false,
|
||||
"printWidth": 120,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
18
packages/solid/app.config.ts
Normal file
18
packages/solid/app.config.ts
Normal file
@ -0,0 +1,18 @@
|
||||
"use server";
|
||||
|
||||
import { defineConfig } from "@solidjs/start/config";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [tsconfigPaths()],
|
||||
optimizeDeps: {
|
||||
include: ["@m3-components/solid"],
|
||||
esbuildOptions: {
|
||||
jsx: "automatic",
|
||||
jsxDev: true,
|
||||
jsxImportSource: "solid-js/h"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
1785
packages/solid/bun.lock
Normal file
1785
packages/solid/bun.lock
Normal file
File diff suppressed because it is too large
Load Diff
10
packages/solid/drizzle-cred.config.ts
Normal file
10
packages/solid/drizzle-cred.config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
out: "./src/drizzle/cred",
|
||||
schema: "./src/db/schema.ts",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL_CRED!
|
||||
}
|
||||
});
|
||||
11
packages/solid/drizzle-main.config.ts
Normal file
11
packages/solid/drizzle-main.config.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import "dotenv/config";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
out: "./src/drizzle/main",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL_MAIN!
|
||||
},
|
||||
tablesFilter: ["*"],
|
||||
});
|
||||
45
packages/solid/package.json
Normal file
45
packages/solid/package.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "solid",
|
||||
"scripts": {
|
||||
"dev": "vinxi dev --port 7400 --host",
|
||||
"build": "vinxi build",
|
||||
"start": "bunx node-env-run --exec bun -- run vinxi start --port 7400",
|
||||
"version": "vinxi version",
|
||||
"format": "prettier . --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@m3-components/solid": "0.2.6",
|
||||
"@solid-primitives/media": "^2.3.3",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@solidjs/start": "^1.1.7",
|
||||
"@types/luxon": "^3.7.1",
|
||||
"@ungap/has-own": "^0.1.1",
|
||||
"animejs": "^4.1.2",
|
||||
"axios": "^1.11.0",
|
||||
"dotenv": "^16.6.1",
|
||||
"drizzle-orm": "^0.44.4",
|
||||
"luxon": "^3.7.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"postgres": "^3.4.7",
|
||||
"solid-js": "^1.9.8",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "3",
|
||||
"vinxi": "^0.5.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@csstools/postcss-bundler": "^2.0.8",
|
||||
"@csstools/postcss-cascade-layers": "^5.0.2",
|
||||
"@csstools/postcss-oklab-function": "^4.0.10",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"postcss": "^8.5.6",
|
||||
"tsx": "^4.20.3",
|
||||
"vite-plugin-solid": "^2.11.8",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
6
packages/solid/postcss.config.ts
Normal file
6
packages/solid/postcss.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
BIN
packages/solid/public/fonts/IBMPlexSansCondDigits-Medium.woff2
Normal file
BIN
packages/solid/public/fonts/IBMPlexSansCondDigits-Medium.woff2
Normal file
Binary file not shown.
4
packages/solid/public/icons/zh/appbar_desktop_dark.svg
Normal file
4
packages/solid/public/icons/zh/appbar_desktop_dark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 21 KiB |
15
packages/solid/public/icons/zh/appbar_desktop_light.svg
Normal file
15
packages/solid/public/icons/zh/appbar_desktop_light.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 22 KiB |
14
packages/solid/public/icons/zh/appbar_mobile_dark.svg
Normal file
14
packages/solid/public/icons/zh/appbar_mobile_dark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.3 KiB |
14
packages/solid/public/icons/zh/appbar_mobile_light.svg
Normal file
14
packages/solid/public/icons/zh/appbar_mobile_light.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.4 KiB |
241
packages/solid/src/app.css
Normal file
241
packages/solid/src/app.css
Normal file
@ -0,0 +1,241 @@
|
||||
@import url("https://assets.projectcvsa.com/hm-sans/index.css");
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--md-sys-color-background: #fff8f6;
|
||||
--md-sys-color-on-background: #2a1613;
|
||||
--md-sys-color-surface: #fff8f6;
|
||||
--md-sys-color-surface-dim: #f7d2cc;
|
||||
--md-sys-color-surface-bright: #fff8f6;
|
||||
--md-sys-color-surface-container-lowest: #ffffff;
|
||||
--md-sys-color-surface-container-low: #fff0ee;
|
||||
--md-sys-color-surface-container: #ffe9e6;
|
||||
--md-sys-color-surface-container-high: #ffe2dd;
|
||||
--md-sys-color-surface-container-highest: #ffdad4;
|
||||
--md-sys-color-on-surface: #2a1613;
|
||||
--md-sys-color-surface-variant: #ffdad4;
|
||||
--md-sys-color-on-surface-variant: #5f3e39;
|
||||
--md-sys-color-inverse-surface: #422b27;
|
||||
--md-sys-color-inverse-on-surface: #ffedea;
|
||||
--md-sys-color-outline: #946e68;
|
||||
--md-sys-color-outline-variant: #eabcb4;
|
||||
--md-sys-color-shadow: #000000;
|
||||
--md-sys-color-scrim: #000000;
|
||||
--md-sys-color-surface-tint: #c00100;
|
||||
--md-sys-color-primary: #ee0000;
|
||||
--md-sys-color-on-primary: #ffffff;
|
||||
--md-sys-color-primary-container: #ee0000;
|
||||
--md-sys-color-on-primary-container: #ffffff;
|
||||
--md-sys-color-inverse-primary: #ffb4a8;
|
||||
--md-sys-color-secondary: #b4271a;
|
||||
--md-sys-color-on-secondary: #ffffff;
|
||||
--md-sys-color-secondary-container: #ff7460;
|
||||
--md-sys-color-on-secondary-container: #2f0000;
|
||||
--md-sys-color-tertiary: #6f4800;
|
||||
--md-sys-color-on-tertiary: #ffffff;
|
||||
--md-sys-color-tertiary-container: #9f6900;
|
||||
--md-sys-color-on-tertiary-container: #ffffff;
|
||||
--md-sys-color-error: #ba1a1a;
|
||||
--md-sys-color-on-error: #ffffff;
|
||||
--md-sys-color-error-container: #ffdad6;
|
||||
--md-sys-color-on-error-container: #410002;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--md-sys-color-background: #fff8f6;
|
||||
--md-sys-color-on-background: #2a1613;
|
||||
--md-sys-color-surface: #fff8f6;
|
||||
--md-sys-color-surface-dim: #f7d2cc;
|
||||
--md-sys-color-surface-bright: #fff8f6;
|
||||
--md-sys-color-surface-container-lowest: #ffffff;
|
||||
--md-sys-color-surface-container-low: #fff0ee;
|
||||
--md-sys-color-surface-container: #ffe9e6;
|
||||
--md-sys-color-surface-container-high: #ffe2dd;
|
||||
--md-sys-color-surface-container-highest: #ffdad4;
|
||||
--md-sys-color-on-surface: #2a1613;
|
||||
--md-sys-color-surface-variant: #ffdad4;
|
||||
--md-sys-color-on-surface-variant: #5f3e39;
|
||||
--md-sys-color-inverse-surface: #422b27;
|
||||
--md-sys-color-inverse-on-surface: #ffedea;
|
||||
--md-sys-color-outline: #946e68;
|
||||
--md-sys-color-outline-variant: #eabcb4;
|
||||
--md-sys-color-shadow: #000000;
|
||||
--md-sys-color-scrim: #000000;
|
||||
--md-sys-color-surface-tint: #c00100;
|
||||
--md-sys-color-primary: #ee0000;
|
||||
--md-sys-color-on-primary: #ffffff;
|
||||
--md-sys-color-primary-container: #ee0000;
|
||||
--md-sys-color-on-primary-container: #ffffff;
|
||||
--md-sys-color-inverse-primary: #ffb4a8;
|
||||
--md-sys-color-secondary: #b4271a;
|
||||
--md-sys-color-on-secondary: #ffffff;
|
||||
--md-sys-color-secondary-container: #ff7460;
|
||||
--md-sys-color-on-secondary-container: #2f0000;
|
||||
--md-sys-color-tertiary: #6f4800;
|
||||
--md-sys-color-on-tertiary: #ffffff;
|
||||
--md-sys-color-tertiary-container: #9f6900;
|
||||
--md-sys-color-on-tertiary-container: #ffffff;
|
||||
--md-sys-color-error: #ba1a1a;
|
||||
--md-sys-color-on-error: #ffffff;
|
||||
--md-sys-color-error-container: #ffdad6;
|
||||
--md-sys-color-on-error-container: #410002;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--md-sys-color-background: #210e0b;
|
||||
--md-sys-color-on-background: #ffdad4;
|
||||
--md-sys-color-surface: #210e0b;
|
||||
--md-sys-color-surface-dim: #210e0b;
|
||||
--md-sys-color-surface-bright: #4b332f;
|
||||
--md-sys-color-surface-container-lowest: #1b0907;
|
||||
--md-sys-color-surface-container-low: #2a1613;
|
||||
--md-sys-color-surface-container: #2f1a17;
|
||||
--md-sys-color-surface-container-high: #3a2421;
|
||||
--md-sys-color-surface-container-highest: #462f2b;
|
||||
--md-sys-color-on-surface: #ffdad4;
|
||||
--md-sys-color-surface-variant: #5f3e39;
|
||||
--md-sys-color-on-surface-variant: #eabcb4;
|
||||
--md-sys-color-inverse-surface: #ffdad4;
|
||||
--md-sys-color-inverse-on-surface: #422b27;
|
||||
--md-sys-color-outline: #b08780;
|
||||
--md-sys-color-outline-variant: #5f3e39;
|
||||
--md-sys-color-shadow: #000000;
|
||||
--md-sys-color-scrim: #000000;
|
||||
--md-sys-color-surface-tint: #ffb4a8;
|
||||
--md-sys-color-primary: #ffb4a8;
|
||||
--md-sys-color-on-primary: #690000;
|
||||
--md-sys-color-primary-container: #de0000;
|
||||
--md-sys-color-on-primary-container: #ffffff;
|
||||
--md-sys-color-inverse-primary: #c00100;
|
||||
--md-sys-color-secondary: #ffb4a8;
|
||||
--md-sys-color-on-secondary: #690000;
|
||||
--md-sys-color-secondary-container: #870100;
|
||||
--md-sys-color-on-secondary-container: #ffc9c0;
|
||||
--md-sys-color-tertiary: #feba54;
|
||||
--md-sys-color-on-tertiary: #452b00;
|
||||
--md-sys-color-tertiary-container: #966300;
|
||||
--md-sys-color-on-tertiary-container: #ffffff;
|
||||
--md-sys-color-error: #ffb4ab;
|
||||
--md-sys-color-on-error: #690005;
|
||||
--md-sys-color-error-container: #93000a;
|
||||
--md-sys-color-on-error-container: #ffdad6;
|
||||
}
|
||||
}
|
||||
|
||||
.gradient-blur {
|
||||
inset: auto 0 0 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.gradient-blur > div,
|
||||
.gradient-blur::before,
|
||||
.gradient-blur::after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.gradient-blur::before {
|
||||
content: "";
|
||||
z-index: 1;
|
||||
backdrop-filter: blur(0.5px);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 1) 12.5%,
|
||||
rgba(0, 0, 0, 1) 25%,
|
||||
rgba(0, 0, 0, 0) 37.5%
|
||||
);
|
||||
}
|
||||
.gradient-blur > div:nth-of-type(1) {
|
||||
z-index: 2;
|
||||
backdrop-filter: blur(1px);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 12.5%,
|
||||
rgba(0, 0, 0, 1) 25%,
|
||||
rgba(0, 0, 0, 1) 37.5%,
|
||||
rgba(0, 0, 0, 0) 50%
|
||||
);
|
||||
}
|
||||
.gradient-blur > div:nth-of-type(2) {
|
||||
z-index: 3;
|
||||
backdrop-filter: blur(2px);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 25%,
|
||||
rgba(0, 0, 0, 1) 37.5%,
|
||||
rgba(0, 0, 0, 1) 50%,
|
||||
rgba(0, 0, 0, 0) 62.5%
|
||||
);
|
||||
}
|
||||
.gradient-blur > div:nth-of-type(3) {
|
||||
z-index: 4;
|
||||
backdrop-filter: blur(4px);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 37.5%,
|
||||
rgba(0, 0, 0, 1) 50%,
|
||||
rgba(0, 0, 0, 1) 62.5%,
|
||||
rgba(0, 0, 0, 0) 75%
|
||||
);
|
||||
}
|
||||
.gradient-blur > div:nth-of-type(4) {
|
||||
z-index: 5;
|
||||
backdrop-filter: blur(8px);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 50%,
|
||||
rgba(0, 0, 0, 1) 62.5%,
|
||||
rgba(0, 0, 0, 1) 75%,
|
||||
rgba(0, 0, 0, 0) 87.5%
|
||||
);
|
||||
}
|
||||
.gradient-blur > div:nth-of-type(5) {
|
||||
z-index: 6;
|
||||
backdrop-filter: blur(13px);
|
||||
mask: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0) 62.5%,
|
||||
rgba(0, 0, 0, 1) 75%,
|
||||
rgba(0, 0, 0, 1) 87.5%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
}
|
||||
.gradient-blur > div:nth-of-type(6) {
|
||||
z-index: 7;
|
||||
backdrop-filter: blur(24px);
|
||||
mask: linear-gradient(to bottom, rgba(0, 0, 0, 0) 75%, rgba(0, 0, 0, 1) 87.5%, rgba(0, 0, 0, 1) 100%);
|
||||
}
|
||||
.gradient-blur::after {
|
||||
content: "";
|
||||
z-index: 8;
|
||||
backdrop-filter: blur(48px);
|
||||
mask: linear-gradient(to bottom, rgba(0, 0, 0, 0) 87.5%, rgba(0, 0, 0, 1) 100%);
|
||||
}
|
||||
|
||||
a:not(.clear) {
|
||||
@apply text-primary;
|
||||
}
|
||||
|
||||
:root {
|
||||
font-family: "HarmonyOS Sans SC", sans-serif;
|
||||
@apply bg-surface text-on-surface;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IPSD";
|
||||
src: url("/fonts/IBMPlexSansCondDigits-Medium.woff2") format("woff2");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[type="search"]::-webkit-search-cancel-button,
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
40
packages/solid/src/app.tsx
Normal file
40
packages/solid/src/app.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
// polyfill
|
||||
import "@ungap/has-own";
|
||||
|
||||
import { MetaProvider } from "@solidjs/meta";
|
||||
import { Router } from "@solidjs/router";
|
||||
import { FileRoutes } from "@solidjs/start/router";
|
||||
import { onMount, Suspense } from "solid-js";
|
||||
import "@m3-components/solid/index.css";
|
||||
import "./app.css";
|
||||
import { setActiveTab, tabMap } from "./components/layout/Navigation";
|
||||
import { minimatch } from "minimatch";
|
||||
|
||||
export const refreshTab = (path: string) => {
|
||||
for (const [key, value] of Object.entries(tabMap)) {
|
||||
if (!minimatch(path, key)) continue;
|
||||
setActiveTab(value);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
onMount(() => {
|
||||
refreshTab(location.pathname);
|
||||
window.addEventListener("popstate", (event) => {
|
||||
refreshTab(location.pathname);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Router
|
||||
root={(props) => (
|
||||
<MetaProvider>
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</MetaProvider>
|
||||
)}
|
||||
>
|
||||
<FileRoutes />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
4
packages/solid/src/components/common.d.ts
vendored
Normal file
4
packages/solid/src/components/common.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
import { JSX } from "solid-js";
|
||||
|
||||
export type DivProps = JSX.HTMLAttributes<HTMLDivElement>;
|
||||
export type ElementProps = JSX.HTMLAttributes<HTMLElement>;
|
||||
23
packages/solid/src/components/icons/Album.tsx
Normal file
23
packages/solid/src/components/icons/Album.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { SVGIconComponent } from "./types";
|
||||
|
||||
export const AlbumIcon: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 16.5q1.875 0 3.188-1.312T16.5 12t-1.312-3.187T12 7.5T8.813 8.813T7.5 12t1.313 3.188T12 16.5m0-3.5q-.425 0-.712-.288T11 12t.288-.712T12 11t.713.288T13 12t-.288.713T12 13m0 9q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4T6.325 6.325T4 12t2.325 5.675T12 20m0-8"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const AlbumFilledIcon: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 16.5q1.875 0 3.188-1.312T16.5 12t-1.312-3.187T12 7.5T8.813 8.813T7.5 12t1.313 3.188T12 16.5m0-3.5q-.425 0-.712-.288T11 12t.288-.712T12 11t.713.288T13 12t-.288.713T12 13m0 9q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
12
packages/solid/src/components/icons/Arrow.tsx
Normal file
12
packages/solid/src/components/icons/Arrow.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { SVGIconComponent } from "~/components/icons/types";
|
||||
|
||||
export const RightArrow: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 28 28" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M18.84 15.17h-13c-.34 0-.61-.12-.84-.34-.22-.22-.33-.5-.33-.83a1.13 1.13 0 0 1 1.17-1.17h13l-3.32-3.32c-.24-.23-.35-.5-.34-.82.01-.3.12-.58.34-.81.23-.24.5-.36.83-.37.32 0 .6.1.83.34l5.34 5.33c.11.12.2.25.25.38a1.34 1.34 0 0 1 0 .88 1 1 0 0 1-.25.38l-5.34 5.33c-.23.24-.51.35-.83.34a1.19 1.19 0 0 1-1.17-1.18c0-.31.1-.58.34-.82l3.32-3.32Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
12
packages/solid/src/components/icons/Edit.tsx
Normal file
12
packages/solid/src/components/icons/Edit.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { SVGIconComponent } from "./types";
|
||||
|
||||
export const EditIcon: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5 19h1.425L16.2 9.225L14.775 7.8L5 17.575zm-2 2v-4.25L16.2 3.575q.3-.275.663-.425t.762-.15t.775.15t.65.45L20.425 5q.3.275.438.65T21 6.4q0 .4-.137.763t-.438.662L7.25 21zM19 6.4L17.6 5zm-3.525 2.125l-.7-.725L16.2 9.225z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
12
packages/solid/src/components/icons/History.tsx
Normal file
12
packages/solid/src/components/icons/History.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { SVGIconComponent } from "./types";
|
||||
|
||||
export const HistoryIcon: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 21q-3.15 0-5.575-1.912T3.275 14.2q-.1-.375.15-.687t.675-.363q.4-.05.725.15t.45.6q.6 2.25 2.475 3.675T12 19q2.925 0 4.963-2.037T19 12t-2.037-4.962T12 5q-1.725 0-3.225.8T6.25 8H8q.425 0 .713.288T9 9t-.288.713T8 10H4q-.425 0-.712-.288T3 9V5q0-.425.288-.712T4 4t.713.288T5 5v1.35q1.275-1.6 3.113-2.475T12 3q1.875 0 3.513.713t2.85 1.924t1.925 2.85T21 12t-.712 3.513t-1.925 2.85t-2.85 1.925T12 21m1-9.4l2.5 2.5q.275.275.275.7t-.275.7t-.7.275t-.7-.275l-2.8-2.8q-.15-.15-.225-.337T11 11.975V8q0-.425.288-.712T12 7t.713.288T13 8z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
23
packages/solid/src/components/icons/Home.tsx
Normal file
23
packages/solid/src/components/icons/Home.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { SVGIconComponent } from "./types";
|
||||
|
||||
export const HomeFilledIcon: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4 19v-9q0-.475.213-.9t.587-.7l6-4.5q.525-.4 1.2-.4t1.2.4l6 4.5q.375.275.588.7T20 10v9q0 .825-.588 1.413T18 21h-3q-.425 0-.712-.288T14 20v-5q0-.425-.288-.712T13 14h-2q-.425 0-.712.288T10 15v5q0 .425-.288.713T9 21H6q-.825 0-1.412-.587T4 19"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const HomeIcon: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M6 19h3v-5q0-.425.288-.712T10 13h4q.425 0 .713.288T15 14v5h3v-9l-6-4.5L6 10zm-2 0v-9q0-.475.213-.9t.587-.7l6-4.5q.525-.4 1.2-.4t1.2.4l6 4.5q.375.275.588.7T20 10v9q0 .825-.588 1.413T18 21h-4q-.425 0-.712-.288T13 20v-5h-2v5q0 .425-.288.713T10 21H6q-.825 0-1.412-.587T4 19m8-6.75"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
12
packages/solid/src/components/icons/Link.tsx
Normal file
12
packages/solid/src/components/icons/Link.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { SVGIconComponent } from "./types";
|
||||
|
||||
export const LinkIcon: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M11 17H7a4.82 4.82 0 0 1-3.54-1.46A4.82 4.82 0 0 1 2 12c0-1.38.49-2.56 1.46-3.54A4.82 4.82 0 0 1 7 7h4v2H7c-.83 0-1.54.3-2.13.88A2.9 2.9 0 0 0 4 12c0 .83.3 1.54.88 2.13.58.58 1.29.87 2.12.87h4v2Zm-3-4v-2h8v2H8Zm5 4v-2h4c.83 0 1.54-.3 2.13-.88.58-.58.87-1.29.87-2.12 0-.83-.3-1.54-.88-2.13A2.9 2.9 0 0 0 17 9h-4V7h4c1.38 0 2.56.49 3.54 1.46A4.82 4.82 0 0 1 22 12c0 1.38-.49 2.56-1.46 3.54A4.8 4.8 0 0 1 17 17h-4Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
12
packages/solid/src/components/icons/Music.tsx
Normal file
12
packages/solid/src/components/icons/Music.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { SVGIconComponent } from "./types";
|
||||
|
||||
export const MusicIcon: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
d="M13.82 13.82q.58-.58.58-1.42V8H16q.35 0 .57-.23a.8.8 0 0 0 .23-.57.8.8 0 0 0-.23-.57.8.8 0 0 0-.57-.23h-1.6a.8.8 0 0 0-.57.23.8.8 0 0 0-.23.57v3.6a2 2 0 0 0-1.2-.4q-.84 0-1.42.58t-.58 1.42.58 1.42 1.42.58 1.42-.58M8.8 16.8q-.66 0-1.13-.47a1.5 1.5 0 0 1-.47-1.13V5.6q0-.66.47-1.13T8.8 4h9.6q.66 0 1.13.47T20 5.6v9.6q0 .66-.47 1.13t-1.13.47zm0-1.6h9.6V5.6H8.8zM5.6 20q-.66 0-1.13-.47A1.5 1.5 0 0 1 4 18.4V8q0-.34.23-.57a.8.8 0 0 1 .57-.23q.35 0 .57.23.23.22.23.57v10.4H16q.34 0 .57.23.23.22.23.57a.8.8 0 0 1-.8.8zM8.8 5.6v9.6z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
12
packages/solid/src/components/icons/Search.tsx
Normal file
12
packages/solid/src/components/icons/Search.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { SVGIconComponent } from "~/components/icons/types";
|
||||
|
||||
export const SearchIcon: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
34
packages/solid/src/components/icons/StarBadges.tsx
Normal file
34
packages/solid/src/components/icons/StarBadges.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { SVGIconComponent } from "./types";
|
||||
|
||||
export const StarBadge4: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17.78 4c.78 0 1.05.08 1.34.23.28.15.5.37.65.65.15.29.23.56.23 1.34v11.56c0 .78-.08 1.05-.23 1.34-.15.28-.37.5-.65.65-.29.15-.56.23-1.34.23H6.22c-.78 0-1.05-.08-1.34-.23-.28-.15-.5-.37-.65-.65-.15-.29-.23-.56-.23-1.34V6.22c0-.78.08-1.05.23-1.34.15-.28.37-.5.65-.65.29-.15.56-.23 1.34-.23h11.56ZM12 8a.45.45 0 0 0-.45.31l-.78 1.84a.43.43 0 0 1-.36.26l-1.97.16c-.24.02-.39.13-.46.34a.5.5 0 0 0 .15.54l1.52 1.31c.12.1.17.26.13.42l-.44 1.93c-.05.22.01.4.19.52.17.12.35.13.54 0l1.71-1.01a.4.4 0 0 1 .44 0l1.71 1.02c.19.12.37.11.54-.01.18-.13.24-.3.19-.52l-.44-1.93a.43.43 0 0 1 .13-.42l1.52-1.31a.48.48 0 0 0 .15-.54c-.07-.21-.22-.32-.46-.34l-1.97-.16a.43.43 0 0 1-.36-.26l-.78-1.84a.45.45 0 0 0-.45-.3Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const StarBadge6: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12.52 2.64c.17.1.3.23.4.4l1.97 3.42h4.54c.18 0 .36.05.52.14h.01c.52.3.7.96.4 1.47L18.09 12l2.25 3.9a1.07 1.07 0 0 1-.93 1.6h-4.5l-1.97 3.43c-.1.16-.22.3-.38.39h-.01c-.52.3-1.17.13-1.47-.39l-1.97-3.42H4.57a1.07 1.07 0 0 1-.93-1.61l2.27-3.93-2.25-3.9a1.07 1.07 0 0 1-.14-.52v-.01c0-.6.48-1.08 1.07-1.08h4.5l1.97-3.42c.3-.52.95-.7 1.46-.4ZM12 8.61a.4.4 0 0 0-.39.25l-.67 1.6a.43.43 0 0 1-.36.26l-1.7.13c-.2.01-.34.11-.4.3a.42.42 0 0 0 .14.47l1.3 1.13c.12.1.17.26.13.42l-.38 1.66c-.04.19.01.34.17.45.15.11.3.11.47.01l1.47-.88c.14-.08.3-.08.44 0l1.47.88c.17.1.32.1.48 0a.4.4 0 0 0 .16-.45l-.38-1.67a.43.43 0 0 1 .14-.42l1.3-1.13c.14-.13.19-.29.13-.47s-.19-.28-.39-.3l-1.7-.13a.43.43 0 0 1-.36-.26l-.67-1.59a.4.4 0 0 0-.4-.26Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const StarBadge8: SVGIconComponent = (props) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m12.22 14.25 1.43.86c.16.1.31.1.46-.01a.4.4 0 0 0 .16-.44l-.37-1.62a.42.42 0 0 1 .14-.41l1.26-1.1a.4.4 0 0 0 .13-.46c-.06-.18-.19-.28-.39-.3l-1.65-.12a.42.42 0 0 1-.35-.26l-.65-1.54a.39.39 0 0 0-.39-.26.39.39 0 0 0-.39.26l-.65 1.54a.42.42 0 0 1-.35.26l-1.65.13c-.2.01-.33.1-.39.29a.4.4 0 0 0 .13.46l1.26 1.1c.12.1.18.26.14.41l-.37 1.62c-.04.18 0 .33.16.44.15.1.3.1.46 0l1.43-.85c.14-.08.3-.08.44 0Zm-3.27 4.6h-2.1c-.46 0-.87-.16-1.2-.5-.34-.33-.5-.74-.5-1.2v-2.1a.42.42 0 0 0-.13-.3L3.5 13.2a1.93 1.93 0 0 1-.36-.57 1.64 1.64 0 0 1 0-1.26c.08-.2.2-.4.36-.57l1.53-1.55a.42.42 0 0 0 .12-.3v-2.1c0-.46.17-.87.5-1.2.34-.34.75-.5 1.22-.5h2.1c.1 0 .21-.05.3-.13L10.8 3.5c.17-.15.36-.28.57-.36a1.64 1.64 0 0 1 1.26 0c.21.08.4.2.57.36l1.55 1.53a.4.4 0 0 0 .3.12h2.1c.46 0 .87.17 1.2.5.34.34.5.75.5 1.22v2.1c0 .1.05.21.13.3l1.53 1.54c.15.17.28.36.36.57a1.64 1.64 0 0 1 0 1.26c-.08.21-.2.4-.36.57l-1.53 1.55a.42.42 0 0 0-.12.3v2.1c0 .46-.17.87-.5 1.2-.34.34-.75.5-1.22.5h-2.1a.42.42 0 0 0-.3.13L13.2 20.5a1.9 1.9 0 0 1-.57.36 1.64 1.64 0 0 1-1.26 0c-.2-.08-.4-.2-.57-.36l-1.55-1.53a.42.42 0 0 0-.3-.12Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
5
packages/solid/src/components/icons/index.tsx
Normal file
5
packages/solid/src/components/icons/index.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./Home";
|
||||
export * from "./Music";
|
||||
export * from "./Album";
|
||||
export * from "./Search";
|
||||
export * from "./Edit";
|
||||
3
packages/solid/src/components/icons/types.d.ts
vendored
Normal file
3
packages/solid/src/components/icons/types.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { Component, JSX } from "solid-js";
|
||||
|
||||
type SVGIconComponent = Component<JSX.SvgSVGAttributes<SVGSVGElement>>;
|
||||
10
packages/solid/src/components/layout/Body.tsx
Normal file
10
packages/solid/src/components/layout/Body.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { Component } from "solid-js";
|
||||
import { DivProps } from "~/components/common";
|
||||
|
||||
export const BodyRegion: Component<DivProps> = (props) => {
|
||||
return (
|
||||
<div class="w-full min-h-full" {...props}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
37
packages/solid/src/components/layout/Navigation/Desktop.tsx
Normal file
37
packages/solid/src/components/layout/Navigation/Desktop.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Component } from "solid-js";
|
||||
import { DynamicImage } from "~/components/utils/DynamicImage";
|
||||
import {
|
||||
AppBar,
|
||||
AppBarLeadingElement,
|
||||
AppBarSearchBox,
|
||||
AppBarSearchContainer,
|
||||
AppBarTrailingElement,
|
||||
AppBarTrailingElementGroup,
|
||||
IconButton
|
||||
} from "@m3-components/solid";
|
||||
|
||||
export const NavigationDesktop: Component = () => {
|
||||
return (
|
||||
<AppBar class="hidden lg:flex h-20 xl:h-22 2xl:h-24 z-20" variant="search">
|
||||
<AppBarLeadingElement class="ml-4 h-full grow shrink basis-0">
|
||||
<DynamicImage
|
||||
class="lg:block h-full"
|
||||
darkSrc="/icons/zh/appbar_desktop_dark.svg"
|
||||
lightSrc="/icons/zh/appbar_desktop_light.svg"
|
||||
/>
|
||||
</AppBarLeadingElement>
|
||||
<AppBarSearchContainer>
|
||||
<AppBarSearchBox
|
||||
class="mx-auto text-center placeholder-on-surface-variant text-on-surface
|
||||
placeholder:font-light"
|
||||
placeholder="搜索"
|
||||
/>
|
||||
</AppBarSearchContainer>
|
||||
<AppBarTrailingElementGroup class="h-full grow shrink basis-0">
|
||||
<AppBarTrailingElement>
|
||||
<IconButton></IconButton>
|
||||
</AppBarTrailingElement>
|
||||
</AppBarTrailingElementGroup>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
114
packages/solid/src/components/layout/Navigation/Mobile.tsx
Normal file
114
packages/solid/src/components/layout/Navigation/Mobile.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import { Component, createEffect, createSignal, For } from "solid-js";
|
||||
import {
|
||||
NavigationRailFAB,
|
||||
NavigationRail,
|
||||
NavigationRailAction,
|
||||
NavigationRailActions,
|
||||
NavigationRailMenu,
|
||||
AppBar,
|
||||
AppBarLeadingElement,
|
||||
AppBarSearchBox,
|
||||
AppBarTrailingElementGroup,
|
||||
AppBarTrailingElement,
|
||||
IconButton,
|
||||
AppBarSearchContainer,
|
||||
ExtendedFAB
|
||||
} from "@m3-components/solid";
|
||||
import { A } from "@solidjs/router";
|
||||
import { SearchIcon } from "~/components/icons/Search";
|
||||
import { Portal } from "solid-js/web";
|
||||
import { animate } from "animejs";
|
||||
import { actions, actionsEn, activeTab, navigationExpanded, searchT, setActiveTab, setNavigationExpanded } from ".";
|
||||
|
||||
export const NavigationMobile: Component<{ lang?: "zh" | "en" }> = (props) => {
|
||||
const [el, setEl] = createSignal<HTMLElement | null>(null);
|
||||
|
||||
createEffect(() => {
|
||||
if (!el) return;
|
||||
if (navigationExpanded()) {
|
||||
animate(el()!, {
|
||||
x: 0,
|
||||
duration: 500,
|
||||
z: 100,
|
||||
ease: "cubicBezier(0.27, 1.06, 0.18, 1.00)"
|
||||
});
|
||||
} else {
|
||||
animate(el()!, {
|
||||
x: -380,
|
||||
duration: 500,
|
||||
z: 0,
|
||||
ease: "cubicBezier(0.27, 1.06, 0.18, 1.00)"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavigationRailMenu
|
||||
class="top-3 left-4 fixed z-[100] backdrop-blur-md shadow-xl lg:hidden"
|
||||
onClick={() => {
|
||||
setNavigationExpanded(!navigationExpanded());
|
||||
}}
|
||||
/>
|
||||
<AppBar class="z-20 lg:hidden" variant="search">
|
||||
<AppBarLeadingElement>
|
||||
<NavigationRailMenu class="invisible" />
|
||||
</AppBarLeadingElement>
|
||||
<AppBarSearchContainer class="max-sm:w-[calc(100%-7.9rem)]">
|
||||
<AppBarSearchBox placeholder="搜索" class="placeholder-on-surface-variant text-on-surface" />
|
||||
</AppBarSearchContainer>
|
||||
<AppBarTrailingElementGroup>
|
||||
<AppBarTrailingElement>
|
||||
<IconButton></IconButton>
|
||||
</AppBarTrailingElement>
|
||||
</AppBarTrailingElementGroup>
|
||||
</AppBar>
|
||||
<Portal mount={document.getElementById("modal") || undefined}>
|
||||
<div
|
||||
class="fixed lg:hidden top-0 left-0 h-full z-50"
|
||||
style="transform: translateX(-300px);"
|
||||
ref={(el) => {
|
||||
setEl(el);
|
||||
}}
|
||||
>
|
||||
<NavigationRail
|
||||
class="z-20 top-0 bg-background overflow-auto rounded-r-2xl shadow-shadow shadow-2xl"
|
||||
width={256}
|
||||
expanded={true}
|
||||
>
|
||||
<ExtendedFAB
|
||||
text={searchT[props.lang || "zh"]}
|
||||
class="left-5 top-5 font-medium leading-6 duration-100 whitespace-nowrap
|
||||
transition-none w-24 h-14 text-base mt-6"
|
||||
color="primary"
|
||||
position="unset"
|
||||
>
|
||||
<SearchIcon />
|
||||
</ExtendedFAB>
|
||||
<NavigationRailActions>
|
||||
<For each={props.lang == "en" ? actionsEn : actions}>
|
||||
{(action, index) => (
|
||||
<A href={action.href} class="clear">
|
||||
<NavigationRailAction
|
||||
activated={activeTab() == index()}
|
||||
label={action.label}
|
||||
icon={action.icon}
|
||||
onClick={() => {
|
||||
setNavigationExpanded(false);
|
||||
setActiveTab(index);
|
||||
}}
|
||||
/>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</NavigationRailActions>
|
||||
</NavigationRail>
|
||||
<div
|
||||
onclick={() => setNavigationExpanded(false)}
|
||||
class="w-screen h-screen z-10 absoluter bg-transparent"
|
||||
></div>
|
||||
</div>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
66
packages/solid/src/components/layout/Navigation/index.tsx
Normal file
66
packages/solid/src/components/layout/Navigation/index.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { Component, createSignal } from "solid-js";
|
||||
import { AlbumIcon, HomeIcon, MusicIcon } from "~/components/icons";
|
||||
|
||||
export const [activeTab, setActiveTab] = createSignal(-1);
|
||||
export const [navigationExpanded, setNavigationExpanded] = createSignal(false);
|
||||
|
||||
interface Action {
|
||||
icon: Component;
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export const actions: Action[] = [
|
||||
{
|
||||
icon: HomeIcon,
|
||||
label: "主页",
|
||||
href: "/"
|
||||
},
|
||||
{
|
||||
icon: MusicIcon,
|
||||
label: "歌曲",
|
||||
href: "/songs"
|
||||
},
|
||||
{
|
||||
icon: AlbumIcon,
|
||||
label: "专辑",
|
||||
href: "/albums"
|
||||
}
|
||||
];
|
||||
|
||||
export const actionsEn: Action[] = [
|
||||
{
|
||||
icon: HomeIcon,
|
||||
label: "Home",
|
||||
href: "/en/"
|
||||
},
|
||||
{
|
||||
icon: MusicIcon,
|
||||
label: "Songs",
|
||||
href: "/en/songs"
|
||||
},
|
||||
{
|
||||
icon: AlbumIcon,
|
||||
label: "Albums",
|
||||
href: "/en/albums"
|
||||
}
|
||||
];
|
||||
|
||||
export const tabMap = {
|
||||
"/": 0,
|
||||
"/song*": 1,
|
||||
"/song/**/*": 1,
|
||||
"/albums": 2,
|
||||
"/album/**/*": 2,
|
||||
"/en/": 0,
|
||||
"/en/songs": 1,
|
||||
"/en/song*": 1,
|
||||
"/en/song/**/*": 1,
|
||||
"/en/albums": 2,
|
||||
"/en/album/**/*": 2
|
||||
};
|
||||
|
||||
export const searchT = {
|
||||
zh: "搜索",
|
||||
en: "Search"
|
||||
};
|
||||
28
packages/solid/src/components/layout/index.tsx
Normal file
28
packages/solid/src/components/layout/index.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { NavigationMobile } from "./Navigation/Mobile";
|
||||
import { DivProps } from "../common";
|
||||
import { Component } from "solid-js";
|
||||
import { BeforeLeaveEventArgs, useBeforeLeave } from "@solidjs/router";
|
||||
import { refreshTab } from "~/app";
|
||||
import { NavigationDesktop } from "./Navigation/Desktop";
|
||||
import { BodyRegion } from "./Body";
|
||||
|
||||
interface LayoutProps extends DivProps {
|
||||
lang?: "zh" | "en";
|
||||
}
|
||||
|
||||
export const Layout: Component<LayoutProps> = (props) => {
|
||||
useBeforeLeave((e: BeforeLeaveEventArgs) => {
|
||||
if (typeof e.to === "number") {
|
||||
refreshTab(e.to.toString());
|
||||
return;
|
||||
}
|
||||
refreshTab(e.to);
|
||||
});
|
||||
return (
|
||||
<div class="relatve w-screen max-w-full min-h-screen">
|
||||
<NavigationMobile lang={props.lang} />
|
||||
<NavigationDesktop />
|
||||
<BodyRegion>{props.children}</BodyRegion>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
48
packages/solid/src/components/loginStatusContext.tsx
Normal file
48
packages/solid/src/components/loginStatusContext.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { dbCred } from "~db/index";
|
||||
import { loginSessions, users } from "~db/cred/schema";
|
||||
import { and, eq, gt, isNull, sql } from "drizzle-orm";
|
||||
import { SensitiveUserFields, UserType } from "~db/outerSchema";
|
||||
|
||||
type ReturnedUser = Omit<UserType, SensitiveUserFields>;
|
||||
|
||||
export const getUserLoggedin = async (sessionID?: string): Promise<ReturnedUser | null> => {
|
||||
if (!sessionID) {
|
||||
return null;
|
||||
}
|
||||
const session = await dbCred
|
||||
.select({
|
||||
uid: loginSessions.uid
|
||||
})
|
||||
.from(loginSessions)
|
||||
.where(
|
||||
and(
|
||||
eq(loginSessions.id, sessionID),
|
||||
gt(loginSessions.expireAt, sql`now()`),
|
||||
isNull(loginSessions.deactivatedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
if (session.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uid = session[0].uid;
|
||||
const user: ReturnedUser[] = await dbCred
|
||||
.select({
|
||||
id: users.id,
|
||||
username: users.username,
|
||||
nickname: users.nickname,
|
||||
role: users.role,
|
||||
createdAt: users.createdAt
|
||||
})
|
||||
.from(users)
|
||||
.where(eq(users.id, uid))
|
||||
.limit(1);
|
||||
|
||||
if (user.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("Query for sessionID:", sessionID);
|
||||
return user[0];
|
||||
};
|
||||
34
packages/solid/src/components/requestContext.tsx
Normal file
34
packages/solid/src/components/requestContext.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Accessor, Component, createSignal } from "solid-js";
|
||||
import { createContext, useContext } from "solid-js";
|
||||
|
||||
type Hook = {
|
||||
memoizedValue: any | null;
|
||||
deps: any[] | null;
|
||||
promise: Promise<any> | null;
|
||||
};
|
||||
|
||||
export type RequestContextValue = Map<string, Hook>;
|
||||
|
||||
export type Context = [Accessor<RequestContextValue>, (v: RequestContextValue) => void];
|
||||
|
||||
export const RequestContext = createContext<Context | null>(null);
|
||||
|
||||
export const RequestContextProvider: Component<{ children: any }> = (props) => {
|
||||
const initValue: RequestContextValue = new Map();
|
||||
const [value, setValue] = createSignal(initValue);
|
||||
const updateValue = (v: RequestContextValue) => {
|
||||
setValue(v);
|
||||
};
|
||||
|
||||
const context: Context = [value, updateValue];
|
||||
|
||||
return <RequestContext.Provider value={context}>{props.children}</RequestContext.Provider>;
|
||||
};
|
||||
|
||||
export function useRequestContext(): Context {
|
||||
const ctx = useContext(RequestContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useRequestContext must be used within a RequestContextProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
148
packages/solid/src/components/song/Content.tsx
Normal file
148
packages/solid/src/components/song/Content.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import { Component } from "solid-js";
|
||||
import { Card, CardContent, CardMedia, Typography } from "@m3-components/solid";
|
||||
import { TabSwitcher } from "~/components/song/TabSwitcher";
|
||||
import { Staff } from "~/components/song/Staff";
|
||||
import { SongType } from "~db/outerSchema";
|
||||
|
||||
export const Content: Component<{data: SongType | null}> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Card variant="outlined" class="w-full max-lg:rounded-none max-lg:border-none">
|
||||
<CardMedia
|
||||
round={false}
|
||||
src={props.data?.image || ""}
|
||||
referrerpolicy="no-referrer"
|
||||
class="relative w-full z-[2] max-lg:hidden"
|
||||
/>
|
||||
<div class="relative w-full overflow-hidden lg:hidden">
|
||||
<CardMedia
|
||||
round={false}
|
||||
src={props.data?.image || ""}
|
||||
referrerpolicy="no-referrer"
|
||||
class="relative w-full z-[2]"
|
||||
/>
|
||||
<div class="h-10 lg:h-0" />
|
||||
<CardMedia
|
||||
round={false}
|
||||
src={props.data?.image || ""}
|
||||
referrerpolicy="no-referrer"
|
||||
class="w-full absolute lg:hidden top-10 z-[1]"
|
||||
/>
|
||||
<span
|
||||
class="left-3 absolute bottom-14 z-10 text-sm text-white/95"
|
||||
style="text-shadow:0px 1px 1px rgb(0 0 0 / 0.2) "
|
||||
>
|
||||
赤羽 & 洛凛
|
||||
</span>
|
||||
<span
|
||||
class="left-3 absolute bottom-3 z-10 font-medium text-4xl text-white/90 "
|
||||
style="text-shadow: 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075)"
|
||||
>
|
||||
尘海绘仙缘
|
||||
</span>
|
||||
<span
|
||||
class="font-[Inter] right-3 absolute bottom-10 z-10 text-xl text-white/95"
|
||||
style="text-shadow: 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1)"
|
||||
>
|
||||
4:54
|
||||
</span>
|
||||
<span
|
||||
class="font-[Inter] right-3 absolute bottom-3 z-10 text-xl text-white/95"
|
||||
style="text-shadow: 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1)"
|
||||
>
|
||||
12,422
|
||||
<span class="ml-1 text-sm">再生</span>
|
||||
</span>
|
||||
<div class="lg:hidden w-full gradient-blur !absolute !h-32">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent class="max-lg:hidden px-7 py-6 flex flex-col gap-4">
|
||||
<Typography.Display class="leading-[2.75rem]" variant="small">
|
||||
{props.data?.name}
|
||||
</Typography.Display>
|
||||
<div class="grid grid-cols-2 grid-rows-3 gap-2">
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
演唱
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">
|
||||
<a href="#">赤羽</a>
|
||||
</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
时长
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">4:28</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
投稿
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">
|
||||
<a href="#">洛凛</a>
|
||||
</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
链接
|
||||
</Typography.Label>
|
||||
<Typography.Body class="flex gap-2" variant="large">
|
||||
<a href="https://www.bilibili.com/video/BV1eaq9Y3EVV/">哔哩哔哩</a>
|
||||
<a href="https://vocadb.net/S/742394">VocaDB</a>
|
||||
</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
发布时间
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">2024-12-15 12:15:00</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
再生
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">1.24 万 (12,422)</Typography.Body>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div class="mx-1 my-6 lg:hidden">
|
||||
<TabSwitcher />
|
||||
</div>
|
||||
<article class="mt-6">
|
||||
<Typography.Headline class="mx-4" variant="medium">
|
||||
简介
|
||||
</Typography.Headline>
|
||||
<Typography.Body class="mx-4 mt-2" variant="large">
|
||||
<span class="font-medium">《尘海绘仙缘》</span>是<a href="#">洛凛</a>于
|
||||
<span>
|
||||
 2024 年 12 月 15 日
|
||||
</span>
|
||||
投稿至
|
||||
<a href="#">哔哩哔哩</a>的 <a href="#">Synthesizer V</a> 
|
||||
<span>中文</span>
|
||||
<span>原创歌曲</span>, 由<a href="#">赤羽</a>演唱。
|
||||
</Typography.Body>
|
||||
<div class="h-7" />
|
||||
<Typography.Headline class="mx-4" variant="medium">
|
||||
制作人员
|
||||
</Typography.Headline>
|
||||
<div class="mt-3 mx-1">
|
||||
<Staff num={1} name="洛凛" role="策划、作词" />
|
||||
<Staff num={2} name="鱼柳" role="作曲、编曲" />
|
||||
<Staff num={3} name="月华" role="混音" />
|
||||
<Staff num={4} name="城西阿灵" role="视频" />
|
||||
<Staff num={5} name="与嬴酌棠" role="题字" />
|
||||
</div>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
};
|
||||
51
packages/solid/src/components/song/LeftSideBar.tsx
Normal file
51
packages/solid/src/components/song/LeftSideBar.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { Component } from "solid-js";
|
||||
import { A } from "@solidjs/router";
|
||||
import { Button } from "@m3-components/solid";
|
||||
import { HomeIcon, MusicIcon } from "~/components/icons";
|
||||
import { StarBadge4, StarBadge6, StarBadge8 } from "~/components/icons/StarBadges";
|
||||
import { HistoryIcon } from "../icons/History";
|
||||
|
||||
export const LeftSideBar: Component = () => {
|
||||
return (
|
||||
<>
|
||||
<div class="inline-flex flex-col gap-4">
|
||||
<A href="/">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<HomeIcon class="w-5 h-5 text-xl -translate-y-0.25" />
|
||||
<span>主页</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<MusicIcon class="w-5 h-5 text-xl" />
|
||||
<span>歌曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/milestone/denndou/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<StarBadge4 class="w-5 h-5 text-xl" />
|
||||
<span>殿堂曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/milestone/densetsu/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<StarBadge6 class="w-5 h-5 text-xl" />
|
||||
<span>传说曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/milestone/shinwa/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<StarBadge8 class="w-5 h-5 text-xl" />
|
||||
<span>神话曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="../history">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<HistoryIcon class="w-5 h-5 text-xl" />
|
||||
<span>页面历史</span>
|
||||
</Button>
|
||||
</A>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
17
packages/solid/src/components/song/RightSideBar.tsx
Normal file
17
packages/solid/src/components/song/RightSideBar.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { Component } from "solid-js";
|
||||
import { ExtendedFAB } from "@m3-components/solid";
|
||||
import { EditIcon } from "~/components/icons";
|
||||
import { TabSwitcher } from "~/components/song/TabSwitcher";
|
||||
|
||||
export const RightSideBar: Component = () => {
|
||||
return (
|
||||
<>
|
||||
<div class="w-48 self-center 2xl:self-end flex justify-end mb-6">
|
||||
<ExtendedFAB position="unset" size="small" elevation={false} text="编辑" color="primary">
|
||||
<EditIcon />
|
||||
</ExtendedFAB>
|
||||
</div>
|
||||
<TabSwitcher />
|
||||
</>
|
||||
);
|
||||
};
|
||||
37
packages/solid/src/components/song/Staff.tsx
Normal file
37
packages/solid/src/components/song/Staff.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Component } from "solid-js";
|
||||
import { A } from "@solidjs/router";
|
||||
import { IconButton, Typography } from "@m3-components/solid";
|
||||
import { RightArrow } from "~/components/icons/Arrow";
|
||||
|
||||
export const Staff: Component<{ name: string; role: string; num: number }> = (props) => {
|
||||
return (
|
||||
<A
|
||||
href={`/author/${props.name}/info`}
|
||||
class="group rounded-[1.25rem] hover:bg-surface-container h-16 flex items-center
|
||||
px-4 justify-between"
|
||||
>
|
||||
<div class="ml-2 flex gap-5 lg:gap-4 grow w-full">
|
||||
<span
|
||||
class="font-[IPSD] font-medium text-[2rem] text-on-surface-variant"
|
||||
style="
|
||||
-webkit-text-stroke: var(--md-sys-color-on-surface-variant);
|
||||
-webkit-text-stroke-width: 1.2px;
|
||||
-webkit-text-fill-color: transparent;"
|
||||
>
|
||||
{props.num}
|
||||
</span>
|
||||
<div class="flex flex-col gap-[3px]">
|
||||
<Typography.Body variant="large" class="text-on-surface font-medium">
|
||||
{props.name}
|
||||
</Typography.Body>
|
||||
<Typography.Label variant="large" class="text-on-surface-variant">
|
||||
{props.role}
|
||||
</Typography.Label>
|
||||
</div>
|
||||
</div>
|
||||
<IconButton class="text-on-surface-variant opacity-0 group-hover:opacity-80 duration-200">
|
||||
<RightArrow />
|
||||
</IconButton>
|
||||
</A>
|
||||
);
|
||||
};
|
||||
40
packages/solid/src/components/song/TabSwitcher.tsx
Normal file
40
packages/solid/src/components/song/TabSwitcher.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Button } from "@m3-components/solid";
|
||||
import { A } from "@solidjs/router";
|
||||
import { Component, splitProps } from "solid-js";
|
||||
import { ElementProps } from "../common";
|
||||
|
||||
export const TabSwitcher: Component<ElementProps> = (props) => {
|
||||
const [_v, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<nav class="flex flex-col" {...rest}>
|
||||
<div class="w-full lg:w-48 gap-4 flex overflow-auto lg:flex-col items-center lg:self-center 2xl:self-end">
|
||||
<A class="min-w-20 w-full" href="../info">
|
||||
<Button class="w-full" variant="filled">
|
||||
信息
|
||||
</Button>
|
||||
</A>
|
||||
<A class="min-w-20 w-full" href="../lyrics">
|
||||
<Button class="w-full" variant="outlined">
|
||||
歌词
|
||||
</Button>
|
||||
</A>
|
||||
<A class="min-w-20 w-full" href="../analytics">
|
||||
<Button class="w-full" variant="outlined">
|
||||
数据
|
||||
</Button>
|
||||
</A>
|
||||
<A class="min-w-20 w-full" href="../relations">
|
||||
<Button class="w-full" variant="outlined">
|
||||
相关
|
||||
</Button>
|
||||
</A>
|
||||
<A class="min-w-20 w-full" href="../discussion">
|
||||
<Button class="w-full" variant="outlined">
|
||||
讨论
|
||||
</Button>
|
||||
</A>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
22
packages/solid/src/components/utils/DynamicImage.tsx
Normal file
22
packages/solid/src/components/utils/DynamicImage.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { createPrefersDark } from "@solid-primitives/media";
|
||||
import { Component, JSX, Match, splitProps, Switch } from "solid-js";
|
||||
|
||||
interface Props extends JSX.ImgHTMLAttributes<HTMLImageElement> {
|
||||
lightSrc: string;
|
||||
darkSrc: string;
|
||||
}
|
||||
|
||||
export const DynamicImage: Component<Props> = (props) => {
|
||||
const isDark = createPrefersDark();
|
||||
const [v, rest] = splitProps(props, ["lightSrc", "darkSrc", "alt"]);
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={isDark()}>
|
||||
<img src={v.darkSrc} alt={v.alt} {...rest} />
|
||||
</Match>
|
||||
<Match when={!isDark()}>
|
||||
<img src={v.lightSrc} alt={v.alt} {...rest} />
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
};
|
||||
44
packages/solid/src/drizzle/cred/0000_moaning_shotgun.sql
Normal file
44
packages/solid/src/drizzle/cred/0000_moaning_shotgun.sql
Normal file
@ -0,0 +1,44 @@
|
||||
-- Current sql file was generated after introspecting the database
|
||||
-- If you want to run this migration please uncomment this code before executing migrations
|
||||
/*
|
||||
CREATE SEQUENCE "public"."captcha_difficulty_settings_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1;--> statement-breakpoint
|
||||
CREATE SEQUENCE "public"."users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1;--> statement-breakpoint
|
||||
CREATE TABLE "captcha_difficulty_settings" (
|
||||
"id" integer DEFAULT nextval('captcha_difficulty_settings_id_seq'::regclass) NOT NULL,
|
||||
"method" text NOT NULL,
|
||||
"path" text NOT NULL,
|
||||
"duration" real NOT NULL,
|
||||
"threshold" integer NOT NULL,
|
||||
"difficulty" integer NOT NULL,
|
||||
"global" boolean NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "login_sessions" (
|
||||
"id" text NOT NULL,
|
||||
"uid" integer NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"expire_at" timestamp with time zone,
|
||||
"last_used_at" timestamp with time zone,
|
||||
"ip_address" "inet",
|
||||
"user_agent" text,
|
||||
"deactivated_at" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "users" (
|
||||
"id" integer DEFAULT nextval('users_id_seq'::regclass) NOT NULL,
|
||||
"nickname" text,
|
||||
"username" text NOT NULL,
|
||||
"password" text NOT NULL,
|
||||
"unq_id" text DEFAULT gen_random_uuid() NOT NULL,
|
||||
"role" text DEFAULT 'USER' NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "captcha_difficulty_settings_pkey" ON "captcha_difficulty_settings" USING btree ("id" int4_ops);--> statement-breakpoint
|
||||
CREATE INDEX "inx_login-sessions_uid" ON "login_sessions" USING btree ("uid" int4_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "login_sessions_pkey" ON "login_sessions" USING btree ("id" text_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "users_pkey" ON "users" USING btree ("id" int4_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "users_pkey1" ON "users" USING btree ("id" int4_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "users_unq_id_key" ON "users" USING btree ("unq_id" text_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "users_username_key" ON "users" USING btree ("username" text_ops);
|
||||
*/
|
||||
350
packages/solid/src/drizzle/cred/meta/0000_snapshot.json
Normal file
350
packages/solid/src/drizzle/cred/meta/0000_snapshot.json
Normal file
@ -0,0 +1,350 @@
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"prevId": "",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.captcha_difficulty_settings": {
|
||||
"name": "captcha_difficulty_settings",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "nextval('captcha_difficulty_settings_id_seq'::regclass)"
|
||||
},
|
||||
"method": {
|
||||
"name": "method",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"path": {
|
||||
"name": "path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"threshold": {
|
||||
"name": "threshold",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"difficulty": {
|
||||
"name": "difficulty",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"global": {
|
||||
"name": "global",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"captcha_difficulty_settings_pkey": {
|
||||
"name": "captcha_difficulty_settings_pkey",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "id",
|
||||
"asc": true,
|
||||
"nulls": "last",
|
||||
"opclass": "int4_ops",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {},
|
||||
"policies": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.login_sessions": {
|
||||
"name": "login_sessions",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"uid": {
|
||||
"name": "uid",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
},
|
||||
"expire_at": {
|
||||
"name": "expire_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_used_at": {
|
||||
"name": "last_used_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "inet",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"deactivated_at": {
|
||||
"name": "deactivated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"inx_login-sessions_uid": {
|
||||
"name": "inx_login-sessions_uid",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "uid",
|
||||
"asc": true,
|
||||
"nulls": "last",
|
||||
"opclass": "int4_ops",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"login_sessions_pkey": {
|
||||
"name": "login_sessions_pkey",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "id",
|
||||
"asc": true,
|
||||
"nulls": "last",
|
||||
"opclass": "text_ops",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {},
|
||||
"policies": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "nextval('users_id_seq'::regclass)"
|
||||
},
|
||||
"nickname": {
|
||||
"name": "nickname",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"unq_id": {
|
||||
"name": "unq_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'USER'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_pkey": {
|
||||
"name": "users_pkey",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "id",
|
||||
"asc": true,
|
||||
"nulls": "last",
|
||||
"opclass": "int4_ops",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"users_pkey1": {
|
||||
"name": "users_pkey1",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "id",
|
||||
"asc": true,
|
||||
"nulls": "last",
|
||||
"opclass": "int4_ops",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"users_unq_id_key": {
|
||||
"name": "users_unq_id_key",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "unq_id",
|
||||
"asc": true,
|
||||
"nulls": "last",
|
||||
"opclass": "text_ops",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"users_username_key": {
|
||||
"name": "users_username_key",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "username",
|
||||
"asc": true,
|
||||
"nulls": "last",
|
||||
"opclass": "text_ops",
|
||||
"isExpression": false
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {},
|
||||
"policies": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {
|
||||
"public.captcha_difficulty_settings_id_seq": {
|
||||
"name": "captcha_difficulty_settings_id_seq",
|
||||
"schema": "public",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"increment": "1",
|
||||
"cycle": false,
|
||||
"cache": "1"
|
||||
},
|
||||
"public.users_id_seq": {
|
||||
"name": "users_id_seq",
|
||||
"schema": "public",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"increment": "1",
|
||||
"cycle": false,
|
||||
"cache": "1"
|
||||
}
|
||||
},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {
|
||||
"captcha_difficulty_settings": {
|
||||
"columns": {
|
||||
"id": {
|
||||
"isDefaultAnExpression": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
"columns": {
|
||||
"id": {
|
||||
"isDefaultAnExpression": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
packages/solid/src/drizzle/cred/meta/_journal.json
Normal file
13
packages/solid/src/drizzle/cred/meta/_journal.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1750513073792,
|
||||
"tag": "0000_moaning_shotgun",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
2
packages/solid/src/drizzle/cred/relations.ts
Normal file
2
packages/solid/src/drizzle/cred/relations.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import { relations } from "drizzle-orm/relations";
|
||||
import {} from "./schema";
|
||||
93
packages/solid/src/drizzle/cred/schema.ts
Normal file
93
packages/solid/src/drizzle/cred/schema.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import {
|
||||
pgTable,
|
||||
uniqueIndex,
|
||||
integer,
|
||||
text,
|
||||
real,
|
||||
boolean,
|
||||
index,
|
||||
timestamp,
|
||||
inet,
|
||||
pgSequence
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
export const captchaDifficultySettingsIdSeq = pgSequence("captcha_difficulty_settings_id_seq", {
|
||||
startWith: "1",
|
||||
increment: "1",
|
||||
minValue: "1",
|
||||
maxValue: "2147483647",
|
||||
cache: "1",
|
||||
cycle: false
|
||||
});
|
||||
export const usersIdSeq = pgSequence("users_id_seq", {
|
||||
startWith: "1",
|
||||
increment: "1",
|
||||
minValue: "1",
|
||||
maxValue: "2147483647",
|
||||
cache: "1",
|
||||
cycle: false
|
||||
});
|
||||
|
||||
export const captchaDifficultySettings = pgTable(
|
||||
"captcha_difficulty_settings",
|
||||
{
|
||||
id: integer()
|
||||
.default(sql`nextval('captcha_difficulty_settings_id_seq'::regclass)`)
|
||||
.notNull(),
|
||||
method: text().notNull(),
|
||||
path: text().notNull(),
|
||||
duration: real().notNull(),
|
||||
threshold: integer().notNull(),
|
||||
difficulty: integer().notNull(),
|
||||
global: boolean().notNull()
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("captcha_difficulty_settings_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const loginSessions = pgTable(
|
||||
"login_sessions",
|
||||
{
|
||||
id: text().notNull(),
|
||||
uid: integer().notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
expireAt: timestamp("expire_at", { withTimezone: true, mode: "string" }),
|
||||
lastUsedAt: timestamp("last_used_at", { withTimezone: true, mode: "string" }),
|
||||
ipAddress: inet("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
deactivatedAt: timestamp("deactivated_at", { withTimezone: true, mode: "string" })
|
||||
},
|
||||
(table) => [
|
||||
index("inx_login-sessions_uid").using("btree", table.uid.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("login_sessions_pkey").using("btree", table.id.asc().nullsLast().op("text_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const users = pgTable(
|
||||
"users",
|
||||
{
|
||||
id: integer()
|
||||
.default(sql`nextval('users_id_seq'::regclass)`)
|
||||
.notNull(),
|
||||
nickname: text(),
|
||||
username: text().notNull(),
|
||||
password: text().notNull(),
|
||||
unqId: text("unq_id")
|
||||
.default(sql`gen_random_uuid()`)
|
||||
.notNull(),
|
||||
role: text().default("USER").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull()
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("users_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("users_pkey1").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("users_unq_id_key").using("btree", table.unqId.asc().nullsLast().op("text_ops")),
|
||||
uniqueIndex("users_username_key").using("btree", table.username.asc().nullsLast().op("text_ops"))
|
||||
]
|
||||
);
|
||||
7
packages/solid/src/drizzle/index.ts
Normal file
7
packages/solid/src/drizzle/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { drizzle } from "drizzle-orm/postgres-js";
|
||||
import { sqlCred, sql } from "@cvsa/core";
|
||||
|
||||
export const dbMain = drizzle(sql);
|
||||
export const dbCred = drizzle(sqlCred);
|
||||
157
packages/solid/src/drizzle/main/0000_fresh_mac_gargan.sql
Normal file
157
packages/solid/src/drizzle/main/0000_fresh_mac_gargan.sql
Normal file
@ -0,0 +1,157 @@
|
||||
-- Current sql file was generated after introspecting the database
|
||||
-- If you want to run this migration please uncomment this code before executing migrations
|
||||
/*
|
||||
CREATE SEQUENCE "public"."all_data_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1;--> statement-breakpoint
|
||||
CREATE SEQUENCE "public"."labeling_result_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1;--> statement-breakpoint
|
||||
CREATE SEQUENCE "public"."songs_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1;--> statement-breakpoint
|
||||
CREATE SEQUENCE "public"."video_snapshot_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1;--> statement-breakpoint
|
||||
CREATE SEQUENCE "public"."views_increment_rate_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1;--> statement-breakpoint
|
||||
CREATE TABLE "content" (
|
||||
"page_id" text PRIMARY KEY NOT NULL,
|
||||
"page_content" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updated_at" timestamp with time zone,
|
||||
"deleted_at" timestamp with time zone
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "bilibili_user" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"uid" bigint NOT NULL,
|
||||
"username" text NOT NULL,
|
||||
"desc" text NOT NULL,
|
||||
"fans" integer NOT NULL,
|
||||
CONSTRAINT "unq_bili-user_uid" UNIQUE("uid")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "bilibili_metadata" (
|
||||
"id" integer DEFAULT nextval('all_data_id_seq'::regclass) NOT NULL,
|
||||
"aid" bigint NOT NULL,
|
||||
"bvid" varchar(12),
|
||||
"description" text,
|
||||
"uid" bigint,
|
||||
"tags" text,
|
||||
"title" text,
|
||||
"published_at" timestamp with time zone,
|
||||
"duration" integer,
|
||||
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
|
||||
"status" integer DEFAULT 0 NOT NULL,
|
||||
"cover_url" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "classified_labels_human" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"aid" bigint NOT NULL,
|
||||
"author" uuid NOT NULL,
|
||||
"label" smallint NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "labelling_result" (
|
||||
"id" integer DEFAULT nextval('labeling_result_id_seq'::regclass) NOT NULL,
|
||||
"aid" bigint NOT NULL,
|
||||
"label" smallint NOT NULL,
|
||||
"model_version" text NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"logits" smallint[]
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "latest_video_snapshot" (
|
||||
"aid" bigint PRIMARY KEY NOT NULL,
|
||||
"time" timestamp with time zone NOT NULL,
|
||||
"views" integer NOT NULL,
|
||||
"coins" integer NOT NULL,
|
||||
"likes" integer NOT NULL,
|
||||
"favorites" integer NOT NULL,
|
||||
"replies" integer NOT NULL,
|
||||
"danmakus" integer NOT NULL,
|
||||
"shares" integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "video_snapshot" (
|
||||
"id" integer DEFAULT nextval('video_snapshot_id_seq'::regclass) NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"views" integer NOT NULL,
|
||||
"coins" integer NOT NULL,
|
||||
"likes" integer NOT NULL,
|
||||
"favorites" integer NOT NULL,
|
||||
"shares" integer NOT NULL,
|
||||
"danmakus" integer NOT NULL,
|
||||
"aid" bigint NOT NULL,
|
||||
"replies" integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "snapshot_schedule" (
|
||||
"id" bigserial NOT NULL,
|
||||
"aid" bigint NOT NULL,
|
||||
"type" text,
|
||||
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"started_at" timestamp with time zone,
|
||||
"finished_at" timestamp with time zone,
|
||||
"status" text DEFAULT 'pending' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "songs" (
|
||||
"id" integer DEFAULT nextval('songs_id_seq'::regclass) NOT NULL,
|
||||
"name" text,
|
||||
"aid" bigint,
|
||||
"published_at" timestamp with time zone,
|
||||
"duration" integer,
|
||||
"type" smallint,
|
||||
"romanized_name" text,
|
||||
"netease_id" bigint,
|
||||
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"deleted" boolean DEFAULT false NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "views_increment_rate" (
|
||||
"id" integer DEFAULT nextval('views_increment_rate_id_seq'::regclass) NOT NULL,
|
||||
"aid" bigint NOT NULL,
|
||||
"old_time" timestamp with time zone NOT NULL,
|
||||
"new_time" timestamp with time zone NOT NULL,
|
||||
"old_views" integer NOT NULL,
|
||||
"new_views" integer NOT NULL,
|
||||
"interval" interval NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"speed" real
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX "idx_content_created-at" ON "content" USING btree ("created_at" timestamptz_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_bili-user_uid" ON "bilibili_user" USING btree ("uid" int8_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "all_data_pkey" ON "bilibili_metadata" USING btree ("id" int4_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_all-data_aid" ON "bilibili_metadata" USING btree ("aid" int8_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_all-data_bvid" ON "bilibili_metadata" USING btree ("bvid" text_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_all-data_uid" ON "bilibili_metadata" USING btree ("uid" int8_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_bili-meta_status" ON "bilibili_metadata" USING btree ("status" int4_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "unq_all-data_aid" ON "bilibili_metadata" USING btree ("aid" int8_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_classified-labels-human_aid" ON "classified_labels_human" USING btree ("aid" int8_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_classified-labels-human_author" ON "classified_labels_human" USING btree ("author" uuid_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_classified-labels-human_created-at" ON "classified_labels_human" USING btree ("created_at" timestamptz_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_classified-labels-human_label" ON "classified_labels_human" USING btree ("label" int2_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_labeling_label_model-version" ON "labelling_result" USING btree ("label" int2_ops,"model_version" int2_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_labeling_model-version" ON "labelling_result" USING btree ("model_version" text_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_labelling_aid-label" ON "labelling_result" USING btree ("aid" int2_ops,"label" int2_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "labeling_result_pkey" ON "labelling_result" USING btree ("id" int4_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "unq_labelling-result_aid_model-version" ON "labelling_result" USING btree ("aid" int8_ops,"model_version" int8_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_latest-video-snapshot_time" ON "latest_video_snapshot" USING btree ("time" timestamptz_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_latest-video-snapshot_views" ON "latest_video_snapshot" USING btree ("views" int4_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_vid_snapshot_aid" ON "video_snapshot" USING btree ("aid" int8_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_vid_snapshot_time" ON "video_snapshot" USING btree ("created_at" timestamptz_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_vid_snapshot_views" ON "video_snapshot" USING btree ("views" int4_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "video_snapshot_pkey" ON "video_snapshot" USING btree ("id" int4_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_snapshot_schedule_aid" ON "snapshot_schedule" USING btree ("aid" int8_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_snapshot_schedule_started_at" ON "snapshot_schedule" USING btree ("started_at" timestamptz_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_snapshot_schedule_status" ON "snapshot_schedule" USING btree ("status" text_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_snapshot_schedule_type" ON "snapshot_schedule" USING btree ("type" text_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "snapshot_schedule_pkey" ON "snapshot_schedule" USING btree ("id" int8_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_aid" ON "songs" USING btree ("aid" int8_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_hash_songs_aid" ON "songs" USING hash ("aid" int8_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_netease_id" ON "songs" USING btree ("netease_id" int8_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_published_at" ON "songs" USING btree ("published_at" timestamptz_ops);--> statement-breakpoint
|
||||
CREATE INDEX "idx_type" ON "songs" USING btree ("type" int2_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "songs_pkey" ON "songs" USING btree ("id" int4_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "unq_songs_aid" ON "songs" USING btree ("aid" int8_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "unq_songs_netease_id" ON "songs" USING btree ("netease_id" int8_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "unq_views-increment-rate_aid_interval" ON "views_increment_rate" USING btree ("aid" int8_ops,"interval" int8_ops);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "views_increment_rate_pkey" ON "views_increment_rate" USING btree ("id" int4_ops);
|
||||
*/
|
||||
1387
packages/solid/src/drizzle/main/meta/0000_snapshot.json
Normal file
1387
packages/solid/src/drizzle/main/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
13
packages/solid/src/drizzle/main/meta/_journal.json
Normal file
13
packages/solid/src/drizzle/main/meta/_journal.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1750513105905,
|
||||
"tag": "0000_fresh_mac_gargan",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
3
packages/solid/src/drizzle/main/relations.ts
Normal file
3
packages/solid/src/drizzle/main/relations.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { relations } from "drizzle-orm/relations";
|
||||
import { } from "./schema";
|
||||
|
||||
174
packages/solid/src/drizzle/main/schema.ts
Normal file
174
packages/solid/src/drizzle/main/schema.ts
Normal file
@ -0,0 +1,174 @@
|
||||
import { pgTable, uniqueIndex, index, integer, bigint, varchar, text, timestamp, unique, serial, smallint, boolean, bigserial, uuid, pgSequence } from "drizzle-orm/pg-core"
|
||||
import { sql } from "drizzle-orm"
|
||||
|
||||
|
||||
export const allDataIdSeq = pgSequence("all_data_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
|
||||
export const labelingResultIdSeq = pgSequence("labeling_result_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
|
||||
export const songsIdSeq = pgSequence("songs_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
|
||||
export const videoSnapshotIdSeq = pgSequence("video_snapshot_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
|
||||
export const viewsIncrementRateIdSeq = pgSequence("views_increment_rate_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "9223372036854775807", cache: "1", cycle: false })
|
||||
|
||||
export const bilibiliMetadata = pgTable("bilibili_metadata", {
|
||||
id: integer().default(sql`nextval('all_data_id_seq'::regclass)`).notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
bvid: varchar({ length: 12 }),
|
||||
description: text(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
uid: bigint({ mode: "number" }),
|
||||
tags: text(),
|
||||
title: text(),
|
||||
publishedAt: timestamp("published_at", { withTimezone: true, mode: 'string' }),
|
||||
duration: integer(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`),
|
||||
status: integer().default(0).notNull(),
|
||||
coverUrl: text("cover_url"),
|
||||
}, (table) => [
|
||||
uniqueIndex("all_data_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
index("idx_all-data_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_all-data_bvid").using("btree", table.bvid.asc().nullsLast().op("text_ops")),
|
||||
index("idx_all-data_uid").using("btree", table.uid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_bili-meta_status").using("btree", table.status.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("unq_all-data_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
]);
|
||||
|
||||
export const bilibiliUser = pgTable("bilibili_user", {
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
uid: bigint({ mode: "number" }).notNull(),
|
||||
username: text().notNull(),
|
||||
desc: text().notNull(),
|
||||
fans: integer().notNull(),
|
||||
}, (table) => [
|
||||
index("idx_bili-user_uid").using("btree", table.uid.asc().nullsLast().op("int8_ops")),
|
||||
unique("unq_bili-user_uid").on(table.uid),
|
||||
]);
|
||||
|
||||
export const labellingResult = pgTable("labelling_result", {
|
||||
id: integer().default(sql`nextval('labeling_result_id_seq'::regclass)`).notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
label: smallint().notNull(),
|
||||
modelVersion: text("model_version").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
logits: smallint().array(),
|
||||
}, (table) => [
|
||||
index("idx_labeling_label_model-version").using("btree", table.label.asc().nullsLast().op("int2_ops"), table.modelVersion.asc().nullsLast().op("int2_ops")),
|
||||
index("idx_labeling_model-version").using("btree", table.modelVersion.asc().nullsLast().op("text_ops")),
|
||||
index("idx_labelling_aid-label").using("btree", table.aid.asc().nullsLast().op("int2_ops"), table.label.asc().nullsLast().op("int2_ops")),
|
||||
uniqueIndex("labeling_result_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("unq_labelling-result_aid_model-version").using("btree", table.aid.asc().nullsLast().op("int8_ops"), table.modelVersion.asc().nullsLast().op("int8_ops")),
|
||||
]);
|
||||
|
||||
export const latestVideoSnapshot = pgTable("latest_video_snapshot", {
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).primaryKey().notNull(),
|
||||
time: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||
views: integer().notNull(),
|
||||
coins: integer().notNull(),
|
||||
likes: integer().notNull(),
|
||||
favorites: integer().notNull(),
|
||||
replies: integer().notNull(),
|
||||
danmakus: integer().notNull(),
|
||||
shares: integer().notNull(),
|
||||
}, (table) => [
|
||||
index("idx_latest-video-snapshot_time").using("btree", table.time.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_latest-video-snapshot_views").using("btree", table.views.asc().nullsLast().op("int4_ops")),
|
||||
]);
|
||||
|
||||
export const videoSnapshot = pgTable("video_snapshot", {
|
||||
id: integer().default(sql`nextval('video_snapshot_id_seq'::regclass)`).notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
views: integer().notNull(),
|
||||
coins: integer().notNull(),
|
||||
likes: integer().notNull(),
|
||||
favorites: integer().notNull(),
|
||||
shares: integer().notNull(),
|
||||
danmakus: integer().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
replies: integer().notNull(),
|
||||
}, (table) => [
|
||||
index("idx_vid_snapshot_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_vid_snapshot_aid_created_at").using("btree", table.aid.asc().nullsLast().op("timestamptz_ops"), table.createdAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_vid_snapshot_time").using("btree", table.createdAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_vid_snapshot_views").using("btree", table.views.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("video_snapshot_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
]);
|
||||
|
||||
export const songs = pgTable("songs", {
|
||||
id: integer().default(sql`nextval('songs_id_seq'::regclass)`).notNull(),
|
||||
name: text(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }),
|
||||
publishedAt: timestamp("published_at", { withTimezone: true, mode: 'string' }),
|
||||
duration: integer(),
|
||||
type: smallint(),
|
||||
romanizedName: text("romanized_name"),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
neteaseId: bigint("netease_id", { mode: "number" }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
deleted: boolean().default(false).notNull(),
|
||||
image: text(),
|
||||
}, (table) => [
|
||||
index("idx_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_hash_songs_aid").using("hash", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_netease_id").using("btree", table.neteaseId.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_published_at").using("btree", table.publishedAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_type").using("btree", table.type.asc().nullsLast().op("int2_ops")),
|
||||
uniqueIndex("songs_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("unq_songs_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
uniqueIndex("unq_songs_netease_id").using("btree", table.neteaseId.asc().nullsLast().op("int8_ops")),
|
||||
]);
|
||||
|
||||
export const singer = pgTable("singer", {
|
||||
id: serial().primaryKey().notNull(),
|
||||
name: text().notNull(),
|
||||
});
|
||||
|
||||
export const relations = pgTable("relations", {
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
sourceId: bigint("source_id", { mode: "number" }).notNull(),
|
||||
sourceType: text("source_type").notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
targetId: bigint("target_id", { mode: "number" }).notNull(),
|
||||
targetType: text("target_type").notNull(),
|
||||
relation: text().notNull(),
|
||||
}, (table) => [
|
||||
index("idx_relations_source_id_source_type_relation").using("btree", table.sourceId.asc().nullsLast().op("int8_ops"), table.sourceType.asc().nullsLast().op("int8_ops"), table.relation.asc().nullsLast().op("text_ops")),
|
||||
index("idx_relations_target_id_target_type_relation").using("btree", table.targetId.asc().nullsLast().op("text_ops"), table.targetType.asc().nullsLast().op("text_ops"), table.relation.asc().nullsLast().op("text_ops")),
|
||||
unique("unq_relations").on(table.sourceId, table.sourceType, table.targetId, table.targetType, table.relation),
|
||||
]);
|
||||
|
||||
export const snapshotSchedule = pgTable("snapshot_schedule", {
|
||||
id: bigserial({ mode: "bigint" }).notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
type: text(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
startedAt: timestamp("started_at", { withTimezone: true, mode: 'string' }),
|
||||
finishedAt: timestamp("finished_at", { withTimezone: true, mode: 'string' }),
|
||||
status: text().default('pending').notNull(),
|
||||
}, (table) => [
|
||||
index("idx_snapshot_schedule_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_snapshot_schedule_started_at").using("btree", table.startedAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_snapshot_schedule_status").using("btree", table.status.asc().nullsLast().op("text_ops")),
|
||||
index("idx_snapshot_schedule_type").using("btree", table.type.asc().nullsLast().op("text_ops")),
|
||||
uniqueIndex("snapshot_schedule_pkey").using("btree", table.id.asc().nullsLast().op("int8_ops")),
|
||||
]);
|
||||
|
||||
export const classifiedLabelsHuman = pgTable("classified_labels_human", {
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
author: uuid().notNull(),
|
||||
label: smallint().notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
}, (table) => [
|
||||
index("idx_classified-labels-human_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_classified-labels-human_author").using("btree", table.author.asc().nullsLast().op("uuid_ops")),
|
||||
index("idx_classified-labels-human_created-at").using("btree", table.createdAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_classified-labels-human_label").using("btree", table.label.asc().nullsLast().op("int2_ops")),
|
||||
]);
|
||||
10
packages/solid/src/drizzle/outerSchema.d.ts
vendored
Normal file
10
packages/solid/src/drizzle/outerSchema.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { users } from "~db/cred/schema";
|
||||
import { bilibiliMetadata, latestVideoSnapshot, songs, videoSnapshot } from "~db/main/schema";
|
||||
|
||||
export type UserType = InferSelectModel<typeof users>;
|
||||
export type SensitiveUserFields = "password" | "unqId";
|
||||
export type BilibiliMetadataType = InferSelectModel<typeof bilibiliMetadata>;
|
||||
export type VideoSnapshotType = InferSelectModel<typeof videoSnapshot>;
|
||||
export type LatestVideoSnapshotType = InferSelectModel<typeof latestVideoSnapshot>;
|
||||
export type SongType = InferSelectModel<typeof songs>;
|
||||
15
packages/solid/src/entry-client.tsx
Normal file
15
packages/solid/src/entry-client.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
// @refresh reload
|
||||
import { mount, StartClient } from "@solidjs/start/client";
|
||||
import { RequestContextProvider } from "./components/requestContext";
|
||||
import { MetaProvider } from "@solidjs/meta";
|
||||
|
||||
mount(
|
||||
() => (
|
||||
<RequestContextProvider>
|
||||
<MetaProvider>
|
||||
<StartClient />
|
||||
</MetaProvider>
|
||||
</RequestContextProvider>
|
||||
),
|
||||
document.getElementById("app")!
|
||||
);
|
||||
29
packages/solid/src/entry-server.tsx
Normal file
29
packages/solid/src/entry-server.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
// @refresh reload
|
||||
import { createHandler, StartServer } from "@solidjs/start/server";
|
||||
import { MetaProvider } from "@solidjs/meta";
|
||||
import { RequestContextProvider } from "~/components/requestContext";
|
||||
|
||||
export default createHandler(() => (
|
||||
<RequestContextProvider>
|
||||
<MetaProvider>
|
||||
<StartServer
|
||||
document={({ assets, children, scripts }) => (
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
{assets}
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
{children}
|
||||
</div>
|
||||
<div id="modal"></div>
|
||||
{scripts}
|
||||
</body>
|
||||
</html>
|
||||
)}
|
||||
/>
|
||||
</MetaProvider>
|
||||
</RequestContextProvider>
|
||||
));
|
||||
1
packages/solid/src/global.d.ts
vendored
Normal file
1
packages/solid/src/global.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="@solidjs/start/env" />
|
||||
25
packages/solid/src/lib/const.ts
Normal file
25
packages/solid/src/lib/const.ts
Normal file
@ -0,0 +1,25 @@
|
||||
const N_1024 = BigInt(
|
||||
"129023318876534346704360951712586568674758913224876821534686030409476129469193481910786173836188085930974906857867802234113909470848523288588793477904039083513378341278558405407018889387577114155572311708428733260891448259786041525189132461448841652472631435226032063278124857443496954605482776113964107326943"
|
||||
);
|
||||
|
||||
const N_2048 = BigInt(
|
||||
"23987552118069940970878653610463005981599204778388399885550631951871084945075866571231062435627294546200946516668493107358732376187241747090707087544153108117326163500579370560400058549184722138636116585329496684877258304519458316233517215780035360354808658620079068489084797380781488445517430961701007542207001544091884001098497324624368085682074645221148086075871342544591022944384890014176612259729018968864426602901247715051556212559854689574013699665035317257438297910516976812428036717668766321871780963854649899276251822244719887233041422346429752896925499321431273560130952088238625622570366815755926694833109"
|
||||
);
|
||||
|
||||
const N_1792 = BigInt(
|
||||
"23987552118069940970878653610463005981599204778388399885550631951871084945075866571231062435627294546200946516668493107358732376187241747090707087544153108117326163500579370560400058549184722138636116585329496684877258304519458316233517215780035360354808658620079068489084797380781488445517430961701007542207001544091884001098497324624368085682074645221148086075871342544591022944384890014176612259729018968864426602901247715051556212559854689574013699665035317257438297910516976812428036717668766321871780963854649899276251822244719887233041422346429752896925499321431273560130952088238625622570366815755926694833109"
|
||||
);
|
||||
|
||||
const N_1536 = BigInt(
|
||||
"1694330250214463438908848400950857073137355630337290254958754184668036770489801447652464038218330711288158361242955860326168191830448553710492926795708495297280933502917598985378231124113971732841791156356676046934277122699383776036675381503510992810963611269045078440132744168908318454891211962146563551929591147663448816841024591820348784855441153716551049843185172472891407933214238000452095646085222944171689449292644270516031799660928056315886939284985905227"
|
||||
);
|
||||
|
||||
const N_3072 = BigInt(
|
||||
"4432919939296042464443862503456460073874727648022810391370558006281079088795179408238989283371442564716849343712703672836423961818025813387453469700639513190304802553045342607888612037304066433501317127429264242784608682213025490491212489901736408833027611579294436675682774458141490718959615677971745638214649336218217578937534746160749039668886450447773018369168258067682196337978245372237157696236362344796867228581553446331915147012787367438751646936429739232247148712001806846526947508445039707404287951727838234648917450736371192435665040644040487427986702098273581288935278964444790007953559851323281510927332862225214878776790605026472021669614552481167977412450477230442015077669503312683966631454347169703030544483487968842349634064181183599641180349414682042575010303056241481622837185325228233789954078775053744988023738762706404546546146837242590884760044438874357295029411988267287001033032827035809135092270843"
|
||||
);
|
||||
|
||||
const N_4096 = BigInt(
|
||||
"703671044356805218391078271512201582198770553281951369783674142891088501340774249238173262580562112786670043634665390581120113644316651934154746357220932310140476300088580654571796404198410555061275065442553506658401183560336140989074165998202690496991174269748740565700402715364422506782445179963440819952745241176450402011121226863984008975377353558155910994380700267903933205531681076494639818328879475919332604951949178075254600102192323286738973253864238076198710173840170988339024438220034106150475640983877458155141500313471699516670799821379238743709125064098477109094533426340852518505385314780319279862586851512004686798362431227795743253799490998475141728082088984359237540124375439664236138519644100625154580910233437864328111620708697941949936338367445851449766581651338876219676721272448769082914348242483068204896479076062102236087066428603930888978596966798402915747531679758905013008059396214343112694563043918465373870648649652122703709658068801764236979191262744515840224548957285182453209028157886219424802426566456408109642062498413592155064289314088837031184200671561102160059065729282902863248815224399131391716503171191977463328439766546574118092303414702384104112719959325482439604572518549918705623086363111"
|
||||
);
|
||||
|
||||
export const N_ARRAY = [N_1024, N_1536, N_1792, N_2048, N_3072, N_4096];
|
||||
32
packages/solid/src/lib/dbCache.ts
Normal file
32
packages/solid/src/lib/dbCache.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Context } from "~/components/requestContext";
|
||||
|
||||
export async function useCachedFetch<T>(
|
||||
fetcher: () => Promise<T>,
|
||||
identifier: string,
|
||||
context: Context,
|
||||
deps: any[]
|
||||
): Promise<T> {
|
||||
const [contextSignal, updateContext] = context;
|
||||
const hooks = contextSignal();
|
||||
let hook = hooks.get(identifier);
|
||||
|
||||
if (hook && hook.promise) {
|
||||
return hook.promise;
|
||||
}
|
||||
|
||||
hook = {
|
||||
memoizedValue: null,
|
||||
deps: deps,
|
||||
promise: null
|
||||
};
|
||||
const promise = fetcher().then((result) => {
|
||||
hook!.memoizedValue = result;
|
||||
hooks.set(identifier, hook!);
|
||||
updateContext(hooks);
|
||||
return result;
|
||||
});
|
||||
hook.promise = promise;
|
||||
hooks.set(identifier, hook!);
|
||||
updateContext(hooks);
|
||||
return promise;
|
||||
}
|
||||
70
packages/solid/src/lib/net.ts
Normal file
70
packages/solid/src/lib/net.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import axios, { AxiosRequestConfig, AxiosError, Method, AxiosResponse } from "axios";
|
||||
|
||||
export class ApiRequestError extends Error {
|
||||
public code: number | undefined;
|
||||
public response: unknown | undefined;
|
||||
constructor(message: string, res?: unknown, code?: number) {
|
||||
super(message);
|
||||
this.name = "ApiRequestError";
|
||||
this.code = code;
|
||||
this.response = res;
|
||||
}
|
||||
}
|
||||
|
||||
type HttpMethod = Extract<Method, "GET" | "POST" | "PUT" | "DELETE" | "PATCH">;
|
||||
|
||||
const httpMethods = {
|
||||
get: axios.get,
|
||||
post: axios.post,
|
||||
put: axios.put,
|
||||
delete: axios.delete,
|
||||
patch: axios.patch
|
||||
};
|
||||
|
||||
export function fetcher(url: string): Promise<unknown>;
|
||||
export function fetcher<JSON = unknown>(
|
||||
url: string,
|
||||
init?: Omit<AxiosRequestConfig, "method"> & { method?: Exclude<HttpMethod, "DELETE"> }
|
||||
): Promise<JSON>;
|
||||
export function fetcher(
|
||||
url: string,
|
||||
init: Omit<AxiosRequestConfig, "method"> & { method: "DELETE" }
|
||||
): Promise<AxiosResponse>;
|
||||
|
||||
export async function fetcher<JSON = unknown>(
|
||||
url: string,
|
||||
init?: Omit<AxiosRequestConfig, "method"> & { method?: HttpMethod }
|
||||
): Promise<JSON | AxiosResponse<any, any>> {
|
||||
const { method = "get", data, ...config } = init || {};
|
||||
|
||||
const fullConfig: AxiosRequestConfig = {
|
||||
method,
|
||||
...config,
|
||||
timeout: 10000
|
||||
};
|
||||
|
||||
try {
|
||||
const m = method.toLowerCase() as keyof typeof httpMethods;
|
||||
if (["post", "patch", "put"].includes(m)) {
|
||||
const response = await httpMethods[m](url, data, fullConfig);
|
||||
return response.data;
|
||||
} else if (m === "delete") {
|
||||
const response = await axios.delete(url, fullConfig);
|
||||
return response;
|
||||
} else {
|
||||
const response = await httpMethods[m](url, fullConfig);
|
||||
return response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
if (axiosError.response) {
|
||||
const { status, data } = axiosError.response;
|
||||
throw new ApiRequestError(`HTTP error! status: ${status}`, data, status);
|
||||
} else if (axiosError.request) {
|
||||
throw new ApiRequestError("No response received", undefined, -1);
|
||||
} else {
|
||||
throw new ApiRequestError(axiosError.message || "Unknown error");
|
||||
}
|
||||
}
|
||||
}
|
||||
117
packages/solid/src/lib/vdf.ts
Normal file
117
packages/solid/src/lib/vdf.ts
Normal file
@ -0,0 +1,117 @@
|
||||
// Define interfaces for input and output
|
||||
interface VdfProgressCallback {
|
||||
(progress: number): void;
|
||||
}
|
||||
|
||||
interface VdfResult {
|
||||
result: bigint;
|
||||
time: number; // Time taken in milliseconds
|
||||
}
|
||||
|
||||
// The content of the Web Worker script
|
||||
const workerContent = `addEventListener("message", async (event) => {
|
||||
const { g, N, difficulty } = event.data;
|
||||
|
||||
// Although pow is not used in the iterative VDF, it's good to keep the original worker code structure.
|
||||
// The iterative computeVDFWithProgress is better for progress reporting.
|
||||
function pow(base, exponent, mod) {
|
||||
let result = 1n;
|
||||
base = base % mod;
|
||||
while (exponent > 0n) {
|
||||
if (exponent % 2n === 1n) {
|
||||
result = (result * base) % mod;
|
||||
}
|
||||
base = (base * base) % mod;
|
||||
exponent = exponent / 2n;
|
||||
// Using BigInt division (/) which performs integer division
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Compute VDF iteratively to report progress
|
||||
function computeVDFWithProgress(g, N, T, postProgress) {
|
||||
let result = g;
|
||||
let latestTime = performance.now();
|
||||
const totalSteps = T; // T is the difficulty, representing 2^T squaring steps
|
||||
|
||||
for (let i = 0n; i < totalSteps; i++) {
|
||||
result = (result * result) % N;
|
||||
// Report progress periodically (approx. every 16ms to match typical frame rate)
|
||||
if (performance.now() - latestTime > 16) {
|
||||
// Calculate progress as a percentage
|
||||
const progress = Number((i + 1n) * 10000n / totalSteps) / 100; // Using 10000 for better precision before dividing by 100
|
||||
postProgress(progress);
|
||||
latestTime = performance.now();
|
||||
}
|
||||
}
|
||||
// Ensure final progress is reported
|
||||
postProgress(100);
|
||||
return result;
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
// The worker computes g^(2^difficulty) mod N. The loop runs 'difficulty' times, performing squaring.
|
||||
const result = computeVDFWithProgress(g, N, difficulty, (progress) => {
|
||||
// Post progress back to the main thread
|
||||
postMessage({ type: "progress", progress: progress });
|
||||
});
|
||||
const endTime = performance.now();
|
||||
const timeTaken = endTime - startTime;
|
||||
|
||||
// Post the final result and time taken back to the main thread
|
||||
postMessage({ type: "result", result: result.toString(), time: timeTaken });
|
||||
});
|
||||
`;
|
||||
|
||||
/**
|
||||
* Computes the Verifiable Delay Function (VDF) result g^(2^difficulty) mod N
|
||||
* in a Web Worker and reports progress.
|
||||
* @param g - The base (bigint).
|
||||
* @param N - The modulus (bigint).
|
||||
* @param difficulty - The number of squaring steps (T) (bigint).
|
||||
* @param onProgress - Optional callback function to receive progress updates (0-100).
|
||||
* @returns A Promise that resolves with the VDF result and time taken.
|
||||
*/
|
||||
export function computeVdfInWorker(
|
||||
g: bigint,
|
||||
N: bigint,
|
||||
difficulty: bigint,
|
||||
onProgress?: VdfProgressCallback
|
||||
): Promise<VdfResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Create a Blob containing the worker script
|
||||
const blob = new Blob([workerContent], { type: "text/javascript" });
|
||||
// Create a URL for the Blob
|
||||
const workerUrl = URL.createObjectURL(blob);
|
||||
// Create a new Web Worker
|
||||
const worker = new window.Worker(workerUrl);
|
||||
|
||||
// Handle messages from the worker
|
||||
worker.onmessage = (event) => {
|
||||
const { type, progress, result, time } = event.data;
|
||||
|
||||
if (type === "progress") {
|
||||
if (onProgress) {
|
||||
onProgress(progress);
|
||||
}
|
||||
} else if (type === "result") {
|
||||
// Resolve the promise with the result and time
|
||||
resolve({ result: BigInt(result), time });
|
||||
// Terminate the worker and revoke the URL
|
||||
worker.terminate();
|
||||
URL.revokeObjectURL(workerUrl);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle potential errors in the worker
|
||||
worker.onerror = (error) => {
|
||||
reject(error);
|
||||
// Terminate the worker and revoke the URL in case of error
|
||||
worker.terminate();
|
||||
URL.revokeObjectURL(workerUrl);
|
||||
};
|
||||
|
||||
// Post the data to the worker to start the computation
|
||||
worker.postMessage({ g, N, difficulty });
|
||||
});
|
||||
}
|
||||
36
packages/solid/src/misc/postcss-calc-keyword-polyfill.js
Normal file
36
packages/solid/src/misc/postcss-calc-keyword-polyfill.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { plugin } from "postcss";
|
||||
|
||||
export default plugin("postcss-calc-keyword-polyfill", () => {
|
||||
const replacements = {
|
||||
pi: "3.141592653589793",
|
||||
e: "2.718281828459045",
|
||||
infinity: "1e308", // A very large number to simulate infinity
|
||||
"-infinity": "-1e308", // A very small number to simulate -infinity
|
||||
nan: "0/0" // Division by zero in calc() results in NaN in modern browsers
|
||||
};
|
||||
|
||||
// Regex to find the keywords, case-insensitive
|
||||
const keywordRegex = new RegExp(`\\b(-?(${Object.keys(replacements).join("|")}))\\b`, "gi");
|
||||
|
||||
return (root) => {
|
||||
root.walkDecls((decl) => {
|
||||
// Check if the declaration value contains calc()
|
||||
if (decl.value.toLowerCase().includes("calc(")) {
|
||||
decl.value = decl.value.replace(/calc\(([^)]+)\)/gi, (match, expression) => {
|
||||
const newExpression = expression.replace(keywordRegex, (keyword) => {
|
||||
const lowerKeyword = keyword.toLowerCase();
|
||||
if (lowerKeyword in replacements) {
|
||||
return replacements[lowerKeyword];
|
||||
}
|
||||
// Handle cases like -pi and -e
|
||||
if (lowerKeyword.startsWith("-") && lowerKeyword.substring(1) in replacements) {
|
||||
return `-${replacements[lowerKeyword.substring(1)]}`;
|
||||
}
|
||||
return keyword; // Should not happen with the current regex, but as a fallback
|
||||
});
|
||||
return `calc(${newExpression})`;
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
18
packages/solid/src/routes/[...404].tsx
Normal file
18
packages/solid/src/routes/[...404].tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { Title } from "@solidjs/meta";
|
||||
import { HttpStatusCode } from "@solidjs/start";
|
||||
import { Layout } from "~/components/layout";
|
||||
import { A } from "@solidjs/router";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<Layout>
|
||||
<Title>找不到页面啾~</Title>
|
||||
<HttpStatusCode code={404} />
|
||||
<main class="w-full h-[calc(100vh-6rem)] flex flex-col flex-grow items-center justify-center gap-8">
|
||||
<h1 class="text-9xl font-thin">404</h1>
|
||||
<p class="text-xl font-medium">咦……页面去哪里了(゚Д゚≡゚д゚)!?</p>
|
||||
<A href="/">带我回首页!</A>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
73
packages/solid/src/routes/en/song/[id]/info.tsx
Normal file
73
packages/solid/src/routes/en/song/[id]/info.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { Layout } from "~/components/layout";
|
||||
import { query } from "@solidjs/router";
|
||||
import { Card, CardContent, CardMedia, Typography } from "@m3-components/solid";
|
||||
|
||||
export default function Info() {
|
||||
return (
|
||||
<Layout lang="en">
|
||||
<title></title>
|
||||
<main class="w-full pt-14 lg:max-w-lg xl:max-w-xl lg:mx-auto">
|
||||
<Card variant="outlined">
|
||||
<CardMedia
|
||||
round={false}
|
||||
src="https://i0.hdslb.com/bfs/archive/8ad220336f96e4d2ea05baada3bc04592d56b2a5.jpg"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<CardContent>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Headline variant="small">尘海绘仙缘</Typography.Headline>
|
||||
<Typography.Body class="font-medium text-on-surface-variant" variant="large">
|
||||
Chen Hai Hui Xian Yuan
|
||||
</Typography.Body>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-2 grid-rows-3 gap-2 ">
|
||||
<div class="flex flex-col">
|
||||
<Typography.Body class="font-semibold" variant="small">
|
||||
PUBLISHER
|
||||
</Typography.Body>
|
||||
<Typography.Body variant="large">洛凛</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Body class="font-semibold" variant="small">
|
||||
DURATION
|
||||
</Typography.Body>
|
||||
<Typography.Body variant="large">4:28</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Body class="font-semibold" variant="small">
|
||||
SINGER
|
||||
</Typography.Body>
|
||||
<Typography.Body variant="large">
|
||||
<a href="#">赤羽</a> <span class="text-on-surface-variant">(Chiyu)</span>
|
||||
</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Body class="font-semibold" variant="small">
|
||||
PUBLISH TIME
|
||||
</Typography.Body>
|
||||
<Typography.Body variant="large">2024-12-15 12:15:00</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Body class="font-semibold" variant="small">
|
||||
VIEWS
|
||||
</Typography.Body>
|
||||
<Typography.Body variant="large">12.4K (12,422)</Typography.Body>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<Typography.Body class="font-semibold" variant="small">
|
||||
LINKS
|
||||
</Typography.Body>
|
||||
<Typography.Body class="flex gap-2" variant="large">
|
||||
<a href="https://www.bilibili.com/video/BV1eaq9Y3EVV/">bilibili</a>
|
||||
<a href="https://vocadb.net/S/742394">VocaDB</a>
|
||||
</Typography.Body>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
70
packages/solid/src/routes/index.tsx
Normal file
70
packages/solid/src/routes/index.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { Layout } from "~/components/layout";
|
||||
import { dbMain } from "~/drizzle";
|
||||
import { bilibiliMetadata, latestVideoSnapshot } from "~db/main/schema";
|
||||
import { and, desc, eq, gte, lt } from "drizzle-orm";
|
||||
import { createAsync, query, RouteDefinition } from "@solidjs/router";
|
||||
import { Component, createResource, For, Suspense } from "solid-js";
|
||||
import { BilibiliMetadataType, LatestVideoSnapshotType } from "~db/outerSchema";
|
||||
import { Card, CardContent, CardMedia, Typography } from "@m3-components/solid";
|
||||
|
||||
const getVideoCloseTo1M = query(async () => {
|
||||
"use server";
|
||||
return dbMain
|
||||
.select()
|
||||
.from(bilibiliMetadata)
|
||||
.innerJoin(latestVideoSnapshot, eq(latestVideoSnapshot.aid, bilibiliMetadata.aid))
|
||||
.where(and(gte(latestVideoSnapshot.views, 900000), lt(latestVideoSnapshot.views, 1000000)))
|
||||
.orderBy(desc(latestVideoSnapshot.views))
|
||||
.limit(20);
|
||||
}, "videosCloseTo1M");
|
||||
|
||||
interface VideoCardProps {
|
||||
video: {
|
||||
bilibili_metadata: BilibiliMetadataType;
|
||||
latest_video_snapshot: LatestVideoSnapshotType;
|
||||
};
|
||||
}
|
||||
|
||||
export const route = {
|
||||
preload: () => getVideoCloseTo1M()
|
||||
} satisfies RouteDefinition;
|
||||
|
||||
const VideoCard: Component<VideoCardProps> = (props) => {
|
||||
return (
|
||||
<Card variant="outlined" class="w-64 h-64 grow-0 shrink-0 basis-64">
|
||||
<CardMedia
|
||||
class="w-64 h-32 object-cover"
|
||||
round={false}
|
||||
src={props.video.bilibili_metadata.coverUrl || ""}
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<CardContent class="py-3 px-4">
|
||||
<Typography.Body variant="large" class="text-wrap">
|
||||
{props.video.bilibili_metadata.title}
|
||||
</Typography.Body>
|
||||
<span>{props.video.latest_video_snapshot.views} 播放</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const videos = createAsync(() => getVideoCloseTo1M());
|
||||
return (
|
||||
<Layout>
|
||||
<title>中V档案馆</title>
|
||||
<main class="w-full pt-20 lg:max-w-3xl xl:max-w-4xl 2xl:max-w-6xl lg:mx-auto">
|
||||
<h1 class="text-4xl mb-8">中 V 档案馆</h1>
|
||||
<h2 class="text-2xl font-normal">传说助攻</h2>
|
||||
<div
|
||||
class="flex overflow-x-auto overflow-y-hidden gap-4 whitespace-nowrap w-full
|
||||
py-2 px-4 mt-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<For each={videos()}>{(video) => <VideoCard video={video} />}</For>
|
||||
</Suspense>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
208
packages/solid/src/routes/song/[id]/_info.tsx
Normal file
208
packages/solid/src/routes/song/[id]/_info.tsx
Normal file
@ -0,0 +1,208 @@
|
||||
import { DateTime } from "luxon";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { createResource } from "solid-js";
|
||||
import { Suspense } from "solid-js";
|
||||
import { For } from "solid-js";
|
||||
import { useCachedFetch } from "~/lib/dbCache";
|
||||
import { dbMain } from "~/drizzle";
|
||||
import { bilibiliMetadata, videoSnapshot } from "~db/main/schema";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { BilibiliMetadataType, VideoSnapshotType } from "~db/outerSchema";
|
||||
import { Context, useRequestContext } from "~/components/requestContext";
|
||||
import { Layout } from "~/components/layout";
|
||||
|
||||
async function getAllSnapshots(aid: number, context: Context) {
|
||||
"use server";
|
||||
return useCachedFetch(
|
||||
async () => {
|
||||
return dbMain
|
||||
.select()
|
||||
.from(videoSnapshot)
|
||||
.where(eq(videoSnapshot.aid, aid))
|
||||
.orderBy(desc(videoSnapshot.createdAt));
|
||||
},
|
||||
"all-snapshots",
|
||||
context,
|
||||
[aid]
|
||||
);
|
||||
}
|
||||
|
||||
async function getVideoMetadata(avORbv: number | string, context: Context) {
|
||||
"use server";
|
||||
if (typeof avORbv === "number") {
|
||||
return useCachedFetch(
|
||||
async () => {
|
||||
return dbMain.select().from(bilibiliMetadata).where(eq(bilibiliMetadata.aid, avORbv)).limit(1);
|
||||
},
|
||||
"bili-metadata",
|
||||
context,
|
||||
[avORbv]
|
||||
);
|
||||
} else {
|
||||
return useCachedFetch(
|
||||
async () => {
|
||||
return dbMain.select().from(bilibiliMetadata).where(eq(bilibiliMetadata.bvid, avORbv)).limit(1);
|
||||
},
|
||||
"bili-metadata",
|
||||
context,
|
||||
[avORbv]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const MetadataRow = ({ title, desc }: { title: string; desc: string | number | undefined | null }) => {
|
||||
if (!desc) return <></>;
|
||||
return (
|
||||
<tr>
|
||||
<td class="max-w-14 min-w-14 md:max-w-24 md:min-w-24 border dark:border-zinc-500 px-2 md:px-3 py-2 font-semibold">
|
||||
{title}
|
||||
</td>
|
||||
<td class="break-all max-w-[calc(100vw-4.5rem)] border dark:border-zinc-500 px-4 py-2">{desc}</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default function VideoInfoPage() {
|
||||
const params = useParams();
|
||||
const { id } = params;
|
||||
const context = useRequestContext();
|
||||
const [data] = createResource(async () => {
|
||||
let videoInfo: BilibiliMetadataType | null = null;
|
||||
let snapshots: VideoSnapshotType[] = [];
|
||||
|
||||
try {
|
||||
const videoData = await getVideoMetadata(id, context);
|
||||
if (videoData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const snapshotsData = await getAllSnapshots(videoData[0].aid, context);
|
||||
videoInfo = videoData[0];
|
||||
if (snapshotsData) {
|
||||
snapshots = snapshotsData;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if (!videoInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = `${videoInfo.title} - 歌曲信息 - 中 V 档案馆`;
|
||||
|
||||
return {
|
||||
v: videoInfo,
|
||||
s: snapshots,
|
||||
t: title
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<main class="flex flex-col items-center min-h-screen gap-8 mt-10 md:mt-6 relative z-0 overflow-x-auto pb-8">
|
||||
<div class="w-full lg:max-w-4xl lg:mx-auto lg:p-6">
|
||||
<Suspense fallback={<div>loading</div>}>
|
||||
<title>{data()?.t}</title>
|
||||
<span>{data()?.t}</span>
|
||||
<h1 class="text-2xl font-medium ml-2 mb-4">
|
||||
视频信息:{" "}
|
||||
<a href={`https://www.bilibili.com/video/av${data()?.v.aid}`} class="underline">
|
||||
av{data()?.v.aid}
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<div class="mb-6">
|
||||
<h2 class="px-2 mb-2 text-xl font-medium">基本信息</h2>
|
||||
<div class="overflow-x-auto max-w-full px-2">
|
||||
<table class="table-fixed">
|
||||
<tbody>
|
||||
<MetadataRow title="ID" desc={data()?.v.id} />
|
||||
<MetadataRow title="av 号" desc={data()?.v.aid} />
|
||||
<MetadataRow title="BV 号" desc={data()?.v.bvid} />
|
||||
<MetadataRow title="标题" desc={data()?.v.title} />
|
||||
<MetadataRow title="描述" desc={data()?.v.description} />
|
||||
<MetadataRow title="UID" desc={data()?.v.uid} />
|
||||
<MetadataRow title="标签" desc={data()?.v.tags} />
|
||||
<MetadataRow
|
||||
title="发布时间"
|
||||
desc={
|
||||
data()?.v.publishedAt
|
||||
? DateTime.fromJSDate(
|
||||
new Date(data()?.v.publishedAt || "")
|
||||
).toFormat("yyyy-MM-dd HH:mm:ss")
|
||||
: null
|
||||
}
|
||||
/>
|
||||
<MetadataRow title="时长 (秒)" desc={data()?.v.duration} />
|
||||
<MetadataRow
|
||||
title="创建时间"
|
||||
desc={DateTime.fromJSDate(new Date(data()?.v.createdAt || "")).toFormat(
|
||||
"yyyy-MM-dd HH:mm:ss"
|
||||
)}
|
||||
/>
|
||||
<MetadataRow title="封面" desc={data()?.v?.coverUrl} />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="px-2 mb-2 text-xl font-medium">播放量历史数据</h2>
|
||||
|
||||
<div class="overflow-x-auto px-2">
|
||||
<table class="table-auto w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">创建时间</th>
|
||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">观看</th>
|
||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">硬币</th>
|
||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">点赞</th>
|
||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">收藏</th>
|
||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">分享</th>
|
||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">弹幕</th>
|
||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">评论</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={data()?.s}>
|
||||
{(snapshot) => (
|
||||
<tr>
|
||||
<td class="border dark:border-zinc-500 px-4 py-2">
|
||||
{DateTime.fromJSDate(new Date(snapshot.createdAt)).toFormat(
|
||||
"yyyy-MM-dd HH:mm:ss"
|
||||
)}
|
||||
</td>
|
||||
<td class="border dark:border-zinc-500 px-4 py-2">
|
||||
{snapshot.views}
|
||||
</td>
|
||||
<td class="border dark:border-zinc-500 px-4 py-2">
|
||||
{snapshot.coins}
|
||||
</td>
|
||||
<td class="border dark:border-zinc-500 px-4 py-2">
|
||||
{snapshot.likes}
|
||||
</td>
|
||||
<td class="border dark:border-zinc-500 px-4 py-2">
|
||||
{snapshot.favorites}
|
||||
</td>
|
||||
<td class="border dark:border-zinc-500 px-4 py-2">
|
||||
{snapshot.shares}
|
||||
</td>
|
||||
<td class="border dark:border-zinc-500 px-4 py-2">
|
||||
{snapshot.danmakus}
|
||||
</td>
|
||||
<td class="border dark:border-zinc-500 px-4 py-2">
|
||||
{snapshot.replies}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
80
packages/solid/src/routes/song/[id]/info.tsx
Normal file
80
packages/solid/src/routes/song/[id]/info.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { Layout } from "~/components/layout";
|
||||
import { LeftSideBar } from "~/components/song/LeftSideBar";
|
||||
import { RightSideBar } from "~/components/song/RightSideBar";
|
||||
import { Content } from "~/components/song/Content";
|
||||
import { createAsync, query, RouteDefinition, useParams } from "@solidjs/router";
|
||||
import { dbMain } from "~/drizzle";
|
||||
import { bilibiliMetadata, songs } from "~db/main/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const getVideoAID = async (id: string) => {
|
||||
"use server";
|
||||
if (id.startsWith("av")) {
|
||||
return parseInt(id.slice(2));
|
||||
} else if (id.startsWith("BV")) {
|
||||
const data = await dbMain
|
||||
.select()
|
||||
.from(bilibiliMetadata)
|
||||
.where(eq(bilibiliMetadata.bvid, id));
|
||||
return data[0].aid;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const findSongIDFromAID = async (aid: number) => {
|
||||
"use server";
|
||||
const data = await dbMain.select({
|
||||
id: songs.id,
|
||||
}).from(songs).where(eq(songs.aid, aid)).limit(1);
|
||||
return data[0].id;
|
||||
}
|
||||
|
||||
const getSongInfo = query(async (songID: number) => {
|
||||
"use server";
|
||||
const data = await dbMain.select().from(songs).where(eq(songs.id, songID));
|
||||
return data[0] || null;
|
||||
}, "songs");
|
||||
|
||||
const getSongInfoFromID = query(async (id: string) => {
|
||||
"use server";
|
||||
const aid = await getVideoAID(id);
|
||||
if (!aid && parseInt(id)) {
|
||||
return getSongInfo(parseInt(id));
|
||||
}
|
||||
else if (!aid) {
|
||||
return null;
|
||||
}
|
||||
const songID = await findSongIDFromAID(aid);
|
||||
return getSongInfo(songID);
|
||||
}, "songsRaw")
|
||||
|
||||
export const route = {
|
||||
preload: ({ params }) => getSongInfoFromID(params.id)
|
||||
} satisfies RouteDefinition;
|
||||
|
||||
|
||||
export default function Info() {
|
||||
const params = useParams();
|
||||
const info = createAsync(() => getSongInfoFromID(params.id));
|
||||
return (
|
||||
<Layout>
|
||||
<title>尘海绘仙缘 - 歌曲信息 - 中 V 档案馆</title>
|
||||
<div
|
||||
class="pt-8 w-full sm:w-120 sm:mx-auto lg:w-full 2xl:w-360 lg:grid lg:grid-cols-[1fr_560px_1fr]
|
||||
xl:grid-cols-[1fr_648px_1fr]"
|
||||
>
|
||||
<nav class="top-32 hidden lg:block pb-12 px-6 self-start sticky">
|
||||
<LeftSideBar />
|
||||
</nav>
|
||||
<main class="mb-24">
|
||||
<Content data={info() || null}/>
|
||||
</main>
|
||||
<div class="top-32 hidden lg:flex self-start sticky flex-col pb-12 px-6">
|
||||
<RightSideBar />
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
9
packages/solid/src/routes/songs/index.tsx
Normal file
9
packages/solid/src/routes/songs/index.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { Layout } from "~/components/layout";
|
||||
|
||||
export default function SongsHome() {
|
||||
return (
|
||||
<Layout>
|
||||
<h1 class="text-4xl mb-8">歌曲</h1>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
56
packages/solid/tailwind.config.ts
Normal file
56
packages/solid/tailwind.config.ts
Normal file
@ -0,0 +1,56 @@
|
||||
function generateSpacing(nums: number[], divisor = 4): Record<number, string> {
|
||||
return nums.reduce<Record<number, string>>((acc, n) => {
|
||||
acc[n] = `${n / divisor}rem`;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
spacing: generateSpacing([360]),
|
||||
colors: {
|
||||
background: "var(--md-sys-color-background)",
|
||||
"on-background": "var(--md-sys-color-on-background)",
|
||||
surface: "var(--md-sys-color-surface)",
|
||||
"surface-dim": "var(--md-sys-color-surface-dim)",
|
||||
"surface-bright": "var(--md-sys-color-surface-bright)",
|
||||
"surface-container-lowest": "var(--md-sys-color-surface-container-lowest)",
|
||||
"surface-container-low": "var(--md-sys-color-surface-container-low)",
|
||||
"surface-container": "var(--md-sys-color-surface-container)",
|
||||
"surface-container-high": "var(--md-sys-color-surface-container-high)",
|
||||
"surface-container-highest": "var(--md-sys-color-surface-container-highest)",
|
||||
"on-surface": "var(--md-sys-color-on-surface)",
|
||||
"surface-variant": "var(--md-sys-color-surface-variant)",
|
||||
"on-surface-variant": "var(--md-sys-color-on-surface-variant)",
|
||||
"inverse-surface": "var(--md-sys-color-inverse-surface)",
|
||||
"inverse-on-surface": "var(--md-sys-color-inverse-on-surface)",
|
||||
outline: "var(--md-sys-color-outline)",
|
||||
"outline-variant": "var(--md-sys-color-outline-variant)",
|
||||
shadow: "var(--md-sys-color-shadow)",
|
||||
scrim: "var(--md-sys-color-scrim)",
|
||||
"surface-tint": "var(--md-sys-color-surface-tint)",
|
||||
primary: "var(--md-sys-color-primary)",
|
||||
"on-primary": "var(--md-sys-color-on-primary)",
|
||||
"primary-container": "var(--md-sys-color-primary-container)",
|
||||
"on-primary-container": "var(--md-sys-color-on-primary-container)",
|
||||
"inverse-primary": "var(--md-sys-color-inverse-primary)",
|
||||
secondary: "var(--md-sys-color-secondary)",
|
||||
"on-secondary": "var(--md-sys-color-on-secondary)",
|
||||
"secondary-container": "var(--md-sys-color-secondary-container)",
|
||||
"on-secondary-container": "var(--md-sys-color-on-secondary-container)",
|
||||
tertiary: "var(--md-sys-color-tertiary)",
|
||||
"on-tertiary": "var(--md-sys-color-on-tertiary)",
|
||||
"tertiary-container": "var(--md-sys-color-tertiary-container)",
|
||||
"on-tertiary-container": "var(--md-sys-color-on-tertiary-container)",
|
||||
error: "var(--md-sys-color-error)",
|
||||
"on-error": "var(--md-sys-color-on-error)",
|
||||
"error-container": "var(--md-sys-color-error-container)",
|
||||
"on-error-container": "var(--md-sys-color-on-error-container)"
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
};
|
||||
20
packages/solid/tsconfig.json
Normal file
20
packages/solid/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"types": ["vinxi/types/client"],
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"~/*": ["./src/*"],
|
||||
"~db/*": ["./src/drizzle/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
7
packages/temp_frontend/.gitignore
vendored
7
packages/temp_frontend/.gitignore
vendored
@ -1,6 +1,3 @@
|
||||
.DS_Store
|
||||
/node_modules/
|
||||
|
||||
# React Router
|
||||
/.react-router/
|
||||
/build/
|
||||
.react-router/
|
||||
build/
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
// Generated by React Router
|
||||
|
||||
import "react-router";
|
||||
|
||||
declare module "react-router" {
|
||||
interface Future {
|
||||
v8_middleware: false
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
// Generated by React Router
|
||||
|
||||
import "react-router"
|
||||
|
||||
declare module "react-router" {
|
||||
interface Register {
|
||||
pages: Pages
|
||||
routeFiles: RouteFiles
|
||||
}
|
||||
}
|
||||
|
||||
type Pages = {
|
||||
"/": {
|
||||
params: {};
|
||||
};
|
||||
"/song/:id/info": {
|
||||
params: {
|
||||
"id": string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type RouteFiles = {
|
||||
"root.tsx": {
|
||||
id: "root";
|
||||
page: "/" | "/song/:id/info";
|
||||
};
|
||||
"routes/home.tsx": {
|
||||
id: "routes/home";
|
||||
page: "/";
|
||||
};
|
||||
"routes/song/[id]/info.tsx": {
|
||||
id: "routes/song/[id]/info";
|
||||
page: "/song/:id/info";
|
||||
};
|
||||
};
|
||||
@ -1,17 +0,0 @@
|
||||
// Generated by React Router
|
||||
|
||||
declare module "virtual:react-router/server-build" {
|
||||
import { ServerBuild } from "react-router";
|
||||
export const assets: ServerBuild["assets"];
|
||||
export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"];
|
||||
export const basename: ServerBuild["basename"];
|
||||
export const entry: ServerBuild["entry"];
|
||||
export const future: ServerBuild["future"];
|
||||
export const isSpaMode: ServerBuild["isSpaMode"];
|
||||
export const prerender: ServerBuild["prerender"];
|
||||
export const publicPath: ServerBuild["publicPath"];
|
||||
export const routeDiscovery: ServerBuild["routeDiscovery"];
|
||||
export const routes: ServerBuild["routes"];
|
||||
export const ssr: ServerBuild["ssr"];
|
||||
export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"];
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
// Generated by React Router
|
||||
|
||||
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||
|
||||
type Module = typeof import("../root.js")
|
||||
|
||||
type Info = GetInfo<{
|
||||
file: "root.tsx",
|
||||
module: Module
|
||||
}>
|
||||
|
||||
type Matches = [{
|
||||
id: "root";
|
||||
module: typeof import("../root.js");
|
||||
}];
|
||||
|
||||
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||
|
||||
export namespace Route {
|
||||
// links
|
||||
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||
export type LinksFunction = Annotations["LinksFunction"];
|
||||
|
||||
// meta
|
||||
export type MetaArgs = Annotations["MetaArgs"];
|
||||
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||
export type MetaFunction = Annotations["MetaFunction"];
|
||||
|
||||
// headers
|
||||
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||
|
||||
// middleware
|
||||
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||
|
||||
// clientMiddleware
|
||||
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||
|
||||
// loader
|
||||
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||
|
||||
// clientLoader
|
||||
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||
|
||||
// action
|
||||
export type ActionArgs = Annotations["ActionArgs"];
|
||||
|
||||
// clientAction
|
||||
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||
|
||||
// HydrateFallback
|
||||
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||
|
||||
// Component
|
||||
export type ComponentProps = Annotations["ComponentProps"];
|
||||
|
||||
// ErrorBoundary
|
||||
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
// Generated by React Router
|
||||
|
||||
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||
|
||||
type Module = typeof import("../home.js")
|
||||
|
||||
type Info = GetInfo<{
|
||||
file: "routes/home.tsx",
|
||||
module: Module
|
||||
}>
|
||||
|
||||
type Matches = [{
|
||||
id: "root";
|
||||
module: typeof import("../../root.js");
|
||||
}, {
|
||||
id: "routes/home";
|
||||
module: typeof import("../home.js");
|
||||
}];
|
||||
|
||||
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||
|
||||
export namespace Route {
|
||||
// links
|
||||
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||
export type LinksFunction = Annotations["LinksFunction"];
|
||||
|
||||
// meta
|
||||
export type MetaArgs = Annotations["MetaArgs"];
|
||||
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||
export type MetaFunction = Annotations["MetaFunction"];
|
||||
|
||||
// headers
|
||||
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||
|
||||
// middleware
|
||||
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||
|
||||
// clientMiddleware
|
||||
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||
|
||||
// loader
|
||||
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||
|
||||
// clientLoader
|
||||
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||
|
||||
// action
|
||||
export type ActionArgs = Annotations["ActionArgs"];
|
||||
|
||||
// clientAction
|
||||
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||
|
||||
// HydrateFallback
|
||||
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||
|
||||
// Component
|
||||
export type ComponentProps = Annotations["ComponentProps"];
|
||||
|
||||
// ErrorBoundary
|
||||
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
// Generated by React Router
|
||||
|
||||
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||
|
||||
type Module = typeof import("../info.js")
|
||||
|
||||
type Info = GetInfo<{
|
||||
file: "routes/song/[id]/info.tsx",
|
||||
module: Module
|
||||
}>
|
||||
|
||||
type Matches = [{
|
||||
id: "root";
|
||||
module: typeof import("../../../../root.js");
|
||||
}, {
|
||||
id: "routes/song/[id]/info";
|
||||
module: typeof import("../info.js");
|
||||
}];
|
||||
|
||||
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
|
||||
|
||||
export namespace Route {
|
||||
// links
|
||||
export type LinkDescriptors = Annotations["LinkDescriptors"];
|
||||
export type LinksFunction = Annotations["LinksFunction"];
|
||||
|
||||
// meta
|
||||
export type MetaArgs = Annotations["MetaArgs"];
|
||||
export type MetaDescriptors = Annotations["MetaDescriptors"];
|
||||
export type MetaFunction = Annotations["MetaFunction"];
|
||||
|
||||
// headers
|
||||
export type HeadersArgs = Annotations["HeadersArgs"];
|
||||
export type HeadersFunction = Annotations["HeadersFunction"];
|
||||
|
||||
// middleware
|
||||
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
|
||||
|
||||
// clientMiddleware
|
||||
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
|
||||
|
||||
// loader
|
||||
export type LoaderArgs = Annotations["LoaderArgs"];
|
||||
|
||||
// clientLoader
|
||||
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
|
||||
|
||||
// action
|
||||
export type ActionArgs = Annotations["ActionArgs"];
|
||||
|
||||
// clientAction
|
||||
export type ClientActionArgs = Annotations["ClientActionArgs"];
|
||||
|
||||
// HydrateFallback
|
||||
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
|
||||
|
||||
// Component
|
||||
export type ComponentProps = Annotations["ComponentProps"];
|
||||
|
||||
// ErrorBoundary
|
||||
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
|
||||
}
|
||||
118
src/fillSongInfo.ts
Normal file
118
src/fillSongInfo.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import arg from "arg";
|
||||
import logger from "@core/log/logger";
|
||||
import { sql } from "@core/index";
|
||||
import type { Row } from "postgres";
|
||||
|
||||
const quit = (reason?: string) => {
|
||||
reason && logger.error(reason);
|
||||
process.exit();
|
||||
};
|
||||
|
||||
const args = arg({
|
||||
"--file": String
|
||||
});
|
||||
|
||||
const dataPath = args["--file"];
|
||||
if (!dataPath) {
|
||||
quit("Missing --file <path>");
|
||||
}
|
||||
|
||||
interface Item {
|
||||
name: string;
|
||||
singer: string[];
|
||||
}
|
||||
|
||||
type DataFile = {
|
||||
[key: string]: Item;
|
||||
};
|
||||
|
||||
const pg = sql;
|
||||
|
||||
async function getVideoInfo(id: string): Promise<Row | undefined> {
|
||||
if (parseInt(id)) {
|
||||
return (
|
||||
await pg`
|
||||
SELECT aid, bvid
|
||||
FROM bilibili_metadata
|
||||
WHERE aid = ${id}
|
||||
`
|
||||
)[0];
|
||||
} else if (id.startsWith("av")) {
|
||||
return (
|
||||
await pg`
|
||||
SELECT aid, bvid
|
||||
FROM bilibili_metadata
|
||||
WHERE aid = ${id.replace("av", "")}
|
||||
`
|
||||
)[0];
|
||||
} else if (id.startsWith("BV")) {
|
||||
return (
|
||||
await pg`
|
||||
SELECT aid, bvid
|
||||
FROM bilibili_metadata
|
||||
WHERE bvid = ${id}
|
||||
`
|
||||
)[0];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function getSingerID(name: string): Promise<number | undefined> {
|
||||
const singer = await pg`
|
||||
SELECT id
|
||||
FROM singer
|
||||
WHERE name = ${name}
|
||||
`;
|
||||
if (singer.length > 0) {
|
||||
return singer[0]?.id;
|
||||
}
|
||||
const singerID = await pg`
|
||||
INSERT INTO singer (name)
|
||||
VALUES (${name})
|
||||
RETURNING id
|
||||
`;
|
||||
return singerID[0]?.id;
|
||||
}
|
||||
|
||||
async function processVideo(key: string, item: Item) {
|
||||
const videoInfo = await getVideoInfo(key);
|
||||
if (!videoInfo) {
|
||||
logger.warn(`Video not found: ${key}`);
|
||||
return;
|
||||
}
|
||||
const aid = videoInfo.aid;
|
||||
await pg`
|
||||
UPDATE songs
|
||||
SET name = ${item.name}
|
||||
WHERE aid = ${aid}
|
||||
`;
|
||||
const singerIDs = (await Promise.all(item.singer.map(async (singer) => await getSingerID(singer)))).filter(
|
||||
(id) => id !== undefined
|
||||
);
|
||||
for (const singerID of singerIDs) {
|
||||
await pg`
|
||||
INSERT INTO
|
||||
relations (source_id, source_type, target_id, target_type, relation)
|
||||
VALUES (${aid}, 'song', ${singerID}, 'singer', 'sing')
|
||||
ON CONFLICT (source_id, source_type, target_id, target_type, relation) DO NOTHING;
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function fillSongInfo() {
|
||||
let fixQuery = "";
|
||||
let i = 0;
|
||||
const file = Bun.file(dataPath!);
|
||||
const candidates: DataFile = await file.json();
|
||||
const length = Object.keys(candidates).length;
|
||||
for (const videoID in candidates) {
|
||||
await processVideo(videoID, candidates[videoID]!);
|
||||
i++;
|
||||
logger.log(`Progress: ${i}/${length}`);
|
||||
}
|
||||
return fixQuery;
|
||||
}
|
||||
|
||||
await fillSongInfo();
|
||||
quit();
|
||||
68
src/fixCover.ts
Normal file
68
src/fixCover.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import arg from "arg";
|
||||
import { Database } from "bun:sqlite";
|
||||
import logger from "@core/log/logger";
|
||||
import type { VideoDetailsData } from "@core/net/bilibili.d.ts";
|
||||
import { sql } from "@core/index";
|
||||
|
||||
const quit = (reason?: string) => {
|
||||
reason && logger.error(reason);
|
||||
process.exit();
|
||||
};
|
||||
|
||||
const args = arg({
|
||||
"--db": String
|
||||
});
|
||||
|
||||
const dbPath = args["--db"];
|
||||
if (!dbPath) {
|
||||
quit("Missing --db <path>");
|
||||
}
|
||||
|
||||
const sqlite = new Database(dbPath);
|
||||
const pg = sql;
|
||||
|
||||
async function fixMissingCover() {
|
||||
let fixQuery = "";
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
const candidates = await pg`
|
||||
SELECT aid
|
||||
FROM
|
||||
bilibili_metadata
|
||||
WHERE
|
||||
cover_url IS NULL
|
||||
`;
|
||||
const query = sqlite.query(`SELECT data FROM bili_info_crawl WHERE aid = $aid`);
|
||||
for (const video of candidates) {
|
||||
j++;
|
||||
logger.log(`Progress: ${j}/${candidates.length}`);
|
||||
const aid: number = video.aid;
|
||||
try {
|
||||
const sqliteData: any = query.get({ $aid: aid });
|
||||
const rawData: VideoDetailsData | null = JSON.parse(sqliteData.data);
|
||||
if (!rawData) {
|
||||
logger.warn(`Data not exists for aid: ${aid}`);
|
||||
continue;
|
||||
}
|
||||
const coverURL = rawData.View.pic;
|
||||
if (!coverURL) continue;
|
||||
const q = `UPDATE bilibili_metadata SET cover_url = '${coverURL}' WHERE aid = ${aid};\n`;
|
||||
logger.log(`Fixing cover for aid: ${aid}`);
|
||||
i++;
|
||||
fixQuery += q;
|
||||
} catch (e) {
|
||||
//logger.error(e as Error, undefined, aid.toString());
|
||||
logger.error(aid.toString());
|
||||
}
|
||||
if (j % 1000 === 0) {
|
||||
const bytes = await Bun.write("scripts/fix_2.sql", fixQuery);
|
||||
logger.warn(`Wrote ${bytes} bytes`, "backup");
|
||||
}
|
||||
}
|
||||
logger.log(`Fixed ${i} videos, query length ${fixQuery.length}.`);
|
||||
return fixQuery;
|
||||
}
|
||||
|
||||
const q = await fixMissingCover();
|
||||
await Bun.write("scripts/fix_2.sql", q);
|
||||
quit();
|
||||
70
src/fixPubDate.ts
Normal file
70
src/fixPubDate.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import arg from "arg";
|
||||
import { Database } from "bun:sqlite";
|
||||
import logger from "@core/log/logger";
|
||||
import type { VideoDetailsData } from "@core/net/bilibili.d.ts";
|
||||
import { sql } from "@core/index";
|
||||
|
||||
const quit = (reason?: string) => {
|
||||
reason && logger.error(reason);
|
||||
process.exit();
|
||||
};
|
||||
|
||||
const args = arg({
|
||||
"--db": String
|
||||
});
|
||||
|
||||
const dbPath = args["--db"];
|
||||
if (!dbPath) {
|
||||
quit("Missing --db <path>");
|
||||
}
|
||||
|
||||
const sqlite = new Database(dbPath);
|
||||
const pg = sql;
|
||||
|
||||
async function fixTimezoneError() {
|
||||
let fixQuery = "";
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
const candidates = await pg`
|
||||
SELECT aid, published_at
|
||||
FROM
|
||||
bilibili_metadata
|
||||
WHERE
|
||||
published_at >= '2025-04-26'
|
||||
AND published_at <= '2025-06-01'
|
||||
AND status = 0
|
||||
`;
|
||||
const query = sqlite.query(`SELECT data FROM bili_info_crawl WHERE aid = $aid`);
|
||||
for (const video of candidates) {
|
||||
const aid: number = video.aid;
|
||||
try {
|
||||
const sqliteData: any = query.get({ $aid: aid });
|
||||
const rawData: VideoDetailsData | null = JSON.parse(sqliteData.data);
|
||||
if (!rawData) {
|
||||
logger.warn(`Data not exists for aid: ${aid}`);
|
||||
continue;
|
||||
}
|
||||
const realTimestamp = rawData.View.pubdate;
|
||||
const dbTimestamp = video.published_at.getTime() / 1000;
|
||||
const diff = dbTimestamp - realTimestamp;
|
||||
if (Math.abs(diff) > 1) {
|
||||
logger.warn(`Find incorrect timestamp for aid ${aid} with diff of ${diff} sec`);
|
||||
const date = new Date(realTimestamp * 1000).toISOString();
|
||||
const q = `UPDATE bilibili_metadata SET published_at = '${date}' WHERE aid = ${aid};\n`;
|
||||
fixQuery += q;
|
||||
i++;
|
||||
}
|
||||
} catch (e) {
|
||||
//logger.error(e as Error, undefined, aid.toString());
|
||||
logger.error(aid.toString());
|
||||
}
|
||||
j++;
|
||||
logger.log(`Progress: ${j}/${candidates.length}`);
|
||||
}
|
||||
logger.log(`Fixed ${i} videos, query length ${fixQuery.length}.`);
|
||||
return fixQuery;
|
||||
}
|
||||
|
||||
const q = await fixTimezoneError();
|
||||
await Bun.write("scripts/fix.sql", q);
|
||||
quit();
|
||||
@ -1,32 +0,0 @@
|
||||
import arg from "arg";
|
||||
//import { getVideoDetails } from "@crawler/net/getVideoDetails";
|
||||
import logger from "@core/log/logger";
|
||||
|
||||
const quit = (reason: string) => {
|
||||
logger.error(reason);
|
||||
process.exit();
|
||||
};
|
||||
|
||||
const args = arg({
|
||||
"--aids": String // --port <number> or --port=<number>
|
||||
});
|
||||
|
||||
const aidsFileName = args["--aids"];
|
||||
|
||||
if (!aidsFileName) {
|
||||
quit("Missing --aids <file_path>");
|
||||
}
|
||||
|
||||
const aidsFile = Bun.file(aidsFileName!);
|
||||
const fileExists = await aidsFile.exists();
|
||||
if (!fileExists) {
|
||||
quit(`${aidsFile} does not exist.`);
|
||||
}
|
||||
|
||||
const aidsText = await aidsFile.text();
|
||||
const aids = aidsText
|
||||
.split("\n")
|
||||
.map((line) => parseInt(line))
|
||||
.filter((num) => !Number.isNaN(num));
|
||||
|
||||
logger.log(`Read ${aids.length} aids.`);
|
||||
Loading…
Reference in New Issue
Block a user