Auto scroll otomatis buat baca manga di Shinigami, lengkap dengan kontrol speed dan UI floating panel.
// ==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);
})();