🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

YouTube Timestamp Saver Ultimate

Save, organize, and navigate YouTube video timestamps with favorites | Made With 💖 by @AZXAD

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         YouTube Timestamp Saver Ultimate
// @namespace    http://tampermonkey.net/
// @version      4.4
// @description  Save, organize, and navigate YouTube video timestamps with favorites | Made With 💖 by @AZXAD
// @author       @AZXAD
// @license      CC-BY-NC-ND-4.0
// @match        https://www.youtube.com/*
// @grant        GM_setClipboard
// @grant        GM_addStyle
// ==/UserScript==

/*
 * YouTube Timestamp Saver Ultimate
 * Copyright (c) 2025 @AZXAD
 * 
 * Licensed under CC BY-NC-ND 4.0
 * https://creativecommons.org/licenses/by-nc-nd/4.0/
 */

(function() {
    'use strict';

    const AUTHOR = '@AZXAD';
    const WATERMARK = 'Made With 💖 by @AZXAD';
    const SCRIPT_URL = 'https://greasyfork.org/en/scripts/555619-youtube-timestamp-saver-ultimate';

    let timestamps = [];
    let currentVideoId = '';
    let markerContainer = null;
    let previewTooltip = null;
    let urlObserver = null;
    let markerUpdateInterval = null;
    let videoElement = null;
    let showOnlyFavorites = false;

    const storage = {
        getItem(key) {
            try {
                return localStorage.getItem(key);
            } catch (e) {
                console.error('localStorage get error:', e);
                return null;
            }
        },
        setItem(key, value) {
            try {
                localStorage.setItem(key, value);
                return true;
            } catch (e) {
                if (e.name === 'QuotaExceededError' || e.code === 22 || e.code === 1014) {
                    showNotification('⚠️ Storage full! Clear old timestamps or browser data.');
                } else {
                    showNotification('⚠️ Storage error. Check browser settings.');
                }
                console.error('localStorage set error:', e);
                return false;
            }
        },
        removeItem(key) {
            try {
                localStorage.removeItem(key);
            } catch (e) {
                console.error('localStorage remove error:', e);
            }
        }
    };

    GM_addStyle(`
        #timestamp-panel {
            position: fixed;
            top: 80px;
            right: 20px;
            width: 350px;
            max-height: 500px;
            background: #1f1f1f;
            border: 2px solid #3ea6ff;
            border-radius: 8px;
            color: white;
            font-family: Arial, sans-serif;
            z-index: 10000;
            display: none;
            overflow: hidden;
        }
        #timestamp-header {
            background: #3ea6ff;
            padding: 10px;
            font-weight: bold;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        #timestamp-list {
            max-height: 350px;
            overflow-y: auto;
            padding: 10px;
        }
        .timestamp-item {
            background: #2d2d2d;
            margin: 8px 0;
            padding: 8px;
            border-radius: 5px;
            font-size: 12px;
            position: relative;
            border: 2px solid transparent;
            transition: border-color 0.2s;
        }
        .timestamp-item.favorite {
            background: #3a2d1f;
            border: 2px solid #ffd700;
        }
        .timestamp-time {
            color: #3ea6ff;
            font-weight: bold;
            cursor: pointer;
        }
        .timestamp-time:hover {
            text-decoration: underline;
        }
        .timestamp-note {
            color: #aaa;
            margin-top: 5px;
            word-wrap: break-word;
        }
        .timestamp-actions {
            float: right;
            display: flex;
            gap: 5px;
        }
        .timestamp-favorite {
            color: #888;
            cursor: pointer;
            font-size: 16px;
            transition: color 0.2s;
        }
        .timestamp-favorite.active {
            color: #ffd700;
        }
        .timestamp-favorite:hover {
            color: #ffd700;
        }
        .timestamp-delete {
            color: #ff4444;
            cursor: pointer;
            font-size: 16px;
        }
        .timestamp-edit {
            color: #ffaa00;
            cursor: pointer;
            font-size: 14px;
        }
        #timestamp-controls {
            padding: 10px;
            background: #282828;
            display: flex;
            gap: 5px;
            justify-content: space-between;
            flex-wrap: wrap;
        }
        .control-btn {
            padding: 6px 12px;
            background: #3ea6ff;
            border: none;
            border-radius: 4px;
            color: white;
            cursor: pointer;
            font-size: 11px;
            flex: 1;
            min-width: 70px;
        }
        .control-btn:hover {
            background: #2d8fd8;
        }
        .control-btn.active {
            background: #ffd700;
            color: #000;
        }
        #close-panel {
            cursor: pointer;
            font-size: 20px;
        }
        #timestamp-footer {
            padding: 8px 10px;
            background: #1a1a1a;
            text-align: center;
            font-size: 10px;
            color: #888;
            border-top: 1px solid #333;
        }
        #timestamp-footer a {
            color: #3ea6ff;
            text-decoration: none;
        }
        #timestamp-counter {
            position: fixed;
            top: 70px;
            left: 20px;
            background: rgba(255, 0, 0, 0.9);
            color: white;
            padding: 8px 12px;
            border-radius: 20px;
            font-size: 13px;
            font-weight: bold;
            z-index: 9999;
            font-family: Arial, sans-serif;
            box-shadow: 0 2px 8px rgba(0,0,0,0.3);
            display: none;
        }
        #timestamp-counter.show {
            display: block;
        }
        .timestamp-marker-container {
            position: absolute;
            bottom: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: 31;
        }
        .timestamp-marker {
            position: absolute;
            bottom: 0;
            width: 8px;
            height: 8px;
            background: #ff0000;
            border-radius: 50%;
            transform: translateX(-50%);
            cursor: pointer;
            pointer-events: all;
            box-shadow: 0 0 4px rgba(255, 0, 0, 0.8);
            transition: all 0.2s ease;
        }
        .timestamp-marker.favorite {
            background: #ffd700;
            box-shadow: 0 0 6px rgba(255, 215, 0, 0.9);
        }
        .timestamp-marker:hover {
            width: 12px;
            height: 12px;
            background: #ff3333;
            box-shadow: 0 0 8px rgba(255, 0, 0, 1);
        }
        .timestamp-marker.favorite:hover {
            background: #ffed4e;
            box-shadow: 0 0 10px rgba(255, 215, 0, 1);
        }
        .timestamp-marker-tooltip {
            position: absolute;
            bottom: 12px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0, 0, 0, 0.9);
            color: white;
            padding: 4px 8px;
            border-radius: 4px;
            font-size: 11px;
            white-space: nowrap;
            display: none;
            z-index: 2000;
        }
        .timestamp-marker:hover .timestamp-marker-tooltip {
            display: block;
        }
        #seekbar-preview {
            position: absolute;
            bottom: 15px;
            background: rgba(0, 0, 0, 0.95);
            color: white;
            padding: 8px 12px;
            border-radius: 6px;
            font-size: 12px;
            white-space: nowrap;
            z-index: 2100;
            pointer-events: none;
            display: none;
            border: 1px solid #3ea6ff;
            max-width: 250px;
        }
        #seekbar-preview.show {
            display: block;
        }
        .preview-time {
            color: #3ea6ff;
            font-weight: bold;
        }
        .preview-note {
            color: #ccc;
            margin-top: 3px;
            font-size: 11px;
            white-space: normal;
        }
    `);

    function cleanup() {
        document.querySelectorAll('.timestamp-marker-container').forEach(el => el.remove());
        
        if (markerContainer && markerContainer.parentNode) {
            markerContainer.remove();
        }
        markerContainer = null;
        
        if (markerUpdateInterval) {
            clearInterval(markerUpdateInterval);
            markerUpdateInterval = null;
        }
        if (videoElement) {
            videoElement.removeEventListener('loadedmetadata', onVideoMetadataLoaded);
            videoElement = null;
        }
    }

    function formatTime(seconds) {
        if (!isFinite(seconds)) return '00:00:00';
        const hours = Math.floor(seconds / 3600);
        const minutes = Math.floor((seconds % 3600) / 60);
        const secs = Math.floor(seconds % 60);
        return [hours, minutes, secs].map(val => val.toString().padStart(2, '0')).join(':');
    }

    function getVideo() {
        return document.querySelector('video');
    }

    function getVideoId() {
        const urlParams = new URLSearchParams(window.location.search);
        const videoId = urlParams.get('v');
        if (!videoId && window.location.pathname.includes('/embed/')) {
            return window.location.pathname.split('/embed/')[1]?.split('?')[0];
        }
        return videoId;
    }

    function getVideoTitle() {
        const titleElement = document.querySelector('h1.ytd-video-primary-info-renderer') || 
                           document.querySelector('yt-formatted-string.style-scope.ytd-watch-metadata') ||
                           document.querySelector('#container h1');
        const title = titleElement ? titleElement.textContent.trim() : 'Unknown Video';
        return title.substring(0, 200);
    }

    function sanitizeHTML(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    function checkDuplicate(newTime) {
        const tolerance = 5;
        for (let item of timestamps) {
            if (Math.abs(item.time - newTime) < tolerance) {
                return item;
            }
        }
        return null;
    }

    function updateCounter() {
        let counter = document.getElementById('timestamp-counter');
        if (!counter) {
            counter = document.createElement('div');
            counter.id = 'timestamp-counter';
            counter.innerHTML = '📌 <span id="counter-value">0</span>';
            document.body.appendChild(counter);
        }
        
        const value = counter.querySelector('#counter-value');
        if (value) {
            const favCount = timestamps.filter(t => t.favorite).length;
            if (favCount > 0) {
                value.textContent = `${timestamps.length} (⭐${favCount})`;
            } else {
                value.textContent = timestamps.length;
            }
        }
        
        if (timestamps.length > 0) {
            counter.classList.add('show');
        } else {
            counter.classList.remove('show');
        }
    }

    function loadTimestamps() {
        const videoId = getVideoId();
        if (!videoId) {
            timestamps = [];
            updateCounter();
            return;
        }
        
        const saved = storage.getItem(`yt_timestamps_${videoId}`);
        if (saved) {
            try {
                timestamps = JSON.parse(saved);
                timestamps.sort((a, b) => a.time - b.time);
            } catch (e) {
                console.error('Error parsing timestamps:', e);
                timestamps = [];
            }
        } else {
            timestamps = [];
        }
        updateCounter();
    }

    function saveTimestamps() {
        const videoId = getVideoId();
        if (!videoId) return false;
        
        timestamps.sort((a, b) => a.time - b.time);
        
        const success = storage.setItem(`yt_timestamps_${videoId}`, JSON.stringify(timestamps));
        if (success) {
            updateCounter();
        }
        return success;
    }

    // Toggle favorite status
    function toggleFavorite(index) {
        if (index < 0 || index >= timestamps.length) return;
        
        timestamps[index].favorite = !timestamps[index].favorite;
        saveTimestamps();
        updatePanel();
        updateSeekbarMarkers();
        
        const status = timestamps[index].favorite ? '⭐ Favorited' : 'Removed from favorites';
        showNotification(status);
    }

    // Navigate to next timestamp
    function jumpToNextTimestamp() {
        const video = getVideo();
        if (!video || timestamps.length === 0) {
            showNotification('No timestamps available');
            return;
        }

        const currentTime = video.currentTime;
        const nextTimestamp = timestamps.find(t => t.time > currentTime + 1);
        
        if (nextTimestamp) {
            video.currentTime = nextTimestamp.time;
            const favIcon = nextTimestamp.favorite ? '⭐ ' : '';
            showNotification(`${favIcon}Next: ${formatTime(nextTimestamp.time)}`);
        } else {
            showNotification('Already at last timestamp');
        }
    }

    // Navigate to previous timestamp
    function jumpToPreviousTimestamp() {
        const video = getVideo();
        if (!video || timestamps.length === 0) {
            showNotification('No timestamps available');
            return;
        }

        const currentTime = video.currentTime;
        const reversedTimestamps = [...timestamps].reverse();
        const prevTimestamp = reversedTimestamps.find(t => t.time < currentTime - 1);
        
        if (prevTimestamp) {
            video.currentTime = prevTimestamp.time;
            const favIcon = prevTimestamp.favorite ? '⭐ ' : '';
            showNotification(`${favIcon}Previous: ${formatTime(prevTimestamp.time)}`);
        } else {
            showNotification('Already at first timestamp');
        }
    }

    function exportBackup() {
        const allData = {};
        let count = 0;
        
        try {
            for (let i = 0; i < localStorage.length; i++) {
                const key = localStorage.key(i);
                if (key && key.startsWith('yt_timestamps_')) {
                    const data = storage.getItem(key);
                    if (data) {
                        allData[key] = JSON.parse(data);
                        count++;
                    }
                }
            }
            
            allData['_metadata'] = {
                version: '4.3',
                author: AUTHOR,
                exported: new Date().toISOString(),
                credit: WATERMARK,
                scriptUrl: SCRIPT_URL
            };
        } catch (e) {
            showNotification('Error creating backup: ' + e.message);
            return;
        }
        
        const dataStr = JSON.stringify(allData, null, 2);
        const blob = new Blob([dataStr], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        
        const link = document.createElement('a');
        link.href = url;
        link.download = `youtube_timestamps_backup_${Date.now()}.json`;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        
        URL.revokeObjectURL(url);
        showNotification(`Backup downloaded! (${count} videos)`);
    }

    function importBackup() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';
        
        input.onchange = (e) => {
            const file = e.target.files[0];
            if (!file) return;
            
            if (file.size > 10 * 1024 * 1024) {
                showNotification('⚠️ File too large (max 10MB)');
                return;
            }
            
            const reader = new FileReader();
            reader.onload = (event) => {
                try {
                    const data = JSON.parse(event.target.result);
                    let count = 0;
                    let failed = 0;
                    
                    for (let key in data) {
                        if (key.startsWith('yt_timestamps_')) {
                            const success = storage.setItem(key, JSON.stringify(data[key]));
                            if (success) {
                                count++;
                            } else {
                                failed++;
                            }
                        }
                    }
                    
                    loadTimestamps();
                    updatePanel();
                    updateSeekbarMarkers();
                    
                    if (failed > 0) {
                        showNotification(`Imported ${count} videos. ${failed} failed (storage full?)`);
                    } else {
                        showNotification(`Imported ${count} video(s) successfully!`);
                    }
                } catch (err) {
                    showNotification('⚠️ Invalid backup file format');
                    console.error('Import error:', err);
                }
            };
            reader.readAsText(file);
        };
        
        input.click();
    }

    function createSeekbarPreview() {
        if (document.getElementById('seekbar-preview')) return;
        
        const preview = document.createElement('div');
        preview.id = 'seekbar-preview';
        document.body.appendChild(preview);
        previewTooltip = preview;
    }

    function showSeekbarPreview(mouseX, progressBar) {
        const video = getVideo();
        if (!video || !previewTooltip || timestamps.length === 0 || !isFinite(video.duration)) return;
        
        const rect = progressBar.getBoundingClientRect();
        const percentage = Math.max(0, Math.min(1, (mouseX - rect.left) / rect.width));
        const hoverTime = percentage * video.duration;
        
        const nearby = timestamps.filter(item => Math.abs(item.time - hoverTime) < 30)
            .sort((a, b) => Math.abs(a.time - hoverTime) - Math.abs(b.time - hoverTime))
            .slice(0, 3);
        
        if (nearby.length > 0) {
            let html = nearby.map(item => {
                const sanitizedNote = item.note ? sanitizeHTML(item.note) : '';
                const favIcon = item.favorite ? '⭐ ' : '';
                return `
                    <div class="preview-time">${favIcon}${formatTime(item.time)}</div>
                    ${sanitizedNote ? `<div class="preview-note">${sanitizedNote}</div>` : ''}
                `;
            }).join('<hr style="border: 1px solid #444; margin: 5px 0;">');
            
            previewTooltip.innerHTML = html;
            previewTooltip.style.left = Math.min(mouseX, window.innerWidth - 270) + 'px';
            previewTooltip.classList.add('show');
        } else {
            previewTooltip.classList.remove('show');
        }
    }

    function setupSeekbarHover() {
        const progressBar = document.querySelector('.ytp-progress-bar-container') ||
                          document.querySelector('.ytp-progress-bar');
        if (!progressBar) return;
        
        progressBar.addEventListener('mousemove', (e) => {
            showSeekbarPreview(e.clientX, progressBar);
        });
        
        progressBar.addEventListener('mouseleave', () => {
            if (previewTooltip) {
                previewTooltip.classList.remove('show');
            }
        });
    }

    function onVideoMetadataLoaded() {
        updateSeekbarMarkers();
    }

    function updateSeekbarMarkers() {
        const video = getVideo();
        if (!video) return;
        
        if (!isFinite(video.duration) || video.duration === 0) {
            if (videoElement !== video) {
                videoElement = video;
                video.addEventListener('loadedmetadata', onVideoMetadataLoaded, { once: true });
            }
            return;
        }

        const progressBar = document.querySelector('.ytp-progress-bar-container') || 
                          document.querySelector('.ytp-progress-bar') ||
                          document.querySelector('.ytp-progress-bar-padding');
        
        if (!progressBar) {
            setTimeout(updateSeekbarMarkers, 500);
            return;
        }

        if (markerContainer && markerContainer.parentNode) {
            markerContainer.remove();
            markerContainer = null;
        }

        markerContainer = document.createElement('div');
        markerContainer.className = 'timestamp-marker-container';
        progressBar.appendChild(markerContainer);

        timestamps.forEach((item, index) => {
            const percentage = (item.time / video.duration) * 100;
            if (percentage < 0 || percentage > 100) return;
            
            const marker = document.createElement('div');
            marker.className = 'timestamp-marker';
            if (item.favorite) {
                marker.classList.add('favorite');
            }
            marker.style.left = `${percentage}%`;
            marker.dataset.time = item.time;
            marker.dataset.index = index;

            const tooltip = document.createElement('div');
            tooltip.className = 'timestamp-marker-tooltip';
            const sanitizedNote = item.note ? sanitizeHTML(item.note) : '';
            const favIcon = item.favorite ? '⭐ ' : '';
            tooltip.textContent = sanitizedNote ? `${favIcon}${formatTime(item.time)} - ${item.note}` : `${favIcon}${formatTime(item.time)}`;
            marker.appendChild(tooltip);

            marker.addEventListener('click', (e) => {
                e.stopPropagation();
                video.currentTime = parseFloat(marker.dataset.time);
            });

            markerContainer.appendChild(marker);
        });
    }

    function createPanel() {
        const existingPanel = document.getElementById('timestamp-panel');
        if (existingPanel) {
            existingPanel.remove();
        }

        const panel = document.createElement('div');
        panel.id = 'timestamp-panel';
        panel.innerHTML = `
            <div id="timestamp-header">
                <span>Timestamps (${timestamps.length})</span>
                <span id="close-panel">×</span>
            </div>
            <div id="timestamp-list"></div>
            <div id="timestamp-controls">
                <button class="control-btn" id="filter-favorites-btn">⭐ Favorites</button>
                <button class="control-btn" id="clear-all">Clear All</button>
                <button class="control-btn" id="export-btn">Export</button>
                <button class="control-btn" id="backup-btn">💾 Backup</button>
                <button class="control-btn" id="import-btn">📁 Import</button>
            </div>
            <div id="timestamp-footer">${WATERMARK}</div>
        `;
        document.body.appendChild(panel);

        document.getElementById('close-panel').addEventListener('click', () => {
            panel.style.display = 'none';
        });

        document.getElementById('filter-favorites-btn').addEventListener('click', () => {
            showOnlyFavorites = !showOnlyFavorites;
            updatePanel();
            const btn = document.getElementById('filter-favorites-btn');
            if (showOnlyFavorites) {
                btn.classList.add('active');
            } else {
                btn.classList.remove('active');
            }
        });

        document.getElementById('clear-all').addEventListener('click', () => {
            if (confirm('Clear all timestamps for this video?')) {
                timestamps = [];
                saveTimestamps();
                updatePanel();
                updateSeekbarMarkers();
                showNotification('All timestamps cleared');
            }
        });

        document.getElementById('export-btn').addEventListener('click', exportTimestamps);
        document.getElementById('backup-btn').addEventListener('click', exportBackup);
        document.getElementById('import-btn').addEventListener('click', importBackup);
    }

    function updatePanel() {
        const list = document.getElementById('timestamp-list');
        if (!list) return;

        const header = document.querySelector('#timestamp-header span');
        if (header) {
            const favCount = timestamps.filter(t => t.favorite).length;
            if (showOnlyFavorites) {
                header.textContent = `Favorites (${favCount})`;
            } else {
                header.textContent = `Timestamps (${timestamps.length})`;
            }
        }

        const displayTimestamps = showOnlyFavorites ? 
            timestamps.filter(t => t.favorite) : 
            timestamps;

        if (displayTimestamps.length === 0) {
            const message = showOnlyFavorites ? 
                'No favorite timestamps' : 
                'No timestamps saved';
            list.innerHTML = `<div style="text-align: center; color: #aaa; padding: 20px;">${message}</div>`;
            return;
        }

        list.innerHTML = displayTimestamps.map((item, displayIndex) => {
            const realIndex = timestamps.indexOf(item);
            const sanitizedNote = item.note ? sanitizeHTML(item.note) : '';
            const favoriteClass = item.favorite ? 'favorite' : '';
            const favoriteIcon = item.favorite ? 'active' : '';
            
            return `
                <div class="timestamp-item ${favoriteClass}">
                    <div class="timestamp-actions">
                        <span class="timestamp-favorite ${favoriteIcon}" data-index="${realIndex}">★</span>
                        <span class="timestamp-edit" data-index="${realIndex}">✎</span>
                        <span class="timestamp-delete" data-index="${realIndex}">×</span>
                    </div>
                    <div class="timestamp-time" data-time="${item.time}">${formatTime(item.time)}</div>
                    ${sanitizedNote ? `<div class="timestamp-note">${sanitizedNote}</div>` : ''}
                </div>
            `;
        }).join('');

        list.querySelectorAll('.timestamp-time').forEach(el => {
            el.addEventListener('click', () => {
                const video = getVideo();
                if (video) {
                    video.currentTime = parseFloat(el.dataset.time);
                }
            });
        });

        list.querySelectorAll('.timestamp-favorite').forEach(el => {
            el.addEventListener('click', () => {
                const index = parseInt(el.dataset.index);
                toggleFavorite(index);
            });
        });

        list.querySelectorAll('.timestamp-delete').forEach(el => {
            el.addEventListener('click', () => {
                const index = parseInt(el.dataset.index);
                timestamps.splice(index, 1);
                saveTimestamps();
                updatePanel();
                updateSeekbarMarkers();
                showNotification('Timestamp deleted');
            });
        });

        list.querySelectorAll('.timestamp-edit').forEach(el => {
            el.addEventListener('click', () => {
                const index = parseInt(el.dataset.index);
                editTimestamp(index);
            });
        });
    }

    function editTimestamp(index) {
        if (index < 0 || index >= timestamps.length) return;
        
        const item = timestamps[index];
        const newNote = prompt(`Edit note for ${formatTime(item.time)}:`, item.note || '');
        
        if (newNote !== null) {
            timestamps[index].note = newNote.trim().substring(0, 500);
            saveTimestamps();
            updatePanel();
            updateSeekbarMarkers();
            showNotification('Timestamp updated!');
        }
    }

    function exportTimestamps() {
        if (timestamps.length === 0) {
            showNotification('No timestamps to export');
            return;
        }

        const videoTitle = getVideoTitle();
        const videoUrl = window.location.href.split('&')[0];
        
        let output = `Video: ${videoTitle}\nURL: ${videoUrl}\n\nTimestamps:\n`;
        output += timestamps.map(item => {
            const time = formatTime(item.time);
            const favIcon = item.favorite ? '⭐ ' : '';
            return item.note ? `${favIcon}${time} - ${item.note}` : `${favIcon}${time}`;
        }).join('\n');
        
        const favCount = timestamps.filter(t => t.favorite).length;
        if (favCount > 0) {
            output += `\n\nFavorites: ${favCount}`;
        }
        
        output += `\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
        output += `${WATERMARK}\n`;
        output += `\n📥 Get this script: ${SCRIPT_URL}\n`;
        output += `⭐ If you find this useful, please leave a review!\n`;
        output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`;

        GM_setClipboard(output);
        showNotification(`✅ Exported ${timestamps.length} timestamps!`);
    }

    function showNotification(message) {
        const notification = document.createElement('div');
        notification.textContent = message;
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background-color: rgba(0, 0, 0, 0.9);
            color: white;
            padding: 15px 20px;
            border-radius: 5px;
            z-index: 10001;
            font-size: 14px;
            font-family: Arial, sans-serif;
            border: 2px solid #3ea6ff;
        `;
        document.body.appendChild(notification);
        setTimeout(() => {
            if (notification.parentNode) {
                notification.remove();
            }
        }, 2500);
    }

    document.addEventListener('keydown', function(event) {
        const videoId = getVideoId();
        if (!videoId) return;

        if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') return;

        // Alt+X: Save timestamp
        if (event.altKey && event.key.toLowerCase() === 'x') {
            event.preventDefault();
            const video = getVideo();
            if (!video || !isFinite(video.currentTime)) return;

            const currentTime = video.currentTime;
            const duplicate = checkDuplicate(currentTime);
            
            if (duplicate) {
                const proceed = confirm(`Similar timestamp exists at ${formatTime(duplicate.time)}.\nAdd anyway?`);
                if (!proceed) return;
            }
            
            const note = prompt('Add a note (optional):');
            
            if (note !== null) {
                timestamps.push({
                    time: currentTime,
                    note: note.trim().substring(0, 500),
                    videoTitle: getVideoTitle(),
                    favorite: false
                });
                if (saveTimestamps()) {
                    updatePanel();
                    updateSeekbarMarkers();
                    showNotification(`Saved: ${formatTime(currentTime)}`);
                }
            }
        }

        // Alt+C: Export timestamps
        if (event.altKey && event.key.toLowerCase() === 'c') {
            event.preventDefault();
            exportTimestamps();
        }

        // Alt+Z: Toggle panel
        if (event.altKey && event.key.toLowerCase() === 'z') {
            event.preventDefault();
            const panel = document.getElementById('timestamp-panel');
            if (panel) {
                panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
                if (panel.style.display === 'block') {
                    updatePanel();
                }
            }
        }

        // Alt+D: Delete last timestamp
        if (event.altKey && event.key.toLowerCase() === 'd') {
            event.preventDefault();
            if (timestamps.length > 0) {
                const deleted = timestamps.pop();
                saveTimestamps();
                updatePanel();
                updateSeekbarMarkers();
                showNotification(`Deleted: ${formatTime(deleted.time)}`);
            }
        }

        // Alt+E: Edit last timestamp
        if (event.altKey && event.key.toLowerCase() === 'e') {
            event.preventDefault();
            if (timestamps.length > 0) {
                editTimestamp(timestamps.length - 1);
            } else {
                showNotification('No timestamps to edit');
            }
        }

        // Alt+N: Jump to next timestamp
        if (event.altKey && event.key.toLowerCase() === 'n') {
            event.preventDefault();
            jumpToNextTimestamp();
        }

        // Alt+M: Jump to previous timestamp
        if (event.altKey && event.key.toLowerCase() === 'm') {
            event.preventDefault();
            jumpToPreviousTimestamp();
        }
    });

    function init() {
        const videoId = getVideoId();
        
        if (videoId !== currentVideoId) {
            cleanup();
            currentVideoId = videoId;
            
            if (videoId) {
                loadTimestamps();
                createPanel();
                createSeekbarPreview();
                setupSeekbarHover();
                
                if (markerUpdateInterval) {
                    clearInterval(markerUpdateInterval);
                }
                markerUpdateInterval = setInterval(() => {
                    if (markerContainer && !document.contains(markerContainer)) {
                        updateSeekbarMarkers();
                    }
                }, 3000);
                
                setTimeout(() => {
                    const video = getVideo();
                    if (video) {
                        if (isFinite(video.duration) && video.duration > 0) {
                            updateSeekbarMarkers();
                        } else {
                            video.addEventListener('loadedmetadata', onVideoMetadataLoaded, { once: true });
                        }
                    }
                }, 500);
            }
        }
    }

    console.log('%c' + WATERMARK, 'color: #ff0000; font-size: 14px; font-weight: bold;');
    console.log('%cScript URL: ' + SCRIPT_URL, 'color: #3ea6ff; font-size: 12px;');

    let lastUrl = location.href;
    urlObserver = new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            init();
        }
    });
    
    urlObserver.observe(document, {
        subtree: true,
        childList: true
    });

    setTimeout(init, 2000);
    window.addEventListener('beforeunload', cleanup);
})();