您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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 })();