您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Hitomi.la is very user unfriendly, this script aims to fix that.
// ==UserScript== // @name Hitomi.la Cleanser - because fuck the dev! // @namespace http://tampermonkey.net/ // @version 0.1 // @description Hitomi.la is very user unfriendly, this script aims to fix that. // @match *://hitomi.la/* // @match *://*.hitomi.la/* // @match *://gold-usergeneratedcontent.net/* // @match *://*.gold-usergeneratedcontent.net/* // @grant unsafeWindow // @run-at document-start // ==/UserScript== const Win = unsafeWindow const interval = 50 const panicAfter = 3e3 / interval // This intercepts a variable, function, etc. that the dev has assigned const Patcher = class { constructor(name, type, desired) { this.name = name this.type = type this.desired = desired this.panicCount = 0 this.handle = setInterval(() => this.tick(), interval) } tick() { this.panicCount++ // give up after a few seconds if (this.panicCount > panicAfter) { console.warn(`Patcher: never saw ${this.name} as a ${this.type}`) clearInterval(this.handle) return } // only patch when the page actually sets it if (typeof Win[this.name] === this.type) { // store the original in case we need it later or something this.original = Win[this.name] Win[this.name] = this.desired console.log(`Patcher: overridden ${this.name}`) clearInterval(this.handle) } } } const BSS = {} // this is a STRICT whitelist for things that can come in and out of the page // this gives ads the middle finger and tells them to go fuck themselves const policy = [ "default-src 'self' https://hitomi.la https://*.hitomi.la https://*.gold-usergeneratedcontent.net", "script-src 'self' https://hitomi.la https://*.hitomi.la https://*.gold-usergeneratedcontent.net 'unsafe-inline'", "style-src 'self' https://hitomi.la https://*.hitomi.la https://*.gold-usergeneratedcontent.net 'unsafe-inline'", "img-src * data:", "connect-src 'self' https://hitomi.la https://*.hitomi.la https://*.gold-usergeneratedcontent.net", "frame-ancestors 'none'" ].join('; ') // a whitelist for us to refer to when cleansing containers of otherwise reserved ad content const classWhitelist = [ 'list-title', 'lillie', 'cover-column', 'gallery-preview', 'gallery', 'acg-gallery', 'posts', ] const FuckOff = { // stops ads in their tracks ads() { const meta = document.createElement('meta') meta.setAttribute('http-equiv', 'Content-Security-Policy') meta.setAttribute('content', policy) // prepend before any other <head> content const root = document.head || document.documentElement root.insertBefore(meta, root.firstChild) // stub XHR & fetch to known ad hosts const adHostsRE = /(?:a\.magsrv\.com|s\.magsrv\.com|js\.wpadmngr\.com|chaseherbalpasty\.com|adsco\.re|displayvertising\.com|adsco\.re|qrivescript\.min\.js)/i // XMLHttpRequest.open const origOpen = XMLHttpRequest.prototype.open XMLHttpRequest.prototype.open = function(method, url, ...args) { if (adHostsRE.test(url)) { console.log('XHR.open blocked:', url) return } return origOpen.call(this, method, url, ...args) } // fetch() override const origFetch = window.fetch window.fetch = function(input, init) { let url = typeof input === 'string' ? input : (input && input.url) || '' if (adHostsRE.test(url)) { console.log('Fetch blocked:', url) return Promise.resolve(new Response('', { status: 204 // leave us the fuck alone })) } return origFetch(input, init) } }, adContainers(name) { setTimeout(() => { // remove space reserved for ads let el = document.getElementsByClassName(name) // snapshot the children into an array for (let child of Array.from(el[0].children)) { let ok = [...child.classList].some(c => classWhitelist.includes(c)) if (!ok) { child.remove() } } }, 2e3) }, popunder() { // STOP REDIRECTING PEOPLE FOR TRYING TO NAVIGATE THE DAMN PAGE JFC let el = document.getElementById('popmagicldr') if (el) { el.remove() } }, btcBegging() { // This mfer does not need any more money let container = document.getElementsByClassName('donate') container[0].parentNode.removeChild(container[0]) }, mutationObserver() { // NOP out of any MutationObserver so the page "webp shim" or other mutation-based tricks never fire Win.MutationObserver = class { observe() {} disconnect() {} } }, contextMenuBlocking() { // listen in capture phase and stop propagation of any blocking handlers. document.addEventListener('contextmenu', e => { // re‑enable the normal right‑click menu on all images if (e.target && e.target.tagName === 'IMG') { e.stopImmediatePropagation() } }, true) } } const MainTweaks = () => { // dont really need to touch anything here yet } const ReaderTweaks = () => { // intercept createImageBitMap and show_original values so we dont get a FUCKING AVIF BSS.showOriginal = new Patcher('show_original', 'boolean', true) BSS.createImgBitmap = new Patcher('createImageBitmap', 'function', fileBlob => { document.querySelectorAll('.lillie').forEach(img => { img.src = URL.createObjectURL(fileBlob) }) }) // this fucking guy has the panel/scene jump buttons backwards BSS.prevPanel = new Patcher('prev_panel', 'function', () => BSS.nextPanel.original.apply(this)) BSS.nextPanel = new Patcher('next_panel', 'function', () => BSS.prevPanel.original.apply(this)) BSS.prevScene = new Patcher('prev_scene', 'function', () => BSS.nextScene.original.apply(this)) BSS.nextScene = new Patcher('next_scene', 'function', () => BSS.prevScene.original.apply(this)) BSS.atFirstScene = new Patcher('at_first_scene', 'function', () => BSS.atLastScene.original.apply(this)) BSS.atLastScene = new Patcher('at_last_scene', 'function', () => BSS.atFirstScene.original.apply(this)) FuckOff.mutationObserver() FuckOff.contextMenuBlocking() } ;(() => { // are we on the frontpage, or a reader? const isReader = location.pathname.startsWith('/reader/') const isMain = location.pathname === '/' || location.pathname.startsWith('/hitomi.la/') console.log('Script injected on ', location.href) // clear these fucking ads FuckOff.ads() FuckOff.adContainers('top-content') FuckOff.adContainers('content') setTimeout(() => { FuckOff.popunder() FuckOff.btcBegging() }, 2e3) // page wide overrides if (isMain) { MainTweaks() } // reader only image overrides if (isReader) { ReaderTweaks() // inject a download button into the reader nav bar let addDownloadButton = () => { let nav = document.querySelector('ul.nav.navbar-nav') if (!nav || nav.querySelector('#downloadButton')) { return } let li = document.createElement('li') let a = document.createElement('a') a.id = 'downloadButton' a.href = '#' a.title = 'Download' let icon = document.createElement('i') icon.className = 'icon-download icon-white' a.appendChild(icon) a.appendChild(document.createTextNode(' Download')) a.addEventListener('click', async (e) => { e.preventDefault() let img = document.querySelector('.lillie') if (!img || !img.src) { return alert('No image to download!') } try { let resp = await fetch(img.src, { mode: 'cors' }) if (!resp.ok) { throw new Error(`HTTP ${resp.status}`) } let blob = await resp.blob() let blobUrl = URL.createObjectURL(blob) let link = document.createElement('a') link.href = blobUrl const parts = img.src.split('/').pop().split('?')[0] link.download = parts || 'image' document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(blobUrl) } catch (err) { console.error('Download failed:', err) alert('Download failed: ' + err.message) } }) li.appendChild(a) nav.appendChild(li) } // wait until nav is available let tries = 0 let maxTries = panicAfter let iv = setInterval(() => { tries++; if (tries >= maxTries) { clearInterval(iv) return } addDownloadButton() if (document.querySelector('#downloadButton')) { clearInterval(iv) } }, interval) } })()