Catalog Hide

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
})();