diff --git a/BilibiliEmojisExtractor.user.js b/BilibiliEmojisExtractor.user.js new file mode 100644 index 0000000..96ff129 --- /dev/null +++ b/BilibiliEmojisExtractor.user.js @@ -0,0 +1,139 @@ +// ==UserScript== +// @name Bilibili Emojis Extractor +// @name:zh 哔哩哔哩表情提取 +// @namespace https://github.com/jc3213/userscript +// @version 0.6 +// @description Extract official Emojis in users' comments +// @description:zh 提取评论区的官方表情的图片 +// @author jc3213 +// @match https://www.bilibili.com/video/* +// @match https://www.bilibili.com/v/* +// @match https://www.bilibili.com/bangumi/* +// @grant GM_download +// ==/UserScript== + +let calls = {}; +let index = 0; + +function sendMessage(aria2c, params) { + return new Promise((resolve, reject) => { + let id = 'bili-emoji-extract-' + index++; + + let timer = setTimeout(() => { + delete calls[id]; + reject( new Error('"Download With Aria2" is either not installed, disabled, or lower than v4.17.0.3548.') ); + }, 1000); + + calls[id] = (result) => { + clearTimeout(timer); + delete calls[id]; + resolve(result); + }; + + window.postMessage({ aria2c, id, params }); + }); +} + +function aria2Status() { + return sendMessage('aria2c_status'); +} + +function aria2Download(params) { + return sendMessage('aria2c_download', params); +} + +window.addEventListener('message', (event) => { + let message = event.data; + + if (message.aria2c !== 'aria2c_response') return; + + let func = calls[message.id]; + + if (!func) return; + + func(message.result); +}); + +let keyword; + +let result; + +function extractImages(root) { + let topRoot = root.shadowRoot; + + if (!topRoot) return; + + let topTree = topRoot.querySelector('bili-rich-text'); + + if (!topTree) return; + + let subRoot = topTree.shadowRoot; + + if (!subRoot) return; + + let images = subRoot.querySelectorAll('img'); + let total = images.length; + + if (total === 0) return; + + for (let i = 0; i < total; i++) { + let image = images[i]; + let alt = image.alt.toLowerCase(); + + if (!alt.includes(keyword)) return; + + let src = image.src; + let url = src.split('@')[0]; + let out = `${alt.slice(1, -1)}.${url.split('.').pop()}`; + + result.set(url, out); + } +} + + +document.addEventListener('keydown', (event) => { + if (!(event.altKey && event.shiftKey && event.code === 'KeyE')) return; + + keyword = prompt('输入表情包关键词:').toLowerCase(); + + result = new Map(); + + let comments = document.getElementById('commentapp').children[0].shadowRoot.children[1].children[1].children; + + for (let i = 0, l = comments.length; i < l; i++) { + let components = comments[i].shadowRoot.children; + let comment = components[0]; + let replies = components[1].children[0].shadowRoot.children[0].children[0].children; + + extractImages(comment); + + for (let j = 0, m = replies.length; j < m; j++) { + extractImages(replies[j]); + } + } + + let size = result.size; + + if (size === 0) { + alert('未找到所输入关键词对应的表情!'); + return; + } else { + alert(`找到${size}个表情……`); + } + + aria2Status().then(() => { + let session = []; + + for (let entries of result) { + session.push({ url: entries[0], options: { out: entries[1] } }); + } + + aria2Download(session); + }).catch(() => { + let index = 0; + + for (let entries of result) { + setTimeout(() => GM_download(entries[0], entries[1]), index++ * 200); + } + }); +}); diff --git a/BilibiliLiveRoomFilter.user.js b/BilibiliLiveRoomFilter.user.js index 1761649..e24b638 100644 --- a/BilibiliLiveRoomFilter.user.js +++ b/BilibiliLiveRoomFilter.user.js @@ -1,236 +1,309 @@ // ==UserScript== -// @name bilibili 直播间屏蔽工具 -// @namespace https://github.com/jc3213/userscript -// @version 12.1 -// @description try to take over the world! -// @author jc3213 -// @match *://live.bilibili.com/* -// @grant GM_getValue -// @grant GM_setValue +// @name Bilibili Liveroom Filter +// @name:zh 哔哩哔哩直播间屏蔽工具 +// @namespace https://github.com/jc3213/userscript +// @version 1.11.1 +// @description Filtering Bilibili liveroom, batch management, export, import banlist... +// @description:zh 哔哩哔哩直播间屏蔽工具,支持管理列表,批量屏蔽,导出、导入列表等…… +// @author jc3213 +// @match *://live.bilibili.com/* +// @grant GM_getValue +// @grant GM_setValue // @noframes // ==/UserScript== 'use strict'; -var ban_id = GM_getValue('id', []); -var ban_liver = GM_getValue('liver', []); - -var css = document.createElement('style'); -css.innerHTML = '.fancybutton {background-color: #23ade5; color: #ffffff; padding: 5px 10px; border-radius: 3px; font-size: 14px; text-align: center; user-select: none; cursor: pointer;}\ -.fancylist {background-color: #fff; font-size: 14px; width: 270px; height: 360px; overflow-y: auto; border: 1px solid #23ade5; z-index: 999999; position: absolute;}\ -.fancylist div:nth-child(n+2) span:nth-child(2) {background-color: #ddd;}\ -.fancylist span:nth-child(1) {width: calc(50% - 50px);}\ -.fancylist span:nth-child(2) {width: calc(50% + 25px);}\ -.fancylist > .fancybutton {margin: 0px 3px 0px 2px;}\ -.fancyitem {display: inline-block; padding: 5px; text-align: center; border: 1px solid #fff;}\ -.fancytitle {background-color: #000; color: #fff;}\ -.fancybutton:hover {filter: opacity(60%);}\ -.fancybutton:active {filter: opacity(30%);}\ -div.sort-box > span:nth-child(n+2), div.room-info-down-row > span {margin-left: 5px}'; -document.head.appendChild(css); - -var player = document.querySelector('section.player-and-aside-area'); -if (player) { - var id = location.pathname.match(/\d+/)[0]; - var liver = player.querySelector('a.room-owner-username').innerHTML; - var area = player.querySelector('a.area-link').href; - var block = document.createElement('span'); - block.innerHTML = '屏蔽直播间'; - block.className = 'fancybutton'; - block.addEventListener('click', (event) => { - if (confirm('确定要永久屏蔽【 ' + liver + ' 】的直播间吗?')) { - addBanlist(id, liver); - saveBanlist(); - open(area, '_self'); +let storage = new Map(GM_getValue('storage', [])); +let caches = new Map(); +let showTable; +let showRooms = new Map(); +let firstRun = true; + +let biliitem = document.createElement('div'); +biliitem.innerHTML = '
'; + +let bilicss = document.createElement('style'); +bilicss.textContent = ` +.bililive-button { background-color: #00ADEB; border-radius: 5px; color: #ffffff; cursor: pointer; font-size: 16px; padding: 3px 10px; user-select: none; text-align: center; } +.bililive-button:hover { filter: contrast(75%); } +.bililive-button:active { filter: contrast(45%); } +`; + +let area = location.pathname.slice(1); +if (isNaN(area)) { + biliLiveSpecialArea(); +} else { + PromiseSelector('.header-info-ctnr > .rows-content').then((liver) => biliLiveShowRoom(liver, area)).catch((error) => biliLiveShowFrame(area)); +} + +async function biliLiveSpecialArea() { + let area = await PromiseSelector('#room-card-list'); + biliLiveManagerDeployed(area); + for (let node of document.querySelectorAll('.index_item_JSGkw')) { + biliLiveShowCover(node); + } + let observer = new MutationObserver((mutationsList) => { + for (let mutation of mutationsList) { + if (mutation.type !== 'childList') return; + for (let node of mutation.addedNodes) { + if (node.tagName !== 'DIV' || node.className !== 'index_item_JSGkw') return; + biliLiveShowCover(node); + } } }); - player.querySelector('a.room-owner-username').after(block); - if (ban_id.includes(id)) { + observer.observe(area, { childList: true, subtree: true }); +} + +let menuEvent = { + 'bililive-block' ({ liver, id }) { + if (!confirm('确定要永久屏蔽【 ' + liver + ' 】的直播间吗?')) return; + blockLiveRoom(id, liver); + SaveMapAsArray(); + }, + 'bililive-image' ({ title, image }) { + if (!confirm('确定要打开直播《 ' + title + ' 》的封面吗?')) return; + open(image, '_blank'); + } +}; + +async function biliLiveShowCover(node) { + if (node.cover) return; + + let pane = node.children[0]; + let room = pane.href; + let id = room.slice(room.lastIndexOf('/') + 1, room.indexOf('?')); + let [top, center] = pane.children[1].children; + let thumb = top.children[0].style['background-image']; + let image = 'https' + thumb.slice(thumb.indexOf(':'), thumb.lastIndexOf('@')); + let [name, user] = center.children[1].children; + let title = name.textContent.trim(); + let liver = user.children[0].textContent.trim(); + + let menu = document.createElement('div'); + menu.className = 'bililive-balloon bililive-hidden'; + menu.innerHTML = '
屏蔽直播间
查看封面图
{ + let menu = menuEvent[event.target.id]; + if (!menu) return; + event.preventDefault(); + menu({ liver, id, title, image }); + }); + + showRooms.set(id, node); + + center.after(menu); + node.cover = true; + storage.has(id) ? node.classList.add('bililive-hidden') : node.classList.remove('bililive-hidden'); + node.addEventListener('mouseover', () => menu.classList.remove('bililive-hidden')); + node.addEventListener('mouseout', () => menu.classList.add('bililive-hidden')); +} + +async function biliLiveShowRoom(menu, id, xid) { + let [upper, lower] = menu.children; + let left = upper.children[0]; + let liver = left.children[0].textContent.trim(); + let area = lower.children[0].children[1].children[0].href; + + if (storage.has(id)) { if (!confirm('【 ' + liver + ' 】的直播间已被屏蔽,是否继续观看?')) { open(area, '_self'); } + return; } -} -var list = document.querySelector('ul.list'); -if (!list) { - return; + let block = document.createElement('div'); + block.textContent = '屏蔽直播间'; + block.className = 'bililive-button'; + block.addEventListener('click', (event) => { + if (!confirm('确定要永久屏蔽【 ' + liver + ' 】的直播间吗?')) return; + blockLiveRoom(id, liver); + if (xid) { + blockLiveRoom(xid, liver); + } + SaveMapAsArray(); + open(area, '_self'); + }); + + bilicss.textContent += '.bililive-button {margin-left: 10px;}'; + left.append(block, bilicss); } -list.querySelectorAll('li').forEach(item => addMenuToLiveRoom(item)); -list.addEventListener('DOMNodeInserted', (event) => { - if (event.target.tagName === 'LI' && event.target.className === '') { - addMenuToLiveRoom(event.target); - } -}); -function addBanlist(id, liver) { - if (!ban_id.includes(id)) { - ban_id.push(id); - ban_liver.push(liver); - makeBanlist(id, liver); - } +async function biliLiveShowFrame(id) { + let iframe = await PromiseSelector('iframe[src*="live.bilibili.com"]'); + let menu = await PromiseSelector('.rows-ctnr.rows-content', iframe.contentDocument); + let xid = iframe.src.match(/\/(\d+)/)[1]; + biliLiveShowRoom(menu, id, xid); } -function removeBanlist(id) { - var index = ban_id.indexOf(id); - ban_id = [...ban_id.slice(0, index), ...ban_id.slice(index + 1)]; - ban_liver = [...ban_liver.slice(0, index), ...ban_liver.slice(index + 1)]; - ban_list.querySelector('#banned_' + id).remove(); +function makeFilterItem(id, liver) { + let cell = biliitem.cloneNode(true); + let [room, user] = cell.children; + user.textContent = room.className = liver; + room.textContent = room.id = id; + return cell; } -function saveBanlist() { - GM_setValue('id', ban_id); - GM_setValue('liver', ban_liver); - list.querySelectorAll('li').forEach(item => banLiveRoom(item)); +function addToFilterList(id, liver) { + let cell = caches.get(id) ?? makeFilterItem(id, liver); + showTable.appendChild(cell); } -function banLiveRoom(element) { - var id = element.querySelector('a').href.match(/\d+/)[0]; - element.style.display = ban_id.includes(id) ? 'none' : 'block'; - return id; +function blockLiveRoom(id, liver) { + if (storage.has(id)) return; + storage.set(id, liver); + let room = showRooms.get(id); + if (room) { + room.classList.add('bililive-hidden'); + } + if (!firstRun) { + addToFilterList(id, liver); + } } -function blobToFile(blob, name) { - var a = document.createElement('a'); - a.href = URL.createObjectURL(blob); - a.download = name; - a.click(); +function unblockLiveRoom(id) { + let room = showRooms.get(id); + if (!room) return; + room.classList.remove('bililive-hidden'); } -function addMenuToLiveRoom(element) { - var id = banLiveRoom(element); - var liver = element.querySelector('div.room-anchor > span').innerHTML; - var name = element.querySelector('span.room-title').innerHTML; - var preview = element.querySelector('div.cover-ctnr').style['background-image'].match(/https:\/\/[^\@]+/)[0]; +function batchBlockLiveRoom(string) { + let rules = string.match(/[^\r\n]+/g); + if (rules) { + for (let str of rules) { + let rule = str.match(/(\d+)[,:= ]+(.+)/); + if (rule?.length === 3) { + blockLiveRoom(rule[1], rule[2]); + } + } + SaveMapAsArray(); + } +} - var block = document.createElement('span'); - block.innerHTML = '屏蔽直播间'; - block.className = 'fancybutton'; - block.addEventListener('click', (event) => { - event.preventDefault(); - if (confirm('确定要永久屏蔽【 ' + liver + ' 】的直播间吗?')) { - addBanlist(id, liver); - saveBanlist(); +let manageEvent = { + 'bililive-manage' ({ popup }) { + popup.classList.toggle('bililive-hidden'); + if (!firstRun) return; + for (let [id, liver] of storage) { + addToFilterList(id, liver); } + firstRun = false; + }, + 'bililive-block' ({ entry }) { + if (!confirm('确定要屏蔽列表中的直播间吗?')) return; + batchBlockLiveRoom(entry.value); + entry.value = ''; + }, + 'bililive-import' ({ file }) { + file.click(); + }, + 'bililive-export' ({ save }) { + if (!confirm('确定要导出当前屏蔽列表吗?')) return; + let output = [...storage].join('\n'); + let blob = new Blob([output], { type: 'application/x-bililive' }); + save.href = URL.createObjectURL(blob); + save.download = '哔哩哔哩直播屏蔽列表.bililive'; + save.click(); + setTimeout(() => URL.revokeObjectURL(save.href), 1000); + }, + 'bililive-clear' ({ tbody }) { + if (!confirm('确定要清空当前屏蔽列表吗?')) return; + for (let i of storage.keys()) { + unblockLiveRoom(i); + } + storage.clear(); + SaveMapAsArray(); + tbody.innerHTML = ''; + } +}; + +function biliLiveManagerDeployed(area) { + let pane = document.createElement('div'); + pane.className = 'bililive-container'; + pane.innerHTML = ` +
管理列表
+
+
批量屏蔽
+
导入列表
+
导出列表
+
清空列表
+ +
+
直播间ID
+
主播昵称
+
+
+ + +
+`; + + let [menu, popup] = pane.children; + let [batch,,,, entry,, tbody, file, save] = popup.children; + + pane.addEventListener('click', (event) => { + let manage = manageEvent[event.target.id]; + manage?.({ popup, entry, tbody, batch, file, save }); }); - var download = document.createElement('span'); - download.innerHTML = '下载封面'; - download.className = 'fancybutton'; - download.style.cssText = 'margin-left: 5px;'; - download.addEventListener('click', (event) => { - event.preventDefault(); - if (confirm('确定要下载直播《' + name + '》的封面吗?')) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', preview, true); - xhr.responseType = 'blob'; - xhr.onload = () => blobToFile(xhr.response, id + '_' + name); - xhr.send(); - } + tbody.addEventListener('click', (event) => { + let { id, className, parentNode } = event.target; + if (!id || !confirm('确定要解除对【 ' + className + ' 】的屏蔽吗?') ) return; + parentNode.remove(); + storage.delete(id); + SaveMapAsArray(); + unblockLiveRoom(id); }); - element.addEventListener('DOMNodeInserted', (event) => { - if (event.target.tagName === 'DIV' && event.target.classList.contains('hover-panel-wrapper')) { - var container = document.createElement('span'); - container.style.cssText = 'display: block; margin-bottom: 10px;'; - event.target.prepend(container); - container.appendChild(block); - container.appendChild(download); - } + file.addEventListener('change', (event) => { + let file = event.target.files[0]; + event.target.value = ''; + if (!confirm('确定要导入所选屏蔽列表吗?')) return; + let reader = new FileReader(); + reader.onload = () => batchBlockLiveRoom(reader.result); + reader.readAsText(file); }); + + showTable = tbody; + + document.getElementsByClassName('tabs')[0].appendChild(pane); + + bilicss.textContent += `#main { min-height: 1200px; } +.bililive-button { flex: 1; } +.bililive-balloon { display: flex; gap: 5px; margin: 8px 12px 0px 6px; } +.bililive-container {position: relative;} +.bililive-hidden, .bililive-container > a { display: none !important; } +.bililive-manager { display: flex; gap: 3px; flex-wrap: wrap; background-color: #ffffff; border: 1px solid #000000; font-size: 16px; padding: 5px; margin-top: 3px; position: absolute; width: 520px; z-index: 9999; } +.bililive-manager > textarea, .bililive-manager > .bililive-table { flex-basis: 100%; } +.bililive-manager > textarea { font-size: 16px; margin: 3px 0px; padding: 5px; resize: none; } +.bililive-thead, .bililive-tbody > div { display: flex; } +.bililive-thead > *, .bililive-tbody > div > * { border: 1px solid #ffffff; flex: 1; padding: 5px; text-align: center; user-select: text !important; } +.bililive-thead > * { background-color: #000000; color: #ffffff; } +.bililive-tbody { height: 480px; overflow-y: auto; border: 1px solid #000000; } +.bililive-tbody > * > :first-child { background-color: #FF6699; color: #ffffff; cursor: pointer; } +.bililive-tbody > * > :first-child:active { contrast(45%); } +.bililive-tbody > :nth-child(2n) > :last-child { background-color: #E2E3E4; } +.bililive-tbody > :nth-child(2n + 1) > :last-child { background-color: #F1F2F3; } +`; + area.append(bilicss); } -var manager = document.createElement('span'); -manager.innerHTML = '管理屏蔽列表'; -manager.className = 'fancybutton'; -manager.addEventListener('click', (event) => { - if (ban_list.style.display === 'none') { - ban_list.style.display = 'block'; - batch_box.style.display = 'block'; - } - else { - ban_list.style.display = 'none'; - batch_box.style.display = 'none'; - } -}); -document.querySelector('div.sort-box').appendChild(manager); - -var ban_list = document.createElement('div'); -var ban_head = '
直播间主播
'; -ban_list.innerHTML = ban_head; -ban_list.className = 'fancylist'; -ban_list.style.cssText = 'display: none; left: 242px;' -manager.after(ban_list); - -ban_id.forEach((item, index) => makeBanlist(item, ban_liver[index])); - -function makeBanlist(id, liver) { - var box = document.createElement('div'); - box.id = 'banned_' + id; - var ban_id = document.createElement('span'); - ban_id.innerHTML = id; - ban_id.className = 'fancyitem fancybutton'; - ban_id.addEventListener('click', (event) => { - if (confirm('确定要解除对【 ' + liver + ' 】的屏蔽吗?')) { - removeBanlist(id); - saveBanlist(); - } - }); - var ban_liver = document.createElement('span'); - ban_liver.innerHTML = liver; - ban_liver.className = 'fancyitem'; - ban_list.appendChild(box); - box.appendChild(ban_id); - box.appendChild(ban_liver); +function SaveMapAsArray() { + GM_setValue('storage', [...storage]); } -var batch_box = document.createElement('div'); -batch_box.className = 'fancylist'; -batch_box.style.cssText = 'display: none;'; -manager.after(batch_box); - -var ban_box = document.createElement('textarea'); -ban_box.style.cssText = 'resize: none; height: calc(100% - 40px); width: calc(100% - 8px); font-size: 14px; padding: 3px;'; -batch_box.appendChild(ban_box); - -var submit = document.createElement('span'); -submit.innerHTML = '批量屏蔽'; -submit.className = 'fancybutton'; - -submit.addEventListener('click', (event) => { - if (confirm('确定要屏蔽列表中的直播间吗?')) { - var pattern = ban_box.value.split('\n'); - pattern.forEach(item => { - var rule = item.match(/^(\d+)[\s\/\.\@\#\$\,\/\\]+([^\s\/\.\@\#\$\,\/\\]+)/); - if (rule) { - addBanlist(rule[1], rule[2]); +function PromiseSelector(selector, anchor = document) { + return new Promise((resolve, reject) => { + let quota = 15; + let timer = setInterval(() => { + let node = anchor.querySelector(selector); + if (node) { + clearInterval(timer); + resolve(node); } - }); - saveBanlist(); - ban_box.value = ''; - } -}); -batch_box.appendChild(submit); - -var save = document.createElement('span'); -save.innerHTML = '导出列表'; -save.className = 'fancybutton'; -save.addEventListener('click', (event) => { - if (confirm('确定要导出当前屏蔽列表吗?')) { - var list = ban_id.map((item, index) => item + ', ' + ban_liver[index]).join('\n'); - blobToFile(new Blob([list], {type: 'text/plain'}), 'bilibili直播间屏蔽列表'); - } -}); -batch_box.appendChild(save); - -var clear = document.createElement('span'); -clear.innerHTML = '清空列表'; -clear.className = 'fancybutton'; -clear.addEventListener('click', (event) => { - if (confirm('确定要清空当前屏蔽列表吗?')) { - ban_list.innerHTML = ban_head; - ban_id = []; - ban_liver = []; - saveBanlist(); - } -}); -batch_box.appendChild(clear); + if (--quota === 0) { + clearInterval(timer); + reject(new Error(`Timeout: Unable to locate element "${selector}" within 3 seconds.`)); + } + }, 200); + }); +} diff --git a/BilibiliVideoDownloader.user.js b/BilibiliVideoDownloader.user.js new file mode 100644 index 0000000..e5bd5d0 --- /dev/null +++ b/BilibiliVideoDownloader.user.js @@ -0,0 +1,267 @@ +// ==UserScript== +// @name Bilibili Video Downloader +// @name:zh 哔哩哔哩视频下载器 +// @namespace https://github.com/jc3213/userscript +// @version 2.0.0 +// @description Download videos from Bilibili (No Bangumi) +// @description:zh 下载哔哩哔哩视频(不支持番剧) +// @author jc3213 +// @match https://www.bilibili.com/video/* +// @match https://www.bilibili.com/v/* +// @match https://www.bilibili.com/festival/* +// @grant GM_download +// ==/UserScript== + +let { autowide = '1', videocodec = 'bilivideo_vc265' } = localStorage; +let { pathname } = location; +let bvTitle; +let bvOpen = true; +let history = {}; +let archive; +let format = { + '30280': { text: '音频 高码率', ext: '.192k.m4a' }, + '30232': { text: '音频 中码率', ext: '.128k.m4a' }, + '30216': { text: '音频 低码率', ext: '.64k.m4a' }, + '127': { text: '8K 超高清', ext: '.8k.mp4' }, + '125': { text: '4K 超清+', ext: '.4k+.mp4' }, + '120': { text: '4K 超清', ext: '.4k.mp4' }, + '116': { text: '1080P 60帧', ext: '.1080f60.mp4' }, + '112': { text: '1080P 高码率', ext: '.1080+.mp4' }, + '80': { text: '1080P 高清', ext: '.1080.mp4' }, + '74': { text: '720P 60帧', ext: '.720f60.mp4' }, + '64': { text: '720P 高清', ext: '.720.mp4' }, + '32': { text: '480P 清晰', ext: '.480.mp4' }, + '16': { text: '360P 流畅', ext: '.360.mp4' }, + '15': { text: '360P 流畅', ext: '.360-.mp4' }, + 'avc1': { title: '视频编码: H.264', alt: 'h264', type: 'video' }, + 'hvc1': { title: '视频编码: HEVC 增强', alt: 'h265', type: 'video' }, + 'hev1': { title: '视频编码: HEVC', alt: 'h265', type: 'video' }, + 'av01': { title: '视频编码:AV1', alt: 'av1', type: 'video' }, + 'mp4a': { title: '音频编码: AAC', alt: 'aac', type: 'audio' } +}; + +const bvCode = pathname.substring(1, pathname.indexOf('/', 2)); +const bvMenu = { + dash: 'result', + offset: 'display: inline-block; top: -12px;', + menu: 'div.toolbar > div.toolbar-left', + widebtn: 'div.bpx-player-ctrl-wide', + widestat: 'bpx-state-entered', + fetch: () => { + let active = document.querySelector('li.bpx-player-ctrl-eplist-menu-item.bpx-state-active'); + let id = active.getAttribute('data-episodeid'); + let name = active.textContent; + let thumb = document.querySelector('img.image_ogv_weslie_common_image__Rg7Xm').src; + bVideoTitle(name); + bVideoThumb(thumb); + bVideoGetter(id, `pgc/player/web/playurl?ep_id=${id}`); + } +}; + +if (bvCode === 'video') { + bvMenu.menu = 'div.video-toolbar-left'; + bvMenu.offset = '10px'; + bvMenu.dash = 'data'; + bvMenu.fetch = () => { + let { videoData } = document.defaultView.__INITIAL_STATE__; + let { title, aid, cid, pic } = videoData; + cid ??= document.querySelector('li.bpx-state-multi-active-item')?.getAttribute('data-cid'); + bVideoTitle(title); + bVideoThumb(pic); + bVideoGetter(cid, 'x/player/playurl?avid=' + aid + '&cid=' + cid); + }; +} else if (bvCode === 'v') { + bvMenu.offset = 'left: -300px;'; + bvMenu.menu = 'div.select-type > ul.type'; + bvMenu.widebtn = 'div.bilibili-player-video-btn-widescreen'; + bvMenu.widestat = 'closed'; + bvMenu.dash = 'data'; + bvMenu.fetch = () => { + let { aid, cid } = document.defaultView; + bVideoTitle(document.querySelector('div.match-info-title').textContent); + bVideoGetter(cid, 'x/player/playurl?avid=' + aid + '&cid=' + cid); + }; +} else if (bvCode === 'festival') { + bvMenu.offset = 'top: 86%; right: 0%;'; + bvMenu.menu = 'div.page-bg-logo'; + bvMenu.dash = 'data'; + bvMenu.fetch = () => { + let { videoInfo } = document.defaultView.__INITIAL_STATE__; + let { title, aid, cid} = videoInfo; + let active = document.querySelector('li.bpx-player-ctrl-eplist-menu-item.bpx-state-active'); + let name = `${title} - ${active.textContent}`; + cid ??= active.getAttribute('data-cid'); + bVideoTitle(title); + //bVideoThumb(pic); + bVideoGetter(cid, 'x/player/playurl?avid=' + aid + '&cid=' + cid); + } +} + +let menuItem = document.createElement('div'); +menuItem.className = 'bilivideo_button'; + +let mainPane = document.createElement('div'); +mainPane.id = 'bilivideo_main'; +mainPane.innerHTML = ` +
+
设置
+
解析
+
+
+

自动宽屏

+ +

编码格式

+ +
+
+`; + +let [menuPane, optionsPane, analysePane] = mainPane.children; + +function bVideoTitle(name) { + bvTitle = name.trim().replace(/[\/\\:*?"<>|\s\r\n]/g, '_'); +} + +function bVideoThumb(url) { + let thumb = menuItem.cloneNode(true); + thumb.classList.add('bilivideo_thumb'); + thumb.textContent = '视频封面'; + thumb.url = url.replace(/^(https?:)?\/\//, 'https://').replace(/@.+/, ''); + thumb.file = bvTitle + thumb.url.slice(url.lastIndexOf('.')); + analysePane.appendChild(thumb); +} + +function bVideoItems(json) { + let { id, codecs, baseUrl } = json; + let codec = codecs.slice(0, codecs.indexOf('.')); + let { text, ext } = format[id]; + let { title, alt, type } = format[codec]; + let menu = menuItem.cloneNode(true); + menu.classList.add('bilivideo_' + type, 'bilivideo_' + alt); + menu.textContent = text; + menu.title = title; + menu.url = baseUrl; + menu.file = bvTitle + ext; + analysePane.appendChild(menu); +} + +function bVideoExtractor(json) { + if (json?.video) { + for (let i of json.video) { + bVideoItems(i); + } + } + if (json?.audio) { + for (let i of json.audio) { + bVideoItems(i); + } + } +} + +async function bVideoGetter(vid, playurl) { + if (bvOpen && history[vid]) { + analysePane.innerHTML = ''; + analysePane.append(...history[vid]); + } else { + let response = await fetch('https://api.bilibili.com/' + playurl + '&fnval=4050', {credentials: 'include'}); + let json = await response.json(); + let videos = json[bvMenu.dash].dash; + bVideoExtractor(videos); + history[vid] = [...analysePane.children]; + } +} + +menuPane.addEventListener('click', (event) => { + let { id } = event.target; + if (id === 'bilivideo_optbtn') { + optionsPane.classList.toggle('bilivideo_hidden'); + analysePane.classList.add('bilivideo_hidden'); + } else if (id === 'bilivideo_anabtn') { + optionsPane.classList.add('bilivideo_hidden'); + analysePane.classList.toggle('bilivideo_hidden'); + if (!bvOpen) return; + analysePane.classList.add(videocodec); + bvOpen = false; + analysePane.innerHTML = ''; + bvMenu.fetch(); + } +}); + +optionsPane.addEventListener('change', (event) => { + let { name, value } = event.target; + self[name] = localStorage[name] = value; + if (name === 'autowide') { + document.querySelector(bvMenu.widebtn).click(); + } else if (name === 'videocodec') { + analysePane.classList.replace(videocodec, value); + } +}); + +analysePane.addEventListener('click', (event) => { + let { altKey, target: { url, file } } = event; + if (!url || !file) return; + altKey + ? window.postMessage({ aria2c: 'aria2c_download', params: [{ url, options: { out: file, referer: location.href } }] }) + : GM_download({ url, responseType: 'blob', headers: { referer: location.href }, name: file }); +}); + +let [, optionWide,, optionCodec] = optionsPane.children; +optionWide.value = autowide; +optionCodec.value = videocodec; + +let cssPane = document.createElement('style'); +cssPane.textContent = ` +#bilivideo_main { font-size: 16px; position: relative; text-align: center; padding-right: 5px; line-height: 28px; z-index: 9999999; width: fit-content; ${bvMenu.offset ?? ''} } +#bilivideo_menu { display: flex; gap: 5px;} +.bilivideo_button { border: outset 1px #000; padding: 3px; background-color: #c26; color: #fff; cursor: pointer; width: 100px; } +.bilivideo_button:hover { filter: contrast(80%); } +.bilivideo_button:active { filter: contrast(60%); border-style: inset; } +.bilivideo_pane { position: absolute; top: 0px; left: 100%; background-color: #fff; border: solid 1px #000; padding: 5px; } +.bilivideo_pane > h4, .bilivideo_pane > select { width: 110px !important; padding: 5px; text-align: center; } +.bilivideo_pane > h4 { color: #c26; font-weight: bold; margin: auto; } +.bilivideo_pane > select { cursor: pointer; border: 1px solid #000; } +.bilivideo_result { display: grid; grid-template-columns: 1fr 1fr 1fr; grid-auto-flow: dense; gap: 5px; } +.bilivideo_thumb { grid-column: 1; } +.bilivideo_video { grid-column: 2; } +.bilivideo_audio { grid-column: 3; } +.bilivideo_hidden { display: none; } +.bilivideo_vc264 > .bilivideo_video:not(.bilivideo_h264), .bilivideo_vc265 > .bilivideo_video:not(.bilivideo_h265), .bilivideo_vcav1 > .bilivideo_video:not(.bilivideo_av1) { display: none ;} +`; + +const proto = HTMLMediaElement.prototype; +const descriptor = Object.getOwnPropertyDescriptor(proto, 'src'); +const bvplay = proto.play; + +Object.defineProperty(proto, 'src', { + configurable: true, + enumerable: true, + get() { + return descriptor.get.call(this); + }, + set(value) { + descriptor.set.call(this, value); + bvOpen = true; + optionsPane.classList.add('bilivideo_hidden'); + analysePane.classList.add('bilivideo_hidden'); + } +}); + +proto.play = async function (...args) { + proto.play = bvplay; + setTimeout(() => { + let wide = document.querySelector(bvMenu.widebtn); + let menu = document.querySelector(bvMenu.menu); + if (!wide.classList.contains(bvMenu.widestat) && autowide === '1' ) { + wide.click(); + } + menu.append(mainPane, cssPane); + }, 1000); + return bvplay.apply(this, args); +}; diff --git a/HupuBetterHotPost.user.js b/HupuBetterHotPost.user.js deleted file mode 100644 index b9e594f..0000000 --- a/HupuBetterHotPost.user.js +++ /dev/null @@ -1,10 +0,0 @@ -// ==UserScript== -// @name 优化虎扑热评显示 -// @namespace https://github.com/jc3213/userscript -// @version 1 -// @description 限制虎扑热评高度,减少页面占用以优化阅读体验 -// @author jc3213 -// @match *://bbs.hupu.com/* -// ==/UserScript== - -$('div[class="w_reply clearfix"]').css({'height': '400px', 'overflow-y': 'auto'}); diff --git a/KakuyomuAssistant.user.js b/KakuyomuAssistant.user.js new file mode 100644 index 0000000..70ad015 --- /dev/null +++ b/KakuyomuAssistant.user.js @@ -0,0 +1,29 @@ +// ==UserScript== +// @name 「」カクヨム助手 +// @namespace https://github.com/jc3213/userscript +// @version 0.2 +// @description 「」カクヨムのリーディング体験をより良くするためのツールです +// @author jc3213 +// @match https://kakuyomu.jp/works/*/episodes/* +// ==/UserScript== + +// Remove side navigator +document.querySelector('#content').classList.remove('contentAside-isShown'); + +// Wide content field +document.querySelector('div.widget-episode-inner').style['max-width'] = '50%'; + +// Content font to Meiryo +document.querySelector('div.widget-episodeBody').style['font-family'] = '"Segoe UI",Verdana,"メイリオ",Meiryo,sans-serif'; + +// Shortcut hotkey +document.addEventListener('keydown', event => { + switch (event.key) { + case 'ArrowLeft': + open(document.querySelector('#contentMain-previousEpisode > a').href.slice(0, -4), '_self'); + break; + case 'ArrowRight': + open(document.querySelector('#contentMain-nextEpisode > a').href, '_self'); + break; + } +}); diff --git a/MicroMagazinSurveyNovel.user.js b/MicroMagazinSurveyNovel.user.js index e2ef3e1..c18fecb 100644 --- a/MicroMagazinSurveyNovel.user.js +++ b/MicroMagazinSurveyNovel.user.js @@ -1,32 +1,48 @@ // ==UserScript== // @name アンケートノベルプリント // @namespace https://github.com/jc3213/userscript -// @version 3 +// @version 1.4 // @description マイクロマガジン社のアンケート協力者向けの書き下ろしノベルプリントヘルパーです // @author jc3213 -// @match *://micromagazine.net/me/qu/*_thanks.html +// @match *://micromagazine.co.jp/me/qu/*_thanks*.html // ==/UserScript== +var book = document.getElementById('books') ?? document.getElementById('contents_c'); +book.id = 'books'; + var css = document.createElement('link'); css.rel = 'stylesheet'; css.type = 'text/css'; css.href = 'style_a.css'; -document.head.appendChild(css); +book.appendChild(css); + +var footer; +var content = book.querySelectorAll('p'); +content.forEach((item, index) => { + if (!item.children[0]) { + return; + } + if (index > footer) { + item.remove(); + } + else if (item.children[0].tagName === 'BR') { + if (index === 0) { + item.remove(); + } + item.replaceWith(item.children[0]); + } + else if (item.children[0].tagName === 'A') { + item.remove(); + footer = index; + } +}); var button = document.createElement('div'); button.id = 'print_helper'; button.style.cssText = 'position: absolute; left: 25%; top: 5%; z-lindex: 3213; background-color: white; color: black; padding: 5px; border: 1px solid #e9e9e9; cursor: pointer;'; button.innerHTML = 'プリント仕様'; -button.addEventListener('click', (event) => { - var content = document.getElementById('books') || document.getElementById('contents_c'); - content.style.border = 'none'; - content.style.margin = '0px'; - content.style.margin = '0px'; - document.body.replaceWith(content); - - var header = content.children[0]; - if (['p', 'br'].includes(header.tagName.toLowerCase())) { - header.remove(); - } +button.addEventListener('click', event => { + book.style.cssText = 'border: none; margin: 0px; padding: 0px'; + document.body.replaceWith(book); }); document.body.appendChild(button); diff --git a/NGAEmojiManager.user.js b/NGAEmojiManager.user.js new file mode 100644 index 0000000..9cf440a --- /dev/null +++ b/NGAEmojiManager.user.js @@ -0,0 +1,120 @@ +// ==UserScript== +// @name NGA表情管理器 +// @namespace https://github.com/jc3213 +// @version 2.0.3 +// @description 为NGA论坛添加/删除新表情 +// @author jc3213 +// @match *://bbs.nga.cn/thread.php?* +// @match *://bbs.nga.cn/read.php?* +// @match  *://bbs.nga.cn/post.php?* +// @match *://ngabbs.com/thread.php?* +// @match *://ngabbs.com/read.php?* +// @match *://ngabbs.com/post.php?* +// @match *://nga.178.com/thread.php?* +// @match *://nga.178.com/read.php?* +// @match *://nga.178.com/post.php?* +// @grant GM_getValue +// @grant GM_setValue +// @grant GM_deleteValue +// @grant GM_listValues +// ==/UserScript== + +let emojis = new Map(GM_getValue('emoji', [])); +let emojiUI = {}; +let emojiTab; +let emojiPanel; + +let css = document.createElement('style'); +css.innerText = `.single_ttip2 img { max-height: 64px; }` +document.head.append(css); + +let manager = document.createElement('button'); +manager.textContent = '添加新表情'; +manager.className = 'block_txt_big'; +manager.addEventListener('click', (event) => loader.click()); + +let loader = document.createElement('input'); +loader.type = 'file'; +loader.accept = '.json'; +loader.multiple = true; +loader.addEventListener('change', async (event) => { + for (let file of event.target.files) { + let text = await file.text(); + let { name, emoji, author } = JSON.parse(text); + if (!name || !emoji) return; + emojis.set(name, { author, emoji }); + GM_setValue('emoji', [...emojis]); + printEmojiUI(name, author, emoji); + } + event.target.value = ''; +}); + +function printEmojiUI(name, author, emoji) { + let panel = emojiUI[name] ??= createEmojiUI(name, author, emoji); + panel.innerHTML = ''; + panel.style.display = 'none'; +} + +function createEmojiUI(name, author, emoji) { + let tab = document.createElement('button'); + tab.className = 'block_txt_big'; + tab.textContent = name; + tab.title = author ? '制作者: ' + author : ''; + manager.before(tab); + let panel = document.createElement('div'); + emojiPanel.append(panel); + tab.addEventListener('click', (event) => { + if (event.ctrlKey && confirm('确定要删除表情包【'+ name + '】吗?')) { + emojis.delete(name); + GM_setValue('emoji', [...emojis]); + tab.remove(); + panel.remove(); + delete emojiUI[name]; + } + else { + for (let pane of emojiPanel.childNodes) { + pane.style.display = 'none'; + } + panel.style.display = ''; + appendEmojiToUI(name, emoji, panel); + } + }); + panel.addEventListener('click', (event) => { + let { alt } = event.target; + if (!alt) return; + postfunc.addText(`[img]${alt}[/img]`); + postfunc.selectSmilesw._.hide(); + }); + return panel; +} + +function appendEmojiToUI(name, emoji, panel) { + if (panel.runOnce) return; + panel.runOnce = true; + for (let em of emoji) { + let img = document.createElement('img'); + img.src = 'https://img.nga.178.com/attachments/' + em; + img.alt = em; + panel.appendChild(img); + } +} + +let observer = new MutationObserver((mutationList) => { + for (let mutation of mutationList) { + for (let node of mutation.addedNodes) { + if (node.id.startsWith('commonwindow')) { + let [menu, body] = node.children[0].children; + if (menu.textContent !== '​插入表情') continue; + let pane = body.children[0].children; + emojiTab = pane[0]; + emojiPanel = pane[1]; + emojiTab.appendChild(manager); + for (let [name, { author, emoji }] of emojis) { + printEmojiUI(name, author, emoji); + } + return observer.disconnect(); + } + } + } +}); +observer.observe(document.body, { childList: true }); diff --git a/NGAUserBlocker.user.js b/NGAUserBlocker.user.js new file mode 100644 index 0000000..d48c6f5 --- /dev/null +++ b/NGAUserBlocker.user.js @@ -0,0 +1,75 @@ +// ==UserScript== +// @name NGA User Blocker +// @name:zh NGA表情管理器 +// @namespace https://github.com/jc3213/userscript +// @version 0.3 +// @description Block user posts and quotes on NGA +// @description:zh 屏蔽用户的发言跟与之相关的引用 +// @author jc3213 +// @match *://bbs.nga.cn/* +// @match *://ngabbs.com/* +// @match *://nga.178.com/* +// @grant GM_setValue +// @grant GM_getValue +// ==/UserScript== + +let storage = GM_getValue('block', []); +let blocker = new Map(storage); + +function blockPosts(posts) { + [...posts].forEach((post) => { + if (post.localName !== 'table') { + return; + } + + let title = post.children[0].children[0].children[0].children[0].children[0]; + let body = title.children; + let user = body[1].textContent; + let id = body[body.length - 1].textContent; + + if (blocker.has(id)) { + post.style.display = 'none'; + } + + let quote = post.children[0].children[0].children[1].children[4].children[2].children[0]; + if (quote.localName === 'div' && quote.className === 'quote') { + let quser = quote.children[2]; + let qname = quser.textContent; + let qid = quser.href.slice(quser.href.lastIndexOf('=') + 1); + if (blocker.has(qid)) { + quote.textContent = `${qname}已被屏蔽……`; + } + } + + let button = document.createElement('button'); + button.id = id; + button.user = user; + button.title = `屏蔽用户[${user}]`; + button.textContent = '屏蔽此人'; + button.style.cssText = 'margin-left: 5px;'; + button.addEventListener('click', blockThisUser); + button.post = post; + title.appendChild(button); + }); +} + +function blockThisUser(event) { + let {id, title, user, post} = event.target; + if (confirm(`确定要${title}吗?`)) { + blocker.set(id, user); + post.style.display = 'none'; + GM_setValue('block', [...blocker]); + } +} + +new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + let post = mutation.addedNodes[0]; + if (post && post.id === 'm_posts') { + setTimeout(() => blockPosts(post.children[0].children), 100); + } + }); + +}).observe(document.getElementById('mc'), { childList: true, subtree: true }); + +blockPosts(document.getElementById('m_posts_c').children); diff --git a/NarouSyosetuManager.user.js b/NarouSyosetuManager.user.js new file mode 100644 index 0000000..3352e95 --- /dev/null +++ b/NarouSyosetuManager.user.js @@ -0,0 +1,277 @@ +// ==UserScript== +// @name 小説家になろう書庫管理 +// @namespace https://github.com/jc3213/userscript +// @version 3.0 +// @description ログインを必要としない小説情報を管理するツールです +// @author jc3213 +// @match https://ncode.syosetu.com/* +// @exclude https://ncode.syosetu.com/impression/* +// @exclude https://ncode.syosetu.com/novelpdf/* +// @exclude https://ncode.syosetu.com/novelview/* +// @exclude https://ncode.syosetu.com/novelreview/* +// @grant GM_getValue +// @grant GM_setValue +// @grant GM_deleteValue +// ==/UserScript== + +'use strict'; +// 書庫関連 +let [, novelcode, novelread] = location.pathname.match(/^\/(?:.*\/ncode\/)?(n\w+)\/(?:(\d+))?\/?$/); +if (!novelcode) return; + +let novels = new Map(GM_getValue('bookmark', [])); +let validate = {}; +let novelname = document.querySelector('.c-announce-box > :last-child > :first-child, h1.novel_title').textContent; +let shelf = false; + +// ホットキー関連 +let shortcut = ''; +let shorttime; + +document.addEventListener('keydown', (event) => { + let { key } = event; + let hot = hotkeyMap[key]; + if (!hot || event.target.localName === 'input') return; + event.preventDefault(); + hot(key); +}); + +const hotkeyMap = { + 'Escape': () => clearTimeout(shorttime), + 'ArrowLeft': () => document.querySelector('a.c-pager__item--before')?.click(), + 'ArrowRight': () => document.querySelector('a.c-pager__item--next')?.click() +}; +for (let i = 0; i < 10; i++) { + hotkeyMap[i] = hotkeyChapter; +} +function hotkeyChapter(key) { + clearTimeout(shorttime); + shortcut += key; + shorttime = setTimeout(() => { + open(`https://ncode.syosetu.com/${novelcode}/${Number(shortcut)}`, '_blank'); + shortcut = ''; + }, 1000); +} + +// 通知UI +let overlay = document.createElement('div'); +overlay.className = 'narou-overlay'; +overlay.addEventListener('click', (event) => { + let notify = event.target; + if (notify.className === 'narou-notify') { + notify.remove(); + } +}); +document.body.append(overlay); + +function notification(ncode, message) { + let notify = document.createElement('div'); + notify.className = 'narou-notify'; + notify.textcontent = `Nコード[ ${ncode} ]${message}`; + setTimeout(() => notify.remove(), 5000); + overlay.appendChild(notify); +} + +// 書庫UI +let shelf_head = `
NCODE
小説タイトル
`; +let mainmenu = document.createElement('div'); +mainmenu.className = 'narou-assist'; +mainmenu.innerHTML = ` +
書庫管理
+
本文のみ
+
+ +
NCODE登録
+
書庫エクスポート
+
書庫インスポート
+ + +
+ ${shelf_head} +
+
+ +`; +mainmenu.addEventListener('click', (event) => { + let { id } = event.target; + menuDispatch[id]?.(); +}); +document.querySelector('nav').appendChild(mainmenu); + +function bookInShelf(ncode, title) { + let novel = document.createElement('div'); + novel.id = ncode; + novel.className = 'narou-book'; + novel.innerHTML = `
${ncode}
${title}
`; + shelf_table.appendChild(novel); +} + +// 前書きと後書き +let epilogue = document.createElement('style'); +epilogue.textContent = '.p-novel__text--preface, .p-novel__text--afterword { display: none; }'; + +// 詳細ノードを取得 +let [manager, clearfix, bookshelf] = mainmenu.children; +let [input_ncode, input_submit,,, input_save, input_file, shelf_table] = bookshelf.children; + +if (novelread) { + clearfixHandler(); +} else { + clearfix.remove(); +} + +input_ncode.addEventListener('keydown', (event) => { + if (event.key === 'Enter') { + input_submit.click(); + } +}); + +input_file.addEventListener('change', (event) => { + let [file] = input_file.files; + if (!confirm(`書庫「${file.name}」をインポートしますか?`)) return; + let reader = new FileReader(); + reader.onload = () => { + let json = JSON.parse(reader.result); + shelf_table.innerHTML = shelf_head; + novels = new Map(json); + managerHandler(); + GM_setValue('bookmark', [...novels]); + }; + reader.readAsText(file); + input_file.value = ''; +}); + +shelf_table.addEventListener('click', (event) => { + let action = event.target.getAttribute('action'); + if (!action) return; + let novel = event.target.parentNode; + let ncode = novel.id; + let title = novels.get(ncode); + bookManage[action](ncode, title, novel); +}); + +// 事件配分 +const menuDispatch = { + 'narou-manager': menuManager, + 'narou-clearfix': menuClearfix, + 'narou-submit': menuSubmit, + 'narou-export': menuExport, + 'narou-import': () => input_file.click() +}; + +const bookManage = { + 'remove': bookRemove, + 'visit': bookVisit +}; + +function menuManager() { + if (shelf === false) { + shelf = true; + managerHandler(); + } + bookshelf.style.display = !bookshelf.style.display ? 'flex' : ''; + manager.classList.toggle('narou-checked'); +} +function managerHandler() { + for (let [ncode, title] of novels) { + bookInShelf(ncode, title); + } +} + +function menuClearfix() { + if (localStorage.clearfix === '1') { + localStorage.clearfix = '0'; + } else { + localStorage.clearfix = '1'; + } + clearfixHandler(); +} + +function clearfixHandler() { + if (localStorage.clearfix === '1') { + clearfix.classList.add('narou-checked'); + document.body.append(epilogue); + } else { + clearfix.classList.remove('narou-checked'); + epilogue.remove(); + } +} + +async function menuSubmit() { + let { value } = input_ncode; + let ncode = value.match(/^(?:https:\/\/.+\/)?(n\w+)\/?(?:\d+\/)?$/)?.[1]; + if (!ncode) { + notification(value, 'は違法な入力です!'); + return; + } + let title = novels.get(ncode); + if (title) { + notification(ncode, `小説名「 ${title} 」はは既に書庫に登録しています!`); + return; + } + if (ncode === novelcode) { + submitHandler(ncode, novelname); + return; + } + if (validate[ncode]) { + notification(ncode, 'を検証しています、しばらくお待ちください!'); + return; + } + validate[ncode] = true; + let res = await fetch('/' + ncode); + let text = await res.text(); + title = text.match(/]*>(.*)<\/title>/)[1]; + title === 'エラー' + ? notification(ncode, 'は「小説家になろう」に存在しません!') + : submitHandler(ncode, title) + delete validate[ncode]; +} +function submitHandler(ncode, title) { + bookInShelf(ncode, title); + novels.set(ncode, title); + GM_setValue('bookmark', [...novels]); + notification(ncode, `小説名「 ${title} 」は書庫に登録しました!`); +} + +function menuExport() { + if (!confirm(`現在の書庫をエクスポートしますか?`)) return; + let text = JSON.stringify([...novels], null, 4); + let blob = new Blob([text]); + let url = URL.createObjectURL(blob); + input_save.href = url; + input_save.click(); + URL.revokeObjectURL(url); +} + +function bookRemove(ncode, title, novel) { + if (!confirm(`小説「 ${title} 」を書庫から削除しますか?`)) return; + novels.delete(ncode); + novel.remove(); + GM_setValue('bookmark', [...novels]); + notification(ncode, `小説名「 ${title} 」は書庫から削除しました!`); +} + +function bookVisit(ncode, title) { + if (!confirm(`小説「 ${title} 」を開きますか?`)) return; + open(`https://ncode.syosetu.com/${ncode}/`, '_blank'); +} diff --git a/NyaaTorrentHelper.user.js b/NyaaTorrentHelper.user.js index 38ef649..6d6245f 100644 --- a/NyaaTorrentHelper.user.js +++ b/NyaaTorrentHelper.user.js @@ -1,217 +1,336 @@ // ==UserScript== -// @name Nyaa Torrent Helper -// @namespace https://github.com/jc3213/userscript -// @version 24 -// @description Nyaa Torrent right click to open available open preview in new tab -// @author jc3213 -// @match *://*.nyaa.si/* -// @exclude *://*.nyaa.si/view/* -// @grant GM_xmlhttpRequest -// @connect * +// @name Nyaa Torrent Helper +// @name:zh Nyaa 助手 +// @namespace https://github.com/jc3213/userscript +// @version 2.1 +// @description Nyaa Torrent ease to access torrent info and preview, filter search result, and aria2c intergration +// @description:zh 能便捷操作 Nyaa 的种子信息,预览缩微图,过滤搜索结果,联动aria2c +// @author jc3213 +// @match *://*.nyaa.si/* +// @exclude *://*.nyaa.si/view/* +// @grant GM_setValue +// @grant GM_getValue +// @grant GM_deleteValue +// @grant GM_deleteValues +// @grant GM_listValues +// @grant GM_openInTab // ==/UserScript== -'use strict'; -// Variables -var filter = false; -var keyword = []; -var queue = []; -var action = {}; -var hosts = { - 'hentai-covers.site': '#image-viewer-container > img', - 'e-hentai.org': '#gd1 > div', - 'www.dlsite.com': 'div.slider_body' +// variables +let torrents = []; +let preview = {}; +let working = {}; +let selected = new Set(); +let keyword; +let filterResult = {}; +let active = document.querySelector('.pagination > .active'); + +// i18n +let messages = { + 'en-US': { + name: 'Name:', + preview: 'Preview:', + torrent: 'Torrent:', + magnet: 'Magnet:', + prompt: 'Enter a filter keyword:', + oncopy: 'Confirm copying selected torrent data to clipboard?', + aria2c: 'Confirm sending JSON-RPC request to aria2c?', + onsend: 'JSON-RPC request has been sent.', + clear: 'Confirm clearing all cached torrent data?', + onclear: 'All cached data has been cleared.' + }, + 'zh-CN': { + name: '名字:', + preview: '预览:', + torrent: '种子:', + magnet: '磁链:', + prompt: '输入过滤关键词:', + oncopy: '确认复制所选种子数据到剪贴板?', + aria2c: '确认向 aria2c 发送 JSON-RPC 请求?', + onsend: 'JSON-RPC 请求已发送。', + clear: '确认清除所有已缓存的种子数据?', + onclear: '所有缓存数据已清除。' + } +}; +let i18n = messages[navigator.language] ?? messages['en-US']; + +// css +let css = document.createElement('style'); +css.textContent = ` +.nyaa-hidden { display: none; } +.nyaa-cached > td { background-color: #cde7f0 !important; } +.nyaa-checked > td { background-color: #f0d8d8 !important; } +.nyaa-preview { position: absolute; } +.nyaa-select { position: absolute; border: 1px dashed #007bff; background: rgba(0, 123, 255, 0.2); pointer-events: none; display: none; } +.nyaa-noselect { user-select: none; } +`; +document.body.appendChild(css); + +// shortcut +function filterTorrents() { + let result = prompt(i18n.prompt, keyword); + if (result === null) { + return; + } + if (result === '' || result === keyword) { + for (let tr of torrents) { + tr.classList.remove('nyaa-hidden'); + } + keyword = ''; + delete filterResult[result]; + } else { + let match = result.toLowerCase(); + for (let tr of torrents) { + tr.info.name.toLowerCase().includes(match) + ? tr.classList.remove('nyaa-hidden') + : tr.classList.add('nyaa-hidden'); + } + keyword = result; + } } -// Create UI -var css= document.createElement('style'); -css.innerHTML = '.filterButton {background-color: #056b00; border: 1px solid black; color: #FFF; padding: 10px; cursor: pointer; user-select: none;}\ -.filterButton:active {filter: opacity(30%);}\ -.filterItem {position: relative; padding: 3px;}\ -.filterItem span {color: #FFF; border: 1px transparent solid; padding: 10px 5px; width: fit-content; border-radius: 3px; position: absolute;}\ -.filterItem a {color: #FFF; text-decoration: none;}\ -.filterItem span:nth-child(n+2) {margin-left: 5px; user-select: none; cursor: pointer; width: 60px; text-align: center;}\ -.filterItem span:nth-child(n+2):hover, filterButton:hover {filter: opacity(60%);}\ -.filterItem span:nth-child(1) {background-color: #2bceec; overflow: hidden; z-index: 3213; width: 910px;}\ -.filterItem span:nth-child(1):hover {min-width: fit-content;}\ -.filterItem span:nth-child(2) {background-color: #ee1c1c; left: 915px;}\ -.filterItem span:nth-child(3) {background-color: #0056b0; left: 980px;}\ -.filterItem span:nth-child(4) {background-color: #056b00; left: 1045px;}\ -.filterItem span:nth-child(5) {background-color: #0005CC; left: 1110px;}\ -.previewItem {position: fixed; z-index: 3213;}'; -document.head.appendChild(css); - -var container = document.createElement('div'); -container.style.cssText = 'position:fixed; top: 6px; left: calc(40% - 50px); z-index: 3213; padding: 1px;'; -document.body.appendChild(container); - -var input = document.createElement('input'); -input.style.cssText = 'color: black; border: 1px solid black; padding: 5px; width: 180px; height: 38px;'; -input.placeholder = 'Keyword...'; -input.addEventListener('keypress', (event) => { if (event.key === 'Enter') {button.click();} }); -container.appendChild(input); - -var button = document.createElement('span'); -button.className = 'filterButton'; -button.innerHTML = 'Filter'; -button.addEventListener('click', (event) => { - var keys = input.value.split(/[\|\/\\\+\,\:\; ]+/); - if (filter && keys.join() === keyword.join()) { - popup.style.display = 'none'; - filter = false; - } - else { - popup.innerHTML = ''; - popup.style.display = 'block'; - queue.forEach(data => { - if (keys.filter(key => data.name.includes(key)).length === keys.length) { - getFilterResult(data); - } - }); - popup.querySelectorAll('div').forEach((element, index) => { element.style.top = index * 40 + 'px'; }); - keyword = keys; - filter = true; +async function copyToClipboard() { + if (!confirm(i18n.oncopy)) return; + let data = []; + for (let tr of selected) { + data.push(getClipboardInfo(tr)); } -}); -container.appendChild(button); -// Show filter result -function getFilterResult(data) { - var box = document.createElement('div'); - box.className = 'filterItem'; - popup.appendChild(box); - getFilterItem(box, {html: data.name}); - getFilterItem(box, {html: 'Preview', click: (event) => {event.target.style.cssText = 'background-color: #C3C;'; getPreviewHandler(data, {top: event.clientY, left: event.clientX});}}); - getFilterItem(box, {html: 'Torrent', style: 'display: ' + (data.torrent ? 'block' : 'none')}); - getFilterItem(box, {html: 'Magnet'}); - getFilterItem(box, {html: 'Copy', click: (event) => navigator.clipboard.writeText(data.name + '\n' + (data.torrent ? data.torrent + '\n' : '') + data.magnet)}); + let info = await Promise.all(data); + let copy = info.join('\n\n'); + navigator.clipboard.writeText(copy); + alert(copy); } -function getFilterItem(box, props) { - var item = document.createElement('span'); - item.innerHTML = props.html; - if (props.style) { - item.style.cssText = props.style; + +function downloadWithAria2() { + if (!confirm(i18n.aria2c)) return; + let params = []; + for (let tr of selected) { + params.push(tr.info.magnet); } - if (props.click) { - item.addEventListener('click', props.click); + postMessage({ aria2c: 'aria2c_download', params }); + alert(i18n.onsend); +} + +async function clearStorage() { + if (!confirm(i18n.clear)) return; + GM_deleteValues(GM_listValues()); + for (let tr of torrents) { + tr.classList.remove('nyaa-cached'); } - box.appendChild(item); + alert(i18n.onclear); } -var popup = document.createElement('div'); -popup.style.cssText = 'position: fixed; left: calc(50% - 600px); background-color: #dff0d8; width: 1200px; height: 560px; white-space: nowrap; overflow-y: scroll; display: none;'; -container.appendChild(popup); - -// Extract data -document.querySelectorAll('table > tbody > tr').forEach((element) => { - var data = {}; - var a = element.querySelectorAll('td:nth-child(2) > a'); - a = a.length === 2 ? a[1] : a[0]; - data.id = a.href.split('/').pop(); - data.name = a.innerHTML; - data.src = a.href; - var link = element.querySelectorAll('td:nth-child(3) > a'); - if (link.length === 2) { - data.torrent = link[0].href; - data.magnet = link[1].href; - } - else { - data.magnet = link[0].href; - } - queue.push(data); - a.addEventListener('contextmenu', (event) => { - event.preventDefault(); - a.style.cssText = 'color: #C33;'; - getPreviewHandler(data, {top: event.clientY, left: event.clientX}); - }); -}); +const hotkeys = { + 'ctrl+arrowleft': () => active.previousElementSibling.click(), + 'ctrl+arrowright': () => active.nextElementSibling.click(), + 'alt+shift+e': clearStorage, + 'alt+c': copyToClipboard, + 'alt+d': downloadWithAria2, + 'alt+f': filterTorrents +} -// Preview handler -function getPreviewHandler(data, mouse) { - if (action[data.id] || document.getElementById('preview-' + data.id)) { - return; +document.addEventListener('keydown', (event) => { + let { ctrlKey, altKey, shiftKey, key } = event; + let keys = []; + if (ctrlKey) { + keys.push('ctrl'); } - action[data.id] = true; - if (data.image) { - createPreview(data, mouse); + if (altKey) { + keys.push('alt'); } - else if (data.new) { - openWebPreview(data); + if (shiftKey) { + keys.push('shift'); } - else if (data.sel) { - xmlNodeHandler(data, mouse, getWebPreview); - } - else if (data.none) { - noValidPreview(data); + keys.push(key.toLowerCase()); + let combo = keys.join('+'); + let hotkey = hotkeys[combo]; + if (hotkey) { + event.preventDefault(); + hotkey(); } - else { - xmlNodeHandler(data, mouse, getPreviewURL); +}); + +// get torrent info +for (let tr of document.querySelectorAll('table > tbody > tr')) { + let [genre, name, link, size] = tr.children; + let id = name.children[0].href.match(/\/([^/#]+)(?:#comments)?$/)[1]; + let url = '/view/' + id; + let [{ href: magnet }, { href: torrent } = {}] = [...link.children].reverse(); + tr.info = { id, url, magnet, torrent, size: size.textContent, name: name.textContent }; + torrents.push(tr); + if (GM_getValue(id)) { + tr.classList.add('nyaa-cached'); } -} -function xmlNodeHandler(data, mouse, handler) { - GM_xmlhttpRequest({ - method: 'GET', - url: data.src, - onload: (details) => { - var node = document.createElement('div'); - node.innerHTML = details.response; - handler(node, data, mouse); - }, - onerror: (details) => { - action[data.id] = false; + name.addEventListener('contextmenu', async (event) => { + event.preventDefault(); + let { ctrlKey, altKey, layerX, layerY } = event; + if (ctrlKey) { + let copy = await getClipboardInfo(tr); + navigator.clipboard.writeText(copy) + } else if (altKey) { + postMessage({ aria2c: 'aria2c_download', params: [magnet] }); + } else { + getTorrentPreview(tr, layerY, layerX); + } + }); + tr.addEventListener('click', async (event) => { + let { shiftKey, ctrlKey } = event; + if (ctrlKey) { + event.preventDefault(); + GM_deleteValue(id); + tr.classList.remove('nyaa-cached'); + } else if (shiftKey) { + event.preventDefault(); + if (selected.has(tr)) { + tr.classList.remove('nyaa-checked'); + selected.delete(tr); + } else { + tr.classList.add('nyaa-checked'); + selected.add(tr); + } } }); } -function getPreviewURL(node, data, mouse) { - var description = node.querySelector('#torrent-description').innerHTML; - var url = description.match(/(\*\*)?https?:\/\/[^(\*\*)\r\n]+(\*\*)?/g); - var img = description.match(/\(https?:\/\/[^\)]+\)/g); - if (img) { - data.image = document.createElement('img'); - data.image.src = img[0].substring(1, img[0].length - 1); - createPreview(data, mouse); - } - else if (url) { - var src = url[0].replace(/\*\*/g, ''); - var host = src.split(/[\/:]+/)[1]; - data.src = src; - if (data.sel = hosts[host]) { - xmlNodeHandler(data, mouse, getWebPreview); + +function fetchTorrent(id, url, tr, retries = 3) { + if (working[url]) { + throw new SyntaxError(`${GM_info.script.name} is processing "${url}"`); + } + working[url] = true; + let site = new Set(); + let image = new Set(); + let info = {}; + return fetch(url).then((res) => res.text()).then((text) => { + let result = text.match(/]*id=["']torrent-description["'][^>]*>([\s\S]*?)<\/div>/i)[1]; + let urls = result.match(/https?:\/\/[^\]&)* ]+/g); + if (urls) { + for (let u of urls) { + /\.(jpe?g|png|gif|avif|bmp|webp)/i.test(u) ? image.add(u) : site.add(u); + } } - else { - data.new = true; - openWebPreview(data); + if (image.size > 0) { + info.image = [...image][0]; } - } - else { - data.none = true; - noValidPreview(data); - } + else if (site.size > 0) { + info.site = [...site][0]; + } + GM_setValue(id, info); + tr.classList.add('nyaa-cached'); + delete working[url]; + return info; + }).catch((err) => { + delete working[url]; + if (retries === 0) throw {}; + return new Promise((resolve) => { + setTimeout(() => { + resolve(fetchTorrent(id, url, tr, --retries)); + }, 5000); + }); + }); } -function getWebPreview(node, data, mouse) { - data.image = node.querySelector(data.sel); - if (data.image) { - createPreview(data, mouse); - } - else { - alert(data.name + '\n' + data.src + '\nFailed Previewing'); - action[data.id] = false; - } + +async function getTorrentDetail(tr) { + let { id, url, name, torrent, magnet, size } = tr.info; + let info = GM_getValue(id) ?? fetchTorrent(id, url, tr); + Object.assign(info, tr.info); + return info; } -function openWebPreview(data) { - if (confirm(data.name + '\n' + data.src + '\nNot Supported!\nOpen in New Tab?')) { - open(data.src, '_blank'); - } - action[data.id] = false; + +// copy info to clipboard +async function getClipboardInfo(tr) { + let { url, name, torrent, magnet, size, image, site } = await getTorrentDetail(tr); + return `${i18n.name} + ${name} (${size}) +${i18n.preview} + ${image ?? site ?? 'Null'} +${torrent ? `${i18n.torrent}\n ${torrent}\n` : ''}${i18n.magnet}\n ${magnet.slice(0, magnet.indexOf('&'))}`; } -function noValidPreview(data) { - alert(data.name + '\nNo Preview!'); - action[data.id] = false; + +// show/open preview +async function getTorrentPreview(tr, top, left) { + let { image, site } = await getTorrentDetail(tr); + if (image) { + let img = preview[image]; + if (!img) { + img = document.createElement('img'); + img.src = redirectURL(image); + img.className = 'nyaa-preview'; + img.addEventListener('click', event => img.remove()); + preview[image] = img; + } + img.style.cssText = `top: ${top}px; left: ${left}px;`; + document.body.append(img); + } else if (site) { + GM_openInTab(site); + } } -// Create preview -function createPreview(data, mouse) { - data.image.className = 'previewItem'; - data.image.style.cssText = 'max-height: 800px; width: auto; top: ' + (mouse.top + 900 > screen.availHeight ? screen.availHeight - 900 : mouse.top) + 'px; left: ' + (mouse.left + 600 > screen.availWidth ? screen.availWidth - 600 : mouse.left) + 'px;'; - data.image.addEventListener('click', (event) => data.image.remove()); - document.body.appendChild(data.image); - action[data.id] = false; +function redirectURL(url) { + let start = url.indexOf('//') + 2; + let end = url.indexOf('/', start); + let host = url.substring(start, end); + if (host === 'files.catbox.moe') { + return url.replace('files.catbox.moe', 'i0.wp.com/files.catbox.moe') + '?ssl=1'; + } + return url; } + +// torrent multiple selection with drag'n'drop +let startX; +let startY; +let startClick = false; +let startSelect = false; +let selectBox = document.createElement('div'); +selectBox.className = 'nyaa-select'; +document.body.appendChild(selectBox); + +document.addEventListener('mousedown', (event) => { + if (event.button !== 0 || event.altKey) return; + startX = event.pageX; + startY = event.pageY; + startSelect = true; + document.body.classList.add('nyaa-noselect'); +}); + +document.addEventListener('mousemove', (event) => { + if (!startSelect) return; + let dx = event.pageX - startX; + let dy = event.pageY - startY; + if (Math.abs(dx) > 5 || Math.abs(dy) > 5) { + let x = dx < 0 ? event.pageX : startX; + let y = dy < 0 ? event.pageY : startY; + let w = Math.abs(dx); + let h = Math.abs(dy); + selectBox.style.cssText = `left: ${x}px; top: ${y}px; width: ${w}px; height: ${h}px; display: block;`; + } +}); + +document.addEventListener('mouseup', (event) => { + if (!startSelect) return; + + startSelect = false; + let boxRect = selectBox.getBoundingClientRect(); + let { shiftKey } = event; + + for (let tr of torrents) { + let { left, right, top, bottom } = tr.getBoundingClientRect(); + let overlap = right < boxRect.left || left > boxRect.right || bottom < boxRect.top || top > boxRect.bottom; + + for (let tr of torrents) { + let { left, right, top, bottom } = tr.getBoundingClientRect(); + let overlap = right < boxRect.left || left > boxRect.right || bottom < boxRect.top || top > boxRect.bottom; + + if (!overlap) { + tr.classList.add('nyaa-checked'); + selected.add(tr); + } else if (!shiftKey) { + tr.classList.remove('nyaa-checked'); + selected.delete(tr); + } + } + + } + + document.body.classList.remove('nyaa-noselect'); + selectBox.style.cssText = 'display: none; width: 0; height: 0;'; +}); diff --git a/README.md b/README.md index accf4bc..842da45 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,34 @@ -# How to use +# UserScript for TamperMonkey -- Download `Tampermonkey` extension for `Chromium` or `Mozilla Firefox` -- Click `Raw` then `Tampermonkey` will install the script automatically +## How to use +- Download [Tampermonkey](https://www.tampermonkey.net/) browser extension +- Click `Userscript` below to install them + +## Recommended +- [BilibiliLiveRoomFilter.user.js](https://jc3213.github.io/userscript/BilibiliLiveRoomFilter.user.js) +- [BilibiliVideoDownloader.user.js](https://jc3213.github.io/userscript/BilibiliVideoDownloader.user.js) +- [NGAEmojiManager.user.js](https://jc3213.github.io/userscript/NGAEmojiManager.user.js) +- [NGAUserBlocker.user.js](https://jc3213.github.io/userscript/NGAUserBlocker.user.js) +- [tsdmAutoSignAndWork.user.js](https://jc3213.github.io/userscript/tsdmAutoSignAndWork.user.js) + +## Special Usage +- [NarouSyosetuManager.user.js](https://jc3213.github.io/userscript/NarouSyosetuManager.user.js) +- [NyaaTorrentHelper.user.js](https://jc3213.github.io/userscript/NyaaTorrentHelper.user.js) + - `RightClick`: Preview the thumbnail in current torrent's description + - `Ctrl` + `RightClick`: Copy current torrent's info to clipboard + - `Alt` + `RightClick`: Send magnet uri to aria2 JSON-RPC + - Requires [Download with Aria2](https://github.com/jc3213/download_with_aria2) + - `Ctrl` + `ArrowLeft`: Previous page + - `Ctrl` + `ArrowRight`: Next page + - `Shift` + `LeftClick`: Select/unselect torrents + - `Alt` + `C`: Copy selected torrents' info to clipboard + - Will store torrents' info + - `Alt` + `D`: Send selected magnet uris to aria2 JSON-RPC + - Requires [Download with Aria2](https://github.com/jc3213/download_with_aria2) + - `Alt` + `F`: Filter current page of torrent based on keyword prompt + - `Alt` + `Shift` + `E`: Clear all stored torrents' info +- [KakuyomuAssistant.user.js](https://jc3213.github.io/userscript/KakuyomuAssistant.user.js) +- [SpeedRunHelper.user.js](https://jc3213.github.io/userscript/SpeedRunHelper.user.js) + - `RightCLick` to open the player instead of open a new page +- [BilibiliEmojisExtractor.user.js](https://jc3213.github.io/userscript/BilibiliEmojisExtractor.user.js) + - `Alt` + `Shift` + `E`: Trigger filter prompt based on images' **alt** property diff --git a/RawMangaAssistant.user.js b/RawMangaAssistant.user.js index b7589f0..c6cab19 100644 --- a/RawMangaAssistant.user.js +++ b/RawMangaAssistant.user.js @@ -1,624 +1,322 @@ // ==UserScript== // @name Raw Manga Assistant -// @namespace https://github.com/jc3213/userscript // @name:zh 漫画生肉网站助手 -// @version 55 -// @description Assistant for raw manga online (LoveHeaven, MangaSum, BatoScan, Komiraw and etc.) -// @description:zh 漫画生肉网站 (LoveHeaven, MangaSum, BatoScan, Komiraw等) 助手脚本 +// @namespace https://github.com/jc3213/userscript +// @version 2.0 +// @description Assistant for raw manga online website +// @description:zh 漫画生肉网站助手脚本 // @author jc3213 -// @match *://loveheaven.net/* -// @match *://loveha.net/* -// @match *://mangasum.com/* -// @match *://mangant.com/* -// @match *://manga1000.com/* -// @match *://manga1001.com/* -// @match *://batoscan.net/* -// @match *://manga11.com/* -// @match *://komiraw.com/* +// @match https://jestful.net/* +// @match https://weloma.art/* +// @match https://rawdevart.art/* +// @match https://manga1000.top/* // @connect * -// @grant GM_getValue -// @grant GM_setValue -// @grant GM_addValueChangeListener // @grant GM_xmlhttpRequest -// @grant GM_webRequest -// @webRequest {"selector": "*.adtrue.com/*", "action": "cancel"} -// @webRequest {"selector": "*.bidgear.com/*", "action": "cancel"} -// @webRequest {"selector": "*.googlesyndication.com/*", "action": "cancel"} -// @webRequest {"selector": "*.googletagservices.com/*", "action": "cancel"} -// @webRequest {"selector": "*.cloudfront.net/*", "action": "cancel"} -// @webRequest {"selector": "*.bidadx.com/*", "action": "cancel"} -// @webRequest {"selector": "*.adxpub.com/*", "action": "cancel"} -// @webRequest {"selector": "*.vdo.ai/*", "action": "cancel"} -// @webRequest {"selector": "*.vidazoo.com/*", "action": "cancel"} -// @webRequest {"selector": "*simrubwan.com/*", "action": "cancel"} -// @webRequest {"selector": "*dyecowwhy.com/*", "action": "cancel"} -// @webRequest {"selector": "*mehebborc.com/*", "action": "cancel"} -// @webRequest {"selector": "*alignclamstram.com/*", "action": "cancel"} -// @webRequest {"selector": "*wowjogsot.com/*", "action": "cancel"} -// @webRequest {"selector": "*.optad360.io/*", "action": "cancel"} -// @webRequest {"selector": "*cogleapad.com/*", "action": "cancel"} -// @webRequest {"selector": "*.vlitag.com/*", "action": "cancel"} -// @webRequest {"selector": "*.your-notice.com/*", "action": "cancel"} -// @webRequest {"selector": "*beiven.pw/*", "action": "cancel"} -// @webRequest {"selector": "*runative-syndicate.com/*", "action": "cancel"} -// @webRequest {"selector": "*.mgid.com/*", "action": "cancel"} -// @webRequest {"selector": "*.exdynsrv.com/*", "action": "cancel"} -// @webRequest {"selector": "*.exosrv.com/*", "action": "cancel"} -// @webRequest {"selector": "*.adsco.re/*", "action": "cancel"} -// @webRequest {"selector": "*engine.4dsply.com/*", "action": "cancel"} -// @webRequest {"selector": "*.spolecznosci.net/*", "action": "cancel"} -// @webRequest {"selector": "*prosumsit.com/*", "action": "cancel"} -// @webRequest {"selector": "*.lzrikate.com/*", "action": "cancel"} -// @webRequest {"selector": "*jiltlargosirk.com/*", "action": "cancel"} -// @webRequest {"selector": "*eyefuneve.com/*", "action": "cancel"} -// @webRequest {"selector": "*.popcash.net/*", "action": "cancel"} -// @webRequest {"selector": "*.betteradsystem.com/*", "action": "cancel"} -// @webRequest {"selector": "*.popads.net/*", "action": "cancel"} -// @webRequest {"selector": "*.leadzutw.com/*", "action": "cancel"} -// @webRequest {"selector": "*.cacafly.net/*", "action": "cancel"} -// @webRequest {"selector": "*.ycxtpbfcsl.com/*", "action": "cancel"} -// @webRequest {"selector": "*.clfvfumqqok.com/*", "action": "cancel"} -// @webRequest {"selector": "*.akhlkkdrxwav.com/*", "action": "cancel"} -// @webRequest {"selector": "*.pgqpibyycasfvl.com/*", "action": "cancel"} -// @webRequest {"selector": "*.ntkjbweenycfq.com/*", "action": "cancel"} -// @webRequest {"selector": "*badskates.com/*", "action": "cancel"} +// @run-at document-idle // ==/UserScript== 'use strict'; -// Initial variables -var urls = []; -var save = []; -var aria2 = []; -var fail = []; -var download; -var observer; -var images; -var watching; -var mousedown; -var moving = false; -var position = GM_getValue('position', {top: screen.availHeight * 0.3, left: screen.availWidth * 0.15}); -var offset = {}; -var lazyload; -var warning; -var header = ['Cookie: ' + document.cookie, 'Referer: ' + location.href, 'User-Agent: ' + navigator.userAgent]; +let { host, pathname } = location; +let sites = { + 'jestful.net': { + viewer: /-chapter-/, + manga: { selector: 'img.chapter-img', attr: 'data-aload', except: ['olimposcan2', 'knet_64ba650e3ad61.png', 'cr_649a4491439a0.jpg'] }, + title: () => { + let [, title, chap] = document.title.match(/^(.+)(?:\s-\sRaw)?\sChapter\s(\d+\.?\d?)/); + return { name: title.replace(' - Raw', ''), chap }; + }, + shortcut: { prev: 'a.btn.btn-info.prev', next: 'a.btn.btn-info.next' }, + patch: () => { + localStorage.setItem('shown_at', 3000000000000); + document.querySelector('#list-imga').oncontextmenu = ''; + }, + ads: ', #list-imga > center' + }, + 'weloma.art': { + viewer: /^\/\d+\/\d+/, + manga: { selector: 'img.chapter-img', attr: 'data-src', except: ['/uploads/'] }, + title: { selector: 'img.chapter-img', attr: 'alt', regexp: /^(.+)(?:\s-\sRAW)\sChapter\s(\d+(?:\.\d)?)/ }, + shortcut: { prev: 'a.btn.btn-info.prev', next: 'a.btn.btn-info.next' } + }, + 'rawdevart.art': { + viewer: /\/chapter-/, + manga: { selector: 'canvas[data-srcset]', attr: 'data-srcset', except: ['450x375.png', '800x700.jpeg'] }, + title: { selector: 'meta[property="og:title"]', attr: 'content', regexp: /^(.+)\s(?:RAW)?\s?\sChapter\s(\d+(?:\.\d)?)/ }, + shortcut: { prev: '#sub-app .chapter-btn.prev > a', next: '#sub-app .chapter-btn.next > a' }, + }, + 'manga1000.top': { + viewer: /-chapter-/, + manga: { selector: '#listImgs > img', attr: 'data-src' }, + title: { selector: 'meta[name="description"]', attr: 'content', regexp: /^Read\sraw\smanga\sjp\s(.+)\s-\s(?:RAW)?\schap\s(\d+(?:\.\d)?)/ }, + shortcut: { prev: 'a.rd_top-left', next: 'a.rd_top-right' }, + }, +}; +let watch = sites[host]; + +if (!watch.viewer.test(pathname)) { + return; +} -// i18n strings and labels -var messages = { +let folder; +let urls = []; +let headers = { 'cookie': document.cookie, 'referer': location.href, 'user-agent': navigator.userAgent }; +watch?.patch(); + +let locale = { 'en-US': { save: { - label: 'Download', - done: 'All %n% images have been successfully downloaded', - error: 'Some image can\'t be downloaded' + label: 'Download Chapter', + done: '%n% images downloaded successfully' }, copy: { - label: 'Copy Urls', - done: 'All %n% urls have been copied to clipboard' + label: 'Copy to Clipboard', + done: '%n% URLs copied to clipboard' }, aria2: { - label: 'Send to Aria2 RPC', - done: 'All %n% image urls have been sent to Aria2 RPC', - norpc: 'No response from Aria2 RPC server', - nokey: 'Aria2 RPC secret token is invalid' + label: 'Download with Aria2', + done: '%n% images are sent to Download with Aria2', + error: 'Please config Download with Aria2 correctly', + fatal: '"Download With Aria2" is either not installed, disabled, or lower than v4.17.0.3548' }, - secret: 'Secret token updated, reloading page in 5 seconds', - btop: { + gotop: { label: 'Back to Top', }, - lazy: { - label: 'Preload All Images' - }, - menu: { - label: 'Context Menu Mode' - }, extract: { - start: 'Extracting manga source', - done: 'A total of %n% image urls has been extracted', - fail: 'Download function not available due to extraction failure', - error: 'Can\'t be extracted image extension', - fatal: 'No manga source for extraction. Please send feedback' + start: 'Extracting manga images...', + done: 'Extracted %n% image URLs', + error: 'Unsupported image format' } }, 'zh-CN': { save: { - label: '下载图像', - done: '已成功下载全部 %n% 图像', - error: '无法下载>%部分图像', + label: '下载本章', + done: '已成功下载 %n% 张图片' }, copy: { - label: '复制链接', - done: '%n% 图像链接已复制到剪切板' + label: '复制到剪切板', + done: '已复制 %n% 个图片链接到剪贴板' }, aria2: { - label: '发送至 Aria2 RPC', - icon: '🖅', - done: '全部 %n% 图像链接已发送至Aria2 RPC', - norpc: 'Aria2 RPC 服务器没有响应', - nokey: 'Aria2 RPC 密钥不正确' + label: '通过 Aria2 下载', + done: '%n% 个图片已传入"通过 Aria2 下载"', + error: '请检查"通过 Aria2 下载"是否正确配置', + fatal: '"通过 Aria2 下载" 未安装,或未启用,或版本低于4.17.0.3548' }, - secret: '密钥已更新,5秒后自动刷新页面', - btop: { + gotop: { label: '回到顶部', }, - lazy: { - label: '预加载所有图像', - }, - menu: { - label: '右键菜单模式', - }, extract: { - start: '正在解析图像来源', - done: '已成功解析全部 %n% 图像来源', - fail: '无法解析图像来源,下载功能无法使用', - error: '无法解析图像后缀', - fatal: '无法获取图像来源,请反馈问题' + start: '正在提取漫画图片...', + done: '已成功提取 %n% 张图片链接', + error: '不支持的图片格式' } } }; -var i18n = messages[navigator.language] || messages['en-US']; +let i18n = locale[navigator.language] ?? locale['en-US']; -// Supported sites -var mangas = { - 'loveheaven.net': { - chapter: /\/read-(.+)-raw-chapter-(.+)\.html/, - folder: () => {return chapter[1].replace(/-manga/, '') + '\\' + chapter[2]}, - selector: 'img.chapter-img', - ads: ['h3', 'br:nth-child(-n+3)', 'div.float-ck', 'div.chapter-content center'], - shortcut: {prev: 'a[class="btn btn-info prev"]', next: 'a[class="btn btn-info next"]'} - }, - 'mangasum.com': { - chapter: /\/manga\/(.+)-raw\/chapter-(.+)\//, - folder: () => {return chapter[1].replace(/-manga/, '') + '\\' + chapter[2]}, - selector: 'div[id^="page_"] > img', - lazyload: 'data-original' - }, - 'batoscan.net': { - chapter: /\/read-(.+)-raw-chapter-(.+)\.html/, - folder: () => {return chapter[1].replace(/-manga/, '') + '\\' + chapter[2]}, - selector: 'img[class="chapter-img"]', - lazyload: 'data-original', - }, - 'manga1000.com': { - chapter: /%E3%80%90%E7%AC%AC(.+)%E8%A9%B1%E3%80%91(.+)-raw/, - folder: () => {return decodeURI(chapter[2]) + '\\' + chapter[1]}, - selector: 'figure[class="wp-block-image"] > img', - shortcut: 'div[class="linkchap"] > a' - }, - 'komiraw.com': { - chapter: /\/([^\/]+)-raw-chap-(.+)/, - folder: () => {return chapter.slice(1).join('\\')}, - selector: 'img[class^="chapter-img"]', - shortcut: {prev: 'a[id="prev_chap"]', next: 'a[id="next_chap"]'} - } -}; -mangas['loveha.net'] = mangas['loveheaven.net']; -mangas['mangant.com'] = mangas['mangasum.com']; -mangas['manga1001.com'] = mangas['manga1000.com']; -mangas['manga11.com'] = mangas['komiraw.com']; -watching = mangas[location.host]; +let overlay = document.createElement('div'); +overlay.className = 'assist-overlay'; -var css = document.createElement('style'); -css.innerHTML = '.menuOverlay {position: fixed; z-index: 999999999; background-color: white;}\ -.menuContainer {min-width: fit-content; max-width: 330px; border: 1px ridge darkblue; font-size: 14px;}\ -.assistantIcon {width: 30px; display: inline-block; text-align: center}\ -.assistantMenu {color: black; width: 190px; padding: 10px; height: 40px; display: block; user-select: none;}\ -.assistantMenu:hover {background-color: darkviolet; color: white; cursor: default;}\ -.menuAria2Item {width: 300px; height: 42px; overflow: hidden;}\ -.menuAria2Item:focus {background-color: darkblue; color: white;}' -document.head.appendChild(css); +let css = document.createElement('style'); +css.textContent = ` +.assist-overlay { position: fixed; top: 0px; left: 0px; z-index: 999999999; width: 100%; inset: 0; display: flex; flex-direction: column; pointer-events: none; align-items: center; } +.assist-notify { pointer-events: auto; width: fit-content; margin-top: 10px; } +.assist-mainmenu, .assist-menuitem, .assist-notify { color: #000; background-color: #fff; border: 1px solid darkviolet; } +.assist-mainmenu { position: fixed; z-index: 999999999; } +.assist-menuitem:hover { filter: brightness(1.0) contrast(0.7); } +.assist-menuitem:active { filter: brightness(0.9) contrast(0.6); } +.assist-menuitem, .assist-notify { padding: 5px; text-align: center; font-size: 16px; user-select: none; } +.assist-menuitem { width: 180px; } +.assist-hidden ${watch.ads} { display: none !important } +`; -var button = document.createElement('span'); -button.id = 'assistant_button'; -button.innerHTML = '🖱️'; -button.className = 'menuOverlay menuContainer'; -button.draggable = true; -button.style.cssText = 'top: ' + position.top + 'px; left: ' + position.left + 'px; text-align: center; padding-top: 10px; width: 42px; height: 42px;'; -document.body.appendChild(button); +let ctxmenu = document.createElement('div'); +ctxmenu.className = 'assist-mainmenu assist-hidden'; +ctxmenu.innerHTML = ` +
${i18n.save.label}
+
${i18n.copy.label}
+
${i18n.aria2.label}
+
${i18n.gotop.label}
+`; -var container = document.createElement('div'); -container.id = 'assistant_container'; -container.className = 'menuOverlay'; -container.style.cssText = 'top: ' + position.top + 'px; left: ' + (position.left + button.offsetWidth) + 'px; display: none'; -document.body.appendChild(container); +let aria2btn = ctxmenu.children[2]; -// Draggable button and menu -document.addEventListener('dragstart', (event) => { - offset.top = event.clientY; - offset.left = event.clientX -}); -document.addEventListener('dragend', (event) => { - position.top += event.clientY - offset.top; - position.left += event.clientX - offset.left; - GM_setValue('position', position); -}); -GM_addValueChangeListener('position', (name, old_value, new_value, remote) => movingIconAndContainer(new_value)); -function movingIconAndContainer(position) { - button.style.top = position.top + 'px'; - button.style.left = position.left + 'px'; - container.style.top = button.offsetTop + 'px'; - container.style.left = button.offsetLeft + button.offsetWidth + 'px'; -} -document.addEventListener('click', (event) => { - if (event.target.id === 'assistant_aria2_server' || event.target.id === 'assistant_aria2_secret') { - return; - } - if (event.target.id === 'assistant_button') { - if (container.style.display === 'none') { - container.style.display = 'block'; - } - else { - container.style.display = 'none'; - } - } - else { - container.style.display = 'none'; - } +ctxmenu.addEventListener('click', (event) => { + let { id } = event.target; + ctxMenuEvent[id]?.(); }); -// Create menuitems -var downBox = document.createElement('div'); -downBox.id = 'assistant_down'; -downBox.className = 'menuContainer'; -downBox.style.display = 'none'; -container.appendChild(downBox); +document.body.append(ctxmenu, css, overlay); -var downMenu = { - save: { - icon: '💾', - click: () => { - download = []; - save.forEach((item, index) => { - GM_xmlhttpRequest({ - method: 'GET', - url: item[0], - responseType: 'blob', - onload: (details) => { - var a = document.createElement('a'); - a.href = URL.createObjectURL(details.response); - a.download = item[1]; - a.click(); - download.push(index); - if (download.length === images.length) { - notification('save', 'done'); - download = []; - } - }, - onerror: () => notification('save', 'error', item[0]) - }); - }); - } - }, - copy: { - icon: '📄', - click: () => { - navigator.clipboard.writeText(urls.join('\n')); - notification('copy', 'done'); - } - }, - aria2: { - icon: '🖅', - click: () => { - downMenu.aria2.handler({ - method: 'aria2.getGlobalOption' - }, (details) => { - if (details.status === 200) { - if (details.response.includes('Unauthorized')) { - notification('aria2', 'nokey'); - } - else { - var dir = details.response.match(/"dir":"([^"]+)"/)[1] + '\\' + watching.folder(); - var aria2 = save.map((item, index) => downMenu.aria2.handler({ - method: 'aria2.addUri', - options: [[item[0]], {out: item[1], dir: dir, header: header}] - }, () => { - if (index === images.length - 1) { - notification('aria2', 'done'); - } - })); - } - } - else { - notification('aria2', 'norpc'); - } - }, (error) => { - notification('aria2', 'norpc'); - }); - }, - handler: (property, onload, onerror) => { - GM_xmlhttpRequest({ - url: document.getElementById('assistant_aria2_server').value, - method: 'POST', - data: JSON.stringify({ - id: '', - jsonrpc: '2.0', - method: property.method, - params: ['token:' + document.getElementById('assistant_aria2_secret').value].concat(property.options) - }), - onload: onload, - onerror: onerror - }); - }, - event: { - contextmenu: (event) => { - event.preventDefault(); - if (aria2Box.style.display === 'block') { - aria2Box.style.display = 'none'; - } - else { - aria2Box.style.display = 'block'; - } - } - } +const ctxMenuEvent = { + 'assist-download': downloadAllUrls, + 'assist-clipboard': copyAllUrls, + 'assist-message': downloadWithAria2, + 'assist-scrolltop'() { + document.documentElement.scrollTop = 0; } }; -Object.entries(downMenu).forEach((item) => downBox.appendChild(click_menu_item(...item))); - -var clickBox = document.createElement('div'); -clickBox.id = 'assistant_click'; -clickBox.className = 'menuContainer'; -container.appendChild(clickBox); -var clickMenu = { - btop: { - icon: '⬆️', - click: () => { - document.documentElement.scrollTop = 0; - } +async function downloadAllUrls() { + let index = 0; + let download = []; + for (let url of urls) { + let props = { url, headers, method: 'GET', responseType: 'blob' }; + let time = index++ * 200; + let name = String(index).padStart(3, '0'); + download.push(new Promise((resolve, reject) => { + props.onload = (details) => { + setTimeout(() => { + let blob = details.response; + let a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = name + '.' + blob.type.slice(blob.type.indexOf('/') + 1); + a.click(); + URL.revokeObjectURL(a.href); + a = null; + resolve(true); + }, time); + }; + props.onerror = reject; + GM_xmlhttpRequest(props); + })); } -}; -Object.entries(clickMenu).forEach((item) => clickBox.appendChild(click_menu_item(...item))); - -var switchBox = document.createElement('div'); -switchBox.id = 'assistant_switch'; -switchBox.className = 'menuContainer'; -container.appendChild(switchBox); - -var switchMenu = { - lazy: { - value: GM_getValue('lazy', false), - on: () => { - if (!images || !watching.lazyload) { - return; - } - lazyload = setInterval(() => { - if (images.length === urls.length) { - images.forEach((element) => element.setAttribute('src', element.getAttribute(watching.lazyload))); - clearInterval(lazyload); - } - }, 100); - }, - off: () => { - clearInterval(lazyload); - } - }, - menu: { - value: GM_getValue('menu', true), - on: () => { - button.style.display = 'none'; - document.addEventListener('contextmenu', switchMenu.menu.handler); - }, - off: () => { - document.removeEventListener('contextmenu', switchMenu.menu.handler); - button.style.display = 'block'; - container.style.top = button.offsetTop + 'px'; - container.style.left = button.offsetLeft + button.offsetWidth + 'px'; - }, - handler: (event) => { - if (event.target.id === 'assistant_menu_aria2' || event.shiftKey) { - if (container.style.display = 'block') { - container.style.display = 'none'; - } - return; - } - event.preventDefault(); - container.style.top = event.clientY + 'px'; - container.style.left = event.clientX + 'px'; - container.style.display = 'block'; - } - }, + await Promise.all(download); + notification('save', 'done'); } -Object.entries(switchMenu).forEach((item) => switchBox.appendChild(switch_menu_item(...item))); - -var aria2Box = document.createElement('div'); -aria2Box.id = 'assistant_aria2'; -aria2Box.className = 'menuContainer'; -aria2Box.style.cssText = 'position: absolute; display: none; top: ' + Object.keys(downMenu).indexOf('aria2') * 40 + 'px; left: 190px;'; -container.appendChild(aria2Box); -var aria2Menu = { - // Aria2 menu items - server: { - value: GM_getValue('server', 'http://localhost:6800/jsonrpc') - }, - secret: { - type: 'password', - value: GM_getValue('secret', '') - } -}; -Object.entries(aria2Menu).forEach((item) => aria2Box.appendChild(input_menu_item(...item))); - -// UI maker callback handler -function iconic_menu_item(name, props) { - var menu = document.createElement('span'); - menu.id = 'assistant_menu_' + name; - menu.className = 'assistantMenu'; - var icon = document.createElement('span'); - icon.className = 'assistantIcon'; - icon.innerHTML = props.icon || ''; - var label = document.createTextNode(i18n[name].label || name); - menu.appendChild(icon); - menu.appendChild(label); - return menu; +function copyAllUrls() { + navigator.clipboard.writeText(urls.join('\n')); + notification('copy', 'done'); } -function click_menu_item(name, props) { - var menu = iconic_menu_item(name, props); - menu.addEventListener('click', props.click); - if (props.event) { - Object.entries(props.event).forEach((item) => menu.addEventListener(...item)); - } - return menu; -} -function switch_menu_item(name, props) { - var menu = iconic_menu_item(name, props); - menu.value = props.value; - menu.addEventListener('click', () => GM_setValue(name, !menu.value)); - switch_item_handler(menu, props); - GM_addValueChangeListener(name, (name, old_value, new_value, remote) => { - menu.value = new_value; - switch_item_handler(menu, props); - }); - return menu; -} -function switch_item_handler(menu, props) { - if (menu.value) { - menu.firstElementChild.innerHTML = '✅'; - props.on(); - } - else { - menu.firstElementChild.innerHTML = ''; - props.off(); - } -} -function input_menu_item(name, props) { - var menu = document.createElement('input'); - menu.id = 'assistant_aria2_' + name; - menu.className = 'assistantMenu menuAria2Item'; - menu.value = props.value; - menu.setAttribute('type', props.type); - menu.addEventListener('change', (event) => GM_setValue(name, event.target.value)); - menu.addEventListener('focus', (event) => event.target.setAttribute('type', 'text')); - menu.addEventListener('blur', (event) => event.target.setAttribute('type', props.type)); - GM_addValueChangeListener(name, (name, old_value, new_value, remote) => { - menu.value = new_value; - }); - return menu; + +function downloadWithAria2() { + bridge('aria2c_status').then(async ({ options }) => { + let params = []; + options.dir += folder; + for (let url of urls) { + params.push({ url, options }); + } + let { result, error } = await bridge('aria2c_download', params); + let action = result ? 'done' : 'error'; + notification('aria2', action); + }).catch(notification); } -// Extract images data -if (watching) { - var chapter = location.pathname.match(watching.chapter); - if (chapter) { - images = document.querySelectorAll(watching.selector); - removeMultipleElement(watching.ads); - extractImage(watching.lazyload); - shortcuts(watching.shortcut); +document.addEventListener('contextmenu', (event) => { + let { shiftKey, ctrlKey, altKey, clientY, clientX } = event; + if (shiftKey || ctrlKey || altKey) { + return; } -} + event.preventDefault(); + ctxmenu.classList.remove('assist-hidden'); + let { innerWidth, innerHeight } = document.defaultView; + let { offsetWidth, offsetHeight } = ctxmenu; + let top = clientY + offsetHeight > innerHeight ? innerHeight - offsetHeight : clientY; + let left = clientX + offsetWidth > innerWidth ? innerWidth - offsetWidth : clientX; + ctxmenu.style.cssText = `top: ${top}px; left: ${left}px;`; +}); -function removeMultipleElement(selector, node) { - node = node || document; - Array.isArray(selector) ? selector.forEach(item => removeElement(item)) : removeElement(selector); +document.addEventListener('click', (event) => { + ctxmenu.classList.add('assist-hidden'); +}); - function removeElement(sel) { - node.querySelectorAll(sel).forEach(item => item.remove()); +document.addEventListener('keydown', (event) => { + if (!watch.shortcut) return; + let { code } = event; + let hotkey = code === 'ArrowLeft' + ? document.querySelector(watch.shortcut.prev) + : code === 'ArrowRight' ? document.querySelector(watch.shortcut.next) + : null; + if (hotkey) { + hotkey.href ? open(hotkey.href , '_self') : hotkey.click(); } -} +}); -function extractImage(lazyload) { +let started = notification('extract', 'start'); +setTimeout(() => { + let { selector, except } = watch.manga; + let images = document.querySelectorAll(selector); if (images.length === 0) { - return notification('extract', 'fatal'); + notification('extract', 'error'); + return; } - warning = notification('extract', 'start'); - images.forEach((element, index) => { - var source = element.getAttribute(lazyload || 'src'); - var url = source.trim().replace(/^\/\//, 'http://'); - var name = ('000' + index).substr(index.toString().length); - getExtension(index, url, name); - }); - observer = setInterval(() => { - if (images.length === urls.length + fail.length) { - warning.remove(); - clearInterval(observer); - if (fail.length === 0) { - document.getElementById('assistant_down').style.display = 'block'; - notification('extract', 'done'); - } - else { - notification('extract', 'fail', '\n' + fail.join('\n')); + for (let i of document.querySelectorAll(selector)) { + let src = i.getAttribute(watch.manga.attr) ?? i.getAttribute('src'); + let url = src.trim(); + if (url.startsWith('//')) { + url.replace('//', 'http://'); + } + if (except) { + let matched = false; + for (let s of except) { + if (url.includes(s)) { + i.remove(); + matched = true; + break; + } } + if (matched) continue; } - }, 100); -} -function getExtension(index, url, name) { - var ext = url.match(/(png|jpg|jpeg|webp)/); - if (ext) { - storeImageInfo(index, url, name, ext); + urls.push(url); } - else { - GM_xmlhttpRequest({ - url, url, - method: 'HEAD', - onload: (details) => { - ext = details.responseHeaders.match(/(png|jpg|jpeg|webp)/); - storeImageInfo(index, url, name, ext); - }, - onerror: () => { - fail.push(url); - } - }); + let name; + let chap; + let symbol = navigator.platform === 'Win32' ? '\\' : '/'; + if (typeof watch.title === 'function') { + let title = watch.title(); + name = title.name; + chap = title.chap; + } else { + let title = document.querySelector(watch.title.selector).getAttribute(watch.title.attr); + let temp = title.match(watch.title.regexp); + console.log(title, watch.title.regexp); + console.log(temp); + name = temp[1]; + chap = temp[2] } -} -function storeImageInfo(index, url, name, ext) { - if (ext) { - name += '.' + ext[0]; + let title = symbol + name + symbol + chap.padStart(2, '0'); + folder = title.replace(/[:\?\"\']/g, '_'); + console.log(folder); + started.remove(); + notification('extract', 'done'); +}, 2000); + +function notification(action, status, url) { + let text = i18n[action][status] ?? i18n[action]; + if (text.includes('%n%')) { + text = text.replace('%n%', urls.length); } - urls.push(url); - save.push([url, name]); + let notify = document.createElement('div'); + notify.className = 'assist-notify'; + notify.textContent = `🔔 ${text}`; + notify.addEventListener('click', (e) => notify.remove()); + setTimeout(() => notify.remove(), 5000); + overlay.appendChild(notify); + return notify; } -// Append shortcut event -function shortcuts(shortcut) { - if (typeof shortcut === 'string') { - var prev = document.querySelectorAll(shortcut)[0]; - var next = document.querySelectorAll(shortcut)[1]; - } - else if (Array.isArray(shortcut)) { - prev = document.querySelector(shortcut[0]); - next = document.querySelector(shortcut[1]); - } - else if (typeof shortcut === 'object') { - prev = document.querySelector(shortcut.prev); - next = document.querySelector(shortcut.next); - } - document.addEventListener('keydown', (event) => { - shortcut_event_handler(event, 'ArrowLeft', prev); - shortcut_event_handler(event, 'ArrowRight', next); +let message = {}; + +function bridge(aria2c, params) { + return new Promise((resolve, reject) => { + let id = crypto.randomUUID(); + let timer = setTimeout(() => { + delete message[id]; + reject( new Error() ); + }, 3000); + message[id] = (result) => { + clearTimeout(timer); + delete message[id]; + resolve(result); + } + window.postMessage({ aria2c, id, params }); }); } -function shortcut_event_handler(event, key, element) { - if (!element || element.hasAttribute('disabled')) { - return; - } - if (typeof key === 'string' && event.key === key || - typeof key === 'number' && event.keyCode === key) { - element.click(); - } -} -// Notifications -function notification(action, status, url) { - var warn = i18n[action][status] || i18n[action]; - var html = '⚠️' + warn.replace('%n%', '' + images.length + ''); - if (url) { - html += '

' + url + '

'; - } - var caution = document.createElement('div'); - caution.id = 'assistant_caution'; - caution.className = 'menuOverlay assistantMenu'; - caution.style.cssText = 'width: fit-content; height: 50px; text-align: center; font-size: 16px; padding: 12px; border: 1px ridge darkviolet; border-radius: 10px;'; - caution.innerHTML = html; - document.body.appendChild(caution); - align_notification(); - if (action === 'extract' && ['start', 'fail'].includes(status)) { - return caution; +window.addEventListener('message', (event) => { + let { aria2c, id, result } = event.data; + if (aria2c === 'aria2c_response') { + message[id]?.(result); } - setTimeout(() => { - caution.remove(); - align_notification(); - }, 3000); -} -function align_notification() { - document.querySelectorAll('#assistant_caution').forEach((element, index) => { - element.style.top = index * (element.offsetHeight + 5) + 10 + 'px'; - element.style.left = (screen.availWidth - element.offsetWidth) / 2 + 'px'; - }); -} +}); diff --git a/SpeedRunHelper.user.js b/SpeedRunHelper.user.js index 53dba80..5802c1b 100644 --- a/SpeedRunHelper.user.js +++ b/SpeedRunHelper.user.js @@ -1,99 +1,181 @@ // ==UserScript== // @name Speedrun.com Helper // @namespace https://github.com/jc3213/userscript -// @version 4 +// @version 1.6.0 // @description Easy way for speedrun.com to open record window // @author jc3213 -// @match *://www.speedrun.com/* -// @grant GM_xmlhttpRequest -// @grant GM_webRequest -// @webRequest {"selector": "https://www.speedrun.com/assets/js/ads.js", "action": "cancel"} -// @webRequest {"selector": "*.amazon-adsystem.com/*", "action": "cancel"} -// @webRequest {"selector": "*.doubleclick.net/*", "action": "cancel"} -// @webRequest {"selector": "*lngtd.com/*", "action": "cancel"} -// @webRequest {"selector": "*.hotjar.com/*", "action": "cancel"} -// @webRequest {"selector": "*.scorecardresearch.com/*", "action": "cancel"} -// @webRequest {"selector": "*adservice.google.com/*", "action": "cancel"} -// @webRequest {"selector": "*quantcast.mgr.consensu.org/*", "action": "cancel"} +// @match https://www.speedrun.com/* // ==/UserScript== 'use strict'; -var logger = {}; -var offset = {}; +let speedrun = {}; +let worker = {}; +let style = {}; +let srDrag; +let srY; +let srX; +let srPane = -1; +let srWatch = location.pathname.split('/')?.[1]; -var css = document.createElement('style'); -css.innerHTML = '.speedrun-menu {background-color: #FFF; cursor: pointer; border: 1px outset #F00; padding: 1px; font-size: 14px;}\ -.speedrun-menu:hover {filter: opacity(60%);}\ -.speedrun-menu:active {border: 2px inset #00F; padding: 0px; filter: opacity(30%);}'; -document.head.appendChild(css); +let { innerHeight, innerWidth } = window; +let fixedX; +let fixedY; +if (innerHeight < 720) { + fixedX = 854; + fixedY = 480; +} else if (innerHeight < 1080) { + fixedX = 1280; + fixedY = 720; +} else if (innerHeight < 1440) { + fixedX = 1920; + fixedY = 1080; +} else if (innerHeight < 2160) { + fixedX = 2560; + fixedY = 1440; +} else { + fixedX = 3840; + fixedY = 2160; +} -document.querySelectorAll('div[data-ad]').forEach(item => item.remove()); -document.querySelector('body > main > div > div:nth-child(5)').remove(); +let css = document.createElement('style'); +css.innerHTML = ` +#app-main [class$="lg:w-[400px]"] { display: none !important; } +.speedrun-window { position: absolute; z-index: 999999; display: grid; grid-template-columns: calc(100% - 66px) 66px; } +.speedrun-window iframe { width: ${fixedX}px; height: ${fixedY}px; grid-column: span 2; } +.speedrun-record, .speedrun-menu { background-color: #181B1C; display: flex; height: 22px; } +.speedrun-record > * { flex: 1; margin: auto; padding: 0px 5px 0px 3px; } +.speedrun-record * { display: inline-block !important; } +.speedrun-record img { height: 16px !important; width: 16px !important; position: relative !important; margin-right: 3px; } +.speedrun-menu {margin-left: auto;} +.speedrun-menu > * { background-color: #fff; color: #000; cursor: pointer; height: 20px; width: 20px; font-size: 14px; text-align: center; vertical-align: top; margin-left: 2px; } +.speedrun-menu > :hover { filter: opacity(60%); } +.speedrun-menu > :active { filter: opacity(30%); } +#speedrun-minimum { line-height: 30px; } +.speedrun-minimum { bottom: 0px; left: 0px; width: 30% !important; height: 20px !important; z-index: 99999; } +.speedrun-minimum iframe { display: none !important; } +.speedrun-maximum { top: 0px; left: 0px; width: ${innerWidth - 4}px !important; height: ${innerHeight - 4}px !important; z-index: 999999; } +.speedrun-maximum iframe { width: ${innerWidth - 4}px !important; height: ${innerHeight - 24}px !important; } +#speedrun-restore, .speedrun-minimum #speedrun-minimum, .speedrun-maximum #speedrun-maximum { display: none; } +.speedrun-minimum #speedrun-restore, .speedrun-maximum #speedrun-restore { display: block; }`; +document.body.append(css); -document.getElementById('leaderboarddiv').addEventListener('contextmenu', (event) => { - event.preventDefault(); - var element; - document.querySelectorAll('tr[data-target]').forEach(item => { if (item.contains(event.target)) element = item; }); - if (element) { - var src = element.getAttribute('data-target'); - var id = src.split('/').pop(); - viewSpeedrunRecord(id, src, event.clientY, event.clientX); +document.addEventListener('dragstart', (event) => { + let pane = event.target.closest('div[id^=speedrun-'); + if (pane.draggable) { + srDrag = pane; + srX = event.clientX - srDrag.offsetLeft; + srY = event.clientY - srDrag.offsetTop; } }); -function viewSpeedrunRecord(id, src, top, left) { - if (document.getElementById('speedrun-' + id)) { - document.getElementById('speedrun-' + id).remove(); +document.addEventListener('dragover', (event) => { + event.preventDefault(); +}); + +document.addEventListener('drop', (event) => { + event.preventDefault(); + let left = event.clientX - srX; + let top = event.clientY - srY; + srDrag.offset = srDrag.style.cssText = `left: ${left}px; top: ${top}px;`; + srDrag = null; +}); + +document.querySelector('main').addEventListener('contextmenu', async (event) => { + if (event.ctrlKey || event.altKey || event.shiftKey) { + return; } - if (logger[id]) { - createRecordWindow(id, logger[id], top, left); + let result; + if (srWatch === '') { + let record = event.target.closest('div.cursor-pointer.x-focus-outline-offset.overflow-hidden'); + if (record) { + let [rank, time, player] = record.querySelectorAll('a > .truncate, a.x-username-truncate'); + result = { url: rank.parentNode.href, rank, player, time }; + } + } else if (srWatch === 'series') { + let record = event.target.closest('div.cursor-pointer.x-focus-outline-offset.overflow-hidden'); + if (record) { + let [rank, time, player] = record.querySelectorAll('a > .truncate, a.x-username-truncate'); + result = { url: rank.parentNode.href, rank, player, time }; + } + } else if (srWatch === 'users') { + let record = event.target.closest('div.cursor-pointer.x-focus-outline-offset.overflow-hidden'); + if (record) { + let player = document.querySelector('.x-username > span'); + let [rank, time] = record.querySelectorAll('a > .truncate'); + result = { url: rank.parentNode.href, rank, player, time }; + } + } else { + let record = event.target.closest('tr'); + if (record) { + let [rank, player, time] = record.querySelectorAll('a'); + result = { url: rank.href, rank, player, time }; + } } - else { - GM_xmlhttpRequest({ - url: src, - method: 'GET', - onload: (response) => { - var xml = document.createElement('div'); - xml.innerHTML = response.responseText; - logger[id] = xml.querySelector('#centerbar').querySelector('iframe'); - if (!logger[id]) { - logger[id] = xml.querySelector('#centerbar > div > div > center > a').href; - } - createRecordWindow(id, logger[id], top, left); - } - }); + if (!result) { + return; } -} - -function createRecordWindow(id, content, top, left) { - if (typeof content === 'string') { - return open(content, '_blank'); + event.preventDefault(); + let { url, rank, player, time } = result; + let id = url.substring(url.lastIndexOf('/') + 1); + if (worker[id]) { + return; } - - var container = document.createElement('div'); - container.id = 'speedrun-' + id; - container.draggable = 'true'; - container.style.cssText = 'position: fixed; top: ' + top / 2 + 'px; left: ' + left / 2 + 'px; z-index: 3213; width: 850px; height: 500px;'; - document.body.appendChild(container); - - var box = document.createElement('div'); - box.style.cssText = 'background-color: #000; height: 25px; width: 100%; text-align: right; user-select: none;'; - container.appendChild(box); - - var close = document.createElement('span'); - close.className = 'speedrun-menu'; - close.addEventListener('click', (event) => container.remove()) - close.innerHTML = '✖'; - box.appendChild(close); - - container.appendChild(content); -} - -document.addEventListener('dragstart', (event) => { - offset.top = event.clientY; - offset.left = event.clientX -}); -document.addEventListener('dragend', (event) => { - event.target.style.top = event.target.offsetTop + event.clientY - offset.top + 'px'; - event.target.style.left = event.target.offsetLeft + event.clientX - offset.left + 'px'; + worker[id] = true; + let title = `
Rank: ${rank.innerHTML}
Player: ${player.innerHTML}
Time: ${time.textContent}
`; + let pane = speedrun[id]; + if (pane) { + worker[id] = false; + pane.style.cssText = pane.offset; + pane.classList.remove('speedrun-maximum', 'speedrun-minimum'); + document.body.appendChild(speedrun[id]); + return; + } + let response = await fetch(url); + let html = await response.text(); + let start = html.indexOf('', start) + 9; + let iframe = html.substring(start, end); + let offset = ++srPane * 30; + let top = 130 + offset; + let left = (innerWidth - fixedX) / 2 + offset; + if (left < 0) { + left = 0; + } + let style = `top: ${top}px; left: ${left}px;`; + console.log(iframe, style); + pane = document.createElement('div'); + pane.id = 'speedrun-' + id; + pane.className = 'speedrun-window'; + pane.draggable = true; + pane.innerHTML = ` +
${title}
+
+
+
🔳
+
🔲
+
+
+${iframe}`; + pane.style.cssText = pane.offset = style; + pane.addEventListener('click', (event) => { + let { id } = event.target; + if (id === 'speedrun-minimum') { + pane.classList.add('speedrun-minimum'); + pane.classList.remove('speedrun-maximum'); + pane.style.cssText = ''; + } else if (id === 'speedrun-maximum') { + pane.classList.add('speedrun-maximum'); + pane.classList.remove('speedrun-minimum'); + pane.style.cssText = ''; + } else if (id === 'speedrun-restore') { + pane.style.cssText = pane.offset; + pane.classList.remove('speedrun-maximum', 'speedrun-minimum'); + } else if (id === 'speedrun-remove') { + pane.remove(); + --srPane; + } + }); + document.body.appendChild(pane); + speedrun[id] = pane; + worker[id] = false; }); diff --git a/TategakiPDFManager.user.js b/TategakiPDFManager.user.js deleted file mode 100644 index 5a09315..0000000 --- a/TategakiPDFManager.user.js +++ /dev/null @@ -1,359 +0,0 @@ -// ==UserScript== -// @name 縦書きPDF書庫 -// @namespace https://github.com/jc3213/userscript -// @version 18 -// @description 「小説家になろう」の小説情報を管理し、縦書きPDFをダウンロードするツールです -// @author jc3213 -// @match *://ncode.syosetu.com/n* -// @match *://novel18.syosetu.com/n* -// @connect pdfnovels.net -// @grant GM_getValue -// @grant GM_setValue -// @grant GM_xmlhttpRequest -// @grant GM_webRequest -// @webRequest [ {"selector": "*.microad.net/*", "action": "cancel"}, {"selector": "*.microad.jp/*", "action": "cancel"} ] -// ==/UserScript== - -'use strict'; -var novelist = {ncode: location.pathname.match(/n\w{4,}/g), title: document.title, now: Date.now()}; -novelist.myncode = novelist.ncode = novelist.ncode.pop(); -var validate = {}; -var download = {}; -var session = {}; -var worker = false; -var bookmark = GM_getValue('bookmark', {}); -var scheduler = GM_getValue('scheduler', novelist.now); - -// UI作成関連 -var css = document.createElement('style'); -css.innerHTML = '.manager-button {background-color: #FFF; padding: 5px; border: 1px outset #000 ; user-select: none ; z-index: 3213; display: inline-block; font-weight: bold; cursor: pointer;}\ -.manager-button:hover {filter: opacity(60%);}\ -.manager-button:active {filter: opacity(30%);}\ -.manager-checked {padding: 4px; border: 2px inset #00F;}\ -.manager-container {position: fixed; top: 47px; left: calc((100% - 880px) / 2); background-color: #FFF; padding: 10px; z-index: 3213; border: 1px solid #CCC; width: 880px; height: 600px;}\ -.manager-container > span, .manager-container > input {margin-left: 5px;}\ -.manager-container > div {padding: 5px; margin-top: 5px;}\ -.manager-shelf, .manager-logs {overflow-y: scroll; height: 552px;}\ -.manager-shelf div:nth-child(2n+1) {background-color: #DDD;}\ -.manager-shelf span {height: 40px; text-align: center; vertical-align: middle; display: inline-block; padding: 5px;}\ -.manager-shelf input {width: 50px;}\ -.manager-shelf span:nth-child(1) {width: 80px; margin-top: 15px; cursor: pointer;}\ -.manager-shelf span:nth-child(2) {width: 460px; overflow-y: hidden; text-align: left; cursor: pointer;}\ -.manager-shelf span:nth-child(3) {width: 90px; margin-top: 15px;}\ -.manager-shelf span:nth-child(4) {width: 180px; overflow-y: hidden; cursor: pointer;}\ -.manager-shelf div:nth-child(1) span {height: 20px; overflow-y: hidden; text-align: center; margin: 0px; cursor: default;}\ -.manager-shelf div:nth-child(n+2) span:hover {filter: opacity(60%);}\ -.manager-shelf div:nth-child(n+2) span:active {filter: opacity(30%);}\ -.fancylog {font-size: 16px;}\ -.notification {position: fixed; width: fit-content; border-radius: 5px; border: 1px solid #000;}'; -document.head.appendChild(css); - -var manager = document.createElement('span'); -manager.innerHTML = '書庫管理'; -manager.className = 'manager-button'; -manager.style.cssText = 'margin: 10px 5px;' -manager.addEventListener('click', (event) => { - if (manager.classList.contains('manager-checked')) { - container.style.display = 'none'; - } - else { - container.style.display = 'block'; - } - manager.classList.toggle('manager-checked'); -}); -document.getElementById('head_nav').appendChild(manager); - -var container = document.createElement('div'); -container.className = 'manager-container'; -container.style.cssText = 'display: none;'; -document.body.appendChild(container); - -var subscribe = document.createElement('span'); -subscribe.innerHTML = 'NCODE登録'; -subscribe.className = 'manager-button'; -subscribe.addEventListener('click', (event) => { - if (bookmark[novelist.myncode]) { - myFancyPopup(novelist.myncode, bookmark[novelist.myncode].title, 'は既に書庫に登録しています!'); - } - else if (novelist.myncode === novelist.ncode) { - subscribeNcode(novelist.ncode, novelist.title); - } - else { - validateNcode(novelist.myncode); - } -}); -container.appendChild(subscribe); -// NCODE検証&登録 -function validateNcode(ncode) { - if (validate[ncode] === '検証中') { - return myFancyPopup('', 'Nコード' + ncode, 'は既に検証しています、何度もクリックしないでください!'); - } - if (validate[ncode]) { - return validNcodeResponse(ncode, validate[ncode]); - } - validate[ncode] = '検証中'; - myFancyLog('', 'Nコード' + ncode, 'を検証しています、しばらくお待ちください!', true) - GM_xmlhttpRequest({ - url: 'https://ncode.syosetu.com/' + ncode, - method: 'GET', - onload: (details) => { - validate[ncode] = details.responseXML.title; - validNcodeResponse(ncode, validate[ncode]); - } - }); -} -function validNcodeResponse(ncode, title) { - if (title === 'エラー') { - myFancyLog('', 'Nコード' + ncode, 'は存在しないか既にサーバーから削除されています!', true); - } - else { - subscribeNcode(ncode, title); - } -} -function subscribeNcode(ncode, title) { - bookmark[ncode] = { - title: title, - last: 0, - next: 3 - } - GM_setValue('bookmark', bookmark); - fancyTableItem(ncode, bookmark[ncode]); - myFancyLog(ncode, title, 'は書庫に登録しました!', true); -} - -var ncodeBox = document.createElement('input'); -ncodeBox.style.cssText = 'padding: 6px; font-size: 16px;'; -ncodeBox.addEventListener('change', (event) => { novelist.myncode = event.target.value || novelist.ncode; }); -container.appendChild(ncodeBox); - -var update = document.createElement('span'); -update.innerHTML = 'NCODE一括更新'; -update.className = 'manager-button'; -update.addEventListener('click', bookmarkSyncPreHandler); -container.appendChild(update); - -var openlog = document.createElement('span'); -openlog.innerHTML = 'ログ表示'; -openlog.className = 'manager-button'; -openlog.addEventListener('click', (event) => { - if (openlog.classList.contains('manager-checked')) { - fancylog.style.display = 'none'; - shelf.style.display = 'block'; - } - else { - fancylog.style.display = 'block'; - shelf.style.display = 'none'; - } - openlog.classList.toggle('manager-checked'); -}); -container.appendChild(openlog); - -var fancylog = document.createElement('div'); -fancylog.className = 'manager-logs'; -fancylog.style.cssText = 'display: none;'; -container.appendChild(fancylog); - -var shelf = document.createElement('div'); -shelf.className = 'manager-shelf'; -container.appendChild(shelf); - -var head = document.createElement('div'); -head.style.cssText = 'background-color: #000; color: #FFF;'; -head.innerHTML = 'NCODE小説タイトル更新間隔ダウンロード'; -shelf.appendChild(head); - -// ブックマーク表記生成 -Object.entries(bookmark).forEach(item => fancyTablePreHandler(...item)); -function fancyTablePreHandler(ncode, book) { - if (document.getElementById(ncode)) { - document.getElementById(ncode + '-next').value = book.next; - document.getElementById(ncode + '-update').innerHTML = book.last; - } - else { - fancyTableItem(ncode, book) - } -} -function fancyTableItem(ncode, book) { - var box = makeFancyItem(shelf, {'tag': 'div', 'attr': {'id': ncode}}); - makeFancyItem(box, {'tag': 'span', 'title': 'NCODEを書庫から削除します', 'html': ncode, 'click': (event) => removeNcodeFromLibrary(ncode, book.title)}); - makeFancyItem(box, {'tag': 'span', 'title': '小説のウェブページを開きます', 'html': book.title, 'click': (event) => openNovelPage(ncode, book.title)}); - var item3 = makeFancyItem(box, {'tag': 'span', 'title': '更新間隔を' + book.next + '日に設定します'}); - makeFancyItem(item3, {'tag': 'input', 'change': (event) => updatePeriodHandler(event, item3, ncode, book.title), 'attr': {'id': ncode + '-next', 'value': book.next}}); - makeFancyItem(box, {'tag': 'span', 'title': '縦書きPDFの更新をチェックします', 'html': new Date(book.last), 'click': (event) => updateTategakiPDF(ncode, book.title), 'attr': {'id': ncode + '-update'}}); -} -function makeFancyItem(box, props) { - var tag = document.createElement(props.tag); - if (props.title) { - tag.title = props.title; - } - if (props.html) { - tag.innerHTML = props.html; - } - if (props.click) { - tag.addEventListener('click', props.click); - } - if (props.change) { - tag.addEventListener('change', props.change); - } - if (props.attr) { - Object.entries(props.attr).forEach(item => { tag[item[0]] = item[1] }); - } - box.appendChild(tag); - return tag; -} -// ブックマーク事件処理 -function removeNcodeFromLibrary(ncode, title) { - if (confirm('【 ' + title + ' 】を書庫から削除しますか?')) { - document.getElementById(ncode).remove(); - delete bookmark[ncode]; - GM_setValue('bookmark', bookmark); - myFancyLog(ncode, title, 'は書庫から削除しました!', true); - } -} -function openNovelPage(ncode, title) { - if (confirm('小説【 ' + title + ' 】を開きますか?')) { - open('https://ncode.syosetu.com/' + ncode + '/', '_blank') - } -} -function updatePeriodHandler(event, node, ncode, title) { - var value = event.target.value.match(/\d+/)[0]; - node.title = '更新間隔を' + value + '日に設定します'; - bookmark[ncode].next = value; - GM_setValue('bookmark', bookmark); - myFancyPopup(ncode, title, 'は ' + value + ' 日置きに更新します!'); -} -function updateTategakiPDF(ncode, title) { - if (confirm(title + ' をダウンロードしますか?')) { - updateObserver(1); - batchDownloadPreHandler(ncode, title); - } -} - -// PDF自動更新関連 -if (novelist.now >= scheduler) { - bookmarkSyncPreHandler(); -} -function bookmarkSyncPreHandler() { - if (worker) { - return myFancyPopup('', '', '更新スケジュールを処理しています、何度もクリックしないでください!'); - } - worker = true; - myFancyPopup('', '登録した全てのNコード', 'を更新しています!'); - updateObserver(Object.keys(bookmark).length, () => { - worker = false; - GM_setValue('scheduler', Date.now() + 14400000); - myFancyPopup('', '登録した全てのNコード', 'の更新が完了しました!'); - }); - Object.keys(bookmark).forEach(item => updateFancyBookmark(item)); -} -function updateObserver(worker, callback) { - var observer = setInterval(() => { - if (Object.keys(session).length === worker) { - if (typeof callback === 'function') { - callback(); - } - session = {}; - GM_setValue('bookmark', bookmark); - clearInterval(observer); - } - }, 1000); -} -function updateFancyBookmark(ncode) { - // パッチ - delete bookmark[ncode].hash; - // - var next = bookmark[ncode].next - (novelist.now - bookmark[ncode].last) / 86400000; - if (next > 0) { - next = next >= 1 ? (next | 0) + '日' : (next * 24 | 0) + '時間'; - myFancyLog(ncode, bookmark[ncode].title, 'は ' + next + ' 後に更新する予定です!'); - session[ncode] = 'のち更新'; - } - else { - batchDownloadPreHandler(ncode); - } -} -function batchDownloadPreHandler(ncode) { - if (download[ncode] === 'ダウンロード') { - return myFancyPopup(ncode, bookmark[ncode].title, 'はまだ処理しています、しばらくお待ちください!'); - } - download[ncode] = 'ダウンロード'; - myFancyLog(ncode, bookmark[ncode].title, 'のダウンロードを開始しました!', true); - GM_xmlhttpRequest({ - url: 'https://pdfnovels.net/' + ncode + '/main.pdf', - method: 'GET', - responseType: 'blob', - onload: (details) => { - if (details.response.type === 'application/pdf') { - var a = document.createElement('a'); - a.href = URL.createObjectURL(details.response); - a.download = bookmark[ncode].title; - a.click(); - bookmark[ncode].last = novelist.now; - document.getElementById(ncode + '-update').innerHTML = new Date(novelist.now); - myFancyLog(ncode, bookmark[ncode].title, 'のダウンロードは完了しました!', true); - delete download[ncode]; - session[ncode] = '完了'; - } - else { - downloadPDFHandler(ncode); - } - } - }); -} -function downloadPDFHandler(ncode) { - myFancyPopup(ncode, bookmark[ncode].title, 'の縦書きPDFは只今生成中です、 60秒後に再試行します!'); - var timer = 60; - var retry = setInterval(() => { - timer.html --; - if (timer === 0) { - delete download[ncode]; - batchDownloadPreHandler(ncode); - clearInterval(retry); - } - }, 1000); -} - -// ログ関連 -function myFancyLog(ncode, title, result, popup) { - var html = myFancyNcode(ncode, title) + ' ' + result + ''; - var log = document.createElement('p'); - log.className = 'fancylog'; - log.innerHTML = html; - fancylog.prepend(log); - if (popup === true) { - myFancyPopup(ncode, title, result); - } -} -function myFancyPopup(ncode, title, result) { - var html = myFancyNcode(ncode, title) + ' ' + result + ''; - var popup = document.createElement('div'); - popup.innerHTML = html; - popup.className = 'notification fancylog manager-container'; - popup.style.height = 'fit-content'; - popup.addEventListener('click', (event) => removePopup(popup)); - document.body.appendChild(popup); - alignFancyPopup(); - setTimeout(() => removePopup(popup), 5000); -} -function myFancyNcode(ncode, title) { - if (ncode && title) { - return '「' + title + '」'; - } - if (!ncode && title) { - return '' + title + ''; - } - if (ncode && !title) { - return '' + ncode + ''; - } - return ''; -} -function alignFancyPopup() { - document.querySelectorAll('.notification').forEach((element, index) => { - element.style.top = (element.offsetHeight + 5) * index + 10 + 'px'; - element.style.left = (screen.availWidth - element.offsetWidth) / 2 + 'px'; - }); -} -function removePopup(popup) { - popup.remove() - alignFancyPopup(); -} diff --git a/TechPowerUp_Specs.user.js b/TechPowerUp_Specs.user.js new file mode 100644 index 0000000..daf08d8 --- /dev/null +++ b/TechPowerUp_Specs.user.js @@ -0,0 +1,190 @@ +// ==UserScript== +// @name TechPowerUp CPU/GPU规格中文化 +// @namespace https://github.com/jc3213/userscript +// @version 0.6 +// @description 将TechPowerUp关于CPU和GPU规格的相关网页进行中文化 +// @author jc3213 +// @match https://www.techpowerup.com/cpu-specs/* +// @match https://www.techpowerup.com/gpu-specs/* +// ==/UserScript== + +const locale = { +// 处理器 + 'Physical': '芯片信息', + 'Processor': '处理器', + 'Core Config': '核心配置', + 'Performance': '规格', + 'Architecture': '架构', + 'Cores': '核心', + 'Cache': '缓存', + 'Features': '功能集', + 'Notes': '其他', + 'Socket:': '插座:', + 'Foundry:': '代工工厂:', + 'Process Size:': '制程:', + 'Transistors:': '晶体管数量:', + 'Die Size:': '芯片面积:', + 'Density': '晶体管密度', + 'Package:': '封装:', + 'I/O Process Size:': 'I/O芯片制程:', + 'I/O Die Size:': 'I/O芯片面积:', + 'I/O Transistors:': 'I/O芯片晶体管:', + 'tCaseMax:': '平均温度:', + 'tJMax:': '最大温度:', + 'Frequency:': '频率:', + 'Turbo Clock:': '睿频:', + 'P-Core Turbo:': '性能核 睿频:', + 'P-Core Boost 3.0:': '性能核 睿频3.0:', + 'E-Core Frequency:': '能效核 频率:', + 'E-Core Turbo Clock:': '能效核 睿频:', + 'Base Clock:': '外频:', + 'Multiplier:': '倍频:', + 'Multiplier Unlocked:': '可超频:', + 'TDP:': '热设计功耗:', + 'PL1:': '功耗锁1:', + 'PL2:': '功耗锁2:', + 'PL2 Tau Limit:': '功耗锁2 上限:', + 'PL2 "Extreme": ': '功耗锁2 极致:', + 'Market:': '市场:', + 'Production Status:': '生产状态:', + 'Release Date:': '发布时间:', + 'Retail Launch:': '上市时间:', + 'Launch Price:': '首发定价:', + 'Launch MSRP:': '首发定价:', + 'Bundled Cooler:': '捆绑散热器:', + 'Codename:': '研发代号:', + 'Generation:': '产品世代:', + 'Part#:': '钢印编号:', + 'PPT:': '典型功耗:', + 'Memory Support:': '内存支持:', + 'DDR4 Speed:': 'DDR4 频率:', + 'DDR5 Speed:': 'DDR5 频率:', + 'Rated Speed:': '内存频率:', + 'Memory Bus:': '内存通道:', + 'Memory Bandwidth:': '内存带宽:', + 'Dual-channel': '双通道', + 'Eight-channel': '八通道', + 'ECC Memory:': '内存纠错:', + 'PCI-Express:': 'PCI-E 版本:', + 'Chipsets:': '芯片组:', + '# of Cores:': '核心数量:', + '# of Threads:': '线程数量:', + 'Hybrid Cores:': '异构核心:', + 'SMP # CPUs:': '处理器数目:', + 'Integrated Graphics:': '核显:', + 'Cache L0-D:': '零级数据缓存', + 'Cache L0-I:': '零级指令缓存', + 'Cache L1:': '一级缓存:', + 'Cache L2:': '二级缓存:', + 'Cache L3:': '三级缓存:', + 'E-Core L1:': '能效核 一缓:', + 'E-Core L2:': '能效核 二缓:', +// 图形卡 + 'Codename:': '研发代号', + 'Graphics Processor': '图形处理器', + 'Graphics Card': '发售信息', + 'Announced': '发布时间', + 'Relative Performance': '相对性能', + 'Clock Speeds': '频率', + 'Memory': '显存', + 'Render Config': '规格', + 'Theoretical Performance': '理论性能', + 'Board Design': '显示卡规格', + 'Graphics Features': '图形特性', + 'GPU Name': '芯片名称', + 'GPU Variant': '芯片变种', + 'Foundry': '代工工厂', + 'Process Size': '制程', + 'Transistors': '晶体管数量', + 'Die Size': '芯片面积', + 'MCD Process': 'MCD制程', + 'Process Type': '制程类型', + 'GCD Transistors': 'GCD晶体管数量', + 'MCD Transistors': 'MCD晶体管数量', + 'GCD Density': 'GCD晶体管密度', + 'MCD Density': 'MCD晶体管密度', + 'GCD Die Size': 'GCD芯片面积', + 'MCD Die Size': 'MCD芯片面积', + 'Chip Package': '芯片封装', + 'Release Date': '售卖时间', + 'Availability': '上市时间', + 'Generation': '产品线', + 'Predecessor': '上一代', + 'Successor': '下一代', + 'Production': '生产状态', + 'Launch Price': '首发价格', + 'Current Price': '现在价格', + 'Bus Interface': '接口界面', + 'Reviews': '评测', + 'GPU Clock': 'GPU频率', + 'Base Clock': '默认频率', + 'Shader Clock': '渲染器频率', + 'Game Clock': '游戏频率', + 'Boost Clock': '最大频率', + 'Memory Clock': '显存频率', + 'Memory Size': '显存容量', + 'Memory Type': '显存类型', + 'Memory Bus': '显存位宽', + 'Bandwidth': '显存带宽', + 'Shading Units': '通用渲染器', + 'TMUs': '纹理单元', + 'ROPs': '光栅化单元', + 'SM Count': 'SM数量', + 'Compute Units': 'CU数量', + 'Tensor Cores': 'Tensor核心', + 'RT Cores': 'RT核心', + 'L0 Cache': '零级缓存', + 'L1 Cache': '一级缓存', + 'L2 Cache': '二级缓存', + 'L3 Cache': '三级缓存', + 'Pixel Rate': '像素填充率', + 'Texture Rate': '纹理填充率', + 'FP16 (half)': '半精度(FP16)', + 'FP32 (float)': '单精度(FP32)', + 'FP64 (double)': '双精度(FP64)', + 'Slot Width': '卡槽', + 'Length': '长', + 'Width': '宽', + 'Height': '高', + 'TDP': '功耗', + 'Suggested PSU': '推荐电源', + 'Outputs': '输出端口', + 'Power Connectors': '电源接口', + 'Board Number': '主板编号', +}; + +const i18nMap = { + 'table': (table) => { + for (let tr of table.children[0].children) { + for (let el of tr.children) { + i18nElement(tr); + } + } + }, + 'div': (table) => { + for (let tr of table.children) { + i18nElement(tr); + } + } +}; + +setTimeout(() => { + const details = [...document.querySelectorAll('section')].slice(0, -1); + for (let detail of details) { + let [li, table] = detail.children; + i18nElement(li); + i18nMap[table.localName]?.(table); + } +}, 500); + +function i18nElement(tr) { + for (let el of tr.children) { + let text = el.innerText; + let i18n = locale[text]; + if (i18n) { + el.innerText = i18n; + } else { + console.log(text, i18n); + } + } +} diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..e3c5b0a --- /dev/null +++ b/_config.yml @@ -0,0 +1,5 @@ +markdown: GFM +theme: jekyll-theme-cayman +remote_theme: pages-themes/cayman@v0.2.0 +plugins: + - jekyll-remote-theme diff --git a/tsdmAutoSignAndWork.user.js b/tsdmAutoSignAndWork.user.js index 929ec3b..0423645 100644 --- a/tsdmAutoSignAndWork.user.js +++ b/tsdmAutoSignAndWork.user.js @@ -1,178 +1,102 @@ // ==UserScript== // @name 天使动漫自动签到打工 // @namespace https://github.com/jc3213/userscript -// @version 6 +// @version 1.5.1 // @description 天使动漫全自动打工签到脚本 — 完全自动无需任何操作,只需静待一分钟左右 // @author jc3213 -// @match *://www.tsdm39.net/* -// @grant GM_getValue -// @grant GM_setValue +// @match *://www.tsdm39.com/* // @noframes // ==/UserScript== -'use strict'; -var signed = GM_getValue('signed', 0); -var worked = GM_getValue('worked', 0); -var autoed = GM_getValue('autoed', false); -var today = [new Date().getYear(), new Date().getMonth(), new Date().getDate()].join(''); - -if (autoed) { - if (today > signed) { - autoHandler('/plugin.php?id=dsu_paulsign:sign', signHandler); +let signed = localStorage.getItem('signed') | 0; +let worked = localStorage.getItem('worked') | 0; +let today = new Date(); +let date = today.getFullYear() + today.getMonth() + today.getDate(); +let now = today.getTime(); +let action = {}; +let [e_work, e_sign] = document.querySelectorAll('#mn_Nfded_menu > li > a'); + +if (location.pathname === '/forum.php' && document.getElementById('tsdm_newpm')) { + if (date > signed) { + autoSign(); } - if (Date.now() > worked) { - autoHandler('/plugin.php?id=np_cliworkdz:work', workHandler); + if (now > worked) { + autoWork(); + } else { + setTimeout(autoWork, worked - now); } } -var css = document.createElement('style'); -css.innerHTML = '.my-button {padding: 5px; border: 1px outset #000; text-align: center; background-color: #FFF; width: 50px;}\ -.my-button:hover {filter: opacity(60%);}\ -.my-button:active {filter: opacity(30%);}\ -.my-checked {padding: 4px; border: 2px inset #00F;}\ -.my-dropdown {background-color: #FFF;}' -document.head.appendChild(css); - -var container = document.createElement('div'); -container.style.cssText = 'position: absolute; top: 3px; left: calc(50% - 50px); font-weight: bold; cursor: pointer;'; -document.body.appendChild(container); - -var menu = document.createElement('div'); -menu.innerHTML = '菜单'; -menu.className = 'my-button'; -menu.addEventListener('click', (event) => { - if (menu.classList.contains('my-checked')) { - box.style.display = 'none'; - } - else { - box.style.display = 'block'; - } - menu.classList.toggle('my-checked'); +e_work.addEventListener('click', event => { + event.preventDefault(); + autoWork(); }); -container.appendChild(menu); - -var box = document.createElement('div'); -box.className = 'my-dropdown'; -box.style.cssText = 'display: none;'; -container.appendChild(box); - -var sign = document.createElement('div'); -sign.innerHTML = '签到'; -sign.className = 'my-button'; -sign.addEventListener('click', (event) => autoHandler('/plugin.php?id=dsu_paulsign:sign', signHandler)); -box.appendChild(sign); -var work = document.createElement('div'); -work.innerHTML = '打工'; -work.className = 'my-button'; -work.addEventListener('click', (event) => autoHandler('/plugin.php?id=np_cliworkdz:work', workHandler)); -box.appendChild(work); - -var auto = document.createElement('div'); -auto.className = 'my-button'; -auto.addEventListener('click', (event) => { - autoed = !autoed; - autoIcon.innerHTML = autoed ? '✅' : ''; - GM_setValue('autoed', autoed); +e_sign.addEventListener('click', event => { + event.preventDefault(); + autoSign(); }); -box.appendChild(auto); -var autoIcon = document.createElement('span'); -autoIcon.innerHTML = autoed ? '✅' : ''; -var autoText = document.createTextNode('自动'); -auto.appendChild(autoIcon); -auto.appendChild(autoText); - -function autoHandler(url, load) { - var id = 'auto-' + url.match(/\w{4}$/)[0]; - if (document.getElementById(id)) { - return; - } - var warn = warnStart(id); - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.onload = () => { - var node = document.createElement('div'); - node.innerHTML = xhr.response; - load(node, url, id, warn); - }; - xhr.send() -} - -function signHandler(node, url, id, warn) { - if (node.querySelector('#ct_shell > div:nth-child(1) > h1:nth-child(1)')) { - GM_setValue('signed', today); - warnOver(id, node.querySelector('#ct_shell > div:nth-child(1) > h1:nth-child(1)').innerHTML); - } - else { - warn.innerHTML = '开始签到...'; - makeFrame(url, id, autoSign); - } -} -function workHandler(node, url, id, warn) { - if (node.querySelector('#messagetext')) { - var text = node.querySelector('#messagetext > p:nth-child(1)').innerHTML.split(/
|