Guest User

Untitled

a guest
Aug 28th, 2026
30
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.51 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Neuroglancer Auto Import Settings
  3. // @namespace eyewire-elite
  4. // @version 2.2.0
  5. // @description Safely auto-imports named Neuroglancer presets on newly opened links after the viewer stabilizes, but never on refresh.
  6. // @match https://spelunker.cave-explorer.org/*
  7. // @run-at document-idle
  8. // @grant unsafeWindow
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13.  
  14.  
  15. const CONFIG = Object.freeze({
  16. /*
  17. * API exposed by the matched Import/export settings script.
  18. */
  19. importerApiName:
  20. 'NGSettingsPresets',
  21.  
  22. /*
  23. * New three-slot storage.
  24. */
  25. settingsStorageKey:
  26. 'ng-settings-3-slots',
  27.  
  28. /*
  29. * Never import immediately.
  30. */
  31. minimumWaitMilliseconds:
  32. 3000,
  33.  
  34. /*
  35. * Viewer state must remain unchanged
  36. * for this long.
  37. */
  38. requiredStableMilliseconds:
  39. 2500,
  40.  
  41. /*
  42. * How often readiness is checked.
  43. */
  44. checkIntervalMilliseconds:
  45. 250,
  46.  
  47. /*
  48. * Stop waiting for the actual viewer
  49. * after this amount of time.
  50. */
  51. maximumWaitMilliseconds:
  52. 45000,
  53.  
  54. /*
  55. * Once the viewer is stable, give the
  56. * Import/Export userscript a little extra
  57. * time to expose its API if necessary.
  58. */
  59. importerApiWaitMilliseconds:
  60. 5000
  61. });
  62.  
  63.  
  64. // ==========================================================================
  65. // RELOAD PROTECTION
  66. // ==========================================================================
  67.  
  68. /*
  69. * Never auto-import after:
  70. *
  71. * - F5
  72. * - Ctrl+F5
  73. * - Ctrl+R
  74. * - browser Reload button
  75. *
  76. * A genuinely newly opened link is allowed.
  77. */
  78. if (
  79. isReloadNavigation()
  80. ) {
  81. console.log(
  82. '[Auto import] Page reload detected. Automatic settings import skipped.'
  83. );
  84.  
  85. return;
  86. }
  87.  
  88.  
  89. // ==========================================================================
  90. // SAVED-PRESET CHECK
  91. // ==========================================================================
  92.  
  93. /*
  94. * Look specifically for the new three-slot store.
  95. *
  96. * The Import/Export script itself handles migration
  97. * from old ng-settings if needed.
  98. */
  99. if (
  100. !hasSavedPresets()
  101. ) {
  102. console.log(
  103. '[Auto import] No saved named presets found. Viewer will remain unchanged.'
  104. );
  105.  
  106. return;
  107. }
  108.  
  109.  
  110. const startedAt =
  111. performance.now();
  112.  
  113. let lastSnapshot =
  114. null;
  115.  
  116. let stableSince =
  117. null;
  118.  
  119. let importTriggered =
  120. false;
  121.  
  122.  
  123. const intervalId =
  124. window.setInterval(
  125. checkViewer,
  126. CONFIG
  127. .checkIntervalMilliseconds
  128. );
  129.  
  130.  
  131. // ==========================================================================
  132. // NAVIGATION TYPE
  133. // ==========================================================================
  134.  
  135. function isReloadNavigation() {
  136. try {
  137. const entries =
  138. performance
  139. .getEntriesByType(
  140. 'navigation'
  141. );
  142.  
  143. if (
  144. Array.isArray(entries) &&
  145. entries.length > 0
  146. ) {
  147. return (
  148. entries[0].type ===
  149. 'reload'
  150. );
  151. }
  152.  
  153. } catch (error) {
  154. console.warn(
  155. '[Auto import] Could not read Navigation Timing data:',
  156. error
  157. );
  158. }
  159.  
  160.  
  161. try {
  162. if (
  163. performance.navigation &&
  164. performance.navigation.type === 1
  165. ) {
  166. return true;
  167. }
  168.  
  169. } catch (_) {
  170. // Ignore old-browser fallback failure.
  171. }
  172.  
  173.  
  174. return false;
  175. }
  176.  
  177.  
  178. // ==========================================================================
  179. // SAVED PRESETS
  180. // ==========================================================================
  181.  
  182. function hasSavedPresets() {
  183. try {
  184. const text =
  185. localStorage.getItem(
  186. CONFIG
  187. .settingsStorageKey
  188. );
  189.  
  190. if (!text) {
  191. /*
  192. * The three-slot script may not yet have had a
  193. * chance to migrate the old single ng-settings
  194. * value during this page load.
  195. *
  196. * If the legacy key exists, allow Auto Import
  197. * to continue waiting. The importer will migrate it.
  198. */
  199. return Boolean(
  200. localStorage.getItem(
  201. 'ng-settings'
  202. )
  203. );
  204. }
  205.  
  206.  
  207. const parsed =
  208. JSON.parse(text);
  209.  
  210.  
  211. return Boolean(
  212. parsed &&
  213. Array.isArray(
  214. parsed.slots
  215. ) &&
  216. parsed.slots.some(
  217. slot =>
  218. Boolean(
  219. slot &&
  220. slot.settings
  221. )
  222. )
  223. );
  224.  
  225. } catch (error) {
  226. console.warn(
  227. '[Auto import] Could not inspect saved presets:',
  228. error
  229. );
  230.  
  231. return false;
  232. }
  233. }
  234.  
  235.  
  236. // ==========================================================================
  237. // VIEWER STABILITY
  238. // ==========================================================================
  239.  
  240. function checkViewer() {
  241. if (
  242. importTriggered
  243. ) {
  244. window.clearInterval(
  245. intervalId
  246. );
  247.  
  248. return;
  249. }
  250.  
  251.  
  252. const now =
  253. performance.now();
  254.  
  255.  
  256. const totalElapsed =
  257. now -
  258. startedAt;
  259.  
  260.  
  261. if (
  262. totalElapsed >=
  263. CONFIG.maximumWaitMilliseconds
  264. ) {
  265. window.clearInterval(
  266. intervalId
  267. );
  268.  
  269.  
  270. console.warn(
  271. '[Auto import] Viewer did not reach a stable state within ' +
  272. Math.round(
  273. CONFIG.maximumWaitMilliseconds /
  274. 1000
  275. ) +
  276. ' seconds. Automatic import skipped; press / manually.'
  277. );
  278.  
  279.  
  280. return;
  281. }
  282.  
  283.  
  284. if (
  285. totalElapsed <
  286. CONFIG.minimumWaitMilliseconds
  287. ) {
  288. return;
  289. }
  290.  
  291.  
  292. const pageViewer =
  293. getPageViewer();
  294.  
  295.  
  296. if (
  297. !isViewerReady(
  298. pageViewer
  299. )
  300. ) {
  301. resetStability();
  302.  
  303. return;
  304. }
  305.  
  306.  
  307. if (
  308. isLoginPromptVisible()
  309. ) {
  310. resetStability();
  311.  
  312. return;
  313. }
  314.  
  315.  
  316. let snapshot;
  317.  
  318.  
  319. try {
  320. snapshot =
  321. createMeaningfulSnapshot(
  322. pageViewer
  323. );
  324.  
  325. } catch (_) {
  326. resetStability();
  327.  
  328. return;
  329. }
  330.  
  331.  
  332. if (
  333. snapshot !==
  334. lastSnapshot
  335. ) {
  336. lastSnapshot =
  337. snapshot;
  338.  
  339. stableSince =
  340. now;
  341.  
  342. return;
  343. }
  344.  
  345.  
  346. if (
  347. stableSince === null
  348. ) {
  349. stableSince =
  350. now;
  351.  
  352. return;
  353. }
  354.  
  355.  
  356. const stableElapsed =
  357. now -
  358. stableSince;
  359.  
  360.  
  361. if (
  362. stableElapsed >=
  363. CONFIG.requiredStableMilliseconds
  364. ) {
  365. /*
  366. * Stop the stability monitor BEFORE calling
  367. * the importer so it can never trigger twice.
  368. */
  369. importTriggered =
  370. true;
  371.  
  372.  
  373. window.clearInterval(
  374. intervalId
  375. );
  376.  
  377.  
  378. console.log(
  379. '[Auto import] Viewer stabilized after ' +
  380. (
  381. totalElapsed /
  382. 1000
  383. ).toFixed(2) +
  384. ' seconds.'
  385. );
  386.  
  387.  
  388. callImporterWhenReady();
  389. }
  390. }
  391.  
  392.  
  393. // ==========================================================================
  394. // DIRECT IMPORTER API
  395. // ==========================================================================
  396.  
  397. async function callImporterWhenReady() {
  398. const deadline =
  399. performance.now() +
  400. CONFIG
  401. .importerApiWaitMilliseconds;
  402.  
  403.  
  404. let api =
  405. getImporterApi();
  406.  
  407.  
  408. while (
  409. !api &&
  410. performance.now() <
  411. deadline
  412. ) {
  413. await sleep(100);
  414.  
  415. api =
  416. getImporterApi();
  417. }
  418.  
  419.  
  420. if (
  421. !api ||
  422. typeof api.importSettings !==
  423. 'function'
  424. ) {
  425. console.warn(
  426. '[Auto import] Import/export preset API was not available. Press / manually.'
  427. );
  428.  
  429. return;
  430. }
  431.  
  432.  
  433. let presets =
  434. [];
  435.  
  436.  
  437. try {
  438. if (
  439. typeof api.getPresets ===
  440. 'function'
  441. ) {
  442. presets =
  443. api.getPresets() ||
  444. [];
  445. }
  446.  
  447. } catch (error) {
  448. console.warn(
  449. '[Auto import] Could not read preset list:',
  450. error
  451. );
  452. }
  453.  
  454.  
  455. if (
  456. presets.length === 0
  457. ) {
  458. console.log(
  459. '[Auto import] No presets remain to import.'
  460. );
  461.  
  462. return;
  463. }
  464.  
  465.  
  466. if (
  467. presets.length === 1
  468. ) {
  469. console.log(
  470. '[Auto import] One preset exists. Automatically importing "' +
  471. presets[0].name +
  472. '".'
  473. );
  474.  
  475. } else {
  476. console.log(
  477. '[Auto import] ' +
  478. presets.length +
  479. ' presets exist. Asking which state to import.'
  480. );
  481. }
  482.  
  483.  
  484. /*
  485. * IMPORTANT:
  486. *
  487. * Direct API call.
  488. *
  489. * We DO NOT synthesize the "/" key anymore.
  490. * This means exactly one restore operation is requested.
  491. */
  492. try {
  493. const result =
  494. api.importSettings({
  495. source:
  496. 'auto',
  497.  
  498. quietWhenEmpty:
  499. true
  500. });
  501.  
  502.  
  503. if (
  504. result &&
  505. result.ok
  506. ) {
  507. console.log(
  508. '[Auto import] Settings import completed from "' +
  509. result.name +
  510. '".'
  511. );
  512.  
  513. } else {
  514. console.log(
  515. '[Auto import] No automatic settings import was performed.',
  516. result
  517. );
  518. }
  519.  
  520. } catch (error) {
  521. console.error(
  522. '[Auto import] Import API call failed:',
  523. error
  524. );
  525. }
  526. }
  527.  
  528.  
  529. function getImporterApi() {
  530. /*
  531. * The Import/Export script uses @grant none,
  532. * so its API exists in the actual page window.
  533. */
  534. try {
  535. if (
  536. typeof unsafeWindow !==
  537. 'undefined' &&
  538. unsafeWindow &&
  539. unsafeWindow[
  540. CONFIG.importerApiName
  541. ]
  542. ) {
  543. return unsafeWindow[
  544. CONFIG.importerApiName
  545. ];
  546. }
  547.  
  548. } catch (_) {
  549. // Continue to window fallback.
  550. }
  551.  
  552.  
  553. try {
  554. return (
  555. window[
  556. CONFIG.importerApiName
  557. ] ||
  558. null
  559. );
  560.  
  561. } catch (_) {
  562. return null;
  563. }
  564. }
  565.  
  566.  
  567. // ==========================================================================
  568. // VIEWER ACCESS
  569. // ==========================================================================
  570.  
  571. function getPageViewer() {
  572. try {
  573. if (
  574. typeof unsafeWindow !==
  575. 'undefined' &&
  576. unsafeWindow &&
  577. unsafeWindow.viewer
  578. ) {
  579. return unsafeWindow.viewer;
  580. }
  581.  
  582. } catch (_) {
  583. // Continue.
  584. }
  585.  
  586.  
  587. try {
  588. if (
  589. window.viewer
  590. ) {
  591. return window.viewer;
  592. }
  593.  
  594. } catch (_) {
  595. // Viewer not ready.
  596. }
  597.  
  598.  
  599. return null;
  600. }
  601.  
  602.  
  603. function isViewerReady(
  604. pageViewer
  605. ) {
  606. try {
  607. if (
  608. !pageViewer ||
  609. !pageViewer.state ||
  610. typeof pageViewer
  611. .state
  612. .toJSON !==
  613. 'function'
  614. ) {
  615. return false;
  616. }
  617.  
  618.  
  619. const state =
  620. pageViewer
  621. .state
  622. .toJSON();
  623.  
  624.  
  625. if (
  626. !state ||
  627. !Array.isArray(
  628. state.layers
  629. ) ||
  630. state.layers.length === 0
  631. ) {
  632. return false;
  633. }
  634.  
  635.  
  636. const hasSegmentation =
  637. state.layers.some(
  638. layer =>
  639. Boolean(
  640. layer &&
  641. layer.type ===
  642. 'segmentation' &&
  643. layer.source
  644. )
  645. );
  646.  
  647.  
  648. if (
  649. !hasSegmentation
  650. ) {
  651. return false;
  652. }
  653.  
  654.  
  655. const topBar =
  656. document.querySelector(
  657. '.neuroglancer-viewer-top-row'
  658. );
  659.  
  660.  
  661. const renderedPanel =
  662. document.querySelector(
  663. '.neuroglancer-rendered-data-panel, canvas'
  664. );
  665.  
  666.  
  667. return Boolean(
  668. topBar &&
  669. renderedPanel
  670. );
  671.  
  672. } catch (_) {
  673. return false;
  674. }
  675. }
  676.  
  677.  
  678. // ==========================================================================
  679. // STABILITY SNAPSHOT
  680. // ==========================================================================
  681.  
  682. function createMeaningfulSnapshot(
  683. pageViewer
  684. ) {
  685. const state =
  686. pageViewer
  687. .state
  688. .toJSON();
  689.  
  690.  
  691. const layers =
  692. state.layers.map(
  693. layer => {
  694. const result = {
  695. name:
  696. layer.name ||
  697. '',
  698.  
  699. type:
  700. layer.type ||
  701. '',
  702.  
  703. source:
  704. normalizeSource(
  705. layer.source
  706. ),
  707.  
  708. visible:
  709. Object.prototype
  710. .hasOwnProperty.call(
  711. layer,
  712. 'visible'
  713. )
  714. ? layer.visible
  715. : null
  716. };
  717.  
  718.  
  719. if (
  720. layer.type ===
  721. 'segmentation'
  722. ) {
  723. result.segments =
  724. Array.isArray(
  725. layer.segments
  726. )
  727. ? layer.segments.map(
  728. String
  729. )
  730. : [];
  731.  
  732.  
  733. result.segmentCount =
  734. result
  735. .segments
  736. .length;
  737. }
  738.  
  739.  
  740. if (
  741. layer.type ===
  742. 'annotation'
  743. ) {
  744. result.annotationCount =
  745. Array.isArray(
  746. layer.annotations
  747. )
  748. ? layer
  749. .annotations
  750. .length
  751. : 0;
  752. }
  753.  
  754.  
  755. return result;
  756. }
  757. );
  758.  
  759.  
  760. const snapshot = {
  761. url:
  762. window.location.href,
  763.  
  764. dimensions:
  765. Object.prototype
  766. .hasOwnProperty.call(
  767. state,
  768. 'dimensions'
  769. )
  770. ? state.dimensions
  771. : null,
  772.  
  773. position:
  774. Object.prototype
  775. .hasOwnProperty.call(
  776. state,
  777. 'position'
  778. )
  779. ? state.position
  780. : null,
  781.  
  782. crossSectionScale:
  783. Object.prototype
  784. .hasOwnProperty.call(
  785. state,
  786. 'crossSectionScale'
  787. )
  788. ? state.crossSectionScale
  789. : null,
  790.  
  791. projectionScale:
  792. Object.prototype
  793. .hasOwnProperty.call(
  794. state,
  795. 'projectionScale'
  796. )
  797. ? state.projectionScale
  798. : null,
  799.  
  800. projectionDepth:
  801. Object.prototype
  802. .hasOwnProperty.call(
  803. state,
  804. 'projectionDepth'
  805. )
  806. ? state.projectionDepth
  807. : null,
  808.  
  809. projectionOrientation:
  810. Object.prototype
  811. .hasOwnProperty.call(
  812. state,
  813. 'projectionOrientation'
  814. )
  815. ? state.projectionOrientation
  816. : null,
  817.  
  818. layerCount:
  819. layers.length,
  820.  
  821. layers,
  822.  
  823. canvasCount:
  824. document.querySelectorAll(
  825. 'canvas'
  826. ).length,
  827.  
  828. renderedPanelCount:
  829. document.querySelectorAll(
  830. '.neuroglancer-rendered-data-panel'
  831. ).length
  832. };
  833.  
  834.  
  835. return JSON.stringify(
  836. snapshot
  837. );
  838. }
  839.  
  840.  
  841. function normalizeSource(
  842. source
  843. ) {
  844. if (
  845. source === undefined ||
  846. source === null
  847. ) {
  848. return null;
  849. }
  850.  
  851.  
  852. if (
  853. typeof source ===
  854. 'string'
  855. ) {
  856. return source;
  857. }
  858.  
  859.  
  860. try {
  861. return JSON.stringify(
  862. source
  863. );
  864.  
  865. } catch (_) {
  866. return String(
  867. source
  868. );
  869. }
  870. }
  871.  
  872.  
  873. // ==========================================================================
  874. // LOGIN / STABILITY HELPERS
  875. // ==========================================================================
  876.  
  877. function isLoginPromptVisible() {
  878. const text =
  879. document.body
  880. ? document.body
  881. .innerText
  882. .toLowerCase()
  883. : '';
  884.  
  885.  
  886. return (
  887. text.includes(
  888. 'login required'
  889. ) ||
  890. text.includes(
  891. 'request login'
  892. )
  893. );
  894. }
  895.  
  896.  
  897. function resetStability() {
  898. lastSnapshot =
  899. null;
  900.  
  901. stableSince =
  902. null;
  903. }
  904.  
  905.  
  906. function sleep(
  907. milliseconds
  908. ) {
  909. return new Promise(
  910. resolve => {
  911. window.setTimeout(
  912. resolve,
  913. milliseconds
  914. );
  915. }
  916. );
  917. }
  918.  
  919. })();
Add Comment
Please, Sign In to add comment