Created
February 11, 2026 03:05
-
-
Save StuartJAtkinson/49fe5439c01f317e9b8fcef65c235d33 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Bluesky Map Circle Hover Collector (Exhaustive + High Readability) | |
| // @namespace http://tampermonkey.net/ | |
| // @version 2026-02-11 | |
| // @description Collect Bluesky handles from canvas using a circle radius with exhaustive hover, and enable high readability | |
| // @match https://bluesky-map.theo.io/ | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| /* ---------------- Auto-enable High Readability ---------------- */ | |
| function enableHighReadability() { | |
| const cb = document.getElementById('setting-high-readability'); | |
| if (cb && !cb.checked) { | |
| cb.click(); | |
| console.log('β High readability enabled'); | |
| } | |
| } | |
| window.addEventListener('load', enableHighReadability); | |
| /* ---------------- Variables ---------------- */ | |
| let selecting = false; | |
| let dragging = false; | |
| let centerX = 0, centerY = 0; | |
| let radius = 0; | |
| let circle, overlay, canvas; | |
| const handles = new Set(); | |
| /* ---------------- UI ---------------- */ | |
| const panel = document.createElement('div'); | |
| panel.id = 'hover-collector-panel'; | |
| panel.style.cssText = ` | |
| position: fixed; top: 20px; right: 20px; z-index: 999999; | |
| background: #fff; color: #000; padding: 8px 10px; | |
| font-family: system-ui, sans-serif; border: 1px solid #ccc; | |
| border-radius: 6px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| display: flex; flex-direction: column; gap: 6px; | |
| min-width: 220px; | |
| `; | |
| // Buttons | |
| const btnStart = document.createElement('button'); | |
| const btnFinish = document.createElement('button'); | |
| const btnRedo = document.createElement('button'); | |
| btnStart.textContent = 'Scan Select'; | |
| btnFinish.textContent = 'Scan'; | |
| btnRedo.textContent = 'Reset'; | |
| btnFinish.disabled = true; | |
| btnRedo.disabled = true; | |
| // Row container for buttons | |
| const buttonRow = document.createElement('div'); | |
| buttonRow.style.cssText = 'display:flex; gap:6px;'; | |
| [btnStart, btnFinish, btnRedo].forEach(b => { | |
| b.style.cssText = ` | |
| background: #f5f5f5; color: #000; border: 1px solid #ccc; | |
| padding: 4px 8px; cursor: pointer; font-size: 13px; flex:1; | |
| `; | |
| b.addEventListener('mouseenter', () => b.style.background = '#e0e0e0'); | |
| b.addEventListener('mouseleave', () => b.style.background = '#f5f5f5'); | |
| buttonRow.appendChild(b); | |
| }); | |
| panel.appendChild(buttonRow); | |
| // Handles list styled like #selections | |
| const listEl = document.createElement('div'); | |
| listEl.style.cssText = ` | |
| margin-top:6px; max-height:200px; overflow:auto; | |
| width:100%; font-size:13px; line-height:1.3; | |
| `; | |
| panel.appendChild(listEl); | |
| document.body.appendChild(panel); | |
| /* ---------------- Overlay & Circle ---------------- */ | |
| overlay = document.createElement('div'); | |
| overlay.style.cssText = ` | |
| position: fixed; inset:0; cursor: crosshair; z-index: 999998; display:none; | |
| `; | |
| document.body.appendChild(overlay); | |
| circle = document.createElement('div'); | |
| circle.style.cssText = ` | |
| position:absolute; border:2px dashed #4af; | |
| background: rgba(100,150,255,0.15); pointer-events:none; display:none; z-index:999999; | |
| border-radius: 50%; | |
| `; | |
| overlay.appendChild(circle); | |
| function findCanvas() { | |
| canvas = document.querySelector('canvas'); | |
| if (!canvas) console.warn('β οΈ Canvas not found yet.'); | |
| } | |
| findCanvas(); | |
| /* ---------------- Helpers ---------------- */ | |
| function renderList() { | |
| listEl.innerHTML = [...handles] | |
| .map(h => `<div style="padding:2px 0;"><a href="https://bsky.app/profile/${h.replace(/^@/, '')}" target="_blank" style=" | |
| text-decoration:none; color:#3578e5; | |
| ">${h}</a></div>`) | |
| .join(''); | |
| } | |
| function clearSelection() { | |
| circle.style.display = 'none'; | |
| handles.clear(); | |
| renderList(); | |
| btnFinish.disabled = true; | |
| btnRedo.disabled = true; | |
| selecting = false; | |
| dragging = false; | |
| overlay.style.display = 'none'; | |
| btnStart.disabled = false; | |
| } | |
| /* ---------------- Drag Logic ---------------- */ | |
| overlay.addEventListener('mousedown', e => { | |
| if (!selecting || e.button !== 0) return; | |
| dragging = true; | |
| centerX = e.clientX; | |
| centerY = e.clientY; | |
| radius = 0; | |
| circle.style.width = '0px'; | |
| circle.style.height = '0px'; | |
| circle.style.left = centerX + 'px'; | |
| circle.style.top = centerY + 'px'; | |
| circle.style.display = 'block'; | |
| console.log('π’ Drag start', centerX, centerY); | |
| }); | |
| overlay.addEventListener('mousemove', e => { | |
| if (!dragging) return; | |
| const dx = e.clientX - centerX; | |
| const dy = e.clientY - centerY; | |
| radius = Math.sqrt(dx*dx + dy*dy); | |
| circle.style.width = radius*2 + 'px'; | |
| circle.style.height = radius*2 + 'px'; | |
| circle.style.left = centerX - radius + 'px'; | |
| circle.style.top = centerY - radius + 'px'; | |
| }); | |
| overlay.addEventListener('mouseup', e => { | |
| dragging = false; | |
| btnFinish.disabled = false; | |
| btnRedo.disabled = false; | |
| console.log('π’ Drag end, radius:', radius); | |
| }); | |
| /* ---------------- Button Logic ---------------- */ | |
| btnStart.onclick = () => { | |
| findCanvas(); | |
| if (!canvas) return alert('Canvas not found yet, try again after map loads.'); | |
| clearSelection(); | |
| selecting = true; | |
| overlay.style.display = 'block'; | |
| btnStart.disabled = true; | |
| }; | |
| btnRedo.onclick = btnStart.onclick; | |
| /* ---------------- Exhaustive Hover Collection (Circle) ---------------- */ | |
| async function exhaustiveHoverCircle(step = 5) { | |
| if (!canvas) findCanvas(); | |
| if (!canvas) return; | |
| console.log(`π’ Starting exhaustive circle hover with step ${step}px`); | |
| const minX = centerX - radius; | |
| const maxX = centerX + radius; | |
| const minY = centerY - radius; | |
| const maxY = centerY + radius; | |
| for (let y = minY; y <= maxY; y += step) { | |
| for (let x = minX; x <= maxX; x += step) { | |
| const dx = x - centerX; | |
| const dy = y - centerY; | |
| if (dx*dx + dy*dy <= radius*radius) { | |
| try { | |
| canvas.dispatchEvent(new MouseEvent('mousemove', { clientX: x, clientY: y, bubbles: true })); | |
| canvas.dispatchEvent(new PointerEvent('pointermove', { clientX: x, clientY: y, bubbles: true })); | |
| await new Promise(r => setTimeout(r, 5)); | |
| document.querySelectorAll('.tt-handle').forEach(h => { | |
| const txt = h.textContent.trim(); | |
| if (txt && !handles.has(txt)) { | |
| console.log('β Collected:', txt, 'at', x, y); | |
| handles.add(txt); | |
| } | |
| }); | |
| } catch (err) { | |
| console.error('β Hover dispatch error at', x, y, err); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| btnFinish.onclick = async () => { | |
| console.log('π’ Finish clicked, center:', centerX, centerY, 'radius:', radius); | |
| handles.clear(); | |
| renderList(); | |
| try { | |
| await exhaustiveHoverCircle(5); // 5px step for full coverage | |
| } catch (err) { | |
| console.error('β Error in exhaustiveHoverCircle', err); | |
| } | |
| renderList(); | |
| selecting = false; | |
| overlay.style.display = 'none'; | |
| btnStart.disabled = false; | |
| btnFinish.disabled = true; | |
| btnRedo.disabled = true; | |
| console.log(`π’ Done. Collected ${handles.size} handles`); | |
| }; | |
| console.log('π’ Bluesky Map Circle Hover Collector (Exhaustive + High Readability) ready'); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment