jbilcke-hf HF staff commited on
Commit
f8ca042
1 Parent(s): da6f0c4

AiTube Music

Browse files
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.13.2",
65
- "tailwindcss": "3.3.3",
66
- "tailwindcss-animate": "^1.0.6",
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.1"
74
  },
75
  "devDependencies": {
76
  "@types/proper-lockfile": "^4.1.2",
@@ -1923,9 +1923,9 @@
1923
  }
1924
  },
1925
  "node_modules/@upstash/redis": {
1926
- "version": "1.25.2",
1927
- "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.25.2.tgz",
1928
- "integrity": "sha512-iI3jgvmDIbe4Px0PskB8lrn1NXz7ZQyGpW9Ehmonk6SEFqhqssqIB04VmlNh8zZUXwzy6G9DaIa5gIUM6B7DwA==",
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.614",
3166
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.614.tgz",
3167
- "integrity": "sha512-X4ze/9Sc3QWs6h92yerwqv7aB/uU8vCjZcrMjA8N9R1pjMFRe44dLsck5FzLilOYvcXuDn93B+bpGYyufc70gQ=="
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.15.0",
3793
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
3794
- "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
3795
  "dependencies": {
3796
  "reusify": "^1.0.4"
3797
  }
@@ -6419,28 +6419,31 @@
6419
  }
6420
  },
6421
  "node_modules/tailwind-merge": {
6422
- "version": "1.14.0",
6423
- "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz",
6424
- "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==",
 
 
 
6425
  "funding": {
6426
  "type": "github",
6427
  "url": "https://github.com/sponsors/dcastil"
6428
  }
6429
  },
6430
  "node_modules/tailwindcss": {
6431
- "version": "3.3.3",
6432
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
6433
- "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
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.2.12",
6441
  "glob-parent": "^6.0.2",
6442
  "is-glob": "^4.0.3",
6443
- "jiti": "^1.18.2",
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.13.2",
66
- "tailwindcss": "3.3.3",
67
- "tailwindcss-animate": "^1.0.6",
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.1"
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
- `w-5 h-5`
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-neutral-800/40 hover:bg-neutral-800/70" : "hover:bg-neutral-800/70"
89
  ) : "",
 
90
  className,
91
  )}
92
  onPointerEnter={handlePointerEnter}
93
  onPointerLeave={handlePointerLeave}
94
- // onClick={handleClick}
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:opacity-0 w-full h-full top-0 z-30`,
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
- `w-[30%] font-normal text-xs md:text-sm lg:text-base mb-0.5`
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
- `px-2`
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
- // setView("public_video")
36
- // setPublicVideo(video)
37
- console.log("play the track in the background, but don't reload everything")
 
 
 
38
  }
39
 
40
  return (
41
  <div className={cn(
42
- `sm:pr-4`
43
  )}>
44
- <TrackList
45
- items={publicTracks}
46
- onSelect={handleSelect}
47
- layout="table"
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
+ }