Catalog Hide

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

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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