Greasy Fork is available in English.

Gridify

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

// ==UserScript==
// @name        Gridify
// @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 = 122; // --- MODIFIED --- Set to original cover width
  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; // default, remembers toggle via localStorage

// --- Transform single item ---
function transformItem(item) {
    if (!item) return;

    const card = item.querySelector('.compact-card');

    if (!compactMode) {
        // --- REVERT to original layout ---
        if (item.dataset.originalHTML) {
            item.innerHTML = item.dataset.originalHTML;
            item.dataset.compact = '';
        }
        return;
    }

    // already transformed? skip
    if (card) return;

    // store original HTML once
    if (!item.dataset.originalHTML) item.dataset.originalHTML = item.innerHTML;

    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
    const titleLink = item.querySelector('.detail-wrap a.title');
    const titleText = titleLink ? titleLink.textContent.trim() : 'Unknown';

    // Extract Progress text
    let progressText = '';
    const spans = item.querySelectorAll('.detail-wrap span');
    spans.forEach(s => {
        if (/Progress:/i.test(s.textContent)) {
            progressText = s.textContent.replace(/Progress:\s*/i, '').trim();
        }
    });

    // Create Card container
    const newCard = document.createElement('div');
    newCard.className = 'compact-card';
    Object.assign(newCard.style, {
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'stretch',
        textAlign: 'left',
        padding: '0',
        borderRadius: '8px',
        background: '#111',
        overflow: 'hidden',
        position: 'relative',
        boxShadow: '0 2px 6px rgba(0,0,0,0.08)',
        boxSizing: 'border-box',
        height: '100%',
    });

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

    const contentWrap = document.createElement('div');
    Object.assign(contentWrap.style, {
        padding: '4px 8px 8px 8px',
        display: 'flex',
        flexDirection: 'column',
        flexGrow: '1',
        gap: '4px',
    });
    newCard.appendChild(contentWrap);

    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%',
        display: '-webkit-box',
        WebkitLineClamp: '5',
        WebkitBoxOrient: 'vertical',
        overflow: 'hidden',
        textOverflow: 'ellipsis',
        margin: '0',
        padding: '0',
        position: 'relative',
        textAlign: 'left',
        wordBreak: 'break-word',
        minHeight: '28.8px',
        color: '#fff',
    });
    contentWrap.appendChild(titleWrap);

    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',
    });

    if (btnLine) {
        const continueBtn = btnLine.querySelector('a.btn, a.btn-dark, a.btn-secondary') || btnLine.querySelector('a');
        if (continueBtn) {
            Object.assign(continueBtn.style, {
                display: 'inline-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);
        }
    }

    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);
    }

    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',
        });
        controls.appendChild(chapterDiv);
    }

    contentWrap.appendChild(controls);

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

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





// --- 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: '2px',
    justifyContent: 'start', // Aligns grid to the left
    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; // safety check

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

}

// --- Grid toggle button ---


// --- 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 lastFoundTime = Date.now();

function autoClickLoadMore() {
    const loadMoreBtn = document.querySelector('.d-flex.w-100.load-more button');
    if (loadMoreBtn) {
        console.log('[AutoLoad] Clicking Load More');
        loadMoreBtn.click();
        lastFoundTime = Date.now(); // reset last found time
    }
}

// Run once on load and occasionally retry (in case button reappears)
const autoClickInterval = setInterval(() => {
    autoClickLoadMore();

    // stop if no button found for 10 seconds
    if (Date.now() - lastFoundTime > 10000) {
        console.log('[AutoLoad] No Load More button found for 10s, stopping.');
        clearInterval(autoClickInterval);
    }
}, 3000);

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

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

})();