asteriksme / Better Youtube

// ==UserScript==
// @name         Better Youtube
// @namespace    http://tampermonkey.net/
// @version      0.17
// @description  Better Youtube
// @author       You
// @match        https://www.youtube.com/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// @license      MIT
// @updateURL    https://openuserjs.org/meta/asteriksme/Better_Youtube.meta.js
// @downloadURL  https://openuserjs.org/install/asteriksme/Better_Youtube.user.js
// ==/UserScript==

/* jshint esversion: 11 */

(function() {
    'use strict';

    const getRelativeTimeString = (date, lang = navigator.language) => {
        const timeMs = typeof date === "number" ? date : date.getTime();
        const deltaSeconds = Math.round((timeMs - Date.now()) / 1000);
        const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity];
        const units = ["second", "minute", "hour", "day", "week", "month", "year"];
        const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(deltaSeconds));
        const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;
        const rtf = new Intl.RelativeTimeFormat(lang, { numeric: "auto" });
        return rtf.format(Math.floor(deltaSeconds / divisor), units[unitIndex]);
    };

    let running = false;
    const addTimestamps = async () => {
        if (running || !document.querySelector(".ytp-endscreen-content a .ytp-videowall-still-info-author")) {
            return;
        }
        running = true;
        await Promise.all(
            document.querySelector(".ytp-endscreen-content").querySelectorAll("a:not([data-timestamp-added])").map((tag) =>
                fetch(tag.href, {
                    method: 'get',
                }).then((response) => response.text()).then((res) => {
                    const date = new Date(res.match(/publishDate":"([^"]*)"/)[1]);
                    tag.querySelector(".ytp-videowall-still-info-author").innerHTML += ` • ${date.toLocaleDateString()} (${getRelativeTimeString(date)})`;
                    tag.setAttribute("data-timestamp-added", "");
                })
            )
        );
        running = false;
    };

    const changeVideosPerRow = (changes, observer) => {
        const MIN_SIZE = 5;
        const renderers = "ytd-rich-item-renderer, .video_card";
        const itemsPerRow = "--ytd-rich-grid-items-per-row";
        document.querySelectorAll(renderers).forEach((renderer) => {
            if (parseInt(getComputedStyle(renderer).getPropertyValue(itemsPerRow), 10) < MIN_SIZE) {
                renderer.style.setProperty(itemsPerRow, MIN_SIZE.toString());
            }
        });
    };

    const removeUselessSection = (changes, observer) => {
        const renderer = "ytd-rich-section-renderer";
        const sections = document.querySelectorAll(renderer);
        if (sections.length < 2) {
            return;
        }
        sections.forEach((section, index) => index > 0 && setTimeout(() => section.remove()));
    };

    const removeChannelTrailer = (changes, observer) => {
        document.querySelector('[role="main"][page-subtype="channels"] ytd-channel-video-player-renderer video')?.remove();
    };

    (new MutationObserver(addTimestamps)).observe(document, {childList: true, subtree: true});
    (new MutationObserver(changeVideosPerRow)).observe(document, {childList: true, subtree: true});
    (new MutationObserver(removeUselessSection)).observe(document, {childList: true, subtree: true});
    (new MutationObserver(removeChannelTrailer)).observe(document, {childList: true, subtree: true});


    let hideWatched = false;
    let scheduled = false;

    const ICON_EYE = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
  <path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8M1.173 8a13 13 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5s3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5s-3.879-1.168-5.168-2.457A13 13 0 0 1 1.172 8z"/>
  <path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5M4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0"/>
</svg>`;
    const ICON_EYE_SLASH = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye-slash" viewBox="0 0 16 16">
  <path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7 7 0 0 0-2.79.588l.77.771A6 6 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755q-.247.248-.517.486z"/>
  <path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829"/>
  <path d="M3.35 5.47q-.27.24-.518.487A13 13 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7 7 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884-12-12 .708-.708 12 12z"/>
</svg>`;

    const htmlShowWatched = `${ICON_EYE_SLASH}<span style="margin-left:6px;font-size:12px;">Hide watched</span>`;
    const htmlHideWatched = `${ICON_EYE}<span style="margin-left:6px;font-size:12px;">Show watched</span>`;

    function ensureToggleButton() {
        if (document.getElementById('by-hide-watched-btn')) return;
        const btn = document.createElement('button');
        btn.id = 'by-hide-watched-btn';
        btn.type = 'button';
        btn.innerHTML = htmlShowWatched;
        btn.title = 'Hide watched videos';
        Object.assign(btn.style, {
            position: 'fixed',
            display: 'flex',
            gap: 4,
            alignItems: 'center',
            top: '13px',
            right: '245px',
            zIndex: 999999,
            fontSize: '18px',
            borderRadius: '6px',
            padding: '6px 10px',
            border: '1px solid #666',
            background: '#222',
            color: '#fff',
            cursor: 'pointer',
            opacity: '0.85'
        });
        btn.addEventListener('mouseenter', () => { btn.style.opacity = '1'; });
        btn.addEventListener('mouseleave', () => { btn.style.opacity = '0.85'; });
        btn.addEventListener('click', () => {
            hideWatched = !hideWatched;
            btn.innerHTML = hideWatched ? htmlHideWatched : htmlShowWatched;
            btn.title = hideWatched ? 'Show watched videos' : 'Hide watched videos';
            process();
        });
        document.body.appendChild(btn);
    }

    function tagAndStyleWatched() {
        const MIN_PROGRESS_PERCENT = 15;

        document.querySelectorAll('yt-thumbnail-overlay-progress-bar-view-model, ytd-thumbnail-overlay-resume-playback-renderer').forEach(pb => {
            const seg = pb.querySelector('.ytThumbnailOverlayProgressBarHostWatchedProgressBarSegment, .ytd-thumbnail-overlay-resume-playback-renderer');
            let qualifies = false;
            if (seg) {
                const styleAttr = seg.getAttribute('style') || '';
                const m = styleAttr.match(/width\s*:\s*([\d.]+)%/i);
                if (m) {
                    const pct = parseFloat(m[1]);
                    if (!isNaN(pct) && pct >= MIN_PROGRESS_PERCENT) {
                        qualifies = true;
                    }
                }
            }

            const thumbVM = pb.closest('yt-thumbnail-view-model');
            const rich = pb.closest('ytd-rich-item-renderer');
            if (!rich) return;

            if (qualifies) {
                rich.setAttribute('data-better-youtube-watched', '1');
                if (thumbVM) {
                    const img = thumbVM.querySelector('.ytThumbnailViewModelImage');
                    if (img && !img.dataset._betterYoutubeStyled) {
                        img.style.filter = 'grayscale(100%) contrast(50%)';
                        img.dataset._betterYoutubeStyled = '1';
                    }
                }
            } else {
                rich.removeAttribute('data-better-youtube-watched');
                if (thumbVM) {
                    const img = thumbVM.querySelector('.ytThumbnailViewModelImage');
                    if (img && img.dataset._betterYoutubeStyled) {
                        img.style.filter = '';
                        delete img.dataset._betterYoutubeStyled;
                    }
                }
            }
        });
    }

    function applyHideShow() {
        const watched = document.querySelectorAll('ytd-rich-item-renderer[data-better-youtube-watched="1"]');
        watched.forEach(rich => {
            rich.style.display = hideWatched ? 'none' : '';
        });
    }

    function removeFirstColumnAttr() {
        document.querySelectorAll('ytd-rich-item-renderer[is-in-first-column]').forEach(el => {
            el.removeAttribute('is-in-first-column');
        });
    }

    function process() {
        tagAndStyleWatched();
        removeFirstColumnAttr();
        applyHideShow();
    }

    function scheduleProcess() {
        if (scheduled) return;
        scheduled = true;
        requestAnimationFrame(() => {
            scheduled = false;
            process();
        });
    }

    const observer = new MutationObserver(scheduleProcess);
    observer.observe(document, { childList: true, subtree: true });

    const init = () => {
        ensureToggleButton();
        process();
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    setInterval(removeFirstColumnAttr, 5000);
})();