YouTube Timestamp Tool

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

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

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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();
})();