Shinigami Auto Scroll

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

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==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);
})();