Catalog Hide

Реализует скрытие тредов в каталоге

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Catalog Hide
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @description  Реализует скрытие тредов в каталоге
// @author       You
// @match        https://2ch.hk/*/catalog.html
// @match        https://2ch.life/*/catalog.html
// @icon         https://www.google.com/s2/favicons?sz=64&domain=2ch.hk
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

// === ПОДГОТОВКА ===
const svg_hide = `<svg class="de-panel-svg" width="25" height="25" style="filter: drop-shadow(1px 0 0 black) drop-shadow(-1px 0 0 black) drop-shadow(0 1px 0 black) drop-shadow(0 -1px 0 black);position: absolute;top: 10px;left: 10px;">

<style>
    .de-panel-svg:hover .de-svg-stroke {
        stroke: red;
    }
</style><path class="de-svg-stroke" d="M6 19L19 6M6 6l13 13" style="stroke-width: 4px;"></path>

</svg>`

const css = '.de-panel-svg path{ stroke: white }';
const style = document.createElement("style");
style.innerText = css;
document.head.appendChild(style);

// КОНСТАНТЫ
const ATTR_THREAD_NUM = 'data-num';
const LS_HIDDEN_THREADS = 'de-threads';
const LS_HIDDEN_POSTS = 'de-posts';

class ThreadHelper {
    static _idPrefix = 'js-thread-';
    /**
    * @param {HTMLDivElement} thread
    * @returns {number}
    */
    static num(thread) {
        return parseInt(thread.getAttribute(ATTR_THREAD_NUM));
    }

    /**
    * @param {HTMLDivElement} thread
    * @returns {string}
    */
    static id(thread) {
        return this._idPrefix + this.num(thread);
    }

    /**
    * @param {HTMLDivElement} thread
    * @returns {boolean}
    */
    static exists(thread) {
        return document.getElementById(this.id(thread)).style.display != 'none';
    }

    /**
    * @param {HTMLDivElement} thread
    * @returns {string}
    */
    static title(thread) {
        return thread.lastElementChild.firstElementChild.lastElementChild.textContent;
    }

    /**
    * @param {HTMLDivElement} thread
    * @returns {SVGAElement}
    * @returns {null}
    */
    static findHideButton(thread) {
        return thread.firstElementChild.firstElementChild.querySelector("svg[class='de-panel-svg'")
    }

    /**
    * @param {HTMLDivElement} thread
    * @param {HidePolicy} policy
    */
    static hide(thread, policy) {
        const id = this.id(thread);
        const mainContentThread = document.getElementById(id);
        
        policy.hide(thread);
        policy.hide(mainContentThread);
    }
    /**
    * @param {HTMLDivElement} thread
    * @param {HidePolicy} policy
    */
    static unhide(thread, policy) {
        const id = this.id(thread);
        const mainContentThread = document.getElementById(id);
        policy.unhide(thread)
        policy.unhide(mainContentThread);
    }
}

class HidePolicy {
    /**
    * @param {HTMLDivElement} thread
    */
    isHidden(thread) { throw new Element('Not Implemented'); }
    hide(thread) { throw new Element('Not Implemented'); }
    unhide(thread) { throw new Element('Not Implemented'); }
}

class DisplayNonePolicy extends HidePolicy {
    /**
    * @param {HTMLDivElement} thread
    */
    isHidden(thread) {
        return thread.style.display === 'none';
    }

    /**
    * @param {HTMLDivElement} thread
    */
    hide(thread) {
        thread.style.opacity = 1;
        thread.style.display = 'none';
    }
    unhide(thread) {
        thread.style.display = '';
    }
}

class DisplayOpacityPolicy extends HidePolicy {
    /**
    * @param {HTMLDivElement} thread
    */
    isHidden(thread) {
        return thread.style.opacity === 0.1;
    }

    /**
    * @param {HTMLDivElement} thread
    */
    hide(thread) {
        thread.style.display = '';
        thread.style.opacity = 0.1;
    }
    unhide(thread) {
        thread.style.opacity = 1;
    }
}

// Взаимодействия с хранилищем скрытых тредов
class HiddenRepository {
    constructor(board) {
        this._board = board;
        this._storage = window.localStorage;
        this._threads = this.refreshThreads();
        this._posts = this.refreshPosts();
    }

    /**
    * @returns {Object}
    */
    refreshThreads() {
        return JSON.parse(
            this._storage.getItem(LS_HIDDEN_THREADS)
        );
    }

    /**
    * @returns {Object}
    */
    refreshPosts() {
        return JSON.parse(
            this._storage.getItem(LS_HIDDEN_POSTS)
        );
    }

    /**
    * @returns {Object}
    */
    get() {
        return this._threads;
    }

    /**
    * @param {HTMLDivElement} thread
    */
    add(thread) {
        const num = ThreadHelper.num(thread);
        const timestamp = Date.now();
        const title = ThreadHelper.title(thread);
        this._threads[this._board][num] = [timestamp, num, title];
        this._posts[this._board][num] = [timestamp, num, true];
    }

    /**
    * @param {HTMLDivElement} thread
    */
    remove(thread) {
        const num = ThreadHelper.num(thread);
        delete this._threads[this._board][num];
        delete this._posts[this._board][num];
    }

    commit() {
        this._storage.setItem(
            LS_HIDDEN_THREADS,
            JSON.stringify(this._threads)
        );
        this._storage.setItem(
            LS_HIDDEN_POSTS,
            JSON.stringify(this._posts)
        );
    }

    /**
    * @param {HTMLDivElement} thread
    * @returns {boolean}
    */
    isHidden(thread) {
        const num = ThreadHelper.num(thread);
        return num in this._threads[this._board] || (num in this._posts[this._board] && this._posts[this._board][num][2]);
    }
}

// ВЫЧИСЛЯЕМЫЕ
const board = window.location.pathname.split('/')[1];
const hidden = new HiddenRepository(board);
let hidePolicy = new DisplayNonePolicy();


// === ОБСЁРВЕР ДЛЯ ДОБАВЛЕНИЯ КНОПОК СКРЫТИЯ ===
/** Слушает каталог на предмет изменения тредов
* @param {Array<MutationRecord>} mutationList
*/
function threadListener(mutationList, observer) {
    mutationList.forEach(mutation => {
        mutation.addedNodes.forEach(node => {
            resolveThread(node);
        })
    });
}

/** Добавляет тредам новую логику
* @param {HTMLDivElement} thread
*/
function resolveThread(thread) {
    safeCreateHideButton(thread);
    removeHiddenThread(thread);
}

/** Добавляет кнопку скрытия для треда
* @param {HTMLDivElement} thread
* @returns {SVGAElement}
*/
function addHideSvg(thread) {
    const a = thread.firstElementChild.firstElementChild;
    a.insertAdjacentHTML("beforeend", svg_hide);
    return ThreadHelper.findHideButton(thread);
}

/** Создаёт кнопку скрытия действия если она уже не была создана
* @param {HTMLDivElement} thread
* @returns {SVGAElement}
*/
function safeCreateHideButton(thread) {
    const callback = (event) => onHideClick(event, thread);

    const button = ThreadHelper.findHideButton(thread);
    if (button != null) {
        if (button.onclick === null) {
            button.onclick = callback;
        }
        return;
    }
    const hideBtn = addHideSvg(thread);
    hideBtn.onclick = callback;
    return hideBtn;
}

/** Скрывает тред в DOM если он является скрытым
* @param {HTMLDivElement} thread
* @param {boolean} факт удаления треда
*/
function removeHiddenThread(thread) {
    const isHidden = hidden.isHidden(thread);
    if (isHidden) {
        ThreadHelper.hide(thread, hidePolicy);
    }
    return isHidden;
}

/** Логика скрытия при нажатии по соответствующей кнопке
* @param {Event} event
* @param {HTMLDivElement} thread
*/
function onHideClick(event, thread) {
    event.preventDefault();
    if (hidden.isHidden(thread)) {
        ThreadHelper.unhide(thread, hidePolicy);
        hidden.remove(thread);
    } else {
        ThreadHelper.hide(thread, hidePolicy);
        hidden.add(thread);
    }
    hidden.commit();
}

function applyChildlistObserver(target, callback) {
    const observer = new MutationObserver(threadListener);
    observer.observe(target, { "childList": true })
}

function applyHidePolicyToAllThreads() {
    // Прохожу разово по всем малышам для применения к ним логики скрипта
    [...document.getElementById("js-threads").children].forEach(thread => resolveThread(thread));
}

function main() {
    applyHidePolicyToAllThreads();
    const target = document.getElementsByTagName("main")[0];
    applyChildlistObserver(target, threadListener)
}

// == UI ==
// инстациируем
const header = document.querySelector("div[class='header__meta']");
const label = document.createElement('label');
label.className = 'header__ctlgnav';
const span = document.createElement('span');
span.innerText = 'Прозрачное скрытие ';
const checkbox = document.createElement('input');
checkbox.id = 'opacity-hide-policy';
checkbox.type = 'checkbox';
// отображаем
label.appendChild(span);
label.appendChild(checkbox);
header.appendChild(label);
// логика скрытия для чекбокса
checkbox.onclick = () => {
    const checkbox = document.getElementById('opacity-hide-policy');
    if (checkbox.checked) {
      hidePolicy = new DisplayOpacityPolicy();
    } else {
      hidePolicy = new DisplayNonePolicy();
    }
    applyHidePolicyToAllThreads();
}

(function () {
    'use strict';
    window.addEventListener("load", function () {
        main();
    }, false);
})();