您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Improves user experience on mangalib.me
// ==UserScript== // @name Emanon // @version 2.6.3 // @description Improves user experience on mangalib.me // @author abara // @match https://mangalib.me/* // @match https://yaoilib.me/* // @match https://ranobelib.me/* // @match https://hentailib.me/* // @match https://lib.social/* // @match https://animelib.me/* // @grant GM.xmlHttpRequest // @grant GM_addStyle // @connect coub.com // @connect catbox.moe // @icon https://mangalib.me/uploads/users/57692/5N9wWaulef.jpg // @namespace https://greasyfork.org/users/209098 // @license MIT // ==/UserScript== /** * Need refactoring. * Also, feel free to fork. */ (function () { 'use strict'; const YT_LINK_REGEX = /(?:https?:\/\/)?(?:m\.)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?/; const YT_API_KEY = 'AIzaSyAszkTb5uTRo7wbkPmyxD2t1a6K7hR7sVA'; const IMG_LINK_REGEX = /(?:https?:\/\/)i\.imgur\.com\/(\w+)\.([a-z0-9]{3})/; const IMG_CLIENT_ID = '23bfce77f9619b6'; const COUB_LINK_REGEX = /(?:https?:\/\/)coub\.com\/view\/(\w+)/; const CATBOX_LINK_REGEX = /https:\/\/files\.catbox\.moe\/(\w+)\.(webm|mp4)/; const _store = {}; // Bad.. const _default_config = { volume: 50, }; class Api { constructor() { this.nodeMutatons = []; this.messageMutations = []; } // ex. unsafeWindow.emanon.addNodeMutation(node => {//do something}); addNodeMutation(callback) { if (!callback) throw new Error('Invalid node mutaion!'); this.nodeMutatons.push(callback); } // ex. unsafeWindow.emanon.addMessageMutation(message => {//do something}); addMessageMutation(callback) { if (!callback) throw new Error('Invalid message mutaion!'); this.messageMutations.push(callback); } } const emanonApi = new Api(); // Insec.. unsafeWindow.emanon = emanonApi; class CacheItem { /** * @param {string} id * @param {string} title * @param {number} createdAt */ constructor(id, title, createdAt) { this.id = id; this.title = title; this.createdAt = createdAt; } } class Cache { /** * @param {string} id * @returns {CacheItem} */ static getItem(id) { const cache = Storage.getItem('cache'); if (cache.length) return cache.find(item => item.id == id); } /** * @param {string} id * @param {string} title */ static setItem(id, title) { const cache = Storage.getItem('cache'); cache.push(new CacheItem(id, title, Date.now())); Storage.setItem('cache', cache); } static truncateOld() { const cache = Storage.getItem('cache'); const truncated = cache.filter(item => item.createdAt >= (Date.now() - 60 * 1000 * 30)); // 30min Storage.setItem('cache', truncated); } } class User { /** * @param {string} id * @param {string} note * @param {boolean} blocked */ constructor(id, note, blocked) { this.id = id; this.note = note; this.blocked = blocked; } } class MessageCounter { constructor() { this.counter = 0; } incrementCounter() { this.counter += 1; } resetCounter() { this.counter = 0; } } class TitleCounter extends MessageCounter { constructor() { super(); this.title = document.title; } incrementCounter() { super.incrementCounter(); document.title = `(+${this.counter}) ${this.title}`; } resetCounter() { super.resetCounter(); document.title = this.title; } } class ChatMessageCounter extends MessageCounter { constructor() { super(); this.el = makeEl('span'); } incrementCounter() { super.incrementCounter(); this.el.innerText = `(+${this.counter})`; } resetCounter() { super.resetCounter(); this.el.innerText = ''; } get counterEl() { return this.el; } } class Storage { static fill() { for (let k of ['users', 'cache', 'config']) { _store[k] = Storage.getItem(k); } } static getItem(key) { if (_store[key]) return _store[key]; return JSON.parse(localStorage.getItem(`emanon.${key}`)) || []; } static setItem(key, val) { _store[key] = val; localStorage.setItem(`emanon.${key}`, JSON.stringify(_store[key] || [])); } } // Shortcuts const q = (sel, el) => (el || document).querySelector(sel); const qAll = (sel, el) => (el || document).querySelectorAll(sel); /** * @param {string} name * @returns {HTMLElement} */ const makeEl = name => document.createElement(name); /** * https://stackoverflow.com/a/7557433 * @param {Element} el * @returns {boolean} */ const isElementInViewport = function (el) { var rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); } /** * @param {string} url */ const createViewBox = function (url) { const view = makeEl('div'); view.classList.add('_emanon-viewbox'); let mediaEl = null; if (/\.(mp4|webm|gifv)/i.test(url)) { mediaEl = makeEl('video'); mediaEl.autoplay = true; mediaEl.loop = true; mediaEl.controls = true; mediaEl.addEventListener("volumechange", e => { const volume = Math.round(100 * e.target.volume); const configArr = Storage.getItem('config'); if (configArr[0]) { configArr[0].volume = volume; } else { _default_config.volume = volume; configArr.push(_default_config); } Storage.setItem('config', configArr); }); const configArr = Storage.getItem('config'); if (configArr[0]) { const config = configArr[0]; mediaEl.volume = 0 == config.volume ? 0 : config.volume / 100; } } else { mediaEl = makeEl('img'); } mediaEl.src = url.includes(".gifv") ? url.replace(".gifv", ".mp4") : url; view.addEventListener('click', _ => { if (mediaEl.tagName == 'VIDEO') { mediaEl.pause(); mediaEl.src = ''; } view.remove(); }); view.appendChild(mediaEl); q('body').appendChild(view); } /** * Ported from 4chan-x (MIT). * Thx, Kagami <3 */ const getMatroskaTitle = function (body) { const data = new Uint8Array(body); let i = 0, element = 0, size = 0, title = ''; const readInt = function () { let n = data[i++]; let len = 0; while (n < (0x80 >> len)) len++; n ^= (0x80 >> len); while (len-- && i < data.length) { n = (n << 8) ^ data[i++]; } return n; } while (i < data.length) { element = readInt(); size = readInt(); if (size < 0) break; // Title if (element === 0x3BA9) { while (size-- && i < data.length) { title += String.fromCharCode(data[i++]); } return decodeURIComponent(escape(title)); // UTF-8 decoding } else if (element !== 0x8538067 && // Segment element !== 0x549A966) { // Info i += size; } } return ''; } /** * @param {string} userId * @returns {User} */ const getUserById = userId => Storage.getItem('users').find(u => u.id == userId); /** * @param {string} userId * @returns {boolean} */ const checkIfBlocked = function (userId) { const user = getUserById(userId); return user && user.blocked; } /** * @param {string} userId * @returns {string} */ const getNoteByUserId = function (userId) { const user = getUserById(userId); if (user && user.note.length) return user.note; return null; } const getChatWrap = () => _CHAT_INSTANCE.$children[0]; const getChatInstance = function () { const chatWrap = getChatWrap(); return chatWrap.$children[1] ? chatWrap.$children[1] : chatWrap.$children[0]; } /** * @param {Element} message * @returns {void} */ const mutateMessage = function (message) { // Guard against null if (message == null) throw new Error('Message cannot be a null!'); const userName = getChatInstance().store.auth.username; // Get all refs from message qAll('span[title]', message).forEach(ref => { if (ref.innerText.replace('@', '') == userName) { message.classList.add('_emanon-ref'); } }); const exceedThumbLimit = () => qAll('a._emanon-image-thumbnail', message).length > 3 qAll('a', message).forEach(link => { let match; switch (link.hostname) { case 'i.imgur.com': match = link.href.match(IMG_LINK_REGEX); if (match) { if (exceedThumbLimit()) break; link.classList.add('_emanon-image-thumbnail'); link.innerHTML = `<img src="https://i.imgur.com/${match[1]}t.${match[2]}" />`; } break; case 'files.catbox.moe': match = link.href.match(CATBOX_LINK_REGEX); if (match) { if (exceedThumbLimit()) break; const [_, id, ext] = match; link.classList.add('_emanon-image-thumbnail'); link.innerHTML = `<video src="${link.href}" preload="metadata"></video>` // Make play button const play = makeEl('i'); play.classList.add('fa', 'fa-play-circle', 'play'); link.appendChild(play); // Make video overlay const overlay = makeEl('div'); overlay.classList.add('play-overlay'); link.appendChild(overlay); // Load webm title if (ext == 'webm') { const item = Cache.getItem(id); if (item) { link.setAttribute('title', item.title); return; } // Not fancy, but ok GM.xmlHttpRequest({ method: 'GET', url: link.href, responseType: 'arraybuffer', headers: { Range: 'bytes=0-1023', }, onload: res => { if (res.status >= 200 && res.status < 400) { const title = getMatroskaTitle(res.response); if (title) { Cache.setItem(id, title); link.setAttribute('title', title); } } }, onerror: res => console.error(res.responseText) }); } } break; case 'm.youtube.com': case 'youtu.be': case 'youtube.com': case 'www.youtu.be': case 'www.youtube.com': match = link.href.match(YT_LINK_REGEX); if (match) { const id = match[1]; const linkTmp = title => `<i class="fa fa-youtube"></i> Youtube: ${title}`; const item = Cache.getItem(id); if (item) { link.innerHTML = linkTmp(item.title); return; } fetch(`https://www.googleapis.com/youtube/v3/videos?key=${YT_API_KEY}&part=snippet&id=${id}`) .then(res => res.json()) .then(body => { if (body.items.length) { const title = body.items[0].snippet.title; Cache.setItem(id, title); link.innerHTML = linkTmp(title); } }) .catch(err => console.error(err)); } break; case 'coub.com': case 'www.coub.com': match = link.href.match(COUB_LINK_REGEX); if (match) { const id = match[1]; const linkTmp = title => `<i class="fa fa-play-circle"></i> Coub: ${title}`; const item = Cache.getItem(id); if (item) { link.innerHTML = linkTmp(item.title); return; } GM.xmlHttpRequest({ method: 'GET', url: `https://coub.com/api/v2/coubs/${id}`, onload: res => { if (res.status == 200) { const body = JSON.parse(res.responseText); Cache.setItem(id, body.title); link.innerHTML = linkTmp(body.title); } }, onerror: res => console.error(res.responseText) }); } break; } }); // Apply mutations emanonApi.messageMutations.forEach(m => m(message)); } const mutateNode = function (node) { console.log("New node added!"); const avaWrap = q('.chat-item__ava-wrap', node); const userId = q('.chat-item__avatar', avaWrap) .getAttribute('src').split('/')[3]; const userNameEl = q('.chat-item__username', node); userNameEl.setAttribute('title', userNameEl.innerText); // Wrap username const userNameWrap = makeEl('div'); userNameWrap.classList.add("_emanon-username-wrap"); userNameWrap.appendChild(userNameEl.cloneNode(true)); userNameEl.remove(); q('.chat-item__header-content', node).prepend(userNameWrap); // Add hide icon const dateEl = q('.chat-item__date', node); const hideEl = makeEl('span'); hideEl.classList.add('fa', 'fa-eye-slash', 'tooltip', '_emanon-hide-button'); hideEl.setAttribute('aria-label', 'Скрывать сообщения от этого пользователя'); hideEl.setAttribute('data-place', 'bottom-end'); hideEl.addEventListener('click', _ => toogleHidden()); dateEl.parentNode.insertBefore(hideEl, dateEl); if (checkIfBlocked(userId)) { node.prepend(makeHiddenBlock()); } function makeHiddenBlock() { const aHiddenLink = makeEl('div'); aHiddenLink.innerText = 'Сообщение скрыто. Раскрыть>>'; aHiddenLink.classList.add('_emanon-hidden-info'); aHiddenLink.addEventListener('click', _ => toogleHidden()); return aHiddenLink; } function toogleHidden() { const hiddenLink = q('._emanon-hidden-info', node); const blocked = hiddenLink != null; if (blocked) { hiddenLink.remove(); } else { node.prepend(makeHiddenBlock()); } const users = Storage.getItem('users'); const index = users.findIndex(u => u.id == userId); // Save to storage if (index != -1) users[index].blocked = !blocked; else users.push(new User(userId, '', !blocked)); Storage.setItem('users', users); } // Add note tooltip const note = getNoteByUserId(userId); if (note) { avaWrap.setAttribute('aria-label', note); avaWrap.setAttribute('data-place', 'bottom-start'); avaWrap.classList.add('tooltip', '_emanon-note-tooltip'); } // Apply mutations emanonApi.nodeMutatons.forEach(m => m(node)); mutateMessage(q('.chat-item__message', node)); } // Let's play GM_addStyle("[contentEditable=true]:empty:not(:focus):before{ content:attr(data-placeholder); color: #999; }"); GM_addStyle("._emanon-file-input { position: absolute; left: 0; top: 0; width: 40px; height: 40px; opacity: 0; overflow: hidden; z-index: 1; }"); GM_addStyle("._emanon-icon_container { position: absolute; left: 0; top: 0; width: 40px; height: 40px; text-align: center; font-size: 20px; color: #818181; border: 0; background: none; }"); GM_addStyle("._emanon-image-thumbnail { display: inline-flex; justify-content: center; align-items: center; vertical-align: top; border: 1px solid #b1b1b1 !important; margin: 1px; line-height: 0; font-size: 0; position: relative; text-decoration: none; }"); GM_addStyle("._emanon-image-thumbnail:first-child { margin-left: 0; }"); GM_addStyle("._emanon-image-thumbnail:last-child:not(:only-child) { margin-right: 0; }"); GM_addStyle("._emanon-image-thumbnail > img, ._emanon-image-thumbnail > video { max-height: 160px; max-width: 160px; }"); GM_addStyle("._emanon-image-thumbnail .play { position: absolute; font-size: 30px; color: #f77519; z-index: 1; }"); GM_addStyle("._emanon-image-thumbnail .play-overlay { position: absolute; width: 100%; height: 100%; background: #00000096; z-index: 0; }"); GM_addStyle("._emanon-viewbox { position: fixed; background: rgba(0,0,0,0.9); z-index: 9999; width: 100%; height: 100%; top: 0; left: 0; display: flex; justify-content: center; align-items: center; }"); GM_addStyle("._emanon-viewbox > * { width: auto; height: auto; max-width: 99%; max-height: 99%; padding: 0; margin: 0; }"); GM_addStyle("._emanon-note-tooltip:after { width: 240px; white-space: normal; word-wrap: break-word; padding: 8px; height: initial; z-index: 1; }"); GM_addStyle("._emanon-hidden-info { text-align: center; color: #cacaca; font-size: 12px; cursor: pointer; }"); GM_addStyle("._emanon-hidden-info ~ * { display: none; }"); GM_addStyle("._emanon-hide-button { position: absolute; right: 0; color: #ccc; cursor: pointer; }"); GM_addStyle("._emanon-hide-button:hover { color: #868e96; }"); GM_addStyle("._emanon-unread-item { background: #ffecec; }"); GM_addStyle(".chat-item__header { position: relative; }"); // Fix for hide button GM_addStyle(".chat-send > .comment-reply__editor { background: initial !important; min-height: initial; border: none !important; }"); GM_addStyle("._emanon-username-wrap { width: 90%; display: inline-block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }") // Fix long nicknames GM_addStyle("._emanon-rotate { transform: rotate(+180deg); transition-duration: 0.5s; }"); GM_addStyle("._emanon-ref { color: #ff4db6 !important; }"); // Init storage Storage.fill(); Cache.truncateOld(); // Append users notes block const profileInfo = q('.profile-info'); if (profileInfo) { const userId = window.location.pathname.match(/\d+/)[0]; const panel = makeEl('div'); panel.classList.add('aside__panel', 'paper'); const title = makeEl('h3'); title.classList.add('aside__title'); const titleText = makeEl('span'); titleText.classList.add('aside__title-inner'); titleText.innerText = 'Заметки о пользователе'; title.appendChild(titleText); const panelBody = makeEl('div'); panelBody.classList.add('aside__content'); panelBody.contentEditable = true; panelBody.setAttribute('data-placeholder', 'Здесь можно оставить комментарий о пользователе..'); // Get note from storage const note = getNoteByUserId(userId); if (note) panelBody.innerText = note; panelBody.addEventListener('input', e => { const target = e.target; const users = Storage.getItem('users'); const index = users.findIndex(u => u.id == userId); // Update note if (index != -1) { users[index].note = target.innerText } // Add new user else { users.push(new User(userId, target.innerText, false)); } Storage.setItem('users', users); }); panel.appendChild(title); panel.appendChild(panelBody); profileInfo.appendChild(panel); } // Ugly hack const chatInitInterval = setInterval(() => { console.log('Try to init chat'); if (_CHAT_INSTANCE && _CHAT_INSTANCE._isMounted) { clearInterval(chatInitInterval); console.log('Chat init'); // Присасывамся ( ͡° ͜ʖ ͡°) const CHAT_WRAP = getChatWrap(); const CHAT_INSTANCE = getChatInstance(); const chat = q('.chat'); // Add File uploading const sendEl = q('.chat-send', chat); const fileInput = makeEl('input'); fileInput.type = 'file'; fileInput.accept = 'image/*,video/webm,video/mp4'; fileInput.classList.add('_emanon-file-input'); const iconCont = makeEl('button'); iconCont.classList.add('_emanon-icon_container'); const uploadIcon = makeEl('i'); uploadIcon.classList.add('fa', 'fa-upload'); fileInput.addEventListener('change', e => { const files = e.target.files; if (files.length > 0) { const file = files[0]; const sendBtn = q('.chat-send__button', sendEl); // Change icon uploadIcon.classList.remove('fa-upload'); uploadIcon.classList.add('fa-spinner', 'fa-spin'); sendBtn.disabled = true; fileInput.disabled = true; const resetUpload = function () { uploadIcon.classList.add('fa-upload'); uploadIcon.classList.remove('fa-spinner', 'fa-spin'); sendBtn.disabled = false; fileInput.value = ""; fileInput.disabled = false; } const appendLink = function (link) { let text = CHAT_INSTANCE.store.text; if (text) text = text + '\r\n'; CHAT_INSTANCE.store.text = text + link; console.log(`Image link ${link}`); } if (file.type.includes('video')) { const formData = new FormData(); formData.append('reqtype', 'fileupload'); formData.append('userhash', ''); formData.append('fileToUpload', file); GM.xmlHttpRequest({ method: "POST", url: 'https://catbox.moe/user/api.php', data: formData, onload: res => { if (res.status == 200) { appendLink(res.responseText); } }, onerror: res => console.error(res.responseText), onreadystatechange: xhr => { if (xhr.readyState === XMLHttpRequest.DONE) { resetUpload(); } } }); } else { const formData = new FormData(); formData.append('image', file); const headers = new Headers(); headers.append('Authorization', `Client-ID ${IMG_CLIENT_ID}`); fetch('https://api.imgur.com/3/image', { method: 'POST', body: formData, headers: headers, }) .then(res => res.json()) .then(body => { if (body.success) { appendLink(body.data.link); } }) .catch(err => console.error(err)) .finally(() => resetUpload()); } } }); iconCont.appendChild(uploadIcon); iconCont.appendChild(fileInput); // Fix padding const textArea = q('.chat-send__area', sendEl); textArea.style.paddingLeft = '45px'; sendEl.prepend(iconCont); // Add check messages action if (CHAT_WRAP.settings.inWindow) { const checkButton = makeEl('i'); checkButton.classList.add('fa', 'fa-fw', 'fa-refresh'); checkButton.addEventListener('click', () => { checkButton.classList.remove('_emanon-rotate'); setTimeout(() => checkButton.classList.add('_emanon-rotate'), 0); CHAT_INSTANCE.checkMessages(); }); const actionWrap = makeEl('div'); actionWrap.classList.add('chat-wrap__action'); actionWrap.appendChild(checkButton); q('.chat-wrap__actions').prepend(actionWrap); } // Working with chat items const chatItems = q('.chat__items', chat); // Mutate chat items qAll('.chat-item', chatItems).forEach(item => mutateNode(item)); chatItems.addEventListener('click', e => { // Add viewbox const target = e.target; if (target.closest('._emanon-image-thumbnail')) { e.preventDefault(); createViewBox(target.parentElement.href); } // Remove unread mark qAll('._emanon-unread-item', chatItems) .forEach(item => item.classList.remove('_emanon-unread-item')); }); // Add counters const titleCounter = new TitleCounter(); const resetTitle = function () { if (isElementInViewport(chatItems)) { titleCounter.resetCounter(); } } addEventListener('click', _ => resetTitle()); addEventListener('focus', _ => resetTitle()); addEventListener('scroll', _ => resetTitle()); const chatCounter = new ChatMessageCounter(); const chatWrap = q('.chat-wrap__title'); if (chatWrap) { chatWrap.appendChild(chatCounter.counterEl); // Yep, use timeout chatWrap.addEventListener('click', _ => setTimeout(() => { if (CHAT_WRAP.isOpen) { chatCounter.resetCounter(); } }, 100)); } let initCounters = true; // HAHAHAH // Add chat observer const chatObserver = new MutationObserver(mutationsList => { mutationsList.forEach(mutation => mutation.addedNodes.forEach(node => { if (node.tagName == 'DIV' && node.classList.contains('chat-item')) { // Increment counters if (!initCounters) { if (document.hidden) titleCounter.incrementCounter(); if (!CHAT_WRAP.isOpen) chatCounter.incrementCounter(); // Mark as unread if (document.hidden || chatWrap && !CHAT_WRAP.isOpen) { node.classList.add('_emanon-unread-item'); } } mutateNode(node); } }) ) initCounters = false; }); chatObserver.observe(chatItems, { childList: true, subtree: false, attributes: false }); // Add message preview observer const previewObserver = new MutationObserver(mutationsList => mutationsList.forEach(mutation => mutation.addedNodes.forEach(node => { if (node.classList.contains('tippy-popper') && // Not already mutated !node.classList.contains('_emanon-mutated')) { const chatPvInterval = setInterval(() => { if (node.getAttribute('data-init') == 'true') { clearInterval(chatPvInterval); node.classList.add('_emanon-mutated'); mutateMessage(q('.message-preview__content', node)); } }, 50); } } ) )); previewObserver.observe(q('body'), { childList: true, subtree: false, attributes: false }); } }, 50); })();