// ==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);
})();