260 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			260 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
<!DOCTYPE html>
 | 
						|
<html lang="en">
 | 
						|
<head>
 | 
						|
  <meta charset="UTF-8">
 | 
						|
  <title>Hipsterfy QR-Scanner</title>
 | 
						|
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
						|
  <style>
 | 
						|
    html, body {
 | 
						|
      margin: 0; padding: 0;
 | 
						|
      font-family: 'Segoe UI', Arial, sans-serif;
 | 
						|
      background: #181818;
 | 
						|
      color: #fff;
 | 
						|
      min-height: 100vh;
 | 
						|
      width: 100vw;
 | 
						|
    }
 | 
						|
    #app {
 | 
						|
      display: flex;
 | 
						|
      flex-direction: column;
 | 
						|
      align-items: center;
 | 
						|
      justify-content: flex-start;
 | 
						|
      min-height: 100vh;
 | 
						|
      padding: 1.5em 0.5em 2em 0.5em;
 | 
						|
      box-sizing: border-box;
 | 
						|
      width: 100vw;
 | 
						|
      max-width: 100vw;
 | 
						|
    }
 | 
						|
    h2 {
 | 
						|
      margin-bottom: 0.5em;
 | 
						|
      font-size: 2em;
 | 
						|
      letter-spacing: 1px;
 | 
						|
      text-align: center;
 | 
						|
    }
 | 
						|
    #video {
 | 
						|
      width: 100%;
 | 
						|
      max-width: 420px;
 | 
						|
      aspect-ratio: 4/3;
 | 
						|
      border-radius: 18px;
 | 
						|
      margin: 1em 0 1.5em 0;
 | 
						|
      background: #222;
 | 
						|
      box-shadow: 0 4px 24px #0008;
 | 
						|
      object-fit: cover;
 | 
						|
    }
 | 
						|
    #result {
 | 
						|
      margin: 0.5em 0 1em 0;
 | 
						|
      word-break: break-all;
 | 
						|
      text-align: center;
 | 
						|
      font-size: 1.08em;
 | 
						|
    }
 | 
						|
    #backBtn {
 | 
						|
      margin: 1.2em 0 1em 0;
 | 
						|
      padding: 0.9em 2em;
 | 
						|
      border: none;
 | 
						|
      border-radius: 10px;
 | 
						|
      background: #1DB954;
 | 
						|
      color: #fff;
 | 
						|
      font-size: 1.15em;
 | 
						|
      font-weight: 600;
 | 
						|
      cursor: pointer;
 | 
						|
      box-shadow: 0 2px 8px #0004;
 | 
						|
      display: none;
 | 
						|
      transition: background 0.2s;
 | 
						|
    }
 | 
						|
    #backBtn:hover {
 | 
						|
      background: #17a74a;
 | 
						|
    }
 | 
						|
    #preview {
 | 
						|
      margin-top: 1em;
 | 
						|
      width: 100%;
 | 
						|
      max-width: 420px;
 | 
						|
      outline: none;
 | 
						|
      display: none;
 | 
						|
    }
 | 
						|
    #embed {
 | 
						|
      width: 100%;
 | 
						|
      max-width: 420px;
 | 
						|
      margin: 1em 0 0 0;
 | 
						|
      display: flex;
 | 
						|
      justify-content: center;
 | 
						|
      align-items: center;
 | 
						|
      min-height: 170px;
 | 
						|
    }
 | 
						|
    #embed iframe {
 | 
						|
      width: 100%;
 | 
						|
      min-width: 260px;
 | 
						|
      max-width: 420px;
 | 
						|
      height: 170px;
 | 
						|
      border: none;
 | 
						|
      border-radius: 16px;
 | 
						|
      background: #222;
 | 
						|
      box-shadow: 0 2px 12px #0005;
 | 
						|
      transition: box-shadow 0.2s;
 | 
						|
    }
 | 
						|
    a {
 | 
						|
      color: #1DB954;
 | 
						|
      word-break: break-all;
 | 
						|
    }
 | 
						|
    @media (max-width: 600px) {
 | 
						|
      #app {
 | 
						|
        padding: 0.5em 0.1em 1em 0.1em;
 | 
						|
      }
 | 
						|
      #video, #embed, #preview {
 | 
						|
        max-width: 98vw;
 | 
						|
      }
 | 
						|
      #embed iframe {
 | 
						|
        min-width: 180px;
 | 
						|
        max-width: 98vw;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  </style>
 | 
						|
</head>
 | 
						|
<body>
 | 
						|
  <div id="app">
 | 
						|
    <h2>Hipsterfy QR-Scanner</h2>
 | 
						|
    <video id="video" autoplay playsinline></video>
 | 
						|
    <div id="result"></div>
 | 
						|
    <button id="backBtn">Back to scanner</button>
 | 
						|
    <audio id="preview" controls autoplay></audio>
 | 
						|
    <div id="embed"></div>
 | 
						|
  </div>
 | 
						|
  <script src="https://unpkg.com/jsqr/dist/jsQR.js"></script>
 | 
						|
  <script>
 | 
						|
    const video = document.getElementById('video');
 | 
						|
    const resultDiv = document.getElementById('result');
 | 
						|
    const embedDiv = document.getElementById('embed');
 | 
						|
    const backBtn = document.getElementById('backBtn');
 | 
						|
    const previewAudio = document.getElementById('preview');
 | 
						|
    let scanning = true;
 | 
						|
    let stream = null;
 | 
						|
    let animationId = null;
 | 
						|
 | 
						|
    function extractPreviewUrl(url) {
 | 
						|
      try {
 | 
						|
        if (url.endsWith('.mp3')) return url;
 | 
						|
        const u = new URL(url);
 | 
						|
        if (u.searchParams.has('preview_url')) {
 | 
						|
          return u.searchParams.get('preview_url');
 | 
						|
        }
 | 
						|
      } catch {}
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    function extractSpotifyTrackId(url) {
 | 
						|
      try {
 | 
						|
        const match = url.match(/(?:embed\/)?track\/([a-zA-Z0-9]+)/);
 | 
						|
        if (match) {
 | 
						|
          return match[1];
 | 
						|
        }
 | 
						|
      } catch {}
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    function isSpotifyUrl(url) {
 | 
						|
      try {
 | 
						|
        const u = new URL(url);
 | 
						|
        return u.hostname.endsWith('spotify.com');
 | 
						|
      } catch {
 | 
						|
        return false;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    function getSpotifyEmbed(url) {
 | 
						|
      const trackId = extractSpotifyTrackId(url);
 | 
						|
      if (trackId) {
 | 
						|
        return `https://open.spotify.com/embed/track/${trackId}`;
 | 
						|
      }
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    async function startScanner() {
 | 
						|
      scanning = true;
 | 
						|
      embedDiv.innerHTML = '';
 | 
						|
      resultDiv.textContent = '';
 | 
						|
      backBtn.style.display = 'none';
 | 
						|
      previewAudio.style.display = 'none';
 | 
						|
      previewAudio.pause();
 | 
						|
      previewAudio.src = '';
 | 
						|
      if (!stream) {
 | 
						|
        try {
 | 
						|
          stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } });
 | 
						|
          video.srcObject = stream;
 | 
						|
        } catch (e) {
 | 
						|
          resultDiv.textContent = "Could not start camera.";
 | 
						|
          return;
 | 
						|
        }
 | 
						|
      }
 | 
						|
      video.play();
 | 
						|
      requestAnimationFrame(tick);
 | 
						|
    }
 | 
						|
 | 
						|
    function stopScanner() {
 | 
						|
      scanning = false;
 | 
						|
      if (animationId) cancelAnimationFrame(animationId);
 | 
						|
      video.pause();
 | 
						|
    }
 | 
						|
 | 
						|
    function tick() {
 | 
						|
      if (!scanning) return;
 | 
						|
      if (video.readyState === video.HAVE_ENOUGH_DATA) {
 | 
						|
        const canvas = document.createElement('canvas');
 | 
						|
        canvas.width = video.videoWidth;
 | 
						|
        canvas.height = video.videoHeight;
 | 
						|
        const ctx = canvas.getContext('2d');
 | 
						|
        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
 | 
						|
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
 | 
						|
        const code = jsQR(imageData.data, imageData.width, imageData.height);
 | 
						|
        if (code && code.data) {
 | 
						|
          handleResult(code.data);
 | 
						|
          return;
 | 
						|
        }
 | 
						|
      }
 | 
						|
      animationId = requestAnimationFrame(tick);
 | 
						|
    }
 | 
						|
 | 
						|
    function handleResult(data) {
 | 
						|
      stopScanner();
 | 
						|
      const previewUrl = extractPreviewUrl(data);
 | 
						|
      if (previewUrl) {
 | 
						|
        resultDiv.innerHTML = `<span>Spotify preview found and playing.</span>`;
 | 
						|
        previewAudio.src = previewUrl;
 | 
						|
        previewAudio.style.display = 'block';
 | 
						|
        previewAudio.play();
 | 
						|
        embedDiv.innerHTML = '';
 | 
						|
      } else if (isSpotifyUrl(data)) {
 | 
						|
        const embedUrl = getSpotifyEmbed(data);
 | 
						|
        if (embedUrl) {
 | 
						|
          resultDiv.innerHTML = `<span>Spotify link found and loaded below.</span>`;
 | 
						|
          embedDiv.innerHTML = `<iframe src="${embedUrl}" allow="encrypted-media"></iframe>`;
 | 
						|
        } else {
 | 
						|
          resultDiv.textContent = "Spotify link found, but not a supported type.";
 | 
						|
          embedDiv.innerHTML = '';
 | 
						|
        }
 | 
						|
        previewAudio.style.display = 'none';
 | 
						|
        previewAudio.pause();
 | 
						|
        previewAudio.src = '';
 | 
						|
      } else {
 | 
						|
        resultDiv.textContent = "No valid Spotify link found.";
 | 
						|
        embedDiv.innerHTML = '';
 | 
						|
        previewAudio.style.display = 'none';
 | 
						|
        previewAudio.pause();
 | 
						|
        previewAudio.src = '';
 | 
						|
      }
 | 
						|
      backBtn.style.display = 'inline-block';
 | 
						|
    }
 | 
						|
 | 
						|
    backBtn.onclick = () => {
 | 
						|
      startScanner();
 | 
						|
    };
 | 
						|
 | 
						|
    window.onload = startScanner;
 | 
						|
    window.onpagehide = () => {
 | 
						|
      if (stream) {
 | 
						|
        stream.getTracks().forEach(track => track.stop());
 | 
						|
        stream = null;
 | 
						|
      }
 | 
						|
      previewAudio.pause();
 | 
						|
      previewAudio.src = '';
 | 
						|
    };
 | 
						|
  </script>
 | 
						|
</body>
 | 
						|
</html> |