☰

R34 Viewed Thumbs Border

Marks viewed thumbnails on rule34video.com, stores viewed video IDs, and highlights selected thumb titles

スクリプトをインストールするには、Tampermonkey, Greasemonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、 Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Userscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら、通報はこちらへお寄せください
// ==UserScript==
// @name         R34 Viewed Thumbs Border
// @namespace    http://tampermonkey.net/
// @version      1.46
// @description  Marks viewed thumbnails on rule34video.com, stores viewed video IDs, and highlights selected thumb titles
// @author       You
// @match        https://rule34video.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_addValueChangeListener
// @run-at       document-start
// @icon         https://www.google.com/s2/favicons?sz=64&domain=rule34video.com
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'r34_viewed_thumbs_v1';
    const STORAGE_KEY_FAR = 'r34_viewed_thumbs_far_v1';
    const STORAGE_KEY_LAST_ID = 'r34_last_viewed_far_id_v1';
    const PROCESS_DELAY = 120;
    const MIN_PAGE_FOR_DATE_HIGHLIGHT = 100;
    const MIN_PAGE_FOR_LAST_ID = 300;
    const THUMB_SELECTOR = '.item.ma-v, .th, .item.thumb, .thumb:not(img)';
    const WRAP_SELECTOR = '.ma-thumb, .img.wrap_image';
    const VIDEO_LINK_SELECTOR = 'a[href*="/video/"]';
    const LINK_SELECTOR = `${THUMB_SELECTOR} ${VIDEO_LINK_SELECTOR}`;
    const TITLE_SELECTOR = '.ma-v__t, .thumb_title';
    const DATE_SELECTOR = '.video-card-meta__date, .ma-v__m span:nth-child(2)';
    const PAGINATION_ACTIVE_SELECTOR = '.pagination .item.active, .fs-pages .is-cur, .fs-pages [aria-current="page"]';
    const VIEWED_ATTR = 'data-r34-viewed-thumb';
    const FLAGGED_TITLE_ATTR = 'data-r34-flagged-title';
    const VIEWED_DATE_ATTR = 'data-r34-viewed-date';
    const TITLE_HIGHLIGHT_KEYWORDS = `
        // Видео, музыка и монтаж
        pmv
        amv
        hmv
        edit
        clip

        // Сборники и подборки
        compilation
        comp
        collection
        gallery
        best
        hottest

        // Локализация и субтитры
        rus
        subtitles
        legendado
        spanish
        portuguese
        brazilian
        br
        viet

        // Тематика и жанры
        feet
        thigh
        joi
        qos
        goon
        story
        game
        ai
        test
    `;
    const TITLE_HIGHLIGHT_BLACKLIST = `
        sound edit
    `;
    const TITLE_HIGHLIGHT_MATCHER = createTitleHighlightMatcher(TITLE_HIGHLIGHT_KEYWORDS);
    const TITLE_HIGHLIGHT_BLACKLIST_MATCHER = createTitleHighlightMatcher(TITLE_HIGHLIGHT_BLACKLIST);
    const VIDEO_PATH_PATTERN = /\/video\/(\d+)(?:\/|$)/;
    const VIEWED_STYLE = `
        .wrap_image[${VIEWED_ATTR}="1"],
        .ma-thumb[${VIEWED_ATTR}="1"]{
            border: 4px solid rgb(141, 233, 182);
            box-sizing: border-box;
        }

        .thumb_title[${FLAGGED_TITLE_ATTR}="1"],
        .ma-v__t[${FLAGGED_TITLE_ATTR}="1"]{
            color: #d31717;
            text-decoration: 4px underline rgb(255 0 0 / 75%);
        }

        .video-card-meta__date[${VIEWED_DATE_ATTR}="1"],
        .ma-v__m span[${VIEWED_DATE_ATTR}="1"]{
            color: rgb(141, 233, 182);
        }

        .video-card-meta__date[${VIEWED_DATE_ATTR}="1"] svg,
        .ma-v__m span[${VIEWED_DATE_ATTR}="1"] svg{
            fill: rgb(141, 233, 182);
            color: rgb(141, 233, 182);
        }

        .r34-far-date-tooltip {
            position: fixed;
            z-index: 999999;
            pointer-events: none;
            background: rgba(18, 18, 24, 0.95);
            color: rgb(141, 233, 182);
            border: 1px solid rgb(141, 233, 182);
            border-radius: 4px;
            padding: 3px 8px;
            font-size: 12px;
            font-family: sans-serif;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
            white-space: nowrap;
        }

        .r34-last-viewed-tracker {
            position: absolute;
            top: 1355px;
            left: 999px;
            z-index: 9999;
            color: #8d95a5;
            font-size: 14px;
            font-weight: 500;
            font-family: inherit;
            padding: 4px 10px;
            border-radius: 4px;
            box-sizing: border-box;
            white-space: nowrap;
        }
    `;

    let processTimer = null;
    let pendingRoots = new Set();
    let viewedMap = loadViewedMap();
    let viewedFarMap = loadViewedFarMap();
    let lastFarVideoId = readStoredLastId();
    let lastUrl = location.href;
    let domObserver = null;
    let observedBody = null;
    let ajaxHooksInstalled = false;
    let lazyloadHookInstalled = false;
    let tooltipElement = null;
    let lastPointerPos = { x: 0, y: 0 };
    let currentHoveredDate = null;

    // Нормализация объекта
    function isPlainObject(value) {
        return Object.prototype.toString.call(value) === '[object Object]';
    }

    // Экранирование спецсимволов
    function escapeRegExp(value) {
        return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    // Нормализация ключевых слов
    function normalizeTitleHighlightKeywords(source) {
        const rawItems = Array.isArray(source)
            ? source
            : String(source || '')
                .split('\n')
                .map((line) => {
                    const idx = line.search(/\/\/|#/);
                    return idx !== -1 ? line.slice(0, idx) : line;
                })
                .join('\n')
                .split(/[\/\\|\n,;]+/);

        return Array.from(new Set(
            rawItems
                .map((keyword) => String(keyword || '').replace(/[`'"]/g, '').trim().toLowerCase())
                .filter(Boolean)
        ));
    }

    // Создание сопоставителя заголовков
    function createTitleHighlightMatcher(keywords) {
        const normalized = normalizeTitleHighlightKeywords(keywords);

        if (normalized.length === 0) return null;

        const matchers = normalized
            .sort((left, right) => right.length - left.length)
            .map(createTitleRuleMatcher)
            .filter(Boolean);

        if (matchers.length === 0) return null;

        return (text) => matchers.some((matcher) => matcher.test(text));
    }

    // Создание правила сопоставления
    function createTitleRuleMatcher(keyword) {
        const trimmed = String(keyword || '').trim().toLowerCase();
        if (!trimmed) return null;

        const isPartialMatch = trimmed.startsWith('*') && trimmed.endsWith('*') && trimmed.length > 2;
        const value = isPartialMatch ? trimmed.slice(1, -1).trim() : trimmed;
        if (!value) return null;

        const parts = value.split(/\s+/).filter(Boolean).map(escapeRegExp);
        if (parts.length === 0) return null;

        const pattern = parts.join('[\\s._-]+');

        if (isPartialMatch) {
            return new RegExp(pattern, 'iu');
        }

        return new RegExp(`(?:^|[^\\p{L}\\p{N}])(?:${pattern})(?=$|[^\\p{L}\\p{N}])`, 'iu');
    }

    // Нормализация карты просмотров
    function normalizeViewedMap(stored) {
        if (!isPlainObject(stored)) return {};
        const normalized = {};
        for (const [key, value] of Object.entries(stored)) {
            if (/^\d+$/.test(key) && typeof value === 'number' && value > 0) {
                normalized[key] = value;
            }
        }
        return normalized;
    }

    // Чтение резервной копии
    function readBackupMap(key) {
        try {
            const raw = localStorage.getItem(key);
            if (!raw) return {};
            return normalizeViewedMap(JSON.parse(raw));
        } catch (err) {
            return {};
        }
    }

    // Запись резервной копии
    function writeBackupMap(key, map) {
        try {
            localStorage.setItem(key, JSON.stringify(map));
        } catch (err) {}
    }

    // Чтение основного хранилища
    function readStoredViewedMap() {
        const gmData = normalizeViewedMap(GM_getValue(STORAGE_KEY, {}));
        const backupData = readBackupMap(STORAGE_KEY);
        return mergeViewedMaps(backupData, gmData);
    }

    // Чтение хранилища дальнего просмотра
    function readStoredViewedFarMap() {
        const gmData = normalizeViewedMap(GM_getValue(STORAGE_KEY_FAR, {}));
        const backupData = readBackupMap(STORAGE_KEY_FAR);
        return mergeViewedMaps(backupData, gmData);
    }

    // Загрузка основного хранилища
    function loadViewedMap() {
        return readStoredViewedMap();
    }

    // Загрузка хранилища дальнего просмотра
    function loadViewedFarMap() {
        return readStoredViewedFarMap();
    }

    // Объединение карт просмотров
    function mergeViewedMaps(...maps) {
        return Object.assign({}, ...maps.map(normalizeViewedMap));
    }

    // Синхронизация хранилищ просмотров
    function syncViewedMapFromStorage() {
        viewedMap = mergeViewedMaps(viewedMap, readStoredViewedMap());
        viewedFarMap = mergeViewedMaps(viewedFarMap, readStoredViewedFarMap());
        lastFarVideoId = readStoredLastId();
        return viewedMap;
    }

    // Сохранение основного хранилища
    function saveViewedMap() {
        viewedMap = mergeViewedMaps(readStoredViewedMap(), viewedMap);
        GM_setValue(STORAGE_KEY, viewedMap);
        writeBackupMap(STORAGE_KEY, viewedMap);
    }

    // Сохранение хранилища дальнего просмотра
    function saveViewedFarMap() {
        viewedFarMap = mergeViewedMaps(readStoredViewedFarMap(), viewedFarMap);
        GM_setValue(STORAGE_KEY_FAR, viewedFarMap);
        writeBackupMap(STORAGE_KEY_FAR, viewedFarMap);
    }

    // Чтение последнего идентификатора
    function readStoredLastId() {
        let val = GM_getValue(STORAGE_KEY_LAST_ID, null);
        if (!val) {
            try {
                val = localStorage.getItem(STORAGE_KEY_LAST_ID);
            } catch (err) {}
        }
        const num = parseInt(val, 10);
        return Number.isFinite(num) && num > 0 ? num : null;
    }

    // Сохранение последнего идентификатора
    function saveStoredLastId(id) {
        if (!id) return;
        GM_setValue(STORAGE_KEY_LAST_ID, id);
        try {
            localStorage.setItem(STORAGE_KEY_LAST_ID, String(id));
        } catch (err) {}
    }

    // Обновление индикатора последнего видео
    function updateLastIdTrackerUI() {
        const isLatestUpdates = location.pathname.includes('/latest-updates');
        const isOverPage = isLatestUpdates && getCurrentPageNumber() >= MIN_PAGE_FOR_LAST_ID;

        let marker = document.getElementById('r34-last-viewed-tracker');

        if (!isOverPage || !lastFarVideoId) {
            if (marker) marker.style.display = 'none';
            return;
        }

        if (!marker || !marker.isConnected) {
            const container = document.querySelector('.content_general, .twocolumns, #custom_list_videos_latest_videos_list') || document.body;
            if (!container) return;
            if (!marker) {
                marker = document.createElement('div');
                marker.id = 'r34-last-viewed-tracker';
                marker.className = 'r34-last-viewed-tracker';
            }
            container.appendChild(marker);
        }

        marker.textContent = `Последнее: ${lastFarVideoId}`;
        marker.style.display = '';
    }

    // Форматирование даты клика
    function formatFarDate(val) {
        if (!val) return '';
        const timestamp = typeof val === 'number' && val > 1 ? val : Date.now();
        const date = new Date(timestamp);
        const day = date.getDate();
        const months = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'];
        const month = months[date.getMonth()];
        const year = date.getFullYear();
        const hours = String(date.getHours()).padStart(2, '0');
        const minutes = String(date.getMinutes()).padStart(2, '0');
        return `${day} ${month} ${year} ${hours}:${minutes}`;
    }

    // Создание элемента тултипа
    function getOrCreateTooltip() {
        if (!tooltipElement) {
            tooltipElement = document.createElement('div');
            tooltipElement.className = 'r34-far-date-tooltip';
            tooltipElement.style.display = 'none';
            document.body.appendChild(tooltipElement);
        }
        return tooltipElement;
    }

    // Скрытие элемента тултипа
    function hideTooltip() {
        if (tooltipElement) {
            tooltipElement.style.display = 'none';
        }
    }

    // Обновление состояния тултипа
    function updateTooltipState(e) {
        if (e && typeof e.clientX === 'number') {
            lastPointerPos.x = e.clientX;
            lastPointerPos.y = e.clientY;
        }

        const dateEl = currentHoveredDate;
        if (!dateEl || !document.body.contains(dateEl)) {
            hideTooltip();
            return;
        }

        const thumb = dateEl.closest(THUMB_SELECTOR);
        const key = getThumbKey(thumb);
        const farVal = key ? viewedFarMap[key] : null;

        if (!farVal) {
            hideTooltip();
            return;
        }

        const tooltip = getOrCreateTooltip();
        tooltip.textContent = formatFarDate(farVal);
        tooltip.style.left = `${lastPointerPos.x + 12}px`;
        tooltip.style.top = `${lastPointerPos.y + 12}px`;
        tooltip.style.display = 'block';
    }

    // Получение ключа ссылки
    function getVideoKeyFromLink(link) {
        return extractVideoKeyFromUrl(link?.href || '');
    }

    // Получение текущего ключа видео
    function getCurrentVideoKey() {
        return extractVideoKeyFromUrl(location.href);
    }

    // Поиск ссылки миниатюры
    function getThumbLink(thumb) {
        if (!(thumb instanceof Element)) return null;

        if (thumb.matches(VIDEO_LINK_SELECTOR) && getVideoKeyFromLink(thumb)) {
            return thumb;
        }

        const links = thumb.querySelectorAll(VIDEO_LINK_SELECTOR);
        for (const link of links) {
            if (getVideoKeyFromLink(link)) return link;
        }

        return null;
    }

    // Поиск заголовка миниатюры
    function getThumbTitleElement(thumb) {
        return thumb?.querySelector(TITLE_SELECTOR) || null;
    }

    // Извлечение ключа из адреса
    function extractVideoKeyFromUrl(url) {
        try {
            const parsed = new URL(url, window.location.origin);
            const match = parsed.pathname.match(VIDEO_PATH_PATTERN);
            return match ? match[1] : '';
        } catch (err) {
            return '';
        }
    }

    // Получение ключа миниатюры
    function getThumbKey(thumb) {
        return getVideoKeyFromLink(getThumbLink(thumb));
    }

    // Извлечение номера страницы из адреса
    function extractPageFromUrl(url) {
        if (!url) return 0;
        try {
            const parsed = new URL(url, window.location.origin);
            if (!parsed.pathname.includes('/video/')) {
                const pathMatch = parsed.pathname.match(/\/(\d+)\/?$/);
                if (pathMatch) return parseInt(pathMatch[1], 10) || 0;
            }
            for (const [key, val] of parsed.searchParams) {
                if (key === 'from' || key.startsWith('from_')) {
                    const n = parseInt(val, 10);
                    if (Number.isFinite(n) && n > 0) return n;
                }
            }
        } catch (err) {}
        return 0;
    }

    // Определение номера страницы
    function getCurrentPageNumber() {
        const jumpAttr = document.querySelector('[data-rd-jump-page]')?.getAttribute('data-rd-jump-page');
        if (jumpAttr) {
            const num = parseInt(jumpAttr, 10);
            if (Number.isFinite(num) && num > 0) return num;
        }

        const activeItem = document.querySelector(PAGINATION_ACTIVE_SELECTOR);
        if (activeItem) {
            const num = parseInt(activeItem.textContent.trim(), 10);
            if (!Number.isNaN(num) && num > 0) return num;
            const link = activeItem.querySelector('a');
            const paramMatch = link?.getAttribute('data-parameters')?.match(/from(?:_[a-z]+)?:(\d+)/i);
            if (paramMatch) {
                const numParam = parseInt(paramMatch[1], 10);
                if (!Number.isNaN(numParam)) return numParam;
            }
        }

        const urlPage = extractPageFromUrl(location.href);
        if (urlPage > 0) return urlPage;

        return 1;
    }

    // Установка состояния просмотра
    function setThumbViewedState(thumb, isViewed) {
        const wrap = thumb?.querySelector(WRAP_SELECTOR) || (thumb?.matches(WRAP_SELECTOR) ? thumb : null);
        if (!wrap) return;

        if (isViewed) {
            wrap.setAttribute(VIEWED_ATTR, '1');
        } else {
            wrap.removeAttribute(VIEWED_ATTR);
        }
    }

    // Установка подсветки заголовка
    function setThumbTitleHighlightState(thumb, isHighlighted) {
        const title = getThumbTitleElement(thumb);
        if (!title) return;

        if (isHighlighted) {
            title.setAttribute(FLAGGED_TITLE_ATTR, '1');
        } else {
            title.removeAttribute(FLAGGED_TITLE_ATTR);
        }
    }

    // Поиск даты миниатюры
    function getThumbDateElement(thumb) {
        return thumb?.querySelector(DATE_SELECTOR) || null;
    }

    // Установка подсветки даты
    function setThumbDateHighlightState(thumb, isHighlighted) {
        const dateElement = getThumbDateElement(thumb);
        if (!dateElement) return;

        if (isHighlighted) {
            dateElement.setAttribute(VIEWED_DATE_ATTR, '1');
        } else {
            dateElement.removeAttribute(VIEWED_DATE_ATTR);
        }
    }

    // Проверка подсветки заголовка
    function shouldHighlightTitle(text) {
        if (!text) return false;

        const normalizedText = text.toLowerCase();

        if (TITLE_HIGHLIGHT_BLACKLIST_MATCHER?.(normalizedText)) {
            return false;
        }

        return TITLE_HIGHLIGHT_MATCHER?.(normalizedText) || false;
    }

    // Применение состояния миниатюры
    function applyThumbState(thumb) {
        if (!(thumb instanceof HTMLElement) || !thumb.matches(THUMB_SELECTOR)) return;

        const key = getThumbKey(thumb);
        const titleText = getThumbTitleElement(thumb)?.textContent?.trim() || '';
        const isViewed = Boolean(key && viewedMap[key]);
        const isViewedFar = Boolean(key && viewedFarMap[key]);
        const isAfterThreshold = getCurrentPageNumber() > MIN_PAGE_FOR_DATE_HIGHLIGHT;

        setThumbViewedState(thumb, isViewed);
        setThumbTitleHighlightState(thumb, shouldHighlightTitle(titleText));
        setThumbDateHighlightState(thumb, isViewedFar && isAfterThreshold);
    }

    // Обработка корневого элемента
    function processRoot(root) {
        if (!root) return;

        if (root instanceof Element && root.matches(THUMB_SELECTOR)) {
            applyThumbState(root);
        }

        if (root.querySelectorAll) {
            root.querySelectorAll(THUMB_SELECTOR).forEach(applyThumbState);
        }
    }

    // Сброс очереди обработки
    function flushPendingRoots() {
        const roots = Array.from(pendingRoots);
        pendingRoots.clear();
        roots.forEach(processRoot);
        updateLastIdTrackerUI();
    }

    // Планирование обработки
    function scheduleProcess(root = document) {
        pendingRoots.add(root);
        if (processTimer) return;

        processTimer = window.setTimeout(() => {
            processTimer = null;
            flushPendingRoots();
        }, PROCESS_DELAY);
    }

    // Запись основного просмотра видео
    function markVideoAsViewed(key) {
        if (!key || viewedMap[key] === 1) return;

        viewedMap = mergeViewedMaps(readStoredViewedMap(), viewedMap);
        if (viewedMap[key] === 1) return;

        viewedMap[key] = 1;
        saveViewedMap();
    }

    // Запись дальнего просмотра видео
    function markVideoAsViewedFar(key) {
        if (!key) return;

        viewedFarMap = mergeViewedMaps(readStoredViewedFarMap(), viewedFarMap);
        if (!viewedFarMap[key] || viewedFarMap[key] === 1) {
            viewedFarMap[key] = Date.now();
            saveViewedFarMap();
        }
    }

    // Запись текущего видео
    function markCurrentVideoAsViewed() {
        const key = getCurrentVideoKey();
        if (!key) return;

        markVideoAsViewed(key);

        const refPage = extractPageFromUrl(document.referrer);
        if (refPage > MIN_PAGE_FOR_DATE_HIGHLIGHT) {
            markVideoAsViewedFar(key);
        }

        const isLatestUpdatesRef = document.referrer.includes('/latest-updates');
        if (isLatestUpdatesRef && refPage >= MIN_PAGE_FOR_LAST_ID) {
            const keyNum = parseInt(key, 10);
            if (Number.isFinite(keyNum) && keyNum > 0) {
                if (!lastFarVideoId || keyNum < lastFarVideoId) {
                    lastFarVideoId = keyNum;
                    saveStoredLastId(keyNum);
                }
            }
        }
    }

    // Обработка смены адреса
    function handleUrlChange() {
        if (location.href === lastUrl) return;
        lastUrl = location.href;
        syncViewedMapFromStorage();
        markCurrentVideoAsViewed();
        scheduleProcess(document);
        startObserver();
    }

    // Установка перехватчиков истории
    function installHistoryHooks() {
        const { pushState, replaceState } = history;

        history.pushState = function (...args) {
            const result = pushState.apply(this, args);
            handleUrlChange();
            return result;
        };

        history.replaceState = function (...args) {
            const result = replaceState.apply(this, args);
            handleUrlChange();
            return result;
        };

        window.addEventListener('popstate', handleUrlChange, true);
        window.addEventListener('hashchange', handleUrlChange, true);
    }

    // Обработка событий указателя
    function handlePointer(event) {
        if (event.button !== 0 && event.button !== 1) return;

        const target = event.target instanceof Element ? event.target : null;
        if (target?.closest('.vv-cardchip, [data-vv-chip], [data-rd-close], .close, [class*="close"]')) return;

        const link = target?.closest(VIDEO_LINK_SELECTOR);
        const key = getVideoKeyFromLink(link);
        if (!key) return;

        const isAfterThreshold = getCurrentPageNumber() > MIN_PAGE_FOR_DATE_HIGHLIGHT;
        const isLatestUpdates = location.pathname.includes('/latest-updates');
        const isAfterLastIdThreshold = isLatestUpdates && getCurrentPageNumber() >= MIN_PAGE_FOR_LAST_ID;
        const thumb = link?.closest(THUMB_SELECTOR);

        markVideoAsViewed(key);
        if (isAfterThreshold) {
            markVideoAsViewedFar(key);
        }

        if (isAfterLastIdThreshold) {
            const keyNum = parseInt(key, 10);
            if (Number.isFinite(keyNum) && keyNum > 0) {
                if (!lastFarVideoId || keyNum < lastFarVideoId) {
                    lastFarVideoId = keyNum;
                    saveStoredLastId(keyNum);
                    updateLastIdTrackerUI();
                }
            }
        }

        if (thumb) {
            setThumbViewedState(thumb, true);
            if (isAfterThreshold) {
                setThumbDateHighlightState(thumb, true);
            }
        }
    }

    // Обработка наведения указателя
    function handleMouseOver(e) {
        const dateEl = e.target?.closest?.(DATE_SELECTOR);
        if (dateEl) {
            currentHoveredDate = dateEl;
            updateTooltipState(e);
        } else {
            currentHoveredDate = null;
            hideTooltip();
        }
    }

    // Обработка перемещения указателя
    function handleMouseMove(e) {
        if (currentHoveredDate) {
            updateTooltipState(e);
        }
    }

    // Обработка мутаций DOM
    function handleDomMutations(mutations) {
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType === 1) {
                    scheduleProcess(node);
                }
            }
        }
    }

    // Запуск наблюдателя DOM
    function startObserver() {
        if (!document.body) return;

        if (!domObserver) {
            domObserver = new MutationObserver(handleDomMutations);
        }

        if (observedBody !== document.body) {
            domObserver.disconnect();
            observedBody = document.body;
            domObserver.observe(observedBody, { childList: true, subtree: true });
        }

        if (!lazyloadHookInstalled) {
            document.addEventListener('lazyloaded', () => {
                scheduleProcess(document);
            }, true);
            lazyloadHookInstalled = true;
        }
    }

    // Обработка ответа AJAX
    function handleAjaxUpdate() {
        syncViewedMapFromStorage();
        markCurrentVideoAsViewed();
        scheduleProcess(document);
        startObserver();
    }

    // Установка перехватчиков AJAX
    function installAjaxHooks() {
        if (ajaxHooksInstalled) return;

        const jq = window.jQuery;
        if (!jq?.fn?.jquery) return;

        jq(document).on('ajaxComplete.r34ViewedThumbs ajaxSuccess.r34ViewedThumbs', handleAjaxUpdate);
        ajaxHooksInstalled = true;
    }

    // Установка слушателей хранилища
    function installStorageSync() {
        if (typeof GM_addValueChangeListener !== 'function') return;

        GM_addValueChangeListener(STORAGE_KEY, (name, oldValue, newValue, remote) => {
            if (!remote) return;

            viewedMap = mergeViewedMaps(viewedMap, newValue);
            scheduleProcess(document);
        });

        GM_addValueChangeListener(STORAGE_KEY_FAR, (name, oldValue, newValue, remote) => {
            if (!remote) return;

            viewedFarMap = mergeViewedMaps(viewedFarMap, newValue);
            scheduleProcess(document);
        });

        GM_addValueChangeListener(STORAGE_KEY_LAST_ID, (name, oldValue, newValue, remote) => {
            if (!remote) return;

            lastFarVideoId = readStoredLastId();
            updateLastIdTrackerUI();
        });
    }

    // Инициализация скрипта
    function init() {
        syncViewedMapFromStorage();
        markCurrentVideoAsViewed();
        scheduleProcess(document);
        startObserver();
        installAjaxHooks();
    }

    GM_addStyle(VIEWED_STYLE);

    document.addEventListener('mousedown', handlePointer, true);
    document.addEventListener('auxclick', handlePointer, true);
    document.addEventListener('click', handlePointer, true);

    document.addEventListener('mouseover', handleMouseOver, true);
    document.addEventListener('mousemove', handleMouseMove, true);

    installHistoryHooks();
    installStorageSync();

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

    window.addEventListener('load', () => {
        scheduleProcess(document);
        startObserver();
        installAjaxHooks();
    }, { once: true });
})();