Загрузка данных


<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Music Stream Player</title>
  <style>
    :root {
      --bg: #ffffff;
      --panel: #ffffff;
      --panel-2: #f3f4f6;
      --text: #111827;
      --muted: #6b7280;
      --accent: #2563eb;
      --accent-2: #10b981;
      --danger: #dc2626;
      --shadow: 0 10px 30px rgba(0,0,0,0.08);
      --radius: 18px;
    }

    * { box-sizing: border-box; }
    html, body {
      margin: 0;
      font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
      background: #ffffff;
      color: var(--text);
      display: grid;
      place-items: center;
      padding: 20px;
    }

    .app {
      width: min(1100px, 100%);
      display: grid;
      grid-template-columns: 1.15fr 0.85fr;
      gap: 20px;
    }

    .card {
      background: var(--panel);
      border: 1px solid #e5e7eb;
      border-radius: var(--radius);
      box-shadow: var(--shadow);
      overflow: hidden;
    }

    .hero {
      padding: 28px;
      min-height: 620px;
      display: flex;
      flex-direction: column;
      gap: 18px;
    }

    .sidebar {
      padding: 22px;
      min-height: 620px;
      display: flex;
      flex-direction: column;
      gap: 18px;
    }

    h1 {
      margin: 0;
      font-size: clamp(30px, 4vw, 48px);
      line-height: 1.05;
      letter-spacing: -0.04em;
    }

    .sub {
      margin: 0;
      color: var(--muted);
      font-size: 15px;
      line-height: 1.55;
    }

    .dropzone {
      border: 2px dashed #d1d5db;
      border-radius: 16px;
      padding: 18px;
      min-height: 150px;
      display: grid;
      place-items: center;
      text-align: center;
      transition: 0.2s ease;
      background: #f9fafb;
    }

    .dropzone.dragover {
      border-color: var(--accent-2);
      background: rgba(41,211,178,0.10);
      transform: scale(1.01);
    }

    .dropzone strong { display: block; font-size: 18px; margin-bottom: 8px; }
    .dropzone span { color: var(--muted); font-size: 14px; }

    .row {
      display: grid;
      grid-template-columns: 1fr auto;
      gap: 12px;
    }

    .input, .button, .select {
      border: 0;
      outline: none;
      border-radius: 14px;
      font-size: 14px;
    }

    .input, .select {
      width: 100%;
      background: rgba(255,255,255,0.08);
      color: var(--text);
      padding: 14px 16px;
      border: 1px solid rgba(255,255,255,0.10);
    }

    .input::placeholder { color: rgba(244,247,251,0.45); }

    .button {
      cursor: pointer;
      padding: 14px 18px;
      background: var(--accent);
      color: white;
      font-weight: 600;
      border-radius: 14px;
      transition: background 0.15s ease, transform 0.1s ease;
      white-space: nowrap;
    }

    .button:hover { transform: translateY(-1px); filter: brightness(1.05); }
    .button:active { transform: translateY(0px) scale(0.99); }

    .button.secondary {
      background: #f3f4f6;
      color: var(--text);
      border: 1px solid #e5e7eb;
    }

    .button.danger {
      background: #fee2e2;
      color: #991b1b;
      border: 1px solid #fecaca;
    }

    .player {
      margin-top: auto;
      padding: 20px;
      border-radius: 20px;
      background: rgba(255,255,255,0.06);
      border: 1px solid rgba(255,255,255,0.10);
    }

    .now {
      display: flex;
      align-items: center;
      gap: 14px;
      margin-bottom: 16px;
    }

    .cover {
      width: 64px;
      height: 64px;
      border-radius: 16px;
      background: linear-gradient(135deg, rgba(124,92,255,0.95), rgba(41,211,178,0.9));
      display: grid;
      place-items: center;
      font-size: 24px;
      flex: 0 0 auto;
      box-shadow: 0 12px 30px rgba(124,92,255,0.25);
    }

    .track-meta {
      min-width: 0;
      flex: 1;
    }

    .track-title {
      margin: 0;
      font-size: 17px;
      font-weight: 800;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .track-subtitle {
      margin: 4px 0 0;
      color: var(--muted);
      font-size: 13px;
    }

    .progress-wrap {
      margin: 12px 0 10px;
    }

    .bar {
      width: 100%;
      height: 10px;
      appearance: none;
      background: rgba(255,255,255,0.10);
      border-radius: 999px;
      overflow: hidden;
      outline: none;
    }

    .bar::-webkit-slider-thumb {
      appearance: none;
      width: 16px;
      height: 16px;
      border-radius: 50%;
      background: white;
      border: 3px solid var(--accent);
      box-shadow: 0 0 0 6px rgba(124,92,255,0.15);
      cursor: pointer;
    }

    .time-row {
      display: flex;
      justify-content: space-between;
      color: var(--muted);
      font-size: 12px;
      margin-top: 8px;
    }

    .controls {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
      margin-top: 14px;
    }

    .controls .button {
      flex: 1 1 120px;
    }

    .list-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      gap: 10px;
    }

    .list-header h2 {
      margin: 0;
      font-size: 20px;
      letter-spacing: -0.02em;
    }

    .count {
      color: var(--muted);
      font-size: 13px;
    }

    .playlist {
      display: flex;
      flex-direction: column;
      gap: 10px;
      overflow: auto;
      padding-right: 4px;
      max-height: 470px;
    }

    .item {
      display: grid;
      grid-template-columns: 44px 1fr auto;
      gap: 12px;
      align-items: center;
      background: rgba(255,255,255,0.06);
      border: 1px solid rgba(255,255,255,0.08);
      border-radius: 16px;
      padding: 12px;
      cursor: pointer;
      transition: 0.18s ease;
    }

    .item:hover { transform: translateY(-1px); background: rgba(255,255,255,0.09); }
    .item.active {
      border-color: rgba(41,211,178,0.45);
      background: rgba(41,211,178,0.12);
    }

    .mini-cover {
      width: 44px;
      height: 44px;
      border-radius: 14px;
      background: linear-gradient(135deg, rgba(124,92,255,0.85), rgba(41,211,178,0.78));
      display: grid;
      place-items: center;
      font-size: 18px;
      flex: 0 0 auto;
    }

    .name {
      margin: 0;
      font-size: 14px;
      font-weight: 700;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .meta {
      margin-top: 4px;
      color: var(--muted);
      font-size: 12px;
    }

    .small-btn {
      border: 0;
      background: rgba(255,255,255,0.10);
      color: var(--text);
      width: 38px;
      height: 38px;
      border-radius: 12px;
      cursor: pointer;
      display: grid;
      place-items: center;
    }

    .empty {
      color: var(--muted);
      text-align: center;
      padding: 28px 16px;
      border: 1px dashed rgba(255,255,255,0.15);
      border-radius: 18px;
      background: rgba(255,255,255,0.04);
    }

    .hint {
      font-size: 12px;
      color: var(--muted);
      line-height: 1.5;
    }

    audio { display: none; }

    @media (max-width: 900px) {
      .app { grid-template-columns: 1fr; }
      .hero, .sidebar { min-height: auto; }
      .playlist { max-height: 320px; }
    }
  </style>
</head>
<body>
  <div class="app">
    <section class="card hero">
      <div>
        <h1>Музыкальный плеер</h1>
        <p class="sub">Загружай файлы с компьютера или вставляй ссылку на аудио. Это не настоящий стриминг-сервис, а удобный HTML5-плеер для проигрывания треков прямо в браузере.</p>
      </div>

      <div class="dropzone" id="dropzone">
        <div>
          <strong>Перетащи аудиофайл сюда</strong>
          <span>или выбери файл кнопкой ниже</span>
        </div>
      </div>

      <div class="row">
        <input id="fileInput" type="file" accept="audio/*" class="input" />
        <button class="button secondary" id="clearBtn" title="Очистить список">Очистить</button>
      </div>

      <div class="row">
        <input id="urlInput" class="input" type="url" placeholder="Вставь ссылку на аудио или поток (.mp3, .wav, .ogg, .m3u8 и т.п.)" />
        <button class="button" id="addUrlBtn">Добавить</button>
      </div>

      <div class="player">
        <div class="now">
          <div class="cover">♫</div>
          <div class="track-meta">
            <p class="track-title" id="currentTitle">Ничего не выбрано</p>
            <p class="track-subtitle" id="currentSource">Добавь трек, чтобы начать</p>
          </div>
        </div>

        <div class="progress-wrap">
          <input id="seekBar" class="bar" type="range" min="0" max="100" value="0" disabled />
          <div class="time-row">
            <span id="currentTime">00:00</span>
            <span id="duration">00:00</span>
          </div>
        </div>

        <div class="controls">
          <button class="button secondary" id="prevBtn">⏮ Назад</button>
          <button class="button" id="playBtn">▶️ Play</button>
          <button class="button secondary" id="nextBtn">⏭ Далее</button>
          <button class="button danger" id="removeBtn">Удалить текущий</button>
        </div>

        <p class="hint">Подсказка: кликни по треку в списке, чтобы выбрать его. Клавиши Space — play/pause, ←/→ — перемотка на 5 секунд.</p>
      </div>

      <audio id="audio"></audio>
    </section>

    <aside class="card sidebar">
      <div class="list-header">
        <h2>Плейлист</h2>
        <div class="count" id="trackCount">0 треков</div>
      </div>

      <div id="playlist" class="playlist">
        <div class="empty">Список пуст. Добавь первый файл или ссылку справа/сверху.</div>
      </div>
    </aside>
  </div>

  <script>
    const audio = document.getElementById('audio');
    const fileInput = document.getElementById('fileInput');
    const urlInput = document.getElementById('urlInput');
    const addUrlBtn = document.getElementById('addUrlBtn');
    const clearBtn = document.getElementById('clearBtn');
    const removeBtn = document.getElementById('removeBtn');
    const prevBtn = document.getElementById('prevBtn');
    const playBtn = document.getElementById('playBtn');
    const nextBtn = document.getElementById('nextBtn');
    const seekBar = document.getElementById('seekBar');
    const currentTimeEl = document.getElementById('currentTime');
    const durationEl = document.getElementById('duration');
    const currentTitle = document.getElementById('currentTitle');
    const currentSource = document.getElementById('currentSource');
    const playlistEl = document.getElementById('playlist');
    const trackCount = document.getElementById('trackCount');
    const dropzone = document.getElementById('dropzone');

    let tracks = [];
    let currentIndex = -1;
    let objectUrls = new Set();

    function formatTime(seconds) {
      if (!isFinite(seconds)) return '00:00';
      const m = Math.floor(seconds / 60);
      const s = Math.floor(seconds % 60);
      return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
    }

    function updateTrackCount() {
      trackCount.textContent = `${tracks.length} трек${tracks.length === 1 ? '' : tracks.length < 5 ? 'а' : 'ов'}`;
    }

    function renderPlaylist() {
      playlistEl.innerHTML = '';

      if (tracks.length === 0) {
        playlistEl.innerHTML = '<div class="empty">Список пуст. Добавь первый файл или ссылку сверху.</div>';
        updateTrackCount();
        return;
      }

      tracks.forEach((track, index) => {
        const item = document.createElement('div');
        item.className = 'item' + (index === currentIndex ? ' active' : '');
        item.innerHTML = `
          <div class="mini-cover">♫</div>
          <div>
            <p class="name">${escapeHtml(track.name)}</p>
            <div class="meta">${escapeHtml(track.type)} • ${escapeHtml(track.sourceLabel)}</div>
          </div>
          <button class="small-btn" title="Удалить">✕</button>
        `;

        item.addEventListener('click', (e) => {
          if (e.target.closest('button')) {
            removeTrack(index);
            return;
          }
          playTrack(index);
        });

        playlistEl.appendChild(item);
      });

      updateTrackCount();
    }

    function escapeHtml(str) {
      return String(str)
        .replaceAll('&', '&amp;')
        .replaceAll('<', '&lt;')
        .replaceAll('>', '&gt;')
        .replaceAll('"', '&quot;')
        .replaceAll("'", '&#39;');
    }

    function addTrack(track) {
      tracks.push(track);
      if (currentIndex === -1) {
        currentIndex = 0;
        loadCurrentTrack(false);
      }
      renderPlaylist();
    }

    function loadCurrentTrack(autoplay = true) {
      if (currentIndex < 0 || currentIndex >= tracks.length) {
        audio.removeAttribute('src');
        currentTitle.textContent = 'Ничего не выбрано';
        currentSource.textContent = 'Добавь трек, чтобы начать';
        playBtn.textContent = '▶️ Play';
        seekBar.value = 0;
        seekBar.disabled = true;
        currentTimeEl.textContent = '00:00';
        durationEl.textContent = '00:00';
        renderPlaylist();
        return;
      }

      const track = tracks[currentIndex];
      audio.src = track.url;
      audio.load();
      currentTitle.textContent = track.name;
      currentSource.textContent = track.sourceLabel;
      seekBar.disabled = false;
      renderPlaylist();

      if (autoplay) {
        audio.play().catch(() => {
          playBtn.textContent = '▶️ Play';
        });
      }
      playBtn.textContent = '⏸ Pause';
    }

    function playTrack(index) {
      currentIndex = index;
      loadCurrentTrack(true);
    }

    function nextTrack() {
      if (!tracks.length) return;
      currentIndex = (currentIndex + 1) % tracks.length;
      loadCurrentTrack(true);
    }

    function prevTrack() {
      if (!tracks.length) return;
      currentIndex = (currentIndex - 1 + tracks.length) % tracks.length;
      loadCurrentTrack(true);
    }

    function removeTrack(index) {
      const track = tracks[index];
      if (track?.revoke && track.url.startsWith('blob:')) {
        URL.revokeObjectURL(track.url);
        objectUrls.delete(track.url);
      }

      tracks.splice(index, 1);

      if (tracks.length === 0) {
        currentIndex = -1;
        audio.pause();
        loadCurrentTrack(false);
        return;
      }

      if (index < currentIndex) currentIndex--;
      else if (index === currentIndex) {
        currentIndex = Math.min(currentIndex, tracks.length - 1);
        loadCurrentTrack(true);
      }

      renderPlaylist();
    }

    function clearAll() {
      objectUrls.forEach((u) => URL.revokeObjectURL(u));
      objectUrls.clear();
      tracks = [];
      currentIndex = -1;
      audio.pause();
      loadCurrentTrack(false);
    }

    fileInput.addEventListener('change', (e) => {
      const files = [...e.target.files || []];
      for (const file of files) {
        const url = URL.createObjectURL(file);
        objectUrls.add(url);
        addTrack({
          name: file.name,
          url,
          type: file.type || 'audio',
          sourceLabel: 'Локальный файл',
          revoke: true,
        });
      }
      fileInput.value = '';
    });

    addUrlBtn.addEventListener('click', () => {
      const url = urlInput.value.trim();
      if (!url) return;

      const inferredName = url.split('/').pop()?.split('?')[0] || 'Audio stream';
      addTrack({
        name: inferredName,
        url,
        type: 'URL/stream',
        sourceLabel: 'Ссылка',
        revoke: false,
      });
      urlInput.value = '';
    });

    clearBtn.addEventListener('click', clearAll);
    removeBtn.addEventListener('click', () => {
      if (currentIndex >= 0) removeTrack(currentIndex);
    });
    prevBtn.addEventListener('click', prevTrack);
    nextBtn.addEventListener('click', nextTrack);

    playBtn.addEventListener('click', () => {
      if (!tracks.length) return;
      if (audio.paused) {
        audio.play().catch(() => {});
      } else {
        audio.pause();
      }
    });

    audio.addEventListener('play', () => {
      playBtn.textContent = '⏸ Pause';
    });

    audio.addEventListener('pause', () => {
      playBtn.textContent = '▶️ Play';
    });

    audio.addEventListener('loadedmetadata', () => {
      durationEl.textContent = formatTime(audio.duration);
      seekBar.max = String(Math.floor(audio.duration || 0));
      seekBar.value = '0';
    });

    audio.addEventListener('timeupdate', () => {
      if (!isNaN(audio.currentTime)) {
        seekBar.value = String(Math.floor(audio.currentTime));
        currentTimeEl.textContent = formatTime(audio.currentTime);
      }
    });

    audio.addEventListener('ended', nextTrack);

    seekBar.addEventListener('input', () => {
      audio.currentTime = Number(seekBar.value);
    });

    dropzone.addEventListener('dragover', (e) => {
      e.preventDefault();
      dropzone.classList.add('dragover');
    });

    dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'));
    dropzone.addEventListener('drop', (e) => {
      e.preventDefault();
      dropzone.classList.remove('dragover');
      const files = [...e.dataTransfer.files || []].filter(f => f.type.startsWith('audio/'));
      for (const file of files) {
        const url = URL.createObjectURL(file);
        objectUrls.add(url);
        addTrack({
          name: file.name,
          url,
          type: file.type || 'audio',
          sourceLabel: 'Drag & Drop',
          revoke: true,
        });
      }
    });

    document.addEventListener('keydown', (e) => {
      if (e.target.matches('input, textarea')) return;
      if (e.code === 'Space') {
        e.preventDefault();
        if (!tracks.length) return;
        if (audio.paused) audio.play().catch(() => {}); else audio.pause();
      }
      if (e.key === 'ArrowRight') {
        audio.currentTime = Math.min(audio.duration || Infinity, audio.currentTime + 5);
      }
      if (e.key === 'ArrowLeft') {
        audio.currentTime = Math.max(0, audio.currentTime - 5);
      }
    });

    renderPlaylist();
  </script>
</body>
</html>