feat: editor support aplayer (#349)
This commit is contained in:
parent
e7ee565aa8
commit
5104a2c624
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
}
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 ?? "",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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\n`,
|
||||
),
|
||||
)
|
||||
if (file.type.startsWith("image/")) {
|
||||
view?.dispatch(
|
||||
view.state.replaceSelection(
|
||||
`\n\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 })
|
||||
|
|
|
|||
Loading…
Reference in New Issue