jupyterlab_advanced_image_viewer_extension 1.1.2 → 1.1.4
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.
- package/README.md +2 -2
- package/lib/controller.js +4 -0
- package/lib/index.js +39 -15
- package/package.json +32 -95
- package/src/__tests__/controller.spec.ts +17 -0
- package/src/controller.ts +4 -0
- package/src/index.ts +43 -19
package/README.md
CHANGED
|
@@ -20,14 +20,14 @@ The viewer toolbar adds zoom in / out / fit controls and a `help` link that list
|
|
|
20
20
|
- **Wheel zoom** - scroll to zoom in and out, anchored at the cursor; zoom out as far as you like
|
|
21
21
|
- **Drag to pan** - hold and drag to move the image at any zoom; the image can touch a viewport border but never leave it, the grab cursor is always available, and Fit recenters
|
|
22
22
|
- **Toolbar controls** - zoom in, zoom out, and reset-to-fit buttons on the viewer toolbar
|
|
23
|
-
- **Arrow-key navigation** - Left and Right open the previous and next image in the same folder, advancing within one viewer instead of opening new tabs
|
|
23
|
+
- **Arrow-key navigation** - Left and Right open the previous and next image in the same folder, in the order the file browser lists them, advancing within one viewer instead of opening new tabs
|
|
24
24
|
- **Copy to clipboard** - right-click a raster image (PNG/JPG/…) and choose "Copy to Clipboard" to copy it as PNG to the system clipboard
|
|
25
25
|
- **Refresh** - a toolbar refresh button reloads the image from disk, the same control the HTML viewer provides
|
|
26
26
|
- **WebP support** - `.webp` files open in the image viewer with every feature above; stock JupyterLab opens them in the text editor
|
|
27
27
|
|
|
28
28
|
## Requirements
|
|
29
29
|
|
|
30
|
-
- JupyterLab >= 4.
|
|
30
|
+
- JupyterLab >= 4.6
|
|
31
31
|
|
|
32
32
|
## Install
|
|
33
33
|
|
package/lib/controller.js
CHANGED
|
@@ -53,7 +53,11 @@ export class ViewerController {
|
|
|
53
53
|
this.zoomAt(factor, e.clientX - r.left, e.clientY - r.top);
|
|
54
54
|
};
|
|
55
55
|
this.onDown = (e) => {
|
|
56
|
+
// preventDefault stops the browser dragging the image out, and with it the
|
|
57
|
+
// focus a click would give the viewer; focus it here, or after a toolbar
|
|
58
|
+
// click the stock keys and Left/Right go to the button instead.
|
|
56
59
|
e.preventDefault();
|
|
60
|
+
this.host.focus();
|
|
57
61
|
this.panning = true;
|
|
58
62
|
this.start = { x: e.clientX, y: e.clientY, tx: this.tx, ty: this.ty };
|
|
59
63
|
this.host.style.cursor = 'grabbing';
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Dialog, showDialog, Toolbar, ToolbarButton } from '@jupyterlab/apputils';
|
|
2
2
|
import { PathExt } from '@jupyterlab/coreutils';
|
|
3
|
+
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
3
4
|
import { IImageTracker } from '@jupyterlab/imageviewer';
|
|
4
5
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
5
6
|
import { refreshIcon } from '@jupyterlab/ui-components';
|
|
@@ -35,8 +36,8 @@ const plugin = {
|
|
|
35
36
|
description: 'Advanced image viewer: cursor-anchored wheel zoom, drag-to-pan, fit-to-screen reset, and arrow-key folder navigation.',
|
|
36
37
|
autoStart: true,
|
|
37
38
|
requires: [IImageTracker],
|
|
38
|
-
optional: [ISettingRegistry],
|
|
39
|
-
activate: (app, tracker, settingRegistry) => {
|
|
39
|
+
optional: [ISettingRegistry, IDefaultFileBrowser],
|
|
40
|
+
activate: (app, tracker, settingRegistry, fileBrowser) => {
|
|
40
41
|
console.log('JupyterLab extension jupyterlab_advanced_image_viewer_extension is activated!');
|
|
41
42
|
const controllers = new WeakMap();
|
|
42
43
|
const state = { ...defaults };
|
|
@@ -97,6 +98,7 @@ const plugin = {
|
|
|
97
98
|
widget.toolbar.addItem('advanced-refresh', new ToolbarButton({
|
|
98
99
|
icon: refreshIcon,
|
|
99
100
|
tooltip: 'Reload image from disk',
|
|
101
|
+
noFocusOnClick: true,
|
|
100
102
|
onClick: async () => {
|
|
101
103
|
await widget.context.revert();
|
|
102
104
|
widget.update();
|
|
@@ -105,16 +107,19 @@ const plugin = {
|
|
|
105
107
|
widget.toolbar.addItem('advanced-zoom-out', new ToolbarButton({
|
|
106
108
|
label: '-',
|
|
107
109
|
tooltip: 'Zoom out',
|
|
110
|
+
noFocusOnClick: true,
|
|
108
111
|
onClick: () => app.commands.execute(CommandIDs.zoomOut)
|
|
109
112
|
}));
|
|
110
113
|
widget.toolbar.addItem('advanced-zoom-in', new ToolbarButton({
|
|
111
114
|
label: '+',
|
|
112
115
|
tooltip: 'Zoom in',
|
|
116
|
+
noFocusOnClick: true,
|
|
113
117
|
onClick: () => app.commands.execute(CommandIDs.zoomIn)
|
|
114
118
|
}));
|
|
115
119
|
widget.toolbar.addItem('advanced-reset-fit', new ToolbarButton({
|
|
116
120
|
label: 'Fit',
|
|
117
121
|
tooltip: 'Reset to fit',
|
|
122
|
+
noFocusOnClick: true,
|
|
118
123
|
onClick: () => app.commands.execute(CommandIDs.resetFit)
|
|
119
124
|
}));
|
|
120
125
|
widget.toolbar.addItem('advanced-spacer', Toolbar.createSpacerItem());
|
|
@@ -218,20 +223,39 @@ const plugin = {
|
|
|
218
223
|
}
|
|
219
224
|
const path = widget.context.path;
|
|
220
225
|
const dirPath = PathExt.dirname(path);
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
const isImage = (item) => item.type === 'file' &&
|
|
227
|
+
IMAGE_EXTS.has(PathExt.extname(item.name).toLowerCase());
|
|
228
|
+
// Step through the images in the order the file browser shows them
|
|
229
|
+
// when it lists this folder, so its sort key, its direction and any
|
|
230
|
+
// sort plugin decide what comes next: a C-locale sort puts '.' before
|
|
231
|
+
// '_' where localeCompare puts '_' first, and a re-sort here skipped
|
|
232
|
+
// files. FileBrowser keeps `listing` protected, but it is the only
|
|
233
|
+
// source of the displayed order. The listing re-sorts on a refresh only
|
|
234
|
+
// while it is visible, so a hidden one is re-sorted here; re-sorting a
|
|
235
|
+
// visible one would re-render every row on each step. Otherwise fall
|
|
236
|
+
// back to natural name order, the stock file browser default.
|
|
237
|
+
let images = [];
|
|
238
|
+
if (fileBrowser && fileBrowser.model.path === dirPath) {
|
|
239
|
+
const shown = fileBrowser
|
|
240
|
+
.listing;
|
|
241
|
+
if (!shown.isVisible) {
|
|
242
|
+
shown.sort(shown.sortState);
|
|
243
|
+
}
|
|
244
|
+
images = Array.from(shown.sortedItems()).filter(isImage);
|
|
245
|
+
}
|
|
246
|
+
if (!images.some(item => item.path === path)) {
|
|
247
|
+
const listing = await app.serviceManager.contents.get(dirPath, {
|
|
248
|
+
content: true
|
|
249
|
+
});
|
|
250
|
+
const content = listing.content;
|
|
251
|
+
if (!content) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
images = content.filter(isImage).sort((a, b) => a.name.localeCompare(b.name, undefined, {
|
|
255
|
+
numeric: true,
|
|
256
|
+
sensitivity: 'base'
|
|
257
|
+
}));
|
|
227
258
|
}
|
|
228
|
-
const images = content
|
|
229
|
-
.filter(item => item.type === 'file' &&
|
|
230
|
-
IMAGE_EXTS.has(PathExt.extname(item.name).toLowerCase()))
|
|
231
|
-
.sort((a, b) => a.name.localeCompare(b.name, undefined, {
|
|
232
|
-
numeric: true,
|
|
233
|
-
sensitivity: 'base'
|
|
234
|
-
}));
|
|
235
259
|
if (images.length <= 1) {
|
|
236
260
|
return;
|
|
237
261
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_advanced_image_viewer_extension",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "JupyterLab extension for advanced image viewing: fit-to-screen, cursor-anchored wheel zoom, drag-to-pan, and Left/Right arrow keys to move to the previous and next image in a folder",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "jlpm build:lib && jlpm build:labextension:dev",
|
|
33
33
|
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
|
|
34
|
-
"build:labextension": "jupyter
|
|
35
|
-
"build:labextension:dev": "jupyter
|
|
34
|
+
"build:labextension": "jupyter-builder build .",
|
|
35
|
+
"build:labextension:dev": "jupyter-builder build --development True .",
|
|
36
36
|
"build:lib": "tsc --sourceMap",
|
|
37
37
|
"build:lib:prod": "tsc",
|
|
38
38
|
"clean": "jlpm clean:lib",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"clean:labextension": "rimraf jupyterlab_advanced_image_viewer_extension/labextension jupyterlab_advanced_image_viewer_extension/_version.py",
|
|
42
42
|
"clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
|
|
43
43
|
"eslint": "jlpm eslint:check --fix",
|
|
44
|
-
"eslint:check": "eslint . --cache
|
|
44
|
+
"eslint:check": "eslint . --cache",
|
|
45
45
|
"install:extension": "jlpm build",
|
|
46
46
|
"lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
|
|
47
47
|
"lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
|
|
@@ -53,46 +53,46 @@
|
|
|
53
53
|
"test": "jest --coverage",
|
|
54
54
|
"watch": "run-p watch:src watch:labextension",
|
|
55
55
|
"watch:src": "tsc -w --sourceMap",
|
|
56
|
-
"watch:labextension": "jupyter
|
|
56
|
+
"watch:labextension": "jupyter-builder watch ."
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@jupyterlab/application": "^4.
|
|
60
|
-
"@jupyterlab/apputils": "^4.
|
|
61
|
-
"@jupyterlab/coreutils": "^6.
|
|
62
|
-
"@jupyterlab/docregistry": "^4.
|
|
63
|
-
"@jupyterlab/
|
|
64
|
-
"@jupyterlab/
|
|
65
|
-
"@jupyterlab/
|
|
66
|
-
"@
|
|
67
|
-
"@lumino/
|
|
59
|
+
"@jupyterlab/application": "^4.6.0",
|
|
60
|
+
"@jupyterlab/apputils": "^4.7.0",
|
|
61
|
+
"@jupyterlab/coreutils": "^6.6.0",
|
|
62
|
+
"@jupyterlab/docregistry": "^4.6.0",
|
|
63
|
+
"@jupyterlab/filebrowser": "^4.6.0",
|
|
64
|
+
"@jupyterlab/imageviewer": "^4.6.0",
|
|
65
|
+
"@jupyterlab/settingregistry": "^4.6.0",
|
|
66
|
+
"@jupyterlab/ui-components": "^4.6.0",
|
|
67
|
+
"@lumino/disposable": "^2.1.0",
|
|
68
|
+
"@lumino/widgets": "^2.9.0"
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
70
|
-
"@
|
|
71
|
-
"@
|
|
72
|
-
"@
|
|
71
|
+
"@eslint/js": "^9.39.0",
|
|
72
|
+
"@jupyter/builder": "^1.2.0",
|
|
73
|
+
"@jupyter/eslint-plugin": "^1.3.0",
|
|
74
|
+
"@jupyterlab/testutils": "^4.6.0",
|
|
75
|
+
"@types/jest": "^29.5.0",
|
|
73
76
|
"@types/json-schema": "^7.0.11",
|
|
74
|
-
"@types/react": "^18.0
|
|
77
|
+
"@types/react": "^18.3.0",
|
|
75
78
|
"@types/react-addons-linked-state-mixin": "^0.14.22",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"eslint-plugin-prettier": "^5.0.0",
|
|
82
|
-
"jest": "^29.2.0",
|
|
79
|
+
"eslint": "^9.39.0",
|
|
80
|
+
"eslint-config-prettier": "^9.1.0",
|
|
81
|
+
"eslint-plugin-prettier": "^5.5.0",
|
|
82
|
+
"globals": "^15.15.0",
|
|
83
|
+
"jest": "^29.7.0",
|
|
83
84
|
"mkdirp": "^1.0.3",
|
|
84
85
|
"npm-run-all2": "^7.0.1",
|
|
85
|
-
"prettier": "^3.
|
|
86
|
+
"prettier": "^3.9.0",
|
|
86
87
|
"rimraf": "^5.0.1",
|
|
87
|
-
"
|
|
88
|
-
"style-loader": "^3.3.1",
|
|
89
|
-
"stylelint": "^15.10.1",
|
|
88
|
+
"stylelint": "^15.11.0",
|
|
90
89
|
"stylelint-config-recommended": "^13.0.0",
|
|
91
90
|
"stylelint-config-standard": "^34.0.0",
|
|
92
|
-
"stylelint-csstree-validator": "^3.
|
|
93
|
-
"stylelint-prettier": "^4.
|
|
91
|
+
"stylelint-csstree-validator": "^3.1.0",
|
|
92
|
+
"stylelint-prettier": "^4.1.0",
|
|
94
93
|
"typescript": "~5.8.0",
|
|
95
|
-
"
|
|
94
|
+
"typescript-eslint": "^8.71.0",
|
|
95
|
+
"yjs": "^13.6.0"
|
|
96
96
|
},
|
|
97
97
|
"resolutions": {
|
|
98
98
|
"lib0": "0.2.111",
|
|
@@ -117,69 +117,6 @@
|
|
|
117
117
|
"outputDir": "jupyterlab_advanced_image_viewer_extension/labextension",
|
|
118
118
|
"schemaDir": "schema"
|
|
119
119
|
},
|
|
120
|
-
"eslintIgnore": [
|
|
121
|
-
"node_modules",
|
|
122
|
-
"dist",
|
|
123
|
-
"coverage",
|
|
124
|
-
"**/*.d.ts",
|
|
125
|
-
"tests",
|
|
126
|
-
"**/__tests__",
|
|
127
|
-
"ui-tests"
|
|
128
|
-
],
|
|
129
|
-
"eslintConfig": {
|
|
130
|
-
"extends": [
|
|
131
|
-
"eslint:recommended",
|
|
132
|
-
"plugin:@typescript-eslint/eslint-recommended",
|
|
133
|
-
"plugin:@typescript-eslint/recommended",
|
|
134
|
-
"plugin:prettier/recommended"
|
|
135
|
-
],
|
|
136
|
-
"parser": "@typescript-eslint/parser",
|
|
137
|
-
"parserOptions": {
|
|
138
|
-
"project": "tsconfig.json",
|
|
139
|
-
"sourceType": "module"
|
|
140
|
-
},
|
|
141
|
-
"plugins": [
|
|
142
|
-
"@typescript-eslint"
|
|
143
|
-
],
|
|
144
|
-
"rules": {
|
|
145
|
-
"@typescript-eslint/naming-convention": [
|
|
146
|
-
"error",
|
|
147
|
-
{
|
|
148
|
-
"selector": "interface",
|
|
149
|
-
"format": [
|
|
150
|
-
"PascalCase"
|
|
151
|
-
],
|
|
152
|
-
"custom": {
|
|
153
|
-
"regex": "^I[A-Z]",
|
|
154
|
-
"match": true
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
],
|
|
158
|
-
"@typescript-eslint/no-unused-vars": [
|
|
159
|
-
"warn",
|
|
160
|
-
{
|
|
161
|
-
"args": "none"
|
|
162
|
-
}
|
|
163
|
-
],
|
|
164
|
-
"@typescript-eslint/no-explicit-any": "off",
|
|
165
|
-
"@typescript-eslint/no-namespace": "off",
|
|
166
|
-
"@typescript-eslint/no-use-before-define": "off",
|
|
167
|
-
"@typescript-eslint/quotes": [
|
|
168
|
-
"error",
|
|
169
|
-
"single",
|
|
170
|
-
{
|
|
171
|
-
"avoidEscape": true,
|
|
172
|
-
"allowTemplateLiterals": false
|
|
173
|
-
}
|
|
174
|
-
],
|
|
175
|
-
"curly": [
|
|
176
|
-
"error",
|
|
177
|
-
"all"
|
|
178
|
-
],
|
|
179
|
-
"eqeqeq": "error",
|
|
180
|
-
"prefer-arrow-callback": "error"
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
120
|
"prettier": {
|
|
184
121
|
"singleQuote": true,
|
|
185
122
|
"trailingComma": "none",
|
|
@@ -297,6 +297,23 @@ describe('ViewerController', () => {
|
|
|
297
297
|
c.dispose();
|
|
298
298
|
});
|
|
299
299
|
|
|
300
|
+
// DEF-KEYS-2: the mousedown handler cancels the browser's image drag, which
|
|
301
|
+
// also cancels the focus a click gives; the viewer must take focus itself.
|
|
302
|
+
it('takes keyboard focus from a toolbar button when the image is pressed', () => {
|
|
303
|
+
host.tabIndex = 0;
|
|
304
|
+
const button = document.createElement('button');
|
|
305
|
+
document.body.appendChild(button);
|
|
306
|
+
new ViewerController(host, img, 0.1);
|
|
307
|
+
button.focus();
|
|
308
|
+
expect(document.activeElement).toBe(button);
|
|
309
|
+
host.dispatchEvent(
|
|
310
|
+
new MouseEvent('mousedown', { clientX: 0, clientY: 0, cancelable: true })
|
|
311
|
+
);
|
|
312
|
+
window.dispatchEvent(new MouseEvent('mouseup'));
|
|
313
|
+
expect(document.activeElement).toBe(host);
|
|
314
|
+
button.remove();
|
|
315
|
+
});
|
|
316
|
+
|
|
300
317
|
it('stops observing the host and the image on dispose', () => {
|
|
301
318
|
stubGeometry(80, 60);
|
|
302
319
|
const c = new ViewerController(host, img, 0.1);
|
package/src/controller.ts
CHANGED
|
@@ -147,7 +147,11 @@ export class ViewerController {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
private onDown = (e: MouseEvent): void => {
|
|
150
|
+
// preventDefault stops the browser dragging the image out, and with it the
|
|
151
|
+
// focus a click would give the viewer; focus it here, or after a toolbar
|
|
152
|
+
// click the stock keys and Left/Right go to the button instead.
|
|
150
153
|
e.preventDefault();
|
|
154
|
+
this.host.focus();
|
|
151
155
|
this.panning = true;
|
|
152
156
|
this.start = { x: e.clientX, y: e.clientY, tx: this.tx, ty: this.ty };
|
|
153
157
|
this.host.style.cursor = 'grabbing';
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { PathExt } from '@jupyterlab/coreutils';
|
|
|
14
14
|
|
|
15
15
|
import { IDocumentWidget } from '@jupyterlab/docregistry';
|
|
16
16
|
|
|
17
|
+
import { DirListing, IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
18
|
+
|
|
17
19
|
import { IImageTracker, ImageViewer } from '@jupyterlab/imageviewer';
|
|
18
20
|
|
|
19
21
|
import { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
@@ -67,11 +69,12 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
67
69
|
'Advanced image viewer: cursor-anchored wheel zoom, drag-to-pan, fit-to-screen reset, and arrow-key folder navigation.',
|
|
68
70
|
autoStart: true,
|
|
69
71
|
requires: [IImageTracker],
|
|
70
|
-
optional: [ISettingRegistry],
|
|
72
|
+
optional: [ISettingRegistry, IDefaultFileBrowser],
|
|
71
73
|
activate: (
|
|
72
74
|
app: JupyterFrontEnd,
|
|
73
75
|
tracker: IImageTracker,
|
|
74
|
-
settingRegistry: ISettingRegistry | null
|
|
76
|
+
settingRegistry: ISettingRegistry | null,
|
|
77
|
+
fileBrowser: IDefaultFileBrowser | null
|
|
75
78
|
): void => {
|
|
76
79
|
console.log(
|
|
77
80
|
'JupyterLab extension jupyterlab_advanced_image_viewer_extension is activated!'
|
|
@@ -144,6 +147,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
144
147
|
new ToolbarButton({
|
|
145
148
|
icon: refreshIcon,
|
|
146
149
|
tooltip: 'Reload image from disk',
|
|
150
|
+
noFocusOnClick: true,
|
|
147
151
|
onClick: async () => {
|
|
148
152
|
await widget.context.revert();
|
|
149
153
|
widget.update();
|
|
@@ -155,6 +159,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
155
159
|
new ToolbarButton({
|
|
156
160
|
label: '-',
|
|
157
161
|
tooltip: 'Zoom out',
|
|
162
|
+
noFocusOnClick: true,
|
|
158
163
|
onClick: () => app.commands.execute(CommandIDs.zoomOut)
|
|
159
164
|
})
|
|
160
165
|
);
|
|
@@ -163,6 +168,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
163
168
|
new ToolbarButton({
|
|
164
169
|
label: '+',
|
|
165
170
|
tooltip: 'Zoom in',
|
|
171
|
+
noFocusOnClick: true,
|
|
166
172
|
onClick: () => app.commands.execute(CommandIDs.zoomIn)
|
|
167
173
|
})
|
|
168
174
|
);
|
|
@@ -171,6 +177,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
171
177
|
new ToolbarButton({
|
|
172
178
|
label: 'Fit',
|
|
173
179
|
tooltip: 'Reset to fit',
|
|
180
|
+
noFocusOnClick: true,
|
|
174
181
|
onClick: () => app.commands.execute(CommandIDs.resetFit)
|
|
175
182
|
})
|
|
176
183
|
);
|
|
@@ -285,29 +292,46 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
285
292
|
}
|
|
286
293
|
const path = widget.context.path;
|
|
287
294
|
const dirPath = PathExt.dirname(path);
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
const isImage = (item: { name: string; type: string }): boolean =>
|
|
296
|
+
item.type === 'file' &&
|
|
297
|
+
IMAGE_EXTS.has(PathExt.extname(item.name).toLowerCase());
|
|
298
|
+
// Step through the images in the order the file browser shows them
|
|
299
|
+
// when it lists this folder, so its sort key, its direction and any
|
|
300
|
+
// sort plugin decide what comes next: a C-locale sort puts '.' before
|
|
301
|
+
// '_' where localeCompare puts '_' first, and a re-sort here skipped
|
|
302
|
+
// files. FileBrowser keeps `listing` protected, but it is the only
|
|
303
|
+
// source of the displayed order. The listing re-sorts on a refresh only
|
|
304
|
+
// while it is visible, so a hidden one is re-sorted here; re-sorting a
|
|
305
|
+
// visible one would re-render every row on each step. Otherwise fall
|
|
306
|
+
// back to natural name order, the stock file browser default.
|
|
307
|
+
let images: Array<{ name: string; path: string; type: string }> = [];
|
|
308
|
+
if (fileBrowser && fileBrowser.model.path === dirPath) {
|
|
309
|
+
const shown = (fileBrowser as unknown as { listing: DirListing })
|
|
310
|
+
.listing;
|
|
311
|
+
if (!shown.isVisible) {
|
|
312
|
+
shown.sort(shown.sortState);
|
|
313
|
+
}
|
|
314
|
+
images = Array.from(shown.sortedItems()).filter(isImage);
|
|
298
315
|
}
|
|
299
|
-
|
|
300
|
-
.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
316
|
+
if (!images.some(item => item.path === path)) {
|
|
317
|
+
const listing = await app.serviceManager.contents.get(dirPath, {
|
|
318
|
+
content: true
|
|
319
|
+
});
|
|
320
|
+
const content = listing.content as Array<{
|
|
321
|
+
name: string;
|
|
322
|
+
path: string;
|
|
323
|
+
type: string;
|
|
324
|
+
}> | null;
|
|
325
|
+
if (!content) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
images = content.filter(isImage).sort((a, b) =>
|
|
306
329
|
a.name.localeCompare(b.name, undefined, {
|
|
307
330
|
numeric: true,
|
|
308
331
|
sensitivity: 'base'
|
|
309
332
|
})
|
|
310
333
|
);
|
|
334
|
+
}
|
|
311
335
|
if (images.length <= 1) {
|
|
312
336
|
return;
|
|
313
337
|
}
|