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,174 @@
1
+ /**
2
+ * FigureExportUi - the export dialog, and following the job it starts.
3
+ *
4
+ * Two things this does that a plain "download" button does not, and both are
5
+ * the point:
6
+ *
7
+ * **It asks before it renders.** The preflight comes back with the effective
8
+ * resolution of every panel and with anything the export cannot reproduce, and
9
+ * that is shown while the dialog is still open. A warning attached to a file
10
+ * that is already written is a warning nobody acts on.
11
+ *
12
+ * **It shows what it is doing.** An eighteen-panel figure at 600 DPI is
13
+ * minutes. A spinner with no panel count is indistinguishable from a hang, and
14
+ * a user who cannot tell those apart reloads the page.
15
+ *
16
+ * Cancelling is a real answer at any point: an export is a render, not an edit,
17
+ * so stopping one changes nothing about the figure and leaves nothing
18
+ * half-written behind.
19
+ */
20
+ class FigureExportUi {
21
+
22
+ /** How often the job is asked how it is getting on. Fast enough that the
23
+ * panel counter moves, slow enough not to be a request per frame. */
24
+ static get POLL_MS() { return 700; }
25
+
26
+ constructor(options) {
27
+ this.api = options.api;
28
+ this.figureId = options.figureId;
29
+ this.state = options.state;
30
+ this.jobId = null;
31
+ this.timer = null;
32
+ }
33
+
34
+ el(id) {
35
+ return document.getElementById(id);
36
+ }
37
+
38
+ setup() {
39
+ this.el("fb_export_open")?.addEventListener("click", () => this.open());
40
+ this.el("fb_export_cancel")?.addEventListener("click", () => this.close());
41
+ this.el("fb_export_start")?.addEventListener("click", () => this.start());
42
+ this.el("fb_export_stop")?.addEventListener("click", () => this.stop());
43
+ for (const id of ["fb_export_format", "fb_export_dpi"]) {
44
+ this.el(id)?.addEventListener("change", () => this.preflight());
45
+ }
46
+ }
47
+
48
+ open() {
49
+ const dialog = this.el("fb_export_dialog");
50
+ if (!dialog) return;
51
+ const dpi = this.el("fb_export_dpi");
52
+ if (dpi && !dpi.value) dpi.value = String(this.state.document.settings.dpi_default);
53
+ this.setPhase("idle");
54
+ dialog.showModal?.();
55
+ this.preflight();
56
+ }
57
+
58
+ close() {
59
+ this.stopPolling();
60
+ this.el("fb_export_dialog")?.close?.();
61
+ }
62
+
63
+ options() {
64
+ return {
65
+ format: this.el("fb_export_format")?.value || "pdf",
66
+ dpi: Number(this.el("fb_export_dpi")?.value || 300),
67
+ provenance: this.el("fb_export_provenance")?.checked !== false,
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Ask what this export would be like, before rendering any of it.
73
+ *
74
+ * Re-run on every change of format or DPI, because the answer depends on
75
+ * both: the same figure is fine at 150 and enlarged past its source at 600.
76
+ */
77
+ async preflight() {
78
+ const target = this.el("fb_export_warnings");
79
+ if (!target) return;
80
+ target.innerHTML = `<div class="fb-muted">Checking sources…</div>`;
81
+
82
+ const result = await this.api.preflightExport(this.figureId, this.options());
83
+ if (!result.ok) {
84
+ target.innerHTML = `<div class="fb-banner-detail">${FigureSchema.escapeHtml(
85
+ result.data.error || "This figure could not be checked.")}</div>`;
86
+ return;
87
+ }
88
+ const warnings = result.data.warnings || [];
89
+ if (!warnings.length) {
90
+ target.innerHTML = `<div class="fb-muted">${
91
+ FigureSchema.countPhrase(result.data.panels, "panel")} ready at ${
92
+ result.data.dpi} DPI.</div>`;
93
+ return;
94
+ }
95
+ target.innerHTML = warnings.map((warning) =>
96
+ `<div class="fb-banner fb-banner-warning">
97
+ <span class="fas fa-triangle-exclamation"></span>
98
+ <div><div class="fb-banner-detail">${FigureSchema.escapeHtml(warning.message)}</div></div>
99
+ </div>`).join("");
100
+ }
101
+
102
+ async start() {
103
+ this.setPhase("running", "Starting…");
104
+ const result = await this.api.startExport(this.figureId, this.options());
105
+ if (!result.ok) {
106
+ this.setPhase("failed", result.data.error || "This figure could not be exported.");
107
+ return;
108
+ }
109
+ this.jobId = result.data.job_id;
110
+ this.poll();
111
+ }
112
+
113
+ async stop() {
114
+ if (!this.jobId) return;
115
+ await this.api.cancelExport(this.figureId, this.jobId);
116
+ // Not marked cancelled here: the job reports its own state, and
117
+ // claiming it stopped before it has would be a message that is
118
+ // sometimes wrong.
119
+ }
120
+
121
+ stopPolling() {
122
+ if (this.timer) window.clearTimeout(this.timer);
123
+ this.timer = null;
124
+ }
125
+
126
+ async poll() {
127
+ if (!this.jobId) return;
128
+ const result = await this.api.exportStatus(this.figureId, this.jobId);
129
+ if (result.status === 501) {
130
+ this.setPhase("failed", result.data.detail
131
+ || "This build cannot write that format.");
132
+ return;
133
+ }
134
+ if (!result.ok) {
135
+ this.setPhase("failed", "The export could not be followed.");
136
+ return;
137
+ }
138
+ const job = result.data.job;
139
+ if (job.status === "running") {
140
+ this.setPhase("running", job.progress.message
141
+ + (job.progress.total > 1
142
+ ? ` (${job.progress.done} of ${job.progress.total})` : ""));
143
+ this.timer = window.setTimeout(() => this.poll(), FigureExportUi.POLL_MS);
144
+ return;
145
+ }
146
+ this.stopPolling();
147
+ if (job.status === "cancelled") {
148
+ this.setPhase("idle", "Cancelled. Nothing was written.");
149
+ } else if (job.status === "done") {
150
+ this.setPhase("done", FigureSchema.countPhrase(job.result.panels, "panel")
151
+ + " rendered.");
152
+ const link = this.el("fb_export_download");
153
+ if (link) {
154
+ link.href = this.api.exportDownloadUrl(this.figureId, this.jobId);
155
+ link.hidden = false;
156
+ }
157
+ } else {
158
+ this.setPhase("failed", job.error || "The export failed.");
159
+ }
160
+ }
161
+
162
+ setPhase(phase, message) {
163
+ const status = this.el("fb_export_status");
164
+ if (status) status.textContent = message || "";
165
+ for (const [id, phases] of [
166
+ ["fb_export_start", ["idle", "done", "failed"]],
167
+ ["fb_export_stop", ["running"]],
168
+ ["fb_export_download", ["done"]],
169
+ ]) {
170
+ const element = this.el(id);
171
+ if (element) element.hidden = !phases.includes(phase);
172
+ }
173
+ }
174
+ }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * FigureLibrary - the Figures page.
3
+ *
4
+ * Fetches the list once and does search and rendering client-side, exactly as
5
+ * core's Open Project page does, because it is the same interaction on the same
6
+ * furniture. Self-boots when its root element is present and does nothing
7
+ * anywhere else: this plugin's scripts also load on the viewer page and on a
8
+ * figure's own page, and a controller that assumed its DOM would throw on both.
9
+ *
10
+ * A figure whose database could not be read is rendered as a card rather than
11
+ * omitted. That is the whole reason the server reports `readable: false`
12
+ * instead of dropping the row: the damaged figure is precisely the one the user
13
+ * needs to see, and a list that silently skips it presents "damaged" as
14
+ * "deleted".
15
+ */
16
+ class FigureLibrary {
17
+
18
+ constructor(options) {
19
+ this.api = (options && options.api) || new FigureBuilderApi();
20
+ this.figures = [];
21
+ this.query = "";
22
+ this.pendingDelete = null;
23
+
24
+ this.resultsEl = document.getElementById("fb_library_results");
25
+ this.countEl = document.getElementById("fb_library_count");
26
+ this.emptyEl = document.getElementById("fb_library_empty");
27
+ this.noResultsEl = document.getElementById("fb_library_no_results");
28
+ this.searchEl = document.getElementById("fb_library_search");
29
+ }
30
+
31
+ static boot() {
32
+ if (!document.getElementById("fb_library_results")) return null;
33
+ const library = new FigureLibrary();
34
+ library.setup();
35
+ return library;
36
+ }
37
+
38
+ setup() {
39
+ // Being on this page means the next figure opened was opened from
40
+ // HERE, so the viewer's "you came from a slide" note is out of date and
41
+ // the figure page's back arrow should point at this list again. Cleared
42
+ // rather than left to be ignored: the note is keyed by figure, and
43
+ // reopening the same figure from here would otherwise still find it.
44
+ try {
45
+ window.sessionStorage.removeItem("plexora:figure-builder-origin");
46
+ } catch (error) {
47
+ /* Private-browsing modes throw; the arrow just keeps its default. */
48
+ }
49
+
50
+ this.searchEl?.addEventListener("input", () => {
51
+ this.query = this.searchEl.value;
52
+ this.render();
53
+ });
54
+
55
+ document.getElementById("fb_library_create")
56
+ ?.addEventListener("click", () => this.createFigure());
57
+ document.getElementById("fb_library_create_empty")
58
+ ?.addEventListener("click", () => this.createFigure());
59
+
60
+ // Delegated, because every card is re-rendered on every keystroke in
61
+ // the search box -- handlers bound to the cards themselves would be
62
+ // rebound hundreds of times and leak the ones that were replaced.
63
+ this.resultsEl.addEventListener("click", (event) => this.onCardClick(event));
64
+
65
+ const modal = document.getElementById("fb_delete_modal");
66
+ modal?.addEventListener("show.bs.modal", (event) => {
67
+ this.pendingDelete = event.relatedTarget?.dataset.figureId || null;
68
+ const nameEl = document.getElementById("fb_delete_modal_name");
69
+ const figure = this.figures.find((f) => f.figure_id === this.pendingDelete);
70
+ if (nameEl) nameEl.textContent = figure ? figure.title : "this figure";
71
+ });
72
+ document.getElementById("fb_delete_modal_confirm")
73
+ ?.addEventListener("click", () => this.confirmDelete());
74
+
75
+ this.refresh();
76
+ }
77
+
78
+ async refresh() {
79
+ const result = await this.api.listFigures();
80
+ this.figures = (result.ok && Array.isArray(result.data.figures)) ? result.data.figures : [];
81
+ this.render();
82
+ }
83
+
84
+ async createFigure() {
85
+ const task = window.PlexoraStatus?.begin("Creating figure");
86
+ const result = await this.api.createFigure("");
87
+ if (!result.ok) {
88
+ task?.fail("Could not create a figure");
89
+ return;
90
+ }
91
+ task?.done();
92
+ window.location.href = this.api.figureHref(result.data.figure_id);
93
+ }
94
+
95
+ onCardClick(event) {
96
+ const action = event.target.closest("[data-fb-action]");
97
+ if (!action) return;
98
+ const figureId = action.dataset.figureId;
99
+ if (action.dataset.fbAction === "duplicate") {
100
+ event.preventDefault();
101
+ this.duplicate(figureId);
102
+ } else if (action.dataset.fbAction === "rename") {
103
+ event.preventDefault();
104
+ this.rename(figureId);
105
+ }
106
+ // "delete" is left alone: the anchor carries bootstrap's data API
107
+ // attributes and opening the dialog is the whole of its job here.
108
+ }
109
+
110
+ async duplicate(figureId) {
111
+ const task = window.PlexoraStatus?.begin("Duplicating");
112
+ const result = await this.api.duplicateFigure(figureId, "");
113
+ if (!result.ok) {
114
+ task?.fail("Could not duplicate");
115
+ return;
116
+ }
117
+ task?.done();
118
+ this.refresh();
119
+ }
120
+
121
+ async rename(figureId) {
122
+ const figure = this.figures.find((f) => f.figure_id === figureId);
123
+ const title = window.prompt("Rename figure", figure ? figure.title : "");
124
+ if (title === null) return;
125
+ const trimmed = title.trim();
126
+ if (!trimmed) return;
127
+
128
+ const task = window.PlexoraStatus?.begin("Renaming");
129
+ // Renaming is an ordinary edit, so it goes through the same revision
130
+ // check every other edit does -- a figure open in another tab must not
131
+ // have its title changed out from under a save it is about to make.
132
+ const current = await this.api.getFigure(figureId);
133
+ if (!current.ok) {
134
+ task?.fail("Could not rename");
135
+ return;
136
+ }
137
+ const result = await this.api.patchFigure(figureId, current.data.document.revision,
138
+ [{ op: "set_meta", changes: { title: trimmed } }]);
139
+ if (!result.ok) {
140
+ task?.fail(result.status === 409 ? "Changed in another session" : "Could not rename");
141
+ return;
142
+ }
143
+ task?.done();
144
+ this.refresh();
145
+ }
146
+
147
+ async confirmDelete() {
148
+ const figureId = this.pendingDelete;
149
+ this.pendingDelete = null;
150
+ if (!figureId) return;
151
+ const task = window.PlexoraStatus?.begin("Deleting");
152
+ const result = await this.api.deleteFigure(figureId);
153
+ if (!result.ok) {
154
+ task?.fail("Delete failed");
155
+ return;
156
+ }
157
+ task?.done();
158
+ this.figures = this.figures.filter((f) => f.figure_id !== figureId);
159
+ this.render();
160
+ }
161
+
162
+ filtered() {
163
+ const query = this.query.trim().toLowerCase();
164
+ if (!query) return this.figures.slice();
165
+ return this.figures.filter((figure) =>
166
+ figure.title.toLowerCase().includes(query)
167
+ || (figure.sources || []).some((name) => String(name).toLowerCase().includes(query)));
168
+ }
169
+
170
+ render() {
171
+ const total = this.figures.length;
172
+ if (this.countEl) {
173
+ this.countEl.textContent = FigureSchema.countPhrase(total, "figure");
174
+ }
175
+ if (total === 0) {
176
+ this.resultsEl.innerHTML = "";
177
+ if (this.emptyEl) this.emptyEl.hidden = false;
178
+ if (this.noResultsEl) this.noResultsEl.hidden = true;
179
+ return;
180
+ }
181
+ if (this.emptyEl) this.emptyEl.hidden = true;
182
+
183
+ const list = this.filtered();
184
+ if (!list.length) {
185
+ this.resultsEl.innerHTML = "";
186
+ if (this.noResultsEl) this.noResultsEl.hidden = false;
187
+ return;
188
+ }
189
+ if (this.noResultsEl) this.noResultsEl.hidden = true;
190
+ this.resultsEl.innerHTML = list.map((figure) => this.cardMarkup(figure)).join("");
191
+ }
192
+
193
+ cardMarkup(figure) {
194
+ const escape = FigureSchema.escapeHtml.bind(FigureSchema);
195
+ const title = escape(figure.title);
196
+ if (!figure.readable) {
197
+ return `<div class="project-card fb-card fb-card-damaged">
198
+ <span class="project-thumb fb-thumb project-thumb-fallback">
199
+ <span class="fas fa-triangle-exclamation project-thumb-icon"></span>
200
+ </span>
201
+ <span class="project-card-name">${escape(figure.figure_id)}</span>
202
+ <span class="project-card-date">This figure could not be read</span>
203
+ </div>`;
204
+ }
205
+
206
+ const href = this.api.figureHref(figure.figure_id);
207
+ const summary = [
208
+ FigureSchema.countPhrase(figure.panel_count, "panel"),
209
+ FigureSchema.countPhrase(figure.page_count, "page"),
210
+ ].join(" · ");
211
+ const when = FigureSchema.timeAgo(figure.updated_at);
212
+ const sources = (figure.sources || []).slice(0, 3).map(escape).join(", ");
213
+
214
+ return `<div class="project-card fb-card">
215
+ <a class="project-card-link" href="${href}" title="${title}">
216
+ <span class="project-thumb fb-thumb">
217
+ ${figure.has_thumbnail
218
+ ? `<img src="${this.api.thumbnailUrl(figure.figure_id, figure.revision)}" alt="" loading="lazy">`
219
+ : `<span class="fas fa-image project-thumb-icon"></span>`}
220
+ </span>
221
+ <span class="project-card-name">${title}</span>
222
+ <span class="project-card-date">${escape(summary)}${when ? " · " + escape(when) : ""}</span>
223
+ ${sources ? `<span class="fb-card-sources">${sources}</span>` : ""}
224
+ </a>
225
+ <span class="project-actions">
226
+ <a href="#" class="project-action" title="Rename"
227
+ data-fb-action="rename" data-figure-id="${escape(figure.figure_id)}">
228
+ <span class="fas fa-pencil"></span></a>
229
+ <a href="#" class="project-action" title="Duplicate"
230
+ data-fb-action="duplicate" data-figure-id="${escape(figure.figure_id)}">
231
+ <span class="fas fa-copy"></span></a>
232
+ <a href="#" class="project-action project-action-danger" title="Delete"
233
+ data-fb-action="delete" data-bs-toggle="modal" data-bs-target="#fb_delete_modal"
234
+ data-figure-id="${escape(figure.figure_id)}">
235
+ <span class="fas fa-trash"></span></a>
236
+ </span>
237
+ </div>`;
238
+ }
239
+ }
240
+
241
+ if (typeof document !== "undefined" && document.addEventListener) {
242
+ if (document.readyState === "loading") {
243
+ document.addEventListener("DOMContentLoaded", () => FigureLibrary.boot());
244
+ } else {
245
+ FigureLibrary.boot();
246
+ }
247
+ }