Website and Page Blocker

Click-to-block websites, pages, or prefixes — for digital well-being. Supports timed or permanent blocks.

// ==UserScript==
// @name         Website and Page Blocker
// @namespace    https://greasyfork.org/en/users/1483582-merryberries
// @version      2.0
// @license      MIT
// @author       MerryBerries
// @description  Click-to-block websites, pages, or prefixes — for digital well-being. Supports timed or permanent blocks.
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function () {
    'use strict';

    const hostname = location.hostname;
    const fullUrl = location.href;
    const pathname = location.pathname;
    const now = Date.now();

    let siteBlockList = GM_getValue('siteBlockList', {});
    let pageBlockList = GM_getValue('pageBlockList', {});
    let pagePrefixBlockList = GM_getValue('pagePrefixBlockList', {});

    // === Block check logic ===
    function blockPage(message) {
        document.documentElement.innerHTML = `
            <div style="font-size: 2em; color: red; text-align: center; margin-top: 20%;">
                ${message}
            </div>`;
        document.title = "Blocked";
    }

    if (siteBlockList[hostname]) {
        const until = siteBlockList[hostname];
        if (until === -1 || now < until) {
            blockPage(`🚫 The entire site (${hostname}) is blocked.`);
            return;
        } else {
            delete siteBlockList[hostname];
            GM_setValue('siteBlockList', siteBlockList);
        }
    }

    const pageKey = pageBlockList[fullUrl] ? fullUrl : pageBlockList[pathname] ? pathname : null;
    if (pageKey) {
        const until = pageBlockList[pageKey];
        if (until === -1 || now < until) {
            blockPage(`🚫 This page is blocked.`);
            return;
        } else {
            delete pageBlockList[pageKey];
            GM_setValue('pageBlockList', pageBlockList);
        }
    }

    for (const prefix in pagePrefixBlockList) {
        const until = pagePrefixBlockList[prefix];
        if ((until === -1 || now < until) && fullUrl.startsWith(prefix)) {
            blockPage(`🚫 This group of pages is blocked.\n(Matched prefix: ${prefix})`);
            return;
        }
        if (until !== -1 && now >= until) {
            delete pagePrefixBlockList[prefix];
            GM_setValue('pagePrefixBlockList', pagePrefixBlockList);
        }
    }

    // === Draggable floating button ===
    const mainBtn = document.createElement('button');
    mainBtn.innerText = '⚙ Block options';

    // Get saved position or use default
    const savedPosition = GM_getValue('buttonPosition', { top: '10px', right: '10px' });

    Object.assign(mainBtn.style, {
        position: 'fixed',
        top: savedPosition.top,
        right: savedPosition.right,
        left: savedPosition.left || 'auto',
        bottom: savedPosition.bottom || 'auto',
        zIndex: 9999,
        background: '#f44',
        color: '#fff',
        border: 'none',
        padding: '6px 12px',
        borderRadius: '6px',
        fontSize: '14px',
        cursor: 'move',
        userSelect: 'none',
        opacity: '0.9',
        transition: 'opacity 0.2s ease'
    });

    // Add hover effect
    mainBtn.addEventListener('mouseenter', () => {
        mainBtn.style.opacity = '1';
        mainBtn.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
    });

    mainBtn.addEventListener('mouseleave', () => {
        mainBtn.style.opacity = '0.9';
        mainBtn.style.boxShadow = 'none';
    });

    document.body.appendChild(mainBtn);

    // === Drag functionality ===
    let isDragging = false;
    let dragOffset = { x: 0, y: 0 };
    let dragStartTime = 0;

    mainBtn.addEventListener('mousedown', (e) => {
        isDragging = true;
        dragStartTime = Date.now();

        const rect = mainBtn.getBoundingClientRect();
        dragOffset.x = e.clientX - rect.left;
        dragOffset.y = e.clientY - rect.top;

        mainBtn.style.cursor = 'grabbing';
        mainBtn.style.opacity = '0.7';

        // Prevent text selection during drag
        document.body.style.userSelect = 'none';

        e.preventDefault();
    });

    document.addEventListener('mousemove', (e) => {
        if (!isDragging) return;

        const newX = e.clientX - dragOffset.x;
        const newY = e.clientY - dragOffset.y;

        // Keep button within viewport bounds
        const maxX = window.innerWidth - mainBtn.offsetWidth;
        const maxY = window.innerHeight - mainBtn.offsetHeight;

        const clampedX = Math.max(0, Math.min(newX, maxX));
        const clampedY = Math.max(0, Math.min(newY, maxY));

        // Reset positioning properties
        mainBtn.style.left = clampedX + 'px';
        mainBtn.style.top = clampedY + 'px';
        mainBtn.style.right = 'auto';
        mainBtn.style.bottom = 'auto';

        e.preventDefault();
    });

    document.addEventListener('mouseup', (e) => {
        if (!isDragging) return;

        isDragging = false;
        mainBtn.style.cursor = 'move';
        mainBtn.style.opacity = '0.9';
        document.body.style.userSelect = '';

        // Save position
        const rect = mainBtn.getBoundingClientRect();
        const position = {
            left: rect.left + 'px',
            top: rect.top + 'px',
            right: 'auto',
            bottom: 'auto'
        };
        GM_setValue('buttonPosition', position);

        // If the drag was very short (< 200ms), treat it as a click
        const dragDuration = Date.now() - dragStartTime;
        if (dragDuration < 200) {
            // Small delay to ensure drag state is reset
            setTimeout(() => {
                showMainMenu();
            }, 10);
        }

        e.preventDefault();
    });

    // === Check if item already exists ===
    function checkExistingBlock(type, key) {
        let existing = null;
        let timeText = '';

        if (type === 'site' && siteBlockList[key]) {
            existing = siteBlockList[key];
        } else if (type === 'page' && pageBlockList[key]) {
            existing = pageBlockList[key];
        } else if (type === 'prefix' && pagePrefixBlockList[key]) {
            existing = pagePrefixBlockList[key];
        }

        if (existing !== null) {
            if (existing === -1) {
                timeText = 'permanently';
            } else if (existing > now) {
                timeText = `until ${new Date(existing).toLocaleString()}`;
            } else {
                // Expired block, can be overwritten
                return null;
            }
            return timeText;
        }
        return null;
    }

    // === Popup style with X button ===
    function createPopup(title, buttons, showBlockList = false) {
        const overlay = document.createElement('div');
        Object.assign(overlay.style, {
            position: 'fixed',
            top: 0,
            left: 0,
            width: '100vw',
            height: '100vh',
            backgroundColor: 'rgba(0,0,0,0.4)',
            zIndex: 10000,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center'
        });

        const box = document.createElement('div');
        Object.assign(box.style, {
            background: '#fff',
            padding: '20px',
            borderRadius: '10px',
            textAlign: 'center',
            minWidth: '300px',
            maxWidth: showBlockList ? '90%' : '90%',
            maxHeight: showBlockList ? '85vh' : 'auto',
            fontSize: '16px',
            position: 'relative',
            overflow: showBlockList ? 'hidden' : 'visible',
            display: 'flex',
            flexDirection: 'column'
        });

        // X button
        const closeBtn = document.createElement('button');
        closeBtn.innerText = '×';
        Object.assign(closeBtn.style, {
            position: 'absolute',
            top: '5px',
            right: '10px',
            background: 'none',
            border: 'none',
            fontSize: '24px',
            cursor: 'pointer',
            color: '#999',
            padding: '0',
            width: '30px',
            height: '30px'
        });
        closeBtn.onclick = () => document.body.removeChild(overlay);
        box.appendChild(closeBtn);

        const titleEl = document.createElement('div');
        titleEl.innerText = title;
        titleEl.style.marginBottom = '15px';
        titleEl.style.fontSize = '18px';
        titleEl.style.marginRight = '40px'; // Space for X button
        box.appendChild(titleEl);

        if (showBlockList) {
            createBlockListContent(box, overlay);
        }

        buttons.forEach(({ label, color, onClick }) => {
            const btn = document.createElement('button');
            btn.innerText = label;
            Object.assign(btn.style, {
                margin: '6px',
                padding: '8px 14px',
                fontSize: '14px',
                backgroundColor: color,
                color: '#fff',
                border: 'none',
                borderRadius: '6px',
                cursor: 'pointer'
            });
            btn.onclick = () => {
                document.body.removeChild(overlay);
                onClick();
            };
            box.appendChild(btn);
        });

        overlay.appendChild(box);
        document.body.appendChild(overlay);
    }

    // === Block list display with multi-select ===
    function createBlockListContent(container, overlay) {
        let selectedItems = new Set();

        // Control buttons
        const controlsDiv = document.createElement('div');
        Object.assign(controlsDiv.style, {
            marginBottom: '15px',
            display: 'flex',
            gap: '10px',
            justifyContent: 'center',
            flexWrap: 'wrap'
        });

        const selectAllBtn = document.createElement('button');
        selectAllBtn.innerText = '☑ Select All';
        Object.assign(selectAllBtn.style, {
            padding: '6px 12px',
            fontSize: '12px',
            backgroundColor: '#3498db',
            color: '#fff',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
        });

        const deselectAllBtn = document.createElement('button');
        deselectAllBtn.innerText = '☐ Deselect All';
        Object.assign(deselectAllBtn.style, {
            padding: '6px 12px',
            fontSize: '12px',
            backgroundColor: '#95a5a6',
            color: '#fff',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
        });

        const deleteSelectedBtn = document.createElement('button');
        deleteSelectedBtn.innerText = '🗑 Delete Selected';
        Object.assign(deleteSelectedBtn.style, {
            padding: '6px 12px',
            fontSize: '12px',
            backgroundColor: '#e74c3c',
            color: '#fff',
            border: 'none',
            borderRadius: '4px',
            cursor: 'pointer'
        });

        const selectedCountSpan = document.createElement('span');
        selectedCountSpan.innerText = '0 selected';
        Object.assign(selectedCountSpan.style, {
            fontSize: '12px',
            color: '#666',
            alignSelf: 'center'
        });

        controlsDiv.appendChild(selectAllBtn);
        controlsDiv.appendChild(deselectAllBtn);
        controlsDiv.appendChild(deleteSelectedBtn);
        controlsDiv.appendChild(selectedCountSpan);
        container.appendChild(controlsDiv);

        // Scrollable list container
        const listContainer = document.createElement('div');
        Object.assign(listContainer.style, {
            textAlign: 'left',
            flex: '1',
            overflow: 'auto',
            border: '1px solid #ddd',
            padding: '10px',
            borderRadius: '5px',
            minHeight: '200px'
        });

        let hasBlocks = false;
        let allCheckboxes = [];

        function updateSelectedCount() {
            selectedCountSpan.innerText = `${selectedItems.size} selected`;
            deleteSelectedBtn.style.backgroundColor = selectedItems.size > 0 ? '#e74c3c' : '#bdc3c7';
        }

        // Site blocks
        if (Object.keys(siteBlockList).length > 0) {
            hasBlocks = true;
            const siteHeader = document.createElement('h4');
            siteHeader.innerText = '🟥 Blocked Sites:';
            siteHeader.style.color = '#c0392b';
            listContainer.appendChild(siteHeader);

            Object.entries(siteBlockList).forEach(([site, until]) => {
                const { item, checkbox } = createSelectableBlockItem(site, until, 'site', selectedItems, updateSelectedCount);
                allCheckboxes.push(checkbox);
                listContainer.appendChild(item);
            });
        }

        // Page blocks
        if (Object.keys(pageBlockList).length > 0) {
            hasBlocks = true;
            const pageHeader = document.createElement('h4');
            pageHeader.innerText = '🟨 Blocked Pages:';
            pageHeader.style.color = '#f39c12';
            pageHeader.style.marginTop = '15px';
            listContainer.appendChild(pageHeader);

            Object.entries(pageBlockList).forEach(([page, until]) => {
                const { item, checkbox } = createSelectableBlockItem(page, until, 'page', selectedItems, updateSelectedCount);
                allCheckboxes.push(checkbox);
                listContainer.appendChild(item);
            });
        }

        // Prefix blocks
        if (Object.keys(pagePrefixBlockList).length > 0) {
            hasBlocks = true;
            const prefixHeader = document.createElement('h4');
            prefixHeader.innerText = '🟦 Blocked Prefixes:';
            prefixHeader.style.color = '#2980b9';
            prefixHeader.style.marginTop = '15px';
            listContainer.appendChild(prefixHeader);

            Object.entries(pagePrefixBlockList).forEach(([prefix, until]) => {
                const { item, checkbox } = createSelectableBlockItem(prefix, until, 'prefix', selectedItems, updateSelectedCount);
                allCheckboxes.push(checkbox);
                listContainer.appendChild(item);
            });
        }

        if (!hasBlocks) {
            listContainer.innerHTML = '<p style="text-align: center; color: #999;">No active blocks</p>';
            controlsDiv.style.display = 'none';
        }

        container.appendChild(listContainer);

        // Control button events
        selectAllBtn.onclick = () => {
            allCheckboxes.forEach(checkbox => {
                checkbox.checked = true;
                const key = checkbox.dataset.key;
                const type = checkbox.dataset.type;
                selectedItems.add(`${type}:${key}`);
            });
            updateSelectedCount();
        };

        deselectAllBtn.onclick = () => {
            allCheckboxes.forEach(checkbox => {
                checkbox.checked = false;
            });
            selectedItems.clear();
            updateSelectedCount();
        };

        deleteSelectedBtn.onclick = () => {
            if (selectedItems.size === 0) {
                alert('No items selected');
                return;
            }

            if (confirm(`Delete ${selectedItems.size} selected item(s)?`)) {
                // Refresh the data from storage before deletion
                siteBlockList = GM_getValue('siteBlockList', {});
                pageBlockList = GM_getValue('pageBlockList', {});
                pagePrefixBlockList = GM_getValue('pagePrefixBlockList', {});

                selectedItems.forEach(item => {
                    const colonIndex = item.indexOf(':');
                    const type = item.substring(0, colonIndex);
                    const key = item.substring(colonIndex + 1);

                    if (type === 'site') {
                        delete siteBlockList[key];
                        GM_setValue('siteBlockList', siteBlockList);
                    } else if (type === 'page') {
                        delete pageBlockList[key];
                        GM_setValue('pageBlockList', pageBlockList);
                    } else if (type === 'prefix') {
                        delete pagePrefixBlockList[key];
                        GM_setValue('pagePrefixBlockList', pagePrefixBlockList);
                    }
                });
                document.body.removeChild(overlay);
                location.reload();
            }
        };

        updateSelectedCount();
    }

    // === Individual selectable block item ===
    function createSelectableBlockItem(url, until, type, selectedItems, updateCallback) {
        const item = document.createElement('div');
        Object.assign(item.style, {
            marginBottom: '8px',
            padding: '8px',
            backgroundColor: '#f9f9f9',
            borderRadius: '4px',
            display: 'flex',
            alignItems: 'center',
            gap: '10px'
        });

        // Checkbox
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.dataset.key = url;
        checkbox.dataset.type = type;
        Object.assign(checkbox.style, {
            transform: 'scale(1.2)',
            cursor: 'pointer'
        });

        checkbox.onchange = () => {
            const itemKey = `${type}:${url}`;
            if (checkbox.checked) {
                selectedItems.add(itemKey);
            } else {
                selectedItems.delete(itemKey);
            }
            updateCallback();
        };

        // Info section
        const info = document.createElement('div');
        info.style.flex = '1';
        info.style.wordBreak = 'break-all';

        const timeText = until === -1 ? 'Permanent' :
            until > now ? `Until ${new Date(until).toLocaleString()}` : 'Expired';

        info.innerHTML = `
            <div style="font-weight: bold; font-size: 14px;">${url}</div>
            <div style="font-size: 12px; color: #666;">${timeText}</div>
        `;

        // Individual delete button
        const removeBtn = document.createElement('button');
        removeBtn.innerText = '🗑';
        Object.assign(removeBtn.style, {
            background: '#e74c3c',
            color: '#fff',
            border: 'none',
            padding: '4px 8px',
            borderRadius: '4px',
            cursor: 'pointer',
            fontSize: '12px'
        });

        removeBtn.onclick = () => {
            if (confirm(`Remove block for: ${url}?`)) {
                if (type === 'site') {
                    delete siteBlockList[url];
                    GM_setValue('siteBlockList', siteBlockList);
                } else if (type === 'page') {
                    delete pageBlockList[url];
                    GM_setValue('pageBlockList', pageBlockList);
                } else if (type === 'prefix') {
                    delete pagePrefixBlockList[url];
                    GM_setValue('pagePrefixBlockList', pagePrefixBlockList);
                }
                location.reload();
            }
        };

        item.appendChild(checkbox);
        item.appendChild(info);
        item.appendChild(removeBtn);

        return { item, checkbox };
    }

    // === Duration choices ===
    function chooseDuration(callback) {
        createPopup('⏱ How long to block?', [
            { label: '10 min', color: '#666', onClick: () => callback(10) },
            { label: '30 min', color: '#444', onClick: () => callback(30) },
            { label: '1 hour', color: '#222', onClick: () => callback(60) },
            { label: 'Permanent', color: '#000', onClick: () => callback(-1) }
        ]);
    }

    // === Main menu function ===
    function showMainMenu() {
        createPopup('🧱 What do you want to block?', [
            {
                label: '📋 View Block List',
                color: '#8e44ad',
                onClick: () => {
                    createPopup('📋 Current Block List', [], true);
                }
            },
            {
                label: '🟥 Entire Site',
                color: '#c0392b',
                onClick: () => {
                    const existing = checkExistingBlock('site', hostname);
                    if (existing) {
                        if (!confirm(`This site (${hostname}) is already blocked ${existing}.\n\nDo you want to update the block time?`)) {
                            return;
                        }
                    }
                    chooseDuration((minutes) => {
                        const until = minutes === -1 ? -1 : now + minutes * 60 * 1000;
                        siteBlockList[hostname] = until;
                        GM_setValue('siteBlockList', siteBlockList);
                        alert(`Site blocked ${minutes === -1 ? 'permanently' : `for ${minutes} minutes`}.`);
                        location.reload();
                    });
                }
            },
            {
                label: '🟨 This Page',
                color: '#f39c12',
                onClick: () => {
                    const existing = checkExistingBlock('page', fullUrl);
                    if (existing) {
                        if (!confirm(`This page is already blocked ${existing}.\n\nDo you want to update the block time?`)) {
                            return;
                        }
                    }
                    chooseDuration((minutes) => {
                        const until = minutes === -1 ? -1 : now + minutes * 60 * 1000;
                        pageBlockList[fullUrl] = until;
                        GM_setValue('pageBlockList', pageBlockList);
                        alert(`Page blocked ${minutes === -1 ? 'permanently' : `for ${minutes} minutes`}.`);
                        location.reload();
                    });
                }
            },
            {
                label: '🟦 Prefix Match',
                color: '#2980b9',
                onClick: () => {
                    const suggestedPrefix = fullUrl.split(/[?#]/)[0];
                    const input = prompt(`Enter prefix to block`, suggestedPrefix);
                    if (!input) return;

                    const existing = checkExistingBlock('prefix', input);
                    if (existing) {
                        if (!confirm(`This prefix (${input}) is already blocked ${existing}.\n\nDo you want to update the block time?`)) {
                            return;
                        }
                    }

                    chooseDuration((minutes) => {
                        const until = minutes === -1 ? -1 : now + minutes * 60 * 1000;
                        pagePrefixBlockList[input] = until;
                        GM_setValue('pagePrefixBlockList', pagePrefixBlockList);
                        alert(`Prefix "${input}" blocked ${minutes === -1 ? 'permanently' : `for ${minutes} minutes`}.`);
                        location.reload();
                    });
                }
            }
        ]);
    }

    // === Main click logic (separated from drag) ===
    // Click handling is now done in the mouseup event to distinguish from drag
})();