Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
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 |
+
}
|