feat: editor support aplayer (#349)

This commit is contained in:
Steve Yu 2023-04-17 07:57:50 +08:00 committed by GitHub
parent e7ee565aa8
commit 5104a2c624
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 188 additions and 7 deletions

View File

@ -87,6 +87,7 @@ module.exports = withBundleAnalyzer(
productionBrowserSourceMaps: true,
webpack(config) {
config.resolve.fallback = { fs: false } // polyfill node-id3
config.plugins.push(new UnoCSS())
return config
},

View File

@ -56,6 +56,8 @@
"@uiw/codemirror-extensions-events": "4.19.16",
"@uiw/react-codemirror": "4.19.16",
"@urql/core": "4.0.4",
"aplayer": "^1.10.1",
"aplayer-react": "^1.0.5",
"axios": "1.3.5",
"canvas-confetti": "^1.6.0",
"chroma-js": "^2.4.2",
@ -83,6 +85,7 @@
"next": "13.2.4",
"next-i18next": "^13.2.2",
"nextjs-progressbar": "0.0.16",
"node-id3": "^0.2.6",
"pinyin": "3.0.0-alpha.5",
"prismjs": "1.29.0",
"react": "18.2.0",

View File

@ -91,6 +91,12 @@ dependencies:
'@urql/core':
specifier: 4.0.4
version: 4.0.4(graphql@16.6.0)
aplayer:
specifier: ^1.10.1
version: 1.10.1
aplayer-react:
specifier: ^1.0.5
version: 1.0.5(aplayer@1.10.1)(react@18.2.0)
axios:
specifier: 1.3.5
version: 1.3.5
@ -172,6 +178,9 @@ dependencies:
nextjs-progressbar:
specifier: 0.0.16
version: 0.0.16(next@13.2.4)(react@18.2.0)
node-id3:
specifier: ^0.2.6
version: 0.2.6
pinyin:
specifier: 3.0.0-alpha.5
version: 3.0.0-alpha.5
@ -5646,6 +5655,27 @@ packages:
normalize-path: 3.0.0
picomatch: 2.3.1
/aplayer-react@1.0.5(aplayer@1.10.1)(react@18.2.0):
resolution: {integrity: sha512-iVMOJmI+NxY2AIgTkZlhVyiesLYR1Oqmyp2pUjPVhZpvzZjPKshLralGsTUy4gQlDs0X4yahSzHK7+TA5vA9KQ==}
peerDependencies:
aplayer: ^1.10.1
react: ^18.2.0
dependencies:
aplayer: 1.10.1
clsx: 1.2.1
fast-average-color: 9.3.0
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/aplayer@1.10.1:
resolution: {integrity: sha512-HAfyxgCUTLAqtYlxzzK9Fyqg6y+kZ9CqT1WfeWE8FSzwspT6oBqWOZHANPHF3RGTtC33IsyEgrfthPDzU5r9kQ==}
dependencies:
balloon-css: 0.5.2
promise-polyfill: 7.1.0
smoothscroll: 0.4.0
dev: false
/aproba@2.0.0:
resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==}
dev: false
@ -5963,6 +5993,10 @@ packages:
/balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
/balloon-css@0.5.2:
resolution: {integrity: sha512-zheJpzwyNrG4t39vusA67v3BYg1HTVXOF8cErPEHzWK88PEOFwgo6Ea9VHOgOWNMgeuOtFVtB73NE2NWl9uDyQ==}
dev: false
/base-x@3.0.9:
resolution: {integrity: sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==}
dependencies:
@ -8955,6 +8989,13 @@ packages:
'@babel/runtime': 7.21.0
dev: false
/iconv-lite@0.6.2:
resolution: {integrity: sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==}
engines: {node: '>=0.10.0'}
dependencies:
safer-buffer: 2.1.2
dev: false
/iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@ -10895,6 +10936,12 @@ packages:
hasBin: true
dev: false
/node-id3@0.2.6:
resolution: {integrity: sha512-w8GuKXLlPpDjTxLowCt/uYMhRQzED3cg2GdSG1i6RSGKeDzPvxlXeLQuQInKljahPZ0aDnmyX7FX8BbJOM7REg==}
dependencies:
iconv-lite: 0.6.2
dev: false
/node-releases@2.0.10:
resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==}
@ -11579,6 +11626,10 @@ packages:
resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==}
dev: false
/promise-polyfill@7.1.0:
resolution: {integrity: sha512-P6NJ2wU/8fac44ENORsuqT8TiolKGB2u0fEClPtXezn7w5cmLIjM/7mhPlTebke2EPr6tmqZbXvnX0TxwykGrg==}
dev: false
/prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
dependencies:
@ -12718,6 +12769,10 @@ packages:
is-fullwidth-code-point: 4.0.0
dev: true
/smoothscroll@0.4.0:
resolution: {integrity: sha512-sggQ3U2Un38b3+q/j1P4Y4fCboCtoUIaBYoge+Lb6Xg1H8RTIif/hugVr+ErMtIDpvBbhQfTjtiTeYAfbw1ZGQ==}
dev: false
/sonic-boom@2.8.0:
resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==}
dependencies:

View File

@ -0,0 +1,25 @@
import { AudioInfo } from "aplayer-react"
import { APlayer as AplayerReact } from "aplayer-react"
import { useEffect, useState } from "react"
export const APlayer: React.FC<
{
children: string[]
src: string
} & React.HTMLAttributes<HTMLAudioElement>
> = ({ src, children }) => {
const [audio, setAudio] = useState<AudioInfo>()
useEffect(() => {
if (!Array.isArray(children) || children.length < 3) {
throw new Error("Aplayer params error, exactly 3 parameters required")
}
const [artist, name, cover] = children as Array<string>
setAudio({ artist, name, cover, url: src })
}, [])
if (!audio) {
return null
} else {
return <AplayerReact audio={audio} />
}
}

View File

@ -9,6 +9,7 @@ import rehypeRaw from "rehype-raw"
import { refractor } from "refractor"
import rehypePrismGenerator from "rehype-prism-plus/generator"
import { rehypeImage } from "./rehype-image"
import { rehypeAudio } from "./rehype-audio"
import { rehypeTable } from "./rehype-table"
import { remarkCallout } from "./remark-callout"
import { rehypeExternalLink } from "./rehyper-external-link"
@ -35,6 +36,7 @@ import { remarkMermaid } from "./remark-mermaid"
import { Mermaid } from "~/components/ui/Mermaid"
import rehypeRewrite from "rehype-rewrite"
import { toText } from "hast-util-to-text"
import { APlayer } from "~/components/ui/APlayer"
export type MarkdownEnv = {
excerpt: string
@ -109,6 +111,7 @@ export const renderPageContent = (
.use(rehypeStringify)
.use(rehypeRaw)
.use(rehypeImage, { env })
.use(rehypeAudio)
.use(rehypeSanitize, sanitizeScheme)
.use(rehypePrism, {
ignoreMissing: true,
@ -181,6 +184,7 @@ export const renderPageContent = (
anchor: Element,
mention: Mention,
mermaid: Mermaid,
audio: APlayer,
} as any,
})
.use(() => (tree) => {

View File

@ -0,0 +1,58 @@
import { Root } from "rehype-raw"
import { Plugin } from "unified"
import { visit } from "unist-util-visit"
import { getUserContentsUrl } from "~/lib/user-contents"
import { toGateway } from "~/lib/ipfs-parser"
export const rehypeAudio: Plugin<Array<void>, Root> = () => {
return (tree) => {
visit(tree, { tagName: "audio" }, (node, i, parent) => {
if (!node.properties) {
return
}
const src = node.properties.src
if (!src || typeof src !== "string") {
return
}
let url = src
const ipfsUrl = toGateway(url)
node.properties.src = ipfsUrl
url = ipfsUrl
node.properties.src = getUserContentsUrl(url)
if (parent) {
const coverRaw = node.properties.cover
const cover =
typeof coverRaw === "string" ? toGateway(coverRaw) : undefined
parent.children[i!] = {
type: "element",
tagName: "audio",
properties: {
src: url,
},
// children array format:
// artist - name - cover
children: [
{
type: "text",
value: `${node.properties.artist}`,
},
{
type: "text",
value: `${node.properties.name}`,
},
{
type: "text",
value: cover ?? "",
},
],
}
}
})
}
}

View File

@ -1,5 +1,6 @@
import "~/css/main.css"
import "~/generated/uno.css"
import "aplayer/dist/APlayer.min.css"
import { Toaster } from "react-hot-toast"
import { createClient, WagmiConfig } from "wagmi"

View File

@ -44,6 +44,7 @@ import { serverSidePropsHandler } from "~/lib/server-side-props"
import { getDefaultSlug } from "~/lib/default-slug"
import { useMobileLayout } from "~/hooks/useMobileLayout"
import { OptionsButton } from "~/components/dashboard/OptionsButton"
import NodeID3 from "node-id3"
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
async (ctx) => {
@ -391,19 +392,52 @@ export default function SubdomainEditor() {
async (file: File) => {
const toastId = toast.loading("Uploading...")
try {
if (!file.type.startsWith("image/")) {
throw new Error("You can only upload images")
if (
!file.type.startsWith("image/") &&
!file.type.startsWith("audio/")
) {
throw new Error("You can only upload images or audios")
}
const { key } = await uploadFile(file)
toast.success("Uploaded!", {
id: toastId,
})
view?.dispatch(
view.state.replaceSelection(
`\n![${file.name.replace(/\.\w+$/, "")}](${key})\n`,
),
)
if (file.type.startsWith("image/")) {
view?.dispatch(
view.state.replaceSelection(
`\n![${file.name.replace(/\.\w+$/, "")}](${key})\n`,
),
)
} else if (file.type.startsWith("audio/")) {
const fileArrayBuffer = await file.arrayBuffer()
const fileBuffer = Buffer.from(fileArrayBuffer)
const tags = NodeID3.read(fileBuffer)
const name = tags.title ?? file.name
const artist = tags.artist
const cover = await (async () => {
const image = tags.image
if (!image || typeof image === "string") return image
const toastId = toast.loading("Uploading cover...")
const { key } = await uploadFile(
new Blob([image.imageBuffer], { type: image.type.name }),
)
toast.success("Uploaded cover!", {
id: toastId,
})
return key
})()
view?.dispatch(
view.state.replaceSelection(
`\n<audio src="${key}" name="${name}" ${
artist ? `artist="${artist}"` : ""
} ${cover ? `cover="${cover}"` : ""}><audio>\n`,
),
)
} else {
throw new Error("Unknown upload file type")
}
} catch (error) {
if (error instanceof Error) {
toast.error(error.message, { id: toastId })