YouTube Timestamp Manager

Full-featured timestamp manager with video tracking, inline editing, seeking, and sharp UI.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         YouTube Timestamp Manager
// @namespace    http://tampermonkey.net/
// @version      2.6
// @description  Full-featured timestamp manager with video tracking, inline editing, seeking, and sharp UI.
// @author       Tanuki
// @match        *://www.youtube.com/*
// @icon         https://www.youtube.com/s/desktop/8fa11322/img/favicon_144x144.png
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    const DB_NAME = 'TanStampsDB';
    const DB_VERSION = 1;
    const STORE_NAME = 'timestamps';
    const NOTE_PLACEHOLDER = '[No note]'; // Placeholder text for empty notes

    let currentVideoId = null;
    let manager = null;
    let noteInput = null;
    let uiContainer = null;
    let progressMarkers = [];
    let currentTooltip = null;
    let updateMarkers = null;
    let timeUpdateInterval = null;

    // --- CSS Injection ---
    const style = document.createElement('style');
    style.textContent = `
        .tanuki-ui-container {
          display: inline-flex;
          align-items: center;
          margin-left: 8px;
        }
        .tanuki-timestamp {
          cursor: pointer;
          color: #fff;
          font-family: Arial, sans-serif;
          font-size: 14px;
          line-height: 24px;
          margin: 0 4px;
          user-select: none;
        }
        .tanuki-button {
          background: #333;
          color: #fff;
          border: 1px solid #555;
          padding: 4px 8px;
          border-radius: 0;
          cursor: pointer;
          font-size: 12px;
          transition: background 0.2s, border-color 0.2s;
          margin: 0 2px;
          user-select: none;
        }
        .tanuki-button:hover {
          background: #444;
          border-color: #777;
        }
        .tanuki-button:active {
          background: #222;
        }
        .tanuki-progress-marker {
          position: absolute;
          height: 100%;
          width: 3px;
          background: #3ea6ff;
          box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
          z-index: 999;
          pointer-events: auto;
          transform: translateX(-1.5px);
          cursor: pointer;
          border-radius: 0;
        }
        .tanuki-tooltip {
          position: fixed;
          background: rgba(0, 0, 0, 0.9);
          color: #fff;
          padding: 8px 12px;
          border-radius: 0;
          font-size: 12px;
          white-space: nowrap;
          z-index: 10000;
          pointer-events: none;
          transform: translate(-50%, -100%);
          margin-top: -4px;
        }
        .tanuki-note-input {
          position: fixed;
          background: rgba(30, 30, 30, 0.95);
          color: #fff;
          padding: 20px;
          border-radius: 0;
          z-index: 10000;
          display: flex;
          flex-direction: column;
          align-items: center;
          box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
          border: 1px solid #555;
        }
        .tanuki-note-input input {
          padding: 8px 10px;
          margin-bottom: 12px;
          border: 1px solid #666;
          border-radius: 0;
          width: 220px;
          background: #222;
          color: #fff;
          font-size: 14px;
        }
        .tanuki-note-input input:focus {
          outline: none;
          border-color: #3ea6ff;
        }
        .tanuki-note-input button {
          background: #007bff;
          border: none;
          border-radius: 0;
          padding: 8px 16px;
          cursor: pointer;
          color: #fff;
          font-weight: bold;
          transition: background 0.2s;
        }
        .tanuki-note-input button:hover {
          background: #0056b3;
        }
        .tanuki-manager {
          position: fixed;
          background: rgba(25, 25, 25, 0.97);
          color: #eee;
          padding: 15px 20px 20px 20px;
          border-radius: 0;
          z-index: 99999;
          width: 540px;
          height: 380px;
          overflow: hidden;
          box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5);
          border: 1px solid #444;
          display: flex;
          flex-direction: column;
        }
        .tanuki-manager-header {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 15px;
          padding-bottom: 10px;
          border-bottom: 1px solid #555;
          flex-shrink: 0;
        }
        .tanuki-manager h3 {
          margin: 0;
          padding: 0;
          border-bottom: none;
          color: #fff;
          font-size: 18px;
          text-align: left;
          flex-grow: 1;
          line-height: 1.2;
        }
        .tanuki-manager button.close-btn {
          background: #666;
          border: 1px solid #888;
          color: #fff;
          font-size: 18px;
          font-weight: bold;
          line-height: 1;
          padding: 3px 7px;
          border-radius: 0;
          cursor: pointer;
          transition: background 0.2s, transform 0.2s;
          position: static;
          margin-left: 10px;
          flex-shrink: 0;
        }
        .tanuki-manager button.close-btn:hover {
          background: #777;
          transform: scale(1.1);
        }
        .tanuki-manager-list {
          display: flex;
          flex-direction: column;
          gap: 8px;
          flex-grow: 1;
          overflow-y: auto;
          margin-bottom: 15px;
        }
        .tanuki-timestamp-item {
          display: flex;
          justify-content: space-between;
          align-items: center;
          background: #3a3a3a;
          padding: 10px 12px;
          border-radius: 0;
          transition: background 0.15s;
          font-size: 15px;
        }
        .tanuki-timestamp-item:hover {
          background: #4a4a4a;
        }
        .tanuki-timestamp-item span:first-child {
          margin-right: 12px;
          cursor: pointer;
          min-width: 70px;
          text-align: right;
          font-weight: bold;
          color: #3ea6ff;
          user-select: none;
        }
        .tanuki-timestamp-item span:nth-child(2) {
          flex: 1;
          margin-right: 12px;
          cursor: pointer;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
          color: #ddd;
          user-select: none;
        }
        .tanuki-timestamp-item .tanuki-note-placeholder {
          color: #999;
          font-style: italic;
        }
        .tanuki-timestamp-item input {
          padding: 6px 8px;
          border: 1px solid #666;
          border-radius: 0;
          background: #222;
          color: #fff;
          font-size: 15px;
          font-family: inherit;
          box-sizing: border-box;
        }
        .tanuki-timestamp-item input:focus {
          outline: none;
          border-color: #3ea6ff;
        }
        .tanuki-timestamp-item input.time-input {
          width: 80px;
          text-align: right;
          font-weight: bold;
          color: #3ea6ff;
        }
        .tanuki-timestamp-item input.note-input {
          flex: 1;
          margin-right: 12px;
        }
        .tanuki-timestamp-item button {
          background: #555;
          border: 1px solid #777;
          padding: 4px 8px;
          border-radius: 0;
          cursor: pointer;
          color: #fff;
          margin-left: 6px;
          font-size: 16px;
          line-height: 1;
          transition: background 0.2s, border-color 0.2s;
        }
        .tanuki-timestamp-item button:hover {
          background: #666;
          border-color: #888;
        }
        .tanuki-timestamp-item button.delete-btn {
          background: #d9534f;
          border-color: #d43f3a;
        }
        .tanuki-timestamp-item button.delete-btn:hover {
          background: #c9302c;
          border-color: #ac2925;
        }
        .tanuki-timestamp-item button.go-btn {
          background: #5cb85c;
          border-color: #4cae4c;
        }
        .tanuki-timestamp-item button.go-btn:hover {
          background: #449d44;
          border-color: #398439;
        }
        .tanuki-manager-footer {
          display: flex;
          justify-content: flex-end;
          flex-shrink: 0;
          padding-top: 10px;
          border-top: 1px solid #555;
        }
        .tanuki-manager button.delete-all-btn {
          background: #c9302c;
          border: 1px solid #ac2925;
          color: #fff;
          padding: 6px 12px;
          border-radius: 0;
          cursor: pointer;
          font-size: 13px;
          font-weight: bold;
          transition: background 0.2s, border-color 0.2s;
        }
        .tanuki-manager button.delete-all-btn:hover {
          background: #ac2925;
          border-color: #761c19;
        }
        .tanuki-manager button.delete-all-btn:disabled {
          background: #777;
          border-color: #999;
          color: #ccc;
          cursor: not-allowed;
        }
        .tanuki-empty-msg {
          color: #999;
          text-align: center;
          padding: 20px;
          font-style: italic;
          font-size: 14px;
        }
        .tanuki-notification {
          position: fixed;
          background: rgba(20, 20, 20, 0.9);
          color: #fff;
          padding: 12px 20px;
          border-radius: 0;
          font-size: 14px;
          transition: opacity 0.4s ease-out, transform 0.4s ease-out;
          z-index: 100001;
          pointer-events: none;
          box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3);
          border: 1px solid #444;
          opacity: 0;
          transform: translate(-50%, -50%) scale(0.9);
        }
        .tanuki-notification.show {
          opacity: 1;
          transform: translate(-50%, -50%) scale(1);
        }
        .tanuki-confirmation {
          position: fixed;
          background: rgba(30, 30, 30, 0.95);
          color: #eee;
          padding: 25px;
          border-radius: 0;
          z-index: 100000;
          min-width: 320px;
          text-align: center;
          box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
          border: 1px solid #555;
        }
        .tanuki-confirmation div.tanuki-confirmation-message {
          margin-bottom: 18px;
          font-size: 15px;
          line-height: 1.4;
        }
        .tanuki-confirmation button {
          border: none;
          padding: 10px 20px;
          border-radius: 0;
          cursor: pointer;
          color: #fff;
          font-weight: bold;
          font-size: 14px;
          transition: background 0.2s, transform 0.1s;
          margin: 0 5px;
        }
        .tanuki-confirmation button:hover {
          transform: translateY(-1px);
        }
        .tanuki-confirmation button:active {
          transform: translateY(0px);
        }
        .tanuki-confirmation button.confirm-btn {
          background: #d9534f;
        }
        .tanuki-confirmation button.confirm-btn:hover {
          background: #c9302c;
        }
        .tanuki-confirmation button.cancel-btn {
          background: #555;
        }
        .tanuki-confirmation button.cancel-btn:hover {
          background: #666;
        }
    `;
    document.head.appendChild(style);

    // --- Database Functions ---
    async function openDatabase() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(DB_NAME, DB_VERSION);
            request.onupgradeneeded = (event) => {
                const db = event.target.result;
                if (!db.objectStoreNames.contains(STORE_NAME)) {
                    db.createObjectStore(STORE_NAME, { keyPath: ['videoId', 'time'] });
                }
            };
            request.onsuccess = (event) => {
                resolve(event.target.result);
            };
            request.onerror = (event) => {
                console.error(`Tanuki Timestamp DB Error: ${event.target.error}`);
                reject(`Database error: ${event.target.error}`);
            };
        });
    }

    async function getTimestamps(videoId) {
        try {
            const db = await openDatabase();
            return new Promise((resolve, reject) => {
                const transaction = db.transaction(STORE_NAME, 'readonly');
                const store = transaction.objectStore(STORE_NAME);
                const request = store.getAll();
                request.onsuccess = (event) => {
                    resolve(event.target.result
                        .filter(t => t.videoId === videoId)
                        .sort((a, b) => a.time - b.time));
                };
                request.onerror = (event) => {
                    console.error(`Tanuki Timestamp DB Get Error: ${event.target.error}`);
                    reject(event.target.error);
                };
            });
        } catch (error) {
            console.error('Tanuki Timestamp Error loading timestamps:', error);
            return [];
        }
    }

    async function saveTimestamp(videoId, time, note) {
        try {
            const db = await openDatabase();
            return new Promise((resolve, reject) => {
                const transaction = db.transaction(STORE_NAME, 'readwrite');
                const store = transaction.objectStore(STORE_NAME);
                const request = store.put({ videoId, time, note });
                request.onsuccess = () => {
                    resolve();
                };
                request.onerror = (event) => {
                    console.error(`Tanuki Timestamp DB Save Error: ${event.target.error}`);
                    reject(event.target.error);
                };
            });
        } catch (error) {
            console.error('Tanuki Timestamp Error saving timestamp:', error);
        }
    }

    async function deleteTimestamp(videoId, time) {
        try {
            const db = await openDatabase();
            return new Promise((resolve, reject) => {
                const transaction = db.transaction(STORE_NAME, 'readwrite');
                const store = transaction.objectStore(STORE_NAME);
                const request = store.delete([videoId, time]);
                request.onsuccess = () => {
                    resolve();
                };
                request.onerror = (event) => {
                    console.error(`Tanuki Timestamp DB Delete Error: ${event.target.error}`);
                    reject(event.target.error);
                };
            });
        } catch (error) {
            console.error('Tanuki Timestamp Error deleting timestamp:', error);
        }
    }

    // --- Utility Functions ---
    function getCurrentVideoId() {
        const currentUrl = window.location;
        const urlParams = new URLSearchParams(currentUrl.search);
        const videoIdFromParam = urlParams.get('v');
        if (videoIdFromParam && videoIdFromParam.length > 0) {
            return videoIdFromParam;
        }

        const pathname = currentUrl.pathname;
        const pathParts = pathname.split('/').filter(part => part !== '');
        if (pathParts.length === 2) {
            const potentialId = pathParts[1];
            const recognizedPathTypes = ['live', 'embed', 'shorts'];
            if (recognizedPathTypes.includes(pathParts[0]) && potentialId.length > 0) {
                return potentialId;
            }
        }
        return null;
    }

    function formatTime(seconds) {
        const h = Math.floor(seconds / 3600);
        const m = Math.floor((seconds % 3600) / 60);
        const s = Math.floor(seconds % 60);
        return [h, m, s].map(n => n.toString().padStart(2, '0')).join(':');
    }

    function parseTime(timeString) {
        const parts = timeString.split(':').map(Number);
        if (parts.some(isNaN) || parts.length < 2 || parts.length > 3) {
            return null;
        }
        while (parts.length < 3) {
            parts.unshift(0);
        }
        const [h, m, s] = parts;
        if (h < 0 || m < 0 || m > 59 || s < 0 || s > 59) {
            return null;
        }
        return h * 3600 + m * 60 + s;
    }

    function isLiveStream() {
        const timeDisplay = document.querySelector('.ytp-time-display');
        return timeDisplay && (timeDisplay.classList.contains('ytp-live') || timeDisplay.classList.contains('ytp-live-badge'));
    }

    // --- UI Notification & Confirmation ---
    function showNotification(message) {
        const existingToast = document.querySelector('.tanuki-notification');
        if (existingToast) {
            existingToast.remove();
        }

        const toast = document.createElement('div');
        toast.className = 'tanuki-notification';
        toast.textContent = message;
        document.body.appendChild(toast);

        const video = document.querySelector('video');
        if (video) {
             const videoRect = video.getBoundingClientRect();
             toast.style.left = `${videoRect.left + videoRect.width / 2}px`;
             toast.style.top = `${videoRect.top + 50}px`;
             toast.style.transform = 'translateX(-50%) scale(0.9)';
        } else {
            toast.style.left = '50%';
            toast.style.top = '10%';
            toast.style.transform = 'translateX(-50%) scale(0.9)';
        }

        requestAnimationFrame(() => {
            toast.classList.add('show');
        });

        setTimeout(() => {
            toast.style.opacity = '0';
            toast.style.transform = toast.style.transform.replace('scale(1)', 'scale(0.9)');
            setTimeout(() => {
                if (toast.parentNode) {
                   toast.remove();
                }
            }, 400);
        }, 2500);
    }

    function showConfirmation(message) {
        return new Promise(resolve => {
            const existingModal = document.querySelector('.tanuki-confirmation');
            if (existingModal) {
                existingModal.remove();
            }

            const modal = document.createElement('div');
            modal.className = 'tanuki-confirmation';
            modal.addEventListener('click', e => e.stopPropagation());

            const video = document.querySelector('video');
            if (video) {
                const videoRect = video.getBoundingClientRect();
                modal.style.left = `${videoRect.left + videoRect.width / 2}px`;
                modal.style.top = `${videoRect.top + videoRect.height / 2}px`;
                modal.style.transform = 'translate(-50%, -50%)';
            } else {
                modal.style.position = 'fixed';
                modal.style.top = '50%';
                modal.style.left = '50%';
                modal.style.transform = 'translate(-50%, -50%)';
            }

            const messageEl = document.createElement('div');
            messageEl.textContent = message;
            messageEl.className = 'tanuki-confirmation-message';

            const buttonContainer = document.createElement('div');

            const confirmBtn = document.createElement('button');
            confirmBtn.textContent = 'Confirm';
            confirmBtn.className = 'confirm-btn';
            confirmBtn.addEventListener('click', () => {
                resolve(true);
                cleanup();
            });

            const cancelBtn = document.createElement('button');
            cancelBtn.textContent = 'Cancel';
            cancelBtn.className = 'cancel-btn';
            cancelBtn.addEventListener('click', () => {
                resolve(false);
                cleanup();
            });

            buttonContainer.append(confirmBtn, cancelBtn);
            modal.append(messageEl, buttonContainer);
            document.body.appendChild(modal);

            let timeoutId = null;

            const cleanup = () => {
                if (modal.parentNode) {
                    document.body.removeChild(modal);
                }
                document.removeEventListener('click', outsideClickForConfirm, true);
                document.removeEventListener('keydown', keyHandlerForConfirm);
                clearTimeout(timeoutId);
            };

            const outsideClickForConfirm = (e) => {
                if (!modal.contains(e.target)) {
                    resolve(false);
                    cleanup();
                }
            };

             const keyHandlerForConfirm = (e) => {
                if (e.key === 'Escape') {
                    resolve(false);
                    cleanup();
                }
            };

            timeoutId = setTimeout(() => {
                document.addEventListener('click', outsideClickForConfirm, true);
                document.addEventListener('keydown', keyHandlerForConfirm);
                confirmBtn.focus();
            }, 0);
        });
    }

    // --- UI Cleanup ---
    function cleanupUI() {
        if (manager) {
            closeManager();
        }
        if (noteInput && noteInput.parentNode) {
            noteInput.remove();
            noteInput = null;
        }
        if (uiContainer && uiContainer.parentNode) {
            uiContainer.remove();
            uiContainer = null;
        }
        removeProgressMarkers();
        if (currentTooltip && currentTooltip.parentNode) {
            currentTooltip.remove();
            currentTooltip = null;
        }
        const video = document.querySelector('video');
        if (updateMarkers && video) {
            video.removeEventListener('timeupdate', updateMarkers);
            updateMarkers = null;
        }
        if (timeUpdateInterval) {
            clearInterval(timeUpdateInterval);
            timeUpdateInterval = null;
        }
    }

    // --- Progress Bar Markers ---
    function removeProgressMarkers() {
        progressMarkers.forEach((marker) => {
            if (marker && marker.parentNode) {
                 marker.remove();
            }
        });
        progressMarkers = [];
    }

    function updateMarker(oldTime, newTime, newNote) {
        const marker = progressMarkers.find(m => parseInt(m.dataset.time) === oldTime);
        if (!marker) {
            return;
        }

        marker.dataset.time = newTime;
        marker.dataset.note = newNote || '';
        marker.title = formatTime(newTime) + (newNote ? ` - ${newNote}` : '');

        const video = document.querySelector('video');
        const progressBar = document.querySelector('.ytp-progress-bar');
        if (!video || !progressBar) {
            return;
        }

        const isLive = isLiveStream();
        const duration = isLive ? video.currentTime : video.duration;
        if (!duration || isNaN(duration) || duration <= 0) {
            return;
        }

        const position = Math.min(100, Math.max(0, (newTime / duration) * 100));
        marker.style.left = `${position}%`;
    }

    function removeMarker(time) {
         const index = progressMarkers.findIndex(m => parseInt(m.dataset.time) === time);
         if (index !== -1) {
             const markerToRemove = progressMarkers[index];
             if (markerToRemove && markerToRemove.parentNode) {
                 markerToRemove.remove();
             }
             progressMarkers.splice(index, 1);
         }
    }

    async function createProgressMarkers() {
        removeProgressMarkers();
        const video = document.querySelector('video');
        const progressBar = document.querySelector('.ytp-progress-bar');
        if (!video || !progressBar || !currentVideoId) {
            return;
        }

        const timestamps = await getTimestamps(currentVideoId);
        const isLive = isLiveStream();
        const duration = isLive ? video.currentTime : video.duration;
        if (!duration || isNaN(duration) || duration <= 0) {
            return;
        }

        timestamps.forEach(ts => {
            addProgressMarker(ts, duration);
        });
    }

    function addProgressMarker(ts, videoDuration = null) {
        const progressBar = document.querySelector('.ytp-progress-bar');
        if (!progressBar) {
            return;
        }

        let duration = videoDuration;
        if (duration === null) {
             const video = document.querySelector('video');
             if (!video) {
                 return;
             }
             const isLive = isLiveStream();
             duration = isLive ? video.currentTime : video.duration;
        }

        if (!duration || isNaN(duration) || duration <= 0) {
            return;
        }

        const existingMarkerIndex = progressMarkers.findIndex(m => parseInt(m.dataset.time) === ts.time);
        if (existingMarkerIndex !== -1) {
            const existingMarker = progressMarkers[existingMarkerIndex];
            existingMarker.dataset.note = ts.note || '';
            existingMarker.title = formatTime(ts.time) + (ts.note ? ` - ${ts.note}` : '');
            const position = Math.min(100, Math.max(0, (ts.time / duration) * 100));
            existingMarker.style.left = `${position}%`;
            return;
        }

        const marker = document.createElement('div');
        marker.className = 'tanuki-progress-marker';
        const position = Math.min(100, Math.max(0, (ts.time / duration) * 100));
        marker.style.left = `${position}%`;
        marker.dataset.time = ts.time;
        marker.dataset.note = ts.note || '';
        marker.title = formatTime(ts.time) + (ts.note ? ` - ${ts.note}` : '');
        marker.addEventListener('mouseenter', showMarkerTooltip);
        marker.addEventListener('mouseleave', hideMarkerTooltip);
        marker.addEventListener('click', (e) => {
             e.stopPropagation();
             const video = document.querySelector('video');
             if (video) {
                 video.currentTime = ts.time;
             }
        });
        progressBar.appendChild(marker);
        progressMarkers.push(marker);
    }

    function showMarkerTooltip(e) {
        if (currentTooltip) {
            currentTooltip.remove();
        }

        const marker = e.target;
        const note = marker.dataset.note;
        const time = parseInt(marker.dataset.time);
        const formattedTime = formatTime(time);
        const tooltipText = note ? `${formattedTime} - ${note}` : formattedTime;

        currentTooltip = document.createElement('div');
        currentTooltip.className = 'tanuki-tooltip';
        currentTooltip.textContent = tooltipText;

        const rect = marker.getBoundingClientRect();
        currentTooltip.style.left = `${rect.left + rect.width / 2}px`;
        currentTooltip.style.top = `${rect.top}px`;

        document.body.appendChild(currentTooltip);
    }

    function hideMarkerTooltip() {
        if (currentTooltip) {
            currentTooltip.remove();
            currentTooltip = null;
        }
    }

    // --- Main UI Setup ---
    function setupUI() {
        if (uiContainer) {
            return;
        }

        const controls = document.querySelector('.ytp-left-controls');
        const video = document.querySelector('video');
        if (!controls || !video || !video.duration || video.duration <= 0) {
            return;
        }

        uiContainer = document.createElement('span');
        uiContainer.className = 'tanuki-ui-container';

        const timestampEl = document.createElement('span');
        timestampEl.className = 'tanuki-timestamp';
        timestampEl.textContent = formatTime(video.currentTime); // Initial time
        timestampEl.title = 'Click to copy current time';
        timestampEl.addEventListener('click', async () => {
            const video = document.querySelector('video');
            if (video) {
                const time = Math.floor(video.currentTime);
                try {
                    await navigator.clipboard.writeText(formatTime(time));
                    showNotification('Current timestamp copied!');
                } catch (error) {
                    showNotification('Copy failed');
                    console.error("Tanuki Timestamp Copy Error:", error);
                }
            }
        });

        const createButton = (label, title, handler) => {
            const btn = document.createElement('button');
            btn.className = 'tanuki-button';
            btn.textContent = label;
            btn.title = title;
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                handler();
            });
            return btn;
        };

        const addButton = createButton('+', 'Add timestamp at current time', async () => {
            const video = document.querySelector('video');
            if (video && currentVideoId) {
                const time = Math.floor(video.currentTime);
                showNoteInput(video, time);
            }
        });

        const removeButton = createButton('-', 'Remove nearest timestamp', async () => {
            const video = document.querySelector('video');
            if (video && currentVideoId) {
                const currentTime = Math.floor(video.currentTime);
                const timestamps = await getTimestamps(currentVideoId);
                if (!timestamps.length) {
                    showNotification('No timestamps to remove');
                    return;
                }
                const closest = timestamps.reduce((prev, curr) =>
                    Math.abs(curr.time - currentTime) < Math.abs(prev.time - currentTime) ? curr : prev
                );

                const confirmed = await showConfirmation(`Delete timestamp at ${formatTime(closest.time)}?`);
                if (confirmed) {
                    await deleteTimestamp(currentVideoId, closest.time);
                    removeMarker(closest.time);
                    if (manager) {
                        const itemToRemove = manager.querySelector(`.tanuki-timestamp-item[data-time="${closest.time}"]`);
                        if (itemToRemove) {
                            itemToRemove.remove();
                        }
                        checkManagerEmpty();
                    }
                    showNotification(`Removed ${formatTime(closest.time)}`);
                }
            }
        });

        const copyButton = createButton('C', 'Copy all timestamps', async () => {
            if (!currentVideoId) {
                return;
            }
            const timestamps = await getTimestamps(currentVideoId);
            if (!timestamps.length) {
                showNotification('No timestamps to copy');
                return;
            }
            const formatted = timestamps
                .map(t => `${formatTime(t.time)}${t.note ? ` ${t.note}` : ''}`)
                .join('\n');
            try {
                await navigator.clipboard.writeText(formatted);
                showNotification('Copied all timestamps!');
            } catch (error) {
                showNotification('Copy failed');
                console.error("Tanuki Timestamp Copy All Error:", error);
            }
        });

        const manageButton = createButton('M', 'Manage timestamps', () => {
            showManager();
        });

        uiContainer.appendChild(timestampEl);
        uiContainer.appendChild(addButton);
        uiContainer.appendChild(removeButton);
        uiContainer.appendChild(copyButton);
        uiContainer.appendChild(manageButton);

        // Append to the end of left controls (generally safer for compatibility)
        if (controls && uiContainer) {
           controls.appendChild(uiContainer);
        }

        // Update timestamp display periodically
        if (timeUpdateInterval) {
            clearInterval(timeUpdateInterval); // Clear previous if any
        }
        timeUpdateInterval = setInterval(() => {
            const video = document.querySelector('video');
            const currentTsEl = uiContainer?.querySelector('.tanuki-timestamp');
            if (video && currentTsEl) {
                 currentTsEl.textContent = formatTime(video.currentTime);
            } else if (!currentTsEl && timeUpdateInterval) {
                clearInterval(timeUpdateInterval);
                timeUpdateInterval = null;
            }
        }, 1000);

        createProgressMarkers();

        // Handle live stream marker updates
        if (isLiveStream() && video) {
            updateMarkers = () => {
                const video = document.querySelector('video'); // Re-select video in case element changed
                if (!video) {
                   return;
                }
                const currentTime = video.currentTime;
                if (!currentTime || currentTime <= 0) {
                    return;
                }
                progressMarkers.forEach(marker => {
                    const time = parseInt(marker.dataset.time);
                    if (time <= currentTime) {
                        const position = Math.min(100, Math.max(0, (time / currentTime) * 100));
                        marker.style.left = `${position}%`;
                    } else {
                        marker.style.left = '100%';
                    }
                });
            };
            video.addEventListener('timeupdate', updateMarkers);
        }
    }

    // --- Note Input Popup ---
    function showNoteInput(video, time, initialNote = '') {
        if (noteInput) {
            return;
        }

        noteInput = document.createElement('div');
        noteInput.className = 'tanuki-note-input';
        noteInput.addEventListener('click', e => e.stopPropagation());

        const input = document.createElement('input');
        input.type = 'text';
        input.placeholder = 'Enter note (optional)';
        input.value = initialNote;

        const saveBtn = document.createElement('button');
        saveBtn.textContent = 'Save';

        noteInput.append(input, saveBtn);
        document.body.appendChild(noteInput);

        const videoRect = video.getBoundingClientRect();
        noteInput.style.left = `${videoRect.left + videoRect.width / 2}px`;
        noteInput.style.top = `${videoRect.top + videoRect.height / 2}px`;
        noteInput.style.transform = 'translate(-50%, -50%)';

        setTimeout(() => {
            input.focus();
            input.setSelectionRange(input.value.length, input.value.length);
        }, 50);

        let timeoutId = null;

        const cleanup = () => {
            if (noteInput && noteInput.parentNode) {
                noteInput.remove();
            }
            noteInput = null;
            document.removeEventListener('click', outsideClick, true);
            document.removeEventListener('keydown', handleEscape);
            clearTimeout(timeoutId);
        };

        const saveHandler = async () => {
            const note = input.value.trim();
            const ts = { videoId: currentVideoId, time, note };

             if (!initialNote) {
                const existingTimestamps = await getTimestamps(currentVideoId);
                if (existingTimestamps.some(t => t.time === time)) {
                    const confirmed = await showConfirmation(`Timestamp at ${formatTime(time)} already exists. Overwrite note?`);
                    if (!confirmed) {
                         cleanup();
                         return;
                    }
                }
             }

            await saveTimestamp(currentVideoId, time, note);
            addProgressMarker(ts);

            if (manager) {
                const list = manager.querySelector('.tanuki-manager-list');
                const existingItem = list?.querySelector(`.tanuki-timestamp-item[data-time="${time}"]`);
                if (existingItem) {
                    updateTimestampItem(existingItem, ts);
                } else if (list) {
                    const newItem = createTimestampItem(ts);
                    const timestamps = await getTimestamps(currentVideoId);
                    let inserted = false;
                    const items = list.querySelectorAll('.tanuki-timestamp-item');
                    for (let i = 0; i < items.length; i++) {
                        const itemTime = parseInt(items[i].dataset.time);
                        if (time < itemTime) {
                            list.insertBefore(newItem, items[i]);
                            inserted = true;
                            break;
                        }
                    }
                    if (!inserted) {
                        list.appendChild(newItem);
                    }
                    checkManagerEmpty(false);
                }
            }
            cleanup();
            showNotification(`Saved ${formatTime(time)}${note ? ` - "${note}"` : ''}`);
        };

        const outsideClick = (e) => {
            if (noteInput && !noteInput.contains(e.target)) {
                cleanup();
            }
        };

        const handleEscape = (e) => {
            if (e.key === 'Escape') {
                cleanup();
            }
        };

        saveBtn.addEventListener('click', (e) => {
             e.stopPropagation();
             saveHandler();
        });
        input.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                saveHandler();
            }
        });

         timeoutId = setTimeout(() => {
            document.addEventListener('click', outsideClick, true);
            document.addEventListener('keydown', handleEscape);
        }, 0);
    }

    // --- Timestamp Manager Popup ---
    async function showManager() {
        if (!currentVideoId || manager) {
            return;
        }

        manager = document.createElement('div');
        manager.className = 'tanuki-manager';
        manager.addEventListener('click', e => e.stopPropagation());

        const header = document.createElement('div');
        header.className = 'tanuki-manager-header';

        const title = document.createElement('h3');
        title.textContent = 'Timestamp Manager';

        const closeButton = document.createElement('button');
        closeButton.textContent = '✕';
        closeButton.title = 'Close Manager (Esc)';
        closeButton.className = 'close-btn';
        closeButton.addEventListener('click', closeManager);

        header.append(title, closeButton);
        manager.appendChild(header);

        const list = document.createElement('div');
        list.className = 'tanuki-manager-list';
        manager.appendChild(list);

        const footer = document.createElement('div');
        footer.className = 'tanuki-manager-footer';

        const deleteAllBtn = document.createElement('button');
        deleteAllBtn.textContent = 'Delete All Timestamps';
        deleteAllBtn.title = 'Delete all timestamps for this video';
        deleteAllBtn.className = 'delete-all-btn';
        deleteAllBtn.addEventListener('click', handleDeleteAll);
        footer.appendChild(deleteAllBtn);
        manager.appendChild(footer);

        const timestamps = await getTimestamps(currentVideoId);
        if (!timestamps.length) {
            checkManagerEmpty(true, list);
            deleteAllBtn.disabled = true;
        } else {
            timestamps.forEach(ts => {
                const item = createTimestampItem(ts);
                list.appendChild(item);
            });
             deleteAllBtn.disabled = false;
        }

        positionManager();
        document.body.appendChild(manager);

        setTimeout(() => {
            document.addEventListener('keydown', managerKeydownHandler);
            document.addEventListener('click', managerOutsideClickHandler, true);
        }, 0);
    }

    // --- Manager Helper Functions ---

    function closeManager() {
         if (manager) {
            if (manager.parentNode) {
                manager.remove();
            }
            manager = null;
            document.removeEventListener('keydown', managerKeydownHandler);
            document.removeEventListener('click', managerOutsideClickHandler, true);
         }
    }

    function managerKeydownHandler(e) {
        if (e.key === 'Escape') {
            const activeElement = document.activeElement;
            const isInputFocused = manager && manager.contains(activeElement) && activeElement.tagName === 'INPUT';
            if (!isInputFocused) {
                 closeManager();
            } else {
                 activeElement.blur();
            }
        }
    }

    function managerOutsideClickHandler(e) {
        const isInsideConfirmation = !!e.target.closest('.tanuki-confirmation');
        const isManageButton = !!e.target.closest('.tanuki-button[title="Manage timestamps"]');
        if (manager && !manager.contains(e.target) && !isManageButton && !isInsideConfirmation) {
            closeManager();
        }
    }

    function positionManager() {
        if (!manager) {
            return;
        }
        const video = document.querySelector('video');
        if (video) {
            const videoRect = video.getBoundingClientRect();
            const managerWidth = 540;
            const managerHeight = 380;
            let left = videoRect.left + (videoRect.width - managerWidth) / 2;
            let top = videoRect.top + (videoRect.height - managerHeight) / 2;

            left = Math.max(10, Math.min(window.innerWidth - managerWidth - 10, left));
            top = Math.max(10, Math.min(window.innerHeight - managerHeight - 10, top));

            manager.style.left = `${left}px`;
            manager.style.top = `${top}px`;
            manager.style.transform = '';
        } else {
            manager.style.position = 'fixed';
            manager.style.top = '50%';
            manager.style.left = '50%';
            manager.style.transform = 'translate(-50%, -50%)';
        }
    }

    function checkManagerEmpty(forceShow = null, list = null) {
        if (!manager && !list) {
            return;
        }

        const theList = list || manager?.querySelector('.tanuki-manager-list');
        const deleteAllBtn = manager?.querySelector('.delete-all-btn');

        if (!theList) {
             return;
        }

        const emptyMsgClass = 'tanuki-empty-msg';
        let emptyMsg = theList.querySelector(`.${emptyMsgClass}`);
        const hasItems = !!theList.querySelector('.tanuki-timestamp-item');

        if (forceShow === true || (forceShow === null && !hasItems)) {
            if (!emptyMsg) {
                emptyMsg = document.createElement('div');
                emptyMsg.className = emptyMsgClass;
                emptyMsg.textContent = 'No timestamps created for this video yet.';
                theList.prepend(emptyMsg);
            }
             if (deleteAllBtn) {
                 deleteAllBtn.disabled = true;
             }
        } else if (forceShow === false || (forceShow === null && hasItems)) {
            if (emptyMsg) {
                emptyMsg.remove();
            }
             if (deleteAllBtn) {
                 deleteAllBtn.disabled = false;
             }
        }
    }

    async function handleDeleteAll() {
        if (!currentVideoId || !manager) {
            return;
        }

        const listElement = manager.querySelector('.tanuki-manager-list');
        const deleteAllButton = manager.querySelector('.delete-all-btn');
        if (!listElement) {
            console.error("Tanuki Timestamp: Manager list element not found in handleDeleteAll.");
            return;
        }

        const timestamps = await getTimestamps(currentVideoId);
        if (timestamps.length === 0) {
            showNotification("No timestamps to delete.");
            return;
        }

        const confirmed = await showConfirmation(`Are you sure you want to delete all ${timestamps.length} timestamps for this video? This cannot be undone.`);

        if (!manager || !manager.contains(listElement)) {
             return; // Manager closed or list is stale after confirmation
        }

        if (confirmed) {
            try {
                const deletePromises = timestamps.map(ts => deleteTimestamp(currentVideoId, ts.time));
                await Promise.all(deletePromises);

                while (listElement.firstChild) {
                    listElement.removeChild(listElement.firstChild);
                }

                removeProgressMarkers();
                checkManagerEmpty(true, listElement);
                showNotification("All timestamps deleted successfully.");

            } catch (error) {
                console.error("Tanuki Timestamp: Error deleting all timestamps:", error);
                showNotification("Error occurred while deleting timestamps.");
                if (deleteAllButton) {
                     deleteAllButton.disabled = timestamps.length === 0;
                }
            }
        }
    }

    // --- Manager Timestamp Item Creation & Editing ---
    function createTimestampItem(ts) {
        const item = document.createElement('div');
        item.className = 'tanuki-timestamp-item';
        item.dataset.time = ts.time;

        const timeEl = document.createElement('span');
        timeEl.textContent = formatTime(ts.time);
        timeEl.title = 'Double-click to edit time';

        const noteEl = document.createElement('span');
        if (ts.note) {
            noteEl.textContent = ts.note;
        } else {
            noteEl.textContent = NOTE_PLACEHOLDER;
            noteEl.classList.add('tanuki-note-placeholder');
        }
        noteEl.title = 'Double-click to edit note';

        const goBtn = document.createElement('button');
        goBtn.textContent = '▶';
        goBtn.title = 'Go to timestamp';
        goBtn.className = 'go-btn';

        const deleteBtn = document.createElement('button');
        deleteBtn.textContent = '✕';
        deleteBtn.title = 'Delete timestamp';
        deleteBtn.className = 'delete-btn';

        const buttonContainer = document.createElement('div');
        buttonContainer.style.display = 'flex';
        buttonContainer.style.alignItems = 'center';
        buttonContainer.append(goBtn, deleteBtn);

        item.append(timeEl, noteEl, buttonContainer);

        goBtn.addEventListener('click', () => {
            const video = document.querySelector('video');
            if (video) {
                video.currentTime = ts.time;
            }
        });

        deleteBtn.addEventListener('click', async () => {
            const currentItemTime = parseInt(item.dataset.time);
            const confirmed = await showConfirmation(`Delete timestamp at ${formatTime(currentItemTime)}?`);
            if (confirmed) {
                await deleteTimestamp(currentVideoId, currentItemTime);
                removeMarker(currentItemTime);
                item.remove();
                checkManagerEmpty();
            }
        });

        const makeEditable = (element, inputClass, originalValue, saveCallback, validationCallback = null) => {
             if (element.parentNode && element.parentNode.querySelector('input')) {
                 return;
             }

             const input = document.createElement('input');
             input.type = 'text';
             input.className = inputClass;
             input.value = originalValue;

             const originalElement = element;
             originalElement.replaceWith(input);
             input.focus();
             input.select();

             let isSaving = false;

             const saveChanges = async () => {
                 if (!input.parentNode) {
                      return false;
                 }
                 if (isSaving) {
                     return false;
                 }
                 isSaving = true;

                 const newValue = input.value.trim();

                 if (validationCallback && !(await validationCallback(newValue))) {
                     input.replaceWith(originalElement);
                     isSaving = false;
                     return false;
                 }

                 const originalTimeSeconds = (inputClass === 'time-input') ? parseTime(originalElement.textContent) : null;
                 const hasChanged = (inputClass === 'time-input')
                    ? parseTime(newValue) !== originalTimeSeconds
                    : newValue !== (ts.note || '');

                 if (hasChanged) {
                    try {
                        await saveCallback(newValue, input, originalElement);
                    } catch (error) {
                         console.error("Tanuki Timestamp: Error during save callback:", error);
                         if (input.parentNode) {
                             input.replaceWith(originalElement);
                         }
                    }
                 } else {
                     if (input.parentNode) {
                         input.replaceWith(originalElement);
                     }
                 }
                 isSaving = false;
                 return true;
             };

             const handleBlur = async (e) => {
                 const relatedTarget = e.relatedTarget;
                 if (!relatedTarget || !item.contains(relatedTarget) || !['BUTTON', 'INPUT', 'A'].includes(relatedTarget.tagName)) {
                    await saveChanges();
                 }
             };

             input.addEventListener('blur', (e) => setTimeout(() => handleBlur(e), 150));

             input.addEventListener('keydown', async (e) => {
                 if (e.key === 'Enter') {
                     e.preventDefault();
                     await saveChanges();
                 } else if (e.key === 'Escape') {
                     e.preventDefault();
                     if (input.parentNode) {
                        input.replaceWith(originalElement);
                     }
                 }
             });
         };

        timeEl.addEventListener('dblclick', () => {
            makeEditable(timeEl, 'time-input', timeEl.textContent,
                async (newTimeString, inputElement, originalDisplayElement) => {
                    const newTime = parseTime(newTimeString);
                    const oldTime = ts.time;

                    await deleteTimestamp(currentVideoId, oldTime);
                    await saveTimestamp(currentVideoId, newTime, ts.note);

                    ts.time = newTime;
                    item.dataset.time = newTime;
                    originalDisplayElement.textContent = formatTime(newTime);
                    if (inputElement.parentNode) {
                        inputElement.replaceWith(originalDisplayElement);
                    }
                    updateMarker(oldTime, newTime, ts.note);

                    const list = manager?.querySelector('.tanuki-manager-list');
                    if(list) {
                        const items = Array.from(list.querySelectorAll('.tanuki-timestamp-item'));
                        items.sort((a, b) => parseInt(a.dataset.time) - parseInt(b.dataset.time));
                        items.forEach(sortedItem => list.appendChild(sortedItem));
                    }
                    showNotification(`Time updated to ${formatTime(newTime)}`);
                },
                async (newTimeString) => {
                    const newTime = parseTime(newTimeString);
                    if (newTime === null || newTime < 0) {
                        showNotification('Invalid time format (HH:MM:SS)');
                        return false;
                    }
                    if (newTime !== ts.time) {
                        const existingTimestamps = await getTimestamps(currentVideoId);
                        if (existingTimestamps.some(t => t.time === newTime)) {
                            showNotification(`Timestamp at ${formatTime(newTime)} already exists.`);
                            return false;
                        }
                    }
                    return true;
                }
            );
        });

        noteEl.addEventListener('dblclick', () => {
            makeEditable(noteEl, 'note-input', ts.note || '',
                async (newNote, inputElement, originalDisplayElement) => {
                    await saveTimestamp(currentVideoId, ts.time, newNote);

                    ts.note = newNote;
                    if (newNote) {
                        originalDisplayElement.textContent = newNote;
                        originalDisplayElement.classList.remove('tanuki-note-placeholder');
                    } else {
                        originalDisplayElement.textContent = NOTE_PLACEHOLDER;
                        originalDisplayElement.classList.add('tanuki-note-placeholder');
                    }
                    if (inputElement.parentNode) {
                        inputElement.replaceWith(originalDisplayElement);
                    }
                    updateMarker(ts.time, ts.time, newNote);
                    showNotification(`Note updated for ${formatTime(ts.time)}`);
                }
            );
        });

        return item;
    }

    function updateTimestampItem(itemElement, ts) {
        if (!itemElement) {
            return;
        }

        const timeEl = itemElement.querySelector('span:first-child');
        const noteEl = itemElement.querySelector('span:nth-child(2)');

        if (timeEl) {
            timeEl.textContent = formatTime(ts.time);
        }
        if (noteEl) {
             if (ts.note) {
                noteEl.textContent = ts.note;
                noteEl.classList.remove('tanuki-note-placeholder');
            } else {
                noteEl.textContent = NOTE_PLACEHOLDER;
                noteEl.classList.add('tanuki-note-placeholder');
            }
        }
        itemElement.dataset.time = ts.time;
    }

    // --- Initialization and Video Change Detection ---
    let initInterval = setInterval(() => {
        const videoId = getCurrentVideoId();
        const videoPlayer = document.querySelector('video');
        const controlsExist = !!document.querySelector('.ytp-left-controls');

        if (videoId && videoPlayer && videoPlayer.readyState >= 1 && controlsExist) {
            if (videoId !== currentVideoId) {
                cleanupUI();
                currentVideoId = videoId;
                setTimeout(setupUI, 500);
            } else if (!uiContainer && currentVideoId === videoId) {
                 cleanupUI();
                 setTimeout(setupUI, 500);
            }
        } else if (currentVideoId && (!videoId || !videoPlayer || !controlsExist)) {
            cleanupUI();
            currentVideoId = null;
        }
    }, 1000);

})();