☰

[DOFANTASY] Comics Counter

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

您需要先安装一款用户脚本管理器扩展,例如 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         [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 });
})();