Better Sturdy

Easy image filtering and post marking + hide your own posts

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         Better Sturdy
// @namespace    dunkydonut
// @version      1.1
// @description  Easy image filtering and post marking + hide your own posts
// @author       ILoveS10
// @license      MIT
// @match        https://sturdychan.help/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    let hiddenCount = 0;
    const hiddenDisplay = document.querySelector('#hidden');
    const hiddenPosts = new Map();

    function updateHiddenDisplay() {
        if (hiddenDisplay) hiddenDisplay.textContent = `Hidden: ${hiddenPosts.size}`;
    }

    function hashFiltering(postElement) {
        const image = postElement.querySelector('figure img');
        const filterArea = document.querySelector('#filterArea');
        if (!image || !filterArea) return null;

        const imageURL = image.src;
        const filenameWithExt = imageURL.split('/').pop();
        const filenameWithoutExt = filenameWithExt.split('.')[0];
        const hashLine = `hash:${filenameWithoutExt}`;

        filterArea.value += `\n${hashLine}`;
        return hashLine;
    }

    function showMessage(text, undoCallback) {
        const message = document.createElement('div');
        message.id = 'sturdy-message';
        message.style.position = 'fixed';
        message.style.top = '30px';
        message.style.left = '50%';
        message.style.transform = 'translateX(-50%)';
        message.style.backgroundColor = '#000';
        message.style.color = '#fff';
        message.style.padding = '10px 20px';
        message.style.borderRadius = '8px';
        message.style.zIndex = '10000';
        message.style.fontSize = '16px';
        message.style.boxShadow = '0 2px 10px rgba(0,0,0,0.5)';
        message.style.display = 'flex';
        message.style.alignItems = 'center';
        message.style.gap = '10px';

        const textNode = document.createElement('span');
        textNode.textContent = text;

        const showButton = document.createElement('button');
        showButton.textContent = '[Show]';
        showButton.style.background = 'none';
        showButton.style.color = '#0af';
        showButton.style.border = 'none';
        showButton.style.cursor = 'pointer';
        showButton.style.fontSize = '16px';

        const undoButton = document.createElement('button');
        undoButton.textContent = '[Undo]';
        undoButton.style.background = 'none';
        undoButton.style.color = '#0af';
        undoButton.style.border = 'none';
        undoButton.style.cursor = 'pointer';
        undoButton.style.fontSize = '16px';

        showButton.addEventListener('click', () => {
            const filterBox = document.getElementById('megukascript-options');
            const filterTabs = filterBox?.querySelectorAll('.tab-link');
            const filtersTab = Array.from(filterTabs || []).find(tab => tab.dataset.id === '1');
            const filterArea = document.getElementById('filterArea');

            if (filterBox) filterBox.style.display = 'block';
            if (filtersTab) filtersTab.click();
            if (filterArea) filterArea.scrollIntoView({ behavior: 'smooth', block: 'center' });
        });

        undoButton.addEventListener('click', () => {
            undoCallback();
            message.remove();
        });

        message.appendChild(textNode);
        message.appendChild(showButton);
        message.appendChild(undoButton);
        document.body.appendChild(message);

        const removeMessage = () => {
            message.remove();
            document.removeEventListener('keydown', onKeyDown);
        };

        const onKeyDown = (e) => {
            if (e.key === 'Escape') removeMessage();
        };

        document.addEventListener('keydown', onKeyDown);
        setTimeout(removeMessage, 10000);
    }

    function hideReplies(post) {
        const backlinks = post.querySelectorAll('.backlinks a.post-link[data-id]');
        backlinks.forEach(link => {
            const replyId = link.getAttribute('data-id');
            const replyPost = document.getElementById(`p${replyId}`);
            if (replyPost && !hiddenPosts.has(replyPost)) {
                const originalClass = replyPost.className;
                replyPost.classList.add('hidden');
                hiddenPosts.set(replyPost, originalClass);
            }
        });
    }

    function addButtons() {
        const dropdowns = document.querySelectorAll('ul.popup-menu.glass');

        dropdowns.forEach(dropdown => {
            const post = dropdown.closest('article');
            if (!post) return;

            if (!dropdown.querySelector('li[data-id="hide"]')) {
                const hideItem = document.createElement('li');
                hideItem.setAttribute('data-id', 'hide');
                hideItem.textContent = 'Hide';
                hideItem.addEventListener('click', e => {
                    e.stopPropagation();
                    const originalClass = post.classList.contains('postMine') ? 'glass postMine' : 'glass';
                    post.classList.remove('postMine');
                    post.classList.add('hidden');
                    hiddenPosts.set(post, originalClass);
                    hideReplies(post);
                    updateHiddenDisplay();
                });
                dropdown.insertBefore(hideItem, dropdown.firstChild);
            }

            if (!dropdown.querySelector('li[data-id="addHashToFilter"]')) {
                const hasImage = post.querySelector('figcaption a[download]');
                if (hasImage) {
                    const hashFilter = document.createElement('li');
                    hashFilter.setAttribute('data-id', 'addHashToFilter');
                    hashFilter.textContent = 'Filter Hash';

                    hashFilter.addEventListener('click', e => {
                        e.stopPropagation();
                        const hashLine = hashFiltering(post);
                        if (!hashLine) return;

                        const saveButton = document.getElementById('filterArea_button');
                        if (saveButton) saveButton.click();

                        showMessage('Image Filtered. Refresh page.', () => {
                            const filterArea = document.querySelector('#filterArea');
                            if (!filterArea) return;
                            const lines = filterArea.value.split('\n').filter(line => line.trim() !== hashLine);
                            filterArea.value = lines.join('\n');
                            if (saveButton) saveButton.click();
                        });
                    });

                    dropdown.appendChild(hashFilter);
                }
            }
        });
    }

    addButtons();
    const observer = new MutationObserver(addButtons);
    observer.observe(document.body, { childList: true, subtree: true });

    function addToggleMarkButtons() {
        document.querySelectorAll('article.glass .popup-menu.glass').forEach(menu => {
            const article = menu.closest('article');
            if (!article) return;

            let existingToggle = menu.querySelector('li[data-id="toggle-own"]');
            if (!existingToggle) {
                const toggleItem = document.createElement('li');
                toggleItem.setAttribute('data-id', 'toggle-own');

                function updateToggleText() {
                    toggleItem.textContent = article.classList.contains('postMine') ?
                        'Unmark post as your own' : 'Mark post as your own';
                }

                toggleItem.addEventListener('click', () => {
                    const postId = article.id.replace('p', '');
                    const replySelectors = `a.post-link[data-id="${postId}"]`;

                    if (article.classList.contains('postMine')) {
                        article.classList.remove('postMine');
                        const author = article.querySelector('b.name i');
                        if (author && author.textContent.trim() === '(You)') author.remove();
                        document.querySelectorAll(replySelectors).forEach(link => {
                            const replyArticle = link.closest('article');
                            if (replyArticle) {
                                link.innerHTML = link.innerHTML.replace(' (You)', '');
                                replyArticle.classList.remove('postReply');
                            }
                        });
                    } else {
                        article.classList.add('postMine');
                        const nameSpan = article.querySelector('b.name span');
                        if (nameSpan && !article.querySelector('b.name i')) {
                            const youMarker = document.createElement('i');
                            youMarker.textContent = ' (You)';
                            nameSpan.insertAdjacentElement('afterend', youMarker);
                        }
                        document.querySelectorAll(replySelectors).forEach(link => {
                            const replyArticle = link.closest('article');
                            if (replyArticle && !link.innerHTML.includes(' (You)')) {
                                link.innerHTML = link.innerHTML.replace(/(>>\d+)/, '$1 (You)');
                                replyArticle.classList.add('postReply');
                            }
                        });
                    }
                    updateToggleText();
                });

                updateToggleText();
                menu.appendChild(toggleItem);
            }
        });
    }

    addToggleMarkButtons();
    const toggleObserver = new MutationObserver(addToggleMarkButtons);
    toggleObserver.observe(document.body, { childList: true, subtree: true });

    if (hiddenDisplay) {
        hiddenDisplay.addEventListener('click', () => {
            hiddenPosts.forEach((originalClass, post) => {
                post.className = originalClass;
            });
            hiddenPosts.clear();
            updateHiddenDisplay();
        });
    }
})();