Spaces:
Running
Running
Commit
•
d160b97
1
Parent(s):
5f6a9eb
improve mobile support
Browse files- TODO.md +4 -4
- src/app/config.ts +4 -0
- src/app/interface/action-button/index.tsx +10 -7
- src/app/interface/channel-card/index.tsx +28 -10
- src/app/interface/left-menu/index.tsx +5 -3
- src/app/interface/left-menu/menu-item/index.tsx +1 -1
- src/app/interface/mobile-bottom-menu/index.tsx +54 -0
- src/app/interface/top-header/index.tsx +7 -4
- src/app/interface/tube-layout/index.tsx +4 -1
- src/app/interface/video-card/index.tsx +4 -4
- src/app/interface/video-list/index.tsx +1 -1
- src/app/views/public-video-view/index.tsx +39 -29
- src/app/views/user-account-view/index.tsx +13 -3
- src/app/views/user-channel-view/index.tsx +1 -4
TODO.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
|
2 |
-
Allow browsing some loras
|
3 |
|
4 |
-
|
5 |
-
|
6 |
-
-
|
|
|
|
1 |
|
|
|
2 |
|
3 |
+
## Video quality improvements
|
4 |
+
|
5 |
+
- Add a "style prompt" to do it like the AI Comic Factory
|
6 |
+
- Make it easier to pick a LoRA
|
src/app/config.ts
CHANGED
@@ -1,3 +1,7 @@
|
|
1 |
export const showBetaFeatures = `${
|
2 |
process.env.NEXT_PUBLIC_SHOW_BETA_FEATURES || ""
|
3 |
}`.trim().toLowerCase() === "true"
|
|
|
|
|
|
|
|
|
|
1 |
export const showBetaFeatures = `${
|
2 |
process.env.NEXT_PUBLIC_SHOW_BETA_FEATURES || ""
|
3 |
}`.trim().toLowerCase() === "true"
|
4 |
+
|
5 |
+
|
6 |
+
export const defaultVideoModel = "SVD"
|
7 |
+
export const defaultVoice = "Julian"
|
src/app/interface/action-button/index.tsx
CHANGED
@@ -1,7 +1,15 @@
|
|
1 |
import { ReactNode } from "react"
|
2 |
|
3 |
import { cn } from "@/lib/utils"
|
4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
export function ActionButton({
|
7 |
className,
|
@@ -14,12 +22,7 @@ export function ActionButton({
|
|
14 |
}) {
|
15 |
|
16 |
const classNames = cn(
|
17 |
-
|
18 |
-
`items-center justify-center text-center`,
|
19 |
-
`rounded-2xl`,
|
20 |
-
`cursor-pointer`,
|
21 |
-
`text-sm font-medium`,
|
22 |
-
`bg-neutral-700/50 hover:bg-neutral-700/90 text-zinc-100`,
|
23 |
className,
|
24 |
)
|
25 |
|
|
|
1 |
import { ReactNode } from "react"
|
2 |
|
3 |
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
export const actionButtonClassName = cn(
|
6 |
+
`flex flex-row space-x-1.5 lg:space-x-2 pl-2 lg:pl-3 pr-3 lg:pr-4 h-8 lg:h-9`,
|
7 |
+
`items-center justify-center text-center`,
|
8 |
+
`rounded-2xl`,
|
9 |
+
`cursor-pointer`,
|
10 |
+
`text-xs lg:text-sm font-medium`,
|
11 |
+
`bg-neutral-700/50 hover:bg-neutral-700/90 text-zinc-100`,
|
12 |
+
)
|
13 |
|
14 |
export function ActionButton({
|
15 |
className,
|
|
|
22 |
}) {
|
23 |
|
24 |
const classNames = cn(
|
25 |
+
actionButtonClassName,
|
|
|
|
|
|
|
|
|
|
|
26 |
className,
|
27 |
)
|
28 |
|
src/app/interface/channel-card/index.tsx
CHANGED
@@ -1,10 +1,12 @@
|
|
1 |
import { useState } from "react"
|
2 |
import dynamic from "next/dynamic"
|
3 |
|
|
|
|
|
|
|
4 |
import { cn } from "@/lib/utils"
|
5 |
import { ChannelInfo } from "@/types"
|
6 |
import { isCertifiedUser } from "@/app/certification"
|
7 |
-
import { RiCheckboxCircleFill } from "react-icons/ri"
|
8 |
|
9 |
const DefaultAvatar = dynamic(() => import("../default-avatar"), {
|
10 |
loading: () => null,
|
@@ -31,6 +33,8 @@ export function ChannelCard({
|
|
31 |
}
|
32 |
}
|
33 |
|
|
|
|
|
34 |
return (
|
35 |
<div
|
36 |
className={cn(
|
@@ -39,8 +43,8 @@ export function ChannelCard({
|
|
39 |
`space-y-1`,
|
40 |
`w-52 h-52`,
|
41 |
`rounded-lg`,
|
42 |
-
`
|
43 |
-
`
|
44 |
`cursor-pointer`,
|
45 |
className,
|
46 |
)}
|
@@ -57,8 +61,17 @@ export function ChannelCard({
|
|
57 |
`w-26 h-26`
|
58 |
)}
|
59 |
>
|
60 |
-
{
|
61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
src={channelThumbnail}
|
63 |
onError={handleBadChannelThumbnail}
|
64 |
/>
|
@@ -76,22 +89,27 @@ export function ChannelCard({
|
|
76 |
`items-center justify-center text-center`,
|
77 |
`space-y-1`
|
78 |
)}>
|
79 |
-
<div className=
|
|
|
|
|
|
|
|
|
|
|
80 |
{/*<div className="text-center text-sm font-semibold">
|
81 |
by <a href={
|
82 |
`https://huggingface.co/${channel.datasetUser}`
|
83 |
} target="_blank">@{channel.datasetUser}</a>
|
84 |
</div>
|
85 |
*/}
|
86 |
-
<div className="flex flex-row items-center space-x-0.5">
|
87 |
<div className="flex flex-row items-center text-center text-xs font-medium">@{channel.datasetUser}</div>
|
88 |
{isCertifiedUser(channel.datasetUser) ? <div className="text-xs text-neutral-400"><RiCheckboxCircleFill className="" /></div> : null}
|
89 |
-
</div>
|
90 |
-
<div className="flex flex-row items-center justify-center text-neutral-400">
|
91 |
<div className="text-center text-xs">{0} videos</div>
|
92 |
<div className="px-1">-</div>
|
93 |
<div className="text-center text-xs">{channel.likes} likes</div>
|
94 |
-
</div>
|
95 |
</div>
|
96 |
</div>
|
97 |
)
|
|
|
1 |
import { useState } from "react"
|
2 |
import dynamic from "next/dynamic"
|
3 |
|
4 |
+
import { RiCheckboxCircleFill } from "react-icons/ri"
|
5 |
+
import { IoAdd } from "react-icons/io5"
|
6 |
+
|
7 |
import { cn } from "@/lib/utils"
|
8 |
import { ChannelInfo } from "@/types"
|
9 |
import { isCertifiedUser } from "@/app/certification"
|
|
|
10 |
|
11 |
const DefaultAvatar = dynamic(() => import("../default-avatar"), {
|
12 |
loading: () => null,
|
|
|
33 |
}
|
34 |
}
|
35 |
|
36 |
+
const isCreateButton = !channel.id
|
37 |
+
|
38 |
return (
|
39 |
<div
|
40 |
className={cn(
|
|
|
43 |
`space-y-1`,
|
44 |
`w-52 h-52`,
|
45 |
`rounded-lg`,
|
46 |
+
`text-neutral-100/80`,
|
47 |
+
isCreateButton ? '' : `hover:bg-neutral-800/30 hover:text-neutral-100/100`,
|
48 |
`cursor-pointer`,
|
49 |
className,
|
50 |
)}
|
|
|
61 |
`w-26 h-26`
|
62 |
)}
|
63 |
>
|
64 |
+
{isCreateButton
|
65 |
+
? <div className={cn(
|
66 |
+
`flex flex-col justify-center items-center text-center`,
|
67 |
+
`w-full h-full rounded-full`,
|
68 |
+
`bg-neutral-700 hover:bg-neutral-600`,
|
69 |
+
`border-2 border-neutral-400 hover:border-neutral-300`
|
70 |
+
)}>
|
71 |
+
<IoAdd className="w-8 h-8" />
|
72 |
+
</div>
|
73 |
+
: channelThumbnail
|
74 |
+
? <img
|
75 |
src={channelThumbnail}
|
76 |
onError={handleBadChannelThumbnail}
|
77 |
/>
|
|
|
89 |
`items-center justify-center text-center`,
|
90 |
`space-y-1`
|
91 |
)}>
|
92 |
+
<div className={cn(
|
93 |
+
`text-center text-base font-medium text-zinc-100`,
|
94 |
+
isCreateButton ? 'mt-2' : ''
|
95 |
+
)}>{
|
96 |
+
isCreateButton ? "Create a channel" : channel.label
|
97 |
+
}</div>
|
98 |
{/*<div className="text-center text-sm font-semibold">
|
99 |
by <a href={
|
100 |
`https://huggingface.co/${channel.datasetUser}`
|
101 |
} target="_blank">@{channel.datasetUser}</a>
|
102 |
</div>
|
103 |
*/}
|
104 |
+
{!isCreateButton && <div className="flex flex-row items-center space-x-0.5">
|
105 |
<div className="flex flex-row items-center text-center text-xs font-medium">@{channel.datasetUser}</div>
|
106 |
{isCertifiedUser(channel.datasetUser) ? <div className="text-xs text-neutral-400"><RiCheckboxCircleFill className="" /></div> : null}
|
107 |
+
</div>}
|
108 |
+
{!isCreateButton && <div className="flex flex-row items-center justify-center text-neutral-400">
|
109 |
<div className="text-center text-xs">{0} videos</div>
|
110 |
<div className="px-1">-</div>
|
111 |
<div className="text-center text-xs">{channel.likes} likes</div>
|
112 |
+
</div>}
|
113 |
</div>
|
114 |
</div>
|
115 |
)
|
src/app/interface/left-menu/index.tsx
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import { GrChannel } from "react-icons/gr"
|
2 |
import { MdVideoLibrary } from "react-icons/md"
|
3 |
import { RiHome8Line } from "react-icons/ri"
|
@@ -6,17 +8,17 @@ import { CgProfile } from "react-icons/cg"
|
|
6 |
|
7 |
import { useStore } from "@/app/state/useStore"
|
8 |
import { cn } from "@/lib/utils"
|
9 |
-
import { MenuItem } from "./menu-item"
|
10 |
import { showBetaFeatures } from "@/app/config"
|
11 |
-
import Link from "next/link"
|
12 |
|
|
|
13 |
|
14 |
export function LeftMenu() {
|
15 |
const view = useStore(s => s.view)
|
16 |
|
17 |
return (
|
18 |
<div className={cn(
|
19 |
-
`
|
|
|
20 |
`w-24 px-1 pt-4`,
|
21 |
`justify-between`
|
22 |
// `bg-orange-500`,
|
|
|
1 |
+
import Link from "next/link"
|
2 |
+
|
3 |
import { GrChannel } from "react-icons/gr"
|
4 |
import { MdVideoLibrary } from "react-icons/md"
|
5 |
import { RiHome8Line } from "react-icons/ri"
|
|
|
8 |
|
9 |
import { useStore } from "@/app/state/useStore"
|
10 |
import { cn } from "@/lib/utils"
|
|
|
11 |
import { showBetaFeatures } from "@/app/config"
|
|
|
12 |
|
13 |
+
import { MenuItem } from "./menu-item"
|
14 |
|
15 |
export function LeftMenu() {
|
16 |
const view = useStore(s => s.view)
|
17 |
|
18 |
return (
|
19 |
<div className={cn(
|
20 |
+
`hidden sm:flex`,
|
21 |
+
`flex-col`,
|
22 |
`w-24 px-1 pt-4`,
|
23 |
`justify-between`
|
24 |
// `bg-orange-500`,
|
src/app/interface/left-menu/menu-item/index.tsx
CHANGED
@@ -22,7 +22,7 @@ export function MenuItem({
|
|
22 |
`items-center justify-center justify-items-stretch`,
|
23 |
// `bg-green-500`,
|
24 |
`cursor-pointer`,
|
25 |
-
`w-full h-21`,
|
26 |
`p-1`,
|
27 |
`group`
|
28 |
)}
|
|
|
22 |
`items-center justify-center justify-items-stretch`,
|
23 |
// `bg-green-500`,
|
24 |
`cursor-pointer`,
|
25 |
+
`w-20 h-18 sm:w-full sm:h-21`,
|
26 |
`p-1`,
|
27 |
`group`
|
28 |
)}
|
src/app/interface/mobile-bottom-menu/index.tsx
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import Link from "next/link"
|
2 |
+
|
3 |
+
import { GrChannel } from "react-icons/gr"
|
4 |
+
import { MdVideoLibrary } from "react-icons/md"
|
5 |
+
import { RiHome8Line } from "react-icons/ri"
|
6 |
+
import { PiRobot } from "react-icons/pi"
|
7 |
+
import { CgProfile } from "react-icons/cg"
|
8 |
+
|
9 |
+
import { useStore } from "@/app/state/useStore"
|
10 |
+
import { cn } from "@/lib/utils"
|
11 |
+
import { showBetaFeatures } from "@/app/config"
|
12 |
+
|
13 |
+
import { MenuItem } from "../left-menu/menu-item"
|
14 |
+
|
15 |
+
export function MobileBottomMenu() {
|
16 |
+
const view = useStore(s => s.view)
|
17 |
+
|
18 |
+
return (
|
19 |
+
<div className={cn(
|
20 |
+
`flex sm:hidden`,
|
21 |
+
`flex-row`,
|
22 |
+
`w-full`,
|
23 |
+
`justify-between`
|
24 |
+
)}>
|
25 |
+
<Link href={{
|
26 |
+
pathname: '/',
|
27 |
+
query: { v: undefined },
|
28 |
+
}}>
|
29 |
+
<MenuItem
|
30 |
+
icon={<RiHome8Line className="h-6 w-6" />}
|
31 |
+
selected={view === "home"}
|
32 |
+
>
|
33 |
+
Discover
|
34 |
+
</MenuItem>
|
35 |
+
</Link>
|
36 |
+
<Link href="/channels">
|
37 |
+
<MenuItem
|
38 |
+
icon={<GrChannel className="h-5 w-5" />}
|
39 |
+
selected={view === "public_channels"}
|
40 |
+
>
|
41 |
+
Channels
|
42 |
+
</MenuItem>
|
43 |
+
</Link>
|
44 |
+
<Link href="/account">
|
45 |
+
<MenuItem
|
46 |
+
icon={<CgProfile className="h-6 w-6" />}
|
47 |
+
selected={view === "user_account" || view === "user_channel"}
|
48 |
+
>
|
49 |
+
Account
|
50 |
+
</MenuItem>
|
51 |
+
</Link>
|
52 |
+
</div>
|
53 |
+
)
|
54 |
+
}
|
src/app/interface/top-header/index.tsx
CHANGED
@@ -98,10 +98,13 @@ export function TopHeader() {
|
|
98 |
</div>
|
99 |
</div>
|
100 |
<div className={cn(
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
|
|
|
|
|
|
105 |
)}>
|
106 |
All the videos are generated using AI, for research purposes only. Some models might produce factually incorrect or biased outputs.
|
107 |
</div>
|
|
|
98 |
</div>
|
99 |
</div>
|
100 |
<div className={cn(
|
101 |
+
// TODO: show the disclaimer on mobile too, maybe with a modal or something
|
102 |
+
`hidden sm:flex`,
|
103 |
+
`flex-col`,
|
104 |
+
`items-center justify-center`,
|
105 |
+
`transition-all duration-200 ease-in-out`,
|
106 |
+
`px-4 py-2 w-max-64`,
|
107 |
+
`text-neutral-400 text-2xs sm:text-xs lg:text-sm italic`
|
108 |
)}>
|
109 |
All the videos are generated using AI, for research purposes only. Some models might produce factually incorrect or biased outputs.
|
110 |
</div>
|
src/app/interface/tube-layout/index.tsx
CHANGED
@@ -5,8 +5,10 @@ import { cn } from "@/lib/utils"
|
|
5 |
import { useStore } from "@/app/state/useStore"
|
6 |
|
7 |
import { LeftMenu } from "../left-menu"
|
|
|
8 |
import { TopHeader } from "../top-header"
|
9 |
|
|
|
10 |
export function TubeLayout({ children }: { children?: ReactNode }) {
|
11 |
const headerMode = useStore(s => s.headerMode)
|
12 |
const view = useStore(s => s.view)
|
@@ -21,7 +23,7 @@ export function TubeLayout({ children }: { children?: ReactNode }) {
|
|
21 |
<LeftMenu />
|
22 |
<div className={cn(
|
23 |
`flex flex-col`,
|
24 |
-
`w-[calc(100vw-96px)]`,
|
25 |
`px-2`
|
26 |
)}>
|
27 |
<TopHeader />
|
@@ -33,6 +35,7 @@ export function TubeLayout({ children }: { children?: ReactNode }) {
|
|
33 |
)}>
|
34 |
{children}
|
35 |
</main>
|
|
|
36 |
</div>
|
37 |
</div>
|
38 |
)
|
|
|
5 |
import { useStore } from "@/app/state/useStore"
|
6 |
|
7 |
import { LeftMenu } from "../left-menu"
|
8 |
+
import { MobileBottomMenu } from "../mobile-bottom-menu"
|
9 |
import { TopHeader } from "../top-header"
|
10 |
|
11 |
+
|
12 |
export function TubeLayout({ children }: { children?: ReactNode }) {
|
13 |
const headerMode = useStore(s => s.headerMode)
|
14 |
const view = useStore(s => s.view)
|
|
|
23 |
<LeftMenu />
|
24 |
<div className={cn(
|
25 |
`flex flex-col`,
|
26 |
+
`w-full sm:w-[calc(100vw-96px)]`,
|
27 |
`px-2`
|
28 |
)}>
|
29 |
<TopHeader />
|
|
|
35 |
)}>
|
36 |
{children}
|
37 |
</main>
|
38 |
+
<MobileBottomMenu />
|
39 |
</div>
|
40 |
</div>
|
41 |
)
|
src/app/interface/video-card/index.tsx
CHANGED
@@ -118,7 +118,7 @@ export function VideoCard({
|
|
118 |
{/* TEXT BLOCK */}
|
119 |
<div className={cn(
|
120 |
`flex flex-row`,
|
121 |
-
isCompact ? `w-51` : `space-x-4`,
|
122 |
)}>
|
123 |
{
|
124 |
isCompact ? null
|
@@ -143,12 +143,12 @@ export function VideoCard({
|
|
143 |
)}>
|
144 |
<h3 className={cn(
|
145 |
`text-zinc-100 font-medium mb-0 line-clamp-2`,
|
146 |
-
isCompact ? `text-sm mb-1.5` : `text-base`
|
147 |
)}>{video.label}</h3>
|
148 |
<div className={cn(
|
149 |
`flex flex-row items-center`,
|
150 |
`text-neutral-400 font-normal space-x-1`,
|
151 |
-
isCompact ? `text-xs` : `text-sm`
|
152 |
)}>
|
153 |
<div>{video.channel.label}</div>
|
154 |
{isCertifiedUser(video.channel.datasetUser) ? <div><RiCheckboxCircleFill className="" /></div> : null}
|
@@ -157,7 +157,7 @@ export function VideoCard({
|
|
157 |
<div className={cn(
|
158 |
`flex flex-row`,
|
159 |
`text-neutral-400 font-normal`,
|
160 |
-
isCompact ? `text-xs` : `text-sm`,
|
161 |
`space-x-1`
|
162 |
)}>
|
163 |
<div>0 views</div>
|
|
|
118 |
{/* TEXT BLOCK */}
|
119 |
<div className={cn(
|
120 |
`flex flex-row`,
|
121 |
+
isCompact ? `w-40 lg:w-44 xl:w-51` : `space-x-4`,
|
122 |
)}>
|
123 |
{
|
124 |
isCompact ? null
|
|
|
143 |
)}>
|
144 |
<h3 className={cn(
|
145 |
`text-zinc-100 font-medium mb-0 line-clamp-2`,
|
146 |
+
isCompact ? `text-2xs md:text-xs lg:text-sm mb-1.5` : `text-base`
|
147 |
)}>{video.label}</h3>
|
148 |
<div className={cn(
|
149 |
`flex flex-row items-center`,
|
150 |
`text-neutral-400 font-normal space-x-1`,
|
151 |
+
isCompact ? `text-3xs md:text-2xs lg:text-xs` : `text-sm`
|
152 |
)}>
|
153 |
<div>{video.channel.label}</div>
|
154 |
{isCertifiedUser(video.channel.datasetUser) ? <div><RiCheckboxCircleFill className="" /></div> : null}
|
|
|
157 |
<div className={cn(
|
158 |
`flex flex-row`,
|
159 |
`text-neutral-400 font-normal`,
|
160 |
+
isCompact ? `text-2xs lg:text-xs` : `text-sm`,
|
161 |
`space-x-1`
|
162 |
)}>
|
163 |
<div>0 views</div>
|
src/app/interface/video-list/index.tsx
CHANGED
@@ -29,7 +29,7 @@ export function VideoList({
|
|
29 |
<div
|
30 |
className={cn(
|
31 |
layout === "grid"
|
32 |
-
? `grid grid-cols-
|
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`,
|
|
|
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`,
|
src/app/views/public-video-view/index.tsx
CHANGED
@@ -13,7 +13,7 @@ import { useStore } from "@/app/state/useStore"
|
|
13 |
import { cn } from "@/lib/utils"
|
14 |
import { VideoPlayer } from "@/app/interface/video-player"
|
15 |
import { VideoInfo } from "@/types"
|
16 |
-
import { ActionButton } from "@/app/interface/action-button"
|
17 |
import { RecommendedVideos } from "@/app/interface/recommended-videos"
|
18 |
import { isCertifiedUser } from "@/app/certification"
|
19 |
|
@@ -78,6 +78,8 @@ export function PublicVideoView() {
|
|
78 |
<div className={cn(
|
79 |
`flex-grow`,
|
80 |
`flex flex-col`,
|
|
|
|
|
81 |
)}>
|
82 |
{/** VIDEO PLAYER - HORIZONTAL */}
|
83 |
<VideoPlayer
|
@@ -88,8 +90,9 @@ export function PublicVideoView() {
|
|
88 |
{/** VIDEO TITLE - HORIZONTAL */}
|
89 |
<div className={cn(
|
90 |
`flex flew-row space-x-2`,
|
91 |
-
`
|
92 |
-
`mb-2
|
|
|
93 |
)}>
|
94 |
<div className="">{video.label}</div>
|
95 |
{/*
|
@@ -108,21 +111,26 @@ export function PublicVideoView() {
|
|
108 |
|
109 |
{/** VIDEO TOOLBAR - HORIZONTAL */}
|
110 |
<div className={cn(
|
111 |
-
`flex flex-row`,
|
112 |
-
`
|
|
|
113 |
`justify-between`,
|
114 |
-
`mb-4
|
115 |
)}>
|
116 |
-
{/** LEFT PART
|
117 |
<div className={cn(
|
118 |
`flex flex-row`,
|
119 |
`items-center`
|
120 |
)}>
|
121 |
{/** CHANNEL LOGO - VERTICAL */}
|
122 |
-
<
|
123 |
-
|
124 |
-
|
125 |
-
|
|
|
|
|
|
|
|
|
126 |
<div className="flex w-10 rounded-full overflow-hidden">
|
127 |
{
|
128 |
channelThumbnail ? <div className="flex flex-col">
|
@@ -141,15 +149,20 @@ export function PublicVideoView() {
|
|
141 |
roundShape
|
142 |
/>}
|
143 |
</div>
|
144 |
-
</
|
145 |
|
146 |
{/** CHANNEL INFO - VERTICAL */}
|
147 |
-
<
|
148 |
-
`flex flex-col
|
149 |
-
|
|
|
|
|
|
|
|
|
150 |
<div className={cn(
|
151 |
`flex flex-row items-center`,
|
152 |
-
`
|
|
|
153 |
)}>
|
154 |
<div>{video.channel.label}</div>
|
155 |
{isCertifiedUser(video.channel.datasetUser) ? <div className="text-sm text-neutral-400"><RiCheckboxCircleFill className="" /></div> : null}
|
@@ -161,10 +174,12 @@ export function PublicVideoView() {
|
|
161 |
<div>0 followers</div>
|
162 |
<div></div>
|
163 |
</div>
|
164 |
-
</
|
|
|
|
|
165 |
</div>
|
166 |
|
167 |
-
{/** RIGHT PART
|
168 |
<div className={cn(
|
169 |
`flex flex-row`,
|
170 |
`items-center`,
|
@@ -178,14 +193,7 @@ export function PublicVideoView() {
|
|
178 |
<CopyToClipboard
|
179 |
text={`https://huggingface.co/spaces/jbilcke-hf/ai-tube?v=${video.id}`}
|
180 |
onCopy={() => setCopied(true)}>
|
181 |
-
<div className={
|
182 |
-
`flex flex-row space-x-2 pl-3 pr-4 h-9`,
|
183 |
-
`items-center justify-center text-center`,
|
184 |
-
`rounded-2xl`,
|
185 |
-
`cursor-pointer`,
|
186 |
-
`text-sm font-medium`,
|
187 |
-
`bg-neutral-700/50 hover:bg-neutral-700/90 text-zinc-100`
|
188 |
-
)}>
|
189 |
<div className="flex items-center justify-center">
|
190 |
{
|
191 |
copied ? <LuCopyCheck className="w-4 h-4" />
|
@@ -234,17 +242,19 @@ export function PublicVideoView() {
|
|
234 |
{/** VIDEO DESCRIPTION - VERTICAL */}
|
235 |
<div className={cn(
|
236 |
`flex flex-col p-3`,
|
|
|
237 |
`rounded-xl`,
|
238 |
`bg-neutral-700/50`,
|
239 |
-
`text-sm
|
240 |
)}>
|
241 |
<p>{video.description}</p>
|
242 |
</div>
|
243 |
</div>
|
244 |
<div className={cn(
|
245 |
-
`sm:w-56 md:w-[450px]`,
|
|
|
246 |
`hidden sm:flex flex-col`,
|
247 |
-
`pl-5 pr-8`,
|
248 |
)}>
|
249 |
<RecommendedVideos video={video} />
|
250 |
</div>
|
|
|
13 |
import { cn } from "@/lib/utils"
|
14 |
import { VideoPlayer } from "@/app/interface/video-player"
|
15 |
import { VideoInfo } from "@/types"
|
16 |
+
import { ActionButton, actionButtonClassName } from "@/app/interface/action-button"
|
17 |
import { RecommendedVideos } from "@/app/interface/recommended-videos"
|
18 |
import { isCertifiedUser } from "@/app/certification"
|
19 |
|
|
|
78 |
<div className={cn(
|
79 |
`flex-grow`,
|
80 |
`flex flex-col`,
|
81 |
+
`transition-all duration-200 ease-in-out`,
|
82 |
+
`px-2 sm:px-0`
|
83 |
)}>
|
84 |
{/** VIDEO PLAYER - HORIZONTAL */}
|
85 |
<VideoPlayer
|
|
|
90 |
{/** VIDEO TITLE - HORIZONTAL */}
|
91 |
<div className={cn(
|
92 |
`flex flew-row space-x-2`,
|
93 |
+
`transition-all duration-200 ease-in-out`,
|
94 |
+
`text-lg lg:text-xl text-zinc-100 font-medium mb-0 line-clamp-2`,
|
95 |
+
`mb-2`,
|
96 |
)}>
|
97 |
<div className="">{video.label}</div>
|
98 |
{/*
|
|
|
111 |
|
112 |
{/** VIDEO TOOLBAR - HORIZONTAL */}
|
113 |
<div className={cn(
|
114 |
+
`flex flex-col space-y-3 xl:space-y-0 xl:flex-row`,
|
115 |
+
`transition-all duration-200 ease-in-out`,
|
116 |
+
`items-start xl:items-center`,
|
117 |
`justify-between`,
|
118 |
+
`mb-4`,
|
119 |
)}>
|
120 |
+
{/** LEFT PART OF THE TOOLBAR */}
|
121 |
<div className={cn(
|
122 |
`flex flex-row`,
|
123 |
`items-center`
|
124 |
)}>
|
125 |
{/** CHANNEL LOGO - VERTICAL */}
|
126 |
+
<a
|
127 |
+
className={cn(
|
128 |
+
`flex flex-col`,
|
129 |
+
`mr-3`,
|
130 |
+
`cursor-pointer`
|
131 |
+
)}
|
132 |
+
href={`https://huggingface.co/datasets/${video.channel.datasetUser}/${video.channel.datasetName}`}
|
133 |
+
target="_blank">
|
134 |
<div className="flex w-10 rounded-full overflow-hidden">
|
135 |
{
|
136 |
channelThumbnail ? <div className="flex flex-col">
|
|
|
149 |
roundShape
|
150 |
/>}
|
151 |
</div>
|
152 |
+
</a>
|
153 |
|
154 |
{/** CHANNEL INFO - VERTICAL */}
|
155 |
+
<a className={cn(
|
156 |
+
`flex flex-row sm:flex-col`,
|
157 |
+
`transition-all duration-200 ease-in-out`,
|
158 |
+
`cursor-pointer`,
|
159 |
+
)}
|
160 |
+
href={`https://huggingface.co/datasets/${video.channel.datasetUser}/${video.channel.datasetName}`}
|
161 |
+
target="_blank">
|
162 |
<div className={cn(
|
163 |
`flex flex-row items-center`,
|
164 |
+
`transition-all duration-200 ease-in-out`,
|
165 |
+
`text-zinc-100 text-sm lg:text-base font-medium space-x-1`,
|
166 |
)}>
|
167 |
<div>{video.channel.label}</div>
|
168 |
{isCertifiedUser(video.channel.datasetUser) ? <div className="text-sm text-neutral-400"><RiCheckboxCircleFill className="" /></div> : null}
|
|
|
174 |
<div>0 followers</div>
|
175 |
<div></div>
|
176 |
</div>
|
177 |
+
</a>
|
178 |
+
|
179 |
+
|
180 |
</div>
|
181 |
|
182 |
+
{/** RIGHT PART OF THE TOOLBAR */}
|
183 |
<div className={cn(
|
184 |
`flex flex-row`,
|
185 |
`items-center`,
|
|
|
193 |
<CopyToClipboard
|
194 |
text={`https://huggingface.co/spaces/jbilcke-hf/ai-tube?v=${video.id}`}
|
195 |
onCopy={() => setCopied(true)}>
|
196 |
+
<div className={actionButtonClassName}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
197 |
<div className="flex items-center justify-center">
|
198 |
{
|
199 |
copied ? <LuCopyCheck className="w-4 h-4" />
|
|
|
242 |
{/** VIDEO DESCRIPTION - VERTICAL */}
|
243 |
<div className={cn(
|
244 |
`flex flex-col p-3`,
|
245 |
+
`transition-all duration-200 ease-in-out`,
|
246 |
`rounded-xl`,
|
247 |
`bg-neutral-700/50`,
|
248 |
+
`text-sm`,
|
249 |
)}>
|
250 |
<p>{video.description}</p>
|
251 |
</div>
|
252 |
</div>
|
253 |
<div className={cn(
|
254 |
+
`w-40 sm:w-56 md:w-64 lg:w-72 xl:w-[450px]`,
|
255 |
+
`transition-all duration-200 ease-in-out`,
|
256 |
`hidden sm:flex flex-col`,
|
257 |
+
`pl-5 pr-1 sm:pr-2 md:pr-3 lg:pr-4 xl:pr-6 2xl:pr-8`,
|
258 |
)}>
|
259 |
<RecommendedVideos video={video} />
|
260 |
</div>
|
src/app/views/user-account-view/index.tsx
CHANGED
@@ -10,6 +10,7 @@ import { ChannelList } from "@/app/interface/channel-list"
|
|
10 |
import { localStorageKeys } from "@/app/state/localStorageKeys"
|
11 |
import { defaultSettings } from "@/app/state/defaultSettings"
|
12 |
import { Input } from "@/components/ui/input"
|
|
|
13 |
|
14 |
export function UserAccountView() {
|
15 |
const [_isPending, startTransition] = useTransition()
|
@@ -73,13 +74,22 @@ export function UserAccountView() {
|
|
73 |
<h2 className="text-3xl font-bold">Your custom channels:</h2>
|
74 |
{userChannels?.length ? <ChannelList
|
75 |
layout="grid"
|
76 |
-
channels={
|
|
|
|
|
|
|
|
|
|
|
77 |
onSelect={(userChannel) => {
|
78 |
-
|
|
|
|
|
79 |
setView("user_channel")
|
80 |
}}
|
81 |
-
/>
|
|
|
82 |
</div> : null}
|
|
|
83 |
</div>
|
84 |
)
|
85 |
}
|
|
|
10 |
import { localStorageKeys } from "@/app/state/localStorageKeys"
|
11 |
import { defaultSettings } from "@/app/state/defaultSettings"
|
12 |
import { Input } from "@/components/ui/input"
|
13 |
+
import { ChannelInfo } from "@/types"
|
14 |
|
15 |
export function UserAccountView() {
|
16 |
const [_isPending, startTransition] = useTransition()
|
|
|
74 |
<h2 className="text-3xl font-bold">Your custom channels:</h2>
|
75 |
{userChannels?.length ? <ChannelList
|
76 |
layout="grid"
|
77 |
+
channels={[
|
78 |
+
// add a fake button to the list, at the beginning
|
79 |
+
// { id: "" } as ChannelInfo,
|
80 |
+
|
81 |
+
...userChannels
|
82 |
+
]}
|
83 |
onSelect={(userChannel) => {
|
84 |
+
if (userChannel.id) {
|
85 |
+
setUserChannel(userChannel)
|
86 |
+
}
|
87 |
setView("user_channel")
|
88 |
}}
|
89 |
+
/>
|
90 |
+
: isLoaded ? <p>You don't seem to have any channel yet. See @flngr on X to learn more about how to do this!</p> : <p>Loading channels..</p>}
|
91 |
</div> : null}
|
92 |
+
|
93 |
</div>
|
94 |
)
|
95 |
}
|
src/app/views/user-channel-view/index.tsx
CHANGED
@@ -17,6 +17,7 @@ import { PendingVideoList } from "@/app/interface/pending-video-list"
|
|
17 |
import { getChannelVideos } from "@/app/server/actions/ai-tube-hf/getChannelVideos"
|
18 |
import { parseVideoModelName } from "@/app/server/actions/utils/parseVideoModelName"
|
19 |
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
|
20 |
|
21 |
export function UserChannelView() {
|
22 |
const [_isPending, startTransition] = useTransition()
|
@@ -24,11 +25,7 @@ export function UserChannelView() {
|
|
24 |
localStorageKeys.huggingfaceApiKey,
|
25 |
defaultSettings.huggingfaceApiKey
|
26 |
)
|
27 |
-
|
28 |
-
const defaultVideoModel = "SVD"
|
29 |
-
const defaultVoice = "Julian"
|
30 |
|
31 |
-
|
32 |
const [titleDraft, setTitleDraft] = useState("")
|
33 |
const [descriptionDraft, setDescriptionDraft] = useState("")
|
34 |
const [tagsDraft, setTagsDraft] = useState("")
|
|
|
17 |
import { getChannelVideos } from "@/app/server/actions/ai-tube-hf/getChannelVideos"
|
18 |
import { parseVideoModelName } from "@/app/server/actions/utils/parseVideoModelName"
|
19 |
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
20 |
+
import { defaultVideoModel, defaultVoice } from "@/app/config"
|
21 |
|
22 |
export function UserChannelView() {
|
23 |
const [_isPending, startTransition] = useTransition()
|
|
|
25 |
localStorageKeys.huggingfaceApiKey,
|
26 |
defaultSettings.huggingfaceApiKey
|
27 |
)
|
|
|
|
|
|
|
28 |
|
|
|
29 |
const [titleDraft, setTitleDraft] = useState("")
|
30 |
const [descriptionDraft, setDescriptionDraft] = useState("")
|
31 |
const [tagsDraft, setTagsDraft] = useState("")
|