☰

[DOFANTASY] Comics Counter

Нумерация, отметка прочитанных/пропущенных комиксов и единый JSON-реестр

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name         [DOFANTASY] Comics Counter
// @name:en      [DOFANTASY] Comics Counter
// @namespace    http://tampermonkey.net/
// @version      1.26
// @description  Нумерация, отметка прочитанных/пропущенных комиксов и единый JSON-реестр
// @description:en Numbering, read/missing status tracking, and unified JSON registry for comics
// @author       Antigravity
// @match       https://www.dofantasy.com/en/categories/comics?locale=en&publisher=dofantasy*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=dofantasy.com
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  // RU Внедрение стилей
  // EN Injecting styles
  const STYLE_ID = 'df-comics-counter-styles';
  if (!document.getElementById(STYLE_ID)) {
    const styleEl = document.createElement('style');
    styleEl.id = STYLE_ID;
    styleEl.textContent = `
      div.w-full.lg\\:w-4\\/5 ul.flex.flex-wrap > li {
        padding-bottom: 16px !important;
      }
      .df-card-box {
        position: relative !important;
        margin-top: 42px !important;
        border: 4px solid transparent !important;
        border-radius: 12px !important;
        padding: 6px !important;
        transition: border-color 0.2s ease, box-shadow 0.2s ease !important;
        box-sizing: border-box !important;
      }
      .df-card-box.df-status-read {
        border-color: #059669 !important;
        box-shadow: 0 0 10px rgba(5, 150, 105, 0.3) !important;
      }
      .df-card-box.df-status-missing {
        border-color: #dc2626 !important;
        box-shadow: 0 0 10px rgba(220, 38, 38, 0.3) !important;
      }
      .df-num-badge {
        position: absolute;
        top: -38px;
        left: 50%;
        transform: translateX(-50%);
        font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
        font-size: 13px;
        font-weight: 600;
        letter-spacing: 0.5px;
        color: #ffffff;
        background: linear-gradient(180deg, #27272a 0%, #18181b 100%);
        padding: 3px 12px;
        border-radius: 8px;
        border: 1px solid rgba(255, 255, 255, 0.15);
        box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.15);
        cursor: pointer;
        user-select: none;
        white-space: nowrap;
        z-index: 30;
        transition: all 0.15s ease;
        line-height: 1.4;
      }
      .df-num-badge:hover {
        background: linear-gradient(180deg, #ef4444 0%, #dc2626 100%);
        border-color: rgba(255, 255, 255, 0.3);
        transform: translateX(-50%) scale(1.08);
        box-shadow: 0 4px 10px rgba(220, 38, 38, 0.45);
      }
      .df-num-badge.df-series-badge {
        background: #3f3f46 !important;
        border-color: rgba(255, 255, 255, 0.2) !important;
        cursor: default !important;
        transform: translateX(-50%) !important;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3) !important;
      }
      .df-num-badge.df-series-badge:hover {
        background: #3f3f46 !important;
        border-color: rgba(255, 255, 255, 0.2) !important;
        transform: translateX(-50%) !important;
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3) !important;
      }
      .df-num-badge.df-anchor-badge {
        border: 2px solid #eab308 !important;
        box-shadow: 0 0 10px rgba(234, 179, 8, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.2) !important;
        background: linear-gradient(180deg, #3f2c06 0%, #1c1917 100%) !important;
        color: #fef08a !important;
      }
      .df-num-badge.df-anchor-badge:hover {
        background: linear-gradient(180deg, #ca8a04 0%, #a16207 100%) !important;
        border-color: #fef08a !important;
        box-shadow: 0 0 14px rgba(234, 179, 8, 0.8) !important;
      }
      .df-num-input {
        position: absolute;
        top: -38px;
        left: 50%;
        transform: translateX(-50%);
        font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
        font-size: 13px;
        font-weight: 600;
        letter-spacing: 0.5px;
        color: #ffffff;
        background: #18181b;
        border: 1px solid #ef4444;
        border-radius: 8px;
        box-shadow: 0 0 10px rgba(239, 68, 68, 0.5), 0 3px 6px rgba(0, 0, 0, 0.4);
        width: 56px;
        height: 24px;
        text-align: center;
        padding: 0;
        margin: 0;
        outline: none;
        z-index: 40;
        box-sizing: border-box;
      }
      .df-status-btn {
        position: absolute;
        top: -37px;
        z-index: 30;
        width: 25px;
        height: 25px;
        border-radius: 50%;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        user-select: none;
        border: 1px solid rgba(255, 255, 255, 0.2);
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
        transition: transform 0.15s ease, background 0.15s ease;
        line-height: 1;
      }
      .df-status-btn:hover {
        transform: scale(1.15);
      }
      .df-btn-missing {
        left: 8px;
        background: #18181b;
        color: #ef4444;
      }
      .df-btn-missing:hover {
        background: #dc2626;
        color: #ffffff;
        border-color: #ef4444;
      }
      .df-btn-read {
        right: 8px;
        background: #18181b;
        color: #10b981;
      }
      .df-btn-read:hover {
        background: #059669;
        color: #ffffff;
        border-color: #10b981;
      }
      .df-btn-read svg {
        width: 14px;
        height: 14px;
        fill: currentColor;
      }
      .df-glyph {
        font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
        font-size: 14px;
        font-weight: 700;
        line-height: 1;
        display: inline-block;
      }
      /* RU Скрытие лишних элементов магазина */
      /* EN Hide unnecessary store elements */
      div.w-full.lg\\:w-4\\/5 ul.flex.flex-wrap li div.mt-auto {
        display: none !important;
      }
      div.w-full.lg\\:w-4\\/5 ul.flex.flex-wrap li button[aria-label="Add to cart"],
      div.w-full.lg\\:w-4\\/5 ul.flex.flex-wrap li button[aria-label="Add to wishlist"] {
        display: none !important;
      }
    `;
    document.head.appendChild(styleEl);
  }

  const STORAGE_KEY_REGISTRY = 'df_comics_registry';
  const LEGACY_KEY_STATUS = 'df_comics_status_map';
  const LEGACY_KEY_NUMS = 'df_comics_number_offsets';

  const BASE_MUSE_NUM = 711;
  const GLYPH_QUESTION = '<span class="df-glyph">?</span>';
  const SVG_CHECK = `
    <svg viewBox="0 0 24 24">
      <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/>
    </svg>
  `;

  // RU Локализация тултипов
  // EN Tooltip localization
  const IS_RU = (navigator.language || navigator.userLanguage || '').toLowerCase().startsWith('ru');
  const I18N = {
    missing: IS_RU ? 'Недоступно' : 'Not available',
    read: IS_RU ? 'Просмотрено' : 'Watched',
    editNum: IS_RU ? 'Изменить нумерацию комикса' : 'Change comic number',
    anchor: IS_RU ? 'Точка отсчета (#711 Sharing the Muse)' : 'Reference anchor (#711 Sharing the Muse)',
    series: IS_RU ? 'Сборник серий' : 'Series bundle',
    menuCopy: IS_RU ? 'Скопировать JSON реестра в буфер' : 'Copy JSON registry to clipboard',
    menuReset: IS_RU ? 'Сбросить весь реестр' : 'Reset all registry',
    copiedAlert: IS_RU ? 'JSON реестра скопирован в буфер обмена!' : 'JSON registry copied to clipboard!'
  };

  // RU Чтение реестра
  // EN Reading registry
  function getRegistry() {
    try {
      let raw = null;
      if (typeof GM_getValue === 'function') {
        raw = GM_getValue(STORAGE_KEY_REGISTRY, null);
      }
      if (!raw) {
        const lsVal = localStorage.getItem(STORAGE_KEY_REGISTRY);
        if (lsVal) {
          try {
            raw = JSON.parse(lsVal);
          } catch {
            raw = lsVal;
          }
        }
      }
      if (typeof raw === 'string') {
        try { raw = JSON.parse(raw); } catch {}
      }
      if (Array.isArray(raw)) return raw;
    } catch {}
    return [];
  }

  // RU Запись реестра
  // EN Saving registry
  function saveRegistry(list) {
    try {
      const sorted = list.slice().sort((a, b) => {
        const numA = typeof a.number === 'number' ? a.number : -1;
        const numB = typeof b.number === 'number' ? b.number : -1;
        return numB - numA;
      });
      if (typeof GM_setValue === 'function') {
        GM_setValue(STORAGE_KEY_REGISTRY, sorted);
      }
      localStorage.setItem(STORAGE_KEY_REGISTRY, JSON.stringify(sorted));
    } catch {}
  }

  // RU Миграция старых данных
  // EN Migrating legacy storage
  function migrateLegacyStorage() {
    try {
      const rawOldStatus = localStorage.getItem(LEGACY_KEY_STATUS);
      const rawOldNums = localStorage.getItem(LEGACY_KEY_NUMS);

      if (rawOldStatus || rawOldNums) {
        const oldStatusMap = rawOldStatus ? JSON.parse(rawOldStatus) : {};
        const oldNumsMap = rawOldNums ? JSON.parse(rawOldNums) : {};

        const currentRegistry = getRegistry();
        const registryMap = new Map();
        currentRegistry.forEach(item => {
          if (item && item.id) registryMap.set(item.id, item);
        });

        Object.keys(oldStatusMap).forEach(slug => {
          if (!registryMap.has(slug)) {
            registryMap.set(slug, {
              id: slug,
              number: oldNumsMap[slug] || null,
              author: "Неизвестен",
              title: slug,
              status: oldStatusMap[slug] || "none",
              existsOnSite: false
            });
          }
        });

        saveRegistry(Array.from(registryMap.values()));

        localStorage.removeItem(LEGACY_KEY_STATUS);
        localStorage.removeItem(LEGACY_KEY_NUMS);
        if (typeof GM_deleteValue === 'function') {
          try { GM_deleteValue(LEGACY_KEY_STATUS); } catch {}
          try { GM_deleteValue(LEGACY_KEY_NUMS); } catch {}
        }
      }
    } catch {}
  }

  // RU Запуск миграции
  // EN Run migration
  migrateLegacyStorage();

  // RU Меню команд
  // EN User script commands
  if (typeof GM_registerMenuCommand === 'function') {
    GM_registerMenuCommand(I18N.menuCopy, () => {
      const registry = getRegistry();
      const prettyJson = JSON.stringify(registry, null, 2);
      if (typeof GM_setClipboard === 'function') {
        GM_setClipboard(prettyJson);
      } else {
        navigator.clipboard.writeText(prettyJson);
      }
      alert(I18N.copiedAlert);
    });

    GM_registerMenuCommand(I18N.menuReset, () => {
      saveRegistry([]);
      location.reload();
    });
  }

  // RU Защита от повторного входа
  // EN Re-entrancy guard
  let isUpdating = false;

  // RU Обновление карточек каталога
  // EN Update catalog cards
  function updateComics() {
    if (isUpdating) return;
    isUpdating = true;

    try {
      const catalogContainer = document.querySelector('div.w-full.lg\\:w-4\\/5 ul.flex.flex-wrap, ul.flex.flex-wrap.-mx-10');
      const cardElements = Array.from(catalogContainer ? catalogContainer.children : [])
        .filter(li => li.tagName === 'LI' && li.querySelector('a[href*="/products/"], a[href*="/series/"]'));

      if (cardElements.length === 0) return;

      const registry = getRegistry();
      const registryMap = new Map();
      registry.forEach(item => {
        if (item && item.id) registryMap.set(item.id, item);
      });

      const urlParams = new URLSearchParams(window.location.search);
      const currentPage = parseInt(urlParams.get('page') || '1', 10);

      let museLocalComicIndex = -1;
      let comicRunningCount = 0;

      const cardsData = cardElements.map((card, i) => {
        const cardBox = card.querySelector('div.h-full.flex.flex-col') || card;
        const titleEl = cardBox.querySelector('h3') || cardBox.querySelector('h1, h2, h4, [class*="title"]');
        const title = titleEl ? titleEl.textContent.trim() : '';
        const authorEl = cardBox.querySelector('h4') || cardBox.querySelector('a.underline');
        const author = authorEl ? authorEl.textContent.trim() : 'Не указан';

        const seriesLink = cardBox.querySelector('a[href*="/series/"]');
        const productLink = cardBox.querySelector('a[href*="/products/"]');
        const isSeries = Boolean(seriesLink && !productLink);

        const linkEl = productLink || seriesLink;
        const href = linkEl ? linkEl.href : '';
        const comicId = href ? href.split('?')[0].split('/').pop() : title;

        let comicIndex = null;
        if (!isSeries) {
          comicIndex = comicRunningCount++;
          // RU Определение эталона
          // EN Identify anchor
          const isSlugMatch = comicId === 'sharing-the-muse';
          const isTitleMatch = title.toLowerCase().includes('sharing the muse') && (!author || author.toLowerCase().includes('montal'));
          if (isSlugMatch || isTitleMatch) {
            museLocalComicIndex = comicIndex;
          }
        }

        return { cardBox, title, author, comicId, isSeries, comicIndex, index: i };
      });

      // RU Базовый расчет номеров
      // EN Base comic numbering
      const totalComics = comicRunningCount;
      const calculatedComicNumbers = new Array(totalComics);

      if (currentPage === 1) {
        for (let c = 0; c < totalComics; c++) {
          if (c <= 11) {
            calculatedComicNumbers[c] = 729 - c;
          } else {
            calculatedComicNumbers[c] = 728 - c;
          }
        }
      } else if (currentPage === 2) {
        for (let c = 0; c < totalComics; c++) {
          calculatedComicNumbers[c] = 712 - c;
        }
      } else {
        const pageStart = 712 - (currentPage - 2) * 14;
        for (let c = 0; c < totalComics; c++) {
          calculatedComicNumbers[c] = pageStart - c;
        }
      }

      // RU Каскадный сдвиг номеров
      // EN Cascade shift for manual overrides
      for (let c = totalComics - 1; c >= 0; c--) {
        const item = cardsData.find(x => x.comicIndex === c);
        const rec = item ? registryMap.get(item.comicId) : null;
        if (rec && rec.manualOverride === true && typeof rec.number === 'number') {
          const shift = rec.number - calculatedComicNumbers[c];
          if (shift !== 0) {
            calculatedComicNumbers[c] = rec.number;
            for (let above = c - 1; above >= 0; above--) {
              calculatedComicNumbers[above] += shift;
            }
          }
        }
      }

      // RU Устранение коллизий дублирования
      // EN Resolve collision duplicates
      for (let c = totalComics - 2; c >= 0; c--) {
        if (calculatedComicNumbers[c] <= calculatedComicNumbers[c + 1]) {
          calculatedComicNumbers[c] = calculatedComicNumbers[c + 1] + 1;
        }
      }

      // RU Отрисовка карточек
      // EN Render cards
      cardsData.forEach((item) => {
        if (!item.cardBox.classList.contains('df-card-box')) {
          item.cardBox.classList.add('df-card-box');
        }

        // RU Обработка сборников
        // EN Series handling
        if (item.isSeries) {
          item.cardBox.classList.remove('df-status-read', 'df-status-missing');

          const existingBtnMissing = item.cardBox.querySelector(':scope > .df-btn-missing');
          if (existingBtnMissing) existingBtnMissing.remove();
          const existingBtnRead = item.cardBox.querySelector(':scope > .df-btn-read');
          if (existingBtnRead) existingBtnRead.remove();

          let badge = item.cardBox.querySelector(':scope > .df-num-badge');
          if (!badge) {
            badge = document.createElement('div');
            badge.className = 'df-num-badge df-series-badge';
            item.cardBox.appendChild(badge);
          }
          badge.className = 'df-num-badge df-series-badge';
          badge.textContent = 'SERIES';
          badge.title = I18N.series;
          badge.onclick = null;

          registryMap.set(item.comicId, {
            id: item.comicId,
            number: 'SERIES',
            author: item.author,
            title: item.title,
            status: 'none',
            existsOnSite: true
          });
          return;
        }

        // RU Обработка одиночных комиксов
        // EN Single comics handling
        const calculatedNum = calculatedComicNumbers[item.comicIndex];
        const existingRecord = registryMap.get(item.comicId);
        const currentStatus = existingRecord ? existingRecord.status : 'none';

        const existingRec = registryMap.get(item.comicId);
        registryMap.set(item.comicId, {
          id: item.comicId,
          number: calculatedNum,
          author: item.author,
          title: item.title,
          status: currentStatus,
          existsOnSite: true,
          manualOverride: (existingRec && existingRec.manualOverride === true) ? true : undefined
        });

        const readClass = 'df-status-read';
        const missingClass = 'df-status-missing';

        if (currentStatus === 'read') {
          if (!item.cardBox.classList.contains(readClass)) item.cardBox.classList.add(readClass);
          item.cardBox.classList.remove(missingClass);
        } else if (currentStatus === 'missing') {
          if (!item.cardBox.classList.contains(missingClass)) item.cardBox.classList.add(missingClass);
          item.cardBox.classList.remove(readClass);
        } else {
          item.cardBox.classList.remove(readClass, missingClass);
        }

        // RU Кнопка пропуска
        // EN Missing button
        let btnMissing = item.cardBox.querySelector(':scope > .df-btn-missing');
        if (!btnMissing) {
          btnMissing = document.createElement('div');
          btnMissing.className = 'df-status-btn df-btn-missing';
          btnMissing.innerHTML = GLYPH_QUESTION;
          btnMissing.title = I18N.missing;
          item.cardBox.appendChild(btnMissing);

          btnMissing.onmousedown = (e) => {
            if (e.button === 1) return;
          };

          btnMissing.onclick = (e) => {
            if (e.button !== 0) return;
            e.preventDefault();
            e.stopPropagation();

            const rec = registryMap.get(item.comicId) || {
              id: item.comicId,
              number: calculatedNum,
              author: item.author,
              title: item.title,
              existsOnSite: true
            };

            if (rec.status === 'missing') {
              rec.status = 'none';
              item.cardBox.classList.remove('df-status-missing');
            } else {
              rec.status = 'missing';
              item.cardBox.classList.remove('df-status-read');
              item.cardBox.classList.add('df-status-missing');
            }
            registryMap.set(item.comicId, rec);
            saveRegistry(Array.from(registryMap.values()));
          };
        } else {
          if (btnMissing.innerHTML !== GLYPH_QUESTION) btnMissing.innerHTML = GLYPH_QUESTION;
          if (btnMissing.title !== I18N.missing) btnMissing.title = I18N.missing;
        }

        // RU Бейдж номера и инлайн-редактирование
        // EN Number badge and inline editing
        let badge = item.cardBox.querySelector(':scope > .df-num-badge');
        if (!badge) {
          badge = document.createElement('div');
          badge.className = 'df-num-badge';
          item.cardBox.appendChild(badge);
        }
        badge.classList.remove('df-series-badge');

        badge.onclick = (e) => {
          e.preventDefault();
          e.stopPropagation();

          badge.style.display = 'none';

          const input = document.createElement('input');
          input.className = 'df-num-input';
          input.type = 'text';
          input.value = calculatedNum;
          item.cardBox.appendChild(input);

          input.focus();
          input.select();

          let submitted = false;

          const finish = (save) => {
            if (submitted) return;
            submitted = true;

            if (save) {
              const val = parseInt(input.value.replace(/[^0-9]/g, ''), 10);
              if (!isNaN(val)) {
                const prevNum = calculatedNum;
                const delta = val - prevNum;

                if (delta !== 0) {
                  for (let above = item.comicIndex - 1; above >= 0; above--) {
                    const aboveItem = cardsData.find(x => x.comicIndex === above);
                    if (aboveItem) {
                      const aboveRec = registryMap.get(aboveItem.comicId);
                      if (aboveRec && aboveRec.manualOverride === true && typeof aboveRec.number === 'number') {
                        aboveRec.number += delta;
                        registryMap.set(aboveItem.comicId, aboveRec);
                      }
                    }
                  }
                }

                const rec = registryMap.get(item.comicId) || {
                  id: item.comicId,
                  author: item.author,
                  title: item.title,
                  status: currentStatus,
                  existsOnSite: true
                };
                rec.number = val;
                rec.manualOverride = true;
                registryMap.set(item.comicId, rec);
                saveRegistry(Array.from(registryMap.values()));
              }
            }
            input.remove();
            badge.style.display = '';
            updateComics();
          };

          input.onkeydown = (ev) => {
            if (ev.key === 'Enter') {
              ev.preventDefault();
              finish(true);
            } else if (ev.key === 'Escape') {
              ev.preventDefault();
              finish(false);
            }
          };

          input.onblur = () => finish(true);
        };

        if (!item.cardBox.querySelector(':scope > .df-num-input')) {
          badge.style.display = '';
          const badgeText = '#' + calculatedNum;
          if (badge.textContent !== badgeText) {
            badge.textContent = badgeText;
          }
        }
        const isAnchorMuse = (item.comicId === 'sharing-the-muse') || item.title.toLowerCase().includes('sharing the muse');
        if (isAnchorMuse) {
          badge.classList.add('df-anchor-badge');
          badge.title = I18N.anchor;
        } else {
          badge.classList.remove('df-anchor-badge');
          badge.title = I18N.editNum;
        }

        // RU Кнопка прочтения
        // EN Read button
        let btnRead = item.cardBox.querySelector(':scope > .df-btn-read');
        if (!btnRead) {
          btnRead = document.createElement('div');
          btnRead.className = 'df-status-btn df-btn-read';
          btnRead.innerHTML = SVG_CHECK;
          btnRead.title = I18N.read;
          item.cardBox.appendChild(btnRead);

          btnRead.onmousedown = (e) => {
            if (e.button === 1) return;
          };

          btnRead.onclick = (e) => {
            if (e.button !== 0) return;
            e.preventDefault();
            e.stopPropagation();

            const rec = registryMap.get(item.comicId) || {
              id: item.comicId,
              number: calculatedNum,
              author: item.author,
              title: item.title,
              existsOnSite: true
            };

            if (rec.status === 'read') {
              rec.status = 'none';
              item.cardBox.classList.remove('df-status-read');
            } else {
              rec.status = 'read';
              item.cardBox.classList.remove('df-status-missing');
              item.cardBox.classList.add('df-status-read');
            }
            registryMap.set(item.comicId, rec);
            saveRegistry(Array.from(registryMap.values()));
          };
        } else {
          if (btnRead.title !== I18N.read) btnRead.title = I18N.read;
        }
      });

      // RU Сохранение реестра
      // EN Save registry
      saveRegistry(Array.from(registryMap.values()));
    } finally {
      isUpdating = false;
    }
  }

  // RU Запуск обновления
  // EN Initial run
  updateComics();

  // RU Навигация истории
  // EN History navigation
  window.addEventListener('popstate', () => {
    setTimeout(updateComics, 100);
  });

  // RU Наблюдатель DOM
  // EN DOM observer
  let debounceTimer = null;
  const observer = new MutationObserver(() => {
    if (isUpdating) return;
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
      updateComics();
    }, 250);
  });

  const catalogContainer = document.querySelector('div.w-full.lg\\:w-4\\/5') || document.body;
  observer.observe(catalogContainer, { childList: true, subtree: true });
})();