Spaces:
Sleeping
Sleeping
Julian Bilcke
commited on
Commit
·
f8ca042
1
Parent(s):
da6f0c4
AiTube Music
Browse files- package-lock.json +24 -21
- package.json +4 -4
- src/app/interface/left-menu/index.tsx +0 -2
- src/app/interface/media-list/index.tsx +4 -0
- src/app/interface/playlist-control/index.tsx +38 -0
- src/app/interface/top-header/index.tsx +2 -2
- src/app/interface/track-card/index.tsx +14 -14
- src/app/interface/tube-layout/index.tsx +1 -1
- src/app/interface/video-card/index.tsx +2 -0
- src/app/views/public-music-videos-view/index.tsx +22 -10
- src/lib/useAudio.ts +0 -152
- src/lib/usePlaylist.ts +173 -0
package-lock.json
CHANGED
|
@@ -61,16 +61,16 @@
|
|
| 61 |
"sentence-splitter": "^4.3.0",
|
| 62 |
"sharp": "^0.32.5",
|
| 63 |
"styled-components": "^6.0.7",
|
| 64 |
-
"tailwind-merge": "^1.
|
| 65 |
-
"tailwindcss": "3.
|
| 66 |
-
"tailwindcss-animate": "^1.0.
|
| 67 |
"temp-dir": "^3.0.0",
|
| 68 |
"ts-node": "^10.9.1",
|
| 69 |
"type-fest": "^4.8.2",
|
| 70 |
"typescript": "5.1.6",
|
| 71 |
"usehooks-ts": "^2.9.1",
|
| 72 |
"uuid": "^9.0.1",
|
| 73 |
-
"zustand": "^4.4.
|
| 74 |
},
|
| 75 |
"devDependencies": {
|
| 76 |
"@types/proper-lockfile": "^4.1.2",
|
|
@@ -1923,9 +1923,9 @@
|
|
| 1923 |
}
|
| 1924 |
},
|
| 1925 |
"node_modules/@upstash/redis": {
|
| 1926 |
-
"version": "1.
|
| 1927 |
-
"resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.
|
| 1928 |
-
"integrity": "sha512-
|
| 1929 |
"dependencies": {
|
| 1930 |
"crypto-js": "^4.2.0"
|
| 1931 |
}
|
|
@@ -3162,9 +3162,9 @@
|
|
| 3162 |
}
|
| 3163 |
},
|
| 3164 |
"node_modules/electron-to-chromium": {
|
| 3165 |
-
"version": "1.4.
|
| 3166 |
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.
|
| 3167 |
-
"integrity": "sha512
|
| 3168 |
},
|
| 3169 |
"node_modules/emoji-regex": {
|
| 3170 |
"version": "9.2.2",
|
|
@@ -3789,9 +3789,9 @@
|
|
| 3789 |
"dev": true
|
| 3790 |
},
|
| 3791 |
"node_modules/fastq": {
|
| 3792 |
-
"version": "1.
|
| 3793 |
-
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.
|
| 3794 |
-
"integrity": "sha512-
|
| 3795 |
"dependencies": {
|
| 3796 |
"reusify": "^1.0.4"
|
| 3797 |
}
|
|
@@ -6419,28 +6419,31 @@
|
|
| 6419 |
}
|
| 6420 |
},
|
| 6421 |
"node_modules/tailwind-merge": {
|
| 6422 |
-
"version": "1.
|
| 6423 |
-
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.
|
| 6424 |
-
"integrity": "sha512-
|
|
|
|
|
|
|
|
|
|
| 6425 |
"funding": {
|
| 6426 |
"type": "github",
|
| 6427 |
"url": "https://github.com/sponsors/dcastil"
|
| 6428 |
}
|
| 6429 |
},
|
| 6430 |
"node_modules/tailwindcss": {
|
| 6431 |
-
"version": "3.
|
| 6432 |
-
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.
|
| 6433 |
-
"integrity": "sha512-
|
| 6434 |
"dependencies": {
|
| 6435 |
"@alloc/quick-lru": "^5.2.0",
|
| 6436 |
"arg": "^5.0.2",
|
| 6437 |
"chokidar": "^3.5.3",
|
| 6438 |
"didyoumean": "^1.2.2",
|
| 6439 |
"dlv": "^1.1.3",
|
| 6440 |
-
"fast-glob": "^3.
|
| 6441 |
"glob-parent": "^6.0.2",
|
| 6442 |
"is-glob": "^4.0.3",
|
| 6443 |
-
"jiti": "^1.
|
| 6444 |
"lilconfig": "^2.1.0",
|
| 6445 |
"micromatch": "^4.0.5",
|
| 6446 |
"normalize-path": "^3.0.0",
|
|
|
|
| 61 |
"sentence-splitter": "^4.3.0",
|
| 62 |
"sharp": "^0.32.5",
|
| 63 |
"styled-components": "^6.0.7",
|
| 64 |
+
"tailwind-merge": "^2.1.0",
|
| 65 |
+
"tailwindcss": "3.4.0",
|
| 66 |
+
"tailwindcss-animate": "^1.0.7",
|
| 67 |
"temp-dir": "^3.0.0",
|
| 68 |
"ts-node": "^10.9.1",
|
| 69 |
"type-fest": "^4.8.2",
|
| 70 |
"typescript": "5.1.6",
|
| 71 |
"usehooks-ts": "^2.9.1",
|
| 72 |
"uuid": "^9.0.1",
|
| 73 |
+
"zustand": "^4.4.7"
|
| 74 |
},
|
| 75 |
"devDependencies": {
|
| 76 |
"@types/proper-lockfile": "^4.1.2",
|
|
|
|
| 1923 |
}
|
| 1924 |
},
|
| 1925 |
"node_modules/@upstash/redis": {
|
| 1926 |
+
"version": "1.27.1",
|
| 1927 |
+
"resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.27.1.tgz",
|
| 1928 |
+
"integrity": "sha512-K9UgTBypJ4Dx65s2u5auoyf/5YoCQjaN91QtxlkNg+3g0rqXXy4ELtzACstk1v+bTa547Mm3rzTjotDX/s9+Zg==",
|
| 1929 |
"dependencies": {
|
| 1930 |
"crypto-js": "^4.2.0"
|
| 1931 |
}
|
|
|
|
| 3162 |
}
|
| 3163 |
},
|
| 3164 |
"node_modules/electron-to-chromium": {
|
| 3165 |
+
"version": "1.4.615",
|
| 3166 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.615.tgz",
|
| 3167 |
+
"integrity": "sha512-/bKPPcgZVUziECqDc+0HkT87+0zhaWSZHNXqF8FLd2lQcptpmUFwoCSWjCdOng9Gdq+afKArPdEg/0ZW461Eng=="
|
| 3168 |
},
|
| 3169 |
"node_modules/emoji-regex": {
|
| 3170 |
"version": "9.2.2",
|
|
|
|
| 3789 |
"dev": true
|
| 3790 |
},
|
| 3791 |
"node_modules/fastq": {
|
| 3792 |
+
"version": "1.16.0",
|
| 3793 |
+
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz",
|
| 3794 |
+
"integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==",
|
| 3795 |
"dependencies": {
|
| 3796 |
"reusify": "^1.0.4"
|
| 3797 |
}
|
|
|
|
| 6419 |
}
|
| 6420 |
},
|
| 6421 |
"node_modules/tailwind-merge": {
|
| 6422 |
+
"version": "2.1.0",
|
| 6423 |
+
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.1.0.tgz",
|
| 6424 |
+
"integrity": "sha512-l11VvI4nSwW7MtLSLYT4ldidDEUwQAMWuSHk7l4zcXZDgnCRa0V3OdCwFfM7DCzakVXMNRwAeje9maFFXT71dQ==",
|
| 6425 |
+
"dependencies": {
|
| 6426 |
+
"@babel/runtime": "^7.23.5"
|
| 6427 |
+
},
|
| 6428 |
"funding": {
|
| 6429 |
"type": "github",
|
| 6430 |
"url": "https://github.com/sponsors/dcastil"
|
| 6431 |
}
|
| 6432 |
},
|
| 6433 |
"node_modules/tailwindcss": {
|
| 6434 |
+
"version": "3.4.0",
|
| 6435 |
+
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz",
|
| 6436 |
+
"integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==",
|
| 6437 |
"dependencies": {
|
| 6438 |
"@alloc/quick-lru": "^5.2.0",
|
| 6439 |
"arg": "^5.0.2",
|
| 6440 |
"chokidar": "^3.5.3",
|
| 6441 |
"didyoumean": "^1.2.2",
|
| 6442 |
"dlv": "^1.1.3",
|
| 6443 |
+
"fast-glob": "^3.3.0",
|
| 6444 |
"glob-parent": "^6.0.2",
|
| 6445 |
"is-glob": "^4.0.3",
|
| 6446 |
+
"jiti": "^1.19.1",
|
| 6447 |
"lilconfig": "^2.1.0",
|
| 6448 |
"micromatch": "^4.0.5",
|
| 6449 |
"normalize-path": "^3.0.0",
|
package.json
CHANGED
|
@@ -62,16 +62,16 @@
|
|
| 62 |
"sentence-splitter": "^4.3.0",
|
| 63 |
"sharp": "^0.32.5",
|
| 64 |
"styled-components": "^6.0.7",
|
| 65 |
-
"tailwind-merge": "^1.
|
| 66 |
-
"tailwindcss": "3.
|
| 67 |
-
"tailwindcss-animate": "^1.0.
|
| 68 |
"temp-dir": "^3.0.0",
|
| 69 |
"ts-node": "^10.9.1",
|
| 70 |
"type-fest": "^4.8.2",
|
| 71 |
"typescript": "5.1.6",
|
| 72 |
"usehooks-ts": "^2.9.1",
|
| 73 |
"uuid": "^9.0.1",
|
| 74 |
-
"zustand": "^4.4.
|
| 75 |
},
|
| 76 |
"devDependencies": {
|
| 77 |
"@types/proper-lockfile": "^4.1.2",
|
|
|
|
| 62 |
"sentence-splitter": "^4.3.0",
|
| 63 |
"sharp": "^0.32.5",
|
| 64 |
"styled-components": "^6.0.7",
|
| 65 |
+
"tailwind-merge": "^2.1.0",
|
| 66 |
+
"tailwindcss": "3.4.0",
|
| 67 |
+
"tailwindcss-animate": "^1.0.7",
|
| 68 |
"temp-dir": "^3.0.0",
|
| 69 |
"ts-node": "^10.9.1",
|
| 70 |
"type-fest": "^4.8.2",
|
| 71 |
"typescript": "5.1.6",
|
| 72 |
"usehooks-ts": "^2.9.1",
|
| 73 |
"uuid": "^9.0.1",
|
| 74 |
+
"zustand": "^4.4.7"
|
| 75 |
},
|
| 76 |
"devDependencies": {
|
| 77 |
"@types/proper-lockfile": "^4.1.2",
|
src/app/interface/left-menu/index.tsx
CHANGED
|
@@ -47,7 +47,6 @@ export function LeftMenu() {
|
|
| 47 |
Channels
|
| 48 |
</MenuItem>
|
| 49 |
</Link>
|
| 50 |
-
{/*
|
| 51 |
<Link href="/music">
|
| 52 |
<MenuItem
|
| 53 |
icon={<MdOutlinePlayCircleOutline className="h-6.5 w-6.5" />}
|
|
@@ -56,7 +55,6 @@ export function LeftMenu() {
|
|
| 56 |
Music
|
| 57 |
</MenuItem>
|
| 58 |
</Link>
|
| 59 |
-
*/}
|
| 60 |
</div>
|
| 61 |
<div className={cn(
|
| 62 |
`flex flex-col w-full`,
|
|
|
|
| 47 |
Channels
|
| 48 |
</MenuItem>
|
| 49 |
</Link>
|
|
|
|
| 50 |
<Link href="/music">
|
| 51 |
<MenuItem
|
| 52 |
icon={<MdOutlinePlayCircleOutline className="h-6.5 w-6.5" />}
|
|
|
|
| 55 |
Music
|
| 56 |
</MenuItem>
|
| 57 |
</Link>
|
|
|
|
| 58 |
</div>
|
| 59 |
<div className={cn(
|
| 60 |
`flex flex-col w-full`,
|
src/app/interface/media-list/index.tsx
CHANGED
|
@@ -9,6 +9,7 @@ export function MediaList({
|
|
| 9 |
layout = "grid",
|
| 10 |
className = "",
|
| 11 |
onSelect,
|
|
|
|
| 12 |
}: {
|
| 13 |
items: VideoInfo[]
|
| 14 |
|
|
@@ -31,6 +32,8 @@ export function MediaList({
|
|
| 31 |
className?: string
|
| 32 |
|
| 33 |
onSelect?: (media: VideoInfo) => void
|
|
|
|
|
|
|
| 34 |
}) {
|
| 35 |
|
| 36 |
return (
|
|
@@ -56,6 +59,7 @@ export function MediaList({
|
|
| 56 |
className="w-full"
|
| 57 |
layout={layout}
|
| 58 |
onSelect={onSelect}
|
|
|
|
| 59 |
index={i}
|
| 60 |
/>
|
| 61 |
)
|
|
|
|
| 9 |
layout = "grid",
|
| 10 |
className = "",
|
| 11 |
onSelect,
|
| 12 |
+
selectedId,
|
| 13 |
}: {
|
| 14 |
items: VideoInfo[]
|
| 15 |
|
|
|
|
| 32 |
className?: string
|
| 33 |
|
| 34 |
onSelect?: (media: VideoInfo) => void
|
| 35 |
+
|
| 36 |
+
selectedId?: string
|
| 37 |
}) {
|
| 38 |
|
| 39 |
return (
|
|
|
|
| 59 |
className="w-full"
|
| 60 |
layout={layout}
|
| 61 |
onSelect={onSelect}
|
| 62 |
+
selected={selectedId === media.id}
|
| 63 |
index={i}
|
| 64 |
/>
|
| 65 |
)
|
src/app/interface/playlist-control/index.tsx
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { IoIosPlay } from "react-icons/io"
|
| 2 |
+
import { IoIosPause } from "react-icons/io"
|
| 3 |
+
|
| 4 |
+
import { cn } from "@/lib/utils"
|
| 5 |
+
import { usePlaylist } from "@/lib/usePlaylist"
|
| 6 |
+
import { VideoInfo } from "@/types"
|
| 7 |
+
|
| 8 |
+
export function PlaylistControl() {
|
| 9 |
+
const playlist = usePlaylist()
|
| 10 |
+
|
| 11 |
+
return (
|
| 12 |
+
<div className="flex flex-row items-center justify-center bg-neutral-900 h-20 w-full">
|
| 13 |
+
{/* center buttons */}
|
| 14 |
+
<div className="flex flex-row items-center justify-center space-x-4">
|
| 15 |
+
|
| 16 |
+
{/*<div className="">{playlist.current?.label}</div>*/}
|
| 17 |
+
|
| 18 |
+
<div className={cn(
|
| 19 |
+
`flex flex-col items-center justify-center text-center`,
|
| 20 |
+
`size-16`,
|
| 21 |
+
`cursor-pointer`,
|
| 22 |
+
`transition-all duration-200 ease-in-out`,
|
| 23 |
+
`rounded-full border border-zinc-500 hover:border-zinc-400 hover:bg-zinc-800 text-zinc-400 hover:text-zinc-300`
|
| 24 |
+
)}
|
| 25 |
+
onClick={() => {
|
| 26 |
+
playlist.togglePause()
|
| 27 |
+
}}
|
| 28 |
+
>
|
| 29 |
+
{playlist.isPlaying
|
| 30 |
+
? <IoIosPause className="size-10" />
|
| 31 |
+
: <IoIosPlay className="pl-1 size-10" />
|
| 32 |
+
}
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
)
|
| 38 |
+
}
|
src/app/interface/top-header/index.tsx
CHANGED
|
@@ -37,7 +37,7 @@ export function TopHeader() {
|
|
| 37 |
|
| 38 |
|
| 39 |
useEffect(() => {
|
| 40 |
-
if (view === "public_video" || view === "public_channel") {
|
| 41 |
setHeaderMode("compact")
|
| 42 |
setMenuMode("slider_hidden")
|
| 43 |
} else {
|
|
@@ -92,7 +92,7 @@ export function TopHeader() {
|
|
| 92 |
`rounded-lg w-6 h-7`
|
| 93 |
)}>
|
| 94 |
<PiPopcornBold className={cn(
|
| 95 |
-
`
|
| 96 |
)} />
|
| 97 |
</div>
|
| 98 |
</div>
|
|
|
|
| 37 |
|
| 38 |
|
| 39 |
useEffect(() => {
|
| 40 |
+
if (view === "public_video" || view === "public_channel" || view === "public_music_videos") {
|
| 41 |
setHeaderMode("compact")
|
| 42 |
setMenuMode("slider_hidden")
|
| 43 |
} else {
|
|
|
|
| 92 |
`rounded-lg w-6 h-7`
|
| 93 |
)}>
|
| 94 |
<PiPopcornBold className={cn(
|
| 95 |
+
`size-5`
|
| 96 |
)} />
|
| 97 |
</div>
|
| 98 |
</div>
|
src/app/interface/track-card/index.tsx
CHANGED
|
@@ -12,18 +12,21 @@ import { isCertifiedUser } from "@/app/certification"
|
|
| 12 |
import { transparentImage } from "@/lib/transparentImage"
|
| 13 |
import { DefaultAvatar } from "../default-avatar"
|
| 14 |
import { formatLargeNumber } from "@/lib/formatLargeNumber"
|
|
|
|
| 15 |
|
| 16 |
export function TrackCard({
|
| 17 |
media,
|
| 18 |
className = "",
|
| 19 |
layout = "grid",
|
| 20 |
onSelect,
|
|
|
|
| 21 |
index
|
| 22 |
}: {
|
| 23 |
media: VideoInfo
|
| 24 |
className?: string
|
| 25 |
layout?: MediaDisplayLayout
|
| 26 |
onSelect?: (media: VideoInfo) => void
|
|
|
|
| 27 |
index: number
|
| 28 |
}) {
|
| 29 |
const ref = useRef<HTMLVideoElement>(null)
|
|
@@ -36,6 +39,8 @@ export function TrackCard({
|
|
| 36 |
const [mediaThumbnailReady, setMediaThumbnailReady] = useState(false)
|
| 37 |
const [shouldLoadMedia, setShouldLoadMedia] = useState(false)
|
| 38 |
|
|
|
|
|
|
|
| 39 |
const isTable = layout === "table"
|
| 40 |
const isMicro = layout === "micro"
|
| 41 |
const isCompact = layout === "vertical"
|
|
@@ -54,9 +59,6 @@ export function TrackCard({
|
|
| 54 |
}
|
| 55 |
}
|
| 56 |
|
| 57 |
-
const handleClick = () => {
|
| 58 |
-
onSelect?.(media)
|
| 59 |
-
}
|
| 60 |
|
| 61 |
const handleBadChannelThumbnail = () => {
|
| 62 |
try {
|
|
@@ -84,14 +86,16 @@ export function TrackCard({
|
|
| 84 |
`flex-col space-y-3`,
|
| 85 |
`bg-line-900`,
|
| 86 |
`cursor-pointer`,
|
|
|
|
| 87 |
(isTable || isMicro) ? (
|
| 88 |
-
(index % 2) ? "bg-
|
| 89 |
) : "",
|
|
|
|
| 90 |
className,
|
| 91 |
)}
|
| 92 |
onPointerEnter={handlePointerEnter}
|
| 93 |
onPointerLeave={handlePointerLeave}
|
| 94 |
-
|
| 95 |
>
|
| 96 |
{/* THUMBNAIL BLOCK */}
|
| 97 |
<div
|
|
@@ -111,9 +115,6 @@ export function TrackCard({
|
|
| 111 |
)}>
|
| 112 |
{!isTable && mediaThumbnailReady && shouldLoadMedia
|
| 113 |
? <video
|
| 114 |
-
// mute the video
|
| 115 |
-
muted
|
| 116 |
-
|
| 117 |
// prevent iOS from attempting to open the video in full screen, which is annoying
|
| 118 |
playsInline
|
| 119 |
|
|
@@ -135,7 +136,7 @@ export function TrackCard({
|
|
| 135 |
`aspect-square object-cover`,
|
| 136 |
`rounded-lg overflow-hidden`,
|
| 137 |
mediaThumbnailReady ? `opacity-100`: 'opacity-0',
|
| 138 |
-
`hover:
|
| 139 |
//`pointer-events-none`,
|
| 140 |
`transition-all duration-500 hover:delay-300 ease-in-out`,
|
| 141 |
)}
|
|
@@ -216,7 +217,7 @@ export function TrackCard({
|
|
| 216 |
<div className={cn(
|
| 217 |
`flex flex-row items-center`,
|
| 218 |
`text-neutral-400 font-normal space-x-1`,
|
| 219 |
-
isTable ? `text-2xs md:text-xs lg:text-sm` :
|
| 220 |
isCompact ? `text-3xs md:text-2xs lg:text-xs` : `text-sm`
|
| 221 |
)}>
|
| 222 |
<div>{media.channel.label}</div>
|
|
@@ -234,13 +235,12 @@ export function TrackCard({
|
|
| 234 |
<div>{formatTimeAgo(media.updatedAt)}</div>
|
| 235 |
</div>}
|
| 236 |
|
| 237 |
-
|
| 238 |
{isTable ? <div className={cn(
|
| 239 |
`hidden md:flex flex-row flex-grow`,
|
| 240 |
`text-zinc-100 mb-0 line-clamp-2`,
|
| 241 |
-
`
|
| 242 |
-
)}>{media.duration}</div> : null}
|
| 243 |
-
*/}
|
| 244 |
</div>
|
| 245 |
</div>
|
| 246 |
</div>
|
|
|
|
| 12 |
import { transparentImage } from "@/lib/transparentImage"
|
| 13 |
import { DefaultAvatar } from "../default-avatar"
|
| 14 |
import { formatLargeNumber } from "@/lib/formatLargeNumber"
|
| 15 |
+
import { usePlaylist } from "@/lib/usePlaylist"
|
| 16 |
|
| 17 |
export function TrackCard({
|
| 18 |
media,
|
| 19 |
className = "",
|
| 20 |
layout = "grid",
|
| 21 |
onSelect,
|
| 22 |
+
selected,
|
| 23 |
index
|
| 24 |
}: {
|
| 25 |
media: VideoInfo
|
| 26 |
className?: string
|
| 27 |
layout?: MediaDisplayLayout
|
| 28 |
onSelect?: (media: VideoInfo) => void
|
| 29 |
+
selected?: boolean
|
| 30 |
index: number
|
| 31 |
}) {
|
| 32 |
const ref = useRef<HTMLVideoElement>(null)
|
|
|
|
| 39 |
const [mediaThumbnailReady, setMediaThumbnailReady] = useState(false)
|
| 40 |
const [shouldLoadMedia, setShouldLoadMedia] = useState(false)
|
| 41 |
|
| 42 |
+
const playlist = usePlaylist()
|
| 43 |
+
|
| 44 |
const isTable = layout === "table"
|
| 45 |
const isMicro = layout === "micro"
|
| 46 |
const isCompact = layout === "vertical"
|
|
|
|
| 59 |
}
|
| 60 |
}
|
| 61 |
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
const handleBadChannelThumbnail = () => {
|
| 64 |
try {
|
|
|
|
| 86 |
`flex-col space-y-3`,
|
| 87 |
`bg-line-900`,
|
| 88 |
`cursor-pointer`,
|
| 89 |
+
`transition-all duration-200 ease-in-out`,
|
| 90 |
(isTable || isMicro) ? (
|
| 91 |
+
(index % 2) ? "bg-zinc-800/30 hover:bg-zinc-800/50" : "hover:bg-zinc-800/50"
|
| 92 |
) : "",
|
| 93 |
+
selected ? `border-2 border-zinc-400` : `border-2 border-transparent`,
|
| 94 |
className,
|
| 95 |
)}
|
| 96 |
onPointerEnter={handlePointerEnter}
|
| 97 |
onPointerLeave={handlePointerLeave}
|
| 98 |
+
onClick={() => onSelect?.(media)}
|
| 99 |
>
|
| 100 |
{/* THUMBNAIL BLOCK */}
|
| 101 |
<div
|
|
|
|
| 115 |
)}>
|
| 116 |
{!isTable && mediaThumbnailReady && shouldLoadMedia
|
| 117 |
? <video
|
|
|
|
|
|
|
|
|
|
| 118 |
// prevent iOS from attempting to open the video in full screen, which is annoying
|
| 119 |
playsInline
|
| 120 |
|
|
|
|
| 136 |
`aspect-square object-cover`,
|
| 137 |
`rounded-lg overflow-hidden`,
|
| 138 |
mediaThumbnailReady ? `opacity-100`: 'opacity-0',
|
| 139 |
+
`hover:brightness-110 w-full h-full top-0 z-30`,
|
| 140 |
//`pointer-events-none`,
|
| 141 |
`transition-all duration-500 hover:delay-300 ease-in-out`,
|
| 142 |
)}
|
|
|
|
| 217 |
<div className={cn(
|
| 218 |
`flex flex-row items-center`,
|
| 219 |
`text-neutral-400 font-normal space-x-1`,
|
| 220 |
+
isTable ? `w-[30%] text-2xs md:text-xs lg:text-sm` :
|
| 221 |
isCompact ? `text-3xs md:text-2xs lg:text-xs` : `text-sm`
|
| 222 |
)}>
|
| 223 |
<div>{media.channel.label}</div>
|
|
|
|
| 235 |
<div>{formatTimeAgo(media.updatedAt)}</div>
|
| 236 |
</div>}
|
| 237 |
|
| 238 |
+
|
| 239 |
{isTable ? <div className={cn(
|
| 240 |
`hidden md:flex flex-row flex-grow`,
|
| 241 |
`text-zinc-100 mb-0 line-clamp-2`,
|
| 242 |
+
`justify-end font-normal text-xs md:text-sm lg:text-base mb-0.5`
|
| 243 |
+
)}>{formatDuration(media.duration || 0)}</div> : null}
|
|
|
|
| 244 |
</div>
|
| 245 |
</div>
|
| 246 |
</div>
|
src/app/interface/tube-layout/index.tsx
CHANGED
|
@@ -24,7 +24,7 @@ export function TubeLayout({ children }: { children?: ReactNode }) {
|
|
| 24 |
<div className={cn(
|
| 25 |
`flex flex-col`,
|
| 26 |
`w-full sm:w-[calc(100vw-96px)]`,
|
| 27 |
-
`
|
| 28 |
)}>
|
| 29 |
<TopHeader />
|
| 30 |
<main className={cn(
|
|
|
|
| 24 |
<div className={cn(
|
| 25 |
`flex flex-col`,
|
| 26 |
`w-full sm:w-[calc(100vw-96px)]`,
|
| 27 |
+
`pl-2`
|
| 28 |
)}>
|
| 29 |
<TopHeader />
|
| 30 |
<main className={cn(
|
src/app/interface/video-card/index.tsx
CHANGED
|
@@ -18,12 +18,14 @@ export function VideoCard({
|
|
| 18 |
className = "",
|
| 19 |
layout = "grid",
|
| 20 |
onSelect,
|
|
|
|
| 21 |
index
|
| 22 |
}: {
|
| 23 |
media: VideoInfo
|
| 24 |
className?: string
|
| 25 |
layout?: MediaDisplayLayout
|
| 26 |
onSelect?: (media: VideoInfo) => void
|
|
|
|
| 27 |
index: number
|
| 28 |
}) {
|
| 29 |
const ref = useRef<HTMLVideoElement>(null)
|
|
|
|
| 18 |
className = "",
|
| 19 |
layout = "grid",
|
| 20 |
onSelect,
|
| 21 |
+
selected,
|
| 22 |
index
|
| 23 |
}: {
|
| 24 |
media: VideoInfo
|
| 25 |
className?: string
|
| 26 |
layout?: MediaDisplayLayout
|
| 27 |
onSelect?: (media: VideoInfo) => void
|
| 28 |
+
selected?: boolean
|
| 29 |
index: number
|
| 30 |
}) {
|
| 31 |
const ref = useRef<HTMLVideoElement>(null)
|
src/app/views/public-music-videos-view/index.tsx
CHANGED
|
@@ -7,6 +7,8 @@ import { cn } from "@/lib/utils"
|
|
| 7 |
import { VideoInfo } from "@/types"
|
| 8 |
import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
|
| 9 |
import { TrackList } from "@/app/interface/track-list"
|
|
|
|
|
|
|
| 10 |
|
| 11 |
export function PublicMusicVideosView() {
|
| 12 |
const [_isPending, startTransition] = useTransition()
|
|
@@ -15,6 +17,8 @@ export function PublicMusicVideosView() {
|
|
| 15 |
const setPublicTrack = useStore(s => s.setPublicTrack)
|
| 16 |
const publicTracks = useStore(s => s.publicTracks)
|
| 17 |
|
|
|
|
|
|
|
| 18 |
useEffect(() => {
|
| 19 |
|
| 20 |
/*
|
|
@@ -31,21 +35,29 @@ export function PublicMusicVideosView() {
|
|
| 31 |
}, [])
|
| 32 |
|
| 33 |
const handleSelect = (media: VideoInfo) => {
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
| 39 |
|
| 40 |
return (
|
| 41 |
<div className={cn(
|
| 42 |
-
`
|
| 43 |
)}>
|
| 44 |
-
<
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
</div>
|
| 50 |
)
|
| 51 |
}
|
|
|
|
| 7 |
import { VideoInfo } from "@/types"
|
| 8 |
import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
|
| 9 |
import { TrackList } from "@/app/interface/track-list"
|
| 10 |
+
import { PlaylistControl } from "@/app/interface/playlist-control"
|
| 11 |
+
import { usePlaylist } from "@/lib/usePlaylist"
|
| 12 |
|
| 13 |
export function PublicMusicVideosView() {
|
| 14 |
const [_isPending, startTransition] = useTransition()
|
|
|
|
| 17 |
const setPublicTrack = useStore(s => s.setPublicTrack)
|
| 18 |
const publicTracks = useStore(s => s.publicTracks)
|
| 19 |
|
| 20 |
+
const playlist = usePlaylist()
|
| 21 |
+
|
| 22 |
useEffect(() => {
|
| 23 |
|
| 24 |
/*
|
|
|
|
| 35 |
}, [])
|
| 36 |
|
| 37 |
const handleSelect = (media: VideoInfo) => {
|
| 38 |
+
console.log("going to play:", media.assetUrl.replace(".mp4", ".mp3"))
|
| 39 |
+
playlist.playback({
|
| 40 |
+
url: media.assetUrl.replace(".mp4", ".mp3"),
|
| 41 |
+
meta: media,
|
| 42 |
+
isLastTrackOfPlaylist: false,
|
| 43 |
+
playNow: true,
|
| 44 |
+
})
|
| 45 |
}
|
| 46 |
|
| 47 |
return (
|
| 48 |
<div className={cn(
|
| 49 |
+
`w-full h-full`
|
| 50 |
)}>
|
| 51 |
+
<div className="flex flex-col w-full overflow-y-scroll h-[calc(100%-80px)] sm:pr-4">
|
| 52 |
+
<TrackList
|
| 53 |
+
items={publicTracks}
|
| 54 |
+
onSelect={handleSelect}
|
| 55 |
+
selectedId={playlist.current?.id}
|
| 56 |
+
layout="table"
|
| 57 |
+
/>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<PlaylistControl />
|
| 61 |
</div>
|
| 62 |
)
|
| 63 |
}
|
src/lib/useAudio.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
| 1 |
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
| 2 |
-
|
| 3 |
-
// Helper Types
|
| 4 |
-
type UseAudioResponse = {
|
| 5 |
-
playback: (base64Data?: string, isLastTrackOfPlaylist?: boolean) => Promise<boolean>;
|
| 6 |
-
progress: number;
|
| 7 |
-
isLoaded: boolean;
|
| 8 |
-
isPlaying: boolean;
|
| 9 |
-
isSwitchingTracks: boolean; // when audio is temporary cut (but it's not a real pause)
|
| 10 |
-
togglePause: () => void;
|
| 11 |
-
};
|
| 12 |
-
|
| 13 |
-
export function useAudio(): UseAudioResponse {
|
| 14 |
-
const audioContextRef = useRef<AudioContext | null>(null);
|
| 15 |
-
const sourceNodeRef = useRef<AudioBufferSourceNode | null>(null);
|
| 16 |
-
const [progress, setProgress] = useState(0.0);
|
| 17 |
-
const [isPlaying, setIsPlaying] = useState(false);
|
| 18 |
-
const [isLoaded, setIsLoaded] = useState(false);
|
| 19 |
-
const [isSwitchingTracks, setSwitchingTracks] = useState(false);
|
| 20 |
-
const startTimeRef = useRef(0);
|
| 21 |
-
const pauseTimeRef = useRef(0);
|
| 22 |
-
|
| 23 |
-
const stopAudio = useCallback(() => {
|
| 24 |
-
try {
|
| 25 |
-
audioContextRef.current?.close();
|
| 26 |
-
} catch (err) {
|
| 27 |
-
// already closed probably
|
| 28 |
-
}
|
| 29 |
-
setSwitchingTracks(false);
|
| 30 |
-
|
| 31 |
-
sourceNodeRef.current = null;
|
| 32 |
-
sourceNodeRef.current = null;
|
| 33 |
-
|
| 34 |
-
// setProgress(0); // Reset progress
|
| 35 |
-
}, []);
|
| 36 |
-
|
| 37 |
-
// Helper function to handle conversion from Base64 to an ArrayBuffer
|
| 38 |
-
async function base64ToArrayBuffer(base64: string): Promise<ArrayBuffer> {
|
| 39 |
-
const response = await fetch(base64);
|
| 40 |
-
return response.arrayBuffer();
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
const playback = useCallback(
|
| 44 |
-
async (base64Data?: string, isLastTrackOfPlaylist?: boolean): Promise<boolean> => {
|
| 45 |
-
stopAudio(); // Stop any playing audio first
|
| 46 |
-
|
| 47 |
-
// If no base64 data provided, we don't attempt to play any audio
|
| 48 |
-
if (!base64Data) {
|
| 49 |
-
return false;
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
// Initialize AudioContext
|
| 53 |
-
const audioContext = new AudioContext();
|
| 54 |
-
audioContextRef.current = audioContext;
|
| 55 |
-
|
| 56 |
-
// Format Base64 string if necessary and get ArrayBuffer
|
| 57 |
-
const formattedBase64 =
|
| 58 |
-
base64Data.startsWith('data:audio/wav') || base64Data.startsWith('data:audio/wav;base64,')
|
| 59 |
-
? base64Data
|
| 60 |
-
: `data:audio/wav;base64,${base64Data}`;
|
| 61 |
-
|
| 62 |
-
console.log(`formattedBase64: ${formattedBase64.slice(0, 50)} (len: ${formattedBase64.length})`);
|
| 63 |
-
|
| 64 |
-
const arrayBuffer = await base64ToArrayBuffer(formattedBase64);
|
| 65 |
-
|
| 66 |
-
return new Promise((resolve, reject) => {
|
| 67 |
-
// Decode the audio data and play
|
| 68 |
-
audioContext.decodeAudioData(arrayBuffer, (audioBuffer) => {
|
| 69 |
-
// Create a source node and gain node
|
| 70 |
-
const source = audioContext.createBufferSource();
|
| 71 |
-
const gainNode = audioContext.createGain();
|
| 72 |
-
|
| 73 |
-
// Set buffer and gain
|
| 74 |
-
source.buffer = audioBuffer;
|
| 75 |
-
gainNode.gain.value = 1.0;
|
| 76 |
-
|
| 77 |
-
// Connect nodes
|
| 78 |
-
source.connect(gainNode);
|
| 79 |
-
gainNode.connect(audioContext.destination);
|
| 80 |
-
|
| 81 |
-
// Assign source node to ref for progress tracking
|
| 82 |
-
sourceNodeRef.current = source;
|
| 83 |
-
source.start(0, pauseTimeRef.current % audioBuffer.duration); // Start at the correct offset if paused previously
|
| 84 |
-
startTimeRef.current = audioContextRef.current!.currentTime - pauseTimeRef.current;
|
| 85 |
-
|
| 86 |
-
setSwitchingTracks(false);
|
| 87 |
-
setProgress(0);
|
| 88 |
-
setIsLoaded(true);
|
| 89 |
-
setIsPlaying(true);
|
| 90 |
-
|
| 91 |
-
// Set up progress interval
|
| 92 |
-
const totalDuration = audioBuffer.duration;
|
| 93 |
-
const updateProgressInterval = setInterval(() => {
|
| 94 |
-
if (sourceNodeRef.current && audioContextRef.current) {
|
| 95 |
-
const currentTime = audioContextRef.current.currentTime;
|
| 96 |
-
const currentProgress = currentTime / totalDuration;
|
| 97 |
-
setProgress(currentProgress);
|
| 98 |
-
if (currentProgress >= 1.0) {
|
| 99 |
-
clearInterval(updateProgressInterval);
|
| 100 |
-
}
|
| 101 |
-
}
|
| 102 |
-
}, 50); // Update every 50ms
|
| 103 |
-
|
| 104 |
-
if (source) {
|
| 105 |
-
source.onended = () => {
|
| 106 |
-
// used to indicate a temporary stop, while we switch tracks
|
| 107 |
-
if (!isLastTrackOfPlaylist) {
|
| 108 |
-
setSwitchingTracks(true);
|
| 109 |
-
}
|
| 110 |
-
setIsPlaying(false);
|
| 111 |
-
clearInterval(updateProgressInterval);
|
| 112 |
-
stopAudio();
|
| 113 |
-
resolve(true);
|
| 114 |
-
};
|
| 115 |
-
}
|
| 116 |
-
}, (error) => {
|
| 117 |
-
console.error('Error decoding audio data:', error);
|
| 118 |
-
reject(error);
|
| 119 |
-
});
|
| 120 |
-
})
|
| 121 |
-
},
|
| 122 |
-
[stopAudio]
|
| 123 |
-
);
|
| 124 |
-
|
| 125 |
-
const togglePause = useCallback(() => {
|
| 126 |
-
if (!audioContextRef.current || !sourceNodeRef.current) {
|
| 127 |
-
return; // Do nothing if audio is not initialized
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
if (isPlaying) {
|
| 131 |
-
// Pause the audio
|
| 132 |
-
pauseTimeRef.current += audioContextRef.current.currentTime - startTimeRef.current;
|
| 133 |
-
sourceNodeRef.current.stop(); // This effectively "pauses" the audio, but it also means the sourceNode will be unusable
|
| 134 |
-
sourceNodeRef.current = null; // As the node is now unusable, we nullify it
|
| 135 |
-
setIsPlaying(false);
|
| 136 |
-
} else {
|
| 137 |
-
// Resume playing
|
| 138 |
-
audioContextRef.current.resume().then(() => {
|
| 139 |
-
playback(); // This will pick up where we left off due to pauseTimeRef
|
| 140 |
-
});
|
| 141 |
-
}
|
| 142 |
-
}, [audioContextRef, sourceNodeRef, isPlaying, playback]);
|
| 143 |
-
|
| 144 |
-
// Effect to handle cleanup on component unmount
|
| 145 |
-
useEffect(() => {
|
| 146 |
-
return () => {
|
| 147 |
-
stopAudio();
|
| 148 |
-
};
|
| 149 |
-
}, [stopAudio]);
|
| 150 |
-
|
| 151 |
-
return { playback, isPlaying, isSwitchingTracks, isLoaded, progress, togglePause };
|
| 152 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/lib/usePlaylist.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { useCallback, useEffect, useRef } from "react";
|
| 4 |
+
import { create } from "zustand";
|
| 5 |
+
|
| 6 |
+
import { VideoInfo } from "@/types";
|
| 7 |
+
|
| 8 |
+
// Define the new track type with an optional playNow property
|
| 9 |
+
interface PlaybackOptions<T> {
|
| 10 |
+
url: string;
|
| 11 |
+
meta: T;
|
| 12 |
+
isLastTrackOfPlaylist?: boolean;
|
| 13 |
+
playNow?: boolean; // New optional parameter
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
// Define the Zustand store
|
| 17 |
+
interface PlaylistState<T> {
|
| 18 |
+
playlist: PlaybackOptions<T>[];
|
| 19 |
+
audio: HTMLAudioElement | null;
|
| 20 |
+
current: T | null;
|
| 21 |
+
interval: NodeJS.Timer | null;
|
| 22 |
+
progress: number;
|
| 23 |
+
setProgress: (progress: number) => void;
|
| 24 |
+
isPlaying: boolean;
|
| 25 |
+
isSwitchingTracks: boolean;
|
| 26 |
+
enqueue: (options: PlaybackOptions<T>) => void;
|
| 27 |
+
dequeue: () => void;
|
| 28 |
+
togglePause: () => void;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
function getAudio(): HTMLAudioElement | null {
|
| 32 |
+
try {
|
| 33 |
+
return new Audio()
|
| 34 |
+
} catch (err) {
|
| 35 |
+
return null
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export const usePlaylistStore = create<PlaylistState<VideoInfo>>((set, get) => ({
|
| 40 |
+
playlist: [],
|
| 41 |
+
audio: getAudio(),
|
| 42 |
+
current: null,
|
| 43 |
+
progress: 0,
|
| 44 |
+
interval: null,
|
| 45 |
+
setProgress: (progress) => set((state) => ({
|
| 46 |
+
progress: isNaN(progress) ? 0 : progress,
|
| 47 |
+
})),
|
| 48 |
+
isPlaying: false,
|
| 49 |
+
isSwitchingTracks: false,
|
| 50 |
+
enqueue: (options) => set((state) => ({ playlist: [...state.playlist, options] })),
|
| 51 |
+
dequeue: () => set((state) => {
|
| 52 |
+
const nextPlaying = state.playlist.length > 0 ? state.playlist[0] : null;
|
| 53 |
+
return {
|
| 54 |
+
current: nextPlaying ? nextPlaying.meta : null,
|
| 55 |
+
playlist: state.playlist.slice(1),
|
| 56 |
+
isSwitchingTracks: state.playlist.length > 1,
|
| 57 |
+
};
|
| 58 |
+
}),
|
| 59 |
+
togglePause: () => {
|
| 60 |
+
const { audio, isPlaying } = get()
|
| 61 |
+
// console.log("togglePause: " + isPlaying)
|
| 62 |
+
if (!audio) { return }
|
| 63 |
+
// console.log("doing the thing")
|
| 64 |
+
|
| 65 |
+
if (isPlaying) {
|
| 66 |
+
// console.log("we are playing! so setting to false..")
|
| 67 |
+
set({ isPlaying: false });
|
| 68 |
+
audio.pause();
|
| 69 |
+
} else {
|
| 70 |
+
// console.log("we are not playing! so setting to true..")
|
| 71 |
+
set({ isPlaying: true });
|
| 72 |
+
try {
|
| 73 |
+
audio.play()
|
| 74 |
+
} catch (err) {
|
| 75 |
+
console.error("Play failed:", err);
|
| 76 |
+
set({ isPlaying: false });
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
}));
|
| 81 |
+
|
| 82 |
+
// The refactored useAudioPlayer hook
|
| 83 |
+
export function usePlaylist() {
|
| 84 |
+
const intervalRef = useRef<NodeJS.Timer>();
|
| 85 |
+
|
| 86 |
+
const {
|
| 87 |
+
playlist,
|
| 88 |
+
current,
|
| 89 |
+
progress,
|
| 90 |
+
isPlaying,
|
| 91 |
+
isSwitchingTracks,
|
| 92 |
+
enqueue,
|
| 93 |
+
dequeue,
|
| 94 |
+
audio,
|
| 95 |
+
interval,
|
| 96 |
+
setProgress,
|
| 97 |
+
togglePause,
|
| 98 |
+
} = usePlaylistStore();
|
| 99 |
+
|
| 100 |
+
const updateProgress = useCallback(() => {
|
| 101 |
+
if (!audio) { return }
|
| 102 |
+
// if (!isPlaying) { return }
|
| 103 |
+
const currentProgress = audio.currentTime / audio.duration;
|
| 104 |
+
// console.log("updateProgress: " + currentProgress)
|
| 105 |
+
setProgress(currentProgress);
|
| 106 |
+
if (currentProgress >= 1) {
|
| 107 |
+
if (!audio.loop) {
|
| 108 |
+
console.log("we reached the end!")
|
| 109 |
+
dequeue();
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
}, [audio?.currentTime, dequeue, setProgress, isPlaying]);
|
| 113 |
+
|
| 114 |
+
const playback = useCallback(async (options?: PlaybackOptions<VideoInfo>): Promise<void> => {
|
| 115 |
+
if (!audio) { return }
|
| 116 |
+
|
| 117 |
+
if (!options) {
|
| 118 |
+
clearInterval(intervalRef.current!);
|
| 119 |
+
// console.log("playback called with nothing, so setting isPlaying to false")
|
| 120 |
+
usePlaylistStore.setState({
|
| 121 |
+
playlist: [],
|
| 122 |
+
current: null,
|
| 123 |
+
isPlaying: false,
|
| 124 |
+
isSwitchingTracks: false
|
| 125 |
+
});
|
| 126 |
+
return
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// console.log("playback!", options)
|
| 130 |
+
|
| 131 |
+
if (options.playNow) {
|
| 132 |
+
clearInterval(intervalRef.current!);
|
| 133 |
+
usePlaylistStore.setState({
|
| 134 |
+
playlist: [options as any], // Clears the previous playlist and adds the new track
|
| 135 |
+
current: options.meta,
|
| 136 |
+
isPlaying: true,
|
| 137 |
+
isSwitchingTracks: false
|
| 138 |
+
});
|
| 139 |
+
|
| 140 |
+
try {
|
| 141 |
+
audio.pause();
|
| 142 |
+
} catch (err) {}
|
| 143 |
+
|
| 144 |
+
try {
|
| 145 |
+
audio.src = options.url;
|
| 146 |
+
audio.load();
|
| 147 |
+
} catch (err) {}
|
| 148 |
+
|
| 149 |
+
try {
|
| 150 |
+
await audio.play();
|
| 151 |
+
} catch (err) {
|
| 152 |
+
}
|
| 153 |
+
intervalRef.current = setInterval(updateProgress, 250);
|
| 154 |
+
} else {
|
| 155 |
+
enqueue(options as any);
|
| 156 |
+
}
|
| 157 |
+
}, [enqueue, updateProgress]);
|
| 158 |
+
|
| 159 |
+
useEffect(() => {
|
| 160 |
+
return () => {
|
| 161 |
+
if (intervalRef.current) {
|
| 162 |
+
clearInterval(intervalRef.current);
|
| 163 |
+
intervalRef.current = undefined;
|
| 164 |
+
usePlaylistStore.setState({ interval: null });
|
| 165 |
+
}
|
| 166 |
+
if (audio) {
|
| 167 |
+
audio.pause();
|
| 168 |
+
}
|
| 169 |
+
};
|
| 170 |
+
}, [audio]);
|
| 171 |
+
|
| 172 |
+
return { current, playlist, playback, progress, isPlaying, isSwitchingTracks, togglePause };
|
| 173 |
+
}
|