jbilcke-hf HF staff commited on
Commit
e3d26ad
1 Parent(s): 0619325

fix the share button

Browse files
src/app/interface/collection-card/index.tsx ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import Link from "next/link"
5
+ import { RiCheckboxCircleFill } from "react-icons/ri"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import { CollectionInfo } from "@/types"
9
+ import { formatDuration } from "@/lib/formatDuration"
10
+ import { formatTimeAgo } from "@/lib/formatTimeAgo"
11
+ import { isCertifiedUser } from "@/app/certification"
12
+ import { transparentImage } from "@/lib/transparentImage"
13
+ import { DefaultAvatar } from "../default-avatar"
14
+
15
+ export function CollectionCard({
16
+ collection,
17
+ className = "",
18
+ layout = "normal",
19
+ onSelect,
20
+ index
21
+ }: {
22
+ collection: CollectionInfo
23
+ className?: string
24
+ layout?: "normal" | "compact"
25
+ onSelect?: (collection: CollectionInfo) => void
26
+ index: number
27
+ }) {
28
+ const [duration, setDuration] = useState(0)
29
+
30
+ const [channelThumbnail, setChannelThumbnail] = useState(collection.channel.thumbnail)
31
+ const [collectionThumbnail, setCollectionThumbnail] = useState(
32
+ `https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/collections/${collection.id}.webp`
33
+ )
34
+ const [collectionThumbnailReady, setCollectionThumbnailReady] = useState(false)
35
+
36
+ const isCompact = layout === "compact"
37
+
38
+ const handleClick = () => {
39
+ onSelect?.(collection)
40
+ }
41
+
42
+ const handleBadChannelThumbnail = () => {
43
+ try {
44
+ if (channelThumbnail) {
45
+ setChannelThumbnail("")
46
+ }
47
+ } catch (err) {
48
+
49
+ }
50
+ }
51
+
52
+
53
+ return (
54
+ <Link href={`/collection?v=${collection.id}`}>
55
+ <div
56
+ className={cn(
57
+ `w-full flex`,
58
+ isCompact ? `flex-row h-24 py-1 space-x-2` : `flex-col space-y-3`,
59
+ `bg-line-900`,
60
+ `cursor-pointer`,
61
+ className,
62
+ )}
63
+ >
64
+ {/* VIDEO BLOCK */}
65
+ <div
66
+ className={cn(
67
+ `flex flex-col items-center justify-center`,
68
+ `rounded-xl overflow-hidden`,
69
+ isCompact ? `w-42 h-[94px]` : `aspect-video`
70
+ )}
71
+ >
72
+ <div className={cn(
73
+ `relative w-full`,
74
+ isCompact ? `w-42 h-[94px]` : `aspect-video`
75
+ )}>
76
+ <img
77
+ src={collectionThumbnail}
78
+ className={cn(
79
+ `absolute`,
80
+ `aspect-video`,
81
+ // `aspect-video object-cover`,
82
+ `rounded-lg overflow-hidden`,
83
+ collectionThumbnailReady ? `opacity-100`: 'opacity-0',
84
+ `hover:opacity-0 w-full h-full top-0 z-30`,
85
+ //`pointer-events-none`,
86
+ `transition-all duration-500 hover:delay-300 ease-in-out`,
87
+ )}
88
+ onLoad={() => {
89
+ setCollectionThumbnailReady(true)
90
+ }}
91
+ onError={() => {
92
+ setCollectionThumbnail(transparentImage)
93
+ setCollectionThumbnailReady(false)
94
+ }}
95
+ />
96
+ </div>
97
+
98
+ <div className={cn(
99
+ // `aspect-video`,
100
+ `z-40`,
101
+ `w-full flex flex-row items-end justify-end`
102
+ )}>
103
+ <div className={cn(
104
+ `-mt-8`,
105
+ `mr-0`,
106
+ )}
107
+ >
108
+ <div className={cn(
109
+ `mb-[5px]`,
110
+ `mr-[5px]`,
111
+ `flex flex-col items-center justify-center text-center`,
112
+ `bg-neutral-900 rounded`,
113
+ `text-2xs font-semibold px-[3px] py-[1px]`,
114
+ )}
115
+ >{formatDuration(duration)}</div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ {/* TEXT BLOCK */}
121
+ <div className={cn(
122
+ `flex flex-row`,
123
+ isCompact ? `w-40 lg:w-44 xl:w-51` : `space-x-4`,
124
+ )}>
125
+ {
126
+ isCompact ? null
127
+ : channelThumbnail ? <div className="flex flex-col">
128
+ <div className="flex w-9 rounded-full overflow-hidden">
129
+ <img
130
+ src={channelThumbnail}
131
+ onError={handleBadChannelThumbnail}
132
+ />
133
+ </div>
134
+ </div>
135
+ : <DefaultAvatar
136
+ username={collection.channel.datasetUser}
137
+ bgColor="#fde047"
138
+ textColor="#1c1917"
139
+ width={36}
140
+ roundShape
141
+ />}
142
+ <div className={cn(
143
+ `flex flex-col`,
144
+ isCompact ? `` : `flex-grow`
145
+ )}>
146
+ <h3 className={cn(
147
+ `text-zinc-100 font-medium mb-0 line-clamp-2`,
148
+ isCompact ? `text-2xs md:text-xs lg:text-sm mb-1.5` : `text-base`
149
+ )}>{collection.label}</h3>
150
+ <div className={cn(
151
+ `flex flex-row items-center`,
152
+ `text-neutral-400 font-normal space-x-1`,
153
+ isCompact ? `text-3xs md:text-2xs lg:text-xs` : `text-sm`
154
+ )}>
155
+ <div>{collection.channel.label}</div>
156
+ {isCertifiedUser(collection.channel.datasetUser) ? <div><RiCheckboxCircleFill className="" /></div> : null}
157
+ </div>
158
+
159
+ <div className={cn(
160
+ `flex flex-row`,
161
+ `text-neutral-400 font-normal`,
162
+ isCompact ? `text-2xs lg:text-xs` : `text-sm`,
163
+ `space-x-1`
164
+ )}>
165
+ <div>{collection.numberOfViews} views</div>
166
+ <div className="font-semibold scale-125">·</div>
167
+ <div>{formatTimeAgo(collection.updatedAt)}</div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </Link>
173
+ )
174
+ }
src/app/interface/collection-list/index.tsx ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { cn } from "@/lib/utils"
2
+ import { CollectionInfo } from "@/types"
3
+
4
+ import { CollectionCard } from "../collection-card"
5
+
6
+ export function CollectionList({
7
+ collections = [],
8
+ layout = "grid",
9
+ className = "",
10
+ onSelect,
11
+ }: {
12
+ collections: CollectionInfo[]
13
+
14
+ /**
15
+ * Layout mode
16
+ *
17
+ * This isn't necessarily based on screen size, it can also be:
18
+ * - based on the device type (eg. a smart TV)
19
+ * - a design choice for a particular page
20
+ */
21
+ layout?: "grid" | "horizontal" | "vertical"
22
+
23
+ className?: string
24
+
25
+ onSelect?: (collection: CollectionInfo) => void
26
+ }) {
27
+
28
+ return (
29
+ <div
30
+ className={cn(
31
+ layout === "grid"
32
+ ? `grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4`
33
+ : layout === "vertical"
34
+ ? `grid grid-cols-1 gap-2`
35
+ : `flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4`,
36
+ className,
37
+ )}
38
+ >
39
+ {collections.map((collection, i) => (
40
+ <CollectionCard
41
+ key={collection.id}
42
+ collection={collection}
43
+ className="w-full"
44
+ layout={layout === "vertical" ? "compact" : "normal"}
45
+ onSelect={onSelect}
46
+ index={i}
47
+ />
48
+ ))}
49
+ </div>
50
+ )
51
+ }
src/app/music/index.tsx CHANGED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { Main } from "../main"
3
+ // import { getChannels } from "../server/actions/ai-tube-hf/getChannels"
4
+
5
+ export default async function MusicPage() {
6
+ // const channels = await getChannels()
7
+
8
+ return (<Main />)
9
+ }
src/app/views/public-music-videos-view/index.tsx CHANGED
@@ -11,7 +11,6 @@ import { VideoList } from "@/app/interface/video-list"
11
  export function PublicMusicVideosView() {
12
  const [_isPending, startTransition] = useTransition()
13
  const setView = useStore(s => s.setView)
14
- const currentTag = useStore(s => s.currentTag)
15
  const setPublicVideos = useStore(s => s.setPublicVideos)
16
  const setPublicVideo = useStore(s => s.setPublicVideo)
17
  const publicVideos = useStore(s => s.publicVideos)
@@ -20,17 +19,19 @@ export function PublicMusicVideosView() {
20
  startTransition(async () => {
21
  const videos = await getVideos({
22
  sortBy: "date",
23
- mandatoryTags: currentTag ? [currentTag] : [],
24
  maxVideos: 25
25
  })
26
 
27
  setPublicVideos(videos)
28
  })
29
- }, [currentTag])
30
 
31
  const handleSelect = (video: VideoInfo) => {
32
- setView("public_video")
33
- setPublicVideo(video)
 
 
34
  }
35
 
36
  return (
 
11
  export function PublicMusicVideosView() {
12
  const [_isPending, startTransition] = useTransition()
13
  const setView = useStore(s => s.setView)
 
14
  const setPublicVideos = useStore(s => s.setPublicVideos)
15
  const setPublicVideo = useStore(s => s.setPublicVideo)
16
  const publicVideos = useStore(s => s.publicVideos)
 
19
  startTransition(async () => {
20
  const videos = await getVideos({
21
  sortBy: "date",
22
+ mandatoryTags:["music"],
23
  maxVideos: 25
24
  })
25
 
26
  setPublicVideos(videos)
27
  })
28
+ }, [])
29
 
30
  const handleSelect = (video: VideoInfo) => {
31
+ //
32
+ // setView("public_video")
33
+ // setPublicVideo(video)
34
+ console.log("play the track in the background, but don't reload everything")
35
  }
36
 
37
  return (
src/app/views/public-video-view/index.tsx CHANGED
@@ -205,7 +205,7 @@ export function PublicVideoView() {
205
  `items-center`
206
  )}>
207
  <CopyToClipboard
208
- text={`https://huggingface.co/spaces/jbilcke-hf/ai-tube?v=${video.id}`}
209
  onCopy={() => setCopied(true)}>
210
  <div className={actionButtonClassName}>
211
  <div className="flex items-center justify-center">
 
205
  `items-center`
206
  )}>
207
  <CopyToClipboard
208
+ text={`https://jbilcke-hf-ai-tube.hf.space/watch?v=${video.id}`}
209
  onCopy={() => setCopied(true)}>
210
  <div className={actionButtonClassName}>
211
  <div className="flex items-center justify-center">
src/types.ts CHANGED
@@ -441,6 +441,64 @@ export type VideoInfo = {
441
  orientation: VideoOrientation
442
  }
443
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  export type PublicUserInfo = {
445
  id: string
446
 
 
441
  orientation: VideoOrientation
442
  }
443
 
444
+ export type CollectionInfo = {
445
+ /**
446
+ * UUID (v4)
447
+ */
448
+ id: string
449
+
450
+ /**
451
+ * Human readable title for the video
452
+ */
453
+ label: string
454
+
455
+ /**
456
+ * Human readable description for the video
457
+ */
458
+ description: string
459
+
460
+ /**
461
+ * URL to the video thumbnail
462
+ */
463
+ thumbnailUrl: string
464
+
465
+ /**
466
+ * Counter for the number of views
467
+ *
468
+ * Note: should be managed by the index to prevent cheating
469
+ */
470
+ numberOfViews: number
471
+
472
+ /**
473
+ * Counter for the number of likes
474
+ *
475
+ * Note: should be managed by the index to prevent cheating
476
+ */
477
+ numberOfLikes: number
478
+
479
+ /**
480
+ * When was the video updated
481
+ */
482
+ updatedAt: string
483
+
484
+ /**
485
+ * Arbotrary string tags to label the content
486
+ */
487
+ tags: string[]
488
+
489
+ /**
490
+ * The owner channel
491
+ */
492
+ channel: ChannelInfo
493
+
494
+ /**
495
+ * Collection duration
496
+ */
497
+ duration: number
498
+
499
+ items: Array<VideoInfo>[]
500
+ }
501
+
502
  export type PublicUserInfo = {
503
  id: string
504