Pikabu Enhancement Suite

Новый новый дизайн пикабу. Не забудь установить юзерстиль.

// ==UserScript==
// @name         Pikabu Enhancement Suite
// @version      1.4.8
// @license      CC-BY-SA-4.0
// @description  Новый новый дизайн пикабу. Не забудь установить юзерстиль.
// @author       NeverLoved
// @match        https://pikabu.ru/*
// @grant        none
// @namespace https://greasyfork.org/users/175168
// ==/UserScript==

(function() {
    'use strict';

    // Source: https://github.com/jserz/js_piece/blob/master/DOM/ParentNode/prepend()/prepend().md
    (function (arr) {
        arr.forEach(function (item) {
            if (item.hasOwnProperty('prepend')) {
                return;
            }
            Object.defineProperty(item, 'prepend', {
                configurable: true,
                enumerable: true,
                writable: true,
                value: function prepend() {
                    var argArr = Array.prototype.slice.call(arguments),
                        docFrag = document.createDocumentFragment();

                    argArr.forEach(function (argItem) {
                        var isNode = argItem instanceof Node;
                        docFrag.appendChild(isNode ? argItem : document.createTextNode(String(argItem)));
                    });

                    this.insertBefore(docFrag, this.firstChild);
                }
            });
        });
    })([Element.prototype, Document.prototype, DocumentFragment.prototype]);

    let _c;
    var defconfig = {
        "top_big_searchbar":false,
        "top_no_avatar":true,
        "porn_stealth":true,
        "restore_f":true,
        "great_comment_links":true,
        "post_header_collapse":true,
        "sidebar_icons":true,
        "sidebar_feed_link":false,
        "enable_nano_nav":true,
        "oldschool_favicon": false,

    };
    try {
        _c = JSON.parse(window.localStorage.getItem('enhancements'));
        if(!Object.keys(_c).length) {
            throw new Error('Empty');
        }
    } catch(e) {
        console.log('Config is empty, using defconfig');
        _c = defconfig;
    }

    var config = {
        values:_c,
        set:function(param, value) {
            this.values[param] = value;
            window.localStorage.setItem('enhancements', JSON.stringify(this.values));
        },
        get:function(param) {
            return this.values[param];
        }
    };

    function findParentByTag(el, tag) {
        while (el.parentNode) {
            el = el.parentNode;
            if (el.tagName === tag)
                return el;
        }
        return null;
    }

    function nodeIndex(node) {
        var children = node.parentNode.children;
        var num = 0;
        for (var i=0; i<children.length; i++) {
            if (children[i]==node) return num;
            if (children[i].nodeType==1) num++;
        }
        return -1;
    }

    var scrollTo = function(elem) {
        if(elem) {
            try {
                elem.scrollIntoView({behavior:'smooth', 'block':'start'});
            } catch(e) {
                console.log('Не удалось проскроллить к элементу', elem, e);
            }
        }
    };

    var scrollInstant = function(elem) {
        if(elem) {
            try {
                elem.scrollIntoView({behavior:'instant', 'block':'start'});
            } catch(e) {
                console.log('Не удалось проскроллить к элементу', elem, e);
            }
        }
    };


    if(window.location.pathname == '/settings') {
        var onchange = function(e) {
            let checkbox = e.target;
            config.set(checkbox.id, checkbox.checked);
        };

        var create_config_item = function(caption, id, value) {
            let checkbox = document.createElement('input');
            checkbox.setAttribute('id', id);
            checkbox.setAttribute('type', 'checkbox');
            if(value) {
                checkbox.checked = true;
            }
            checkbox.onchange = onchange;
            let label = document.createElement('label');
            label.innerText = caption;
            label.setAttribute('for', id);

            let tr = document.createElement('tr');
            tr.append(document.createElement('td'));
            tr.append(document.createElement('td'));
            tr.children[0].append(label);
            tr.children[1].append(checkbox);
            return tr;
        };

        var sett_table = document.createElement('table');
        sett_table.className = "enhanced_settings";
        var pew = document.createElement('style');
        pew.innerText = ".enhanced_settings [type=checkbox] { visibility: visible!important; }";
        document.body.append(pew);
        console.log(pew);
        [
            create_config_item("Иконки в меню сайдбара", 'sidebar_icons', config.get('sidebar_icons')),
            create_config_item('"Моя Лента" в правом меню', 'sidebar_feed_link', config.get('sidebar_feed_link')),
            create_config_item("Скрывать пост нажатием на пустое место в заголовке", 'post_header_collapse', config.get('post_header_collapse')),
            create_config_item("Скрытие просмотренных постов по F", 'restore_f', config.get('restore_f')),
            create_config_item("Включить плавающую навигацию справа", 'enable_nano_nav', config.get('enable_nano_nav')),
            create_config_item("Добавить нормальные ссылки на комментарии [#]", 'great_comment_links', config.get('great_comment_links')),
            create_config_item("Строка поиска всегда развёрнута", 'top_big_searchbar', config.get('top_big_searchbar')),
            create_config_item("Убрать быстрое меню в шапке", 'top_no_avatar', config.get('top_no_avatar')),
            create_config_item("Скрытная иконка клубнички", 'porn_stealth', config.get('porn_stealth')),
            create_config_item("Старая иконка сайта", 'oldschool_favicon', config.get('oldschool_favicon')),
        ].forEach(function(tr) { sett_table.append(tr); });
        document.querySelector('form[name="general"] section').append(sett_table);
        //document.querySelector('form[name="general"] .page-settings__table tbody').append();
    }

    var sidebar = document.querySelector('.sidebar aside');
    var top_menu = document.querySelector('header.header');
    var profile_info = sidebar.querySelector('.profile-info');
    var answers_link = sidebar.querySelector('.menu a.menu__item[href="/answers"]');
    var main_menu = answers_link.parentNode;
    var rating_container = profile_info.children[0];
    var subscribers_container = profile_info.children[1];

    var rating_val = 0;
    try {
        rating_val = JSON.parse(document.querySelector('[data-entry=initParams]').innerText).userKarma;
    } catch(e) {
        if((rating_val = rating_container.getAttribute('aria-label')) == null) {
            rating_val = rating_container.children[0].innerText;
            rating_val = parseFloat(rating_val.replace(/[^0-9.]/g, ''));
        }
    }

    var subscribers_val = 0;
    if((subscribers_val = subscribers_container.getAttribute('aria-label')) == null) {
        subscribers_val = subscribers_container.children[0].innerText;
    }
    subscribers_val = parseInt(subscribers_val.replace(/[^0-9]/g, ''));

    var rating = document.createElement('span');
    rating.innerText = "Рейтинг: "+rating_val;
    rating.style['padding-left'] = getComputedStyle(answers_link)['padding-left'];
    rating.className = "rating";

    var subscribers = document.createElement('span');
    subscribers.innerText = "Подписчиков: "+subscribers_val;
    subscribers.style['padding-left'] = getComputedStyle(answers_link)['padding-left'];
    subscribers.className = "subscribers";

    if(config.get('sidebar_feed_link')) {
        var feed_link_top = top_menu.querySelector('a[href="/subs"]');
        var bell = feed_link_top.querySelector('.bell');
        if(bell) {
            bell.className = "bell bell_profile-menu";
        }
        var clss = "menu__item";
        if(feed_link_top.parentNode.className.indexOf("header-menu__item_current")+1) {
            clss += " menu__item_current";
        }
        feed_link_top.className += clss;
        main_menu.prepend(feed_link_top);
        feed_link_top.style.display = 'block';
    }

    if(config.get('oldschool_favicon')) {
        document.getElementById("favicon").setAttribute("href", "https://cs.pikabu.ru/favicon2x.ico");
    }

    main_menu.prepend(subscribers);
    main_menu.prepend(rating);

    var edits = sidebar.querySelector('.menu a.menu__item[href="/edits"]');
    if(edits) {
        var unneeded_menu = edits.parentNode;
        main_menu.append(edits);
        unneeded_menu.remove();
    }
    profile_info.remove();

    try {
        var styluses = [].slice.call(document.querySelectorAll('.stylus'));
        var stylus;
        for(var i in styluses) {
            stylus = styluses[i];
            stylus.innerHTML = stylus.innerHTML.replace('.sidebar-block__header {', '.'+sidebar.querySelector(".sidebar-block__content").previousElementSibling.className+' {');
        }
    } catch(e) {
        console.log("Не установлен юзерстиль или нет доступа к блоку контента в сайдбаре.");
    }


    if(config.get('sidebar_icons')) {
        var menu_items = [].slice.call(main_menu.children); //Конвертируем HTMLCollection в массив
        menu_items.forEach(function(item) {
            let icon_container = document.createElement('span');

            icon_container.className = "icon_container";
            let i = document.createElement('i');
            let space = document.createTextNode(' ');
            let icon = 'circle-o';
            switch(item.getAttribute('href')) {
                case '/answers':
                    if(item.children.length) {
                        icon = "envelope-o";
                    } else {
                        icon = "envelope-open-o";
                    }
                    break;
                case '/comments':
                    icon = "comments-o";
                    break;
                case '/liked':
                    icon = "thumbs-o-up";
                    break;
                case '/saved-stories':
                    icon = 'floppy-o';
                    break;
                case '/subs-list':
                    icon = 'list';
                    break;
                case '/ignore-list':
                    icon = 'times';
                    break;
                case '/notes':
                    icon = 'sticky-note-o';
                    break;
                case '/edits':
                    icon = 'pencil';
                    break;
                case '/subs':
                    icon = "file-text-o";
                    break;
                default:
                    if(item.className == "rating") {
                        icon = "bar-chart";
                    } else if(item.className == "subscribers") {
                        icon = "users";
                    }
                    break;
            }
            i.className = 'fa fa-'+icon;
            icon_container.prepend(i);
            item.prepend(space);
            item.prepend(icon_container);
        });
    }
    var right_menu = top_menu.querySelector('.header-right-menu');
    var search_bar = right_menu.querySelector('.header-right-menu__search');

    if(config.get('top_big_searchbar')) {
        search_bar.style.width = "250px";
        search_bar.style['background-color'] = "#fff";
        search_bar.style['border-radius'] = "2px";
        search_bar.querySelector('button').style.color = "#8ac858";
    }
    if(config.get('top_no_avatar')) {
        right_menu.querySelector('.avatar').remove();
    }
    var notify_btn = right_menu.querySelector('.header-right-menu__notification');
    notify_btn.innerHTML = '<i class="fa fa-bell fa-bigger" style="font-size:1.3em"></i>';
    notify_btn.style['line-height'] = '1px';
    notify_btn.style['margin-right'] = '0px';


    var user_info = sidebar.querySelector('.user');
    var settings = user_info.querySelector('a[href="/settings"]');
    var exit = user_info.querySelector('.user__exit');
    var nick_container = user_info.querySelector('.user__nick_big').parentNode;

    try {
        var straw = sidebar.querySelector('[data-role="switch-adult"]');
        if(config.get('porn_stealth')) {
            straw.children[0].innerHTML = '<i class="fa fa-bigger fa-user-secret"></i>';
        } else {
            straw.children[0].innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="icon icon--ui__strawberry"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon--ui__strawberry"></use></svg>';
        }
        var qs = straw.parentNode.remove();
    } catch(err) {
        console.log('Настройка клубнички выключена.');
        var straw = document.createTextNode('');
    }

    try {
        var displaymode = sidebar.querySelector('[data-role="display-mode"]');
        displaymode.previousElementSibling.remove();
        var quickset = displaymode.parentNode.parentNode.parentNode;
        displaymode.style.float = "none";
        displaymode.style.display = "inline-block";
        var dtxt = displaymode.children[0];
        if(dtxt.innerText.indexOf('скрыто')+1) {
            dtxt.innerText += ' просмотренных постов';
        } else {
            dtxt.innerText += ' просмотренные посты';
        }
        var panel = document.querySelector('main section:first-child');
        //panel.insertBefore(displaymode, panel.lastChild);
        panel.insertBefore(displaymode, panel.lastElementChild);
        quickset.remove();

    } catch(err) {
        //var straw = document.createTextNode('');
    }

    settings.innerHTML = '<i class="fa fa-cog fa-bigger"></i>';
    settings.style.color = '#757575';

    exit.style.display = "inline-block";
    exit.innerHTML = '<i class="fa fa-sign-out fa-2x"></i>';


    var btn_container = exit.parentNode;
    btn_container.className += " btn_container";

    btn_container.append(settings);
    btn_container.append(document.createTextNode(' '));

    btn_container.append(notify_btn);
    btn_container.append(document.createTextNode(' '));
    btn_container.append(straw);

    user_info.append(exit);
    if(config.get('post_header_collapse')) {
        document.querySelector('main').addEventListener('click', function(e) {
            if(e.target.nodeName == "HEADER") {
                e.target.parentNode.parentNode.querySelector('.collapse-button').click();
            }
        }, false);
    }

    var remove_visited = function(shortcut) {
        document.querySelectorAll('article.story a.story__title-link_visited').forEach(function(item) {
            var story = item.parentNode.parentNode.parentNode.parentNode;
            story.setAttribute('data-visited', "true");
        });
        var curr = current_story();
        var scroll_next = true;
        var is_video = false;
        [].slice.call(document.querySelectorAll('article.story[data-visited="true"]')).forEach(function(item) {
            if(item == curr) {
                var yt = item.querySelector('iframe');
                if(yt) {
                    console.log('Нельзя прятать текущий пост если в нём есть iframe плеера. TODO: получать статус воспроизведения через Youtube Api. Или не TODO, ну его нафиг.');
                    scroll_next = false;
                    if(shortcut) { //Если remove_visited вызвано нажатием клавиши то пытаемся развернуть плеер в фуллскрин
                        var requestFullscreen = yt.requestFullScreen || yt.mozRequestFullScreen || yt.webkitRequestFullScreen || yt.msRequestFullscreen;
                        if(requestFullscreen) {
                            scrollInstant(item); //Скроллим к посту т.к скролл уезжает при удалении предыдущих постов
                            requestFullscreen.call(yt);
                        }
                    }
                    return;
                }
            }
            item.style.display = "none"; //для обратной совместимости с теми, кто не обновит юзерцсс
            item.setAttribute('data-hidden', "true");
        });
        if(scroll_next) {
            var next_story = document.querySelector('article.story[data-visited="false"]');
            scrollInstant(next_story);
        }
    };

    var current_story = function(){
        /* Множитель зума нужен только для хрома, в фаерфоксе там будет ~1 */
        var zoom = parseFloat((window.outerWidth/parseInt(getComputedStyle(document.documentElement,null).width)).toFixed(2));
        var el = document.elementFromPoint((window.outerWidth/zoom)/2, (window.outerHeight/zoom)/4);
        if(el.className.indexOf('stories-feed__container')+1) {
            console.log('Попали в margin.');
            window.scroll(0, window.scrollY-15); //margin-top поста 15px, сдвинув скролл на 15 пикселей мы куда-нибудь да попадём :)
            el = document.elementFromPoint((window.outerWidth/zoom)/2, (window.outerHeight/zoom)/4);
        }
        var story = findParentByTag(el, 'ARTICLE');
        return story;
    };
    var next_story = function() {
        try {
            var cs = current_story();
            if(!cs) {
                console.log('Текущей истории нет, переходим к первой видимой.');
                return document.querySelector('article.story:not([data-hidden="true"])');
            }
            var ni = nodeIndex(cs)+1;
            var ns = [].slice.call(document.querySelectorAll('article.story:nth-child(n+'+ni+'):not(:nth-child('+ni+')):not([data-hidden="true"])'));
            var next = ns.shift();

            var prev = prev_story(true);
            if(prev && prev.getClientRects()[0].top > 0) {
                console.log('Ох уж эти короткие посты.');
                next = cs; //следующий пост на самом деле тот, что мы уже считаем текущим
            }

            if(!next) {
                throw('');
            }
            return next;
        } catch(e) {
            console.log('Нет следующей истории.');
        }
    };
    var prev_story = function(simple) {
        try {
            var cs = current_story();
            var ni = nodeIndex(cs)+1;
            var ps = [].slice.call(document.querySelectorAll('article.story:nth-child(-n+'+ni+'):not(:nth-child('+ni+')):not([data-hidden="true"])'));
            var prev = ps.pop();
            if(!simple) {
                if(prev.getClientRects()[0].top > 0) { //пост слишком маленький, и это он на самом деле является current_story
                    console.log('Ох уж эти короткие посты.');
                    prev = ps.pop(); //достаём предыдущий пост
                }
            }
            if(!prev) {
                throw('');
            }
            return prev;
        } catch(e) {
            if(!simple) {
                console.log('Нет предыдущей стории.');
            }
            //return null;
        }
    };

    if(config.get('restore_f')) {
        if(document.querySelectorAll('article.story').length > 1) {
            /**
             * UPD: mutationObserver больше не нужен для целей скрытия,
             * в последнем обновлении посты сами помечаются прочитанными.
             * Используем это:
            */

            window.addEventListener('keyup', function(e) {
                if(e.key == "f" || e.key == "F" || e.key == "а" || e.key == "А") {
                    remove_visited(true);
                }
            });

            try {
                var fkey = document.querySelector('.hotkeys').parentNode.querySelector('[aria-label="Показать полностью"]');
                fkey.setAttribute('aria-label', 'Скрыть прочитанное');
                fkey.children[1].innerHTML = "<i class='fa fa-eye-slash'></i>";
            } catch(e) {
                //console.log('хоткеи не найдены');
            }
        }
    }

    if(config.get('enable_nano_nav')) {
        if(document.querySelectorAll('article.story').length > 1) {
            var nano_nav = document.createElement('div');
            nano_nav.id = "nano_nav";
            nano_nav.className = "sidebar-block sidebar-block_border";

            var separator = document.createElement('span');
            separator.className = "separator";

            var separator_empty = document.createElement('span');
            separator_empty.className = "separator empty";

            var prev = document.createElement('span');
            var next = document.createElement('span');
            var hide = document.createElement('span');

            prev.className = "link";
            next.className = "link";
            hide.className = "link";
            prev.innerHTML = '<i class="fa fa-chevron-left fa-2x hint" aria-label="Назад (или нажмите кнопку A)"></i>';
            next.innerHTML = '<i class="fa fa-chevron-right fa-2x hint" aria-label="Вперед (или нажмите кнопку D)"></i>';
            hide.innerHTML = '<i class="fa fa-eye-slash fa-2x hint" aria-label="Скрыть просмотренные (или нажмите кнопку F)"></i>';
            prev.onclick = function() { scrollTo(prev_story()); };
            next.onclick = function() { scrollTo(next_story()); };
            hide.onclick = function() { remove_visited(); };
            nano_nav.append(prev);
            nano_nav.append(document.createTextNode(' '));
            nano_nav.append(separator);
            nano_nav.append(document.createTextNode(' '));
            nano_nav.append(next);
            nano_nav.append(document.createTextNode(' '));
            nano_nav.append(separator_empty);
            nano_nav.append(document.createTextNode(' '));
            nano_nav.append(hide);
            document.body.append(nano_nav);
        }
    }

    if(config.get('great_comment_links')) {

        var makeCommentGreatAgain = function(element) {
            let comment_tools = element.querySelector('.comment__tools');
            let branch_link = comment_tools.querySelector('[data-role="link"]');
            let branch_link_href = branch_link.getAttribute('href');
            branch_link.setAttribute('aria-label', 'Ссылка на ветку комментариев');

            let comment_link = document.createElement('a');
            comment_link.className = 'comment__tool hint';
            comment_link.setAttribute('data-role', 'comment_link');
            comment_link.setAttribute('aria-label', 'Ссылка на комментарий');
            comment_link.setAttribute('href', branch_link_href.replace('?cid=', '#comment_'));
            comment_link.innerText = '#';
            comment_tools.insertBefore(comment_link, branch_link);
            element.setAttribute('data-great', "true");
        };
        var makeCommentsGreatAgain = function() {
            document.querySelectorAll('.comment:not([data-great])').forEach(function(item) {
                makeCommentGreatAgain(item);
            });
        };
        makeCommentsGreatAgain();
        var jumpto = function() {
            if(window.location.hash.indexOf('comment_')+1) {
                var comment = document.querySelector(window.location.hash);
                if(comment) {
                    comment.scrollIntoView({behavior:'smooth', 'block':'start'});
                } else { //В списке комментариев нет загруженного комментария
                    var more_comments = document.querySelector('button.comments__more-button');
                    if(more_comments) {
                        more_comments.scrollIntoView({behavior:'smooth', 'block':'start'});
                        more_comments.className = more_comments.className += " button_yellow";
                        (function() { setTimeout(function() { console.log('release');more_comments.className = more_comments.className.replace(' button_yellow', ''); }, 2500); })();
                    }
                }
            }
        };


        if(window.location.href.indexOf('/story/')+1) {
            jumpto();
            var is_comment_already = null;
            if(window.location.hash.indexOf('comment_')+1) {
                 is_comment_already = document.querySelector(window.location.hash);
            }
            var more_comments_btn = document.querySelector('button.comments__more-button');
            if(more_comments_btn) {
                more_comments_btn.addEventListener('click', function() {
                    var emitted = false;
                    var e = document.createEvent('Event');
                    e.initEvent('comments.loaded', true, true);
                    var comment_observer = new MutationObserver(function(records) {
                        records.some(function(record) {
                            if(record.target.className.indexOf('comment')+1) {
                                document.dispatchEvent(e);
                                comment_observer.disconnect();
                                return true;
                            } else {
                                return false;
                            }
                        });
                    });
                    comment_observer.observe(document.querySelector('section.comments'),  {'childList':true, 'subtree':true});

                });
            }



            document.addEventListener('comments.loaded', function() {
                console.log('comments loaded emitted');
                makeCommentsGreatAgain();
                if(!is_comment_already) {
                    jumpto();
                }
            });
        }
    }

})();