Gridify (PC)

10/17/2025, 7:11:55 PM

// ==UserScript==
// @name        Gridify (PC)
// @namespace   Violentmonkey Scripts
// @match       https://wtr-lab.com/en/library*
// @grant       none
// @version     1.2
// @author      -
// @description 10/17/2025, 7:11:55 PM
// ==/UserScript==

(function () {
  /* ---------- config ---------- */
  const TARGET_CARD_WIDTH_PX = 121;  // your width
const TARGET_CARD_HEIGHT_PX = 260; // your fixed height
const TARGET_CARD_GAP_PX = 10;      // grid gap
  const AFTER_LOADMORE_RESTYLE_DELAY = 600; // ms after load-more click to restyle
  const POLL_INTERVAL_MS = 800; // how often we check for initial library presence
  const POLL_TIMEOUT_MS = 30000; // stop trying after this (ms)

  /* ---------- helpers ---------- */
  const q = (s, r = document) => r.querySelector(s);
  const qa = (s, r = document) => Array.from(r.querySelectorAll(s));

  /* ---------- restyle logic (unchanged core, idempotent) ---------- */
  let compactMode = true;

// --- Transform single item ---
function transformItem(item) {
  if (!item) return;
Object.assign(item.style, {
  width: '121px',
  height: '265px',
  margin: '0',
  padding: '0',
  boxSizing: 'border-box',
  display: 'inline-block',
  verticalAlign: 'top',
  flex: 'none',
  gridColumn: 'auto',
  justifySelf: 'start',
  alignSelf: 'start',
});
const style = document.createElement('style');
style.textContent = `
  .serie-item {
    border: none !important;
    outline: none !important;
    box-shadow: none !important;
    width: ${TARGET_CARD_WIDTH_PX}px !important;
    max-width: ${TARGET_CARD_WIDTH_PX}px !important;
    padding: 0 !important;
    margin: 0 !important;
  }

  /* In case the inner shadow/border is from .compact-card */
  .compact-card {
    border: none !important;
    outline: none !important;
    box-shadow: none !important;
  }

  /* Also stop any hover border effects */
  .serie-item:hover,
  .compact-card:hover {
    border: none !important;
    box-shadow: none !important;
  }
`;
document.head.appendChild(style);
  // revert to original if not compact mode (MUST reset styles here)
  if (!compactMode && item.dataset.compact === '1') {
    if (item.dataset.originalHTML) item.innerHTML = item.dataset.originalHTML;
    item.dataset.compact = '';
    item.style.width = '';
    item.style.padding = '';
    item.style.margin = '';
    item.style.boxSizing = '';
    return;
  }

  // store original HTML ONLY IF NOT ALREADY STORED
  if (!item.dataset.originalHTML) item.dataset.originalHTML = item.innerHTML;

  if (item.dataset.compact === '1') return; // already transformed
  item.dataset.compact = '1';

  const imageWrap = item.querySelector('.image-wrap');
  const btnLine = item.querySelector('.w-100.btn-line.merged, .btn-line.merged');
  const chCurrent = item.dataset.currentChapter || '';
  const chTotal = item.dataset.totalChapters || '';

  // --- Get title from detail-wrap ---
  const titleLink = item.querySelector('.detail-wrap a.title');
  const titleText = titleLink ? titleLink.textContent.trim() : 'Unknown';

  const infoLine = item.querySelector('.detail-wrap .info-line.d-flex');
  let progressText = '';
  if (infoLine) {
    // Find the span that contains "Progress:"
    const progressSpan = Array.from(infoLine.querySelectorAll('span')).find(s => s.textContent.includes('Progress:'));
    if (progressSpan) {
      progressText = progressSpan.textContent.trim().replace('Progress: ', '');
    }
  }


  // --- Card container ---
  const card = document.createElement('div');
  card.className = 'compact-card';
// --- Card container ---
Object.assign(card.style, {
  display: 'flex',
  flexDirection: 'column',
  alignItems: 'stretch',
  width: '121px',
  textAlign: 'left',
  padding: '0px',
  borderRadius: '0px',
  background: '#111',
  overflow: 'hidden',
  boxSizing: 'border-box',
  height: '100%',
});



  // --- Cover ---
  if (imageWrap) {
    imageWrap.style.width = '121px';
    imageWrap.style.display = 'block';
    imageWrap.style.margin = '0';
    const img = imageWrap.querySelector('img');
    if (img) {
      img.style.width = '121px';
      img.style.height = '200px';
      img.style.objectFit = 'cover';
      img.style.display = 'block';
    }
    card.appendChild(imageWrap);
  }

  // QOL: Create a content wrapper for padding
  const contentWrap = document.createElement('div');
// --- Content wrapper ---
Object.assign(contentWrap.style, {
  display: 'flex',
  flexDirection: 'column',
  gap: '4px',
  flexGrow: '1',
  width: '121px',
  minWidth: '0',
  maxWidth: '100%',
  boxSizing: 'border-box',
});
  card.appendChild(contentWrap);


// --- Title ---
const titleWrap = document.createElement('div');
titleWrap.textContent = titleText;
titleWrap.className = 'compact-title';
Object.assign(titleWrap.style, {
  fontSize: '12px',
  fontWeight: '500',
  lineHeight: '1.2',
  width: '100%',
  maxWidth: '100%',
  display: '-webkit-box',
  overflow: 'hidden',
  textOverflow: 'ellipsis',
  WebkitLineClamp: 4,              // limit to 2 lines max
  WebkitBoxOrient: 'vertical',     // vertical orientation for clamping
  wordBreak: 'break-word',
  whiteSpace: 'normal',
  margin: '0',
  padding: '0',
  color: '#fff',
  minHeight: `${1.2 * 12 * 2}px`,  // keep consistent height
});
contentWrap.appendChild(titleWrap);


  // --- Controls container ---
  const controls = document.createElement('div');
  Object.assign(controls.style, {
    display: 'flex',
    flexDirection: 'column',
    gap: '2px',
    width: '100%',
    margin: '0',
    padding: '0',
    alignItems: 'flex-start',
    marginTop: 'auto',
  });

  // --- Continue Button ---
  if (btnLine) {
    const continueBtn = btnLine.querySelector('a.btn, a.btn-dark, a.btn-secondary') || btnLine.querySelector('a');
    if (continueBtn) {
Object.assign(continueBtn.style, {
    display: 'block',
    width: '100%',
    height: '24px',
    lineHeight: '22px',
    fontSize: '13px',
    boxSizing: 'border-box',
    whiteSpace: 'nowrap',
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    margin: '0',
    padding: '0',
});
      controls.appendChild(continueBtn);
    }
  }

  // 💡 NEW 3: Progress Number below Continue Button
  if (progressText) {
    const progressDiv = document.createElement('div');
    progressDiv.textContent = `Progress: ${progressText}`;
    Object.assign(progressDiv.style, {
      fontSize: '11px',
      color: '#aaa',
      width: '100%',
      margin: '2px 0 0 0',
      padding: '0',
      textAlign: 'left',
      whiteSpace: 'nowrap',
      overflow: 'hidden',
      textOverflow: 'ellipsis',
    });
    controls.appendChild(progressDiv);
  }

  // --- Chapter Numbers (Original, now hidden) ---
  if (chCurrent && chTotal) {
    const chapterDiv = document.createElement('div');
    chapterDiv.textContent = `${chCurrent}/${chTotal}`;
    Object.assign(chapterDiv.style, {
      fontSize: '12px',
      color: '#555',
      width: '100%',
      margin: '0',
      padding: '0',
      textAlign: 'left',
      whiteSpace: 'nowrap',
      overflow: 'hidden',
      textOverflow: 'ellipsis',
      display: 'none',
    });
    controls.appendChild(chapterDiv);
  }

  contentWrap.appendChild(controls);

  // --- Remove original detail-wrap ---
  const detailWrap = item.querySelector('.detail-wrap');
  if (detailWrap) detailWrap.remove();

  // --- Finalize ---
  item.innerHTML = '';
  item.appendChild(card);
}


// --- Grid container ---
function ensureGridContainer(exampleItem) {
  if (!exampleItem) return null;
  const listParent = exampleItem.parentElement;
  let grid = document.querySelector('.compact-series-grid');
  if (grid) return grid;

  grid = document.createElement('div');
  grid.className = 'compact-series-grid';
  Object.assign(grid.style, {
    display: 'grid',
    gridAutoFlow: 'row',
    // --- MODIFIED --- This is the key fix.
      gridTemplateColumns: 'repeat(auto-fill, minmax(100px, 1fr))',
    gap: '8px',
    justifyContent: 'start',
    alignItems: 'stretch',
    width: '100%',
  });

  listParent.insertBefore(grid, listParent.firstChild);
  return grid;
}

// --- Apply to all ---
function restyleAll() {
  const items = document.querySelectorAll('.serie-item');
  if (!items.length) return;

  const grid = ensureGridContainer(items[0]);
  if (!grid) return;

  items.forEach(item => {
    transformItem(item);
    if (compactMode && !grid.contains(item)) grid.appendChild(item);
  });

  // Move grid items back to parent container if switching to original layout
  if (!compactMode) {
      qa('.serie-item').forEach(item => {
          if (item.parentElement === grid) {
              grid.parentElement.appendChild(item);
          }
      });
      grid.remove();
  }
}
// --- Apply to all ---
function restyleAll() {
  const items = document.querySelectorAll('.serie-item');
  if (!items.length) return;

  const grid = ensureGridContainer(items[0]);
  if (!grid) return; // safety check

  items.forEach(item => {
    transformItem(item);
    if (compactMode && !grid.contains(item)) grid.appendChild(item);
  });

}

function installFolderSwitchListener() {
  document.addEventListener('click', (e) => {
    const folderBtn = e.target.closest('.folder-btn');
    if (folderBtn) {
      console.log('[CompactGrid] Folder switch detected:', folderBtn.textContent.trim());
      // Delay slightly to allow new content to render
      setTimeout(() => {
          restyleAll();
          autoClickLoadMore(); // 🔹 restart auto-load-more
      }, 700);

      setTimeout(() => {
          restyleAll();
          autoClickLoadMore(); // run again for safety if async render
      }, 1500);
    }
  }, { passive: true });
}


// --- Initialize ---
function initCompactGrid() {
  compactMode = localStorage.getItem('compactMode') === '0' ? false : true;
  addGridToggleButton();
  restyleAll();
  window.addEventListener('resize', restyleAll);
}


  /* ---------- robust startup: poll until library exists (then stop) ---------- */
  function waitForLibraryAndInit() {
    const start = Date.now();
    const interval = setInterval(() => {
      const found = q('.serie-item');
      if (found) {
        clearInterval(interval);
        initCompactGrid(); // Call init *after* items are found
        // after 1 second more (in case more items append), run again
        // to tidy
        setTimeout(restyleAll, 900);
      } else if (Date.now() - start > POLL_TIMEOUT_MS) {
        clearInterval(interval);
        console.warn('compact layout: timed out waiting for .serie-item');
      }
    }, POLL_INTERVAL_MS);
  }

  /* ---------- capture any clicks on "Load More" via delegated listener ---------- */
  function installLoadMoreCatchAll() {
    document.addEventListener('click', (e) => {
      // matches the typical wrapper/button for load more — adapt selector if needed
      const lm = e.target.closest('.d-flex.w-100.load-more button, .load-more button, .load-more');
      if (lm) {
        // give DOM a bit of time to append new items
        setTimeout(restyleAll, AFTER_LOADMORE_RESTYLE_DELAY);
        // and run again a touch later to be safe
        setTimeout(restyleAll, AFTER_LOADMORE_RESTYLE_DELAY + 700);
      }
    }, { passive: true });
  }

let lastLoadTime = 0;  // prevent spamming clicks
const LOAD_COOLDOWN = 1500; // ms between clicks

function autoClickLoadMoreOnScroll() {
    window.addEventListener('scroll', () => {
        const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
        const windowHeight = window.innerHeight;
        const docHeight = document.documentElement.scrollHeight;

        const scrollPercent = (scrollTop + windowHeight) / docHeight;

        // Only trigger if scrolled past 70% and cooldown has passed
        if (scrollPercent > 0.7 && Date.now() - lastLoadTime > LOAD_COOLDOWN) {
            const loadMoreBtn = document.querySelector('.d-flex.w-100.load-more button');
            if (loadMoreBtn) {
                console.log('[AutoLoad] Clicking Load More at scroll', (scrollPercent*100).toFixed(0), '%');
                loadMoreBtn.click();
                lastLoadTime = Date.now();
            }
        }
    }, { passive: true });
}

// Run once to attach listener
autoClickLoadMoreOnScroll();




  /* ---------- start ---------- */
waitForLibraryAndInit();
installLoadMoreCatchAll();
installFolderSwitchListener();

  // also expose manual trigger in case you want to run it via console:
  window.reflowCompactLibrary = function () { restyleAll(); };

})();