22
33import Link from "next/link"
44import { useRouter } from "next/navigation"
5- import { FormEvent , useEffect , useState } from "react"
6- import { ChevronRight , Play } from "lucide-react"
5+ import { ChangeEvent , FormEvent , useEffect , useRef , useState } from "react"
6+ import { ChevronRight , Play , Upload } from "lucide-react"
77
88import {
99 TaskSummary ,
10+ LocalDirection ,
1011 createTask ,
1112 listTasks ,
13+ uploadLocalTask ,
1214} from "@/lib/api"
1315import { useI18n } from "@/lib/i18n"
1416import { statusBadgeClass } from "@/lib/status"
@@ -24,6 +26,13 @@ import {
2426import { Input } from "@/components/ui/input"
2527import { Label } from "@/components/ui/label"
2628import { ScrollArea } from "@/components/ui/scroll-area"
29+ import {
30+ Select ,
31+ SelectContent ,
32+ SelectItem ,
33+ SelectTrigger ,
34+ SelectValue ,
35+ } from "@/components/ui/select"
2736
2837function isActive ( status : string ) {
2938 return status === "queued" || status === "running"
@@ -47,8 +56,11 @@ function activeCount(tasks: TaskSummary[]) {
4756export default function Home ( ) {
4857 const router = useRouter ( )
4958 const { activeTasksText, stageLabel, statusLabel, t } = useI18n ( )
59+ const fileInputRef = useRef < HTMLInputElement > ( null )
5060 const [ youtubeUrl , setYoutubeUrl ] = useState ( "" )
5161 const [ bilibiliUrl , setBilibiliUrl ] = useState ( "" )
62+ const [ localFile , setLocalFile ] = useState < File | null > ( null )
63+ const [ localDirection , setLocalDirection ] = useState < LocalDirection > ( "en-zh" )
5264 const [ tasks , setTasks ] = useState < TaskSummary [ ] > ( [ ] )
5365 const [ error , setError ] = useState ( "" )
5466 const [ submitting , setSubmitting ] = useState ( false )
@@ -78,16 +90,27 @@ export default function Home() {
7890 }
7991 } , [ t . home . loadError ] )
8092
93+ function selectLocalFile ( event : ChangeEvent < HTMLInputElement > ) {
94+ setError ( "" )
95+ setLocalFile ( event . target . files ?. [ 0 ] || null )
96+ }
97+
8198 async function submitTask ( event : FormEvent < HTMLFormElement > ) {
8299 event . preventDefault ( )
83100 setError ( "" )
84101 const submittedUrl = youtubeUrl . trim ( ) || bilibiliUrl . trim ( )
85- if ( ! submittedUrl ) return
102+ if ( ! submittedUrl && ! localFile ) return
86103 setSubmitting ( true )
87104 try {
88- const created = await createTask ( submittedUrl )
105+ const created = localFile
106+ ? await uploadLocalTask ( localFile , localDirection )
107+ : await createTask ( submittedUrl )
89108 setYoutubeUrl ( "" )
90109 setBilibiliUrl ( "" )
110+ setLocalFile ( null )
111+ if ( fileInputRef . current ) {
112+ fileInputRef . current . value = ""
113+ }
91114 refreshTasks ( ) . catch ( ( ) => undefined )
92115 router . push ( `/tasks/${ created . id } ` )
93116 } catch ( err ) {
@@ -98,7 +121,9 @@ export default function Home() {
98121 }
99122
100123 const queued = activeCount ( tasks )
101- const canSubmit = Boolean ( ( youtubeUrl . trim ( ) || bilibiliUrl . trim ( ) ) && ! submitting )
124+ const hasUrl = Boolean ( youtubeUrl . trim ( ) || bilibiliUrl . trim ( ) )
125+ const hasLocalFile = Boolean ( localFile )
126+ const canSubmit = Boolean ( ( hasUrl || hasLocalFile ) && ! submitting )
102127
103128 return (
104129 < main className = "min-h-screen bg-[linear-gradient(135deg,#fff5f5_0%,#f2fbff_48%,#fff4fa_100%)] text-foreground" >
@@ -118,7 +143,7 @@ export default function Home() {
118143 value = { youtubeUrl }
119144 onChange = { ( event ) => setYoutubeUrl ( event . target . value ) }
120145 placeholder = "https://www.youtube.com/watch?v=..."
121- disabled = { Boolean ( bilibiliUrl . trim ( ) ) }
146+ disabled = { Boolean ( bilibiliUrl . trim ( ) ) || hasLocalFile }
122147 />
123148 </ div >
124149 < div className = "space-y-2" >
@@ -128,9 +153,38 @@ export default function Home() {
128153 value = { bilibiliUrl }
129154 onChange = { ( event ) => setBilibiliUrl ( event . target . value ) }
130155 placeholder = "https://www.bilibili.com/video/BV..."
131- disabled = { Boolean ( youtubeUrl . trim ( ) ) }
156+ disabled = { Boolean ( youtubeUrl . trim ( ) ) || hasLocalFile }
132157 />
133158 </ div >
159+ < div className = "grid gap-3 sm:grid-cols-[1fr_180px]" >
160+ < div className = "space-y-2" >
161+ < Label htmlFor = "local-video" > { t . home . localVideoLabel } </ Label >
162+ < Input
163+ ref = { fileInputRef }
164+ id = "local-video"
165+ type = "file"
166+ accept = "video/*,.mp4,.mov,.m4v,.mkv,.webm,.avi,.flv,.wmv"
167+ onChange = { selectLocalFile }
168+ disabled = { hasUrl }
169+ />
170+ </ div >
171+ < div className = "space-y-2" >
172+ < Label htmlFor = "local-direction" > { t . home . localDirectionLabel } </ Label >
173+ < Select
174+ value = { localDirection }
175+ onValueChange = { ( value ) => setLocalDirection ( value as LocalDirection ) }
176+ disabled = { hasUrl }
177+ >
178+ < SelectTrigger id = "local-direction" className = "h-10" >
179+ < SelectValue />
180+ </ SelectTrigger >
181+ < SelectContent >
182+ < SelectItem value = "en-zh" > { t . home . localEnZh } </ SelectItem >
183+ < SelectItem value = "zh-en" > { t . home . localZhEn } </ SelectItem >
184+ </ SelectContent >
185+ </ Select >
186+ </ div >
187+ </ div >
134188 < div className = "flex items-center justify-between gap-3" >
135189 { queued > 0 ? (
136190 < p className = "text-xs text-muted-foreground" >
@@ -140,7 +194,7 @@ export default function Home() {
140194 < span />
141195 ) }
142196 < Button type = "submit" disabled = { ! canSubmit } >
143- < Play className = "size-4" />
197+ { hasLocalFile ? < Upload className = "size-4" /> : < Play className = "size-4" /> }
144198 { submitting ? t . home . submitting : t . home . createTask }
145199 </ Button >
146200 </ div >
0 commit comments