eloigil6 commited on
Commit
e60d445
Β·
1 Parent(s): 2916784

Enhance audio playback controls with loop functionality. Added a loop button in index.html and corresponding styles in style.css. Updated ui.js to manage loop state and toggle behavior, allowing users to switch between looping the current tape and playing through the shelf. Refactored loadCassette function to accommodate new looping logic.

Browse files
Files changed (4) hide show
  1. .claude/launch.json +1 -1
  2. frontend/index.html +24 -0
  3. frontend/style.css +36 -0
  4. frontend/ui.js +37 -10
.claude/launch.json CHANGED
@@ -4,7 +4,7 @@
4
  {
5
  "name": "lofinity",
6
  "runtimeExecutable": "/bin/sh",
7
- "runtimeArgs": ["-c", "GRADIO_SERVER_PORT=$PORT LOFINITY_ENGINE=stub exec .venv/bin/python app.py"],
8
  "port": 7860,
9
  "autoPort": true
10
  }
 
4
  {
5
  "name": "lofinity",
6
  "runtimeExecutable": "/bin/sh",
7
+ "runtimeArgs": ["-c", "GRADIO_SERVER_PORT=$PORT exec .venv/bin/python app.py"],
8
  "port": 7860,
9
  "autoPort": true
10
  }
frontend/index.html CHANGED
@@ -254,6 +254,30 @@
254
  <span class="mini-reel"></span>
255
  <span class="mini-reel"></span>
256
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  <a
258
  id="deck-download"
259
  class="disabled"
 
254
  <span class="mini-reel"></span>
255
  <span class="mini-reel"></span>
256
  </div>
257
+ <button
258
+ id="deck-loop"
259
+ class="on"
260
+ aria-label="loop this tape"
261
+ aria-pressed="true"
262
+ title="looping this tape β€” click to play the whole shelf"
263
+ >
264
+ <svg
265
+ viewBox="0 0 24 24"
266
+ width="20"
267
+ height="20"
268
+ fill="none"
269
+ stroke="currentColor"
270
+ stroke-width="2.4"
271
+ stroke-linecap="round"
272
+ stroke-linejoin="round"
273
+ aria-hidden="true"
274
+ >
275
+ <polyline points="17 1 21 5 17 9" />
276
+ <path d="M3 11V9a4 4 0 0 1 4-4h14" />
277
+ <polyline points="7 23 3 19 7 15" />
278
+ <path d="M21 13v2a4 4 0 0 1-4 4H3" />
279
+ </svg>
280
+ </button>
281
  <a
282
  id="deck-download"
283
  class="disabled"
frontend/style.css CHANGED
@@ -1228,6 +1228,42 @@ body {
1228
  animation: reel-spin 2.4s linear infinite;
1229
  }
1230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1231
  #deck-download {
1232
  flex: none;
1233
  width: 44px;
 
1228
  animation: reel-spin 2.4s linear infinite;
1229
  }
1230
 
1231
+ #deck-loop {
1232
+ flex: none;
1233
+ width: 44px;
1234
+ height: 44px;
1235
+ border-radius: 50%;
1236
+ display: flex;
1237
+ align-items: center;
1238
+ justify-content: center;
1239
+ border: 1px solid rgba(255, 255, 255, 0.5);
1240
+ background: rgba(255, 255, 255, 0.22);
1241
+ color: #fff;
1242
+ cursor: pointer;
1243
+ box-sizing: border-box;
1244
+ transition:
1245
+ background 0.15s ease,
1246
+ color 0.15s ease,
1247
+ box-shadow 0.15s ease,
1248
+ border-color 0.15s ease;
1249
+ }
1250
+
1251
+ #deck-loop:hover {
1252
+ background: rgba(255, 255, 255, 0.4);
1253
+ }
1254
+
1255
+ /* lit up = this tape loops; off = roll through the whole shelf */
1256
+ #deck-loop.on {
1257
+ background: #ffd84a;
1258
+ border-color: #ffd84a;
1259
+ color: #3a2c14;
1260
+ box-shadow: 0 0 10px rgba(255, 216, 74, 0.7);
1261
+ }
1262
+
1263
+ #deck-loop:active {
1264
+ transform: translateY(1px);
1265
+ }
1266
+
1267
  #deck-download {
1268
  flex: none;
1269
  width: 44px;
frontend/ui.js CHANGED
@@ -43,6 +43,7 @@ export function initUI({
43
  const deckFill = $("deck-fill");
44
  const deckTime = $("deck-time");
45
  const deckCassette = $("deck-cassette");
 
46
  const deckDownload = $("deck-download");
47
 
48
  // Two audio elements so tapes can crossfade β€” one element can't overlap
@@ -56,6 +57,9 @@ export function initUI({
56
  let fadeTimer = null;
57
  let playlist = []; // collection songs, when playing as a playlist
58
  let playlistIndex = -1; // active track's index in `playlist`, or -1 = single loop
 
 
 
59
  const CROSSFADE = 2.5; // s β€” overlap as one tape rolls into the next
60
  const PICK_FADE = 0.9; // s β€” quick blend when you pick or skip a tape by hand
61
 
@@ -184,13 +188,14 @@ export function initUI({
184
  syncPlayState();
185
  }
186
 
187
- // A freshly vended tape plays on its own and loops (not part of a playlist).
188
- function loadCassette(song, shell) {
189
- playlist = [];
190
- playlistIndex = -1;
191
- setNowPlaying(song, shell);
192
- startTrack(song.url, { loop: true, fadeSec: PICK_FADE });
193
- syncCards();
 
194
  }
195
 
196
  // Play the collection as a playlist from `index`; tracks crossfade into one
@@ -205,13 +210,14 @@ export function initUI({
205
  songsList[playlistIndex],
206
  SHELL_COLORS[playlistIndex % SHELL_COLORS.length],
207
  );
208
- startTrack(songsList[playlistIndex].url, { loop: false, fadeSec });
209
  layoutCarousel();
210
  }
211
 
212
- // Near the end of a playlist track, roll into the next one.
 
213
  function maybeAdvance() {
214
- if (playlistIndex < 0 || fadeTimer || !active.duration) return;
215
  if (active.currentTime < 1) return; // ignore the very start of a track
216
  if (active.duration - active.currentTime <= CROSSFADE) {
217
  playCollection(playlistIndex + 1, CROSSFADE);
@@ -402,6 +408,27 @@ export function initUI({
402
  if (active.src) togglePlay();
403
  });
404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  const seek = (bar) => (e) => {
406
  if (!active.duration) return;
407
  const rect = bar.getBoundingClientRect();
 
43
  const deckFill = $("deck-fill");
44
  const deckTime = $("deck-time");
45
  const deckCassette = $("deck-cassette");
46
+ const deckLoop = $("deck-loop");
47
  const deckDownload = $("deck-download");
48
 
49
  // Two audio elements so tapes can crossfade β€” one element can't overlap
 
57
  let fadeTimer = null;
58
  let playlist = []; // collection songs, when playing as a playlist
59
  let playlistIndex = -1; // active track's index in `playlist`, or -1 = single loop
60
+ // When on (the default), the current tape loops; toggle it off to roll through
61
+ // the rest of the shelf. Controlled by the loop button on the tape deck.
62
+ let loopOne = true;
63
  const CROSSFADE = 2.5; // s β€” overlap as one tape rolls into the next
64
  const PICK_FADE = 0.9; // s β€” quick blend when you pick or skip a tape by hand
65
 
 
188
  syncPlayState();
189
  }
190
 
191
+ // A freshly vended tape starts playing and then rolls into the rest of the
192
+ // session's tapes (newest β†’ older) instead of looping on itself. With only
193
+ // one tape on the shelf, the playlist re-triggers it, so a lone tape still
194
+ // loops seamlessly.
195
+ function loadCassette(song) {
196
+ songsList = sessionTapes; // the shelf doubles as the now-playing playlist
197
+ const idx = sessionTapes.indexOf(song);
198
+ playCollection(idx >= 0 ? idx : 0, PICK_FADE);
199
  }
200
 
201
  // Play the collection as a playlist from `index`; tracks crossfade into one
 
210
  songsList[playlistIndex],
211
  SHELL_COLORS[playlistIndex % SHELL_COLORS.length],
212
  );
213
+ startTrack(songsList[playlistIndex].url, { loop: loopOne, fadeSec });
214
  layoutCarousel();
215
  }
216
 
217
+ // Near the end of a playlist track, roll into the next one β€” unless the user
218
+ // has the loop toggle on, in which case the tape repeats itself natively.
219
  function maybeAdvance() {
220
+ if (loopOne || playlistIndex < 0 || fadeTimer || !active.duration) return;
221
  if (active.currentTime < 1) return; // ignore the very start of a track
222
  if (active.duration - active.currentTime <= CROSSFADE) {
223
  playCollection(playlistIndex + 1, CROSSFADE);
 
408
  if (active.src) togglePlay();
409
  });
410
 
411
+ // Loop toggle: lit = the current tape repeats; off = play through the shelf.
412
+ function syncLoopBtn() {
413
+ deckLoop.classList.toggle("on", loopOne);
414
+ deckLoop.setAttribute("aria-pressed", String(loopOne));
415
+ deckLoop.title = loopOne
416
+ ? "looping this tape β€” click to play the whole shelf"
417
+ : "playing the whole shelf β€” click to loop this tape";
418
+ }
419
+
420
+ function setLoop(on) {
421
+ loopOne = on;
422
+ // apply to the live element(s) so the change takes effect this play, not
423
+ // just on the next track
424
+ audioA.loop = on;
425
+ audioB.loop = on;
426
+ syncLoopBtn();
427
+ }
428
+
429
+ deckLoop.addEventListener("click", () => setLoop(!loopOne));
430
+ syncLoopBtn();
431
+
432
  const seek = (bar) => (e) => {
433
  if (!active.duration) return;
434
  const rect = bar.getBoundingClientRect();