()
| 434 | } |
| 435 | |
| 436 | function mixerScreen() { |
| 437 | const c = state.current || { title: "No track selected", sub: "Pick one from your Library", initial: "♪", gradient: DEFAULT_GRADIENT, stemCount: 0 }; |
| 438 | const sourceTag = c.sub || "—"; |
| 439 | const stemTag = c.stemCount ? `${c.stemCount} stems` : ""; |
| 440 | const dur = curDuration(); |
| 441 | const body = state.mixerView === "stems" ? stemsBody() : analysisBody(); |
| 442 | const ready = engineReady && engineTrackId === state.current?.id; |
| 443 | const preparing = !!state.current && !state.current.loading && !state.current.error && !ready; |
| 444 | const canPlay = ready; |
| 445 | const curIdx = state.current ? state.tracks.findIndex((t) => t.id === state.current.id) : -1; |
| 446 | const hasPrev = curIdx > 0; |
| 447 | const hasNext = curIdx >= 0 && curIdx < state.tracks.length - 1; |
| 448 | |
| 449 | return `<div class="screen scrl"> |
| 450 | <div class="pad"> |
| 451 | <div class="mx-head"> |
| 452 | <button class="icon-btn" data-action="tab" data-tab="library">${ICON.chevron}</button> |
| 453 | <span class="now-playing">NOW PLAYING</span> |
| 454 | <button class="icon-btn">${ICON.dots}</button> |
| 455 | </div> |
| 456 | <div class="cover-wrap"> |
| 457 | <div class="cover" style="${artStyle(c)}"><span>${artLabel(c)}</span></div> |
| 458 | <div class="track-title">${esc(c.title)}</div> |
| 459 | <div class="track-sub">${esc(c.sub)}</div> |
| 460 | <div class="tags"><span class="tag">${esc(sourceTag)}</span>${stemTag ? `<span class="tag">${stemTag}</span>` : ""}</div> |
| 461 | </div> |
| 462 | <div class="wave"> |
| 463 | <div class="wave-bars" data-seek>${mainWaveform()}<div class="playhead" style="left:${state.progress * 100}%"></div></div> |
| 464 | <div class="wave-times"><span class="cur">${fmt(state.progress * dur)}</span><span class="dur">${fmt(dur)}</span></div> |
| 465 | </div> |
| 466 | <div class="transport"> |
| 467 | <button class="t-step" data-action="prev" ${hasPrev ? "" : "disabled"}>${ICON.prev}</button> |
| 468 | <button class="t-play" data-action="play" data-playing="${state.playing}" ${canPlay ? "" : "disabled style=opacity:.45"}>${state.playing ? ICON.pause(26, "#1a1206") : ICON.play(28, "#1a1206")}</button> |
| 469 | <button class="t-step" data-action="next" ${hasNext ? "" : "disabled"}>${ICON.next}</button> |
| 470 | </div> |
| 471 | <div class="speed-row"> |
| 472 | <span class="speed-row-label">Speed</span> |
| 473 | <input type="range" class="speed-slider" data-speed min="0" max="2" step="0.25" value="${state.speed}"> |
| 474 | <span class="speed-row-val">${state.speed % 1 === 0 ? state.speed.toFixed(1) : state.speed}x</span> |
| 475 | </div> |
| 476 | ${preparing ? '<div class="mx-prep">Preparing audio…</div>' : ""} |
| 477 | <div class="segmented"> |
| 478 | <button class="${state.mixerView === "stems" ? "on" : ""}" data-action="mixview" data-view="stems">Stems</button> |
| 479 | <button class="${state.mixerView === "analysis" ? "on" : ""}" data-action="mixview" data-view="analysis">Analysis</button> |
| 480 | </div> |
| 481 | </div> |
| 482 | ${body} |
| 483 | </div>`; |
| 484 | } |
| 485 | |
| 486 | function libraryBody() { |
| 487 | if (state.libState === "loading") { |
no test coverage detected