Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Neuroglancer Copy Completion Row
- // @namespace eyewire-elite
- // @version 2.2.0.annkri
- // @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 USERNAME = 'annkri'
- 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 v = unsafeWindow.viewer;
- if (!v) {
- throw new Error(
- 'The global "viewer" object was not found. Make sure you are in the correct frame/window.'
- );
- }
- // Extract segment IDs from the current long URL before shortening
- const currentUrl = window.location.href;
- const segmentIds = extractSelectedSegmentIds(currentUrl);
- 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 status = 'Complete (cut off)';
- const completionDate = formatLocalDate(new Date());
- const finalSegmentId = segmentIds[0];
- button.textContent = 'Saving state…';
- const state = v.state.toJSON();
- // Determine state server endpoint
- let stateServerUrl =
- state.jsonStateServer ||
- v.jsonStateServer ||
- 'https://global.daf-apis.com/nglstate/api/v1/post';
- // Helper to identify a 32-character CAVE/DAF token
- const findTokenInValue = (val) => {
- if (!val) return null;
- val = val.trim();
- try {
- const parsed = JSON.parse(val);
- if (typeof parsed === 'string') {
- val = parsed;
- } else if (typeof parsed === 'object' && parsed !== null) {
- for (const k in parsed) {
- const subVal = parsed[k];
- if (
- typeof subVal === 'string' &&
- /^[0-9a-f]{32}$/i.test(subVal.trim())
- ) {
- return subVal.trim();
- }
- }
- }
- } catch {}
- val = val.replace(/^["']|["']$/g, '').trim();
- return /^[0-9a-f]{32}$/i.test(val) ? val : null;
- };
- // Scan storage to locate an active token
- const scanForToken = () => {
- const keys = [
- 'token',
- 'middleauth_token',
- 'cave-token',
- 'cave_token',
- 'api_token',
- 'secret',
- 'cave_secret',
- ];
- for (const key of keys) {
- const tok =
- findTokenInValue(localStorage.getItem(key)) ||
- findTokenInValue(sessionStorage.getItem(key));
- if (tok) return tok;
- }
- for (let i = 0; i < localStorage.length; i++) {
- const tok = findTokenInValue(
- localStorage.getItem(localStorage.key(i))
- );
- if (tok) return tok;
- }
- for (let i = 0; i < sessionStorage.length; i++) {
- const tok = findTokenInValue(
- sessionStorage.getItem(sessionStorage.key(i))
- );
- if (tok) return tok;
- }
- return null;
- };
- let token = scanForToken();
- const uploadState = async (authToken) => {
- const headers = { 'Content-Type': 'application/json' };
- if (authToken) {
- headers.Authorization = `Bearer ${authToken}`;
- }
- const response = await fetch(stateServerUrl, {
- method: 'POST',
- headers: headers,
- body: JSON.stringify(state),
- });
- return response;
- };
- let response = await uploadState(token);
- // Prompt user if the state server rejects authorization
- if (response.status === 401) {
- const isFlywire = stateServerUrl.includes('flywire');
- const tokenPortalUrl = isFlywire
- ? 'https://globalv1.flywire-daf.com/auth/api/v1/user/token'
- : 'https://global.daf-apis.com/auth/api/v1/user/token';
- const userToken = prompt(
- `An API token is required to save states to this server.\n\n1. Please visit:\n${tokenPortalUrl}\n2. Copy the token\n3. Paste it here:`,
- token || ''
- );
- if (!userToken) {
- throw new Error('API token is required to generate the short link.');
- }
- token = userToken.trim();
- // Persist token so user doesn't need to re-enter it next time
- if (/^[0-9a-f]{32}$/i.test(token)) {
- localStorage.setItem('cave_token', token);
- }
- response = await uploadState(token);
- }
- if (!response.ok) {
- throw new Error(
- `State server responded with status: ${response.status}`
- );
- }
- // Parse the state ID
- const contentType = response.headers.get('content-type');
- let stateId;
- if (contentType && contentType.includes('application/json')) {
- stateId = await response.json();
- }
- // Format the final shortened URL
- let fetchEndpoint = stateServerUrl.replace(/\/post\/?$/, '');
- let finalUrl;
- if (
- window.location.origin.includes('flywire') ||
- stateServerUrl.includes('flywire')
- ) {
- finalUrl = `https://ngl.flywire.ai/?json_url=${fetchEndpoint}/${stateId}`;
- } else {
- finalUrl = `https://spelunker.cave-explorer.org/#!middleauth+${stateId}`;
- }
- /*
- * Escape quotation marks so the URL can safely be placed inside a
- * Google Sheets HYPERLINK formula.
- */
- const safeFinalUrl = finalUrl.replace(/"/g, '""');
- /*
- * Tab-separated values paste into adjacent Google Sheets columns:
- *
- * K = Date Complete
- * L = Final SegID
- * M = Final Link
- */
- const clipboardText = [
- USERNAME,
- status,
- completionDate,
- finalSegmentId,
- safeFinalUrl,
- ].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