plexora 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. plexora/__init__.py +129 -0
  2. plexora/__main__.py +15 -0
  3. plexora/_url.py +105 -0
  4. plexora/api/__init__.py +55 -0
  5. plexora/api/dataset.py +398 -0
  6. plexora/api/http.py +19 -0
  7. plexora/api/plugin.py +541 -0
  8. plexora/api/store.py +221 -0
  9. plexora/cli.py +777 -0
  10. plexora/client/dist/354_bundle.js +2 -0
  11. plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
  12. plexora/client/dist/418_bundle.js +2 -0
  13. plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
  14. plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
  15. plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
  16. plexora/client/dist/770_bundle.js +1 -0
  17. plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
  18. plexora/client/dist/vendor_bundle.js +2 -0
  19. plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
  20. plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
  21. plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
  22. plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
  23. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
  24. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
  25. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
  26. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
  27. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
  28. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
  29. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
  30. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
  31. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
  32. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
  33. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
  34. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
  35. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
  36. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
  37. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
  38. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
  39. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
  40. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
  41. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
  42. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
  43. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
  44. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
  45. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
  46. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
  47. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
  48. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
  49. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
  50. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
  51. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
  52. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
  53. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
  54. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
  55. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
  56. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
  57. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
  58. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
  59. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
  60. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
  61. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
  62. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
  63. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
  64. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
  65. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
  66. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
  67. plexora/client/src/css/import.css +474 -0
  68. plexora/client/src/css/main.css +903 -0
  69. plexora/client/src/css/openProject.css +363 -0
  70. plexora/client/src/css/quickView.css +166 -0
  71. plexora/client/src/css/tokens.css +51 -0
  72. plexora/client/src/css/viewer.css +1655 -0
  73. plexora/client/src/img/apple-touch-icon.png +0 -0
  74. plexora/client/src/img/favicon.ico +0 -0
  75. plexora/client/src/img/logo.ai +8892 -33
  76. plexora/client/src/img/logo.svg +1 -0
  77. plexora/client/src/img/logo_with_text.ai +9011 -34
  78. plexora/client/src/img/logo_with_text.svg +1 -0
  79. plexora/client/src/js/main.js +704 -0
  80. plexora/client/src/js/pluginRegistry.js +108 -0
  81. plexora/client/src/js/services/appStatus.js +286 -0
  82. plexora/client/src/js/services/browsePicker.js +73 -0
  83. plexora/client/src/js/services/dataLayer.js +400 -0
  84. plexora/client/src/js/services/datasetContext.js +143 -0
  85. plexora/client/src/js/services/glRenderer.js +143 -0
  86. plexora/client/src/js/services/importFormValidation.js +257 -0
  87. plexora/client/src/js/services/numericData.js +100 -0
  88. plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
  89. plexora/client/src/js/services/simpleEventHandler.js +25 -0
  90. plexora/client/src/js/vendor.js +29 -0
  91. plexora/client/src/js/views/channelList.js +663 -0
  92. plexora/client/src/js/views/colorSwatchPicker.js +202 -0
  93. plexora/client/src/js/views/columnClassifier.js +146 -0
  94. plexora/client/src/js/views/coordinateField.js +133 -0
  95. plexora/client/src/js/views/dataSourceField.js +276 -0
  96. plexora/client/src/js/views/imageViewer.js +3306 -0
  97. plexora/client/src/js/views/miniMap.js +787 -0
  98. plexora/client/src/js/views/navbarControls.js +178 -0
  99. plexora/client/src/js/views/openProjectPage.js +239 -0
  100. plexora/client/src/js/views/projectEdit.js +335 -0
  101. plexora/client/src/js/views/quickViewLanding.js +136 -0
  102. plexora/client/src/js/views/rainbow.js +326 -0
  103. plexora/client/src/js/views/requirementsModal.js +572 -0
  104. plexora/client/src/js/views/rgbImageViewer.js +115 -0
  105. plexora/client/src/js/views/searchableSelect.js +411 -0
  106. plexora/client/src/js/views/segmentationProgress.js +122 -0
  107. plexora/client/src/js/views/toolLoader.js +747 -0
  108. plexora/client/src/js/views/viewerControls.js +689 -0
  109. plexora/client/src/js/views/viewerManager.js +434 -0
  110. plexora/client/src/js/views/viewerSidebar.js +1117 -0
  111. plexora/client/src/js/workers/tileDecoder.js +130 -0
  112. plexora/client/src/shaders/frag.glsl +528 -0
  113. plexora/client/src/shaders/vert.glsl +12 -0
  114. plexora/client/templates/base.html +212 -0
  115. plexora/client/templates/index.html +238 -0
  116. plexora/client/templates/open_project.html +96 -0
  117. plexora/client/templates/project_columns.html +86 -0
  118. plexora/client/templates/project_edit.html +196 -0
  119. plexora/client/templates/upload.html +118 -0
  120. plexora/connect.py +528 -0
  121. plexora/datasource.py +782 -0
  122. plexora/jupyter.py +406 -0
  123. plexora/notebook_env.py +232 -0
  124. plexora/paths.py +486 -0
  125. plexora/plugins/__init__.py +12 -0
  126. plexora/plugins/cell_explorer/__init__.py +100 -0
  127. plexora/plugins/cell_explorer/server/__init__.py +11 -0
  128. plexora/plugins/cell_explorer/server/routes.py +167 -0
  129. plexora/plugins/cell_explorer/server/state.py +281 -0
  130. plexora/plugins/cell_explorer/server/values.py +145 -0
  131. plexora/plugins/cell_explorer/server/variables.py +367 -0
  132. plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
  133. plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
  134. plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
  135. plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
  136. plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
  137. plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
  138. plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
  139. plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
  140. plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
  141. plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
  142. plexora/plugins/figure_builder/__init__.py +125 -0
  143. plexora/plugins/figure_builder/server/__init__.py +7 -0
  144. plexora/plugins/figure_builder/server/compose.py +365 -0
  145. plexora/plugins/figure_builder/server/export.py +784 -0
  146. plexora/plugins/figure_builder/server/export_jobs.py +148 -0
  147. plexora/plugins/figure_builder/server/operations.py +556 -0
  148. plexora/plugins/figure_builder/server/pixels.py +142 -0
  149. plexora/plugins/figure_builder/server/provenance.py +174 -0
  150. plexora/plugins/figure_builder/server/render.py +278 -0
  151. plexora/plugins/figure_builder/server/repository.py +701 -0
  152. plexora/plugins/figure_builder/server/routes.py +594 -0
  153. plexora/plugins/figure_builder/server/schema.py +1023 -0
  154. plexora/plugins/figure_builder/server/sources.py +120 -0
  155. plexora/plugins/figure_builder/server/textmetrics.py +155 -0
  156. plexora/plugins/figure_builder/static/figureActions.js +363 -0
  157. plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
  158. plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
  159. plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
  160. plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
  161. plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
  162. plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
  163. plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
  164. plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
  165. plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
  166. plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
  167. plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
  168. plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
  169. plexora/plugins/figure_builder/static/figureRichText.js +634 -0
  170. plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
  171. plexora/plugins/figure_builder/static/figureSchema.js +218 -0
  172. plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
  173. plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
  174. plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
  175. plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
  176. plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
  177. plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
  178. plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
  179. plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
  180. plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
  181. plexora/plugins/gating/__init__.py +76 -0
  182. plexora/plugins/gating/server/__init__.py +0 -0
  183. plexora/plugins/gating/server/anndata_gates.py +367 -0
  184. plexora/plugins/gating/server/database.py +16 -0
  185. plexora/plugins/gating/server/model.py +320 -0
  186. plexora/plugins/gating/server/routes.py +235 -0
  187. plexora/plugins/gating/static/csvGatingList.js +947 -0
  188. plexora/plugins/gating/static/gating.css +289 -0
  189. plexora/plugins/gating/static/gatingApi.js +221 -0
  190. plexora/plugins/gating/static/gatingSidebarController.js +513 -0
  191. plexora/plugins/gating/templates/gating/legacy.html +26 -0
  192. plexora/plugins/gating/templates/gating/panel.html +69 -0
  193. plexora/plugins/roi/__init__.py +95 -0
  194. plexora/plugins/roi/server/__init__.py +8 -0
  195. plexora/plugins/roi/server/adapters.py +655 -0
  196. plexora/plugins/roi/server/geojson.py +267 -0
  197. plexora/plugins/roi/server/geometry.py +181 -0
  198. plexora/plugins/roi/server/mapping.py +169 -0
  199. plexora/plugins/roi/server/operations.py +293 -0
  200. plexora/plugins/roi/server/repository.py +215 -0
  201. plexora/plugins/roi/server/routes.py +409 -0
  202. plexora/plugins/roi/server/schema.py +323 -0
  203. plexora/plugins/roi/static/roi.css +583 -0
  204. plexora/plugins/roi/static/roiApi.js +177 -0
  205. plexora/plugins/roi/static/roiFigureBridge.js +107 -0
  206. plexora/plugins/roi/static/roiGeometry.js +350 -0
  207. plexora/plugins/roi/static/roiRenderer.js +278 -0
  208. plexora/plugins/roi/static/roiSidebarController.js +1038 -0
  209. plexora/plugins/roi/static/roiState.js +518 -0
  210. plexora/plugins/roi/static/roiTools.js +993 -0
  211. plexora/plugins/roi/templates/roi/panel.html +240 -0
  212. plexora/proxy.py +57 -0
  213. plexora/server/models/adapters/__init__.py +94 -0
  214. plexora/server/models/adapters/anndata_adapter.py +388 -0
  215. plexora/server/models/adapters/base.py +93 -0
  216. plexora/server/models/adapters/classify.py +192 -0
  217. plexora/server/models/adapters/csv_adapter.py +105 -0
  218. plexora/server/models/adapters/inspection.py +265 -0
  219. plexora/server/models/adapters/spatialdata_adapter.py +248 -0
  220. plexora/server/models/centroid_tiles.py +305 -0
  221. plexora/server/models/data_model.py +1659 -0
  222. plexora/server/models/database_model.py +169 -0
  223. plexora/server/models/project.py +1270 -0
  224. plexora/server/plugins.py +199 -0
  225. plexora/server/routes/browse_routes.py +41 -0
  226. plexora/server/routes/data_routes.py +294 -0
  227. plexora/server/routes/import_routes.py +603 -0
  228. plexora/server/routes/page_routes.py +139 -0
  229. plexora/server/routes/project_routes.py +438 -0
  230. plexora/server/routes/quick_view_routes.py +62 -0
  231. plexora/server/routes/system_routes.py +39 -0
  232. plexora/server/routes/tool_routes.py +418 -0
  233. plexora/server/utils/addHEColumns.py +11 -0
  234. plexora/server/utils/fast_png.py +97 -0
  235. plexora/server/utils/fullConversion.py +50 -0
  236. plexora/server/utils/native_dialog.py +168 -0
  237. plexora/server/utils/pre_normalization.py +47 -0
  238. plexora/server/utils/segmentation_pyramid.py +598 -0
  239. plexora/server/utils/smallestenclosingcircle.py +126 -0
  240. plexora/server/utils/tiffsurgeon.py +372 -0
  241. plexora/server_cli.py +48 -0
  242. plexora/test.py +0 -0
  243. plexora-0.0.1.dist-info/METADATA +341 -0
  244. plexora-0.0.1.dist-info/RECORD +248 -0
  245. plexora-0.0.1.dist-info/WHEEL +5 -0
  246. plexora-0.0.1.dist-info/entry_points.txt +12 -0
  247. plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
  248. plexora-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * navbarControls.js
3
+ *
4
+ * Wires the unified File/Edit/Tools/View navbar (base.html) to existing
5
+ * viewer functionality. Plain global script (not a module), loaded on every
6
+ * page -- every handler below is written with optional chaining so it
7
+ * no-ops cleanly on pages that don't have a viewer/sidebar at all (quick-look
8
+ * home, upload wizard, datasource config).
9
+ */
10
+ (function () {
11
+ function onReady(fn) {
12
+ if (document.readyState === "loading") {
13
+ document.addEventListener("DOMContentLoaded", fn);
14
+ } else {
15
+ fn();
16
+ }
17
+ }
18
+
19
+ onReady(() => {
20
+ // File > Add Image -- opens a native file picker, then registers the
21
+ // picked image the same way the home page's Quick Look flow does.
22
+ document.getElementById("nav_add_image")?.addEventListener("click", async () => {
23
+ await browseForPath({
24
+ mode: "file",
25
+ filter: "image",
26
+ onPicked: async (path) => {
27
+ try {
28
+ const response = await fetch(plexoraUrl("quick_view"), {
29
+ method: "POST",
30
+ headers: {"Content-Type": "application/json"},
31
+ body: JSON.stringify({path}),
32
+ });
33
+ const result = await response.json();
34
+ if (result.success) {
35
+ window.location.href = result.redirect;
36
+ } else {
37
+ alert(result.error || "Could not load that image.");
38
+ }
39
+ } catch (error) {
40
+ alert("Could not load that image.");
41
+ }
42
+ },
43
+ onUnavailable: () => alert("Automatic file browsing isn't available on this machine."),
44
+ });
45
+ });
46
+
47
+ // File > Export Image submenu -- hover/focus reveals it via CSS
48
+ // (see main.css); this click handler is only the touch/keyboard
49
+ // fallback for devices without hover. Ignore clicks that originated
50
+ // on the PNG/PDF buttons themselves, so they run their own handler
51
+ // below and close the dropdown normally instead of re-toggling.
52
+ const exportMenu = document.getElementById("nav_export_menu");
53
+ exportMenu?.addEventListener("click", (event) => {
54
+ if (event.target.closest(".nav-submenu")) return;
55
+ exportMenu.classList.toggle("open");
56
+ });
57
+ document.getElementById("nav_export_image_png")?.addEventListener("click", () => {
58
+ window.__plexora?.seaDragonViewer?.downloadCurrentView?.("png");
59
+ });
60
+ document.getElementById("nav_export_image_pdf")?.addEventListener("click", () => {
61
+ window.__plexora?.seaDragonViewer?.downloadCurrentView?.("pdf");
62
+ });
63
+
64
+ // File > Quit -- terminates the local server process (see
65
+ // system_routes.py); the fetch is expected to error out as the
66
+ // process exits mid-response, so that's swallowed rather than surfaced.
67
+ document.getElementById("nav_quit")?.addEventListener("click", async () => {
68
+ const confirmed = confirm("Quit Plexora? This will close the local server and this page will stop working.");
69
+ if (!confirmed) return;
70
+ try {
71
+ await fetch(plexoraUrl("shutdown"), { method: "POST", keepalive: true });
72
+ } catch (error) {
73
+ // Expected.
74
+ }
75
+ document.body.innerHTML = `
76
+ <div class="plexora-quit-overlay">
77
+ <span class="fas fa-power-off plexora-quit-icon"></span>
78
+ <h1>Plexora has quit</h1>
79
+ <p>The local server has stopped. You can close this tab.</p>
80
+ </div>`;
81
+ });
82
+
83
+ const sidebarToggle = document.getElementById("nav_toggle_sidebar");
84
+ const scalebarToggle = document.getElementById("nav_toggle_scalebar");
85
+ const hdToggle = document.getElementById("nav_toggle_hd");
86
+
87
+ const sidebarShell = document.getElementById("bodyDiv");
88
+ const sidebarCollapseButton = document.getElementById("sidebar_collapse_button");
89
+ const hdEl = document.getElementById("viewer_controls_hd");
90
+
91
+ // View > Show Sidebar -- reuses the existing collapse/expand toggle
92
+ // (viewerSidebar.js) rather than duplicating the collapse logic; synced
93
+ // from the shell's class each time the View menu opens, since the
94
+ // sidebar has no change event of its own to listen for.
95
+ sidebarToggle?.addEventListener("change", () => sidebarCollapseButton?.click());
96
+ document.getElementById("navbarViewDropdown")?.addEventListener("show.bs.dropdown", () => {
97
+ if (sidebarToggle && sidebarShell) {
98
+ sidebarToggle.checked = !sidebarShell.classList.contains("sidebar-collapsed");
99
+ }
100
+ });
101
+
102
+ // View > Show Scalebar -- sole owner of this state, no sidebar counterpart.
103
+ scalebarToggle?.addEventListener("change", (e) => {
104
+ window.__plexora?.seaDragonViewer?.setScalebarVisible?.(e.target.checked);
105
+ });
106
+
107
+ // View > HD Mode -- two-way mirror against the sidebar checkbox. The
108
+ // write direction sets the sidebar checkbox and dispatches "change" on
109
+ // it, so all the real work (setHdMode) stays in viewerControls.js --
110
+ // nothing is duplicated here. The read direction listens for the
111
+ // plexora:*-changed event that handler dispatches.
112
+ function wireMirror(navEl, sidebarEl, eventName) {
113
+ if (!navEl || !sidebarEl) return;
114
+ navEl.addEventListener("change", () => {
115
+ sidebarEl.checked = navEl.checked;
116
+ sidebarEl.dispatchEvent(new Event("change", { bubbles: true }));
117
+ });
118
+ window.addEventListener(eventName, (e) => {
119
+ navEl.checked = Boolean(e.detail?.enabled);
120
+ });
121
+ }
122
+ wireMirror(hdToggle, hdEl, "plexora:hd-mode-changed");
123
+
124
+ // View > Cells -- the same one-of-four choice the sidebar offers, and
125
+ // the same single implementation behind it: this hands the mode to
126
+ // ViewerControls rather than reproducing any of the loading work.
127
+ //
128
+ // Which options are usable is a property of the project (is there a
129
+ // mask? does it store whole labels? are there coordinates?), which lives
130
+ // in the config the viewer holds. Read on open rather than pushed here,
131
+ // because this script binds on DOMContentLoaded and the config arrives
132
+ // later -- an availability event fired at init would land before anyone
133
+ // is listening.
134
+ const cellModeRadios = Array.from(
135
+ document.querySelectorAll('input[name="nav_cell_mode"]'));
136
+
137
+ cellModeRadios.forEach((radio) => {
138
+ radio.addEventListener("change", () => {
139
+ if (!radio.checked) return;
140
+ const controls = window.__plexora?.viewerControls;
141
+ if (!controls) return;
142
+ // A menu click is as much a decision as a sidebar click, and
143
+ // must equally outrank whatever the automatic fallback chose.
144
+ if (window.__plexora?.seaDragonViewer) {
145
+ window.__plexora.seaDragonViewer.centroidsFromFallback = false;
146
+ }
147
+ controls.selectMode(radio.value);
148
+ });
149
+ });
150
+
151
+ // Mirrors what the sidebar control OFFERS, not merely what the project
152
+ // can draw: with a plugin layer active the choice is narrowed to the
153
+ // modes that plugin uses, and "No Cells" goes away entirely -- the
154
+ // plugin's own card is what turns its layer off. A menu that kept
155
+ // offering the full four would be a second, disagreeing answer to the
156
+ // same question.
157
+ function syncCellMode() {
158
+ const controls = window.__plexora?.viewerControls;
159
+ if (!controls || !cellModeRadios.length) return;
160
+ const offered = controls.offeredModes();
161
+ const available = controls.availability();
162
+ cellModeRadios.forEach((radio) => {
163
+ const usable = Boolean(offered[radio.value]);
164
+ radio.checked = radio.value === controls.mode;
165
+ radio.disabled = !usable;
166
+ // Same rule as the sidebar buttons: a mode this PROJECT cannot
167
+ // draw stays visible and disabled (that is a fact worth seeing),
168
+ // a mode the active PLUGIN does not use is hidden.
169
+ const item = radio.closest(".nav-check-item");
170
+ if (item) item.hidden = !usable && Boolean(available[radio.value]);
171
+ });
172
+ }
173
+
174
+ window.addEventListener("plexora:cell-mode-changed", syncCellMode);
175
+ document.getElementById("navbarViewDropdown")
176
+ ?.addEventListener("show.bs.dropdown", syncCellMode);
177
+ });
178
+ })();
@@ -0,0 +1,239 @@
1
+ /**
2
+ * openProjectPage.js
3
+ *
4
+ * Drives the Open Project page (open_project.html): fetches the project
5
+ * list once, then does search/sort/grid-list rendering entirely client-side.
6
+ * Loaded only on that page (not globally, unlike navbarControls.js).
7
+ */
8
+ (function () {
9
+ function onReady(fn) {
10
+ if (document.readyState === "loading") {
11
+ document.addEventListener("DOMContentLoaded", fn);
12
+ } else {
13
+ fn();
14
+ }
15
+ }
16
+
17
+ function escapeHtml(value) {
18
+ return String(value).replace(/[&<>"']/g, (c) => ({
19
+ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
20
+ }[c]));
21
+ }
22
+
23
+ function timeAgo(iso) {
24
+ if (!iso) return "";
25
+ const then = new Date(iso).getTime();
26
+ if (Number.isNaN(then)) return "";
27
+ const diffSec = Math.round((Date.now() - then) / 1000);
28
+ if (diffSec < 60) return "just now";
29
+ const diffMin = Math.round(diffSec / 60);
30
+ if (diffMin < 60) return diffMin + (diffMin === 1 ? " minute ago" : " minutes ago");
31
+ const diffHour = Math.round(diffMin / 60);
32
+ if (diffHour < 24) return diffHour + (diffHour === 1 ? " hour ago" : " hours ago");
33
+ const diffDay = Math.round(diffHour / 24);
34
+ if (diffDay < 30) return diffDay + (diffDay === 1 ? " day ago" : " days ago");
35
+ return new Date(iso).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
36
+ }
37
+
38
+ onReady(() => {
39
+ const resultsEl = document.getElementById("project-results");
40
+ if (!resultsEl) return; // Not on the Open Project page.
41
+
42
+ const searchInput = document.getElementById("project-search");
43
+ const sortSelect = document.getElementById("project-sort");
44
+ const gridButton = document.getElementById("project-view-grid");
45
+ const listButton = document.getElementById("project-view-list");
46
+ const countEl = document.getElementById("project-count");
47
+ const emptyStateEl = document.getElementById("project-empty-state");
48
+ const noResultsEl = document.getElementById("project-no-results");
49
+
50
+ const state = {
51
+ projects: [],
52
+ query: "",
53
+ sort: "lastOpenedAt",
54
+ view: localStorage.getItem("plexora.openProjectView") === "list" ? "list" : "grid",
55
+ };
56
+
57
+ function dateCaption(project) {
58
+ if (state.sort === "createdAt") {
59
+ return project.createdAt ? "Created " + timeAgo(project.createdAt) : "";
60
+ }
61
+ return project.lastOpenedAt ? "Opened " + timeAgo(project.lastOpenedAt) : "Never opened";
62
+ }
63
+
64
+ function thumbMarkup(project) {
65
+ const thumbUrl = plexoraUrl("project_thumbnail/" + encodeURIComponent(project.name));
66
+ return `<span class="project-thumb">
67
+ <img src="${thumbUrl}" alt="" loading="lazy" onerror="this.parentElement.classList.add('project-thumb-fallback');this.remove();">
68
+ <span class="fas fa-image project-thumb-icon"></span>
69
+ </span>`;
70
+ }
71
+
72
+ function sharedMarkup(project) {
73
+ // Only on shared projects, so an install with no shared roots --
74
+ // which is every single-user one -- looks exactly as it did.
75
+ if (!project.shared) return "";
76
+ return `<span class="project-shared" title="On a shared data directory: open and explore it, but it cannot be edited or deleted here."><span class="fas fa-users"></span>Shared</span>`;
77
+ }
78
+
79
+ function actionsMarkup(project) {
80
+ const editUrl = plexoraUrl("edit_config/" + encodeURIComponent(project.name));
81
+ const name = escapeHtml(project.name);
82
+ // A shared project belongs to whoever provisioned the root it sits
83
+ // on. The server refuses both of these with a 403 regardless; not
84
+ // offering them is what stops the user finding that out only after
85
+ // confirming a delete dialog.
86
+ if (project.shared) return "";
87
+ // Delete carries only the project name -- the request itself is
88
+ // built and sent as a POST when the modal is confirmed. It used to
89
+ // be a plain link to GET /delete/<name>, an irreversible rmtree any
90
+ // crawler, prefetcher or stale bookmark could follow.
91
+ return `<span class="project-actions">
92
+ <a href="${editUrl}" class="project-action" title="Edit"><span class="fas fa-pencil"></span></a>
93
+ <a href="#" class="project-action project-action-danger" title="Delete"
94
+ data-bs-toggle="modal" data-bs-target="#deleteProjectModal"
95
+ data-project-name="${name}"><span class="fas fa-trash"></span></a>
96
+ </span>`;
97
+ }
98
+
99
+ function cardMarkup(project) {
100
+ const href = plexoraUrl(encodeURIComponent(project.name));
101
+ const name = escapeHtml(project.name);
102
+ const caption = escapeHtml(dateCaption(project));
103
+ return `<div class="project-card">
104
+ <a class="project-card-link" href="${href}" title="${name}">
105
+ ${thumbMarkup(project)}
106
+ <span class="project-card-name">${name}</span>
107
+ <span class="project-card-date">${caption}${sharedMarkup(project)}</span>
108
+ </a>
109
+ ${actionsMarkup(project)}
110
+ </div>`;
111
+ }
112
+
113
+ function rowMarkup(project) {
114
+ const href = plexoraUrl(encodeURIComponent(project.name));
115
+ const name = escapeHtml(project.name);
116
+ const caption = escapeHtml(dateCaption(project));
117
+ return `<div class="project-row">
118
+ <a class="project-row-link" href="${href}" title="${name}">
119
+ ${thumbMarkup(project)}
120
+ <span class="project-row-name">${name}</span>
121
+ <span class="project-row-date">${caption}${sharedMarkup(project)}</span>
122
+ </a>
123
+ ${actionsMarkup(project)}
124
+ </div>`;
125
+ }
126
+
127
+ function filteredSorted() {
128
+ const query = state.query.trim().toLowerCase();
129
+ let list = query
130
+ ? state.projects.filter((p) => p.name.toLowerCase().includes(query))
131
+ : state.projects.slice();
132
+
133
+ if (state.sort === "name") {
134
+ list.sort((a, b) => a.name.localeCompare(b.name));
135
+ } else {
136
+ list.sort((a, b) => {
137
+ const aTime = a[state.sort] ? new Date(a[state.sort]).getTime() : 0;
138
+ const bTime = b[state.sort] ? new Date(b[state.sort]).getTime() : 0;
139
+ return bTime - aTime;
140
+ });
141
+ }
142
+ return list;
143
+ }
144
+
145
+ function render() {
146
+ const total = state.projects.length;
147
+ countEl.textContent = total === 1 ? "1 project" : total + " projects";
148
+
149
+ if (total === 0) {
150
+ resultsEl.innerHTML = "";
151
+ emptyStateEl.hidden = false;
152
+ noResultsEl.hidden = true;
153
+ return;
154
+ }
155
+ emptyStateEl.hidden = true;
156
+
157
+ const list = filteredSorted();
158
+ resultsEl.className = "project-results " + (state.view === "grid" ? "project-grid" : "project-list");
159
+
160
+ if (list.length === 0) {
161
+ resultsEl.innerHTML = "";
162
+ noResultsEl.hidden = false;
163
+ return;
164
+ }
165
+ noResultsEl.hidden = true;
166
+ resultsEl.innerHTML = list.map(state.view === "grid" ? cardMarkup : rowMarkup).join("");
167
+ }
168
+
169
+ function updateViewButtons() {
170
+ gridButton.classList.toggle("active", state.view === "grid");
171
+ listButton.classList.toggle("active", state.view === "list");
172
+ }
173
+
174
+ function setView(view) {
175
+ state.view = view;
176
+ localStorage.setItem("plexora.openProjectView", view);
177
+ updateViewButtons();
178
+ render();
179
+ }
180
+
181
+ searchInput.addEventListener("input", () => {
182
+ state.query = searchInput.value;
183
+ render();
184
+ });
185
+ sortSelect.addEventListener("change", () => {
186
+ state.sort = sortSelect.value;
187
+ render();
188
+ });
189
+ gridButton.addEventListener("click", () => setView("grid"));
190
+ listButton.addEventListener("click", () => setView("list"));
191
+
192
+ const deleteModalName = document.getElementById("deleteProjectModalName");
193
+ const deleteModalConfirm = document.getElementById("deleteProjectModalConfirm");
194
+ let pendingDelete = null;
195
+
196
+ document.getElementById("deleteProjectModal")?.addEventListener("show.bs.modal", (event) => {
197
+ pendingDelete = event.relatedTarget.dataset.projectName;
198
+ deleteModalName.textContent = pendingDelete;
199
+ });
200
+
201
+ // The dialog closes on its own -- the button carries data-bs-dismiss, so
202
+ // bootstrap's data API handles it and this never has to reach for a
203
+ // global that the ES-module build does not define. Closing on the click
204
+ // rather than on the response is also what the old code meant to do: it
205
+ // hid in a `finally`, so the outcome never gated it either way. The
206
+ // status chip is where success and failure are reported.
207
+ deleteModalConfirm?.addEventListener("click", async () => {
208
+ if (!pendingDelete) return;
209
+ const name = pendingDelete;
210
+ pendingDelete = null;
211
+ const task = window.PlexoraStatus?.begin("Deleting");
212
+ try {
213
+ const response = await fetch(
214
+ plexoraUrl(`project/${encodeURIComponent(name)}/delete`),
215
+ { method: "POST" },
216
+ );
217
+ if (!response.ok) throw new Error("delete failed");
218
+ task?.done();
219
+ state.projects = state.projects.filter((p) => p.name !== name);
220
+ render();
221
+ } catch (e) {
222
+ task?.fail("Delete failed");
223
+ }
224
+ });
225
+
226
+ updateViewButtons();
227
+
228
+ fetch(plexoraUrl("projects"))
229
+ .then((response) => response.json())
230
+ .then((data) => {
231
+ state.projects = Array.isArray(data) ? data : [];
232
+ render();
233
+ })
234
+ .catch(() => {
235
+ state.projects = [];
236
+ render();
237
+ });
238
+ });
239
+ })();