improve: search UI, added snapshots in song info page
This commit is contained in:
parent
18f6186215
commit
ab5545966e
41
bun.lock
41
bun.lock
@ -92,7 +92,7 @@
|
||||
"chalk": "^5.6.2",
|
||||
"elysia": "^1.4.0",
|
||||
"elysia-ip": "^1.0.10",
|
||||
"zod": "^4.1.11",
|
||||
"zod": "^4.1.12",
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest",
|
||||
@ -199,6 +199,7 @@
|
||||
"@elysiajs/eden": "^1.4.1",
|
||||
"@nivo/core": "^0.99.0",
|
||||
"@nivo/line": "^0.99.0",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@react-router/node": "^7.7.1",
|
||||
@ -758,10 +759,20 @@
|
||||
|
||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||
|
||||
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
|
||||
|
||||
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
|
||||
|
||||
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
|
||||
|
||||
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
|
||||
|
||||
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||
|
||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||
|
||||
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
|
||||
|
||||
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
|
||||
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
@ -778,6 +789,8 @@
|
||||
|
||||
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
|
||||
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
|
||||
|
||||
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@react-router/dev": ["@react-router/dev@7.9.1", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", "@react-router/node": "7.9.1", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "pathe": "^1.1.2", "picocolors": "^1.1.1", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "set-cookie-parser": "^2.6.0", "tinyglobby": "^0.2.14", "valibot": "^0.41.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.9.1", "@vitejs/plugin-rsc": "*", "react-router": "^7.9.1", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-fW/qubsdHq1nsufHPLpXa6hiNvXXV9JBtWqRlJ02OOhFeaWERZw4rGoHjG1DCg8/QTTadgbzplmP97ZnzWPkcA=="],
|
||||
@ -1190,6 +1203,8 @@
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
|
||||
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
|
||||
@ -1270,7 +1285,7 @@
|
||||
|
||||
"bullmq": ["bullmq@5.58.7", "", { "dependencies": { "cron-parser": "^4.9.0", "ioredis": "^5.4.1", "msgpackr": "^1.11.2", "node-abort-controller": "^3.1.1", "semver": "^7.5.4", "tslib": "^2.0.0", "uuid": "^11.1.0" } }, "sha512-rqsKV/ip76wU90q7Cxpr1vS/6PYIVbhuzqr3wgILgjS6XbsnJtWyYrK23jqWHs9+m6/NXM4+62hyf8CSBpufAw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
||||
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
@ -1480,6 +1495,8 @@
|
||||
|
||||
"detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="],
|
||||
|
||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||
|
||||
"deterministic-object-hash": ["deterministic-object-hash@2.0.2", "", { "dependencies": { "base-64": "^1.0.0" } }, "sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ=="],
|
||||
|
||||
"devalue": ["devalue@5.3.2", "", {}, "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw=="],
|
||||
@ -1700,6 +1717,8 @@
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||
|
||||
"get-port": ["get-port@5.1.1", "", {}, "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ=="],
|
||||
|
||||
"get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="],
|
||||
@ -2384,8 +2403,14 @@
|
||||
|
||||
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
|
||||
|
||||
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
|
||||
|
||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||
|
||||
"react-router": ["react-router@7.9.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g=="],
|
||||
|
||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||
|
||||
"react-virtualized-auto-sizer": ["react-virtualized-auto-sizer@1.0.26", "", { "peerDependencies": { "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A=="],
|
||||
|
||||
"read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="],
|
||||
@ -2790,8 +2815,12 @@
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||
|
||||
"use-debounce": ["use-debounce@10.0.6", "", { "peerDependencies": { "react": "*" } }, "sha512-C5OtPyhAZgVoteO9heXMTdW7v/IbFI+8bSVKYCJrSmiWWCLsbUxiBSp4t9v0hNBTGY97bT72ydDIDyGSFWfwXg=="],
|
||||
|
||||
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
@ -3012,6 +3041,8 @@
|
||||
|
||||
"@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="],
|
||||
|
||||
"@types/bun/bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
||||
|
||||
"@types/d3-scale/@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"@types/xml2js/@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="],
|
||||
@ -3090,7 +3121,7 @@
|
||||
|
||||
"dot-prop/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"elysia-api/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||
"elysia-api/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
|
||||
|
||||
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
@ -3360,6 +3391,8 @@
|
||||
|
||||
"@tanstack/server-functions-plugin/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"@types/bun/bun-types/@types/node": ["@types/node@24.6.2", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang=="],
|
||||
|
||||
"@types/xml2js/@types/node/undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
@ -3474,6 +3507,8 @@
|
||||
|
||||
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
|
||||
|
||||
"@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
|
||||
|
||||
"@unocss/cli/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"@unocss/vite/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
31
packages/elysia/lib/schema.ts
Normal file
31
packages/elysia/lib/schema.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const BiliVideoSchema = z.object({
|
||||
duration: z.number().nullable(),
|
||||
id: z.number(),
|
||||
aid: z.number(),
|
||||
publishedAt: z.string().nullable(),
|
||||
createdAt: z.string().nullable(),
|
||||
description: z.string().nullable(),
|
||||
bvid: z.string().nullable(),
|
||||
uid: z.number().nullable(),
|
||||
tags: z.string().nullable(),
|
||||
title: z.string().nullable(),
|
||||
status: z.number(),
|
||||
coverUrl: z.string().nullable()
|
||||
});
|
||||
|
||||
export const SongSchema = z.object({
|
||||
duration: z.number().nullable(),
|
||||
name: z.string().nullable(),
|
||||
id: z.number(),
|
||||
aid: z.number().nullable(),
|
||||
publishedAt: z.string().nullable(),
|
||||
type: z.number().nullable(),
|
||||
neteaseId: z.number().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
deleted: z.boolean(),
|
||||
image: z.string().nullable(),
|
||||
producer: z.string().nullable()
|
||||
});
|
||||
@ -15,7 +15,7 @@
|
||||
"chalk": "^5.6.2",
|
||||
"elysia": "^1.4.0",
|
||||
"elysia-ip": "^1.0.10",
|
||||
"zod": "^4.1.11"
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "latest",
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import { Elysia } from "elysia";
|
||||
import { db } from "@core/drizzle";
|
||||
import { bilibiliMetadata, latestVideoSnapshot, songs } from "@core/drizzle/main/schema";
|
||||
import { eq, like, or } from "drizzle-orm";
|
||||
import type { BilibiliMetadataType, ProducerType, SongType } from "@core/drizzle/outerSchema";
|
||||
import { BiliVideoSchema, SongSchema } from "@elysia/lib/schema";
|
||||
import { z } from "zod";
|
||||
|
||||
interface SongSearchResult {
|
||||
type: "song";
|
||||
@ -27,7 +29,7 @@ const getSongSearchResult = async (searchQuery: string) => {
|
||||
.select()
|
||||
.from(songs)
|
||||
.innerJoin(latestVideoSnapshot, eq(songs.aid, latestVideoSnapshot.aid))
|
||||
.where(like(songs.name, `%${searchQuery}%`))
|
||||
.where(like(songs.name, `%${searchQuery}%`));
|
||||
|
||||
const results = data
|
||||
.map((song) => {
|
||||
@ -88,7 +90,7 @@ const getVideoSearchResult = async (searchQuery: string) => {
|
||||
eq(bilibiliMetadata.bvid, searchQuery),
|
||||
eq(bilibiliMetadata.aid, extractAVID(searchQuery) || 0)
|
||||
)
|
||||
)
|
||||
);
|
||||
return results.map((video) => ({
|
||||
type: "bili-video" as "bili-video",
|
||||
data: { views: video.latest_video_snapshot.views, ...video.bilibili_metadata },
|
||||
@ -96,35 +98,8 @@ const getVideoSearchResult = async (searchQuery: string) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const BiliVideoDataSchema = t.Object({
|
||||
duration: t.Union([t.Number(), t.Null()]),
|
||||
id: t.Number(),
|
||||
aid: t.Number(),
|
||||
publishedAt: t.Union([t.String(), t.Null()]),
|
||||
createdAt: t.Union([t.String(), t.Null()]),
|
||||
description: t.Union([t.String(), t.Null()]),
|
||||
bvid: t.Union([t.String(), t.Null()]),
|
||||
uid: t.Union([t.Number(), t.Null()]),
|
||||
tags: t.Union([t.String(), t.Null()]),
|
||||
title: t.Union([t.String(), t.Null()]),
|
||||
status: t.Number(),
|
||||
coverUrl: t.Union([t.String(), t.Null()]),
|
||||
views: t.Number()
|
||||
});
|
||||
|
||||
const SongDataSchema = t.Object({
|
||||
duration: t.Union([t.Number(), t.Null()]),
|
||||
name: t.Union([t.String(), t.Null()]),
|
||||
id: t.Number(),
|
||||
aid: t.Union([t.Number(), t.Null()]),
|
||||
publishedAt: t.Union([t.String(), t.Null()]),
|
||||
type: t.Union([t.Number(), t.Null()]),
|
||||
neteaseId: t.Union([t.Number(), t.Null()]),
|
||||
createdAt: t.String(),
|
||||
updatedAt: t.String(),
|
||||
deleted: t.Boolean(),
|
||||
image: t.Union([t.String(), t.Null()]),
|
||||
producer: t.Union([t.String(), t.Null()])
|
||||
const BiliVideoDataSchema = BiliVideoSchema.extend({
|
||||
views: z.number()
|
||||
});
|
||||
|
||||
export const searchHandler = new Elysia({ prefix: "/search" }).get(
|
||||
@ -136,34 +111,31 @@ export const searchHandler = new Elysia({ prefix: "/search" }).get(
|
||||
getVideoSearchResult(searchQuery)
|
||||
]);
|
||||
|
||||
const combinedResults = [
|
||||
...songResults,
|
||||
...videoResults
|
||||
];
|
||||
const combinedResults = [...songResults, ...videoResults];
|
||||
return combinedResults.sort((a, b) => b.rank - a.rank);
|
||||
},
|
||||
{
|
||||
response: {
|
||||
200: t.Array(
|
||||
t.Union([
|
||||
t.Object({
|
||||
type: t.Literal("song"),
|
||||
data: SongDataSchema,
|
||||
rank: t.Number()
|
||||
200: z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
type: z.literal("song"),
|
||||
data: SongSchema,
|
||||
rank: z.number()
|
||||
}),
|
||||
t.Object({
|
||||
type: t.Literal("bili-video"),
|
||||
z.object({
|
||||
type: z.literal("bili-video"),
|
||||
data: BiliVideoDataSchema,
|
||||
rank: t.Number()
|
||||
rank: z.number()
|
||||
})
|
||||
])
|
||||
),
|
||||
404: t.Object({
|
||||
message: t.String()
|
||||
404: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
},
|
||||
query: t.Object({
|
||||
query: t.String()
|
||||
query: z.object({
|
||||
query: z.string()
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
@ -66,12 +66,14 @@ export const songInfoHandler = new Elysia({ prefix: "/song" })
|
||||
const songID = await getSongID(id);
|
||||
if (!songID) {
|
||||
return status(404, {
|
||||
code: "SONG_NOT_FOUND",
|
||||
message: "Given song cannot be found."
|
||||
});
|
||||
}
|
||||
const info = await getSongInfo(songID);
|
||||
if (!info) {
|
||||
return status(404, {
|
||||
code: "SONG_NOT_FOUND",
|
||||
message: "Given song cannot be found."
|
||||
});
|
||||
}
|
||||
@ -117,12 +119,14 @@ export const songInfoHandler = new Elysia({ prefix: "/song" })
|
||||
const songID = await getSongID(id);
|
||||
if (!songID) {
|
||||
return status(404, {
|
||||
code: "SONG_NOT_FOUND",
|
||||
message: "Given song cannot be found."
|
||||
});
|
||||
}
|
||||
const info = await getSongInfo(songID);
|
||||
if (!info) {
|
||||
return status(404, {
|
||||
code: "SONG_NOT_FOUND",
|
||||
message: "Given song cannot be found."
|
||||
});
|
||||
}
|
||||
@ -152,7 +156,8 @@ export const songInfoHandler = new Elysia({ prefix: "/song" })
|
||||
updated: t.Any()
|
||||
}),
|
||||
404: t.Object({
|
||||
message: t.String()
|
||||
message: t.String(),
|
||||
code: t.String()
|
||||
})
|
||||
},
|
||||
body: t.Object({
|
||||
|
||||
56
packages/elysia/routes/video/snapshots.ts
Normal file
56
packages/elysia/routes/video/snapshots.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { Elysia } from "elysia";
|
||||
import { dbMain } from "@core/drizzle";
|
||||
import { videoSnapshot } from "@core/drizzle/main/schema";
|
||||
import { bv2av } from "@elysia/lib/av_bv";
|
||||
import { ErrorResponseSchema } from "@elysia/src/schema";
|
||||
import { eq, desc } from "drizzle-orm";
|
||||
import z from "zod";
|
||||
|
||||
export const getVideoSnapshotsHandler = new Elysia({ prefix: "/video" }).get(
|
||||
"/:id/snapshots",
|
||||
async (c) => {
|
||||
const id = c.params.id;
|
||||
let aid: number | null = null;
|
||||
|
||||
if (id.startsWith("BV1")) {
|
||||
aid = bv2av(id as `BV1${string}`);
|
||||
} else if (id.startsWith("av")) {
|
||||
aid = Number.parseInt(id.slice(2));
|
||||
} else {
|
||||
return c.status(400, {
|
||||
code: "MALFORMED_SLOT",
|
||||
message:
|
||||
"We cannot parse the video ID, or we currently do not support this format.",
|
||||
errors: []
|
||||
});
|
||||
}
|
||||
|
||||
const data = await dbMain
|
||||
.select()
|
||||
.from(videoSnapshot)
|
||||
.where(eq(videoSnapshot.aid, aid))
|
||||
.orderBy(desc(videoSnapshot.createdAt));
|
||||
|
||||
return data;
|
||||
},
|
||||
{
|
||||
response: {
|
||||
200: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
createdAt: z.string(),
|
||||
views: z.number(),
|
||||
coins: z.number().nullable(),
|
||||
likes: z.number().nullable(),
|
||||
favorites: z.number().nullable(),
|
||||
shares: z.number().nullable(),
|
||||
danmakus: z.number().nullable(),
|
||||
aid: z.number(),
|
||||
replies: z.number().nullable()
|
||||
})
|
||||
),
|
||||
400: ErrorResponseSchema,
|
||||
500: ErrorResponseSchema
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -10,6 +10,7 @@ import { closeMileStoneHandler } from "@elysia/routes/song/milestone";
|
||||
import { authHandler } from "@elysia/routes/auth";
|
||||
import { onAfterHandler } from "./onAfterHandle";
|
||||
import { searchHandler } from "@elysia/routes/search";
|
||||
import { getVideoSnapshotsHandler } from "@elysia/routes/video/snapshots";
|
||||
|
||||
const [host, port] = getBindingInfo();
|
||||
logStartup(host, port);
|
||||
@ -29,6 +30,7 @@ const app = new Elysia({
|
||||
.use(songInfoHandler)
|
||||
.use(closeMileStoneHandler)
|
||||
.use(searchHandler)
|
||||
.use(getVideoSnapshotsHandler)
|
||||
.listen(15412);
|
||||
|
||||
export const VERSION = "0.7.0";
|
||||
|
||||
141
packages/temp_frontend/app/components/ui/dialog.tsx
Normal file
141
packages/temp_frontend/app/components/ui/dialog.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib//utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
@ -8,32 +8,106 @@ import { Title } from "@/components/Title";
|
||||
import { Search } from "@/components/Search";
|
||||
import { Error } from "@/components/Error";
|
||||
import { Layout } from "@/components/Layout";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
const app = treaty<App>(import.meta.env.VITE_API_URL!);
|
||||
|
||||
type SongInfo = Awaited<ReturnType<ReturnType<typeof app.song>["info"]["get"]>>["data"];
|
||||
type Snapshots = Awaited<ReturnType<ReturnType<typeof app.video>["snapshots"]["get"]>>["data"];
|
||||
type SongInfoError = Awaited<ReturnType<ReturnType<typeof app.song>["info"]["get"]>>["error"];
|
||||
type SnapshotsError = Awaited<ReturnType<ReturnType<typeof app.video>["snapshots"]["get"]>>["error"];
|
||||
|
||||
export async function clientLoader({ params }: Route.LoaderArgs) {
|
||||
return { id: params.id };
|
||||
}
|
||||
|
||||
const SnapshotsView = ({ snapshots }: { snapshots: Snapshots | null }) => {
|
||||
if (!snapshots) {
|
||||
return (
|
||||
<>
|
||||
<h2 className="mt-6 text-2xl font-medium mb-4">历史快照</h2>
|
||||
<Skeleton className="w-full h-20 rounded-lg" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h2 className="mt-6 text-2xl font-medium mb-4">历史快照</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left pr-4">日期</th>
|
||||
<th className="text-left pr-4">播放量</th>
|
||||
<th className="text-left pr-4">弹幕数</th>
|
||||
<th className="text-left pr-4">点赞数</th>
|
||||
<th className="text-left pr-4">收藏数</th>
|
||||
<th className="text-left pr-4">硬币数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{snapshots.map((snapshot: Exclude<Snapshots, null>[number]) => (
|
||||
<tr key={snapshot.id}>
|
||||
<td className="pr-4">{new Date(snapshot.createdAt).toLocaleDateString()}</td>
|
||||
<td className="pr-4">{snapshot.views}</td>
|
||||
<td className="pr-4">{snapshot.danmakus}</td>
|
||||
<td className="pr-4">{snapshot.likes}</td>
|
||||
<td className="pr-4">{snapshot.favorites}</td>
|
||||
<td className="pr-4">{snapshot.coins}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function SongInfo({ loaderData }: Route.ComponentProps) {
|
||||
const [data, setData] = useState<SongInfo | null>(null);
|
||||
const [error, setError] = useState<SongInfoError | null>(null);
|
||||
const [snapshots, setSnapshots] = useState<Snapshots | null>(null);
|
||||
const [error, setError] = useState<SongInfoError | SnapshotsError | null>(null);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [songName, setSongName] = useState("");
|
||||
|
||||
const getSnapshots = async (aid: number) => {
|
||||
const { data, error } = await app.video({ id: `av${aid}` }).snapshots.get();
|
||||
if (error) {
|
||||
console.log(error);
|
||||
setError(error);
|
||||
return;
|
||||
}
|
||||
setSnapshots(data);
|
||||
};
|
||||
|
||||
const getInfo = async () => {
|
||||
const { data, error } = await app.song({ id: loaderData.id }).info.get();
|
||||
if (error) {
|
||||
console.log(error);
|
||||
setError(error);
|
||||
return;
|
||||
}
|
||||
setData(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { data, error } = await app.song({ id: loaderData.id }).info.get();
|
||||
if (error) {
|
||||
console.log(error);
|
||||
setError(error);
|
||||
return;
|
||||
}
|
||||
setData(data);
|
||||
})();
|
||||
getInfo();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
const aid = data.aid;
|
||||
if (!aid) return;
|
||||
getSnapshots(aid);
|
||||
}, [data]);
|
||||
|
||||
// Update local song name when data changes
|
||||
useEffect(() => {
|
||||
if (data?.name) {
|
||||
setSongName(data.name);
|
||||
}
|
||||
}, [data?.name]);
|
||||
|
||||
if (!data && !error) {
|
||||
return (
|
||||
<Layout>
|
||||
@ -56,7 +130,9 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) {
|
||||
<TriangleAlert size={34} className="-translate-y-0.5" />
|
||||
</div>
|
||||
<h1 className="text-3xl font-semibold text-neutral-900 dark:text-neutral-100">无法找到曲目</h1>
|
||||
<a href={`/song/${loaderData.id}/add`} className="text-secondary-foreground">点此收录</a>
|
||||
<a href={`/song/${loaderData.id}/add`} className="text-secondary-foreground">
|
||||
点此收录
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -72,9 +148,13 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) {
|
||||
return `${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const songNameOnChange = async (e: React.FocusEvent<HTMLHeadingElement, Element>) => {
|
||||
const name = e.target.textContent;
|
||||
await app.song({ id: loaderData.id }).info.patch({ name: name || undefined });
|
||||
const handleSongNameChange = async () => {
|
||||
if (songName.trim() === "") return;
|
||||
|
||||
await app.song({ id: loaderData.id }).info.patch({ name: songName });
|
||||
setIsDialogOpen(false);
|
||||
// Refresh the data to show the updated name
|
||||
getInfo();
|
||||
};
|
||||
|
||||
return (
|
||||
@ -93,9 +173,32 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) {
|
||||
/>
|
||||
)}
|
||||
<div className="mt-6 flex justify-between">
|
||||
<h1 className="text-4xl font-medium" contentEditable={true} onBlur={songNameOnChange}>
|
||||
{data!.name ? data!.name : "未知歌曲名"}
|
||||
</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-4xl font-medium" onDoubleClick={() => setIsDialogOpen(true)}>
|
||||
{data!.name ? data!.name : "未知歌曲名"}
|
||||
</h1>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑歌曲名称</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
value={songName}
|
||||
onChange={(e) => setSongName(e.target.value)}
|
||||
placeholder="请输入歌曲名称"
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSongNameChange}>保存</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<div className="flex flex-col items-end h-10 whitespace-nowrap">
|
||||
<span className="leading-5 text-neutral-800 dark:text-neutral-200">
|
||||
{data!.duration ? formatDuration(data!.duration) : "未知时长"}
|
||||
@ -105,6 +208,7 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<SnapshotsView snapshots={snapshots} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
"@elysiajs/eden": "^1.4.1",
|
||||
"@nivo/core": "^0.99.0",
|
||||
"@nivo/line": "^0.99.0",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@react-router/node": "^7.7.1",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user