Spaces:
Running
Running
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:
|
24 |
maxVideos: 25
|
25 |
})
|
26 |
|
27 |
setPublicVideos(videos)
|
28 |
})
|
29 |
-
}, [
|
30 |
|
31 |
const handleSelect = (video: VideoInfo) => {
|
32 |
-
|
33 |
-
|
|
|
|
|
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://
|
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 |
|