KickSkip - Jump to Timestamps on Kick.com Videos

copy, paste, and jump to specific video timestamps effortlessly.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         KickSkip - Jump to Timestamps on Kick.com Videos
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  copy, paste, and jump to specific video timestamps effortlessly.
// @match        https://kick.com/*/videos/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const styles = `
        .timestamp-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            display: none;
            opacity: 0;
            justify-content: center;
            align-items: center;
            z-index: 10000;
            backdrop-filter: blur(2px);
        }

        .timestamp-overlay.visible {
            opacity: 1;
        }

        .timestamp-modal {
            background-color: #1a1a1a;
            border-radius: 12px;
            padding: 24px;
            width: 420px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.24);
            text-align: center;
            opacity: 0;
            transform: scale(0.95);
            transition: all 0.2s ease;
        }

        .timestamp-overlay.visible .timestamp-modal {
            opacity: 1;
            transform: scale(1);
        }

        .timestamp-header {
            position: relative;
            margin-bottom: 20px;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .timestamp-title {
            color: white;
            font-size: 18px;
            font-weight: 600;
            margin: 0;
            text-align: center;
        }

        .close-button {
            position: absolute;
            right: 0;
            top: 50%;
            transform: translateY(-50%);
            background: none;
            border: none;
            color: #777;
            font-size: 24px;
            cursor: pointer;
            padding: 0;
            line-height: 1;
        }

        .close-button:hover {
            color: #fff;
        }

        .timestamp-form {
            display: flex;
            flex-direction: column;
            gap: 16px;
            align-items: center;
        }

        .input-group {
            display: flex;
            gap: 8px;
            justify-content: center;
        }

        .time-input {
            background-color: #2a2a2a;
            border: 1px solid #3a3a3a;
            border-radius: 8px;
            color: white;
            padding: 12px;
            width: 70px;
            text-align: center;
            font-size: 16px;
        }

        .time-input:focus {
            outline: none;
            border-color: #666;
            background-color: #333;
        }

        .time-input::placeholder {
            color: #666;
        }

        .time-input:focus::placeholder {
            color: #888;
        }

        .action-buttons {
            display: flex;
            gap: 12px;
            margin-top: 8px;
            width: 100%;
        }

        .timestamp-button {
            flex: 1;
            padding: 12px;
            border-radius: 8px;
            border: none;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
        }

        .primary-button {
            background-color: #1db954;
            color: black;
            width: 100%;
        }

        .primary-button:hover {
            background-color: #1ed760;
            transform: translateY(-1px);
        }

        .secondary-button {
            background-color: #2a2a2a;
            color: white;
        }

        .secondary-button:hover {
            background-color: #3a3a3a;
            transform: translateY(-1px);
        }

        .button-row {
            display: flex;
            gap: 8px;
            width: 100%;
        }

        .button-row .timestamp-button {
            flex: 1;
        }

        #custom-timestamp-button {
            background: none;
            border: none;
            width: 32px;
            height: 32px;
            padding: 0;
            cursor: pointer;
            fill: white;
            position: relative;
        }

        #custom-timestamp-button .tooltip {
            display: none;
            position: absolute;
            bottom: 40px;
            left: 50%;
            transform: translateX(-50%);
            background-color: white;
            color: black;
            padding: 5px 10px;
            border-radius: 4px;
            font-size: 14px;
            font-weight: 600;
            white-space: nowrap;
            z-index: 10001;
        }

        #custom-timestamp-button:hover .tooltip {
            display: block;
        }

        .tooltip::after {
            content: "";
            position: absolute;
            top: 100%;
            left: 50%;
            margin-left: -5px;
            border-width: 5px;
            border-style: solid;
            border-color: white transparent transparent transparent;
        }

        .toast-message {
            position: fixed;
            bottom: 20px;
            left: 50%;
            transform: translateX(-50%) translateY(20px);
            background-color: #1db954; /* Reverted back to original color */
            color: black;
            padding: 12px 24px;
            border-radius: 8px;
            font-size: 14px;
            z-index: 10001;
            opacity: 0;
            transition: all 0.2s ease;
        }

        .toast-message.visible {
            opacity: 1;
            transform: translateX(-50%) translateY(0);
        }
    `;

   let toastElement;
let toastTimeout;

function showToast(message, duration = 1200) {
    // If a toast is currently visible, clear the timeout and remove the toast
    if (toastTimeout) {
        clearTimeout(toastTimeout);
    }
    if (toastElement) {
        toastElement.remove();
    }

    // Create and display the new toast message
    toastElement = document.createElement('div');
    toastElement.className = 'toast-message';
    toastElement.textContent = message;
    document.body.appendChild(toastElement);

    // Trigger reflow to ensure the animation plays
    toastElement.offsetHeight;
    toastElement.classList.add('visible');

    // Set a timeout to hide and remove the toast
    toastTimeout = setTimeout(() => {
        toastElement.classList.remove('visible');
        // Wait for the fade-out animation to complete before removing
        setTimeout(() => {
            toastElement.remove();
            toastElement = null; // Reset the toast element reference
        }, 300);
    }, duration);
}


    function formatTime(seconds) {
        const d = Math.floor(seconds / (24 * 3600));
        const h = Math.floor((seconds % (24 * 3600)) / 3600);
        const m = Math.floor((seconds % 3600) / 60);
        const s = Math.floor(seconds % 60);
        return {
            days: String(d).padStart(2, '0'),
            hours: String(h).padStart(2, '0'),
            minutes: String(m).padStart(2, '0'),
            seconds: String(s).padStart(2, '0')
        };
    }

    function parseTimestamp(str) {
        const match = str.match(/^(\d+:)?(\d+:)?(\d+:)?(\d+)$/);
        if (!match) return null;

        const parts = str.split(':').map(Number);
        let seconds = 0;
        if (parts.length === 4) {
            seconds = parts[0] * 24 * 3600 + parts[1] * 3600 + parts[2] * 60 + parts[3];
        } else if (parts.length === 3) {
            seconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
        } else if (parts.length === 2) {
            seconds = parts[0] * 60 + parts[1];
        } else {
            seconds = parts[0];
        }
        return seconds;
    }

    function createTimestampModal() {
        const modal = document.createElement('div');
        modal.innerHTML = `
            <div class="timestamp-overlay">
                <div class="timestamp-modal">
                    <div class="timestamp-header">
                        <h2 class="timestamp-title">Input Time</h2>
                        <button class="close-button">×</button>
                    </div>
                    <div class="timestamp-form">
                        <div class="input-group">
                            <input type="text" class="time-input" id="days" placeholder="DD" maxlength="2">
                            <input type="text" class="time-input" id="hours" placeholder="HH" maxlength="2">
                            <input type="text" class="time-input" id="minutes" placeholder="MM" maxlength="2">
                            <input type="text" class="time-input" id="seconds" placeholder="SS" maxlength="2">
                        </div>
                        <div class="button-row">
                            <button class="timestamp-button secondary-button" id="copy-current">Copy Current</button>
                            <button class="timestamp-button secondary-button" id="paste-timestamp">Paste</button>
                        </div>
                        <button class="timestamp-button primary-button" id="jump-to">Jump to Time</button>
                    </div>
                </div>
            </div>
        `;

        document.body.appendChild(modal);

        const overlay = document.querySelector('.timestamp-overlay');
        const closeButton = document.querySelector('.close-button');
        const jumpButton = document.getElementById('jump-to');
        const copyCurrentButton = document.getElementById('copy-current');
        const pasteButton = document.getElementById('paste-timestamp');
        const inputs = document.querySelectorAll('.time-input');

        function showModal() {
            overlay.style.display = 'flex';
            overlay.offsetHeight; // Trigger reflow
            overlay.classList.add('visible');
        }

        function hideModal() {
            overlay.classList.remove('visible');
            setTimeout(() => {
                overlay.style.display = 'none';
            }, 200);
        }

        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) {
                hideModal();
            }
        });

        closeButton.addEventListener('click', () => {
            hideModal();
        });

        inputs.forEach((input, index) => {
            input.addEventListener('input', (e) => {
                if (e.target.value.length === 2 && index < inputs.length - 1) {
                    inputs[index + 1].focus();
                }
            });

            input.addEventListener('keypress', (e) => {
                if (!/[0-9]/.test(e.key)) {
                    e.preventDefault();
                }
            });
        });

        jumpButton.addEventListener('click', () => {
            const videoPlayer = document.querySelector('video');
            if (!videoPlayer) return;

            const days = parseInt(document.getElementById('days').value) || 0;
            const hours = parseInt(document.getElementById('hours').value) || 0;
            const minutes = parseInt(document.getElementById('minutes').value) || 0;
            const seconds = parseInt(document.getElementById('seconds').value) || 0;

            const totalSeconds = (days * 24 * 3600) + (hours * 3600) + (minutes * 60) + seconds;
            if (totalSeconds >= 0) {
                videoPlayer.currentTime = totalSeconds;
                hideModal();
            }
        });

        copyCurrentButton.addEventListener('click', async () => {
            const videoPlayer = document.querySelector('video');
            if (!videoPlayer) return;

            const time = formatTime(videoPlayer.currentTime);
            const timeString = `${time.days}:${time.hours}:${time.minutes}:${time.seconds}`;

            try {
                await navigator.clipboard.writeText(timeString);
                showToast('Timestamp copied to clipboard');
            } catch (err) {
                showToast('Failed to copy timestamp');
            }
        });

        pasteButton.addEventListener('click', async () => {
            try {
                const text = await navigator.clipboard.readText();
                const seconds = parseTimestamp(text);

                if (seconds !== null) {
                    const time = formatTime(seconds);
                    document.getElementById('days').value = time.days;
                    document.getElementById('hours').value = time.hours;
                    document.getElementById('minutes').value = time.minutes;
                    document.getElementById('seconds').value = time.seconds;
                    showToast('Timestamp pasted');
                } else {
                    showToast('Invalid timestamp format');
                }
            } catch (err) {
                showToast('Failed to paste timestamp');
            }
        });

        return { overlay, showModal, hideModal };
    }

    function addTimestampButton() {
        const controlsContainer = document.querySelector('.z-controls .flex.flex-row.items-center.gap-2');
        if (controlsContainer && !document.getElementById('custom-timestamp-button')) {
            const button = document.createElement('button');
            button.id = 'custom-timestamp-button';
            button.innerHTML =
                `<img src="https://i.ibb.co/nP8sLjf/kickship.png" alt="Logo" style="width: 36px; height: 36px; margin: 0 auto; display: block;" />` +
                `<div class="tooltip">KickSkip</div>`;

            // Add styles for the button to align it correctly
            button.style.display = "flex";
            button.style.alignItems = "center";
            button.style.justifyContent = "center";
            button.style.width = "36px"; // Set the width of the button
            button.style.height = "36px"; // Set the height of the button
            button.style.margin = "0 4px"; // Adjust margin as needed

            button.addEventListener('click', function() {
                const { showModal } = createTimestampModal();
                showModal();
            });

            // Find the clip button using its SVG path and place the new button before it
            const clipButton = Array.from(controlsContainer.querySelectorAll('button')).find(button =>
                button.querySelector('svg path[d="M1.82739 7.28856L27.0598 1.71777L28.2433 7.07867L3.01097 12.6495L1.82739 7.28856ZM3.03003 28.9699V13.6999H28.96V28.9699H3.03003ZM19.98 21.3299L13.47 16.7299V25.9299L19.98 21.3299Z"]')
            );

            if (clipButton) {
                controlsContainer.insertBefore(button, clipButton);
            } else {
                controlsContainer.appendChild(button); // Fallback if clip button not found
            }
        }
    }

    // Add styles
    const styleElement = document.createElement('style');
    styleElement.textContent = styles;
    document.head.appendChild(styleElement);

    // Observe changes in the controls container
    const observer = new MutationObserver((mutations, obs) => {
        addTimestampButton();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // Initial call to add the button if controls are already loaded
    addTimestampButton();

})();