YouTube Timestamp Manager

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

目前為 2025-04-09 提交的版本,檢視 最新版本

// ==UserScript==
// @name         YouTube Timestamp Manager
// @namespace    http://tampermonkey.net/
// @version      2.3
// @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;

    // Inject CSS into <head>
    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) => 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) => reject(event.target.error);
            });
        } catch (error) {
            console.error('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) => reject(event.target.error);
            });
        } catch (error) {
            console.error('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) => reject(event.target.error);
            });
        } catch (error) {
            console.error('Error deleting timestamp:', error);
        }
    }

    // --- Utility Functions ---
    function getCurrentVideoId() {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('v');
    }

    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; // Invalid format
        }
        while (parts.length < 3) {
            parts.unshift(0); // Pad with hours/minutes if missing
        }
        const [h, m, s] = parts;
        if (h < 0 || m < 0 || m > 59 || s < 0 || s > 59) {
            return null; // Invalid values
        }
        return h * 3600 + m * 60 + s;
    }

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

    // --- UI Notification & Confirmation ---
    function showNotification(message) {
        // Remove existing toast if any
        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');

        // Center positioning (relative to viewport or video)
        if (video) {
             const videoRect = video.getBoundingClientRect();
             // Position near top-center of video player
             toast.style.left = `${videoRect.left + videoRect.width / 2}px`;
             toast.style.top = `${videoRect.top + 50}px`; // Offset from top
             // Ensure transform origin is correct for centering
             toast.style.transform = 'translateX(-50%) scale(0.9)'; // Initial state for animation
        } else { // Fallback if video isn't found
            toast.style.left = '50%';
            toast.style.top = '10%'; // Near top of viewport
             // Ensure transform origin is correct for centering
            toast.style.transform = 'translateX(-50%) scale(0.9)'; // Initial state for animation
        }


        // Trigger the animation
        requestAnimationFrame(() => {
            toast.classList.add('show'); // Add class to animate in
        });

        // Auto-remove after delay
        setTimeout(() => {
            toast.style.opacity = '0';
            toast.style.transform = toast.style.transform.replace('scale(1)', 'scale(0.9)'); // Animate out
            setTimeout(() => toast.remove(), 400); // Remove after fade out animation
        }, 2500); // Increased display time slightly
    }


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

            const modal = document.createElement('div');
            modal.className = 'tanuki-confirmation';
            // Stop clicks inside modal from propagating to manager's outside click listener
            modal.addEventListener('click', e => e.stopPropagation());

            const video = document.querySelector('video');
             // Center positioning (relative to viewport or 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%)'; // Center using transform
            } else { // Fallback positioning
                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'; // Add class for styling

            const buttonContainer = document.createElement('div'); // Container for buttons

            const confirmBtn = document.createElement('button');
            confirmBtn.textContent = 'Confirm';
            confirmBtn.className = 'confirm-btn'; // Add class for styling
            confirmBtn.addEventListener('click', (e) => {
                // e.stopPropagation(); // Already stopped by modal listener
                resolve(true);
                cleanup();
            });

            const cancelBtn = document.createElement('button');
            cancelBtn.textContent = 'Cancel';
            cancelBtn.className = 'cancel-btn'; // Add class for styling
            cancelBtn.addEventListener('click', (e) => {
                // e.stopPropagation(); // Already stopped by modal listener
                resolve(false);
                cleanup();
            });

            buttonContainer.append(confirmBtn, cancelBtn); // Add buttons to container
            modal.append(messageEl, buttonContainer); // Add message and button container
            document.body.appendChild(modal);

            let timeoutId = null;

            const cleanup = () => {
                if (modal.parentNode) {
                    document.body.removeChild(modal);
                }
                // Remove the document-level listeners specific to this confirmation
                document.removeEventListener('click', outsideClickForConfirm, true);
                document.removeEventListener('keydown', keyHandlerForConfirm);
                clearTimeout(timeoutId);
            };

            // Listener specifically for clicks outside *this confirmation modal*
            const outsideClickForConfirm = (e) => {
                // If the click is outside the modal, resolve false and cleanup
                if (!modal.contains(e.target)) {
                    resolve(false);
                    cleanup();
                }
            };

            // Listener specifically for keydowns while *this confirmation modal* is open
             const keyHandlerForConfirm = (e) => {
                if (e.key === 'Escape') {
                    resolve(false);
                    cleanup();
                } else if (e.key === 'Enter') {
                    // Optional: Confirm on Enter
                    // resolve(true);
                    // cleanup();
                }
            };

            // Use timeout to add listeners after current event cycle finishes
            // Add the specific listeners for this modal instance
            timeoutId = setTimeout(() => {
                document.addEventListener('click', outsideClickForConfirm, true); // Capture phase
                document.addEventListener('keydown', keyHandlerForConfirm);
                confirmBtn.focus(); // Focus the confirm button by default
            }, 0);
        });
    }

    // --- UI Cleanup ---
    function cleanupUI() {
        if (manager) {
            closeManager(); // Use the dedicated close function which handles listeners
        }
        if (noteInput) {
            noteInput.remove();
            noteInput = null;
            // Potentially remove noteInput specific listeners if added globally
        }
        if (uiContainer) {
            uiContainer.remove();
            uiContainer = null;
        }
        removeProgressMarkers();
        if (currentTooltip) {
            currentTooltip.remove();
            currentTooltip = null;
        }
        const video = document.querySelector('video');
        if (updateMarkers && video) {
            video.removeEventListener('timeupdate', updateMarkers);
            updateMarkers = null;
        }
    }

    // --- Progress Bar Markers ---
    function removeProgressMarkers() {
        progressMarkers.forEach((marker, index) => {
            try {
                if (marker && marker.parentNode) { // Check if marker exists and is in DOM
                     marker.remove();
                }
            } catch (e) {
                console.error(`Tanuki Timestamp: Error removing marker at index ${index}:`, e);
            }
        });
        progressMarkers = []; // Clears the array reference
    }


    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}` : ''); // Update title

        // Recalculate position
        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; // Added duration > 0 check

        const position = Math.min(100, Math.max(0, (newTime / duration) * 100)); // Clamp between 0 and 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); // Remove from array regardless of DOM state
         }
    }

    async function createProgressMarkers() {
        removeProgressMarkers(); // Clear existing before adding new ones
        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; // Added duration > 0 check

        timestamps.forEach(ts => {
            addProgressMarker(ts, duration); // Pass duration to avoid recalculating
        });
    }

    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; // Check duration validity

        // Check if marker already exists for this time *in the array*
        const existingMarkerIndex = progressMarkers.findIndex(m => parseInt(m.dataset.time) === ts.time);
        if (existingMarkerIndex !== -1) {
             // Update existing marker's note and ensure position is correct
            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;
        }

        // Create and add new marker
        const marker = document.createElement('div');
        marker.className = 'tanuki-progress-marker';
        const position = Math.min(100, Math.max(0, (ts.time / duration) * 100)); // Clamp position
        marker.style.left = `${position}%`;
        marker.dataset.time = ts.time;
        marker.dataset.note = ts.note || '';
        marker.title = formatTime(ts.time) + (ts.note ? ` - ${ts.note}` : ''); // Add title for hover info
        marker.addEventListener('mouseenter', showMarkerTooltip);
        marker.addEventListener('mouseleave', hideMarkerTooltip);
        marker.addEventListener('click', (e) => { // Seek on marker click
             e.stopPropagation(); // Prevent progress bar seek if user clicks marker directly
             const video = document.querySelector('video');
             if (video) video.currentTime = ts.time;
        });
        progressBar.appendChild(marker);
        progressMarkers.push(marker); // Add to array *after* adding to DOM
    }


    function showMarkerTooltip(e) {
        if (currentTooltip) currentTooltip.remove(); // Remove previous instantly

        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();
        // Position tooltip centered above the marker
        currentTooltip.style.left = `${rect.left + rect.width / 2}px`;
        currentTooltip.style.top = `${rect.top}px`; // Align top with marker top initially
        // transform will move it up

        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');
        // Ensure video has duration and controls exist
        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 = '00:00:00';
        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');
                }
            }
        });

        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(); // Prevent video pause/play
                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;
                }
                // Find the timestamp closest to the current time
                const closest = timestamps.reduce((prev, curr) =>
                    Math.abs(curr.time - currentTime) < Math.abs(prev.time - currentTime) ? curr : prev
                );

                // Show confirmation dialog
                const confirmed = await showConfirmation(`Delete timestamp at ${formatTime(closest.time)}?`);
                if (confirmed) {
                    await deleteTimestamp(currentVideoId, closest.time);
                    removeMarker(closest.time); // Remove from progress bar
                    // If manager is open, remove from list
                    if (manager) {
                        const itemToRemove = manager.querySelector(`.tanuki-timestamp-item[data-time="${closest.time}"]`);
                        if (itemToRemove) itemToRemove.remove();
                        checkManagerEmpty(); // Check if list is now empty
                    }
                    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');
            navigator.clipboard.writeText(formatted)
                .then(() => showNotification('Copied all timestamps!'));
        });

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

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

        // Insert into controls, trying to place it after volume but before other buttons
        const volumePanel = controls.querySelector('.ytp-volume-panel');
        if (volumePanel && volumePanel.nextSibling) {
             controls.insertBefore(uiContainer, volumePanel.nextSibling);
        } else {
             controls.appendChild(uiContainer); // Fallback: append at the end
        }


        // Update timestamp display
        const timeUpdateInterval = setInterval(() => {
            const video = document.querySelector('video');
            const currentTsEl = uiContainer?.querySelector('.tanuki-timestamp'); // Check if still exists
            if (video && currentTsEl) {
                 currentTsEl.textContent = formatTime(video.currentTime);
            } else if (!currentTsEl && timeUpdateInterval) { // Ensure interval exists before clearing
                clearInterval(timeUpdateInterval); // Stop interval if element is gone
            }
        }, 1000);

        createProgressMarkers();

        // Handle live stream marker updates
        if (isLiveStream() && video) {
            updateMarkers = () => {
                const currentTime = video.currentTime;
                if (!currentTime || currentTime <= 0) return; // Ignore if time is invalid
                progressMarkers.forEach(marker => {
                    const time = parseInt(marker.dataset.time);
                    if (time <= currentTime) {
                        const position = Math.min(100, Math.max(0, (time / currentTime) * 100)); // Clamp
                        marker.style.left = `${position}%`;
                    } else {
                        // For live streams, future markers might not be relevant or position is uncertain
                        marker.style.left = '100%'; // Or hide them: marker.style.display = 'none';
                    }
                });
            };
            video.addEventListener('timeupdate', updateMarkers);
        }
    }

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

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

        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);

        // Position relative to video
        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%)'; // Center using transform


        // Focus input after slight delay
        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 };

            // Check if timestamp already exists (only relevant if creating new)
             if (!initialNote) { // Only check when adding, not editing via this popup
                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); // Add or update marker

            // If manager is open, add/update the item
            if (manager) {
                const list = manager.querySelector('.tanuki-manager-list');
                const existingItem = list?.querySelector(`.tanuki-timestamp-item[data-time="${time}"]`); // Add optional chaining for list
                if (existingItem) {
                    updateTimestampItem(existingItem, ts);
                } else if (list) { // Ensure list exists before appending
                    const newItem = createTimestampItem(ts);
                    // Insert sorted
                    const timestamps = await getTimestamps(currentVideoId); // Get fresh sorted list
                    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); // Append if largest time
                    }
                    checkManagerEmpty(false); // Ensure "empty" message is removed
                }
            }
            cleanup();
            showNotification(`Saved ${formatTime(time)}${note ? ` - "${note}"` : ''}`);
        };

        const outsideClick = (e) => {
            // Close only if click is truly outside the input popup
            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(); // Prevent form submission if wrapped
                saveHandler();
            }
        });

        // Use timeout to add listeners after current event cycle
         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()); // Prevent clicks closing it immediately

        // --- Create Header Elements ---
        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 = '✕'; // Use multiplication sign X
        closeButton.title = 'Close Manager (Esc)';
        closeButton.className = 'close-btn';
        closeButton.addEventListener('click', closeManager); // Use named function

        header.append(title, closeButton); // Add title and button to header
        manager.appendChild(header); // Add header to manager

        // --- List ---
        const list = document.createElement('div');
        list.className = 'tanuki-manager-list';
        manager.appendChild(list); // Add list after header

        // --- Footer ---
        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); // Add handler
        footer.appendChild(deleteAllBtn);
        manager.appendChild(footer); // Add footer after list

        // --- Populate List ---
        const timestamps = await getTimestamps(currentVideoId);
        if (!timestamps.length) {
            checkManagerEmpty(true, list); // Show empty message
            deleteAllBtn.disabled = true; // Disable delete all if no timestamps
        } else {
            timestamps.forEach(ts => {
                const item = createTimestampItem(ts);
                list.appendChild(item);
            });
             deleteAllBtn.disabled = false;
        }

        // --- Position and Display ---
        positionManager();
        document.body.appendChild(manager);

        // --- Global Listeners for Closing ---
        // Add listeners AFTER manager is in DOM and initial setup is done
        setTimeout(() => {
            document.addEventListener('keydown', managerKeydownHandler);
            document.addEventListener('click', managerOutsideClickHandler, true); // Capture phase
        }, 0);
    }

    // --- Manager Helper Functions ---

    function closeManager() {
         if (manager) {
            manager.remove();
            manager = null;
            // Remove global listeners when manager closes
            document.removeEventListener('keydown', managerKeydownHandler);
            document.removeEventListener('click', managerOutsideClickHandler, true);
         }
    }

    // Specific handler for manager keydown events
    function managerKeydownHandler(e) {
        if (e.key === 'Escape') {
            // Check if an input field inside the manager has focus
            const activeElement = document.activeElement;
            const isInputFocused = manager && manager.contains(activeElement) && activeElement.tagName === 'INPUT';

            if (!isInputFocused) { // Only close manager if not editing text
                 closeManager();
            } else {
                 // If input is focused, let Escape blur the input first (handled in item creation)
                 activeElement.blur();
            }
        }
    }

    // Specific handler for clicks outside the manager
    function managerOutsideClickHandler(e) {
         // Close only if click is outside manager and not on the 'M' button that opened it
         // AND also check if the click is inside a confirmation dialog
        const isInsideConfirmation = !!e.target.closest('.tanuki-confirmation');
        if (manager && !manager.contains(e.target) && !e.target.closest('.tanuki-button[title="Manage timestamps"]') && !isInsideConfirmation) {
            closeManager();
        }
    }

    function positionManager() {
        if (!manager) return;
        const video = document.querySelector('video');
        if (video) {
            const videoRect = video.getBoundingClientRect();
            const managerWidth = 540; // Match CSS
            const managerHeight = 380; // Match CSS
            // Calculate centered position, ensuring it stays within viewport bounds
            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 = ''; // Reset transform if previously used
        } else { // Fallback positioning (centered in viewport)
            manager.style.position = 'fixed';
            manager.style.top = '50%';
            manager.style.left = '50%';
            manager.style.transform = 'translate(-50%, -50%)';
        }
    }


    // Helper to check if manager list is empty and show/hide message
    function checkManagerEmpty(forceShow = null, list = null) {
        // If manager is gone, don't try to access its children
        if (!manager && !list) {
            // console.log("checkManagerEmpty: No manager or list provided.");
            return;
        }

        // Prefer passed list, fallback to querying manager IF it still exists
        const theList = list || manager?.querySelector('.tanuki-manager-list');
        const deleteAllBtn = manager?.querySelector('.delete-all-btn');

        // Check if theList itself exists now
        if (!theList) {
             // This case can happen if the manager was removed concurrently, e.g., during handleDeleteAll
             // console.warn("checkManagerEmpty: Target list element not found.");
             return;
        }


        const emptyMsgClass = 'tanuki-empty-msg';
        let emptyMsg = theList.querySelector(`.${emptyMsgClass}`);
        // Check for items *within* theList element
        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.'; // Updated message
                theList.prepend(emptyMsg); // Add message at the top
            }
             if (deleteAllBtn) deleteAllBtn.disabled = true; // Disable delete all button
        } else if (forceShow === false || (forceShow === null && hasItems)) {
            if (emptyMsg) {
                emptyMsg.remove();
            }
             if (deleteAllBtn) deleteAllBtn.disabled = false; // Enable delete all button
        }
    }

    // --- Handle Delete All ---
    async function handleDeleteAll() {
        if (!currentVideoId || !manager) return;

        // Get manager elements *before* confirmation/await
        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; // Should not happen if manager exists, but safety check
        }

        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.`);

        // Check if manager still exists after confirmation await
        if (!manager) {
             console.log("Tanuki Timestamp: Manager was closed during confirmation.");
             return;
        }
        // Re-verify listElement still belongs to the current manager
        if (!manager.contains(listElement)) {
             console.error("Tanuki Timestamp: Stale list element reference in handleDeleteAll after confirmation.");
             return;
        }

        if (confirmed) {
            console.log("Tanuki Timestamp: Deleting all timestamps for video:", currentVideoId);
            try {
                // Create an array of delete promises
                const deletePromises = timestamps.map(ts => deleteTimestamp(currentVideoId, ts.time));
                // Wait for all deletions to complete
                await Promise.all(deletePromises);
                console.log("Tanuki Timestamp: Database deletions complete.");

                // Clear UI *after* DB operations
                // Replace innerHTML setting with safe node removal
                while (listElement.firstChild) {
                    listElement.removeChild(listElement.firstChild);
                }
                console.log("Tanuki Timestamp: Manager list cleared safely.");

                removeProgressMarkers(); // <<<< Call marker removal
                console.log("Tanuki Timestamp: Progress markers removed.");

                // Update manager state using the list reference
                checkManagerEmpty(true, listElement); // Show empty message and disable button
                console.log("Tanuki Timestamp: Manager state updated (empty).");

                showNotification("All timestamps deleted successfully.");

            } catch (error) {
                console.error("Tanuki Timestamp: Error deleting all timestamps:", error);
                showNotification("Error occurred while deleting timestamps.");
                // Attempt to restore sensible state if possible
                 if (deleteAllButton) deleteAllButton.disabled = timestamps.length === 0;
            }
        } else {
             console.log("Tanuki Timestamp: Delete all cancelled.");
        }
    }


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

        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 = '▶'; // Using a play symbol
        goBtn.title = 'Go to timestamp';
        goBtn.className = 'go-btn';

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

        // Create a container for the buttons for better layout control if needed
        const buttonContainer = document.createElement('div');
        buttonContainer.style.display = 'flex'; // Keep buttons inline
        buttonContainer.style.alignItems = 'center';
        buttonContainer.append(goBtn, deleteBtn);

        item.append(timeEl, noteEl, buttonContainer); // Add button container


        // --- Event Listeners ---

        // Go to time
        goBtn.addEventListener('click', () => {
            const video = document.querySelector('video');
            if (video) {
                video.currentTime = ts.time;
                // Optional: Close manager after clicking Go
                 // closeManager();
            }
        });

        // Delete Single Item
        deleteBtn.addEventListener('click', async () => {
            // Find the item again in case `ts` object is stale (unlikely here, but good practice)
            const currentItemTime = parseInt(item.dataset.time);
            const confirmed = await showConfirmation(`Delete timestamp at ${formatTime(currentItemTime)}?`);
            if (confirmed) {
                await deleteTimestamp(currentVideoId, currentItemTime);
                removeMarker(currentItemTime); // Remove from progress bar
                item.remove();
                checkManagerEmpty(); // Check if list is now empty after deletion
            }
        });

        // --- Inline Editing Functions ---
        const makeEditable = (element, inputClass, originalValue, saveCallback, validationCallback = null) => {
             // Check if the *parent* of the element is currently showing an input
             if (element.parentNode && element.parentNode.querySelector('input')) return;

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

             // Store reference to the original element being replaced
             const originalElement = element;
             originalElement.replaceWith(input);
             input.focus();
             input.select();

             let isSaving = false; // Flag to prevent concurrent saves on blur/enter

             const saveChanges = async () => {
                 // If input is no longer in the DOM (e.g., parent removed), exit
                 if (!input.parentNode) {
                      // console.log("Tanuki Timestamp: Input parent node missing, aborting save.");
                      return false;
                 }
                 if (isSaving) return false; // Prevent re-entry
                 isSaving = true;

                 const newValue = input.value.trim();

                 // Validation
                 if (validationCallback && !(await validationCallback(newValue))) {
                     input.replaceWith(originalElement); // Revert on invalid input
                     isSaving = false;
                     return false; // Indicate save failed
                 }

                 // Check if value actually changed
                 const originalTimeSeconds = (inputClass === 'time-input') ? parseTime(originalElement.textContent) : null;
                 const hasChanged = (inputClass === 'time-input')
                    ? parseTime(newValue) !== originalTimeSeconds // Compare parsed seconds
                    : newValue !== (ts.note || ''); // Compare trimmed string note


                 if (hasChanged) {
                    try {
                        // Pass input & original element to callback, await its completion
                        await saveCallback(newValue, input, originalElement);
                        // Callback is now responsible for replacing input with originalElement
                    } catch (error) {
                         console.error("Tanuki Timestamp: Error during save callback:", error);
                         // Ensure replacement happens even on error in callback
                         if (input.parentNode) input.replaceWith(originalElement);
                    }
                 } else {
                     // Only replace if input is still in DOM
                     if (input.parentNode) input.replaceWith(originalElement);
                 }
                 isSaving = false;
                 return true; // Indicate save (or revert due to no change) succeeded
             };

             const handleBlur = async (e) => {
                 // Small delay to allow clicking other buttons within the item if needed
                 // Check if focus moved to another element within the *same item*
                 const relatedTarget = e.relatedTarget;
                 // Only save if focus moves outside the item, or to something non-interactive inside
                 if (!relatedTarget || !item.contains(relatedTarget) || !['BUTTON', 'INPUT', 'A'].includes(relatedTarget.tagName)) {
                    await saveChanges();
                 }
             };

             input.addEventListener('blur', (e) => setTimeout(() => handleBlur(e), 150)); // Increased delay slightly

             input.addEventListener('keydown', async (e) => {
                 if (e.key === 'Enter') {
                     e.preventDefault();
                     await saveChanges();
                 } else if (e.key === 'Escape') {
                     e.preventDefault();
                     // Check if input is still in DOM before replacing
                     if (input.parentNode) {
                        input.replaceWith(originalElement); // Cancel edit on Escape
                     }
                 }
             });
         };

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

                    // Update DB: Delete old, add new
                    await deleteTimestamp(currentVideoId, oldTime);
                    await saveTimestamp(currentVideoId, newTime, ts.note);

                    // Update internal state and UI element
                    ts.time = newTime;
                    item.dataset.time = newTime; // Update item's data attribute
                    originalDisplayElement.textContent = formatTime(newTime); // Update the original span's text
                    if (inputElement.parentNode) inputElement.replaceWith(originalDisplayElement); // Put the original span back
                    updateMarker(oldTime, newTime, ts.note); // Update progress marker

                    // Re-sort items in the manager list visually
                    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)); // Re-append in sorted order
                    }
                    showNotification(`Time updated to ${formatTime(newTime)}`);
                },
                async (newTimeString) => { // validationCallback (async)
                    const newTime = parseTime(newTimeString);
                    if (newTime === null || newTime < 0) {
                        showNotification('Invalid time format (HH:MM:SS)');
                        return false;
                    }
                    // Check if time already exists (and it's not the original time)
                    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; // Validation passed
                }
            );
        });

        // Edit Note (Double Click)
        noteEl.addEventListener('dblclick', () => {
            makeEditable(noteEl, 'note-input', ts.note || '', // Use actual note or empty string if placeholder
                async (newNote, inputElement, originalDisplayElement) => { // saveCallback
                    await saveTimestamp(currentVideoId, ts.time, newNote);

                    // Update internal state and UI element
                    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');
                    }
                    // Replace input with the updated original element
                    if (inputElement.parentNode) inputElement.replaceWith(originalDisplayElement);
                    updateMarker(ts.time, ts.time, newNote); // Update progress marker tooltip info
                    showNotification(`Note updated for ${formatTime(ts.time)}`);
                }
                // No specific validation needed for notes other than trimming which happens in saveChanges
            );
        });


        return item;
    }

    // --- Update existing item in manager (used after adding/saving via Note Input) --- (No changes needed)
    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; // Ensure data attribute is updated
    }


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

        // Wait for video metadata (readyState >= 1) and controls
        if (videoId && videoPlayer && videoPlayer.readyState >= 1 && controlsExist) {
            if (videoId !== currentVideoId) {
                // Video changed or first load for this video ID
                // console.log("Tanuki Timestamp: Video detected/changed - ", videoId);
                cleanupUI(); // Clean up previous UI if any
                currentVideoId = videoId;
                // Use timeout to ensure player is fully ready for UI injection
                setTimeout(setupUI, 500);
            } else if (!uiContainer && currentVideoId === videoId) {
                 // If video ID is the same but UI isn't there (e.g., navigating back/forth quickly, or initial load race condition)
                 // console.log("Tanuki Timestamp: Re-initializing UI for ", videoId);
                 cleanupUI(); // Clean up just in case parts exist
                 setTimeout(setupUI, 500); // Attempt to setup UI again
            }
        } else if (currentVideoId && (!videoId || !videoPlayer || !controlsExist)) { // More robust check for leaving video
            // Navigated away from a video page (or video element/controls removed)
            // console.log("Tanuki Timestamp: Navigated away or video/controls lost, cleaning up.");
            cleanupUI();
            currentVideoId = null;
        }
        // Keep checking even if video not found initially, YT navigation might load it later
    }, 1000); // Check every second

})();