YouTube Timestamp Tool

Add, manage, and delete timestamps on YouTube videos with local storage capabilities so your timestamps are saved (temporarily)!

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

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

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         YouTube Timestamp Tool
// @namespace    http://tampermonkey.net/
// @version      1.24
// @description  Add, manage, and delete timestamps on YouTube videos with local storage capabilities so your timestamps are saved (temporarily)!
// @match        https://www.youtube.com/*
// @author       Modern Disappointment
// @license      Custom License: Permission from the original creator (krazete) required for distribution. Credit must be given, and the original work should be linked. No commercial use without permission.
// ==/UserScript==
// This script is based on a project created by krazete, originally posted to r/VirtualYoutubers.
// Credit: krazete
// Original Post: https://redd.it/ifzqe7
// Maintained Source Code: https://github.com/Krazete/bookmarklets/blob/master/ytlivestamper.js

(function() {
    'use strict';

    let previousVideoId = null;

    function getVideoId() {
        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);

        const formattedH = h > 0 ? String(h).padStart(2, '0') + ':' : '';
        const formattedM = String(m).padStart(2, '0');
        const formattedS = String(s).padStart(2, '0');

        return h > 0 ? `${formattedH}${formattedM}:${formattedS}` : `${formattedM}:${formattedS}`;
    }

    function setTimestamp(element, time) {
        element.textContent = formatTime(time);
        element.dataset.time = time;
        element.href = `https://youtu.be/${getVideoId()}?t=${time}`;
    }

    function handleTimestampClick(e) {
        if (e.target.dataset.time) {
            e.preventDefault();
            document.querySelector("video").currentTime = e.target.dataset.time;
        } else if (e.target.dataset.increment) {
            e.preventDefault();
            const timestampElement = e.target.parentElement.querySelector('a');
            const newTime = parseInt(timestampElement.dataset.time) + parseInt(e.target.dataset.increment);
            setTimestamp(timestampElement, newTime);
            saveTimestamps();
        } else if (e.target.dataset.delete) {
            e.preventDefault();
            e.target.parentElement.remove();
            saveTimestamps();
        }
    }

    function createTimestampElement(time) {
        const li = document.createElement("li");
        const decreaseBtn = document.createElement("span");
        const increaseBtn = document.createElement("span");
        const timeLink = document.createElement("a");
        const descInput = document.createElement("input");
        const deleteBtn = document.createElement("button");

        decreaseBtn.textContent = "➖";
        decreaseBtn.dataset.increment = -1;
        increaseBtn.textContent = "➕";
        increaseBtn.dataset.increment = 1;
        deleteBtn.textContent = "🗑️";
        deleteBtn.dataset.delete = 1;

        setTimestamp(timeLink, time);

        li.appendChild(decreaseBtn);
        li.appendChild(increaseBtn);
        li.appendChild(deleteBtn);
        li.appendChild(timeLink);
        li.appendChild(document.createTextNode(' '));
        li.appendChild(descInput);

        return li;
    }

    function copyTimestamps() {
        if (!previousVideoId) return;
        let output = '';
        let hasTimestamps = false;
        document.querySelectorAll('#timestamp-list li').forEach((li) => {
            const time = li.querySelector('a').textContent;
            const desc = li.querySelector('input').value;
            if (time.trim() || desc.trim()) {
                hasTimestamps = true;
                output += `${time} ${desc}\n`;
            }
        });
        if (hasTimestamps) {
            const videoUrl = `https://youtu.be/${previousVideoId}`;
            navigator.clipboard.writeText(`${output.trim()}\n${videoUrl}`);
        }
    }

    function saveTimestamps() {
        const timestamps = [];
        document.querySelectorAll('#timestamp-list li').forEach(li => {
            const time = li.querySelector('a').dataset.time;
            const desc = li.querySelector('input').value;
            if (time.trim() || desc.trim()) {
                timestamps.push({ time, desc });
            }
        });
        if (timestamps.length > 0) {
            localStorage.setItem('yt-timestamps-' + getVideoId(), JSON.stringify(timestamps));
        } else {
            localStorage.removeItem('yt-timestamps-' + getVideoId());
        }
    }

    function loadTimestamps() {
        const videoId = getVideoId();
        const savedTimestamps = JSON.parse(localStorage.getItem('yt-timestamps-' + videoId) || '[]');
        const list = document.querySelector('#timestamp-list');
        list.textContent = '';
        savedTimestamps.forEach(ts => {
            const li = createTimestampElement(ts.time);
            li.querySelector('input').value = ts.desc;
            list.appendChild(li);
        });
    }

    function initTool() {
        if (!document.querySelector("#ytls-pane")) {
            const pane = document.createElement("div");
            const closeBtn = document.createElement("span");
            const list = document.createElement("ul");
            const textarea = document.createElement("textarea");
            const buttonsDiv = document.createElement("div");
            const importBtn = document.createElement("button");
            const addBtn = document.createElement("button");
            const copyBtn = document.createElement("button");
            const style = document.createElement("style");

            pane.id = "ytls-pane";
            closeBtn.textContent = "x";
            closeBtn.addEventListener("click", () => {
                if (confirm("Close timestamp tool?\n!! This will clear the timestamps for this video/stream from your local storage !!")) {
                    localStorage.removeItem('yt-timestamps-' + getVideoId());
                    pane.remove();
                    window.removeEventListener("beforeunload", handleBeforeUnload);
                }
            });

            list.id = "timestamp-list";
            list.addEventListener("click", handleTimestampClick);
            list.addEventListener("touchstart", handleTimestampClick);

            textarea.id = "ytls-box";
            textarea.style.display = 'none';

            importBtn.textContent = "Import List";
            importBtn.addEventListener("click", () => {
                if (textarea.style.display === 'none') {
                    textarea.style.display = 'block';
                    textarea.focus();
                } else {
                    if (textarea.value.trim() === '') {
                        alert('The import list is empty.');
                        textarea.style.display = 'none';
                        return;
                    }

                    if (confirm('Are you sure you want to overwrite the existing timestamps?')) {
                        const timestamps = textarea.value.split("\n").map(line => {
                            const [time, ...desc] = line.split(' ');
                            const parts = time.split(":").map(part => parseInt(part));
                            const timeInSeconds = parts.length === 3 ? parts[0] * 3600 + parts[1] * 60 + parts[2] : parts[0] * 60 + parts[1];
                            return { time: timeInSeconds, desc: desc.join(' ') };
                        });
                        list.textContent = '';
                        timestamps.forEach(ts => {
                            const li = createTimestampElement(ts.time);
                            li.querySelector('input').value = ts.desc;
                            list.appendChild(li);
                        });
                        saveTimestamps();
                        textarea.style.display = 'none';
                    }
                }
            });

            addBtn.textContent = "Add Timestamp";
            addBtn.addEventListener("click", () => {
                const currentTime = Math.max(0, Math.floor(document.querySelector("video").currentTime));
                const li = createTimestampElement(currentTime);
                list.appendChild(li);
                li.querySelector('input').focus();
                saveTimestamps();
            });

            copyBtn.textContent = "Copy List";
            copyBtn.addEventListener("click", copyTimestamps);

            buttonsDiv.id = "ytls-buttons";
            buttonsDiv.appendChild(addBtn);
            buttonsDiv.appendChild(copyBtn);
            buttonsDiv.appendChild(importBtn);

            style.textContent = `
                #ytls-pane {
                    background: rgba(0,0,0,.55);
                    text-align: left;
                    position: fixed;
                    bottom: 0;
                    left: 0;
                    padding: 5px;
                    opacity: .60;
                    z-index: 5000;
                    cursor: move;
                    width: 400px;
                    height: 200px;
                    max-height: 50vh;
                    overflow: hidden;
                }
                #ytls-pane:hover { opacity: 1; }
                #ytls-pane span { cursor: pointer; }
                #ytls-pane ul {
                    list-style: none;
                    margin: 0;
                    padding: 0;
                    max-height: calc(100% - 40px);
                    overflow-y: auto;
                }
                #ytls-pane span, #ytls-pane a, #ytls-pane input {
                    background: none;
                    color: white;
                    font-family: inherit;
                    font-size: initial;
                    text-decoration: none;
                    border: none;
                    outline: none;
                }
                #ytls-box {
                    font-family: monospace;
                    width: 100%;
                    display: block;
                    padding: 5px;
                    border: none;
                    outline: none;
                    resize: none;
                    height: 40px;
                    overflow-y: auto;
                    display: none;
                }
                #ytls-buttons {
                    display: flex;
                    gap: 10px;
                    position: absolute;
                    bottom: 0;
                    left: 0;
                    width: 100%;
                }
                #ytls-buttons button {
                    background: transparent;
                    color: white;
                    font-size: 12px;
                    flex: auto;
                    padding: 2px;
                    border: 1px solid white;
                }
                #ytls-pane li {
                    display: flex;
                    align-items: center;
                    gap: 5px;
                    margin: 2px 0;
                }
                #ytls-pane a {
                    margin-right: 5px;
                }
            `;

            pane.appendChild(closeBtn);
            pane.appendChild(list);
            pane.appendChild(textarea);
            pane.appendChild(buttonsDiv);
            pane.appendChild(style);

            document.body.appendChild(pane);

            let isDragging = false;
            let startX, startY, startLeft, startTop;

            pane.addEventListener('mousedown', (e) => {
                if (e.target === closeBtn) return;
                isDragging = true;
                startX = e.clientX;
                startY = e.clientY;
                startLeft = parseInt(window.getComputedStyle(pane).left, 10) || 0;
                startTop = parseInt(window.getComputedStyle(pane).top, 10) || 0;
                document.addEventListener('mousemove', onMouseMove);
                document.addEventListener('mouseup', onMouseUp);
            });

            function onMouseMove(e) {
                if (!isDragging) return;
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                pane.style.left = `${Math.min(window.innerWidth - pane.offsetWidth, Math.max(0, startLeft + dx))}px`;
                pane.style.top = `${Math.min(window.innerHeight - pane.offsetHeight, Math.max(0, startTop + dy))}px`;
            }

            function onMouseUp() {
                isDragging = false;
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
            }

            function handleFullscreenChange() {
                if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement) {
                    pane.style.display = 'none';
                } else {
                    pane.style.display = 'block';
                }
            }

            document.addEventListener("fullscreenchange", handleFullscreenChange);
            document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
            document.addEventListener("mozfullscreenchange", handleFullscreenChange);
            document.addEventListener("MSFullscreenChange", handleFullscreenChange);
        }

        loadTimestamps();
    }

    function handleBeforeUnload(e) {
        if (document.querySelector("#ytls-pane")) {
            copyTimestamps();
            e.preventDefault();
            e.returnValue = "Close timestamp tool?\n!! This will clear the timestamps for this video/stream from your local storage !!";
            return e.returnValue;
        }
    }

    function handleNavigation() {
        if (location.pathname === '/watch') {
            const videoId = getVideoId();
            if (previousVideoId && previousVideoId !== videoId) {
                copyTimestamps();
            }
            previousVideoId = videoId;
            initTool();
        } else {
            if (document.querySelector("#ytls-pane")) {
                document.querySelector("#ytls-pane").style.display = "none";
            }
        }
    }
    window.addEventListener("yt-navigate-finish", handleNavigation);
    window.addEventListener("beforeunload", handleBeforeUnload);
    handleNavigation();
})();