VK Video Timestamps (Deep Search v3)

Делает таймкоды кликабельными (Deep Shadow DOM support)

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         VK Video Timestamps (Deep Search v3)
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Делает таймкоды кликабельными (Deep Shadow DOM support)
// @author       torch
// @match        https://vk.com/*
// @match        https://vkvideo.ru/*
// @match        https://m.vk.com/*
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // === 1. СТИЛИ ===
    GM_addStyle(`
        .vk-time-link {
            background: rgba(75, 179, 75, 0.15);
            color: #4bb34b !important;
            font-weight: 700;
            cursor: pointer;
            border-bottom: 1px solid rgba(75, 179, 75, 0.5);
            padding: 0 2px;
            border-radius: 3px;
            transition: all 0.2s;
            text-decoration: none !important;
            display: inline-block;
        }
        .vk-time-link:hover {
            background: #4bb34b;
            color: white !important;
        }
        /* Контейнер маркеров */
        #custom-timeline-layer {
            position: absolute;
            bottom: 45px;
            left: 10px;
            right: 10px;
            height: 15px;
            z-index: 99999;
            pointer-events: none;
            display: flex;
        }
        /* Маркеры */
        .timeline-marker {
            position: absolute;
            bottom: 0;
            width: 5px;
            height: 8px;
            background-color: #ffd700;
            border: 1px solid #000;
            transform: translateX(-50%);
            cursor: pointer;
            pointer-events: auto;
            opacity: 0.8;
            transition: all 0.1s;
        }
        .timeline-marker:hover {
            height: 16px;
            width: 8px;
            z-index: 100000;
            background-color: #fff;
            box-shadow: 0 0 5px rgba(0,0,0,0.5);
        }
        /* Тултип */
        .marker-tooltip {
            position: absolute;
            bottom: 22px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0, 0, 0, 0.9);
            color: #fff;
            padding: 5px 8px;
            border-radius: 4px;
            font-size: 12px;
            white-space: nowrap;
            pointer-events: none;
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.2s;
            font-family: sans-serif;
            z-index: 100001;
        }
        .timeline-marker:hover .marker-tooltip {
            opacity: 1;
            visibility: visible;
        }
    `);

    let markersData = [];

    // === 2. ФУНКЦИЯ ГЛУБОКОГО ПОИСКА ВИДЕО (РЕКУРСИЯ) ===
    function findVideoElement(root = document) {
        // 1. Проверяем прямой поиск
        let video = root.querySelector('video');
        if (video) return video;

        // 2. Если это VK плеер (веб-компонент), лезем в его Shadow Root
        const vkPlayer = root.querySelector('vk-video-player');
        if (vkPlayer && vkPlayer.shadowRoot) {
            video = vkPlayer.shadowRoot.querySelector('video');
            if (video) return video;
        }

        // 3. Рекурсивный обход всех элементов с Shadow DOM
        // Это тяжелая операция, поэтому делаем её только если простые методы не сработали
        const allNodes = root.querySelectorAll('*');
        for (let node of allNodes) {
            if (node.shadowRoot) {
                video = findVideoElement(node.shadowRoot);
                if (video) return video;
            }
        }

        return null;
    }

    // === 3. УПРАВЛЕНИЕ ВРЕМЕНЕМ ===
    function parseTime(timeStr) {
        const parts = timeStr.split(':').map(Number);
        if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
        if (parts.length === 2) return parts[0] * 60 + parts[1];
        return 0;
    }

    function seekVideo(seconds) {
        const vid = findVideoElement(); // Ищем видео в момент клика

        if (!vid) {
            console.error('[VK Time] Video element not found inside Shadow DOM.');
            // Не показываем alert каждый раз, чтобы не бесить, если скрипт ошибся
            // Но попробуем визуально мигнуть, что не вышло
            return;
        }

        // Проверка: видео может быть не готово
        if (vid.readyState === 0) {
            console.warn('[VK Time] Video found but not ready.');
            vid.load(); // Пытаемся пнуть его
        }

        vid.currentTime = seconds;
        if (vid.paused) {
            vid.play().then(() => {}).catch(err => console.log('Autoplay blocked:', err));
        }

        // Скролл к видео, если оно уехало вверх
        vid.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }

    // === 4. ПЕРЕХВАТ КЛИКОВ (Capture Phase) ===
    document.addEventListener('click', function(e) {
        const target = e.target.closest('.vk-time-link');
        if (target) {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();

            const seconds = parseFloat(target.dataset.sec);
            console.log('[VK Time] Clicked timestamp:', seconds);
            seekVideo(seconds);
        }
    }, true);

    // === 5. ОБРАБОТКА КОММЕНТАРИЕВ ===
    function processComments() {
        // Расширенный список селекторов для текста комментариев
        const selectors = [
            '.vkitComment__formattedText--dIeRB:not([data-processed])',
            '.wall_reply_text:not([data-processed])',
            '.ReplyItem__text:not([data-processed])',
            '.mv_comment_text:not([data-processed])' // Старый дизайн
        ];

        const commentBlocks = document.querySelectorAll(selectors.join(', '));
        if (commentBlocks.length === 0) return;

        let newMarkersFound = false;
        const regex = /(\d{1,2}:\d{2}(?::\d{2})?)/g;

        commentBlocks.forEach(block => {
            block.setAttribute('data-processed', 'true');
            const html = block.innerHTML;

            if (!regex.test(html)) return;

            // Парсинг текста для маркеров
            const lines = block.innerText.split('\n');
            lines.forEach(line => {
                const match = line.match(/(\d{1,2}:\d{2}(?::\d{2})?)/);
                if (match) {
                    const timeStr = match[1];
                    const seconds = parseTime(timeStr);
                    let label = line.replace(timeStr, '').replace(/^[\s\-\—\.\)]+/, '').trim();
                    if (label.length > 60) label = label.substring(0, 60) + '...';
                    if (!label) label = "Таймкод";

                    markersData.push({ seconds, label, timeStr });
                    newMarkersFound = true;
                }
            });

            // Замена HTML
            block.innerHTML = html.replace(regex, (match) => {
                const seconds = parseTime(match);
                return `<span class="vk-time-link" data-sec="${seconds}">${match}</span>`;
            });
        });

        if (newMarkersFound) updateTimelineMarkers();
    }

    // === 6. ОТРИСОВКА МАРКЕРОВ ===
    function updateTimelineMarkers() {
        const vid = findVideoElement();
        if (!vid || !vid.duration) return;

        // Поиск обертки для вставки слоя
        // Пытаемся найти shadowRoot хост или ближайшего родителя
        let wrapper = null;

        // Специфично для нового дизайна VK (из вашего HTML)
        const customPlayer = document.querySelector('vk-video-player');
        if (customPlayer) {
             // Вставляем внутрь веб-компонента если возможно, или сразу после него
             wrapper = customPlayer.parentElement;
        } else {
             wrapper = vid.parentElement?.parentElement || document.querySelector('.videoplayer_media');
        }

        if (!wrapper) return;

        // Проверяем, есть ли уже слой
        let layer = document.getElementById('custom-timeline-layer');
        if (layer) layer.remove(); // Пересоздаем, чтобы позиция обновилась

        layer = document.createElement('div');
        layer.id = 'custom-timeline-layer';

        // Делаем wrapper relative, чтобы layer позиционировался внутри него
        const style = window.getComputedStyle(wrapper);
        if (style.position === 'static') wrapper.style.position = 'relative';

        wrapper.appendChild(layer);

        // Очищаем дубликаты
        const uniqueMarkers = markersData.filter((v,i,a)=>a.findIndex(t=>(t.seconds===v.seconds))===i);

        uniqueMarkers.forEach(marker => {
            if (marker.seconds > vid.duration) return;

            const leftPercent = (marker.seconds / vid.duration) * 100;
            const pin = document.createElement('div');
            pin.className = 'timeline-marker';
            pin.style.left = `${leftPercent}%`;
            pin.title = marker.label; // Системная подсказка тоже полезна

            // Своя подсказка
            const tooltip = document.createElement('div');
            tooltip.className = 'marker-tooltip';
            tooltip.innerText = `${marker.timeStr} ${marker.label}`;
            pin.appendChild(tooltip);

            pin.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                seekVideo(marker.seconds);
            });

            layer.appendChild(pin);
        });
    }

    // === 7. ЗАПУСК ===
    // Наблюдаем за изменениями DOM
    const observer = new MutationObserver(() => {
        processComments();
        // Пытаемся обновить маркеры (вдруг видео загрузилось)
        const vid = findVideoElement();
        if (vid && !vid.dataset.hasMarkerListener) {
            vid.dataset.hasMarkerListener = 'true';
            vid.addEventListener('loadedmetadata', updateTimelineMarkers);
            vid.addEventListener('durationchange', updateTimelineMarkers);
            // Если уже готово
            if (vid.duration) updateTimelineMarkers();
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // Стартовый пинок
    setTimeout(() => {
        processComments();
        updateTimelineMarkers();
    }, 1000);
    setTimeout(updateTimelineMarkers, 3000); // Повторная попытка через 3 сек

})();