NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Reddikabu // @namespace https://reddit.com/ // @version 1.0 // @author Efog // @match https://www.reddit.com/* // @match https://reddit.com/* // @match http://www.reddit.com/* // @match http://reddit.com/* // @license MIT // @grant none // ==/UserScript== (function() { 'use strict'; class Reddikabu { constructor() { this.auth = null; this.visibleCardTime = 0; this.applyPushStateHook(); this.applyKeyboardHook(); this.applyXHRHook(); this.applyScrollHook(); this.convertLinks(); Reddikabu.log('Initialization successfull!'); } applyScrollHook() { const SCROLL_THRESHOLD = 100; let timeoutId = null; setTimeout(() => { this.handleScroll(); }); window.addEventListener('scroll', () => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { this.handleScroll(); }, SCROLL_THRESHOLD); }); } applyXHRHook() { const _open = XMLHttpRequest.prototype.open; const _setRequestHeader = XMLHttpRequest.prototype.setRequestHeader; const _this = this; XMLHttpRequest.prototype.setRequestHeader = function (...args) { const [name, content] = args; if (name === 'Authorization' && content) { _this.auth = content; } return _setRequestHeader.apply(this, args); }; XMLHttpRequest.prototype.open = function (...args) { this.addEventListener('load', () => { _this.refresh(...args); }); return _open.apply(this, args); }; } applyPushStateHook() { const _pushState = history.pushState; const _this = this; history.pushState = (function (...args) { _this.refresh(...args); return _pushState.apply(this, args); }); } applyKeyboardHook() { document.body.addEventListener('keydown', event => { if (Reddikabu.isInputElement(event.target)) return; if (this.makeAction(event.key, event)) { event.preventDefault(); event.stopPropagation(); return false; } }); } handleScroll() { const HIDE_THRESHOLD = 400; const currentCard = Reddikabu.getTopVisibleCard(); Reddikabu.logSeparator(); Reddikabu.log('Current card:', currentCard); Reddikabu.log('Previous card:', this.visibleCard); Reddikabu.log('Time between:', Date.now() - this.visibleCardTime); if (this.visibleCard && (Date.now() - this.visibleCardTime > HIDE_THRESHOLD) && currentCard !== this.visibleCard) { try { Reddikabu.log(`Hiding post ID#${this.visibleCard.id}.`); this.hidePost(this.visibleCard.id); } catch (e) { Reddikabu.log('Hiding error!', e); } } this.visibleCard = currentCard; this.visibleCardTime = Date.now(); } hidePost(postId) { if (!this.auth) return; const xhr = new XMLHttpRequest(); xhr.open('POST', 'https://oauth.reddit.com/api/hide?redditWebClient=web2x&app=web2x-client-production&raw_json=1&gilding_detail=1'); xhr.setRequestHeader('Authorization', this.auth); xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded'); xhr.send(`id=${postId}`); } convertLinks() { Array.from(document.querySelectorAll('[data-test-id=comment] p > a:first-of-type')) .forEach(link => { try { const [, extension] = link.href.match(/^.*\.([a-z0-9]+)$/); const isHttps = link.href.startsWith('https://'); const isYoutube = /^https:\/\/www\.youtube\.com\/watch\?v\=([a-z0-9]+?)$/.test(link.href); const isImage = extension === 'jpg' || extension === 'jpeg' || extension === 'png' || extension === 'gif'; if (!isHttps) return; if (isImage) { Reddikabu.replaceLinkByImage(link); } else if (isYoutube) { Reddikabu.replaceLinkByYoutube(link); } } catch (e) {} }); } static replaceLinkByImage(link) { const image = document.createElement('img'); image.setAttribute('src', link); image.setAttribute('style', ` max-width: 60%; display: block; `); link.outerHTML = image.outerHTML; } static replaceLinkByYoutube(link) { try { const iFrame = document.createElement('iframe'); iFrame.setAttribute('src', `https://www.youtube.com/embed/${link.href.split('?v=')[1]}`); iFrame.setAttribute('width', '560'); iFrame.setAttribute('height', '315'); iFrame.setAttribute('frameborder', '0'); iFrame.setAttribute('allow', 'encrypted-media'); iFrame.setAttribute('allowfullcreen', ''); link.outerHTML = iFrame.outerHTML; } catch (e) {} } makeAction(key, event) { if ((key === 'c' || key === Reddikabu.getRussianCharacter('c')) && event.ctrlKey) return false; const SCROLL_FOR = 200; const currentCard = Reddikabu.getTopVisibleCard(); const cardActionCharacters = ['w', 'a', 's', 'd', 'e']; const russianCardActionCharacters = cardActionCharacters.map(Reddikabu.getRussianCharacter); const allCardActionsCharacters = [...cardActionCharacters, ...russianCardActionCharacters]; if (allCardActionsCharacters.includes(key) && !currentCard) return false; switch (key) { case 'w': case Reddikabu.getRussianCharacter('w'): currentCard.querySelector('[data-click-id=upvote]').click(); break; case 's': case Reddikabu.getRussianCharacter('s'): currentCard.querySelector('[data-click-id=downvote]').click(); break; case 'a': case Reddikabu.getRussianCharacter('a'): Reddikabu.scrollToCard(Reddikabu.getCardSibling(currentCard, -1)); break; case 'd': case Reddikabu.getRussianCharacter('d'): Reddikabu.scrollToCard(Reddikabu.getCardSibling(currentCard)); break; case 'e': case Reddikabu.getRussianCharacter('e'): Reddikabu.openPost(currentCard); break; case '`': case Reddikabu.getRussianCharacter('`'): window.close(); break; case 'z': case Reddikabu.getRussianCharacter('z'): Reddikabu.setScrollTop(Reddikabu.getScrollTop() - SCROLL_FOR); break; case 'c': case Reddikabu.getRussianCharacter('c'): Reddikabu.setScrollTop(Reddikabu.getScrollTop() + SCROLL_FOR); break; default: return false; } return true; } static getRussianCharacter(character) { const mapping = { 'w': 'ц', 's': 'ы', 'a': 'ф', 'd': 'в', 'e': 'у', '`': 'ё', 'z': 'я', 'c': 'с', }; return mapping[character] || null; } static openPost(card) { const link = card.querySelector('a[data-click-id=body]'); const url = link.href; window.open(url, '_blank'); } static getScrollTop() { return document.scrollingElement.scrollTop; } static setScrollTop(y) { window.scroll({top: y, behavior: 'smooth' }); } static scrollToCard(card) { const magicMargin = 10; if (!card) return; Reddikabu.setScrollTop(Reddikabu.getScrollTop() + card.getClientRects()[0].y - Reddikabu.getHeader().clientHeight - magicMargin); } static getTopVisibleCard() { return Array.from(document.querySelectorAll('.Post.scrollerItem')) .find(post => { const rect = post.getClientRects(); if (!rect || !rect[0]) return; return rect[0].y > Reddikabu.getHeader().clientHeight; }); } static getCardSibling(card, direction = 1) { const fn = direction === 1 ? 'nextSibling' : 'previousSibling'; const sibling = card.parentNode.parentNode[fn]; if (!sibling) return null; const isBlank = sibling.querySelector('.scrollerItem.Blank'); if (isBlank) { return Reddikabu.getCardSibling(isBlank, direction); } else { return sibling.querySelector('.Post.scrollerItem'); } } static getHeader() { return document.querySelector('header'); } static isInputElement(element) { return element.isContentEditable || element.tagName.toLowerCase() === 'input' || element.tagName.toLowerCase() === 'textarea'; } refresh(...args) { setTimeout(() => { this.convertLinks(); }); } static logSeparator() { console.log('\n'); } static log(...messages) { const [message, ...other] = messages; console.log(`%c[Reddikabu] %c[${new Date().toLocaleTimeString()}] %c${message}`, 'color: red;', 'color: blue;', 'color: gray;', ...other); } } const reddikabu = new Reddikabu(); })();