Sleazy Fork is available in English.

hitomi.la 小优化

优化在线阅读,分包下载,下载加速,内容列表标题自动换行,标签汉化,点击内容列表预览图新标签页打开,非搜索增加选页功能,内容列表的标签支持展开(默认显示10个)

2022-07-26 일자. 최신 버전을 확인하세요.

질문, 리뷰하거나, 이 스크립트를 신고하세요.
// ==UserScript==
// @name         hitomi.la 小优化
// @namespace    https://greasyfork.org/zh-CN/users/200067#1
// @version      1.92
// @description  优化在线阅读,分包下载,下载加速,内容列表标题自动换行,标签汉化,点击内容列表预览图新标签页打开,非搜索增加选页功能,内容列表的标签支持展开(默认显示10个)
// @author       不会英语会写点代码的小白
// @match        http*://hitomi.la/*
// @run-at       document-start
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      mirror.ghproxy.com
// @connect      githubusercontent.com
// @connect      github.com
// @connect      download.fastgit.org
// @connect      gh.gh2233.ml
// ==/UserScript==

(() => {
	'use strict'
	let json, isT = !0, $, isOne = !0
	const app = {
		start () {
			if (window.load_ehtagtranslation_db_text) return
			window.load_ehtagtranslation_db_text = (db) => {
				let array = db.data
				db = {
					TMap: {
						'tags': '标签',
						'artists': '艺术家',
						'series': '原作',
						'characters': '角色',
						'language': '语言',
						'type': '类型',
						'group': '团队'
					},
					aliasMap: {
						// 标签
						'loli': 'lolicon',
						'shota': 'shotacon',
						// 类型
						'artist CG': 'artistcg',
						'game CG': 'gamecg'
					},
					alias (str) {
						const str2 = this.aliasMap[str]
						return str2 || str.toLowerCase()
					}
				}
				for (const j of array) db[j.namespace] = j
				json = db
				array = null
				if ($ && isT) (isT = !1, app.translate(), isT = !0)
			}
			if (location.hash.startsWith('#VIEW')) this.location_hash = location.hash

			let is_add_script = !0
			const xhr_onload = (r) => {
				if (r.responseText) {
					if (r.responseText.includes('load_ehtagtranslation_db_')) {
						GM_setValue('js_last_time', new Date().getTime())
						GM_setValue('js_text', r.responseText)
					} else {
						xhr.onerror('数据库加载失败: ' + xhr.url)
						return
					}
				}
				if (is_add_script) {
					is_add_script = !1
					const data = r.response
					const i = document.createElement('script')
					i.type = 'text/javascript'
					i.src = URL.createObjectURL(data)
					document.head.appendChild(i)
				}
			}
			const js_text = GM_getValue('js_text', '')
			if (js_text.includes('load_ehtagtranslation_db_')) {
				xhr_onload({ response: new Blob([js_text], { type: 'application/x-javascript' }) })
				if (GM_getValue('js_last_time', 0) + 86400000 > new Date().getTime()) return
			}
			let index = 0
			const urls = [
					'https://mirror.ghproxy.com/https://github.com/EhTagTranslation/Database/releases/latest/download/db.text.js',
					'https://download.fastgit.org/EhTagTranslation/Database/releases/latest/download/db.text.js',
					'https://gh.gh2233.ml/https://github.com/EhTagTranslation/Database/releases/latest/download/db.text.js',
					'https://github.com/EhTagTranslation/Database/releases/latest/download/db.text.js'
				], isHttps = location.protocol === 'https:',
				xhr = {
					method: 'GET',
					responseType: 'blob',
					timeout: 5000,
					onload: xhr_onload,
					onerror: (er) => {
						console.error(er)
						if (index >= urls.length * 2) {
							alert('用于汉化标签的数据库加载失败,请确保网络可以访问github.com后刷新网页重试')
							return
						}
						GM_xmlhttpRequest(xhr)
					},
					get url () {
						const url = urls[index++ % 4]
						const urlIsHttps = url.startsWith('https://')
						return (isHttps && !urlIsHttps) ? url.replace('http://', 'https://') : (!isHttps && urlIsHttps) ? url.replace('https://', 'http://') : url
					}
				}
			xhr.ontimeout = xhr.onerror
			xhr.onabort = xhr.onerror
			GM_xmlhttpRequest(xhr)
		},
		body () {
			const _limitLists = window.limitLists
			if (_limitLists) {
				window.limitLists = function () {
					$ = window.jQuery
					if (document.querySelector('.gallery-content > div')) {
						(json && isT) && (isT = !1, app.translate(), isT = !0)
						app.main()
						isOne = !1
					} else return _limitLists.apply(this, arguments)
				}
			}
		},
		end () {
			if (location.pathname.startsWith('/reader/')) {
				$ = window.jQuery
				// 改变hash不添加历史
				const script = document.createElement('script')
				script.type = 'text/javascript'
				script.innerHTML = `
'use strict'
${window.singlePageChange.toString().replace(/window.location.hash *= *(val);/, "window.location.replace(location.pathname+'#'+$1);")}
${window.twoPageChange.toString().replace(/window.location.hash *= *(val);/, "window.location.replace(location.pathname+'#'+$1);")}
${window.mobile_singlePageChange.toString().replace(/window.location.hash *= *(val);/, "window.location.replace(location.pathname+'#'+$1);")}
${window.mobile_twoPageChange.toString().replace(/window.location.hash *= *(val);/, "window.location.replace(location.pathname+'#'+$1);")}
`
				document.body.appendChild(script)
				const make_image_element_ = window.make_image_element
				window.make_image_element = function make_image_element () {
					if (arguments.length > 4) { // 取消 img.onmouseover 事件
						arguments[4] = undefined
					}
					return make_image_element_.apply(this, arguments)
				}
				const addEventListener_ = document.addEventListener
				document.addEventListener = function () {
					if (arguments[0] === 'touchmove') {
						arguments[2] = ({ passive: !1 })// 修复 preventDefault报错
						arguments.length = 3
						document.addEventListener = addEventListener_
					}
					return addEventListener_.apply(this, arguments)
				}
				if (window.parent === window) return
				// iframe
				$('.mobile-navbar-inner>.gallery-link').remove()
				$('.mobile-navbar-inner>.mobile-nav-right').css('float', 'left')
				$('.container>a.brand').remove()
				$('.container>.btn-navbar').css('float', 'left')
				$('ul.pull-right').css('float', 'left')
				const hashchange = () => {
					const hash = location.hash
					window.parent.postMessage((hash === '') ? '#1' : hash) // 向父页面发送消息
				}
				window.addEventListener('hashchange', hashchange, !1)
			}
		},
		translate () {
			(this.translate = (() => {
				let translateList
				function each_fun () {
					let obj = /^(.+)( [♀♂])/.exec(this.innerHTML), str
					if (obj) {
						str = obj[2] === ' ♀' ? 'female' : 'male'
						const map = json[str].data[json.alias(obj[1])]
						if (map) {
							this.title = map.intro
							this.innerHTML = map.name + obj[2]
							return
						}
						this.innerHTML = obj[1]
					}
					const str2 = json.alias(this.innerHTML)
					for (const val of translateList) {
						if (val !== str) {
							obj = json[val].data[str2]
							if (obj) {
								this.title = obj.intro
								this.innerHTML = obj.name + (str || (val === 'female' ? '♀' : val === 'male' ? '♂' : ''))
								break
							}
						}
					}
				}
				function each_fun2 () {
					this.title = this.innerHTML
					const str = json.TMap[this.innerHTML.toLowerCase()]
					if (str) this.innerHTML = str
				}
				function each_fun3 () {
					const str = json.alias(this.innerHTML)
					let map, val
					for (val of translateList) {
						map = json[val].data[str]
						if (map) {
							this.innerHTML = map.name
							this.title = map.intro
							break
						}
					}
				}
				const is = $('#dl-button').length, lang = $('nav > ul > li#lang > a')
				// 工具栏
				$('nav > ul > li > a[href]').each(each_fun2)
				if (lang.length) {
					const el2 = $('nav > ul #lang-list')
					if (!el2.children()) {
						lang.parent().hide()
					} else {
						lang.html(lang.children())
						lang.prepend('语言 ')
					}
				}
				return function () {
					translateList = ['female', 'male', 'other', 'mixed', 'artist', 'character', 'cosplayer', 'group', 'parody']
					$('.dj-content td.relatedtags a').each(each_fun) // 列表的标签
					$('.dj-content tr > td:nth-of-type(1)').each(each_fun2) // 列表的行名

					if (is) {
						$('.gallery-info ul#tags a').each(each_fun) // 详情页的标签
						translateList.splice(5, 1)
						translateList.unshift('character')
						$('.gallery-info ul#characters a').each(each_fun) // 详情页的角色
						$('.gallery-info tr > td:nth-of-type(1)').each(each_fun2) // 详情页的行名
					}

					// 原作
					translateList = ['parody']
					$('.dj-content tr:nth-of-type(1) > td:nth-of-type(2) a').each(each_fun3)
					if (is) {
						$('.gallery-info tr:nth-of-type(4) > td:nth-of-type(2) a').each(each_fun3)
						translateList = ['group'] // 团队
						$('.gallery-info tr:nth-of-type(1) > td:nth-of-type(2) a').each(each_fun3)
					}
					// 类型
					translateList = ['reclass']
					$('.dj-content tr:nth-of-type(2) > td:nth-of-type(2) a').each(each_fun3)
					if (is) {
						const el = $('.gallery-info tr:nth-of-type(2) > td:nth-of-type(2)')
						el.html('<a href="/type/' + el.text() + '-all.html">' + el.text() + '</a>')
						el.find('a').each(each_fun3)
					}
				}
			})())()
		},
		main () {
			(this.main = (() => {
				let limit
				function each_fun () {
					const el = $(this)
					if (el.index() >= limit) {
						el.addClass('hidden-list-item2')
					}
				}
				function onclick () {
					const el = $(this).parent().find('.hidden-list-item2')
					$(this).children().html(el.is(':hidden') ? '收起&lt;' : '展开&gt;')
					el.toggle()
				}
				function each_fun2 () {
					const el = $(this), el2 = el.children()
					if (el2.length > limit) {
						el.find('.hidden-list-item2').hide()
						el2.eq(9).after($('<li><a style="cursor: pointer; color:aqua">展开&gt;</a></li>').on('click', onclick))
					}
				}
				GM_addStyle(`
nav > ul > li#lang > a {
    padding: 10px 70px 10px 15px;
}
.gallery-content > div > h1.lillie {
    white-space: normal;
}
@media (max-width : 768px) {
    .dj-img-cont{
        position: sticky;
    }
}

`)
				const paddingTop = (document.body.clientWidth === 768 ? '35px' : '6px')
				function each_fun3 () {
					const d = this.querySelector('a.lillie')
					d.remove()
					const el = this.querySelector('div.dj-content')
					el.style.paddingTop = paddingTop
					el.prepend(d)
				}
				return function () {
					limit = 10
					$('.relatedtags li').each(each_fun)
					$('.relatedtags ul').each(each_fun2)

					limit = 5
					$('.artist-list li').each(each_fun)
					$('.artist-list ul').each(each_fun2)

					const dom = $('.gallery-content > div')
					if (dom.length) {
						dom.find('a.lillie').attr('target', '_blank')
						if (document.body.clientWidth < 769) {
							dom.each(each_fun3)
						}
					}
				}
			})())()

			const dlbt = $('#dl-button')
			if (!dlbt.length) {
				// 增加选页功能
				if (location.pathname !== '/search.html') {
					$('.gallery-content').before($('.page-container').clone())
				}
				return
			}
			dlbt.attr('href', 'javascript:void(0);').children().html('下载')
			const files = window.galleryinfo.files,
				files_length = files.length,
				url_from_url_from_hash = window.url_from_url_from_hash
			// 下载按钮扩展
			{
				GM_addStyle(`
.cover > a , #_div-dlP_ {
    text-align: center;
    display: block;
}
#_div-dlP_ span, #_div-dlP_ label, #_div-dlP_ input {
    vertical-align: middle;
    font-size: 14px;
}
.gallery-info > table {
    table-layout: fixed;/*限制tag长度*/
}
`)

				const div1 = $('<div id="_div-dlP_" style="line-height: 25px;"/>')
				let in2, in1, rd2, rd3, in3, rd5, rd4
				div1.append([
					in2 = $('<span>下载并发数:</span><input type="number" style="width:38px" id="_in2_"/>'),
					'<input type="radio" name="_radio1_" style="" id="_rd1_"/><label for="_rd1_">分包下载数:</label>',
					in1 = $('<input type="number"  style="width:38px;" id="_in1_"/>'),
					rd2 = $('<br/><input type="radio" name="_radio1_" style="margin-left:4px;" id="_rd2_"/><label for="_rd2_">不打包下载</label>'),
					rd3 = $('<input type="radio" name="_radio1_" style="margin-left:4px;" id="_rd3_"/><label for="_rd3_">按大小分包:</label>'),
					in3 = $('<input type="number" style="width:55px" id="_in3_"/><span>M</span>'),
					rd4 = $('<br/><span>图片格式:</span><input type="radio" name="_radio2_" style="margin-left:4px;" id="_rd4_" v="1"/><label for="_rd4_">webp</label>'),
					rd5 = $('<input type="radio" name="_radio2_" style="margin-left:4px;" id="_rd5_" v="2"/><label for="_rd5_">avif</label>')
				]);
				(in2 = in2.eq(1), rd2 = rd2.eq(1), rd3 = rd3.eq(0), in3 = in3.eq(0), rd4 = rd4.eq(2), rd5 = rd5.eq(0),
				in1.val(GM_getValue('_in1_', '1')), in2.val(GM_getValue('_in2_', '1')), in3.val(GM_getValue('_in3_', '1024')))
				const rd_change = (e) => {
					const rd = $(e.target)
					GM_setValue(rd.attr('name'), rd.attr('id'))
				}
				div1.find('input[type=radio][name=_radio1_]#' + GM_getValue('_radio1_', '_rd1_')).attr('checked', 'checked')
				div1.find('input[type=radio][name=_radio1_]').on('change', rd_change)
				div1.find('input[type=radio][name=_radio2_]#' + GM_getValue('_radio2_', '_rd4_')).attr('checked', 'checked')
				div1.find('input[type=radio][name=_radio2_]').on('change', rd_change)
				div1.find('input[type=number]').on('change', (e) => {
					const num = $(e.target)
					GM_setValue(num.attr('id'), num.val())
				})

				const dlt = $('<span style="position:absolute;left:0px;right:0px;vertical-align:middle;"/>'),
					progressbar = $('#progressbar')
				progressbar.append(dlt)
				progressbar.css({
					'text-align': 'center',
					'position': 'relative'
				})
				progressbar.after(div1)

				const get_fileName = (image) => {
					return image.name.replace(/[^.]*$/, rd4.is(':checked') ? 'webp' : 'avif')
				}

				$('.gallery h1 a').parent().prepend('(' + files_length + ')')
				const JSZip = window.JSZip
				window.download_gallery = function (galleryname) {
					const t = new Date().getTime()
					if (!galleryname)galleryname = 'hitomi'
					dlbt.hide()
					dlt.html('0/' + files_length)
					progressbar.show()
					progressbar.progressbar({ value: !1 })

					let in2v = parseInt(in2.val()),
						responseType
					const p = [],
						rd2b = rd2.is(':checked'),
						rd3b = rd3.is(':checked')

					if (in2v < 1)in2v = 1

					if (rd2b) {
						responseType = 'blob'
					} else {
						responseType = 'arraybuffer'
						JSZip.prototype.length = 0
						var val_, ii4 = 0, ii2
						if (rd3b) {
							var max = 0, ii7 = 1
							JSZip.prototype.max = 0
							JSZip.prototype.size = 0
							ii4 = files_length - 20
							val_ = parseInt(in3.val())
							val_ = (val_ < 50 ? 50 : val_ > 2048 ? 2048 : val_) * 1048576// 限制大小50M-2048M
						} else if ((val_ = parseInt(in1.val())) > 1) {
							ii2 = 1
							val_ = parseInt(files_length / val_)
							if (val_ < 50)val_ = 50
							ii4 = parseInt(files_length / val_) - (files_length % val_ < 20 ? 1 : 0)
						}
						var ii5 = 0, ii6 = 0, zipName = galleryname, pp, zip, zips = [new JSZip()]
					}

					function dl (url, index, isRetry) {
						new Promise((resolve, reject) => {
							const xhr = new XMLHttpRequest()
							xhr.onreadystatechange = function () {
								if (this.readyState === 4) {
									if (this.status === 200) {
										resolve(this.response)
									} else {
										reject(new Error(`dl(${url}) failed, xhr.status: ${this.status}`))
									}
								}
							}
							xhr.open('GET', url)
							xhr.responseType = responseType
							xhr.send()
						}).then(re => {
							ii--
							re.index = index
							p.push(re)
							dlt.html(p.length + '/' + files_length)
							progressbar.progressbar('value', p.length / files_length * 100)
						}).catch(er => {
							if (isRetry) {
								setTimeout(dl, 500, url, index, isRetry - 1)
							}
							console.error(er)
						})
					}
					let c = 0, ii = 0, ii3 = 0
					function dl2 () {
						do {
							if (ii3 < p.length) {
								pp = p[ii3]
								delete p[ii3++]
								if (rd2b) {
									app.saveAs(pp, galleryname + '_' + get_fileName(files[pp.index]))
									break
								}
								if (rd3b) {
									ii2 = pp.byteLength
									ii5 = ii6
									if (pp.index > max) {
										if (ii7) {
											max = pp.index
										} else ii5++
									}
								} else if (ii4) {
									ii5 = Math.floor(pp.index / val_)
									if (ii5 > ii4)ii5 = ii4
								}
								zip = zips[ii5]
								if (!zip) {
									zips.push(zip = new JSZip())
									if (rd3b)zip.size = (ii7 || max) + 2
								} else if (rd3b)zip.size++
								zip.file(get_fileName(files[pp.index]), pp)
								if (
									ii3 === files_length || (
										ii4 !== ii5 && (zip.length += ii2) >= val_ && (
											!rd3b || (zip.size < ii4 && (ii7 = zip.size > max ? max : 0))
										)
									)
								) {
									const saveName = zipName + '.zip'
									zip.generateAsync({ type: 'blob' }).then((content) => {
										app.saveAs(content, saveName)
									})
									delete zip.length
									delete zips[ii5]
									zipName = galleryname + ' (' + (++ii6) + ')'
								}
							} else break
						} while (1)
						while (ii < in2v) {
							if (c < files_length) {
								dl(url_from_url_from_hash(window.galleryid, files[c], rd4.is(':checked') ? 'webp' : 'avif', undefined, 'a'), c++, 100)
								ii++
							} else break
						}
						if (ii3 < files_length) {
							setTimeout(dl2, rd2b ? (c === files_length ? 200 : 100) : 50)
						} else {
							zip = null
							progressbar.hide()
							dlbt.show()
							p.length = 0
							// console.log('下载时间:'+(new Date().getTime()-t)/1000+'秒');
						}
					}
					dl2()
				}
			}
			// 在线阅读
			{
				GM_addStyle(`
#_VIEW_ {
	 height: 100%;
	 width: 100%;
	 position: fixed;
	 z-index: 99998;
}
#_VIEW_>iframe {
	 height: 100%;
	 width: 100%;
}
#_VIEW_>.lum-close-button {
		cursor: pointer;
    position: absolute;
    right: 5px;
    top: 3px;
    width: 32px;
    height: 32px;
		z-index: 99999;
    opacity: 1
}
#_VIEW_>.lum-close-button:hover {
    opacity: .7
}
#_VIEW_>.lum-close-button:after,.lum-close-button:before {
    position: absolute;
    left: 15px;
    content: " ";
    height: 33px;
    width: 2px;
    background-color: #fff;
}
#_VIEW_>.lum-close-button:before {
    transform: rotate(45deg)
}
#_VIEW_>.lum-close-button:after {
    transform: rotate(-45deg)
}
`)

				const ro = $('.cover-column > a:eq(0)').children()
				ro.html('在线阅读')
				const reader_href = ro.parent().attr('href')
				const view = $(`
<div tabindex="1" style="display: none;">
    <div id="_VIEW_">
        <iframe src="${reader_href}#1"/>
        <div class="lum-close-button"/>
     </div>
</div>`)
				document.body.prepend(view[0])
				const hideView = (e) => {
					view.hide()
					if (typeof e !== 'undefined') {
						window.history.back(-1)
						e.stopPropagation()
						e.preventDefault()
					}
				}
				view.find('.lum-close-button').click(hideView)

				const _get_pagenum_hash = window.get_pagenum_hash
				window.get_pagenum_hash = function () {
					if (location.hash.startsWith('#VIEW')) {
						if (view.is(':hidden')) {
							view.show()
							ro.html('继续阅读')
						}
						const iframe_hash = reader_href + '#' + location.hash.substring(5)
						$('#gallery-brand a[href*=reader]').attr('href', iframe_hash)
						$('.cover-column a[href*=reader]').attr('href', iframe_hash)
						view.find('#_VIEW_>iframe')[0].contentWindow.location.replace(iframe_hash)
						return files.length
					} else if (!view.is(':hidden'))hideView()
					return _get_pagenum_hash.apply(this, arguments)
				}
				$('.content a[href*=reader]').on('click', function (e) {
					const match = this.href.match(/#([0-9]+)$/)
					const hash = '#VIEW' + (match ? match[1] : '1')
					if (view.is(':hidden')) {
						location.hash = hash
					} else location.replace(hash)
					e.stopPropagation()
					e.preventDefault()
				})
				window.addEventListener('message', function (e) {
					const data = e.data
					if (data.startsWith('#')) {
						location.replace(location.pathname + data.replace('#', '#VIEW'))
					}
				}, !1)
				if (this.location_hash)location.hash = this.location_hash
			}
		},
		saveAs (blob, filename) {
			const url = URL.createObjectURL(blob)
			const save_link = document.createElement('a')
			save_link.href = url
			save_link.download = filename
			save_link.click()
			URL.revokeObjectURL(url)
		}
	}
	let window = this.window
	const URL = window.URL || window.webkitURL || window
	try {
		if (unsafeWindow) window = unsafeWindow
	} catch (er) { }

	if (app.start) app.start()

	if (app.body) {
		let is = !0
		const check_body = () => {
			(is && document.body) &&
			(is = !1, document.removeEventListener('DOMSubtreeModified', check_body, !1), app.body())
		}
		document.addEventListener('DOMSubtreeModified', check_body, !1)
	}

	if (app.end || app.idle) {
		let is = !0
		const fun2 = () => {
			if (is) {
				is = !1
				if (app.end) app.end()
				if (app.idle) setTimeout(() => app.idle(), 1)
			}
		}
		window.addEventListener('DOMContentLoaded', fun2, { capture: !1, once: !0 })
		document.readyState !== 'loading' && fun2()
	}
})()