Web-based Winamp controller for CarPC � Go backend, mobile-first UI
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

197 rindas
7.7KB

  1. 'use strict';
  2. const api = (path, opts = {}) =>
  3. fetch(path, opts).then(r => r.json()).catch(() => null);
  4. // ── State ─────────────────────────────────────────────────────────────────────
  5. let currentVolume = 180; // 0–255
  6. let pollTimer = null;
  7. // ── DOM refs ──────────────────────────────────────────────────────────────────
  8. const $ = id => document.getElementById(id);
  9. const statusDot = $('winamp-status');
  10. const stateLabel = $('state-label');
  11. const trackTitle = $('track-title');
  12. const playlistPos = $('playlist-pos');
  13. const progressFill = $('progress-fill');
  14. const timeCurrent = $('time-current');
  15. const timeLength = $('time-length');
  16. const volumeFill = $('volume-fill');
  17. const btnPlay = $('btn-play');
  18. const killistPanel = $('killist-panel');
  19. const killistItems = $('killist-items');
  20. // ── Playback controls ─────────────────────────────────────────────────────────
  21. btnPlay.addEventListener('click', async () => {
  22. const st = await api('/api/status');
  23. if (!st) return;
  24. if (st.state === 'playing') {
  25. await api('/api/pause', { method: 'POST' });
  26. } else {
  27. await api('/api/play', { method: 'POST' });
  28. }
  29. poll();
  30. });
  31. $('btn-stop').addEventListener('click', async () => {
  32. await api('/api/stop', { method: 'POST' }); poll();
  33. });
  34. $('btn-next').addEventListener('click', async () => {
  35. await api('/api/next', { method: 'POST' }); poll();
  36. });
  37. $('btn-prev').addEventListener('click', async () => {
  38. await api('/api/prev', { method: 'POST' }); poll();
  39. });
  40. // ── Seek buttons ──────────────────────────────────────────────────────────────
  41. document.querySelectorAll('.btn-seek').forEach(btn => {
  42. btn.addEventListener('click', async () => {
  43. const delta = parseInt(btn.dataset.delta, 10);
  44. await api(`/api/seek?delta=${delta}`, { method: 'POST' });
  45. poll();
  46. });
  47. });
  48. // ── Progress bar click-to-seek ────────────────────────────────────────────────
  49. $('progress-bar').addEventListener('click', async e => {
  50. const rect = e.currentTarget.getBoundingClientRect();
  51. const frac = (e.clientX - rect.left) / rect.width;
  52. const st = await api('/api/status');
  53. if (!st || !st.length) return;
  54. const target = Math.round(frac * st.length);
  55. const delta = target - st.position;
  56. await api(`/api/seek?delta=${delta}`, { method: 'POST' });
  57. poll();
  58. });
  59. // ── Volume ────────────────────────────────────────────────────────────────────
  60. $('btn-vol-up').addEventListener('click', async () => {
  61. currentVolume = Math.min(255, currentVolume + 13); // ~5%
  62. await api(`/api/volume?level=${currentVolume}`, { method: 'POST' });
  63. updateVolumeFill();
  64. });
  65. $('btn-vol-down').addEventListener('click', async () => {
  66. currentVolume = Math.max(0, currentVolume - 13);
  67. await api(`/api/volume?level=${currentVolume}`, { method: 'POST' });
  68. updateVolumeFill();
  69. });
  70. $('volume-bar').addEventListener('click', async e => {
  71. const rect = e.currentTarget.getBoundingClientRect();
  72. currentVolume = Math.round((e.clientX - rect.left) / rect.width * 255);
  73. await api(`/api/volume?level=${currentVolume}`, { method: 'POST' });
  74. updateVolumeFill();
  75. });
  76. function updateVolumeFill() {
  77. volumeFill.style.width = (currentVolume / 255 * 100) + '%';
  78. }
  79. // ── KillList ──────────────────────────────────────────────────────────────────
  80. $('btn-kill').addEventListener('click', async () => {
  81. const res = await api('/api/killist', { method: 'POST' });
  82. if (res?.added) {
  83. showToast(`🚫 ${res.added}`);
  84. }
  85. });
  86. $('btn-show-killist').addEventListener('click', async () => {
  87. await refreshKillist();
  88. killistPanel.classList.remove('hidden');
  89. });
  90. $('btn-close-killist').addEventListener('click', () => {
  91. killistPanel.classList.add('hidden');
  92. });
  93. async function refreshKillist() {
  94. const list = await api('/api/killist');
  95. if (!list) return;
  96. killistItems.innerHTML = '';
  97. list.forEach(title => {
  98. const li = document.createElement('li');
  99. li.innerHTML = `<span>${escHtml(title)}</span>`;
  100. const btn = document.createElement('button');
  101. btn.textContent = '✕';
  102. btn.onclick = async () => {
  103. await api(`/api/killist?title=${encodeURIComponent(title)}`, { method: 'DELETE' });
  104. await refreshKillist();
  105. };
  106. li.appendChild(btn);
  107. killistItems.appendChild(li);
  108. });
  109. }
  110. // ── Polling ───────────────────────────────────────────────────────────────────
  111. async function poll() {
  112. const st = await api('/api/status');
  113. if (!st) {
  114. statusDot.className = 'err';
  115. statusDot.textContent = '●';
  116. stateLabel.textContent = 'Keine Verbindung';
  117. trackTitle.textContent = '–';
  118. return;
  119. }
  120. if (!st.running) {
  121. statusDot.className = 'err';
  122. stateLabel.textContent = 'Winamp nicht gestartet';
  123. trackTitle.textContent = '–';
  124. return;
  125. }
  126. statusDot.className = 'ok';
  127. const stateMap = { playing: '▶ Spielt', paused: '⏸ Pause', stopped: '⏹ Stop' };
  128. stateLabel.textContent = stateMap[st.state] ?? st.state;
  129. trackTitle.textContent = st.title || '–';
  130. playlistPos.textContent = st.playlist_length
  131. ? `${st.playlist_pos} / ${st.playlist_length}`
  132. : '';
  133. if (st.length > 0) {
  134. progressFill.style.width = (st.position / st.length * 100).toFixed(1) + '%';
  135. timeCurrent.textContent = fmtTime(st.position);
  136. timeLength.textContent = fmtTime(st.length);
  137. } else {
  138. progressFill.style.width = '0%';
  139. }
  140. // Reflect play/pause state on button
  141. btnPlay.textContent = st.state === 'playing' ? '⏸' : '▶';
  142. }
  143. function startPolling(intervalMs = 2000) {
  144. if (pollTimer) clearInterval(pollTimer);
  145. poll();
  146. pollTimer = setInterval(poll, intervalMs);
  147. }
  148. // ── Helpers ───────────────────────────────────────────────────────────────────
  149. function fmtTime(secs) {
  150. const m = Math.floor(secs / 60);
  151. const s = String(Math.floor(secs % 60)).padStart(2, '0');
  152. return `${m}:${s}`;
  153. }
  154. function escHtml(str) {
  155. return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  156. }
  157. let toastTimer;
  158. function showToast(msg) {
  159. let el = document.getElementById('toast');
  160. if (!el) {
  161. el = document.createElement('div');
  162. el.id = 'toast';
  163. el.style.cssText = `
  164. position:fixed;bottom:24px;left:50%;transform:translateX(-50%);
  165. background:#333;color:#fff;padding:10px 20px;border-radius:8px;
  166. font-size:14px;z-index:999;opacity:0;transition:opacity .2s;
  167. `;
  168. document.body.appendChild(el);
  169. }
  170. el.textContent = msg;
  171. el.style.opacity = '1';
  172. clearTimeout(toastTimer);
  173. toastTimer = setTimeout(() => { el.style.opacity = '0'; }, 2500);
  174. }
  175. // ── Boot ──────────────────────────────────────────────────────────────────────
  176. startPolling(2000);