Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
•
df83860
1
Parent(s):
93f8352
working on the route system
Browse files- src/app/channel/page.tsx +5 -0
- src/app/channels/page.tsx +11 -0
- src/app/globals.css +5 -17
- src/app/interface/left-menu/index.tsx +19 -14
- src/app/interface/top-header/index.tsx +21 -8
- src/app/interface/tube-layout/index.tsx +39 -0
- src/app/interface/video-card/index.tsx +71 -68
- src/app/layout.tsx +25 -0
- src/app/main.tsx +49 -34
- src/app/page.tsx +1 -43
- src/app/server/actions/ai-tube-hf/getIndex.ts +9 -4
- src/app/server/actions/ai-tube-hf/getTags.ts +36 -0
- src/app/server/actions/ai-tube-hf/getVideo.ts +22 -0
- src/app/state/{locaStorageKeys.ts → localStorageKeys.ts} +0 -0
- src/app/state/useStore.ts +20 -0
- src/app/views/home-view/index.tsx +3 -0
- src/app/views/not-found-view/index.tsx +15 -0
- src/app/views/public-video-view/index.tsx +2 -4
- src/app/views/user-account-view/index.tsx +1 -1
- src/app/views/user-channel-view/index.tsx +1 -1
- src/app/views/user-channels-view/index.tsx +1 -1
- src/app/watch/page.tsx +84 -0
- src/types.ts +1 -0
src/app/channel/page.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Main } from "../main"
|
2 |
+
|
3 |
+
export default async function ChannelPage() {
|
4 |
+
return (<Main />)
|
5 |
+
}
|
src/app/channels/page.tsx
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect, useState, useTransition } from "react"
|
2 |
+
import Head from "next/head"
|
3 |
+
import Script from "next/script"
|
4 |
+
import { Metadata, ResolvingMetadata } from "next"
|
5 |
+
|
6 |
+
|
7 |
+
import { Main } from "../main"
|
8 |
+
|
9 |
+
export default async function ChannelsPage() {
|
10 |
+
return (<Main />)
|
11 |
+
}
|
src/app/globals.css
CHANGED
@@ -3,16 +3,16 @@
|
|
3 |
@tailwind utilities;
|
4 |
|
5 |
:root {
|
6 |
-
--foreground-rgb:
|
7 |
-
--background-start-rgb:
|
8 |
-
--background-end-rgb:
|
9 |
}
|
10 |
|
11 |
@media (prefers-color-scheme: dark) {
|
12 |
:root {
|
13 |
--foreground-rgb: 255, 255, 255;
|
14 |
-
--background-start-rgb:
|
15 |
-
--background-end-rgb:
|
16 |
}
|
17 |
}
|
18 |
|
@@ -25,15 +25,3 @@ body {
|
|
25 |
)
|
26 |
rgb(var(--background-start-rgb));
|
27 |
}
|
28 |
-
|
29 |
-
|
30 |
-
/* this is the trick to bypass the style={{}} attribute when printing */
|
31 |
-
@media print {
|
32 |
-
.comic-page[style] { width: 100vw !important; }
|
33 |
-
}
|
34 |
-
|
35 |
-
|
36 |
-
.render-to-image .comic-panel {
|
37 |
-
height: auto !important;
|
38 |
-
/* max-width: fit-content !important; */
|
39 |
-
}
|
|
|
3 |
@tailwind utilities;
|
4 |
|
5 |
:root {
|
6 |
+
--foreground-rgb: 255, 255, 255;
|
7 |
+
--background-start-rgb: 10, 10, 10;
|
8 |
+
--background-end-rgb: 10, 10, 10;
|
9 |
}
|
10 |
|
11 |
@media (prefers-color-scheme: dark) {
|
12 |
:root {
|
13 |
--foreground-rgb: 255, 255, 255;
|
14 |
+
--background-start-rgb: 10, 10, 10;
|
15 |
+
--background-end-rgb: 10, 10, 10;
|
16 |
}
|
17 |
}
|
18 |
|
|
|
25 |
)
|
26 |
rgb(var(--background-start-rgb));
|
27 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/interface/left-menu/index.tsx
CHANGED
@@ -8,6 +8,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 |
|
12 |
|
13 |
export function LeftMenu() {
|
@@ -26,20 +27,24 @@ export function LeftMenu() {
|
|
26 |
<div className={cn(
|
27 |
`flex flex-col w-full`,
|
28 |
)}>
|
29 |
-
<
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
|
|
|
|
|
|
|
|
43 |
</div>
|
44 |
<div className={cn(
|
45 |
`flex flex-col w-full`,
|
|
|
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() {
|
|
|
27 |
<div className={cn(
|
28 |
`flex flex-col w-full`,
|
29 |
)}>
|
30 |
+
<Link href="/">
|
31 |
+
<MenuItem
|
32 |
+
icon={<RiHome8Line className="h-6 w-6" />}
|
33 |
+
selected={view === "home"}
|
34 |
+
// </Link>onClick={() => setView("home")}
|
35 |
+
>
|
36 |
+
Discover
|
37 |
+
</MenuItem>
|
38 |
+
</Link>
|
39 |
+
<Link href="/channels">
|
40 |
+
<MenuItem
|
41 |
+
icon={<GrChannel className="h-5 w-5" />}
|
42 |
+
selected={view === "public_channels"}
|
43 |
+
// onClick={() => setView("public_channels")}
|
44 |
+
>
|
45 |
+
Channels
|
46 |
+
</MenuItem>
|
47 |
+
</Link>
|
48 |
</div>
|
49 |
<div className={cn(
|
50 |
`flex flex-col w-full`,
|
src/app/interface/top-header/index.tsx
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import { Pathway_Gothic_One } from 'next/font/google'
|
2 |
import { PiPopcornBold } from "react-icons/pi"
|
3 |
|
@@ -11,9 +13,10 @@ const pathway = Pathway_Gothic_One({
|
|
11 |
import { videoCategoriesWithLabels } from "@/app/state/categories"
|
12 |
import { useStore } from "@/app/state/useStore"
|
13 |
import { cn } from "@/lib/utils"
|
14 |
-
import {
|
15 |
|
16 |
export function TopHeader() {
|
|
|
17 |
const view = useStore(s => s.view)
|
18 |
const setView = useStore(s => s.setView)
|
19 |
const displayMode = useStore(s => s.displayMode)
|
@@ -32,6 +35,9 @@ export function TopHeader() {
|
|
32 |
const currentVideos = useStore(s => s.currentVideos)
|
33 |
const currentVideo = useStore(s => s.currentVideo)
|
34 |
|
|
|
|
|
|
|
35 |
const isNormalSize = headerMode === "normal"
|
36 |
|
37 |
|
@@ -44,6 +50,13 @@ export function TopHeader() {
|
|
44 |
setMenuMode("normal_icon")
|
45 |
}
|
46 |
}, [view])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
|
48 |
return (
|
49 |
<div className={cn(
|
@@ -109,27 +122,27 @@ export function TopHeader() {
|
|
109 |
`text-[13px] font-semibold`,
|
110 |
`mb-4`
|
111 |
)}>
|
112 |
-
{
|
113 |
-
.map(([ key, label ]) => (
|
114 |
<div
|
115 |
-
key={
|
116 |
className={cn(
|
117 |
`flex flex-col items-center justify-center`,
|
118 |
`rounded-lg px-3 py-1 h-8`,
|
119 |
`cursor-pointer`,
|
120 |
`transition-all duration-200 ease-in-out`,
|
121 |
-
currentTag ===
|
122 |
? `bg-neutral-100 text-neutral-800`
|
123 |
: `bg-neutral-800 text-neutral-50/90 hover:bg-neutral-700 hover:text-neutral-50/90`,
|
124 |
// `text-clip`
|
125 |
)}
|
126 |
onClick={() => {
|
127 |
-
setCurrentTag(
|
128 |
}}
|
129 |
>
|
130 |
<span className={cn(
|
131 |
-
`text-center
|
132 |
-
|
|
|
133 |
</div>
|
134 |
))}
|
135 |
</div> : null}
|
|
|
1 |
+
import { useEffect, useTransition } from 'react'
|
2 |
+
|
3 |
import { Pathway_Gothic_One } from 'next/font/google'
|
4 |
import { PiPopcornBold } from "react-icons/pi"
|
5 |
|
|
|
13 |
import { videoCategoriesWithLabels } from "@/app/state/categories"
|
14 |
import { useStore } from "@/app/state/useStore"
|
15 |
import { cn } from "@/lib/utils"
|
16 |
+
import { getTags } from '@/app/server/actions/ai-tube-hf/getTags'
|
17 |
|
18 |
export function TopHeader() {
|
19 |
+
const [_pending, startTransition] = useTransition()
|
20 |
const view = useStore(s => s.view)
|
21 |
const setView = useStore(s => s.setView)
|
22 |
const displayMode = useStore(s => s.displayMode)
|
|
|
35 |
const currentVideos = useStore(s => s.currentVideos)
|
36 |
const currentVideo = useStore(s => s.currentVideo)
|
37 |
|
38 |
+
const currentTags = useStore(s => s.currentTags)
|
39 |
+
const setCurrentTags = useStore(s => s.setCurrentTags)
|
40 |
+
|
41 |
const isNormalSize = headerMode === "normal"
|
42 |
|
43 |
|
|
|
50 |
setMenuMode("normal_icon")
|
51 |
}
|
52 |
}, [view])
|
53 |
+
|
54 |
+
useEffect(() => {
|
55 |
+
startTransition(async () => {
|
56 |
+
const tags = await getTags()
|
57 |
+
setCurrentTags(tags)
|
58 |
+
})
|
59 |
+
}, [])
|
60 |
|
61 |
return (
|
62 |
<div className={cn(
|
|
|
122 |
`text-[13px] font-semibold`,
|
123 |
`mb-4`
|
124 |
)}>
|
125 |
+
{currentTags.slice(0, 9).map(tag => (
|
|
|
126 |
<div
|
127 |
+
key={tag}
|
128 |
className={cn(
|
129 |
`flex flex-col items-center justify-center`,
|
130 |
`rounded-lg px-3 py-1 h-8`,
|
131 |
`cursor-pointer`,
|
132 |
`transition-all duration-200 ease-in-out`,
|
133 |
+
currentTag === tag
|
134 |
? `bg-neutral-100 text-neutral-800`
|
135 |
: `bg-neutral-800 text-neutral-50/90 hover:bg-neutral-700 hover:text-neutral-50/90`,
|
136 |
// `text-clip`
|
137 |
)}
|
138 |
onClick={() => {
|
139 |
+
setCurrentTag(currentTag === tag ? undefined : tag)
|
140 |
}}
|
141 |
>
|
142 |
<span className={cn(
|
143 |
+
`text-center`,
|
144 |
+
`capitalize`,
|
145 |
+
)}>{tag}</span>
|
146 |
</div>
|
147 |
))}
|
148 |
</div> : null}
|
src/app/interface/tube-layout/index.tsx
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
import { ReactNode } from "react"
|
3 |
+
|
4 |
+
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)
|
13 |
+
return (
|
14 |
+
<div className={cn(
|
15 |
+
`dark flex flex-row h-screen w-screen inset-0 overflow-hidden`,
|
16 |
+
view === "public_video"
|
17 |
+
? `bg-gradient-radial from-neutral-900 to-neutral-950`
|
18 |
+
: ''// bg-gradient-to-br from-neutral-950 via-neutral-950 to-neutral-950`
|
19 |
+
|
20 |
+
)}>
|
21 |
+
<LeftMenu />
|
22 |
+
<div className={cn(
|
23 |
+
`flex flex-col`,
|
24 |
+
`w-[calc(100vw-96px)]`,
|
25 |
+
`px-2`
|
26 |
+
)}>
|
27 |
+
<TopHeader />
|
28 |
+
<main className={cn(
|
29 |
+
`w-full overflow-x-hidden overflow-y-scroll`,
|
30 |
+
headerMode === "normal"
|
31 |
+
? `h-[calc(100vh-112px)]`
|
32 |
+
: `h-[calc(100vh-48px)]`
|
33 |
+
)}>
|
34 |
+
{children}
|
35 |
+
</main>
|
36 |
+
</div>
|
37 |
+
</div>
|
38 |
+
)
|
39 |
+
}
|
src/app/interface/video-card/index.tsx
CHANGED
@@ -5,6 +5,7 @@ import { cn } from "@/lib/utils"
|
|
5 |
import { VideoInfo } from "@/types"
|
6 |
import { formatDuration } from "@/lib/formatDuration"
|
7 |
import { formatTimeAgo } from "@/lib/formatTimeAgo"
|
|
|
8 |
|
9 |
export function VideoCard({
|
10 |
video,
|
@@ -37,83 +38,85 @@ export function VideoCard({
|
|
37 |
}
|
38 |
|
39 |
return (
|
40 |
-
<
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
<div
|
54 |
-
className={cn(
|
55 |
-
`flex flex-col aspect-video items-center justify-center`,
|
56 |
-
`rounded-xl overflow-hidden`,
|
57 |
-
)}
|
58 |
>
|
59 |
-
<
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
<div className={cn(
|
72 |
-
`-mt-8`,
|
73 |
-
`mr-0`,
|
74 |
-
)}
|
75 |
-
>
|
76 |
<div className={cn(
|
77 |
-
|
78 |
-
`mr-
|
79 |
-
`flex flex-col items-center justify-center text-center`,
|
80 |
-
`bg-neutral-900 rounded`,
|
81 |
-
`text-2xs font-semibold px-[3px] py-[1px]`,
|
82 |
)}
|
83 |
-
>
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
src="huggingface-avatar.jpeg"
|
94 |
-
/>
|
95 |
</div>
|
96 |
</div>
|
97 |
-
<div className=
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
|
|
105 |
</div>
|
106 |
-
<div className=
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
</div>
|
115 |
</div>
|
116 |
</div>
|
117 |
-
</
|
118 |
)
|
119 |
}
|
|
|
5 |
import { VideoInfo } from "@/types"
|
6 |
import { formatDuration } from "@/lib/formatDuration"
|
7 |
import { formatTimeAgo } from "@/lib/formatTimeAgo"
|
8 |
+
import Link from "next/link"
|
9 |
|
10 |
export function VideoCard({
|
11 |
video,
|
|
|
38 |
}
|
39 |
|
40 |
return (
|
41 |
+
<Link href={`/watch?v=${video.id}`}>
|
42 |
+
<div
|
43 |
+
className={cn(
|
44 |
+
`w-full`,
|
45 |
+
`flex flex-col`,
|
46 |
+
`bg-line-900`,
|
47 |
+
`space-y-3`,
|
48 |
+
`cursor-pointer`,
|
49 |
+
className,
|
50 |
+
)}
|
51 |
+
onPointerEnter={handlePointerEnter}
|
52 |
+
onPointerLeave={handlePointerLeave}
|
53 |
+
// onClick={handleClick}
|
|
|
|
|
|
|
|
|
|
|
54 |
>
|
55 |
+
<div
|
56 |
+
className={cn(
|
57 |
+
`flex flex-col aspect-video items-center justify-center`,
|
58 |
+
`rounded-xl overflow-hidden`,
|
59 |
+
)}
|
60 |
+
>
|
61 |
+
<video
|
62 |
+
ref={ref}
|
63 |
+
src={video.assetUrl}
|
64 |
+
className="w-full"
|
65 |
+
onLoadedMetadata={handleLoad}
|
66 |
+
muted
|
67 |
+
/>
|
68 |
|
69 |
+
<div className={cn(
|
70 |
+
``,
|
71 |
+
`w-full flex flex-row items-end justify-end`
|
72 |
+
)}>
|
|
|
|
|
|
|
|
|
|
|
73 |
<div className={cn(
|
74 |
+
`-mt-8`,
|
75 |
+
`mr-0`,
|
|
|
|
|
|
|
76 |
)}
|
77 |
+
>
|
78 |
+
<div className={cn(
|
79 |
+
`mb-[5px]`,
|
80 |
+
`mr-[5px]`,
|
81 |
+
`flex flex-col items-center justify-center text-center`,
|
82 |
+
`bg-neutral-900 rounded`,
|
83 |
+
`text-2xs font-semibold px-[3px] py-[1px]`,
|
84 |
+
)}
|
85 |
+
>{formatDuration(duration)}</div>
|
86 |
+
</div>
|
|
|
|
|
87 |
</div>
|
88 |
</div>
|
89 |
+
<div className={cn(
|
90 |
+
`flex flex-row space-x-4`,
|
91 |
+
)}>
|
92 |
+
<div className="flex flex-col">
|
93 |
+
<div className="flex w-9 rounded-full overflow-hidden">
|
94 |
+
<img
|
95 |
+
src="huggingface-avatar.jpeg"
|
96 |
+
/>
|
97 |
+
</div>
|
98 |
</div>
|
99 |
+
<div className="flex flex-col flex-grow">
|
100 |
+
<h3 className="text-zinc-100 text-base font-medium mb-0 line-clamp-2">{video.label}</h3>
|
101 |
+
<div className={cn(
|
102 |
+
`flex flex-row items-center`,
|
103 |
+
`text-neutral-400 text-sm font-normal space-x-1`,
|
104 |
+
)}>
|
105 |
+
<div>{video.channel.label}</div>
|
106 |
+
<div><RiCheckboxCircleFill className="" /></div>
|
107 |
+
</div>
|
108 |
+
<div className={cn(
|
109 |
+
`flex flex-row`,
|
110 |
+
`text-neutral-400 text-sm font-normal`,
|
111 |
+
`space-x-1`
|
112 |
+
)}>
|
113 |
+
<div>0 views</div>
|
114 |
+
<div className="font-semibold scale-125">·</div>
|
115 |
+
<div>{formatTimeAgo(video.updatedAt)}</div>
|
116 |
+
</div>
|
117 |
</div>
|
118 |
</div>
|
119 |
</div>
|
120 |
+
</Link>
|
121 |
)
|
122 |
}
|
src/app/layout.tsx
CHANGED
@@ -4,6 +4,7 @@ import { Roboto } from 'next/font/google'
|
|
4 |
import { cn } from '@/lib/utils'
|
5 |
|
6 |
import './globals.css'
|
|
|
7 |
|
8 |
const roboto = Roboto({
|
9 |
weight: ['100', '300', '400', '500', '700', '900'],
|
@@ -24,12 +25,36 @@ export default function RootLayout({
|
|
24 |
}) {
|
25 |
return (
|
26 |
<html lang="en">
|
|
|
|
|
|
|
|
|
|
|
27 |
<body className={cn(
|
28 |
`h-full w-full overflow-auto`,
|
|
|
29 |
roboto.className
|
30 |
)}>
|
31 |
{children}
|
32 |
</body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
</html>
|
34 |
)
|
35 |
}
|
|
|
4 |
import { cn } from '@/lib/utils'
|
5 |
|
6 |
import './globals.css'
|
7 |
+
import Head from 'next/head'
|
8 |
|
9 |
const roboto = Roboto({
|
10 |
weight: ['100', '300', '400', '500', '700', '900'],
|
|
|
25 |
}) {
|
26 |
return (
|
27 |
<html lang="en">
|
28 |
+
<Head>
|
29 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
30 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" crossOrigin="anonymous" />
|
31 |
+
<meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
|
32 |
+
</Head>
|
33 |
<body className={cn(
|
34 |
`h-full w-full overflow-auto`,
|
35 |
+
`dark text-neutral-100 bg-neutral-950`,
|
36 |
roboto.className
|
37 |
)}>
|
38 |
{children}
|
39 |
</body>
|
40 |
+
|
41 |
+
{/*
|
42 |
+
TODO: use a new tracker
|
43 |
+
|
44 |
+
import Script from "next/script"
|
45 |
+
|
46 |
+
This is the kind of project on which we want custom analytics!
|
47 |
+
<Script src="https://www.googletagmanager.com/gtag/js?id=GTM-NJ2ZZFBX" />
|
48 |
+
<Script id="google-analytics">
|
49 |
+
{`
|
50 |
+
window.dataLayer = window.dataLayer || [];
|
51 |
+
function gtag(){dataLayer.push(arguments);}
|
52 |
+
gtag('js', new Date());
|
53 |
+
|
54 |
+
gtag('config', 'GTM-NJ2ZZFBX');
|
55 |
+
`}
|
56 |
+
</Script>
|
57 |
+
*/}
|
58 |
</html>
|
59 |
)
|
60 |
}
|
src/app/main.tsx
CHANGED
@@ -1,8 +1,5 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import { cn } from "@/lib/utils"
|
4 |
-
import { TopHeader } from "./interface/top-header"
|
5 |
-
import { LeftMenu } from "./interface/left-menu"
|
6 |
import { useStore } from "./state/useStore"
|
7 |
import { HomeView } from "./views/home-view"
|
8 |
import { PublicChannelsView } from "./views/public-channels-view"
|
@@ -11,39 +8,57 @@ import { PublicChannelView } from "./views/public-channel-view"
|
|
11 |
import { UserChannelView } from "./views/user-channel-view"
|
12 |
import { PublicVideoView } from "./views/public-video-view"
|
13 |
import { UserAccountView } from "./views/user-account-view"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
-
export function Main() {
|
16 |
const view = useStore(s => s.view)
|
17 |
-
|
18 |
return (
|
19 |
-
<
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
<
|
24 |
-
<
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
<div className={cn(
|
31 |
-
`w-full overflow-x-hidden overflow-y-scroll`,
|
32 |
-
headerMode === "normal"
|
33 |
-
? `h-[calc(100vh-112px)]`
|
34 |
-
: `h-[calc(100vh-48px)]`
|
35 |
-
)}>
|
36 |
-
{view === "home" && <HomeView />}
|
37 |
-
{view === "public_video" && <PublicVideoView />}
|
38 |
-
{view === "public_channels" && <PublicChannelsView />}
|
39 |
-
{view === "public_channel" && <PublicChannelView />}
|
40 |
-
{view === "user_channels" && <UserChannelsView />}
|
41 |
-
{/*view === "user_videos" && <UserVideosView />*/}
|
42 |
-
{view === "user_channel" && <UserChannelView />}
|
43 |
-
{view === "user_account" && <UserAccountView />}
|
44 |
-
|
45 |
-
</div>
|
46 |
-
</div>
|
47 |
-
</div>
|
48 |
)
|
49 |
}
|
|
|
1 |
"use client"
|
2 |
|
|
|
|
|
|
|
3 |
import { useStore } from "./state/useStore"
|
4 |
import { HomeView } from "./views/home-view"
|
5 |
import { PublicChannelsView } from "./views/public-channels-view"
|
|
|
8 |
import { UserChannelView } from "./views/user-channel-view"
|
9 |
import { PublicVideoView } from "./views/public-video-view"
|
10 |
import { UserAccountView } from "./views/user-account-view"
|
11 |
+
import { NotFoundView } from "./views/not-found-view"
|
12 |
+
import { VideoInfo } from "@/types"
|
13 |
+
import { useEffect } from "react"
|
14 |
+
import { usePathname } from "next/navigation"
|
15 |
+
import { TubeLayout } from "./interface/tube-layout"
|
16 |
+
|
17 |
+
// this is where we transition from the server-side space
|
18 |
+
// and the client-side space
|
19 |
+
// basically, all the views are generated in client-side space
|
20 |
+
// so the role of Main is to map server-side provided params
|
21 |
+
// to the Zustand store (client-side)
|
22 |
+
//
|
23 |
+
// one benefit of doing this is that we will able to add some animations/transitions
|
24 |
+
// more easily
|
25 |
+
export function Main({
|
26 |
+
video
|
27 |
+
}: {
|
28 |
+
// server side params
|
29 |
+
video?: VideoInfo
|
30 |
+
}) {
|
31 |
+
const pathname = usePathname()
|
32 |
+
|
33 |
+
const setCurrentVideo = useStore(s => s.setCurrentVideo)
|
34 |
+
const setView = useStore(s => s.setView)
|
35 |
+
const setPathname = useStore(s => s.setPathname)
|
36 |
+
|
37 |
+
useEffect(() => {
|
38 |
+
if (video?.id) {
|
39 |
+
setCurrentVideo(video)
|
40 |
+
}
|
41 |
+
}, [video?.id])
|
42 |
+
|
43 |
+
|
44 |
+
useEffect(() => {
|
45 |
+
setPathname(pathname)
|
46 |
+
}, [pathname])
|
47 |
+
|
48 |
|
|
|
49 |
const view = useStore(s => s.view)
|
50 |
+
|
51 |
return (
|
52 |
+
<TubeLayout>
|
53 |
+
{view === "home" && <HomeView />}
|
54 |
+
{view === "public_video" && <PublicVideoView />}
|
55 |
+
{view === "public_channels" && <PublicChannelsView />}
|
56 |
+
{view === "public_channel" && <PublicChannelView />}
|
57 |
+
{view === "user_channels" && <UserChannelsView />}
|
58 |
+
{/*view === "user_videos" && <UserVideosView />*/}
|
59 |
+
{view === "user_channel" && <UserChannelView />}
|
60 |
+
{view === "user_account" && <UserAccountView />}
|
61 |
+
{view === "not_found" && <NotFoundView />}
|
62 |
+
</TubeLayout>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
)
|
64 |
}
|
src/app/page.tsx
CHANGED
@@ -1,50 +1,8 @@
|
|
1 |
-
"use client"
|
2 |
-
|
3 |
-
import { useEffect, useState } from "react"
|
4 |
-
import Head from "next/head"
|
5 |
-
import Script from "next/script"
|
6 |
-
|
7 |
-
import { cn } from "@/lib/utils"
|
8 |
-
import { useStore } from "@/app/state/useStore"
|
9 |
|
10 |
import { Main } from "./main"
|
11 |
|
12 |
-
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
13 |
-
|
14 |
export default function Page() {
|
15 |
-
const view = useStore(s => s.view)
|
16 |
-
const [isLoaded, setLoaded] = useState(false)
|
17 |
-
useEffect(() => { setLoaded(true) }, [])
|
18 |
return (
|
19 |
-
|
20 |
-
<Head>
|
21 |
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
22 |
-
<link rel="preconnect" href="https://fonts.googleapis.com" crossOrigin="anonymous" />
|
23 |
-
<meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86" />
|
24 |
-
</Head>
|
25 |
-
<main className={cn(
|
26 |
-
`light text-neutral-100`,
|
27 |
-
// `bg-gradient-to-r from-green-500 to-yellow-400`,
|
28 |
-
view === "public_video"
|
29 |
-
? `bg-gradient-radial from-neutral-900 to-neutral-950`
|
30 |
-
: `bg-neutral-950` // bg-gradient-to-br from-neutral-950 via-neutral-950 to-neutral-950`
|
31 |
-
)}>
|
32 |
-
{isLoaded && <Main />}
|
33 |
-
{/*
|
34 |
-
TODO: use a new tracker
|
35 |
-
This is the kind of project on which we want custom analytics!
|
36 |
-
<Script src="https://www.googletagmanager.com/gtag/js?id=GTM-NJ2ZZFBX" />
|
37 |
-
<Script id="google-analytics">
|
38 |
-
{`
|
39 |
-
window.dataLayer = window.dataLayer || [];
|
40 |
-
function gtag(){dataLayer.push(arguments);}
|
41 |
-
gtag('js', new Date());
|
42 |
-
|
43 |
-
gtag('config', 'GTM-NJ2ZZFBX');
|
44 |
-
`}
|
45 |
-
</Script>
|
46 |
-
*/}
|
47 |
-
</main>
|
48 |
-
</>
|
49 |
)
|
50 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
|
2 |
import { Main } from "./main"
|
3 |
|
|
|
|
|
4 |
export default function Page() {
|
|
|
|
|
|
|
5 |
return (
|
6 |
+
<Main />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
)
|
8 |
}
|
src/app/server/actions/ai-tube-hf/getIndex.ts
CHANGED
@@ -4,17 +4,19 @@ import { adminUsername } from "../config"
|
|
4 |
|
5 |
export async function getIndex({
|
6 |
status,
|
7 |
-
renewCache,
|
|
|
8 |
}: {
|
9 |
status: VideoStatus
|
10 |
|
11 |
renewCache?: boolean
|
|
|
12 |
}): Promise<Record<string, VideoInfo>> {
|
13 |
try {
|
14 |
const response = await fetch(
|
15 |
`https://huggingface.co/datasets/${adminUsername}/ai-tube-index/raw/main/${status}.json`
|
16 |
, {
|
17 |
-
cache: "no-store"
|
18 |
})
|
19 |
|
20 |
const jsonResponse = await response?.json()
|
@@ -31,7 +33,10 @@ export async function getIndex({
|
|
31 |
|
32 |
return videos
|
33 |
} catch (err) {
|
34 |
-
|
35 |
-
|
|
|
|
|
|
|
36 |
}
|
37 |
}
|
|
|
4 |
|
5 |
export async function getIndex({
|
6 |
status,
|
7 |
+
renewCache = true,
|
8 |
+
neverThrow = true,
|
9 |
}: {
|
10 |
status: VideoStatus
|
11 |
|
12 |
renewCache?: boolean
|
13 |
+
neverThrow?: boolean
|
14 |
}): Promise<Record<string, VideoInfo>> {
|
15 |
try {
|
16 |
const response = await fetch(
|
17 |
`https://huggingface.co/datasets/${adminUsername}/ai-tube-index/raw/main/${status}.json`
|
18 |
, {
|
19 |
+
cache: renewCache ? "no-store" : "default"
|
20 |
})
|
21 |
|
22 |
const jsonResponse = await response?.json()
|
|
|
33 |
|
34 |
return videos
|
35 |
} catch (err) {
|
36 |
+
if (neverThrow) {
|
37 |
+
console.error(`failed to get index ${status}:`, err)
|
38 |
+
return {}
|
39 |
+
}
|
40 |
+
throw err
|
41 |
}
|
42 |
}
|
src/app/server/actions/ai-tube-hf/getTags.ts
ADDED
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use server"
|
2 |
+
|
3 |
+
import { getIndex } from "./getIndex"
|
4 |
+
|
5 |
+
export async function getTags({
|
6 |
+
renewCache = true,
|
7 |
+
neverThrow = true,
|
8 |
+
}: {
|
9 |
+
renewCache?: boolean
|
10 |
+
neverThrow?: boolean
|
11 |
+
} = {
|
12 |
+
renewCache: true,
|
13 |
+
neverThrow: true,
|
14 |
+
}): Promise<string[]> {
|
15 |
+
try {
|
16 |
+
const published = Object.values(await getIndex({
|
17 |
+
status: "published",
|
18 |
+
renewCache,
|
19 |
+
}))
|
20 |
+
|
21 |
+
const tags: Record<string, number> = {}
|
22 |
+
for (const video of published) {
|
23 |
+
for (const tag of video.tags) {
|
24 |
+
const key = tag.trim().toLowerCase()
|
25 |
+
tags[key] = 1 + (tags[key] || 0)
|
26 |
+
}
|
27 |
+
}
|
28 |
+
|
29 |
+
return Object.entries(tags).sort((a, b) => b[1] - a[1]).map(i => i[0])
|
30 |
+
} catch (err) {
|
31 |
+
if (neverThrow) {
|
32 |
+
return []
|
33 |
+
}
|
34 |
+
throw err
|
35 |
+
}
|
36 |
+
}
|
src/app/server/actions/ai-tube-hf/getVideo.ts
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use server"
|
2 |
+
|
3 |
+
import { VideoInfo } from "@/types"
|
4 |
+
|
5 |
+
import { getIndex } from "./getIndex"
|
6 |
+
|
7 |
+
export async function getVideo(videoId?: string | string[] | null): Promise<VideoInfo> {
|
8 |
+
const id = `${videoId || ""}`
|
9 |
+
|
10 |
+
if (!id) {
|
11 |
+
throw new Error(`cannot get the video, invalid id: "${id}"`)
|
12 |
+
}
|
13 |
+
const published = await getIndex({ status: "published" })
|
14 |
+
|
15 |
+
const video = published[id] || undefined
|
16 |
+
|
17 |
+
if (!video) {
|
18 |
+
throw new Error(`cannot get the video, nothing found for id "${id}"`)
|
19 |
+
}
|
20 |
+
|
21 |
+
return video
|
22 |
+
}
|
src/app/state/{locaStorageKeys.ts → localStorageKeys.ts}
RENAMED
File without changes
|
src/app/state/useStore.ts
CHANGED
@@ -17,6 +17,8 @@ export const useStore = create<{
|
|
17 |
view: InterfaceView
|
18 |
setView: (view?: InterfaceView) => void
|
19 |
|
|
|
|
|
20 |
currentChannel?: ChannelInfo
|
21 |
setCurrentChannel: (currentChannel?: ChannelInfo) => void
|
22 |
|
@@ -26,6 +28,9 @@ export const useStore = create<{
|
|
26 |
currentTag?: string
|
27 |
setCurrentTag: (currentTag?: string) => void
|
28 |
|
|
|
|
|
|
|
29 |
currentVideos: VideoInfo[]
|
30 |
setCurrentVideos: (currentVideos: VideoInfo[]) => void
|
31 |
|
@@ -46,6 +51,16 @@ export const useStore = create<{
|
|
46 |
set({ view: view || "home" })
|
47 |
},
|
48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
headerMode: "normal",
|
50 |
setHeaderMode: (headerMode: InterfaceHeaderMode) => {
|
51 |
set({ headerMode })
|
@@ -73,6 +88,11 @@ export const useStore = create<{
|
|
73 |
set({ currentTag })
|
74 |
},
|
75 |
|
|
|
|
|
|
|
|
|
|
|
76 |
currentVideos: [],
|
77 |
setCurrentVideos: (currentVideos: VideoInfo[] = []) => {
|
78 |
set({
|
|
|
17 |
view: InterfaceView
|
18 |
setView: (view?: InterfaceView) => void
|
19 |
|
20 |
+
setPathname: (patname: string) => void
|
21 |
+
|
22 |
currentChannel?: ChannelInfo
|
23 |
setCurrentChannel: (currentChannel?: ChannelInfo) => void
|
24 |
|
|
|
28 |
currentTag?: string
|
29 |
setCurrentTag: (currentTag?: string) => void
|
30 |
|
31 |
+
currentTags: string[]
|
32 |
+
setCurrentTags: (currentTags?: string[]) => void
|
33 |
+
|
34 |
currentVideos: VideoInfo[]
|
35 |
setCurrentVideos: (currentVideos: VideoInfo[]) => void
|
36 |
|
|
|
51 |
set({ view: view || "home" })
|
52 |
},
|
53 |
|
54 |
+
setPathname: (pathname: string) => {
|
55 |
+
const routes: Record<string, InterfaceView> = {
|
56 |
+
"/": "home",
|
57 |
+
"/watch": "public_video",
|
58 |
+
"/channels": "public_channels"
|
59 |
+
}
|
60 |
+
console.log("setPathname: ", pathname)
|
61 |
+
set({ view: routes[pathname] || "not_found" })
|
62 |
+
},
|
63 |
+
|
64 |
headerMode: "normal",
|
65 |
setHeaderMode: (headerMode: InterfaceHeaderMode) => {
|
66 |
set({ headerMode })
|
|
|
88 |
set({ currentTag })
|
89 |
},
|
90 |
|
91 |
+
currentTags: [],
|
92 |
+
setCurrentTags: (currentTags?: string[]) => {
|
93 |
+
set({ currentTags })
|
94 |
+
},
|
95 |
+
|
96 |
currentVideos: [],
|
97 |
setCurrentVideos: (currentVideos: VideoInfo[] = []) => {
|
98 |
set({
|
src/app/views/home-view/index.tsx
CHANGED
@@ -7,6 +7,7 @@ import { cn } from "@/lib/utils"
|
|
7 |
import { VideoInfo } from "@/types"
|
8 |
import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
|
9 |
import { VideoList } from "@/app/interface/video-list"
|
|
|
10 |
|
11 |
export function HomeView() {
|
12 |
const [_isPending, startTransition] = useTransition()
|
@@ -18,6 +19,7 @@ export function HomeView() {
|
|
18 |
const setCurrentChannel = useStore(s => s.setCurrentChannel)
|
19 |
const currentTag = useStore(s => s.currentTag)
|
20 |
const setCurrentTag = useStore(s => s.setCurrentTag)
|
|
|
21 |
const currentVideos = useStore(s => s.currentVideos)
|
22 |
const setCurrentVideos = useStore(s => s.setCurrentVideos)
|
23 |
const currentVideo = useStore(s => s.currentVideo)
|
@@ -27,6 +29,7 @@ export function HomeView() {
|
|
27 |
startTransition(async () => {
|
28 |
const videos = await getVideos({
|
29 |
sortBy: "date",
|
|
|
30 |
maxVideos: 25
|
31 |
})
|
32 |
|
|
|
7 |
import { VideoInfo } from "@/types"
|
8 |
import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
|
9 |
import { VideoList } from "@/app/interface/video-list"
|
10 |
+
import { getTags } from "@/app/server/actions/ai-tube-hf/getTags"
|
11 |
|
12 |
export function HomeView() {
|
13 |
const [_isPending, startTransition] = useTransition()
|
|
|
19 |
const setCurrentChannel = useStore(s => s.setCurrentChannel)
|
20 |
const currentTag = useStore(s => s.currentTag)
|
21 |
const setCurrentTag = useStore(s => s.setCurrentTag)
|
22 |
+
const setCurrentTags = useStore(s => s.setCurrentTags)
|
23 |
const currentVideos = useStore(s => s.currentVideos)
|
24 |
const setCurrentVideos = useStore(s => s.setCurrentVideos)
|
25 |
const currentVideo = useStore(s => s.currentVideo)
|
|
|
29 |
startTransition(async () => {
|
30 |
const videos = await getVideos({
|
31 |
sortBy: "date",
|
32 |
+
tag: currentTag,
|
33 |
maxVideos: 25
|
34 |
})
|
35 |
|
src/app/views/not-found-view/index.tsx
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import { cn } from "@/lib/utils"
|
4 |
+
|
5 |
+
export function NotFoundView() {
|
6 |
+
return (
|
7 |
+
<div className={cn(
|
8 |
+
`w-full`,
|
9 |
+
`flex flex-row`,
|
10 |
+
`items-center justify-center`
|
11 |
+
)}>
|
12 |
+
<h1>Sorry, we couldn't find this content.</h1>
|
13 |
+
</div>
|
14 |
+
)
|
15 |
+
}
|
src/app/views/public-video-view/index.tsx
CHANGED
@@ -6,13 +6,11 @@ import { RiCheckboxCircleFill } from "react-icons/ri"
|
|
6 |
import { useStore } from "@/app/state/useStore"
|
7 |
import { cn } from "@/lib/utils"
|
8 |
import { VideoPlayer } from "@/app/interface/video-player"
|
|
|
9 |
|
10 |
|
11 |
export function PublicVideoView() {
|
12 |
-
const displayMode = useStore(s => s.displayMode)
|
13 |
const video = useStore(s => s.currentVideo)
|
14 |
-
const setMenuMode = useStore(s => s.setMenuMode)
|
15 |
-
const setHeaderMode = useStore(s => s.setHeaderMode)
|
16 |
|
17 |
if (!video) { return null }
|
18 |
|
@@ -36,7 +34,7 @@ export function PublicVideoView() {
|
|
36 |
`text-xl text-zinc-100 font-medium mb-0 line-clamp-2`,
|
37 |
`mb-2`
|
38 |
)}>
|
39 |
-
{video
|
40 |
</div>
|
41 |
|
42 |
{/** VIDEO TOOLBAR - HORIZONTAL */}
|
|
|
6 |
import { useStore } from "@/app/state/useStore"
|
7 |
import { cn } from "@/lib/utils"
|
8 |
import { VideoPlayer } from "@/app/interface/video-player"
|
9 |
+
import { VideoInfo } from "@/types"
|
10 |
|
11 |
|
12 |
export function PublicVideoView() {
|
|
|
13 |
const video = useStore(s => s.currentVideo)
|
|
|
|
|
14 |
|
15 |
if (!video) { return null }
|
16 |
|
|
|
34 |
`text-xl text-zinc-100 font-medium mb-0 line-clamp-2`,
|
35 |
`mb-2`
|
36 |
)}>
|
37 |
+
{video.label}
|
38 |
</div>
|
39 |
|
40 |
{/** VIDEO TOOLBAR - HORIZONTAL */}
|
src/app/views/user-account-view/index.tsx
CHANGED
@@ -5,7 +5,7 @@ import { useLocalStorage } from "usehooks-ts"
|
|
5 |
|
6 |
import { cn } from "@/lib/utils"
|
7 |
import { Input } from "@/components/ui/input"
|
8 |
-
import { localStorageKeys } from "@/app/state/
|
9 |
import { defaultSettings } from "@/app/state/defaultSettings"
|
10 |
|
11 |
export function UserAccountView() {
|
|
|
5 |
|
6 |
import { cn } from "@/lib/utils"
|
7 |
import { Input } from "@/components/ui/input"
|
8 |
+
import { localStorageKeys } from "@/app/state/localStorageKeys"
|
9 |
import { defaultSettings } from "@/app/state/defaultSettings"
|
10 |
|
11 |
export function UserAccountView() {
|
src/app/views/user-channel-view/index.tsx
CHANGED
@@ -7,7 +7,7 @@ import { cn } from "@/lib/utils"
|
|
7 |
import { VideoInfo } from "@/types"
|
8 |
|
9 |
import { useLocalStorage } from "usehooks-ts"
|
10 |
-
import { localStorageKeys } from "@/app/state/
|
11 |
import { defaultSettings } from "@/app/state/defaultSettings"
|
12 |
import { Input } from "@/components/ui/input"
|
13 |
import { Textarea } from "@/components/ui/textarea"
|
|
|
7 |
import { VideoInfo } from "@/types"
|
8 |
|
9 |
import { useLocalStorage } from "usehooks-ts"
|
10 |
+
import { localStorageKeys } from "@/app/state/localStorageKeys"
|
11 |
import { defaultSettings } from "@/app/state/defaultSettings"
|
12 |
import { Input } from "@/components/ui/input"
|
13 |
import { Textarea } from "@/components/ui/textarea"
|
src/app/views/user-channels-view/index.tsx
CHANGED
@@ -7,7 +7,7 @@ import { useStore } from "@/app/state/useStore"
|
|
7 |
import { cn } from "@/lib/utils"
|
8 |
import { getChannels } from "@/app/server/actions/ai-tube-hf/getChannels"
|
9 |
import { ChannelList } from "@/app/interface/channel-list"
|
10 |
-
import { localStorageKeys } from "@/app/state/
|
11 |
import { defaultSettings } from "@/app/state/defaultSettings"
|
12 |
import { Input } from "@/components/ui/input"
|
13 |
|
|
|
7 |
import { cn } from "@/lib/utils"
|
8 |
import { getChannels } from "@/app/server/actions/ai-tube-hf/getChannels"
|
9 |
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 |
|
src/app/watch/page.tsx
ADDED
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useEffect, useState, useTransition } from "react"
|
2 |
+
import Head from "next/head"
|
3 |
+
import Script from "next/script"
|
4 |
+
import { Metadata, ResolvingMetadata } from "next"
|
5 |
+
|
6 |
+
|
7 |
+
import { Main } from "../main"
|
8 |
+
|
9 |
+
import { getVideo } from "../server/actions/ai-tube-hf/getVideo"
|
10 |
+
|
11 |
+
type Props = {
|
12 |
+
params: { id: string }
|
13 |
+
searchParams: {
|
14 |
+
v?: string | string[],
|
15 |
+
[key: string]: string | string[] | undefined
|
16 |
+
}
|
17 |
+
}
|
18 |
+
|
19 |
+
// https://nextjs.org/docs/pages/building-your-application/optimizing/fonts
|
20 |
+
export async function generateMetadata(
|
21 |
+
{ params, searchParams: { v: videoId } }: Props,
|
22 |
+
parent: ResolvingMetadata
|
23 |
+
): Promise<Metadata> {
|
24 |
+
// read route params
|
25 |
+
|
26 |
+
const metadataBase = new URL('https://huggingface.co/spaces/jbilcke-hf/ai-tube')
|
27 |
+
|
28 |
+
try {
|
29 |
+
const video = await getVideo(videoId)
|
30 |
+
|
31 |
+
if (!video) {
|
32 |
+
throw new Error("Video not found")
|
33 |
+
}
|
34 |
+
|
35 |
+
return {
|
36 |
+
title: `${video.label} - AI Tube`,
|
37 |
+
metadataBase: new URL('https://huggingface.co/spaces/jbilcke-hf/ai-tube'),
|
38 |
+
openGraph: {
|
39 |
+
type: "website",
|
40 |
+
// url: "https://example.com",
|
41 |
+
title: video.label || "", // put the video title here
|
42 |
+
description: video.description || "", // put the vide description here
|
43 |
+
siteName: "AI Tube",
|
44 |
+
|
45 |
+
videos: [
|
46 |
+
{
|
47 |
+
"url": video.assetUrl
|
48 |
+
}
|
49 |
+
],
|
50 |
+
// images: ['/some-specific-page-image.jpg', ...previousImages],
|
51 |
+
},
|
52 |
+
}
|
53 |
+
} catch (err) {
|
54 |
+
return {
|
55 |
+
title: "AI Tube - 404 Video Not Found",
|
56 |
+
metadataBase,
|
57 |
+
openGraph: {
|
58 |
+
type: "website",
|
59 |
+
// url: "https://example.com",
|
60 |
+
title: "AI Tube - 404 Not Found", // put the video title here
|
61 |
+
description: "", // put the vide description here
|
62 |
+
siteName: "AI Tube",
|
63 |
+
|
64 |
+
videos: [],
|
65 |
+
images: [],
|
66 |
+
},
|
67 |
+
}
|
68 |
+
}
|
69 |
+
}
|
70 |
+
|
71 |
+
|
72 |
+
export default async function WatchPage({ searchParams: { v: videoId } }: Props) {
|
73 |
+
// const [_pending, startTransition] = useTransition()
|
74 |
+
// const setView = useStore(s => s.setView)
|
75 |
+
// const setCurrentVideo = useStore(s => s.setCurrentVideo)
|
76 |
+
const id = `${videoId || ""}`
|
77 |
+
|
78 |
+
const video = await getVideo(videoId)
|
79 |
+
|
80 |
+
// console.log("got video:", video.id)
|
81 |
+
return (
|
82 |
+
<Main video={video} />
|
83 |
+
)
|
84 |
+
}
|
src/types.ts
CHANGED
@@ -366,6 +366,7 @@ export type InterfaceView =
|
|
366 |
| "public_channels"
|
367 |
| "public_channel" // public view of a channel
|
368 |
| "public_video" // public view of a video
|
|
|
369 |
|
370 |
export type Settings = {
|
371 |
huggingfaceApiKey: string
|
|
|
366 |
| "public_channels"
|
367 |
| "public_channel" // public view of a channel
|
368 |
| "public_video" // public view of a video
|
369 |
+
| "not_found"
|
370 |
|
371 |
export type Settings = {
|
372 |
huggingfaceApiKey: string
|