Video Link with Timestamp and QR Code

Copies a link to the website with the video's timestamp.

// ==UserScript==
// @name         Video Link with Timestamp and QR Code
// @namespace    http://tampermonkey.net/
// @version      1.8
// @description  Copies a link to the website with the video's timestamp.
// @license      MIT
// @author       Ko16aska
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @require      https://cdnjs.cloudflare.com/ajax/libs/qrious/4.0.2/qrious.min.js
// ==/UserScript==

(function() {
    'use strict';

    if (window.top !== window.self) return;

    GM_addStyle(`
        #qr-code-modal {
            position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
            background-color: #f4f4f5; /* Very light gray background */
            padding: 20px; border-radius: 16px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); z-index: 2147483647;
            display: flex; flex-direction: column; align-items: center; gap: 15px;
            max-width: 90vw; box-sizing: border-box;
        }
        #qr-code-modal button.close-button {
            position: absolute; top: 10px; right: 10px; background: none; border: none;
            font-size: 1.5em; cursor: pointer; color: #aaa; transition: color 0.2s ease;
            padding: 5px; line-height: 1;
        }
        #qr-code-modal button.close-button:hover { color: #333; }

        /* --- NEW FRAME STYLE --- */
        #qr-visual-container, .text-info-container {
            background-color: #ffffff;
            border: 2px solid #7c3aed; /* Rich purple */
            border-radius: 10px;
            box-sizing: border-box;
            /* The 3D shadow */
            box-shadow: 3px 3px 0px #c4b5fd, 5px 5px 10px rgba(0, 0, 0, 0.15);
        }

        /* --- PADDING FIX --- */
        #qr-visual-container {
            padding: 10px; /* Inner padding so the code doesn't stick to the frame */
            /* width: auto; removed width: 100% */
        }

        #qr-code-modal canvas, #qr-code-modal img.qr-fallback {
            display: block;
            width: 200px;
            height: 200px;
            margin: 0 auto;
            border-radius: 6px; /* No longer needs its own border, as it has a container */
        }

        .text-info-container {
            width: 100%; /* Let the text block stretch */
            display: flex;
            flex-direction: column;
            gap: 8px;
            padding: 12px;
        }

        #qr-code-modal p.link-label {
            margin: 0; color: #333;
            font-size: 1.1em; font-weight: 600;
            text-align: center;
        }
        #qr-code-modal a.generated-link {
            word-break: break-all;
            color: #7c3aed; /* Themed purple link */
            font-weight: 600; /* Make it bolder */
            text-decoration: none;
            font-size: 1.1em; text-align: center;
            max-width: 380px; display: block; line-height: 1.4;
        }
        #qr-code-modal a.generated-link:hover { text-decoration: underline; }
        #qr-code-modal .info-message {
            color: #666; font-style: italic; font-size: 1em;
            text-align: center; max-width: 95%; margin-top: 5px;
            border-top: 1px dashed #ccc; padding-top: 8px;
        }
    `);

    function cropCanvasWhitespace(sourceCanvas) {
        const ctx = sourceCanvas.getContext('2d'), { width, height } = sourceCanvas;
        const imageData = ctx.getImageData(0, 0, width, height), data = imageData.data;
        let minX = width, minY = height, maxX = -1, maxY = -1;
        for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { const i = (y * width + x) * 4; if (data[i + 3] > 0 && (data[i] < 255 || data[i + 1] < 255 || data[i + 2] < 255)) { if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; } } }
        if (maxX === -1) return sourceCanvas;
        const croppedWidth = maxX - minX + 1, croppedHeight = maxY - minY + 1, sideLength = Math.max(croppedWidth, croppedHeight);
        const croppedCanvas = document.createElement('canvas');
        croppedCanvas.width = sideLength; croppedCanvas.height = sideLength;
        const croppedCtx = croppedCanvas.getContext('2d');
        croppedCtx.drawImage(sourceCanvas, minX, minY, croppedWidth, croppedHeight, (sideLength - croppedWidth) / 2, (sideLength - croppedHeight) / 2, croppedWidth, croppedHeight);
        return croppedCanvas;
    }

    function displayQRCode(url, infoMessage) {
        const existingModal = document.getElementById('qr-code-modal');
        if (existingModal) existingModal.remove();
        const modal = document.createElement('div');
        modal.id = 'qr-code-modal';
        const closeButton = document.createElement('button');
        closeButton.className = 'close-button';
        closeButton.textContent = '×';
        closeButton.onclick = () => modal.remove();
        modal.appendChild(closeButton);
        const qrContainer = document.createElement('div');
        qrContainer.id = 'qr-visual-container';
        modal.appendChild(qrContainer);
        const textContainer = document.createElement('div');
        textContainer.className = 'text-info-container';
        const linkLabel = document.createElement('p');
        linkLabel.className = 'link-label';
        linkLabel.textContent = 'Link copied to clipboard:'; // TRANSLATED
        textContainer.appendChild(linkLabel);
        const linkElement = document.createElement('a');
        linkElement.className = 'generated-link';
        linkElement.href = url;
        linkElement.textContent = url;
        linkElement.target = '_blank';
        textContainer.appendChild(linkElement);
        if (infoMessage) {
            const msgElement = document.createElement('p');
            msgElement.className = 'info-message';
            msgElement.textContent = infoMessage;
            textContainer.appendChild(msgElement);
        }
        modal.appendChild(textContainer);
        document.body.appendChild(modal);
        try {
            const tempCanvas = document.createElement('canvas');
            new QRious({ element: tempCanvas, value: url, size: 300 });
            const perfectCanvas = cropCanvasWhitespace(tempCanvas);
            qrContainer.appendChild(perfectCanvas);
        } catch (e) {
            console.error('QRious error, falling back to Google Charts API:', e); // TRANSLATED
            const qrImg = document.createElement('img');
            qrImg.className = 'qr-fallback';
            qrImg.src = `https://chart.googleapis.com/chart?cht=qr&chs=250x250&chl=${encodeURIComponent(url)}`;
            qrImg.alt = 'QR Code';
            qrContainer.appendChild(qrImg);
        }
    }

    function getVideoInfo() {
        let videoElement = null, currentTime = 0, urlToShare = window.location.href, infoMessage = '', videoType = 'page';
        const videosOnPage = Array.from(document.querySelectorAll('video')), playingVideo = videosOnPage.find(v => !v.paused && v.currentTime > 0);
        videoElement = playingVideo || videosOnPage[0];
        if (videoElement && videoElement.duration > 0 && !isNaN(videoElement.currentTime)) {
             currentTime = videoElement.currentTime;
             if (window.location.hostname.includes('youtube.com')) {
                const urlObj = new URL(window.location.href);
                urlObj.searchParams.set('t', Math.floor(currentTime) + 's');
                urlToShare = urlObj.toString();
                videoType = 'youtube';
                infoMessage = `YouTube video with timestamp ${formatTime(currentTime)}`; // TRANSLATED
             } else {
                urlToShare = window.location.href.split('#')[0] + '#t=' + Math.floor(currentTime);
                videoType = 'html5';
                infoMessage = `HTML5 video with timestamp ${formatTime(currentTime)}`; // TRANSLATED
             }
        } else {
            const iframes = document.querySelectorAll('iframe');
            let foundIframeVideo = false;
            for (const iframe of iframes) {
                try {
                    const iframeSrc = iframe.src;
                    if (iframeSrc && (iframeSrc.includes('youtube.com/embed/') || iframeSrc.includes('vimeo.com/video/'))) {
                        urlToShare = iframeSrc;
                        videoType = 'iframe';
                        infoMessage = `Found a video in an iframe. Copied the link to the video source.`; // TRANSLATED
                        foundIframeVideo = true;
                        break;
                    }
                } catch (e) { /* Ignore */ }
            }
            if (!foundIframeVideo) {
                infoMessage = 'No video found. Copying the link to the current page.'; // TRANSLATED
                videoType = 'page';
            }
        }
        return { url: urlToShare, type: videoType, message: infoMessage };
    }

    function formatTime(totalSeconds) {
        if (isNaN(totalSeconds) || totalSeconds < 0) return '00:00';
        const h = Math.floor(totalSeconds / 3600), m = Math.floor((totalSeconds % 3600) / 60), s = Math.floor(totalSeconds % 60);
        return (h > 0 ? [h, m, s] : [m, s]).map(v => String(v).padStart(2, '0')).join(':');
    }

    function handleAction() {
        const videoInfo = getVideoInfo();
        GM_setClipboard(videoInfo.url);
        displayQRCode(videoInfo.url, videoInfo.message);
    }

    GM_registerMenuCommand('Copy QR/Link for video', handleAction, 'q'); // TRANSLATED

})();