// ==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') ? '收起<' : '展开>')
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">展开></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()
}
})()