YouTube Undo Seek

Undos and redos seek actions on YouTube with Ctrl+Z and Ctrl+Y.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name                YouTube Undo Seek
// @icon                https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @author              ElectroKnight22
// @namespace           electroknight22_youtube_undo_seek_namespace
// @version             1.1.0
// @match               *://www.youtube.com/*
// @match               *://m.youtube.com/*
// @match               *://www.youtube-nocookie.com/*
// @exclude             *://www.youtube.com/live_chat*
// @require             https://update.greasyfork.org/scripts/549881/1841778/YouTube%20Helper%20API.js
// @run-at              document-idle
// @grant               none
// @inject-into         page
// @license             MIT
// @description         Undos and redos seek actions on YouTube with Ctrl+Z and Ctrl+Y.
// ==/UserScript==

/*jshint esversion: 11 */
/* global youtubeHelperApi */

(function () {
    'use strict';

    const api = youtubeHelperApi;
    if (!api) return console.error('[YouTube Undo Seek] Helper API not found.');

    let undoStack = [];
    let redoStack = [];
    let lastSeekEventTime = 0;
    let isNavigationLocked = false;

    const performNavigation = (sourceStack, destinationStack) => {
        if (!sourceStack.length || api.video.isCurrentlyLive || api.player.isPlayingAds) return;

        destinationStack.push(api.apiProxy.getCurrentTime());

        isNavigationLocked = true;
        api.apiProxy.seekTo(sourceStack.pop(), true);

        api.eventTarget.addEventListener(
            api.EVENTS.VIDEO_SEEKED,
            () => {
                isNavigationLocked = false;
                lastSeekEventTime = 0;
            },
            { once: true },
        );
    };

    const handleSeekingEvent = () => {
        if (isNavigationLocked || api.player.isPlayingAds || api.video.isCurrentlyLive) return;

        const now = Date.now();
        if (now - lastSeekEventTime < 1000) {
            lastSeekEventTime = now;
            return;
        }

        undoStack.push(api.video.realCurrentProgress);
        redoStack.length = 0;
        lastSeekEventTime = now;

        if (undoStack.length > 30) undoStack.shift();
    };

    document.addEventListener('keydown', (event) => {
        if (!event.ctrlKey) return;

        const key = event.key.toLowerCase();
        if (key !== 'z' && key !== 'y') return;

        const target = event.target;
        if (['INPUT', 'TEXTAREA'].includes(target.tagName) || target.isContentEditable) return;

        event.preventDefault();

        if (key === 'z') performNavigation(undoStack, redoStack);
        if (key === 'y') performNavigation(redoStack, undoStack);
    });

    api.eventTarget.addEventListener(api.EVENTS.API_READY, () => {
        undoStack = [];
        redoStack = [];
        lastSeekEventTime = 0;
    });
    
    api.eventTarget.addEventListener(api.EVENTS.VIDEO_SEEKING, handleSeekingEvent);
})();