Shinigami Auto Scroll

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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