Ahmadraza76 commited on
Commit
e0b94eb
Β·
verified Β·
1 Parent(s): 823d75e

Create a full-featured watch party platform where users can watch YouTube videos together in real-time, with synchronized playback and a modern UI/UX. 🎯 Core Requirements: βœ… Real-time sync: Video playback must stay synced for all users in the party room (like Teleparty, Watch2Gether). βœ… YouTube support: Use yt-dlp (mu38 supported) in the backend to fetch direct video/audio stream URLs for smooth playback. βœ… High quality playback: Allow up to 1080p+ streaming with lowest latency using HTML5 video player. βœ… Custom room system: Room create/join via unique link Host has control (play, pause, seek) Guest syncs automatically βœ… Chat box: Real-time text chat for viewers in the room. βœ… User presence: Show online/offline users with colored indicators (e.g. green dot). βœ… Responsive UI: Should work perfectly on desktop and mobile browsers. βœ… No YouTube Ads during playback (since yt-dlp is used). βœ… Optionally allow private rooms (password protected). βš™οΈ Tech Stack Suggestions: Frontend: React + Tailwind CSS (or Next.js) Backend: Node.js + Socket.IO (for real-time), Express Streaming: yt-dlp (mu38 version), ffmpeg (if needed for format conversion) Database: MongoDB or any fast NoSQL (for room metadata) ✨ Extra Features (if possible): Dark/light mode toggle Video queue/playlist support Host transfer option Emoji reactions in chat Watch history for users πŸ” The entire system must be self-hostable on a VPS, and capable of running without YouTube API key. - Initial Deployment

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +941 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Yp
3
- emoji: πŸš€
4
- colorFrom: green
5
- colorTo: blue
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: yp
3
+ emoji: 🐳
4
+ colorFrom: pink
5
+ colorTo: red
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,941 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SyncStream - Watch Videos Together</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.2/dist/socket.io.min.js"></script>
10
+ <style>
11
+ /* Custom scrollbar */
12
+ ::-webkit-scrollbar {
13
+ width: 8px;
14
+ height: 8px;
15
+ }
16
+ ::-webkit-scrollbar-track {
17
+ background: #1e293b;
18
+ }
19
+ ::-webkit-scrollbar-thumb {
20
+ background: #475569;
21
+ border-radius: 4px;
22
+ }
23
+ ::-webkit-scrollbar-thumb:hover {
24
+ background: #64748b;
25
+ }
26
+
27
+ /* Video player aspect ratio */
28
+ .video-container {
29
+ position: relative;
30
+ padding-bottom: 56.25%; /* 16:9 */
31
+ height: 0;
32
+ overflow: hidden;
33
+ }
34
+ .video-container iframe,
35
+ .video-container video {
36
+ position: absolute;
37
+ top: 0;
38
+ left: 0;
39
+ width: 100%;
40
+ height: 100%;
41
+ }
42
+
43
+ /* Animation for new messages */
44
+ @keyframes fadeIn {
45
+ from { opacity: 0; transform: translateY(5px); }
46
+ to { opacity: 1; transform: translateY(0); }
47
+ }
48
+ .message-animate {
49
+ animation: fadeIn 0.3s ease-out;
50
+ }
51
+
52
+ /* Custom toggle switch */
53
+ .toggle-checkbox:checked {
54
+ right: 0;
55
+ border-color: #3b82f6;
56
+ }
57
+ .toggle-checkbox:checked + .toggle-label {
58
+ background-color: #3b82f6;
59
+ }
60
+ </style>
61
+ </head>
62
+ <body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors duration-200">
63
+ <!-- Navigation -->
64
+ <nav class="bg-white dark:bg-gray-800 shadow-md py-4 px-6 flex justify-between items-center">
65
+ <div class="flex items-center space-x-2">
66
+ <i class="fas fa-film text-blue-500 text-2xl"></i>
67
+ <h1 class="text-xl font-bold">SyncStream</h1>
68
+ </div>
69
+ <div class="flex items-center space-x-4">
70
+ <button id="theme-toggle" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
71
+ <i class="fas fa-moon dark:hidden"></i>
72
+ <i class="fas fa-sun hidden dark:inline"></i>
73
+ </button>
74
+ <button id="user-menu" class="flex items-center space-x-2">
75
+ <div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-semibold">U</div>
76
+ <span class="hidden md:inline">User</span>
77
+ </button>
78
+ </div>
79
+ </nav>
80
+
81
+ <!-- Main Content -->
82
+ <div class="container mx-auto px-4 py-8">
83
+ <!-- Room Controls -->
84
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg mb-6 p-6">
85
+ <div class="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
86
+ <div class="flex items-center space-x-4">
87
+ <div class="relative">
88
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
89
+ <i class="fas fa-link text-gray-400"></i>
90
+ </div>
91
+ <input type="text" id="room-link" class="pl-10 pr-4 py-2 border rounded-lg w-full md:w-64 bg-gray-50 dark:bg-gray-700 border-gray-300 dark:border-gray-600" value="https://syncstream.party/room/abc123" readonly>
92
+ </div>
93
+ <button id="copy-link" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition">
94
+ <i class="fas fa-copy mr-2"></i>Copy Link
95
+ </button>
96
+ </div>
97
+ <div class="flex items-center space-x-4">
98
+ <div class="flex items-center space-x-2">
99
+ <i class="fas fa-users text-gray-500 dark:text-gray-400"></i>
100
+ <span id="user-count" class="font-medium">1</span>
101
+ </div>
102
+ <div class="relative">
103
+ <button id="settings-toggle" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition">
104
+ <i class="fas fa-cog mr-2"></i>Settings
105
+ </button>
106
+ <div id="settings-dropdown" class="hidden absolute right-0 mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 z-10">
107
+ <div class="flex items-center justify-between mb-4">
108
+ <span>Dark Mode</span>
109
+ <div class="relative inline-block w-10 mr-2 align-middle select-none">
110
+ <input type="checkbox" id="dark-mode-toggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
111
+ <label for="dark-mode-toggle" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
112
+ </div>
113
+ </div>
114
+ <div class="flex items-center justify-between mb-4">
115
+ <span>Private Room</span>
116
+ <div class="relative inline-block w-10 mr-2 align-middle select-none">
117
+ <input type="checkbox" id="private-room-toggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
118
+ <label for="private-room-toggle" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
119
+ </div>
120
+ </div>
121
+ <div id="password-field" class="hidden mb-4">
122
+ <label for="room-password" class="block text-sm font-medium mb-1">Room Password</label>
123
+ <input type="password" id="room-password" class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-700 border-gray-300 dark:border-gray-600">
124
+ </div>
125
+ <button id="transfer-host" class="w-full px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition mb-2">
126
+ <i class="fas fa-crown mr-2"></i>Transfer Host
127
+ </button>
128
+ <button id="leave-room" class="w-full px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition">
129
+ <i class="fas fa-sign-out-alt mr-2"></i>Leave Room
130
+ </button>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ </div>
136
+
137
+ <div class="flex flex-col lg:flex-row gap-6">
138
+ <!-- Video Player Section -->
139
+ <div class="lg:w-2/3">
140
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
141
+ <!-- Video Player -->
142
+ <div class="video-container">
143
+ <video id="video-player" class="w-full" controls></video>
144
+ </div>
145
+
146
+ <!-- Video Controls -->
147
+ <div class="p-4 bg-gray-50 dark:bg-gray-700">
148
+ <div class="flex items-center justify-between mb-4">
149
+ <div class="flex items-center space-x-4">
150
+ <button id="play-pause" class="w-10 h-10 rounded-full bg-blue-500 text-white flex items-center justify-center hover:bg-blue-600 transition">
151
+ <i class="fas fa-play" id="play-icon"></i>
152
+ </button>
153
+ <div class="flex items-center space-x-2">
154
+ <button id="volume-btn" class="w-8 h-8 text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100">
155
+ <i class="fas fa-volume-up" id="volume-icon"></i>
156
+ </button>
157
+ <input type="range" id="volume-slider" min="0" max="1" step="0.01" value="1" class="w-20">
158
+ </div>
159
+ </div>
160
+ <div class="flex items-center space-x-4">
161
+ <div class="text-sm text-gray-600 dark:text-gray-300" id="current-time">0:00</div>
162
+ <div class="flex items-center">
163
+ <input type="range" id="progress-bar" min="0" max="100" value="0" class="w-64 md:w-96">
164
+ </div>
165
+ <div class="text-sm text-gray-600 dark:text-gray-300" id="duration">0:00</div>
166
+ </div>
167
+ <div class="flex items-center space-x-2">
168
+ <button id="quality-btn" class="px-3 py-1 bg-gray-200 dark:bg-gray-600 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-500 transition">
169
+ <span id="quality-text">Auto</span>
170
+ <i class="fas fa-caret-down ml-1"></i>
171
+ </button>
172
+ <div id="quality-dropdown" class="hidden absolute mt-2 w-32 bg-white dark:bg-gray-800 rounded-lg shadow-lg py-1 z-10">
173
+ <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700" data-quality="auto">Auto</a>
174
+ <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700" data-quality="1080">1080p</a>
175
+ <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700" data-quality="720">720p</a>
176
+ <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700" data-quality="480">480p</a>
177
+ </div>
178
+ <button id="fullscreen-btn" class="w-8 h-8 text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100">
179
+ <i class="fas fa-expand"></i>
180
+ </button>
181
+ </div>
182
+ </div>
183
+
184
+ <!-- Video Info -->
185
+ <div class="mb-4">
186
+ <h2 class="text-xl font-bold mb-1" id="video-title">No video selected</h2>
187
+ <p class="text-gray-600 dark:text-gray-300 text-sm" id="video-description">Add a video to start watching together</p>
188
+ </div>
189
+
190
+ <!-- Video URL Input -->
191
+ <div class="flex space-x-2">
192
+ <div class="flex-grow relative">
193
+ <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
194
+ <i class="fas fa-link text-gray-400"></i>
195
+ </div>
196
+ <input type="text" id="video-url" class="pl-10 pr-4 py-2 border rounded-lg w-full bg-gray-50 dark:bg-gray-600 border-gray-300 dark:border-gray-500" placeholder="Paste YouTube URL here">
197
+ </div>
198
+ <button id="add-video" class="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition">
199
+ <i class="fas fa-plus mr-2"></i>Add
200
+ </button>
201
+ </div>
202
+ </div>
203
+ </div>
204
+
205
+ <!-- Playlist -->
206
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg mt-6 overflow-hidden">
207
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
208
+ <h3 class="font-semibold text-lg">
209
+ <i class="fas fa-list-ol mr-2 text-blue-500"></i>Playlist
210
+ </h3>
211
+ <button id="clear-playlist" class="text-sm text-red-500 hover:text-red-600">
212
+ <i class="fas fa-trash mr-1"></i>Clear All
213
+ </button>
214
+ </div>
215
+ <div class="divide-y divide-gray-200 dark:divide-gray-700 max-h-64 overflow-y-auto" id="playlist-container">
216
+ <div class="p-4 flex items-center hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer">
217
+ <div class="w-16 h-10 bg-gray-200 dark:bg-gray-600 rounded flex-shrink-0 overflow-hidden">
218
+ <img src="https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg" class="w-full h-full object-cover">
219
+ </div>
220
+ <div class="ml-3 flex-grow">
221
+ <h4 class="font-medium text-sm line-clamp-1">Never Gonna Give You Up</h4>
222
+ <p class="text-xs text-gray-500 dark:text-gray-400">Rick Astley β€’ 3:32</p>
223
+ </div>
224
+ <button class="text-gray-400 hover:text-red-500">
225
+ <i class="fas fa-times"></i>
226
+ </button>
227
+ </div>
228
+ <!-- More playlist items will be added here dynamically -->
229
+ </div>
230
+ </div>
231
+ </div>
232
+
233
+ <!-- Chat Section -->
234
+ <div class="lg:w-1/3">
235
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden h-full flex flex-col">
236
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
237
+ <h3 class="font-semibold text-lg">
238
+ <i class="fas fa-comments mr-2 text-blue-500"></i>Chat
239
+ </h3>
240
+ <div class="flex items-center space-x-2">
241
+ <button id="emoji-picker-btn" class="w-8 h-8 text-gray-600 dark:text-gray-300 hover:text-blue-500">
242
+ <i class="far fa-smile"></i>
243
+ </button>
244
+ <div id="emoji-picker" class="hidden absolute right-0 mt-2 w-64 h-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 z-10 overflow-y-auto">
245
+ <!-- Emoji categories and emojis will be added here -->
246
+ </div>
247
+ </div>
248
+ </div>
249
+
250
+ <!-- Chat Messages -->
251
+ <div class="flex-grow p-4 overflow-y-auto" id="chat-messages">
252
+ <div class="message mb-4 message-animate">
253
+ <div class="flex items-start">
254
+ <div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-semibold mr-2">S</div>
255
+ <div>
256
+ <div class="flex items-center">
257
+ <span class="font-semibold mr-2">System</span>
258
+ <span class="text-xs text-gray-500 dark:text-gray-400">Just now</span>
259
+ </div>
260
+ <p class="text-sm mt-1">Welcome to the room! Start watching videos together with your friends.</p>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ <!-- More chat messages will be added here dynamically -->
265
+ </div>
266
+
267
+ <!-- Chat Input -->
268
+ <div class="p-4 border-t border-gray-200 dark:border-gray-700">
269
+ <div class="flex space-x-2">
270
+ <input type="text" id="chat-input" class="flex-grow px-4 py-2 border rounded-lg bg-gray-50 dark:bg-gray-700 border-gray-300 dark:border-gray-600" placeholder="Type a message...">
271
+ <button id="send-message" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition">
272
+ <i class="fas fa-paper-plane"></i>
273
+ </button>
274
+ </div>
275
+ </div>
276
+ </div>
277
+
278
+ <!-- Users Online -->
279
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg mt-6 overflow-hidden">
280
+ <div class="p-4 border-b border-gray-200 dark:border-gray-700">
281
+ <h3 class="font-semibold text-lg">
282
+ <i class="fas fa-users mr-2 text-blue-500"></i>Users Online
283
+ </h3>
284
+ </div>
285
+ <div class="divide-y divide-gray-200 dark:divide-gray-700 max-h-64 overflow-y-auto" id="users-list">
286
+ <div class="p-3 flex items-center">
287
+ <div class="relative">
288
+ <div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-semibold mr-2">Y</div>
289
+ <div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-white dark:border-gray-800"></div>
290
+ </div>
291
+ <div class="ml-2">
292
+ <p class="font-medium">You (Host)</p>
293
+ <p class="text-xs text-gray-500 dark:text-gray-400">Watching</p>
294
+ </div>
295
+ </div>
296
+ <!-- More users will be added here dynamically -->
297
+ </div>
298
+ </div>
299
+ </div>
300
+ </div>
301
+ </div>
302
+
303
+ <!-- Modals -->
304
+ <!-- Create/Join Room Modal -->
305
+ <div id="room-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
306
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6">
307
+ <div class="flex justify-between items-center mb-4">
308
+ <h3 class="text-xl font-bold">Join a Watch Party</h3>
309
+ <button id="close-room-modal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
310
+ <i class="fas fa-times"></i>
311
+ </button>
312
+ </div>
313
+ <div class="space-y-4">
314
+ <div>
315
+ <label for="room-id" class="block text-sm font-medium mb-1">Room ID</label>
316
+ <input type="text" id="room-id" class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-700 border-gray-300 dark:border-gray-600" placeholder="Enter room ID">
317
+ </div>
318
+ <div id="password-input" class="hidden">
319
+ <label for="join-password" class="block text-sm font-medium mb-1">Room Password</label>
320
+ <input type="password" id="join-password" class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-700 border-gray-300 dark:border-gray-600" placeholder="Enter password">
321
+ </div>
322
+ <div class="flex space-x-3 pt-2">
323
+ <button id="join-room" class="flex-grow px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition">
324
+ Join Room
325
+ </button>
326
+ <button id="create-room" class="flex-grow px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition">
327
+ Create Room
328
+ </button>
329
+ </div>
330
+ </div>
331
+ </div>
332
+ </div>
333
+
334
+ <!-- Transfer Host Modal -->
335
+ <div id="transfer-host-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
336
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6">
337
+ <div class="flex justify-between items-center mb-4">
338
+ <h3 class="text-xl font-bold">Transfer Host</h3>
339
+ <button id="close-transfer-modal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
340
+ <i class="fas fa-times"></i>
341
+ </button>
342
+ </div>
343
+ <div class="space-y-4">
344
+ <p class="text-gray-600 dark:text-gray-300">Select a user to transfer host privileges to:</p>
345
+ <div class="max-h-60 overflow-y-auto" id="transfer-users-list">
346
+ <!-- Users will be listed here -->
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </div>
351
+
352
+ <script>
353
+ // DOM Elements
354
+ const videoPlayer = document.getElementById('video-player');
355
+ const playPauseBtn = document.getElementById('play-pause');
356
+ const playIcon = document.getElementById('play-icon');
357
+ const progressBar = document.getElementById('progress-bar');
358
+ const currentTimeDisplay = document.getElementById('current-time');
359
+ const durationDisplay = document.getElementById('duration');
360
+ const volumeBtn = document.getElementById('volume-btn');
361
+ const volumeIcon = document.getElementById('volume-icon');
362
+ const volumeSlider = document.getElementById('volume-slider');
363
+ const fullscreenBtn = document.getElementById('fullscreen-btn');
364
+ const qualityBtn = document.getElementById('quality-btn');
365
+ const qualityDropdown = document.getElementById('quality-dropdown');
366
+ const qualityText = document.getElementById('quality-text');
367
+ const videoUrlInput = document.getElementById('video-url');
368
+ const addVideoBtn = document.getElementById('add-video');
369
+ const videoTitle = document.getElementById('video-title');
370
+ const videoDescription = document.getElementById('video-description');
371
+ const playlistContainer = document.getElementById('playlist-container');
372
+ const clearPlaylistBtn = document.getElementById('clear-playlist');
373
+ const chatInput = document.getElementById('chat-input');
374
+ const sendMessageBtn = document.getElementById('send-message');
375
+ const chatMessages = document.getElementById('chat-messages');
376
+ const emojiPickerBtn = document.getElementById('emoji-picker-btn');
377
+ const emojiPicker = document.getElementById('emoji-picker');
378
+ const usersList = document.getElementById('users-list');
379
+ const userCount = document.getElementById('user-count');
380
+ const roomLink = document.getElementById('room-link');
381
+ const copyLinkBtn = document.getElementById('copy-link');
382
+ const settingsToggle = document.getElementById('settings-toggle');
383
+ const settingsDropdown = document.getElementById('settings-dropdown');
384
+ const darkModeToggle = document.getElementById('dark-mode-toggle');
385
+ const privateRoomToggle = document.getElementById('private-room-toggle');
386
+ const passwordField = document.getElementById('password-field');
387
+ const roomPassword = document.getElementById('room-password');
388
+ const transferHostBtn = document.getElementById('transfer-host');
389
+ const leaveRoomBtn = document.getElementById('leave-room');
390
+ const themeToggle = document.getElementById('theme-toggle');
391
+ const roomModal = document.getElementById('room-modal');
392
+ const closeRoomModal = document.getElementById('close-room-modal');
393
+ const joinRoomBtn = document.getElementById('join-room');
394
+ const createRoomBtn = document.getElementById('create-room');
395
+ const roomIdInput = document.getElementById('room-id');
396
+ const passwordInput = document.getElementById('password-input');
397
+ const joinPassword = document.getElementById('join-password');
398
+ const transferHostModal = document.getElementById('transfer-host-modal');
399
+ const closeTransferModal = document.getElementById('close-transfer-modal');
400
+ const transferUsersList = document.getElementById('transfer-users-list');
401
+
402
+ // State
403
+ let isHost = true;
404
+ let currentVideo = null;
405
+ let playlist = [];
406
+ let users = [];
407
+ let socket = null;
408
+ let roomId = 'abc123'; // This would be generated or from URL in a real app
409
+ let isPlaying = false;
410
+ let currentVolume = 1;
411
+ let isMuted = false;
412
+ let currentQuality = 'auto';
413
+ let isDarkMode = localStorage.getItem('darkMode') === 'true' ||
414
+ (!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches);
415
+
416
+ // Initialize
417
+ document.documentElement.classList.toggle('dark', isDarkMode);
418
+ darkModeToggle.checked = isDarkMode;
419
+
420
+ // Mock socket connection (in a real app, this would connect to your backend)
421
+ function connectSocket() {
422
+ console.log('Connecting to socket server...');
423
+ // In a real app: socket = io('https://your-backend-url');
424
+
425
+ // Mock socket behavior
426
+ setTimeout(() => {
427
+ console.log('Socket connected');
428
+ addSystemMessage('Connected to the room');
429
+ updateUserList([
430
+ { id: 'user1', name: 'You', isHost: true, status: 'online', watching: true },
431
+ { id: 'user2', name: 'Friend 1', isHost: false, status: 'online', watching: true },
432
+ { id: 'user3', name: 'Friend 2', isHost: false, status: 'online', watching: false }
433
+ ]);
434
+ }, 500);
435
+ }
436
+
437
+ // Event Listeners
438
+ themeToggle.addEventListener('click', toggleDarkMode);
439
+ darkModeToggle.addEventListener('change', toggleDarkMode);
440
+
441
+ privateRoomToggle.addEventListener('change', function() {
442
+ passwordField.classList.toggle('hidden', !this.checked);
443
+ });
444
+
445
+ settingsToggle.addEventListener('click', function() {
446
+ settingsDropdown.classList.toggle('hidden');
447
+ });
448
+
449
+ document.addEventListener('click', function(event) {
450
+ if (!settingsToggle.contains(event.target) && !settingsDropdown.contains(event.target)) {
451
+ settingsDropdown.classList.add('hidden');
452
+ }
453
+ if (!qualityBtn.contains(event.target) && !qualityDropdown.contains(event.target)) {
454
+ qualityDropdown.classList.add('hidden');
455
+ }
456
+ if (!emojiPickerBtn.contains(event.target) && !emojiPicker.contains(event.target)) {
457
+ emojiPicker.classList.add('hidden');
458
+ }
459
+ });
460
+
461
+ qualityBtn.addEventListener('click', function() {
462
+ qualityDropdown.classList.toggle('hidden');
463
+ });
464
+
465
+ document.querySelectorAll('#quality-dropdown a').forEach(item => {
466
+ item.addEventListener('click', function(e) {
467
+ e.preventDefault();
468
+ currentQuality = this.dataset.quality;
469
+ qualityText.textContent = currentQuality === 'auto' ? 'Auto' : `${currentQuality}p`;
470
+ qualityDropdown.classList.add('hidden');
471
+ // In a real app, this would change the video quality
472
+ console.log(`Changed quality to ${currentQuality}`);
473
+ });
474
+ });
475
+
476
+ playPauseBtn.addEventListener('click', togglePlayPause);
477
+ videoPlayer.addEventListener('play', function() {
478
+ isPlaying = true;
479
+ playIcon.className = 'fas fa-pause';
480
+ });
481
+ videoPlayer.addEventListener('pause', function() {
482
+ isPlaying = false;
483
+ playIcon.className = 'fas fa-play';
484
+ });
485
+ videoPlayer.addEventListener('timeupdate', updateProgressBar);
486
+ videoPlayer.addEventListener('durationchange', updateDuration);
487
+ videoPlayer.addEventListener('volumechange', updateVolumeUI);
488
+
489
+ progressBar.addEventListener('input', function() {
490
+ const seekTime = videoPlayer.duration * (this.value / 100);
491
+ videoPlayer.currentTime = seekTime;
492
+ // In a real app, this would sync with other users
493
+ if (isHost) {
494
+ // socket.emit('seek', { time: seekTime });
495
+ }
496
+ });
497
+
498
+ volumeBtn.addEventListener('click', toggleMute);
499
+ volumeSlider.addEventListener('input', function() {
500
+ videoPlayer.volume = this.value;
501
+ currentVolume = this.value;
502
+ updateVolumeUI();
503
+ });
504
+
505
+ fullscreenBtn.addEventListener('click', toggleFullscreen);
506
+
507
+ addVideoBtn.addEventListener('click', addVideoToPlaylist);
508
+ videoUrlInput.addEventListener('keypress', function(e) {
509
+ if (e.key === 'Enter') addVideoToPlaylist();
510
+ });
511
+
512
+ clearPlaylistBtn.addEventListener('click', clearPlaylist);
513
+
514
+ sendMessageBtn.addEventListener('click', sendMessage);
515
+ chatInput.addEventListener('keypress', function(e) {
516
+ if (e.key === 'Enter') sendMessage();
517
+ });
518
+
519
+ emojiPickerBtn.addEventListener('click', function() {
520
+ emojiPicker.classList.toggle('hidden');
521
+ // In a real app, this would load emoji categories and emojis
522
+ });
523
+
524
+ copyLinkBtn.addEventListener('click', copyRoomLink);
525
+
526
+ transferHostBtn.addEventListener('click', openTransferHostModal);
527
+ leaveRoomBtn.addEventListener('click', leaveRoom);
528
+
529
+ closeRoomModal.addEventListener('click', function() {
530
+ roomModal.classList.add('hidden');
531
+ });
532
+
533
+ joinRoomBtn.addEventListener('click', joinRoom);
534
+ createRoomBtn.addEventListener('click', createRoom);
535
+
536
+ closeTransferModal.addEventListener('click', function() {
537
+ transferHostModal.classList.add('hidden');
538
+ });
539
+
540
+ // Functions
541
+ function toggleDarkMode() {
542
+ isDarkMode = !isDarkMode;
543
+ document.documentElement.classList.toggle('dark', isDarkMode);
544
+ localStorage.setItem('darkMode', isDarkMode);
545
+ darkModeToggle.checked = isDarkMode;
546
+ }
547
+
548
+ function togglePlayPause() {
549
+ if (isPlaying) {
550
+ videoPlayer.pause();
551
+ // In a real app, this would sync with other users
552
+ if (isHost) {
553
+ // socket.emit('pause');
554
+ }
555
+ } else {
556
+ videoPlayer.play();
557
+ // In a real app, this would sync with other users
558
+ if (isHost) {
559
+ // socket.emit('play');
560
+ }
561
+ }
562
+ }
563
+
564
+ function updateProgressBar() {
565
+ const percent = (videoPlayer.currentTime / videoPlayer.duration) * 100;
566
+ progressBar.value = percent;
567
+
568
+ // Update time display
569
+ currentTimeDisplay.textContent = formatTime(videoPlayer.currentTime);
570
+ }
571
+
572
+ function updateDuration() {
573
+ durationDisplay.textContent = formatTime(videoPlayer.duration);
574
+ }
575
+
576
+ function formatTime(seconds) {
577
+ const mins = Math.floor(seconds / 60);
578
+ const secs = Math.floor(seconds % 60);
579
+ return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
580
+ }
581
+
582
+ function updateVolumeUI() {
583
+ if (videoPlayer.muted || videoPlayer.volume === 0) {
584
+ volumeIcon.className = 'fas fa-volume-mute';
585
+ isMuted = true;
586
+ } else {
587
+ isMuted = false;
588
+ if (videoPlayer.volume >= 0.5) {
589
+ volumeIcon.className = 'fas fa-volume-up';
590
+ } else if (videoPlayer.volume > 0) {
591
+ volumeIcon.className = 'fas fa-volume-down';
592
+ } else {
593
+ volumeIcon.className = 'fas fa-volume-off';
594
+ }
595
+ }
596
+ volumeSlider.value = videoPlayer.volume;
597
+ }
598
+
599
+ function toggleMute() {
600
+ videoPlayer.muted = !videoPlayer.muted;
601
+ updateVolumeUI();
602
+ }
603
+
604
+ function toggleFullscreen() {
605
+ if (!document.fullscreenElement) {
606
+ videoPlayer.requestFullscreen().catch(err => {
607
+ console.error(`Error attempting to enable fullscreen: ${err.message}`);
608
+ });
609
+ } else {
610
+ document.exitFullscreen();
611
+ }
612
+ }
613
+
614
+ function addVideoToPlaylist() {
615
+ const url = videoUrlInput.value.trim();
616
+ if (!url) return;
617
+
618
+ // In a real app, this would validate the URL and fetch video info from your backend
619
+ // For demo purposes, we'll mock this
620
+ const videoId = extractVideoId(url) || 'dQw4w9WgXcQ'; // Default to Rick Astley if invalid
621
+ const videoInfo = {
622
+ id: videoId,
623
+ title: "Sample Video Title",
624
+ duration: 183, // 3:03 in seconds
625
+ thumbnail: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`,
626
+ url: url
627
+ };
628
+
629
+ playlist.push(videoInfo);
630
+ renderPlaylist();
631
+
632
+ if (!currentVideo) {
633
+ playVideo(videoInfo);
634
+ }
635
+
636
+ videoUrlInput.value = '';
637
+
638
+ // In a real app, this would sync with other users
639
+ if (isHost) {
640
+ // socket.emit('addToPlaylist', videoInfo);
641
+ }
642
+ }
643
+
644
+ function extractVideoId(url) {
645
+ // Simple URL parsing - in a real app you'd want more robust validation
646
+ const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
647
+ const match = url.match(regExp);
648
+ return (match && match[2].length === 11) ? match[2] : null;
649
+ }
650
+
651
+ function renderPlaylist() {
652
+ playlistContainer.innerHTML = '';
653
+
654
+ playlist.forEach((video, index) => {
655
+ const item = document.createElement('div');
656
+ item.className = 'p-4 flex items-center hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer';
657
+ if (currentVideo && currentVideo.id === video.id) {
658
+ item.classList.add('bg-blue-50', 'dark:bg-blue-900');
659
+ }
660
+
661
+ item.innerHTML = `
662
+ <div class="w-16 h-10 bg-gray-200 dark:bg-gray-600 rounded flex-shrink-0 overflow-hidden">
663
+ <img src="${video.thumbnail}" class="w-full h-full object-cover">
664
+ </div>
665
+ <div class="ml-3 flex-grow">
666
+ <h4 class="font-medium text-sm line-clamp-1">${video.title}</h4>
667
+ <p class="text-xs text-gray-500 dark:text-gray-400">${formatTime(video.duration)}</p>
668
+ </div>
669
+ <button class="text-gray-400 hover:text-red-500 remove-video" data-index="${index}">
670
+ <i class="fas fa-times"></i>
671
+ </button>
672
+ `;
673
+
674
+ item.addEventListener('click', () => {
675
+ playVideo(video);
676
+ });
677
+
678
+ playlistContainer.appendChild(item);
679
+ });
680
+
681
+ // Add event listeners to remove buttons
682
+ document.querySelectorAll('.remove-video').forEach(btn => {
683
+ btn.addEventListener('click', function(e) {
684
+ e.stopPropagation();
685
+ const index = parseInt(this.dataset.index);
686
+ removeFromPlaylist(index);
687
+ });
688
+ });
689
+ }
690
+
691
+ function playVideo(video) {
692
+ currentVideo = video;
693
+ videoTitle.textContent = video.title;
694
+ videoDescription.textContent = `Added by You β€’ ${formatTime(video.duration)}`;
695
+
696
+ // In a real app, this would use yt-dlp to get the direct stream URL
697
+ // For demo, we'll just show a placeholder
698
+ videoPlayer.src = '';
699
+ videoPlayer.poster = video.thumbnail;
700
+
701
+ renderPlaylist();
702
+
703
+ // In a real app, this would sync with other users
704
+ if (isHost) {
705
+ // socket.emit('playVideo', video);
706
+ }
707
+ }
708
+
709
+ function removeFromPlaylist(index) {
710
+ playlist.splice(index, 1);
711
+ renderPlaylist();
712
+
713
+ // In a real app, this would sync with other users
714
+ if (isHost) {
715
+ // socket.emit('removeFromPlaylist', index);
716
+ }
717
+ }
718
+
719
+ function clearPlaylist() {
720
+ playlist = [];
721
+ renderPlaylist();
722
+ currentVideo = null;
723
+ videoPlayer.src = '';
724
+ videoTitle.textContent = 'No video selected';
725
+ videoDescription.textContent = 'Add a video to start watching together';
726
+
727
+ // In a real app, this would sync with other users
728
+ if (isHost) {
729
+ // socket.emit('clearPlaylist');
730
+ }
731
+ }
732
+
733
+ function sendMessage() {
734
+ const message = chatInput.value.trim();
735
+ if (!message) return;
736
+
737
+ // In a real app, this would send via socket
738
+ // socket.emit('chatMessage', { text: message });
739
+
740
+ addChatMessage('You', message, new Date(), true);
741
+ chatInput.value = '';
742
+ }
743
+
744
+ function addChatMessage(sender, text, timestamp, isSelf = false) {
745
+ const messageDiv = document.createElement('div');
746
+ messageDiv.className = `message mb-4 message-animate ${isSelf ? 'self-message' : ''}`;
747
+
748
+ const timeString = formatChatTime(timestamp);
749
+
750
+ messageDiv.innerHTML = `
751
+ <div class="flex items-start ${isSelf ? 'flex-row-reverse' : ''}">
752
+ <div class="w-8 h-8 rounded-full ${isSelf ? 'bg-blue-500' : 'bg-purple-500'} flex items-center justify-center text-white font-semibold ${isSelf ? 'ml-2' : 'mr-2'}">
753
+ ${sender.charAt(0).toUpperCase()}
754
+ </div>
755
+ <div class="${isSelf ? 'text-right' : ''}">
756
+ <div class="flex items-center ${isSelf ? 'justify-end' : ''}">
757
+ <span class="font-semibold mr-2">${isSelf ? 'You' : sender}</span>
758
+ <span class="text-xs text-gray-500 dark:text-gray-400">${timeString}</span>
759
+ </div>
760
+ <p class="text-sm mt-1 ${isSelf ? 'bg-blue-500 text-white' : 'bg-gray-100 dark:bg-gray-700'} rounded-lg px-3 py-2 inline-block">${text}</p>
761
+ </div>
762
+ </div>
763
+ `;
764
+
765
+ chatMessages.appendChild(messageDiv);
766
+ chatMessages.scrollTop = chatMessages.scrollHeight;
767
+ }
768
+
769
+ function addSystemMessage(text) {
770
+ const messageDiv = document.createElement('div');
771
+ messageDiv.className = 'message mb-4 message-animate';
772
+
773
+ messageDiv.innerHTML = `
774
+ <div class="flex items-start">
775
+ <div class="w-8 h-8 rounded-full bg-gray-500 flex items-center justify-center text-white font-semibold mr-2">
776
+ <i class="fas fa-info-circle"></i>
777
+ </div>
778
+ <div>
779
+ <div class="flex items-center">
780
+ <span class="font-semibold mr-2">System</span>
781
+ <span class="text-xs text-gray-500 dark:text-gray-400">just now</span>
782
+ </div>
783
+ <p class="text-sm mt-1">${text}</p>
784
+ </div>
785
+ </div>
786
+ `;
787
+
788
+ chatMessages.appendChild(messageDiv);
789
+ chatMessages.scrollTop = chatMessages.scrollHeight;
790
+ }
791
+
792
+ function formatChatTime(date) {
793
+ const now = new Date();
794
+ const diff = now - date;
795
+
796
+ if (diff < 60000) return 'just now';
797
+ if (diff < 3600000) return `${Math.floor(diff / 60000)} min ago`;
798
+
799
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
800
+ }
801
+
802
+ function updateUserList(userList) {
803
+ users = userList;
804
+ usersList.innerHTML = '';
805
+
806
+ let onlineCount = 0;
807
+
808
+ userList.forEach(user => {
809
+ if (user.status === 'online') onlineCount++;
810
+
811
+ const userDiv = document.createElement('div');
812
+ userDiv.className = 'p-3 flex items-center';
813
+
814
+ userDiv.innerHTML = `
815
+ <div class="relative">
816
+ <div class="w-8 h-8 rounded-full ${user.isHost ? 'bg-yellow-500' : 'bg-green-500'} flex items-center justify-center text-white font-semibold mr-2">
817
+ ${user.name.charAt(0).toUpperCase()}
818
+ </div>
819
+ <div class="absolute bottom-0 right-0 w-3 h-3 ${user.status === 'online' ? 'bg-green-500' : 'bg-gray-500'} rounded-full border-2 border-white dark:border-gray-800"></div>
820
+ </div>
821
+ <div class="ml-2">
822
+ <p class="font-medium">${user.name}${user.isHost ? ' (Host)' : ''}</p>
823
+ <p class="text-xs text-gray-500 dark:text-gray-400">${user.watching ? 'Watching' : 'Idle'}</p>
824
+ </div>
825
+ `;
826
+
827
+ usersList.appendChild(userDiv);
828
+ });
829
+
830
+ userCount.textContent = onlineCount;
831
+ }
832
+
833
+ function copyRoomLink() {
834
+ roomLink.select();
835
+ document.execCommand('copy');
836
+
837
+ // Show feedback
838
+ const originalText = copyLinkBtn.innerHTML;
839
+ copyLinkBtn.innerHTML = '<i class="fas fa-check mr-2"></i>Copied!';
840
+ setTimeout(() => {
841
+ copyLinkBtn.innerHTML = originalText;
842
+ }, 2000);
843
+ }
844
+
845
+ function openTransferHostModal() {
846
+ transferUsersList.innerHTML = '';
847
+
848
+ // Filter out yourself and offline users
849
+ const eligibleUsers = users.filter(user =>
850
+ user.id !== 'user1' && user.status === 'online'
851
+ );
852
+
853
+ if (eligibleUsers.length === 0) {
854
+ transferUsersList.innerHTML = '<p class="text-gray-500 dark:text-gray-400 text-center py-4">No eligible users to transfer to</p>';
855
+ } else {
856
+ eligibleUsers.forEach(user => {
857
+ const userItem = document.createElement('div');
858
+ userItem.className = 'p-3 flex items-center hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer rounded-lg';
859
+ userItem.innerHTML = `
860
+ <div class="w-10 h-10 rounded-full bg-green-500 flex items-center justify-center text-white font-semibold mr-3">
861
+ ${user.name.charAt(0).toUpperCase()}
862
+ </div>
863
+ <div>
864
+ <p class="font-medium">${user.name}</p>
865
+ </div>
866
+ `;
867
+
868
+ userItem.addEventListener('click', () => {
869
+ // In a real app, this would transfer host via socket
870
+ // socket.emit('transferHost', user.id);
871
+ addSystemMessage(`You transferred host privileges to ${user.name}`);
872
+ isHost = false;
873
+ transferHostModal.classList.add('hidden');
874
+ });
875
+
876
+ transferUsersList.appendChild(userItem);
877
+ });
878
+ }
879
+
880
+ transferHostModal.classList.remove('hidden');
881
+ }
882
+
883
+ function leaveRoom() {
884
+ // In a real app, this would disconnect from socket
885
+ addSystemMessage('You left the room');
886
+
887
+ // Redirect to home or show room modal
888
+ roomModal.classList.remove('hidden');
889
+ }
890
+
891
+ function joinRoom() {
892
+ const roomId = roomIdInput.value.trim();
893
+ if (!roomId) return;
894
+
895
+ const password = privateRoomToggle.checked ? joinPassword.value.trim() : null;
896
+
897
+ // In a real app, this would validate with the backend
898
+ addSystemMessage(`Joined room ${roomId}`);
899
+ roomModal.classList.add('hidden');
900
+ isHost = false;
901
+ }
902
+
903
+ function createRoom() {
904
+ // In a real app, this would create a room on the backend
905
+ const roomId = generateRoomId();
906
+ const isPrivate = privateRoomToggle.checked;
907
+ const password = isPrivate ? roomPassword.value.trim() : null;
908
+
909
+ addSystemMessage(`Created new room ${roomId}`);
910
+ roomModal.classList.add('hidden');
911
+ isHost = true;
912
+ roomLink.value = `https://syncstream.party/room/${roomId}`;
913
+ }
914
+
915
+ function generateRoomId() {
916
+ // Simple ID generator - in a real app you'd want something more robust
917
+ return Math.random().toString(36).substring(2, 8).toUpperCase();
918
+ }
919
+
920
+ // Initialize the app
921
+ connectSocket();
922
+
923
+ // For demo purposes, add some mock data
924
+ setTimeout(() => {
925
+ addChatMessage('Friend 1', 'Hey everyone! Ready to watch some videos?', new Date(Date.now() - 120000));
926
+ addChatMessage('Friend 2', 'Yeah! What should we watch first?', new Date(Date.now() - 60000));
927
+
928
+ // Add a sample video to playlist
929
+ const sampleVideo = {
930
+ id: 'dQw4w9WgXcQ',
931
+ title: "Never Gonna Give You Up",
932
+ duration: 212,
933
+ thumbnail: "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
934
+ url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
935
+ };
936
+ playlist.push(sampleVideo);
937
+ renderPlaylist();
938
+ }, 1000);
939
+ </script>
940
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Ahmadraza76/yp" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
941
+ </html>