copilot-swe-agent[bot] ArnavSingh76533 commited on
Commit
6fc62e6
·
1 Parent(s): 404d1e4

Fix video player controls and playlist behavior

Browse files

- Add thumbnail field to MediaElement type
- Add thumbnail display to playlist items (YouTube thumbnails auto-detected)
- Show "Unknown" for items without titles, display full URL
- Disable play/pause on clicking video area (only button works)
- Fix playUrl to replace current video instead of adding to playlist
- Update NewTabLink to support title prop

Co-authored-by: ArnavSingh76533 <160649079+ArnavSingh76533@users.noreply.github.com>

components/action/NewTabLink.tsx CHANGED
@@ -4,10 +4,11 @@ import classNames from "classnames"
4
  interface Props {
5
  href: string
6
  className?: string
 
7
  children?: ReactNode
8
  }
9
 
10
- const NewTabLink: FC<Props> = ({ href, children, className }) => {
11
  return (
12
  <a
13
  href={href}
@@ -17,6 +18,7 @@ const NewTabLink: FC<Props> = ({ href, children, className }) => {
17
  )}
18
  target={"_blank"}
19
  rel={"noreferrer"}
 
20
  >
21
  {children}
22
  </a>
 
4
  interface Props {
5
  href: string
6
  className?: string
7
+ title?: string
8
  children?: ReactNode
9
  }
10
 
11
+ const NewTabLink: FC<Props> = ({ href, children, className, title }) => {
12
  return (
13
  <a
14
  href={href}
 
18
  )}
19
  target={"_blank"}
20
  rel={"noreferrer"}
21
+ title={title}
22
  >
23
  {children}
24
  </a>
components/player/Controls.tsx CHANGED
@@ -177,27 +177,13 @@ const Controls: FC<Props> = ({
177
  console.log("Toggled fullscreen")
178
  setFullscreen(!fullscreen)
179
  } else if (touch) {
180
- // Single touch on mobile - show controls and toggle play/pause
 
181
  setShowControls(true)
182
  setMenuOpen(false)
183
- // Toggle play/pause on touch (owner only)
184
- if (isOwner) {
185
- if (playEnded()) {
186
- playAgain()
187
- } else {
188
- setPaused(!paused)
189
- }
190
- }
191
- } else {
192
- // Desktop click on center overlay - toggle play/pause if owner
193
- if (isOwner) {
194
- if (playEnded()) {
195
- playAgain()
196
- } else {
197
- setPaused(!paused)
198
- }
199
- }
200
  }
 
 
201
 
202
  interact()
203
  mouseMoved(touch)
@@ -206,7 +192,7 @@ const Controls: FC<Props> = ({
206
  showControlsAction(!touch)
207
  }}
208
  >
209
- {/* Center play/pause button - positioned absolutely in center */}
210
  <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
211
  {paused ? <IconBigPlay /> : <IconBigPause />}
212
  </div>
 
177
  console.log("Toggled fullscreen")
178
  setFullscreen(!fullscreen)
179
  } else if (touch) {
180
+ // Single touch on mobile - show controls only, no play/pause toggle
181
+ // Play/pause only from control buttons
182
  setShowControls(true)
183
  setMenuOpen(false)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  }
185
+ // Desktop click on center overlay - no play/pause toggle
186
+ // Play/pause only from control buttons
187
 
188
  interact()
189
  mouseMoved(touch)
 
192
  showControlsAction(!touch)
193
  }}
194
  >
195
+ {/* Center play/pause indicator - positioned absolutely in center */}
196
  <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
197
  {paused ? <IconBigPlay /> : <IconBigPause />}
198
  </div>
components/playlist/PlaylistItem.tsx CHANGED
@@ -10,7 +10,6 @@ import InputText from "../input/InputText"
10
  import ControlButton from "../input/ControlButton"
11
  import IconPlay from "../icon/IconPlay"
12
  import IconDisk from "../icon/IconDisk"
13
- import { getDomain } from "../../lib/utils"
14
 
15
  // HACK: this fixes type incompatibility
16
  const Draggable = _Draggable as unknown as FC<DraggableProps>
@@ -24,11 +23,69 @@ interface Props {
24
  updateTitle: (title: string) => void
25
  }
26
 
27
- const titleGen = (item: MediaElement, index: number) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  if (item.title && item.title !== "") {
29
  return item.title
30
  }
31
- return "Item #" + (index + 1)
 
32
  }
33
 
34
  const PlaylistItem: FC<Props> = ({
@@ -59,6 +116,8 @@ const PlaylistItem: FC<Props> = ({
59
  }
60
  }, [edit, item.title, title])
61
 
 
 
62
  return (
63
  <Draggable
64
  key={item.src[0].src + "-" + index}
@@ -71,7 +130,7 @@ const PlaylistItem: FC<Props> = ({
71
  {...provided.draggableProps}
72
  style={provided.draggableProps.style}
73
  className={classNames(
74
- "p-3 rounded-lg flex flex-col border transition-all duration-200",
75
  snapshot.isDragging
76
  ? "bg-dark-700 border-primary-500/50 shadow-glow"
77
  : playing
@@ -79,18 +138,38 @@ const PlaylistItem: FC<Props> = ({
79
  : "bg-dark-800/50 border-dark-700/50 hover:bg-dark-800"
80
  )}
81
  >
82
- <div className={"flex flex-row gap-2 items-center mb-2"}>
83
- <div
84
- className={classNames(
85
- "p-1 transition-colors hover:text-primary-500 cursor-grab active:cursor-grabbing",
86
- snapshot.isDragging && "text-primary-500"
87
- )}
88
- {...provided.dragHandleProps}
89
- >
90
- <IconDrag />
91
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  <div
93
- className={"flex grow"}
94
  onMouseEnter={() => setEdit(true)}
95
  onMouseLeave={() => setEdit(false)}
96
  >
@@ -101,17 +180,19 @@ const PlaylistItem: FC<Props> = ({
101
  value={title}
102
  />
103
  ) : (
104
- <span className={classNames("text-sm font-medium", playing && "text-primary-400")}>
105
- {titleGen(item, index)}
106
  </span>
107
  )}
108
  </div>
109
- <DeleteButton
110
- tooltip={"Delete " + title}
111
- onClick={() => deleteItem(index)}
112
- />
113
  </div>
114
- <div className={"flex flex-row items-center gap-2"}>
 
 
115
  <ControlButton
116
  tooltip={playing ? "Currently playing" : "Play item now"}
117
  onClick={() => {
@@ -133,11 +214,15 @@ const PlaylistItem: FC<Props> = ({
133
  </ControlButton>
134
  <NewTabLink
135
  href={item.src[0].src}
136
- className={"flex flex-row gap-1 items-center text-xs text-dark-400 hover:text-primary-500 transition-colors"}
 
137
  >
138
- <div className={"line-clamp-2"}>{getDomain(item.src[0].src)}</div>
139
- <IconNewTab className={"shrink-0"} />
140
  </NewTabLink>
 
 
 
 
141
  </div>
142
  </div>
143
  )}
 
10
  import ControlButton from "../input/ControlButton"
11
  import IconPlay from "../icon/IconPlay"
12
  import IconDisk from "../icon/IconDisk"
 
13
 
14
  // HACK: this fixes type incompatibility
15
  const Draggable = _Draggable as unknown as FC<DraggableProps>
 
23
  updateTitle: (title: string) => void
24
  }
25
 
26
+ /**
27
+ * Extract YouTube video ID from various YouTube URL formats
28
+ */
29
+ const getYouTubeVideoId = (url: string): string | null => {
30
+ try {
31
+ const urlObj = new URL(url)
32
+ const hostname = urlObj.hostname.toLowerCase()
33
+
34
+ // Check for YouTube domains
35
+ const isYouTube = hostname === 'youtube.com' ||
36
+ hostname === 'www.youtube.com' ||
37
+ hostname === 'm.youtube.com' ||
38
+ hostname.endsWith('.youtube.com') ||
39
+ hostname === 'youtu.be' ||
40
+ hostname === 'www.youtu.be'
41
+
42
+ if (!isYouTube) return null
43
+
44
+ // Handle youtu.be short URLs
45
+ if (hostname === 'youtu.be' || hostname === 'www.youtu.be') {
46
+ return urlObj.pathname.slice(1) || null
47
+ }
48
+
49
+ // Handle youtube.com URLs
50
+ const videoId = urlObj.searchParams.get('v')
51
+ if (videoId) return videoId
52
+
53
+ // Handle embed URLs: youtube.com/embed/VIDEO_ID
54
+ if (urlObj.pathname.startsWith('/embed/')) {
55
+ return urlObj.pathname.split('/')[2] || null
56
+ }
57
+
58
+ return null
59
+ } catch {
60
+ return null
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Get thumbnail URL for a media item
66
+ */
67
+ const getThumbnailUrl = (item: MediaElement): string | null => {
68
+ // If item has explicit thumbnail, use it
69
+ if (item.thumbnail) return item.thumbnail
70
+
71
+ // Try to extract from YouTube URL
72
+ const src = item.src?.[0]?.src
73
+ if (!src) return null
74
+
75
+ const youtubeId = getYouTubeVideoId(src)
76
+ if (youtubeId) {
77
+ return `https://img.youtube.com/vi/${youtubeId}/mqdefault.jpg`
78
+ }
79
+
80
+ return null
81
+ }
82
+
83
+ const titleGen = (item: MediaElement) => {
84
  if (item.title && item.title !== "") {
85
  return item.title
86
  }
87
+ // Show "Unknown" if no title, with the domain in URL
88
+ return "Unknown"
89
  }
90
 
91
  const PlaylistItem: FC<Props> = ({
 
116
  }
117
  }, [edit, item.title, title])
118
 
119
+ const thumbnailUrl = getThumbnailUrl(item)
120
+
121
  return (
122
  <Draggable
123
  key={item.src[0].src + "-" + index}
 
130
  {...provided.draggableProps}
131
  style={provided.draggableProps.style}
132
  className={classNames(
133
+ "p-2 rounded-lg flex flex-row gap-2 border transition-all duration-200",
134
  snapshot.isDragging
135
  ? "bg-dark-700 border-primary-500/50 shadow-glow"
136
  : playing
 
138
  : "bg-dark-800/50 border-dark-700/50 hover:bg-dark-800"
139
  )}
140
  >
141
+ {/* Drag handle */}
142
+ <div
143
+ className={classNames(
144
+ "p-1 transition-colors hover:text-primary-500 cursor-grab active:cursor-grabbing self-center",
145
+ snapshot.isDragging && "text-primary-500"
146
+ )}
147
+ {...provided.dragHandleProps}
148
+ >
149
+ <IconDrag />
150
+ </div>
151
+
152
+ {/* Thumbnail */}
153
+ <div className="flex-shrink-0 self-center">
154
+ {thumbnailUrl ? (
155
+ // eslint-disable-next-line @next/next/no-img-element
156
+ <img
157
+ src={thumbnailUrl}
158
+ alt=""
159
+ className="w-16 h-10 object-cover rounded border border-dark-700/30"
160
+ />
161
+ ) : (
162
+ <div className="w-16 h-10 bg-dark-700 rounded border border-dark-700/30 flex items-center justify-center">
163
+ <IconPlay className="text-dark-500 w-4 h-4" />
164
+ </div>
165
+ )}
166
+ </div>
167
+
168
+ {/* Info section */}
169
+ <div className="flex-grow min-w-0 flex flex-col justify-center gap-1">
170
+ {/* Title */}
171
  <div
172
+ className={"flex"}
173
  onMouseEnter={() => setEdit(true)}
174
  onMouseLeave={() => setEdit(false)}
175
  >
 
180
  value={title}
181
  />
182
  ) : (
183
+ <span className={classNames("text-sm font-medium truncate", playing && "text-primary-400")}>
184
+ {titleGen(item)}
185
  </span>
186
  )}
187
  </div>
188
+ {/* URL */}
189
+ <div className="text-xs text-dark-500 truncate">
190
+ {item.src[0]?.src || 'Unknown URL'}
191
+ </div>
192
  </div>
193
+
194
+ {/* Action buttons */}
195
+ <div className="flex flex-row items-center gap-1 flex-shrink-0">
196
  <ControlButton
197
  tooltip={playing ? "Currently playing" : "Play item now"}
198
  onClick={() => {
 
214
  </ControlButton>
215
  <NewTabLink
216
  href={item.src[0].src}
217
+ className={"p-1 text-dark-400 hover:text-primary-500 transition-colors"}
218
+ title="Open in new tab"
219
  >
220
+ <IconNewTab className={"w-4 h-4"} />
 
221
  </NewTabLink>
222
+ <DeleteButton
223
+ tooltip={"Delete " + (title || titleGen(item))}
224
+ onClick={() => deleteItem(index)}
225
+ />
226
  </div>
227
  </div>
228
  )}
lib/types.ts CHANGED
@@ -10,6 +10,7 @@ export interface MediaOption {
10
 
11
  export interface MediaElement {
12
  title?: string
 
13
  sub: Subtitle[]
14
  src: MediaOption[]
15
  }
 
10
 
11
  export interface MediaElement {
12
  title?: string
13
+ thumbnail?: string // URL to the video thumbnail image
14
  sub: Subtitle[]
15
  src: MediaOption[]
16
  }
pages/api/socketio.ts CHANGED
@@ -340,12 +340,22 @@ const ioHandler = (_: NextApiRequest, res: NextApiResponse) => {
340
  }
341
  }
342
 
343
- // Add new video to playlist at position 0
 
344
  const newMedia = createMediaElement(url)
345
- room.targetState.playlist.items.unshift(newMedia)
 
 
 
 
 
 
 
 
 
 
346
 
347
  room.targetState.playing = newMedia
348
- room.targetState.playlist.currentIndex = 0
349
  room.targetState.progress = 0
350
  room.targetState.lastSync = new Date().getTime() / 1000
351
  room.targetState.paused = false
 
340
  }
341
  }
342
 
343
+ // Replace the currently playing video with the new one
344
+ // If there's a current video at index 0, replace it; otherwise add new
345
  const newMedia = createMediaElement(url)
346
+
347
+ if (room.targetState.playlist.currentIndex >= 0 &&
348
+ room.targetState.playlist.items.length > 0) {
349
+ // Replace the currently playing video at currentIndex
350
+ const currentIdx = room.targetState.playlist.currentIndex
351
+ room.targetState.playlist.items[currentIdx] = newMedia
352
+ } else {
353
+ // No current video, add as first item
354
+ room.targetState.playlist.items.unshift(newMedia)
355
+ room.targetState.playlist.currentIndex = 0
356
+ }
357
 
358
  room.targetState.playing = newMedia
 
359
  room.targetState.progress = 0
360
  room.targetState.lastSync = new Date().getTime() / 1000
361
  room.targetState.paused = false