triflix commited on
Commit
e1bad47
·
verified ·
1 Parent(s): 30d1089

Update templates/room.html

Browse files
Files changed (1) hide show
  1. templates/room.html +319 -26
templates/room.html CHANGED
@@ -1,39 +1,332 @@
1
- <!DOCTYPE html>
2
- <html>
3
  <head>
4
- <title>Room {{ room_code }}</title>
5
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
 
 
6
  </head>
7
- <body class="container mt-3">
8
- <h2>Room: {{ room_code }}</h2>
9
- <p>You are <b>{{ name }}</b> {% if is_admin %}(Admin){% endif %}</p>
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- <h4>Participants</h4>
12
- <ul id="participants" class="list-group mb-3"></ul>
 
 
 
 
 
 
 
 
 
 
 
13
 
14
- <h4>Playlist</h4>
15
- <ul id="playlist" class="list-group mb-3"></ul>
 
16
 
17
- <form id="uploadForm" enctype="multipart/form-data" class="mb-3">
18
- <input type="file" name="file" accept="audio/mp3" class="form-control mb-2" required>
19
- <input type="hidden" name="room_code" value="{{ room_code }}">
20
- <input type="hidden" name="session_id" value="{{ session_id }}">
21
- <button type="submit" class="btn btn-secondary">Upload MP3</button>
22
- </form>
23
 
24
- {% if is_admin %}
25
- <button id="togglePlay" class="btn btn-primary mb-3">▶️ Play</button>
26
- {% endif %}
 
 
 
 
27
 
28
- <audio id="player" controls class="w-100"></audio>
 
29
 
30
  <script>
31
  const ROOM_CODE = "{{ room_code }}";
32
- const SESSION_ID = "{{ session_id }}";
33
- const NAME = "{{ name }}";
34
- const IS_ADMIN = {{ "true" if is_admin else "false" }};
35
  const ADMIN_TOKEN = "{{ admin_token }}";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  </script>
37
- <script src="/static/app.js"></script>
38
  </body>
39
- </html>
 
1
+ <!doctype html>
2
+ <html lang="en">
3
  <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Room {{ room_code }} — Music Sync</title>
7
+ <link rel="stylesheet" href="/static/style.css" />
8
  </head>
9
+ <body>
10
+ <div id="notifications-container"></div>
11
+ <main class="room-page">
12
+ <header class="room-header">
13
+ <div>
14
+ <h2>Room <strong id="room-code">{{ room_code }}</strong></h2>
15
+ <div class="muted">Name: <span id="my-name">{{ name }}</span>
16
+ <span id="admin-badge" style="display: none;">• Admin</span>
17
+ </div>
18
+ </div>
19
+ <div>
20
+ <button id="btn-mute" class="secondary">Mute</button>
21
+ <button id="enable-audio" class="primary">Enable Audio</button>
22
+ </div>
23
+ </header>
24
 
25
+ <section class="main-grid">
26
+ <div class="panel">
27
+ <h3>Now Playing</h3>
28
+ <div id="now-track">No track selected</div>
29
+ <div class="controls" id="admin-controls" style="display: none;">
30
+ <button id="btn-toggle-play" class="primary" disabled>Play</button>
31
+ <label class="muted">Seek to:
32
+ <input id="seek-input" type="number" value="0" min="0" style="width:80px;" />
33
+ <button id="btn-seek" disabled>Go</button>
34
+ </label>
35
+ </div>
36
+ <div class="status muted" id="status">Connecting...</div>
37
+ </div>
38
 
39
+ <div class="panel">
40
+ <h3>Playlist</h3>
41
+ <div id="playlist"></div>
42
 
43
+ <div id="upload-area" style="margin-top:12px; display:none;">
44
+ <form id="upload-form">
45
+ <input id="file-input" type="file" accept=".mp3,audio/*" required />
46
+ <button type="submit" class="secondary">Upload MP3</button>
47
+ </form>
 
48
 
49
+ <div id="upload-progress" style="display:none;margin-top:8px;">
50
+ <div class="progress-bar"><div id="progress-fill" style="width:0%"></div></div>
51
+ <div id="progress-text" class="muted small">Uploading...</div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </section>
56
 
57
+ <audio id="audio" preload="auto"></audio>
58
+ </main>
59
 
60
  <script>
61
  const ROOM_CODE = "{{ room_code }}";
62
+ const NAME = "{{ name|e }}";
63
+ let SESSION_ID = "{{ session_id }}";
64
+ const IS_ADMIN_HINT = {{ "true" if is_admin else "false" }};
65
  const ADMIN_TOKEN = "{{ admin_token }}";
66
+
67
+ if (!SESSION_ID || SESSION_ID === "") {
68
+ const stored = localStorage.getItem("session_id:" + ROOM_CODE);
69
+ if (stored) SESSION_ID = stored;
70
+ } else {
71
+ localStorage.setItem("session_id:" + ROOM_CODE, SESSION_ID);
72
+ }
73
+
74
+ const statusEl = document.getElementById("status");
75
+ const playlistEl = document.getElementById("playlist");
76
+ const nowTrackEl = document.getElementById("now-track");
77
+ const adminBadge = document.getElementById("admin-badge");
78
+ const adminControls = document.getElementById("admin-controls");
79
+ const uploadArea = document.getElementById("upload-area");
80
+ const notificationsContainer = document.getElementById("notifications-container");
81
+
82
+ const enableAudioBtn = document.getElementById("enable-audio");
83
+ const audio = document.getElementById("audio");
84
+
85
+ const btnTogglePlay = document.getElementById("btn-toggle-play");
86
+ const btnMute = document.getElementById("btn-mute");
87
+ const btnSeek = document.getElementById("btn-seek");
88
+ const seekInput = document.getElementById("seek-input");
89
+
90
+ let ws = null;
91
+ let isAdmin = IS_ADMIN_HINT;
92
+ if (isAdmin) {
93
+ adminBadge.style.display = "inline";
94
+ uploadArea.style.display = "block";
95
+ adminControls.style.display = "flex";
96
+ btnTogglePlay.disabled = false;
97
+ btnSeek.disabled = false;
98
+ }
99
+
100
+ let clockOffset = 0;
101
+ let currentState = {};
102
+
103
+ function connectWS() {
104
+ const q = new URLSearchParams({
105
+ session_id: SESSION_ID,
106
+ name: NAME,
107
+ ...(ADMIN_TOKEN && { admin_token: ADMIN_TOKEN })
108
+ });
109
+ const protocol = location.protocol === "https:" ? "wss:" : "ws:";
110
+ const wsUrl = `${protocol}//${location.host}/ws/${ROOM_CODE}?${q.toString()}`;
111
+ ws = new WebSocket(wsUrl);
112
+
113
+ ws.onopen = () => {
114
+ status("Connected. Syncing...");
115
+ ws.send(JSON.stringify({type:"request_sync"}));
116
+ };
117
+ ws.onmessage = (ev) => handleMessage(JSON.parse(ev.data));
118
+ ws.onclose = () => {
119
+ status("Disconnected. Reconnecting...");
120
+ setTimeout(connectWS, 2000);
121
+ };
122
+ ws.onerror = () => status("WebSocket error");
123
+ }
124
+
125
+ function handleMessage(msg) {
126
+ const type = msg.type;
127
+ if (type !== 'pong') console.log("ws recv", msg);
128
+
129
+ if (type === "joined") {
130
+ SESSION_ID = msg.session_id;
131
+ localStorage.setItem("session_id:" + ROOM_CODE, SESSION_ID);
132
+ isAdmin = msg.is_admin;
133
+ if (isAdmin) {
134
+ adminBadge.style.display = "inline";
135
+ uploadArea.style.display = "block";
136
+ adminControls.style.display = "flex";
137
+ btnTogglePlay.disabled = false;
138
+ btnSeek.disabled = false;
139
+ }
140
+ } else if (type === "sync_state") {
141
+ currentState = msg;
142
+ renderPlaylist(msg.tracks, msg.playlist);
143
+ applyCurrentState(msg);
144
+ status(`Synced (seq ${msg.seq})`);
145
+ } else if (type === "play") {
146
+ currentState.play_state = "playing";
147
+ currentState.current_track_id = msg.track_id;
148
+ currentState.play_started_at = msg.play_at - msg.position;
149
+ schedulePlay(msg.play_at, msg.position, msg.track_id);
150
+ updatePlayButton();
151
+ status("Playback started");
152
+ } else if (type === "pause") {
153
+ currentState.play_state = "paused";
154
+ currentState.play_position = msg.position;
155
+ doPause(msg.position);
156
+ updatePlayButton();
157
+ status("Playback paused");
158
+ } else if (type === "seek") {
159
+ currentState.play_position = msg.position;
160
+ if (currentState.play_state === "playing") {
161
+ audio.currentTime = msg.position;
162
+ }
163
+ status("Seeked");
164
+ } else if (type === "set_track") {
165
+ currentState.current_track_id = msg.track_id;
166
+ const track = findTrack(msg.track_id);
167
+ if (track) {
168
+ setAudioSrc(track.url);
169
+ nowTrackEl.textContent = track.original_name;
170
+ }
171
+ currentState.play_state = 'paused';
172
+ updatePlayButton();
173
+ status("Track changed");
174
+ } else if (type === "track_added") {
175
+ if (!currentState.tracks) currentState.tracks = [];
176
+ currentState.tracks.push(msg.track);
177
+ if (!currentState.playlist) currentState.playlist = [];
178
+ currentState.playlist.push(msg.track.track_id);
179
+ renderPlaylist(currentState.tracks, currentState.playlist);
180
+ status("New track added");
181
+ } else if (type === "user_joined" || type === "user_left") {
182
+ showNotification(`${msg.name} has ${type === 'user_joined' ? 'joined' : 'left'} the room.`);
183
+ }
184
+ }
185
+
186
+ function findTrack(track_id) {
187
+ return currentState?.tracks?.find(t => t.track_id === track_id) || null;
188
+ }
189
+
190
+ function renderPlaylist(tracks, playlist) {
191
+ playlistEl.innerHTML = "";
192
+ if (!tracks || !playlist || playlist.length === 0) {
193
+ playlistEl.innerHTML = "<div class='muted'>No tracks uploaded yet.</div>";
194
+ return;
195
+ }
196
+ playlist.forEach(tid => {
197
+ const track = tracks.find(t => t.track_id === tid);
198
+ if (!track) return;
199
+ const item = document.createElement("div");
200
+ item.className = "playlist-item";
201
+ item.innerHTML = `<div class="title">${track.original_name}</div><div class="meta">by ${track.uploader}</div>`;
202
+ if (isAdmin) {
203
+ item.onclick = () => ws.send(JSON.stringify({type: "set_track", track_id: track.track_id}));
204
+ }
205
+ playlistEl.appendChild(item);
206
+ });
207
+ }
208
+
209
+ function updatePlayButton() {
210
+ btnTogglePlay.textContent = currentState.play_state === 'playing' ? 'Pause' : 'Play';
211
+ }
212
+
213
+ function setAudioSrc(url) {
214
+ const fullUrl = url.startsWith("/media/") ? url : `/media/${url}`;
215
+ if (audio.src !== fullUrl) audio.src = fullUrl;
216
+ }
217
+
218
+ function schedulePlay(playAtServerTs, position, trackId) {
219
+ const now = Date.now() / 1000;
220
+ const delay = Math.max(0, playAtServerTs - (now + clockOffset));
221
+ const track = findTrack(trackId);
222
+ if(track) {
223
+ setAudioSrc(track.url);
224
+ nowTrackEl.textContent = track.original_name;
225
+ }
226
+ setTimeout(() => {
227
+ audio.currentTime = position;
228
+ audio.play().catch(e => console.error("Play failed:", e));
229
+ }, delay * 1000);
230
+ }
231
+
232
+ function doPause(position) {
233
+ audio.pause();
234
+ if(position) audio.currentTime = position;
235
+ }
236
+
237
+ function applyCurrentState(state) {
238
+ const track = findTrack(state.current_track_id);
239
+ if (track) {
240
+ setAudioSrc(track.url);
241
+ nowTrackEl.textContent = track.original_name;
242
+ }
243
+ if (state.play_state === "playing") {
244
+ schedulePlay(state.play_started_at + state.play_position, state.play_position, state.current_track_id);
245
+ } else {
246
+ doPause(state.play_position);
247
+ }
248
+ updatePlayButton();
249
+ }
250
+
251
+ function showNotification(message) {
252
+ const notif = document.createElement('div');
253
+ notif.className = 'notification';
254
+ notif.textContent = message;
255
+ notificationsContainer.appendChild(notif);
256
+ setTimeout(() => {
257
+ notif.style.opacity = '0';
258
+ setTimeout(() => notif.remove(), 500);
259
+ }, 3000);
260
+ }
261
+
262
+ btnTogglePlay.addEventListener("click", () => {
263
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
264
+ const action = currentState.play_state === 'playing' ? 'pause' : 'play';
265
+ ws.send(JSON.stringify({type: action, position: audio.currentTime}));
266
+ });
267
+
268
+ btnMute.addEventListener("click", () => {
269
+ audio.muted = !audio.muted;
270
+ btnMute.textContent = audio.muted ? 'Unmute' : 'Mute';
271
+ });
272
+
273
+ btnSeek.addEventListener("click", () => {
274
+ const pos = parseFloat(seekInput.value || "0");
275
+ if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({type: "seek", position: pos}));
276
+ });
277
+
278
+ enableAudioBtn.addEventListener("click", async () => {
279
+ try {
280
+ await audio.play();
281
+ audio.pause();
282
+ enableAudioBtn.textContent = "Audio enabled";
283
+ enableAudioBtn.disabled = true;
284
+ } catch (e) {
285
+ enableAudioBtn.textContent = "Tap again to allow audio";
286
+ }
287
+ });
288
+
289
+ // upload logic
290
+ const uploadForm = document.getElementById("upload-form");
291
+ const fileInput = document.getElementById("file-input");
292
+ const uploadProgress = document.getElementById("upload-progress");
293
+ const progressFill = document.getElementById("progress-fill");
294
+ const progressText = document.getElementById("progress-text");
295
+
296
+ uploadForm.addEventListener("submit", (ev) => {
297
+ ev.preventDefault();
298
+ const file = fileInput.files[0];
299
+ if (!file) return;
300
+
301
+ const fd = new FormData();
302
+ fd.append("file", file);
303
+ fd.append("room_code", ROOM_CODE);
304
+ fd.append("session_id", SESSION_ID);
305
+ fd.append("uploader_name", NAME);
306
+
307
+ const xhr = new XMLHttpRequest();
308
+ xhr.open("POST", "/upload", true);
309
+
310
+ xhr.upload.onprogress = e => {
311
+ if (e.lengthComputable) {
312
+ const percent = Math.round((e.loaded / e.total) * 100);
313
+ uploadProgress.style.display = "block";
314
+ progressFill.style.width = percent + "%";
315
+ progressText.textContent = `Uploading ${percent}%`;
316
+ }
317
+ };
318
+
319
+ xhr.onload = () => {
320
+ uploadProgress.style.display = "none";
321
+ fileInput.value = "";
322
+ if (xhr.status !== 200) alert("Upload failed.");
323
+ };
324
+ xhr.send(fd);
325
+ });
326
+
327
+ function status(txt) { statusEl.textContent = txt; }
328
+
329
+ connectWS();
330
  </script>
 
331
  </body>
332
+ </html>