Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Neuroglancer Copy Completion Row
- // @namespace eyewire-elite
- // @version 2.0.0
- // @description Copies completion date, selected segment ID, and a clickable Neuroglancer link for Google Sheets.
- // @match https://spelunker.cave-explorer.org/*
- // @run-at document-idle
- // @grant GM_setClipboard
- // ==/UserScript==
- (function () {
- 'use strict';
- const CONFIG = Object.freeze({
- buttonId: 'nseraf-copy-completion-widget',
- positionStorageKey:
- 'nseraf-copy-completion-widget-position-v1',
- defaultPosition: {
- top: 75,
- right: 20
- },
- dragThresholdPixels: 4
- });
- let dragState = null;
- installWidget();
- // ==========================================================================
- // DRAGGABLE COPY BUTTON
- // ==========================================================================
- function installWidget() {
- if (document.getElementById(CONFIG.buttonId)) {
- return;
- }
- const widget = document.createElement('div');
- widget.id = CONFIG.buttonId;
- widget.title =
- 'Drag to reposition. Click to copy completion data.';
- Object.assign(widget.style, {
- position: 'fixed',
- zIndex: '2147483647',
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'stretch',
- minWidth: '170px',
- maxWidth: '310px',
- fontFamily: 'Arial, Helvetica, sans-serif',
- userSelect: 'none',
- touchAction: 'none'
- });
- const button = document.createElement('button');
- button.type = 'button';
- button.textContent = 'Copy completion';
- Object.assign(button.style, {
- border: '1px solid rgba(255, 255, 255, 0.65)',
- borderRadius: '7px',
- padding: '9px 13px',
- background: '#1769aa',
- color: '#ffffff',
- fontSize: '13px',
- fontWeight: 'bold',
- cursor: 'grab',
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.45)'
- });
- const message = document.createElement('div');
- Object.assign(message.style, {
- display: 'none',
- marginTop: '5px',
- padding: '7px 9px',
- borderRadius: '6px',
- background: 'rgba(0, 0, 0, 0.88)',
- color: '#ffffff',
- fontSize: '12px',
- lineHeight: '1.35',
- overflowWrap: 'anywhere',
- whiteSpace: 'pre-wrap'
- });
- widget.appendChild(button);
- widget.appendChild(message);
- document.documentElement.appendChild(widget);
- restorePosition(widget);
- keepWidgetOnScreen(widget);
- button.addEventListener(
- 'pointerdown',
- function (event) {
- beginDrag(event, widget, button);
- }
- );
- button.addEventListener(
- 'pointermove',
- function (event) {
- continueDrag(event, widget);
- }
- );
- button.addEventListener(
- 'pointerup',
- function (event) {
- finishDrag(
- event,
- widget,
- button,
- message
- );
- }
- );
- button.addEventListener(
- 'pointercancel',
- function () {
- cancelDrag(button);
- }
- );
- window.addEventListener(
- 'resize',
- function () {
- keepWidgetOnScreen(widget);
- savePosition(widget);
- }
- );
- }
- function beginDrag(event, widget, button) {
- if (
- event.pointerType === 'mouse' &&
- event.button !== 0
- ) {
- return;
- }
- const rectangle =
- widget.getBoundingClientRect();
- dragState = {
- pointerId: event.pointerId,
- startPointerX: event.clientX,
- startPointerY: event.clientY,
- startWidgetLeft: rectangle.left,
- startWidgetTop: rectangle.top,
- moved: false
- };
- button.setPointerCapture(
- event.pointerId
- );
- button.style.cursor = 'grabbing';
- event.preventDefault();
- }
- function continueDrag(event, widget) {
- if (
- !dragState ||
- event.pointerId !== dragState.pointerId
- ) {
- return;
- }
- const deltaX =
- event.clientX -
- dragState.startPointerX;
- const deltaY =
- event.clientY -
- dragState.startPointerY;
- if (
- Math.abs(deltaX) >=
- CONFIG.dragThresholdPixels ||
- Math.abs(deltaY) >=
- CONFIG.dragThresholdPixels
- ) {
- dragState.moved = true;
- }
- if (!dragState.moved) {
- return;
- }
- const rectangle =
- widget.getBoundingClientRect();
- const maximumLeft = Math.max(
- 0,
- window.innerWidth - rectangle.width
- );
- const maximumTop = Math.max(
- 0,
- window.innerHeight - rectangle.height
- );
- const nextLeft = clamp(
- dragState.startWidgetLeft + deltaX,
- 0,
- maximumLeft
- );
- const nextTop = clamp(
- dragState.startWidgetTop + deltaY,
- 0,
- maximumTop
- );
- widget.style.left =
- nextLeft + 'px';
- widget.style.top =
- nextTop + 'px';
- widget.style.right = 'auto';
- widget.style.bottom = 'auto';
- event.preventDefault();
- }
- function finishDrag(
- event,
- widget,
- button,
- message
- ) {
- if (
- !dragState ||
- event.pointerId !== dragState.pointerId
- ) {
- return;
- }
- const wasDragged =
- dragState.moved;
- try {
- button.releasePointerCapture(
- event.pointerId
- );
- } catch (error) {
- // Pointer capture may already be released.
- }
- dragState = null;
- button.style.cursor = 'grab';
- if (wasDragged) {
- keepWidgetOnScreen(widget);
- savePosition(widget);
- showMessage(
- message,
- 'Button position saved.',
- 'success'
- );
- } else {
- copyCompletionData(
- button,
- message
- );
- }
- event.preventDefault();
- }
- function cancelDrag(button) {
- dragState = null;
- button.style.cursor = 'grab';
- }
- // ==========================================================================
- // COPY COMPLETION DATA
- // ==========================================================================
- async function copyCompletionData(
- button,
- message
- ) {
- const originalText =
- button.textContent;
- try {
- button.disabled = true;
- button.textContent =
- 'Reading viewer…';
- const finalUrl =
- window.location.href;
- const segmentIds =
- extractSelectedSegmentIds(
- finalUrl
- );
- if (segmentIds.length === 0) {
- throw new Error(
- 'No selected segment was found in the current Neuroglancer URL.'
- );
- }
- if (segmentIds.length > 1) {
- throw new Error(
- 'Multiple selected segments were found: ' +
- segmentIds.join(', ') +
- '. Leave only the final segment selected and try again.'
- );
- }
- const completionDate =
- formatLocalDate(
- new Date()
- );
- const finalSegmentId =
- segmentIds[0];
- /*
- * Escape quotation marks so the URL can safely be placed inside a
- * Google Sheets HYPERLINK formula.
- */
- const safeFinalUrl =
- finalUrl.replace(
- /"/g,
- '""'
- );
- /*
- * Column M becomes a clickable hyperlink displayed as:
- *
- * https://spelunker
- */
- const hyperlinkFormula =
- '=HYPERLINK("' +
- safeFinalUrl +
- '","https://spelunker")';
- /*
- * Tab-separated values paste into adjacent Google Sheets columns:
- *
- * K = Date Complete
- * L = Final SegID
- * M = Final Link
- */
- const clipboardText = [
- completionDate,
- finalSegmentId,
- hyperlinkFormula
- ].join('\t');
- button.textContent =
- 'Copying…';
- await copyTextToClipboard(
- clipboardText
- );
- button.textContent =
- 'Copied ✓';
- showMessage(
- message,
- 'Copied:\n' +
- completionDate +
- ' | ' +
- finalSegmentId +
- '\nPaste into the row’s column K cell.',
- 'success'
- );
- } catch (error) {
- console.error(
- '[Completion script] Copy failed:',
- error
- );
- button.textContent =
- 'Copy failed';
- showMessage(
- message,
- error && error.message
- ? error.message
- : String(error),
- 'error'
- );
- } finally {
- window.setTimeout(
- function () {
- button.disabled = false;
- button.textContent =
- originalText;
- },
- 1800
- );
- }
- }
- /**
- * Extracts all selected segmentation IDs from the current URL state.
- */
- function extractSelectedSegmentIds(
- url
- ) {
- const marker = '#!';
- const markerIndex =
- url.indexOf(marker);
- if (markerIndex === -1) {
- throw new Error(
- 'The URL does not contain a Neuroglancer #! viewer state.'
- );
- }
- const encodedState =
- url.substring(
- markerIndex +
- marker.length
- );
- let decodedState;
- try {
- decodedState =
- decodeURIComponent(
- encodedState
- );
- } catch (error) {
- throw new Error(
- 'The Neuroglancer URL could not be decoded.'
- );
- }
- let viewerState;
- try {
- viewerState =
- JSON.parse(
- decodedState
- );
- } catch (error) {
- throw new Error(
- 'The decoded Neuroglancer state is not valid JSON.'
- );
- }
- if (
- !Array.isArray(
- viewerState.layers
- )
- ) {
- throw new Error(
- 'The Neuroglancer state does not contain a layer list.'
- );
- }
- const segmentIds = [];
- viewerState.layers.forEach(
- function (layer) {
- if (
- !layer ||
- layer.type !==
- 'segmentation' ||
- !Array.isArray(
- layer.segments
- )
- ) {
- return;
- }
- layer.segments.forEach(
- function (segmentId) {
- const value =
- String(
- segmentId
- ).trim();
- if (/^\d+$/.test(value)) {
- segmentIds.push(value);
- }
- }
- );
- }
- );
- return Array.from(
- new Set(segmentIds)
- );
- }
- /**
- * Produces dates such as 8/5/2026 using the computer's local date.
- */
- function formatLocalDate(date) {
- return [
- date.getMonth() + 1,
- date.getDate(),
- date.getFullYear()
- ].join('/');
- }
- async function copyTextToClipboard(
- text
- ) {
- if (
- typeof GM_setClipboard ===
- 'function'
- ) {
- GM_setClipboard(
- text,
- 'text'
- );
- return;
- }
- if (
- navigator.clipboard &&
- typeof navigator.clipboard
- .writeText === 'function'
- ) {
- await navigator.clipboard
- .writeText(text);
- return;
- }
- const temporaryTextArea =
- document.createElement(
- 'textarea'
- );
- temporaryTextArea.value =
- text;
- temporaryTextArea.setAttribute(
- 'readonly',
- ''
- );
- Object.assign(
- temporaryTextArea.style,
- {
- position: 'fixed',
- left: '-9999px',
- top: '0'
- }
- );
- document.body.appendChild(
- temporaryTextArea
- );
- temporaryTextArea.select();
- const copied =
- document.execCommand(
- 'copy'
- );
- temporaryTextArea.remove();
- if (!copied) {
- throw new Error(
- 'The browser refused clipboard access.'
- );
- }
- }
- // ==========================================================================
- // SAVED BUTTON POSITION
- // ==========================================================================
- function savePosition(widget) {
- const rectangle =
- widget.getBoundingClientRect();
- const position = {
- left: Math.round(
- rectangle.left
- ),
- top: Math.round(
- rectangle.top
- )
- };
- try {
- localStorage.setItem(
- CONFIG.positionStorageKey,
- JSON.stringify(position)
- );
- } catch (error) {
- console.warn(
- '[Completion script] Could not save button position:',
- error
- );
- }
- }
- function restorePosition(widget) {
- let restoredPosition = null;
- try {
- const storedValue =
- localStorage.getItem(
- CONFIG.positionStorageKey
- );
- if (storedValue) {
- restoredPosition =
- JSON.parse(storedValue);
- }
- } catch (error) {
- console.warn(
- '[Completion script] Could not restore button position:',
- error
- );
- }
- if (
- restoredPosition &&
- Number.isFinite(
- restoredPosition.left
- ) &&
- Number.isFinite(
- restoredPosition.top
- )
- ) {
- widget.style.left =
- restoredPosition.left +
- 'px';
- widget.style.top =
- restoredPosition.top +
- 'px';
- widget.style.right =
- 'auto';
- return;
- }
- widget.style.top =
- CONFIG.defaultPosition.top +
- 'px';
- widget.style.right =
- CONFIG.defaultPosition.right +
- 'px';
- }
- function keepWidgetOnScreen(widget) {
- const rectangle =
- widget.getBoundingClientRect();
- const maximumLeft = Math.max(
- 0,
- window.innerWidth -
- rectangle.width
- );
- const maximumTop = Math.max(
- 0,
- window.innerHeight -
- rectangle.height
- );
- const nextLeft = clamp(
- rectangle.left,
- 0,
- maximumLeft
- );
- const nextTop = clamp(
- rectangle.top,
- 0,
- maximumTop
- );
- widget.style.left =
- nextLeft + 'px';
- widget.style.top =
- nextTop + 'px';
- widget.style.right =
- 'auto';
- widget.style.bottom =
- 'auto';
- }
- function showMessage(
- messageElement,
- text,
- type
- ) {
- messageElement.textContent =
- text;
- messageElement.style.background =
- type === 'error'
- ? 'rgba(165, 20, 20, 0.95)'
- : 'rgba(0, 105, 45, 0.95)';
- messageElement.style.display =
- 'block';
- window.clearTimeout(
- showMessage.timeoutId
- );
- showMessage.timeoutId =
- window.setTimeout(
- function () {
- messageElement.style.display =
- 'none';
- },
- 6000
- );
- }
- function clamp(
- value,
- minimum,
- maximum
- ) {
- return Math.min(
- Math.max(
- value,
- minimum
- ),
- maximum
- );
- }
- })();
Advertisement
Add Comment
Please, Sign In to add comment