Shinigami Auto Scroll

Auto scroll otomatis buat baca manga di Shinigami, lengkap dengan kontrol speed dan UI floating panel.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Shinigami Auto Scroll
// @namespace    https://greasyfork.org/users/SyaizZ
// @version      1.0.3
// @description  Auto scroll otomatis buat baca manga di Shinigami, lengkap dengan kontrol speed dan UI floating panel.
// @author       SyaizZ
// @match        https://*.shinigami.asia/chapter/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=shinigami.asia
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
  let scrolling = false;
  let animFrame;
  let speed = 3;
  let navbarVisible = true;

  function getOriPanel() {
    return document.querySelector('.fixed.bottom-0.left-0.w-screen');
  }

  function getOriSettingBtn() {
    const nb = document.querySelector('div.sticky.z-50');
    if (!nb) return null;
    return nb.querySelectorAll('button')[1];
  }

  function injectPlayToPanel() {
    const panel = getOriPanel();
    if (!panel) return;
    if (document.getElementById('sng-inject-play')) return;

    // Cek apakah ini versi prem (ada svg circle = icon play premium)
    const isPrem = !!panel.querySelector('svg circle[r="41.5"]');

    if (isPrem) {
      // Hide konten prem
      const premDiv = panel.querySelector('.text-center.text-base-white.gap-32');
      if (premDiv) premDiv.style.display = 'none';
    }

    // Cari save button row buat inject sebelumnya
    const saveRow = panel.querySelector('.flex.flex-row.gap-8.items-center.self-center');

    // Kalau ada slider ori, bajak
    const oriSlider = panel.querySelector('input[type="range"][min="50"][max="150"]');
    if (oriSlider) {
      oriSlider.addEventListener('input', function () {
        // map 50-150 ke 1-10
        speed = Math.round((parseInt(this.value) - 50) / 10);
        if (speed < 1) speed = 1;
      });
    }

    // Inject tombol play + delay slider sebelum save button
    const wrapper = document.createElement('div');
    wrapper.id = 'sng-inject-play';
    wrapper.innerHTML = `
      <div class="h-1 w-full bg-base-card"></div>
      <div class="md:text-24 text-20 font-semibold">🚀 Auto Scroll</div>
      ${!oriSlider ? `
      <div class="flex flex-col gap-8">
        <div style="font-size:14px;color:white">Speed: <span id="sng-speed-label">3</span></div>
        <input type="range" min="1" max="10" value="3" id="sng-speed-slider"
          class="w-full rounded-full h-6 accent-primary-500">
        <div style="display:flex;justify-content:space-between;font-size:12px;opacity:0.6;color:white">
          <span>Lambat</span><span>Cepat</span>
        </div>
      </div>
      ` : ''}
      <div class="flex flex-col gap-8">
        <div style="font-size:14px;color:white">Delay mulai: <span id="sng-delay-label">0</span> detik</div>
        <input type="range" min="0" max="10" value="0" id="sng-delay-slider"
          class="w-full rounded-full h-6 accent-primary-500">
        <div style="display:flex;justify-content:space-between;font-size:12px;opacity:0.6;color:white">
          <span>0s</span><span>10s</span>
        </div>
      </div>
      <div class="flex flex-row gap-8 items-center self-center">
        <button id="sng-start-btn"
          class="px-24 py-16 text-16 leading-22 rounded-8 bg-primary-500 text-base-white border-2 border-transparent transition-all duration-200 ease-in-out opacity-100 hover:opacity-80 font-medium min-w-190">
          ▶ Mulai Scroll
        </button>
      </div>
    `;

    const container = panel.querySelector('.flex.flex-col.gap-24.w-full');
    if (!container) return;

    if (saveRow) {
      container.insertBefore(wrapper, saveRow);
    } else {
      container.appendChild(wrapper);
    }

    // Speed slider custom (kalau gak prem)
    const customSpeedSlider = document.getElementById('sng-speed-slider');
    if (customSpeedSlider) {
      customSpeedSlider.addEventListener('input', function () {
        speed = parseInt(this.value);
        document.getElementById('sng-speed-label').textContent = this.value;
      });
    }

    document.getElementById('sng-delay-slider').addEventListener('input', function () {
      document.getElementById('sng-delay-label').textContent = this.value;
    });

    document.getElementById('sng-start-btn').addEventListener('click', () => {
      const delay = parseInt(document.getElementById('sng-delay-slider').value) * 1000;

      // Sync speed dari slider ori kalau ada
      if (oriSlider) {
        speed = Math.max(1, Math.round((parseInt(oriSlider.value) - 50) / 10));
      }

      // Tutup panel pake tombol X ori
      const closeBtn = panel.querySelector('button svg path[d*="10.5867"]')?.closest('button');
      if (closeBtn) closeBtn.click();

      if (delay > 0) {
        setTimeout(startScroll, delay);
      } else {
        startScroll();
      }
    });
  }

  function hijackSettingBtn() {
    const btn = getOriSettingBtn();
    if (!btn || btn.dataset.sngHijacked) return;
    btn.dataset.sngHijacked = 'true';

    btn.addEventListener('click', () => {
      const panelObs = new MutationObserver(() => {
        const panel = getOriPanel();
        if (!panel || document.getElementById('sng-inject-play')) return;
        const container = panel.querySelector('.flex.flex-col.gap-24.w-full');
        if (!container || container.children.length === 0) return;
        panelObs.disconnect();
        injectPlayToPanel();
      });
      panelObs.observe(document.body, { childList: true, subtree: true });
      setTimeout(() => panelObs.disconnect(), 3000);
    }, true);
  }

  function createPauseBtn() {
    if (document.getElementById('sng-pause-btn')) return;
    const pauseBtn = document.createElement('button');
    pauseBtn.id = 'sng-pause-btn';
    pauseBtn.innerHTML = `
      <svg width="22" height="22" viewBox="0 0 40 40" fill="none">
        <rect x="8" y="6" width="8" height="28" rx="2" fill="white"/>
        <rect x="24" y="6" width="8" height="28" rx="2" fill="white"/>
      </svg>`;
    pauseBtn.style.cssText = 'position:fixed!important;bottom:90px!important;left:50%!important;transform:translateX(-50%)!important;z-index:999999!important;background:#6F39EE!important;border:none!important;border-radius:50%!important;width:52px!important;height:52px!important;display:none!important;align-items:center!important;justify-content:center!important;cursor:pointer!important;box-shadow:0 4px 15px rgba(111,57,238,0.7)!important;';
    document.body.appendChild(pauseBtn);
    pauseBtn.addEventListener('click', stopScroll);
  }

  function oriNavbar() {
    return document.querySelector('div.sticky.z-50');
  }

  function showNavbar() {
    navbarVisible = true;
    const nb = oriNavbar();
    if (nb) {
      nb.style.opacity = '1';
      nb.style.transform = 'translateY(0)';
      nb.style.pointerEvents = 'auto';
    }
  }

  function hideNavbar() {
    navbarVisible = false;
    const nb = oriNavbar();
    if (nb) {
      nb.style.opacity = '0';
      nb.style.transform = 'translateY(20px)';
      nb.style.pointerEvents = 'none';
    }
  }

  function startScroll() {
    scrolling = true;
    hideNavbar();
    const pauseBtn = document.getElementById('sng-pause-btn');
    if (pauseBtn) pauseBtn.style.display = 'flex';
    doScroll();
  }

  function stopScroll() {
    scrolling = false;
    cancelAnimationFrame(animFrame);
    const pauseBtn = document.getElementById('sng-pause-btn');
    if (pauseBtn) pauseBtn.style.display = 'none';
    showNavbar();
  }

  function doScroll() {
    if (!scrolling) return;
    window.scrollBy(0, speed);
    if ((window.innerHeight + window.scrollY) >= document.body.scrollHeight - 10) {
      stopScroll();
      return;
    }
    animFrame = requestAnimationFrame(doScroll);
  }

  let tapStartX = 0, tapStartY = 0, tapStartTime = 0;

  document.addEventListener('touchstart', e => {
    const nb = oriNavbar();
    const panel = getOriPanel();
    if (nb?.contains(e.target) || panel?.contains(e.target)) return;
    tapStartX = e.touches[0].clientX;
    tapStartY = e.touches[0].clientY;
    tapStartTime = Date.now();
  }, { passive: true });

  document.addEventListener('touchend', e => {
    const nb = oriNavbar();
    const panel = getOriPanel();
    if (nb?.contains(e.target) || panel?.contains(e.target)) return;
    const dx = Math.abs(e.changedTouches[0].clientX - tapStartX);
    const dy = Math.abs(e.changedTouches[0].clientY - tapStartY);
    const dt = Date.now() - tapStartTime;
    if (dx < 10 && dy < 10 && dt < 250) {
      if (scrolling) { stopScroll(); return; }
      navbarVisible ? hideNavbar() : showNavbar();
    }
  }, { passive: true });

  function init() {
    if (document.getElementById('sng-pause-btn')) return;
    const nb = document.querySelector('div.sticky.z-50');
    if (!nb) return;

    nb.style.transition = 'opacity 0.3s, transform 0.3s';
    createPauseBtn();
    hijackSettingBtn();

    const obs = new MutationObserver(() => {
      hijackSettingBtn();
      const panel = getOriPanel();
      if (panel && panel.offsetParent !== null && !document.getElementById('sng-inject-play')) {
        const container = panel.querySelector('.flex.flex-col.gap-24.w-full');
        if (container && container.children.length > 0) {
          injectPlayToPanel();
        }
      }
    });
    obs.observe(document.body, { childList: true, subtree: true });
  }

  const observer = new MutationObserver(() => {
    const komik = document.querySelector('button.max-w-800 img');
    if (komik) {
      observer.disconnect();
      setTimeout(init, 500);
    }
  });

  observer.observe(document.documentElement, { childList: true, subtree: true });
  setTimeout(init, 3000);
  setTimeout(init, 5000);
})();